From 654f932b3c9c9d0b2638fae18f0004e70dff4866 Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Sun, 5 Mar 2017 10:05:22 +0100 Subject: [PATCH] [FIX] account: rewrite of the reconciliation feature With our new views, we broke almost all JS code, including the reconciliation widget, which was (and still is) dependant on the field widget. With this commit, we rewrite and simplify the reconciliation code. We also add some tests, to help with the maintenance. Work done by CHM --- .../account/models/account_bank_statement.py | 36 +- addons/account/models/account_move.py | 84 +- .../static/src/js/account_payment_widget.js | 63 +- .../src/js/account_reconciliation_widgets.js | 2626 ----------------- .../static/src/js/move_line_quickadd.js | 4 +- .../reconciliation/reconciliation_action.js | 285 ++ .../js/reconciliation/reconciliation_model.js | 1068 +++++++ .../reconciliation/reconciliation_renderer.js | 631 ++++ .../src/less/account_journal_dashboard.less | 145 +- .../src/less/account_reconciliation.less | 796 ++--- .../static/src/xml/account_reconciliation.xml | 563 ++-- .../static/test/account_reconciliation.js | 914 ++++++ addons/account/views/account.xml | 35 +- addons/account/views/account_invoice_view.xml | 5 +- .../views/account_journal_dashboard_view.xml | 4 +- 15 files changed, 3560 insertions(+), 3699 deletions(-) delete mode 100644 addons/account/static/src/js/account_reconciliation_widgets.js create mode 100644 addons/account/static/src/js/reconciliation/reconciliation_action.js create mode 100644 addons/account/static/src/js/reconciliation/reconciliation_model.js create mode 100644 addons/account/static/src/js/reconciliation/reconciliation_renderer.js create mode 100644 addons/account/static/test/account_reconciliation.js diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index 0915e4c9f90..3b9267deb9c 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -518,6 +518,7 @@ class AccountBankStatementLine(models.Model): 'partner_id': self.partner_id.id, 'journal_id': self.journal_id.id, 'statement_id': self.statement_id.id, + 'account_id': [self.journal_id.default_debit_account_id.id, self.journal_id.default_debit_account_id.display_name], 'account_code': self.journal_id.default_debit_account_id.code, 'account_name': self.journal_id.default_debit_account_id.name, 'partner_name': self.partner_id.name, @@ -534,10 +535,10 @@ class AccountBankStatementLine(models.Model): return data @api.multi - def get_move_lines_for_reconciliation_widget(self, excluded_ids=None, str=False, offset=0, limit=None): + def get_move_lines_for_reconciliation_widget(self, partner_id=None, excluded_ids=None, str=False, offset=0, limit=None): """ Returns move lines for the bank statement reconciliation widget, formatted as a list of dicts """ - aml_recs = self.get_move_lines_for_reconciliation(excluded_ids=excluded_ids, str=str, offset=offset, limit=limit) + aml_recs = self.get_move_lines_for_reconciliation(partner_id=partner_id, excluded_ids=excluded_ids, str=str, offset=offset, limit=limit) target_currency = self.currency_id or self.journal_id.currency_id or self.journal_id.company_id.currency_id return aml_recs.prepare_move_lines_for_reconciliation_widget(target_currency=target_currency, target_date=self.date) @@ -545,9 +546,10 @@ class AccountBankStatementLine(models.Model): # Reconciliation methods #################################################### - def get_move_lines_for_reconciliation(self, excluded_ids=None, str=False, offset=0, limit=None, additional_domain=None, overlook_partner=False): + def get_move_lines_for_reconciliation(self, partner_id=None, excluded_ids=None, str=False, offset=0, limit=None, additional_domain=None, overlook_partner=False): """ Return account.move.line records which can be used for bank statement reconciliation. + :param partner_id: :param excluded_ids: :param str: :param offset: @@ -555,13 +557,16 @@ class AccountBankStatementLine(models.Model): :param additional_domain: :param overlook_partner: """ + if partner_id is None: + partner_id = self.partner_id.id + # Blue lines = payment on bank account not assigned to a statement yet reconciliation_aml_accounts = [self.journal_id.default_credit_account_id.id, self.journal_id.default_debit_account_id.id] domain_reconciliation = ['&', '&', ('statement_line_id', '=', False), ('account_id', 'in', reconciliation_aml_accounts), ('payment_id','<>', False)] # Black lines = unreconciled & (not linked to a payment or open balance created by statement domain_matching = [('reconciled', '=', False)] - if self.partner_id.id or overlook_partner: + if partner_id or overlook_partner: domain_matching = expression.AND([domain_matching, [('account_id.internal_type', 'in', ['payable', 'receivable'])]]) else: # TODO : find out what use case this permits (match a check payment, registered on a journal whose account type is other instead of liquidity) @@ -569,14 +574,17 @@ class AccountBankStatementLine(models.Model): # Let's add what applies to both domain = expression.OR([domain_reconciliation, domain_matching]) - if self.partner_id.id and not overlook_partner: - domain = expression.AND([domain, [('partner_id', '=', self.partner_id.id)]]) + if partner_id and not overlook_partner: + domain = expression.AND([domain, [('partner_id', '=', partner_id)]]) # Domain factorized for all reconciliation use cases - ctx = dict(self._context or {}) - ctx['bank_statement_line'] = self - generic_domain = self.env['account.move.line'].with_context(ctx).domain_move_lines_for_reconciliation(excluded_ids=excluded_ids, str=str) - domain = expression.AND([domain, generic_domain]) + if str: + str_domain = self.env['account.move.line'].domain_move_lines_for_reconciliation(str=str) + if not partner_id: + str_domain = expression.OR([str_domain, ('partner_id.name', 'ilike', str)]) + domain = expression.AND([domain, str_domain]) + if excluded_ids: + domain = expression.AND([[('id', 'not in', excluded_ids)], domain]) # Domain from caller if additional_domain is None: @@ -585,7 +593,7 @@ class AccountBankStatementLine(models.Model): additional_domain = expression.normalize_domain(additional_domain) domain = expression.AND([domain, additional_domain]) - return self.env['account.move.line'].search(domain, offset=offset, limit=limit, order="date_maturity asc, id asc") + return self.env['account.move.line'].search(domain, offset=offset, limit=limit, order="date_maturity desc, id desc") def _get_common_sql_query(self, overlook_partner = False, excluded_ids = None, split = False): acc_type = "acc.internal_type IN ('payable', 'receivable')" if (self.partner_id or overlook_partner) else "acc.reconcile = true" @@ -629,7 +637,7 @@ class AccountBankStatementLine(models.Model): select_clause, from_clause, where_clause = self._get_common_sql_query(overlook_partner=True, excluded_ids=excluded_ids, split=True) sql_query = select_clause + add_to_select + from_clause + add_to_from + where_clause sql_query += " AND (aml.ref= %(ref)s or m.name = %(ref)s) \ - ORDER BY temp_field_order, date_maturity asc, aml.id asc" + ORDER BY temp_field_order, date_maturity desc, aml.id desc" self.env.cr.execute(sql_query, params) results = self.env.cr.fetchone() if results: @@ -641,7 +649,7 @@ class AccountBankStatementLine(models.Model): liquidity_amt_clause = currency and '%(amount)s' or 'abs(%(amount)s)' sql_query = self._get_common_sql_query(excluded_ids=excluded_ids) + \ " AND ("+field+" = %(amount)s OR (acc.internal_type = 'liquidity' AND "+liquidity_field+" = " + liquidity_amt_clause + ")) \ - ORDER BY date_maturity asc, aml.id asc LIMIT 1" + ORDER BY date_maturity desc, aml.id desc LIMIT 1" self.env.cr.execute(sql_query, params) results = self.env.cr.fetchone() if results: @@ -805,6 +813,8 @@ class AccountBankStatementLine(models.Model): for aml_dict in datum.get('counterpart_aml_dicts', []): aml_dict['move_line'] = AccountMoveLine.browse(aml_dict['counterpart_aml_id']) del aml_dict['counterpart_aml_id'] + if datum.get('partner_id') is not None: + st_line.write({'partner_id': datum['partner_id']}) st_line.process_reconciliation(datum.get('counterpart_aml_dicts', []), payment_aml_rec, datum.get('new_aml_dicts', [])) def fast_counterpart_creation(self): diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index fe38f8a4031..4be1ac19bd7 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -596,7 +596,7 @@ class AccountMoveLine(models.Model): AND NOT a.reconciled AND NOT b.reconciled AND a.account_id = %(account_id)s AND b.account_id = %(account_id)s {partner_id_condition} - ORDER BY a.date asc + ORDER BY a.date desc LIMIT 10 """.format(**locals()) self.env.cr.execute(query, locals()) @@ -615,62 +615,50 @@ class AccountMoveLine(models.Model): return [] @api.model - def domain_move_lines_for_reconciliation(self, excluded_ids=None, str=False): - """ Returns the domain which is common to both manual and bank statement reconciliation. - - :param excluded_ids: list of ids of move lines that should not be fetched + def domain_move_lines_for_reconciliation(self, str): + """ Returns the domain from the str search :param str: search string """ - context = (self._context or {}) - if excluded_ids is None: - excluded_ids = [] - domain = [] - - if excluded_ids: - domain = expression.AND([domain, [('id', 'not in', excluded_ids)]]) - if str: - str_domain = [ - '|', ('move_id.name', 'ilike', str), - '|', ('move_id.ref', 'ilike', str), - '|', ('date_maturity', 'like', str), - '&', ('name', '!=', '/'), ('name', 'ilike', str) + if not str: + return [] + str_domain = [ + '|', ('move_id.name', 'ilike', str), + '|', ('move_id.ref', 'ilike', str), + '|', ('date_maturity', 'like', str), + '&', ('name', '!=', '/'), ('name', 'ilike', str) + ] + try: + amount = float(str) + amount_domain = [ + '|', ('amount_residual', '=', amount), + '|', ('amount_residual_currency', '=', amount), + '|', ('amount_residual', '=', -amount), + '|', ('amount_residual_currency', '=', -amount), + '&', ('account_id.internal_type', '=', 'liquidity'), + '|', '|', ('debit', '=', amount), ('credit', '=', amount), ('amount_currency', '=', amount), ] - try: - amount = float(str) - amount_domain = [ - '|', ('amount_residual', '=', amount), - '|', ('amount_residual_currency', '=', amount), - '|', ('amount_residual', '=', -amount), - '|', ('amount_residual_currency', '=', -amount), - '&', ('account_id.internal_type', '=', 'liquidity'), - '|', '|', ('debit', '=', amount), ('credit', '=', amount), ('amount_currency', '=', amount), - ] - str_domain = expression.OR([str_domain, amount_domain]) - except: - pass - - # When building a domain for the bank statement reconciliation, if there's no partner - # and a search string, search also a match in the partner names - if 'bank_statement_line' in context and not context['bank_statement_line'].partner_id.id: - str_domain = expression.OR([str_domain, [('partner_id.name', 'ilike', str)]]) - - domain = expression.AND([domain, str_domain]) - return domain + str_domain = expression.OR([str_domain, amount_domain]) + except: + pass + return str_domain def _domain_move_lines_for_manual_reconciliation(self, account_id, partner_id=False, excluded_ids=None, str=False): """ Create domain criteria that are relevant to manual reconciliation. """ domain = ['&', ('reconciled', '=', False), ('account_id', '=', account_id)] if partner_id: domain = expression.AND([domain, [('partner_id', '=', partner_id)]]) - generic_domain = self.domain_move_lines_for_reconciliation(excluded_ids=excluded_ids, str=str) - - return expression.AND([generic_domain, domain]) + if excluded_ids: + domain = expression.AND([[('id', 'not in', excluded_ids)], domain]) + if str: + str_domain = self.domain_move_lines_for_reconciliation(str=str) + domain = expression.AND([domain, str_domain]) + return domain @api.model def get_move_lines_for_manual_reconciliation(self, account_id, partner_id=False, excluded_ids=None, str=False, offset=0, limit=None, target_currency_id=False): """ Returns unreconciled move lines for an account or a partner+account, formatted for the manual reconciliation widget """ domain = self._domain_move_lines_for_manual_reconciliation(account_id, partner_id, excluded_ids, str) - lines = self.search(domain, offset=offset, limit=limit, order="date_maturity asc, id asc") + lines = self.search(domain, offset=offset, limit=limit, order="date_maturity desc, id desc") if target_currency_id: target_currency = self.env['res.currency'].browse(target_currency_id) else: @@ -700,13 +688,14 @@ class AccountMoveLine(models.Model): 'ref': line.move_id.ref or '', # For reconciliation between statement transactions and already registered payments (eg. checks) # NB : we don't use the 'reconciled' field because the line we're selecting is not the one that gets reconciled + 'account_id': [line.account_id.id, line.account_id.display_name], 'already_paid': line.account_id.internal_type == 'liquidity', 'account_code': line.account_id.code, 'account_name': line.account_id.name, 'account_type': line.account_id.internal_type, 'date_maturity': line.date_maturity, 'date': line.date, - 'journal_name': line.journal_id.name, + 'journal_id': [line.journal_id.id, line.journal_id.display_name], 'partner_id': line.partner_id.id, 'partner_name': line.partner_id.name, 'currency_id': (line.currency_id and line.amount_currency) and line.currency_id.id or False, @@ -750,8 +739,9 @@ class AccountMoveLine(models.Model): actual_credit = credit > 0 and -amount or 0.0 currency = company_currency if line_currency != target_currency: - amount_currency_str = formatLang(self.env, abs(actual_debit or actual_credit), currency_obj=line_currency) - total_amount_currency_str = formatLang(self.env, total_amount, currency_obj=line_currency) + target_currency.compute(total_amount, line_currency) + amount_currency_str = formatLang(self.env, target_currency.compute(abs(actual_debit or actual_credit), line_currency), currency_obj=line_currency) + total_amount_currency_str = formatLang(self.env, target_currency.compute(total_amount, line_currency), currency_obj=line_currency) if currency != target_currency: ctx = context.copy() ctx.update({'date': target_date or line.date}) @@ -781,7 +771,7 @@ class AccountMoveLine(models.Model): """ for datum in data: if len(datum['mv_line_ids']) >= 1 or len(datum['mv_line_ids']) + len(datum['new_mv_line_dicts']) >= 2: - self.process_reconciliation(datum['mv_line_ids'], datum['new_mv_line_dicts']) + self.browse(datum['mv_line_ids']).process_reconciliation(datum['new_mv_line_dicts']) if datum['type'] == 'partner': partners = self.env['res.partner'].browse(datum['id']) diff --git a/addons/account/static/src/js/account_payment_widget.js b/addons/account/static/src/js/account_payment_widget.js index a7f2b64c413..059d4668338 100644 --- a/addons/account/static/src/js/account_payment_widget.js +++ b/addons/account/static/src/js/account_payment_widget.js @@ -1,17 +1,19 @@ odoo.define('account.payment', function (require) { "use strict"; +var AbstractField = require('web.AbstractField'); var core = require('web.core'); -var form_common = require('web.form_common'); var formats = require('web.formats'); -var Model = require('web.Model'); +var field_registry = require('web.field_registry'); var QWeb = core.qweb; -var ShowPaymentLineWidget = form_common.AbstractField.extend({ - render_value: function() { + +var ShowPaymentLineWidget = AbstractField.extend({ + supportedFieldTypes: ['text'], + render: function() { var self = this; - var info = JSON.parse(this.get('value')); + var info = JSON.parse(this.value); var invoice_id = info.invoice_id; if (info !== false) { _.each(info.content, function(k,v){ @@ -22,23 +24,26 @@ var ShowPaymentLineWidget = form_common.AbstractField.extend({ } }); this.$el.html(QWeb.render('ShowPaymentInfo', { - 'lines': info.content, - 'outstanding': info.outstanding, + 'lines': info.content, + 'outstanding': info.outstanding, 'title': info.title })); this.$('.outstanding_credit_assign').click(function(){ var id = $(this).data('id') || false; - new Model("account.invoice") - .call("assign_outstanding_credit", [invoice_id, id]) - .then(function (result) { - self.view.reload(); - }); + self.trigger_up('perform_model_rpc', { + model: 'account.invoice', + method: 'assign_outstanding_credit', + args: [invoice_id, id], + on_success: function () { + self.trigger_up('reload'); + } + }); }); _.each(this.$('.js_payment_info'), function(k, v){ var options = { 'content': QWeb.render('PaymentPopOver', { - 'name': info.content[v].name, - 'journal_name': info.content[v].journal_name, + 'name': info.content[v].name, + 'journal_name': info.content[v].journal_name, 'date': info.content[v].date, 'amount': info.content[v].amount, 'currency': info.content[v].currency, @@ -54,20 +59,23 @@ var ShowPaymentLineWidget = form_common.AbstractField.extend({ 'delay': { "show": 0, "hide": 100 }, }; $(k).popover(options); - $(k).on('shown.bs.popover', function(event){ + $(k).on('shown.bs.popover', function(){ $(this).parent().find('.js_unreconcile_payment').click(function(){ - var payment_id = parseInt($(this).attr('payment-id')) - if (payment_id !== undefined && payment_id !== NaN){ - new Model("account.move.line") - .call("remove_move_reconcile", [payment_id, {'invoice_id': self.view.datarecord.id}]) - .then(function (result) { - self.view.reload(); - }); + var payment_id = parseInt($(this).attr('payment-id')); + if (payment_id !== undefined && !isNaN(payment_id)){ + self.trigger_up('perform_model_rpc', { + model: 'account.move.line', + method: 'remove_move_reconcile', + args: [payment_id, {'invoice_id': self.res_id}], + on_success: function () { + self.trigger_up('reload'); + } + }); } }); $(this).parent().find('.js_open_payment').click(function(){ - var move_id = parseInt($(this).attr('move-id')) - if (move_id !== undefined && move_id !== NaN){ + var move_id = parseInt($(this).attr('move-id')); + if (move_id !== undefined && !isNaN(move_id)){ //Open form view of account.move with id = move_id self.do_action({ type: 'ir.actions.act_window', @@ -85,8 +93,11 @@ var ShowPaymentLineWidget = form_common.AbstractField.extend({ this.$el.html(''); } }, + has_no_value: function() { + return false; + } }); -core.form_widget_registry.add('payment', ShowPaymentLineWidget); +field_registry.add('payment', ShowPaymentLineWidget); -}); \ No newline at end of file +}); diff --git a/addons/account/static/src/js/account_reconciliation_widgets.js b/addons/account/static/src/js/account_reconciliation_widgets.js deleted file mode 100644 index 13b09dacc74..00000000000 --- a/addons/account/static/src/js/account_reconciliation_widgets.js +++ /dev/null @@ -1,2626 +0,0 @@ -odoo.define('account.reconciliation', function (require) { -"use strict"; - -var ActionManager = require('web.ActionManager'); -var ControlPanelMixin = require('web.ControlPanelMixin'); -var core = require('web.core'); -var utils = require('web.utils'); -var CrashManager = require('web.CrashManager'); -var data = require('web.data'); -var Dialog = require('web.Dialog'); -var formats = require('web.formats'); -var FormView = require('web.FormView'); -var ListView = require('web.ListView'); -var Model = require('web.Model'); -var pyeval = require('web.pyeval'); -var web_client = require('web.web_client'); -var parse_value = require('web.web_client'); -var Widget = require('web.Widget'); -var session = require('web.session'); - -var FieldMany2One = core.form_widget_registry.get('many2one'); -var FieldChar = core.form_widget_registry.get('char'); -var FieldFloat = core.form_widget_registry.get('float'); - -var _t = core._t; -var QWeb = core.qweb; -var bus = core.bus; - -function defaultIfUndef(variable, defaultValue) { - return variable === undefined ? defaultValue : variable; -} - -/* - -ABOUT - -In this module, you'll find two abstract widgets : one that represent a single reconciliation (abstractReconciliationLine) -and one that serves as container and parent for the former (abstractReconciliation), displaying title, progress, etc. - -Those widgets are implemented for the bank statement reconciliation and the manual reconciliation. - -"Implementation classes" must declare a this.children_widget = implementationOfAbstractReconciliationLine -property so abstractReconciliation can instanciate new reconciliations. - -They may specify a template_prefix property (ex : methods in the abstract widget that renders templates will try -template_prefix_some_template before to try some_template) - -When a method's body in an abstract widget is left empty, it means it should be defined by implementation classes - - -TODO - -Reconciliation widgets' instanciation performances can be improved by generating the shared DOM once in the parent -then cloning it for each new child. - -*/ -var abstractReconciliation = Widget.extend(ControlPanelMixin, { - className: 'o_reconciliation', - - events: { - "click *[rel='do_action']": "doActionClickHandler", - "click .presets_config_create": "presetConfigCreateClickHandler", - "click .presets_config_edit": "presetConfigEditClickHandler", - }, - - init: function(parent, context) { - this._super(parent); - - // Only for statistical purposes - this.lines_reconciled_with_ctrl_enter = 0; - this.time_widget_loaded = Date.now(); - - this.action_manager = this.findAncestor(function(ancestor){ return ancestor instanceof ActionManager }); - this.crash_manager = new CrashManager(); - this.formatCurrencies; // Method that formats the currency ; loaded from the server - this.model_res_users = new Model("res.users"); - this.model_tax = new Model("account.tax"); - this.model_presets = new Model("account.reconcile.model"); - this.max_move_lines_displayed = 5; - // Number of reconciliations loaded initially and by clicking 'show more' - this.num_reconciliations_fetched_in_batch = 10; - this.animation_speed = 100; // "Blocking" animations - this.aestetic_animation_speed = 300; // eye candy - // We'll need to get the code of an account selected in a many2one field (which returns the id) - this.map_account_id_code = {}; - // NB : for presets to work correctly, a field id must be the same string as a preset field - this.presets = {}; - // Description of the fields to initialize in the "create new line" form - var domain_account_id = [['deprecated', '=', false]]; - if (context && context.context && context.context.company_ids) { - domain_account_id.push(['company_id', 'in', context.context.company_ids]); - } - this.create_form_fields = { - account_id: { - id: "account_id", - index: 0, // position in the form - corresponding_property: "account_id", // a account.move.line field name - label: _t("Account"), - required: true, - constructor: FieldMany2One, - field_properties: { - relation: "account.account", - string: _t("Account"), - type: "many2one", - domain: domain_account_id, - }, - }, - label: { - id: "label", - index: 5, - corresponding_property: "label", - label: _t("Label"), - required: true, - constructor: FieldChar, - field_properties: { - string: _t("Label"), - type: "char", - }, - }, - tax_id: { - id: "tax_id", - index: 10, - corresponding_property: "tax_id", - label: _t("Tax"), - required: false, - constructor: FieldMany2One, - field_properties: { - relation: "account.tax", - string: _t("Tax"), - type: "many2one", - domain: [['type_tax_use','!=','none']], - }, - }, - amount: { - id: "amount", - index: 15, - corresponding_property: "amount", - label: _t("Amount"), - required: true, - constructor: FieldFloat, - field_properties: { - string: _t("Amount"), - type: "float", - }, - }, - analytic_account_id: { - id: "analytic_account_id", - index: 20, - corresponding_property: "analytic_account_id", - label: _t("Analytic Acc."), - required: false, - group:"analytic.group_analytic_accounting", - constructor: FieldMany2One, - field_properties: { - relation: "account.analytic.account", - string: _t("Analytic Acc."), - type: "many2one", - }, - }, - }; - }, - - start: function() { - var self = this; - return $.when(this._super()).then(function(){ - var deferred_promises = []; - - // Create a dict account id -> account code for display facilities - deferred_promises.push(new Model("account.account") - .query(['id', 'code']) - .all().then(function(data) { - _.each(data, function(o) { self.map_account_id_code[o.id] = o.code }); - }) - ); - - // Get operation templates - deferred_promises.push(self.fetchPresets()); - - // Get the function to format currencies - deferred_promises.push(new Model("res.currency") - .call("get_format_currencies_js_function") - .then(function(data) { - self.formatCurrencies = new Function("amount, currency_id", data); - }) - ); - - // Bind ⌨ events - bus.on("keypress", "body", function(e) { - self.keyboardShortcutsHandler(e); - }); - - self.update_cp(); - - return $.when.apply($, deferred_promises); - }); - }, - - fetchPresets: function() { - var self = this; - var deferred_last_update = self.model_presets.query(['write_date']).order_by('-write_date').first().then(function (data) { - self.presets_last_write_date = (data ? data.write_date : undefined); - }); - var deferred_presets = self.model_presets.query().order_by('-sequence', '-id').all().then(function (data) { - self.presets = {}; - _(data).each(function(datum){ - var preset = { - id: datum.id, - name: datum.name, - sequence: datum.sequence, - lines: [{ - account_id: datum.account_id, - journal_id: datum.journal_id, - label: datum.label, - amount_type: datum.amount_type, - amount: datum.amount, - tax_id: datum.tax_id, - analytic_account_id: datum.analytic_account_id - }] - }; - if (datum.has_second_line) { - preset.lines.push({ - account_id: datum.second_account_id, - journal_id: datum.second_journal_id, - label: datum.second_label, - amount_type: datum.second_amount_type, - amount: datum.second_amount, - tax_id: datum.second_tax_id, - analytic_account_id: datum.second_analytic_account_id - }); - } - self.presets[datum.id] = preset; - }); - }); - return $.when(deferred_last_update, deferred_presets); - }, - - // NB : this method is not called when the widget becomes visible after another view was active - do_show: function() { - var self = this; - var super_call = _.bind(this._super, this); - // Reload presets if they were modified - self.model_presets.query(['write_date']).order_by('-write_date').first().then(function (data) { - if (!data || data.write_date != self.presets_last_write_date) { - self.fetchPresets().then(function() { - _.each(self.getChildren(), function(child) { - child.set("presets", self.presets); - }); - }); - } - // Finally reappear - super_call(); - }); - self.update_cp(); - }, - - // Update the contral panel - update_cp: function() { - var self = this; - self.update_control_panel({ - breadcrumbs: self.action_manager.get_breadcrumbs(), - }); - }, - - doActionClickHandler: function(e) { - e.preventDefault(); - var name = e.currentTarget.dataset.action_nam; - var model = e.currentTarget.dataset.model; - var ids = e.currentTarget.dataset.ids.split(",").map(Number); - this.action_manager.do_action({ - name: name, - res_model: model, - domain: [['id', 'in', ids]], - views: [[false, 'list'], [false, 'form']], - type: 'ir.actions.act_window', - view_type: "list", - view_mode: "list" - }); - }, - - presetConfigCreateClickHandler: function(e) { - e.preventDefault(); - var self = this; - return self.rpc("/web/action/load", {action_id: "account.action_account_reconcile_model"}).then(function(result) { - result.views = [[false, "form"], [false, "list"]]; - return self.do_action(result); - }); - }, - - presetConfigEditClickHandler: function(e) { - e.preventDefault(); - var self = this; - return self.rpc("/web/action/load", {action_id: "account.action_account_reconcile_model"}).then(function(result) { - result.views = [[false, "list"], [false, "form"]]; - return self.do_action(result); - }); - }, - - displayNotifications: function(notifications, speed) { - speed = defaultIfUndef(speed, this.aestetic_animation_speed) - for (var i=0; i
").appendTo(this.$el); - var sandbox_dom = this.$(".sandbox_lillibullero"); - var dropdown_width = $(QWeb.render("presets_dropdown", {presets: []})).appendTo(sandbox_dom).outerWidth(); - var max_width = 540; // Warning : hardcoded value (would be pretty tricky to compute from the DOM) - var total_width = 0; - var html = ""; - while (presets_array.length > 0) { - var preset = presets_array[0]; - var preset_html = QWeb.render("preset_button", {id: preset.id, name: preset.name}); - var actual_max_width = (presets_array.length === 1 ? max_width : max_width - dropdown_width); - total_width += $(preset_html).appendTo(sandbox_dom).outerWidth(); - if (total_width < actual_max_width) { - html += preset_html; - presets_array.shift(); - } else { - break; - } - } - if (presets_array.length > 0) { - html += QWeb.render("presets_dropdown", {presets: presets_array}); - } - temp.remove(); - return html; - }, - - keyboardShortcutsHandler: function(e) { - var self = this; - if ((e.which === 13 || e.which === 10) && (e.ctrlKey || e.metaKey)) { - self.processReconciliations(_.filter(self.getChildren(), function(o) { return self.monetaryIsZero(o.get("balance")); })); - } - }, - - processReconciliations: function(reconciliations) {}, - - displayReconciliation: function() {}, - - childValidated: function(child) {}, - - displayDoneMessage: function() {}, - - updateProgressbar: function() {}, - - // adds fields, prefixed with q_, to the move line for qweb rendering - decorateMoveLine: function(line) { - line.partial_reconcile = false; - line.propose_partial_reconcile = false; - line.q_due_date = (line.date_maturity === false ? line.date : line.date_maturity); - line.q_amount = (line.debit !== 0 ? line.q_debit : "") + (line.credit !== 0 ? line.q_credit : ""); - line.q_label = line.name; - var template_name = (QWeb.has_template(this.template_prefix+"reconciliation_move_line_details") ? this.template_prefix : "") + "reconciliation_move_line_details"; - line.q_popover = QWeb.render(template_name, {line: line}); - if (line.ref && line.ref !== line.name) - line.q_label = line.q_label + " : " + line.ref; - }, - - // Method that tests if a monetary amount == 0, we use 4 digits because no currency uses more - monetaryIsZero: function(amount, digits) { - return utils.float_is_zero(amount, digits === undefined ? 4 : digits); - }, -}); - -var abstractReconciliationLine = Widget.extend({ - className: 'o_reconciliation_line', - - events: { - "click .mv_line": "moveLineClickHandler", - "click .created_line": "createdLineClickHandler", - "click .pager_control_left:not(.disabled)": "pagerControlLeftHandler", - "click .pager_control_right:not(.disabled)": "pagerControlRightHandler", - "keyup .filter": "filterHandler", - "click .add_line": "addLineBeingEdited", - "click .preset": "presetClickHandler", - "click .line_info_button": function(e){e.stopPropagation()}, - // Prevent non natively focusable elements from gaining focus on click - "mousedown *[tabindex='0']": function(e){e.preventDefault()}, - }, - - init: function(parent, context) { - this._super(parent); - var self = this; - - this.decorateMoveLine = this.getParent().decorateMoveLine; - this.formatCurrencies = this.getParent().formatCurrencies; - this.monetaryIsZero = this.getParent().monetaryIsZero; - this.renderPresetsButtons = this.getParent().renderPresetsButtons; - - if (context.initial_data_provided === true && (context.reconciliation_proposition === undefined || context.line === undefined)) - console.error("[Warning] bankStatementReconciliationLine instanciated with incorrect context."); - - if (context.initial_data_provided) { - // Process data - this.partner_id = context.line.partner_id; - _.each(context.reconciliation_proposition, function(line) { - self.decorateMoveLine(line); - }, this); - this.set("mv_lines_selected", context.reconciliation_proposition); - } else { - this.set("mv_lines_selected", []); - } - - this.context = context; - 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_res_users = this.getParent().model_res_users; - this.model_tax = this.getParent().model_tax; - this.map_account_id_code = this.getParent().map_account_id_code; - 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 = ""; - - // Kind of a hack. Let's say : set("prop"), on("change:prop", dostuff), dostuff(){ return $.Deferred() } - // If you set("prop", value), you don't get the deferred back. So this is for the cases where you want - // to wait until move lines (in the "match" pannel) are loaded after you set("mode", "match") for instance. - this.finishedLoadingMoveLines; - - this.set("presets", this.getParent().presets); - this.on("change:presets", this, this.presetsChanged); - 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() {}, - - render: function() {}, - - /* 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 a.index - b.index }); - - // field_manager - var dataset = new data.DataSet(this, "account.account", self.context); - dataset.ids = []; - var fields_view = {}; - fields_view.arch = { - attrs: { string: "Stéphanie de Monaco", version: "7.0", class: "oe_form_container o_form_container" }, - children: [], - tag: "form" - }; - self.field_manager = new FormView ( - this, dataset, fields_view, { - initial_mode: 'edit', - disable_autofocus: false, - $buttons: $(), - $pager: $() - }); - self.field_manager.appendTo(document.createDocumentFragment()); // starts the FormView - - // 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 - self.field_manager.fields_view.fields = {}; - for (var i=0; i 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; }); - - limit += 1; // To see if there's more move lines than we display - if (limit > 0) - return self.updateMatchesGetMvLines(excluded_ids, offset, limit, function(move_lines) { - self.can_fetch_more_move_lines = (move_lines.length === limit); - self.set("mv_lines", move_lines.slice(0, limit-1)); - }); - else - self.set("mv_lines", []); - }, - updateMatchesGetMvLines: function(excluded_ids, offset, limit, callback) {}, - - // 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; - - // 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 = self.model_tax - .call("json_friendly_compute_all", [[tax_id], amount, self.get("currency_id")]) - .then(function(data){ - line_created_being_edited.length = 1; // remove tax lines - line_created_being_edited[0].amount_before_tax = amount; - line_created_being_edited[0].amount = data.total_excluded; - $.each(data.taxes, function(index, tax) { - var tax_account_id = (amount > 0 ? tax.account_id : tax.refund_account_id); - tax_account_id = tax_account_id !== false ? tax_account_id : line_created_being_edited[0].account_id; - line_created_being_edited.push({ - 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, - is_tax_line: true - }); - }); - } - ); - } else { - line_created_being_edited.length = 1; // remove tax lines - deferred_tax.resolve(); - } - } else { deferred_tax.resolve(); } - - return deferred_tax.then(function() { - self.set("line_created_being_edited", line_created_being_edited); - self.createdLinesChanged(); // 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(); - }, - - updateBalance: function() { - var self = this; - var balance = 0; - var mv_lines_selected = self.get("mv_lines_selected"); - _.each(mv_lines_selected, function(o) { - balance = balance - o.debit + o.credit; - }); - _.each(self.getCreatedLines(), function(o) { - balance += o.amount; - }); - // Should work as long as currency's rounding factor is > 0.001 (ie: don't use bitcoins) - balance = Math.round(balance*1000)/1000; - self.set("balance", balance); - }, - - // Returns an object that can be passed to process_reconciliation() - prepareCreatedMoveLinesForPersisting: function(lines) { - lines = _.filter(lines, function(line) { return !line.is_tax_line }); - return _.collect(lines, function(line) { - var dict = { - account_id: line.account_id, - name: line.label - }; - // Use amount_before_tax since the amount of the newly created line is adjusted to - // reflect tax included in price in account_move_line.create() - var amount = line.tax_id ? line.amount_before_tax: line.amount; - dict['credit'] = (amount > 0 ? amount : 0); - dict['debit'] = (amount < 0 ? -1 * amount : 0); - if (line.tax_id) dict['tax_ids'] = [[4, line.tax_id, null]]; - if (line.analytic_account_id) dict['analytic_account_id'] = line.analytic_account_id; - return dict; - }); - }, - - bowOut: function(speed, doPostMortemProcess) { - speed = defaultIfUndef(speed, this.animation_speed); - if (doPostMortemProcess) - var postMortemProcess = this.getPostMortemProcess(); - var self = this; - 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); - return $.when(self.$el.parent().animate({height: 0, marginBottom: 0}, speed*height/150)).then(function() { - $.each(self.$(".bootstrap_popover"), function(){ $(this).popover('destroy') }); - _.each(self.getChildren(), function(o){ o.destroy() }); - self.$el.parent().remove(); - return $.when(self.destroy()).then(function(){ - if (doPostMortemProcess) - postMortemProcess.call(self); - }); - }); - }, - - // Returns the post mortem process function. We need a closure to keep some data after this.destroy() - getPostMortemProcess: function() {}, -}); - -var bankStatementReconciliation = abstractReconciliation.extend({ - className: abstractReconciliation.prototype.className + ' o_bank_statement_reconciliation', - - events: _.defaults({ - "click .statement_name span": "statementNameClickHandler", - "keyup .change_statement_name_field": "changeStatementNameFieldHandler", - "click .change_statement_name_button": "changeStatementButtonClickHandler", - "click .show_more": "showMoreButtonClickHandler", - "click .goto_create_preset": "goToCreatePreset", - }, abstractReconciliation.prototype.events), - - init: function(parent, context) { - this.children_widget = bankStatementReconciliationLine; - this.template_prefix = "bank_statement_"; - this._super(parent, context); - - 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("Bank Reconciliation"); - context.display_name = this.title; // Used by breadcrumb - this.notifications = context.context.notifications || []; - this.lines = []; // list of reconciliations identifiers to instantiate children widgets - this.last_displayed_reconciliation_index = undefined; // Flow control - this.reconciled_lines = 0; // idem - this.num_already_reconciled_lines = 0; // Number of lines of the statement which were already reconciled - this.model_bank_statement = new Model("account.bank.statement"); - this.model_bank_statement_line = new Model("account.bank.statement.line"); - // The same move line cannot be selected for multiple reconciliations - this.excluded_move_lines_ids = {}; - this.widget_childrens = []; - }, - - serverPreprocessResultHandler: function(data) { - // Handles the data returned by a RPC call to reconciliation_widget_preprocess. Extending this method - // and reconciliation_widget_preprocess allows to fetch more data without extra RPC calls. - this.num_already_reconciled_lines = data.num_already_reconciled_lines; - this.notifications = this.notifications.concat(data.notifications); - this.lines = data.st_lines_ids; - if (this.single_statement && data.statement_name) - this.title = data.statement_name; - }, - - start: function() { - var self = this; - return $.when(this._super()).then(function(){ - var deferred_promises = []; - - // Let the server prepare the ground - deferred_promises.push(self.model_bank_statement - .call("reconciliation_widget_preprocess", [self.statement_ids || undefined]) - .then(function(data){ self.serverPreprocessResultHandler(data) }) - ); - - // When queries are done, render template and reconciliation lines - return $.when.apply($, deferred_promises).then(function(){ - - // If there is nothing to do, stop here - // Warning : there is something to do if not all lines are reconciled and/or not all statements are closed - // but it's easier to simply consider use cases : users should ne be able to try to recobncile closed statement(s) - if (self.lines.length === 0 && !self.statement_ids) { - self.$el.prepend(QWeb.render("bank_statement_nothing_to_reconcile")); - if (self.notifications) - self.displayNotifications(self.notifications); - return; - } - - // Render and display - self.$el.prepend(QWeb.render("bank_statement_reconciliation", { - title: self.title, - single_statement: self.single_statement, - total_lines: self.num_already_reconciled_lines+self.lines.length - })); - self.$el.find('.js_automatic_reconciliation').click(function() { - // Let odoo try to reconcile entries for the user - self.model_bank_statement_line - .call("reconciliation_widget_auto_reconcile", [self.lines || undefined, self.num_already_reconciled_lines]) - .then(function(data){ self.serverPreprocessResultHandler(data); }) - .then(function(){ self.$('.js_automatic_reconciliation').hide(); - return self.display_reconciliation_propositions(); - }); - - }); - return self.display_reconciliation_propositions(); - }); - }); - }, - - display_reconciliation_propositions: function() { - var self = this; - self.updateProgressbar(); - var reconciliations_to_show = self.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); - // Delete previous bankStatementReconciliationLine - $.each(self.widget_childrens, function(index, child) { - child.destroy(); - }); - - // If everything is reconciled, show end message - if (self.lines.length === 0) { - self.$(".reconciliation_lines_container").hide(); - if (self.notifications) - self.displayNotifications(self.notifications, 0); - self.displayDoneMessage(true); - return; - } - - // Display the reconciliations - return self.model_bank_statement_line - .call("get_data_for_reconciliation_widget", [reconciliations_to_show]) - .then(function (data) { - var child_promises = []; - var datum = data.shift(); - if (datum !== undefined) - child_promises.push(self.displayReconciliation(datum.st_line.id, 'match', false, true, datum.st_line, datum.reconciliation_proposition)); - while ((datum = data.shift()) !== undefined) - child_promises.push(self.displayReconciliation(datum.st_line.id, 'inactive', false, true, datum.st_line, datum.reconciliation_proposition)); - - // When reconciliations are instanciated, make an entrance - $.when.apply($, child_promises).then(function(){ - self.$(".reconciliation_lines_container").animate({opacity: 1}, self.aestetic_animation_speed, function() { - if (self.notifications) { - self.displayNotifications(self.notifications); - 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"); - }); - }, - - updateProgressbar: function() { - var self = this; - var done = self.num_already_reconciled_lines + self.reconciled_lines; - var total = self.num_already_reconciled_lines + self.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); - }, - - processReconciliations: function(reconciliations) { - if (reconciliations.length === 0) return; - var self = this; - var ids = _.collect(reconciliations, function(o) { return o.line_id }); - var data = _.collect(reconciliations, function(o) { return o.prepareDataForPersisting() }); - var deferred_animation = self.$(".reconciliation_lines_container, .show_more_container").fadeOut(self.aestetic_animation_speed); - var deferred_rpc = self.model_bank_statement_line.call("process_reconciliations", [ids, data]); - return $.when(deferred_animation, deferred_rpc) - .done(function() { - // Remove children - for (var i=0; i= 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.$(".o_protip").hide(); - self.updateShowMoreButton(); - self.$(".oe_form_sheet, o_form_sheet").append(QWeb.render("bank_statement_reconciliation_done_message", { - title: title, - all_was_auto_reconciled: all_was_auto_reconciled, - 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 - if (self.$(".achievement").length !== 0) - 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 self.monetaryIsZero(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.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(); - }, - - goToCreatePreset: function() { - this.action_manager.do_action({ - type: 'ir.actions.act_window', - res_model: 'account.reconcile.model', - views: [[false, 'form']], - target: 'current' - }); - }, -}); - -var bankStatementReconciliationLine = abstractReconciliationLine.extend({ - className: abstractReconciliationLine.prototype.className + ' o_bank_statement_reconciliation_line', - - events: _.defaults({ - "click .change_partner": "changePartnerClickHandler", - "click .button_ok": "persistAndBowOut", - "click .initial_line": "initialLineClickHandler", - "click .line_open_balance": "lineOpenBalanceClickHandler", - "click .do_partial_reconcile_button": "doPartialReconcileButtonClickHandler", - "click .undo_partial_reconcile_button": "undoPartialReconcileButtonClickHandler", - }, abstractReconciliationLine.prototype.events), - - init: function(parent, context) { - this.template_prefix = "bank_statement_"; - this._super(parent, context); - - if (context.line_id === undefined || - context.initial_data_provided === true && (context.reconciliation_proposition === undefined || context.line === undefined)) - console.error("[Warning] bankStatementReconciliationLine instanciated with incorrect context."); - - this.line_id = context.line_id; - this.model_bank_statement_line = this.getParent().model_bank_statement_line; - this.formatCurrencies = this.getParent().formatCurrencies; - this.monetaryIsZero = this.getParent().monetaryIsZero; - - if (context.initial_data_provided) { - // Process data - this.st_line = context.line; - this.decorateStatementLine(this.st_line); - this.set("currency_id", this.st_line.currency_id); - - // 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.st_line = undefined; - this.partner_id = undefined; - } - }, - - 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 = []; - _.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_reconciliation_widget", [[self.line_id], excluded_move_lines_ids]) - .then(function (data) { - self.st_line = data[0].st_line; - self.decorateStatementLine(self.st_line); - self.set("currency_id", self.st_line.currency_id); - 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); - mv_lines.push(line); - }, self); - self.set("mv_lines_selected", self.get("mv_lines_selected").concat(mv_lines)); - }); - }, - - render: function() { - var self = this; - self.$el.prepend(QWeb.render("bank_statement_reconciliation_line", { - line: self.st_line, - presets: self.renderPresetsButtons(self.get("presets")), - })); - - // 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.updateBalance(); - self.$(".change_partner_container").show(0); - self.$el.addClass("no_partner"); - } - - self.finishedLoadingMoveLines = $.Deferred(); - self.set("mode", self.context.mode); - return $.when(self.finishedLoadingMoveLines).then(function(){ - // Make sure the display is OK - self.balanceChanged(); - self.createdLinesChanged(); - self.updateAccountingViewMatchedLines(); - }); - }, - - restart: function(mode) { - var self = this; - mode = defaultIfUndef(mode, 'inactive'); - 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; - this._super(); - - // generate the change partner "form" - var change_partner_field = { - relation: "res.partner", - string: _t("Partner"), - type: "many2one", - domain: [['parent_id','=',false], '|', ['customer','=',true], ['supplier','=',true]], - help: "", - readonly: false, - required: true, - selectable: true, - states: {}, - views: {}, - context: {}, - }; - var change_partner_node = { - tag: "field", - children: [], - required: true, - attrs: { - invisible: "False", - modifiers: '', - name: "change_partner", - nolabel: "True", - } - }; - self.field_manager.fields_view.fields["change_partner"] = change_partner_field; - self.change_partner_field = new FieldMany2One(self.field_manager, change_partner_node); - self.change_partner_field.appendTo(self.$(".change_partner_container")); - self.change_partner_field.on("change:value", self.change_partner_field, function() { - self.changePartner(this.get_value()); - }); - self.change_partner_field.$el.find("input").attr("placeholder", self.st_line.communication_partner_name || _t("Select Partner")); - }, - - /** Utils */ - - /* TODO : if t-call for attr, all in qweb */ - decorateStatementLine: function(line){ - line.q_popover = QWeb.render("bank_statement_reconciliation_statement_line_details", {line: line}); - }, - - - /** Creating */ - - initializeCreateForm: function() { - this._super(); - this.label_field.set("value", this.st_line.name); - }, - - - /** Matching */ - - selectMoveLine: function(mv_line) { - var self = this; - var line = self._super(mv_line); - if (!line) return; - var last_selected_line = _.last(self.get("mv_lines_selected")); - - // Warn the user if he's selecting lines from both a payable and a receivable account - var selected_lines_account_types = _.collect(self.get("mv_lines_selected").concat(line), function(l) { return l.account_type }); - if (selected_lines_account_types.indexOf("payable") != -1 && selected_lines_account_types.indexOf("receivable") != -1) { - new CrashManager().show_warning({data: { - exception_type: _t("Incorrect Operation"), - message: _t("You cannot mix items from receivable and payable accounts.") - }}); - return; - } - - $(mv_line).attr('data-selected','true'); - self.set("mv_lines_selected", self.get("mv_lines_selected").concat(line)); - }, - - deselectMoveLine: function(mv_line) { - var self = this; - var line = self._super(mv_line); - if (!line) return; - - // remove partial reconciliation stuff if necessary - var need_redraw = false; - if (line.partial_reconcile === true) { - self.unpartialReconcileLine(line); - need_redraw = true; - } - if (line.propose_partial_reconcile === true) { - line.propose_partial_reconcile = false; - need_redraw = true; - } - if (need_redraw) self.deselectMoveLine(mv_line); - }, - - - /** Display */ - - initialLineClickHandler: function() { - var self = this; - if (self.get("mode") !== "inactive") { - self.set("mode", "inactive"); - } else { - self.set("mode", "match"); - } - }, - - changePartnerClickHandler: function() { - var self = this; - var partner_name = self.st_line.partner_name; - $.when(self.changePartner(false)).then(function(){ - self.$(".change_partner_container").show(); - self.$(".partner_name").hide(); - self.$(".change_partner_container").find("input").attr("placeholder", partner_name); - self.change_partner_field.$dropdown.trigger("click"); - }); - }, - - - /** 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("Reconcile")).removeClass("btn-primary").hide(); - - // Find out if the counterpart is lower than, equal or greater than the transaction being reconciled - var balance_type = undefined; - var digits = 4; - if (this.get("currency_id") && session.get_currency(this.get("currency_id"))) { - digits = session.get_currency(this.get("currency_id")).digits[1]; - } - if (self.monetaryIsZero(self.get("balance"), digits)) 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("Validate")); - 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").show(); - if (higlight_ok_button) self.$(".button_ok").addClass("btn-primary"); - if (ok_button_text !== undefined) self.$(".button_ok").text(ok_button_text) - } - - function createOpenBalance(name) { - var balance = self.get("balance"); - var amount = self.formatCurrencies(Math.abs(balance), self.get("currency_id")); - var $line = $(QWeb.render("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], - label: name || _t("Open balance") - })); - self.$(".tbody_open_balance").empty().append($line); - } - }, - - mvLinesSelectedChanged: function(elt, val) { - var self = this; - - // Reset partial reconciliation - _.each(self.get("mv_lines_selected"), function(line) { - if (line.partial_reconcile === true) self.unpartialReconcileLine(line); - if (line.propose_partial_reconcile === true) line.propose_partial_reconcile = false; - }); - - 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); - - self._super(elt, val); - }, - - /** Model */ - - doPartialReconcileButtonClickHandler: function(e) { - var line_id = $(e.currentTarget).closest("tr").data("lineid"); - var line = _.find(this.get("mv_lines_selected"), function(o) { return o.id == line_id }); - this.partialReconcileLine(line); - - $(e.currentTarget).popover('destroy'); - this.updateAccountingViewMatchedLines(); - this.updateBalance(); - e.stopPropagation(); - }, - - partialReconcileLine: function(line) { - var balance = this.get("balance"); - line.initial_amount = line.debit !== 0 ? line.debit : -1 * line.credit; - if (balance < 0) - line.debit += balance; - else - line.credit -= balance; - line.propose_partial_reconcile = false; - line.partial_reconcile = true; - }, - - undoPartialReconcileButtonClickHandler: function(e) { - var line_id = $(e.currentTarget).closest("tr").data("lineid"); - var line = _.find(this.get("mv_lines_selected"), function(o) { return o.id == line_id }); - this.unpartialReconcileLine(line); - - $(e.currentTarget).popover('destroy'); - this.updateAccountingViewMatchedLines(); - this.updateBalance(); - e.stopPropagation(); - }, - - unpartialReconcileLine: function(line) { - if (line.initial_amount > 0) - line.debit = line.initial_amount; - else - line.credit = -1 * line.initial_amount; - line.propose_partial_reconcile = true; - line.partial_reconcile = false; - line.amount_str = this.formatCurrencies(Math.abs(line.debit + line.credit), this.get("currency_id")); - }, - - updateBalance: function() { - var self = this; - var mv_lines_selected = self.get("mv_lines_selected"); - - // 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*10000)/10000; - - self.set("balance", balance); - }, - - updateMatchesGetMvLines: function(excluded_ids, offset, limit, callback) { - var self = this; - 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]; - for (var i=0; i 0 ? balance : 0; - dict['credit'] = balance < 0 ? -1*balance : 0; - - return dict; - }, - - prepareDataForPersisting: function() { - var self = this; - var counterpart_aml = _.filter(this.get("mv_lines_selected"), function(line) { return ! line.already_paid }); - var counterpart_aml_dicts = _.collect(counterpart_aml, function(line) { return self.prepareCounterpartAMLDict(line) }); - var payment_aml = _.filter(this.get("mv_lines_selected"), function(line) { return line.already_paid }); - var payment_aml_ids = _.collect(payment_aml, function(line) { return line.id }); - var new_aml_dicts = this.prepareCreatedMoveLinesForPersisting(this.getCreatedLines()); - var digits = 4; - if (this.get("currency_id") && session.get_currency(this.get("currency_id"))) { - digits = session.get_currency(this.get("currency_id")).digits[1]; - } - if (! self.monetaryIsZero(this.get("balance"), digits)) new_aml_dicts.push(this.prepareOpenBalanceForPersisting()); - return { - 'counterpart_aml_dicts': counterpart_aml_dicts, - 'payment_aml_ids': payment_aml_ids, - 'new_aml_dicts': new_aml_dicts, - }; - }, - - // Persist data, notify parent view and terminate widget - persistAndBowOut: function() { - var self = this; - if (! this.is_consistent) return; - self.$(".button_ok").attr("disabled", "disabled"); - this.model_bank_statement_line.call("process_reconciliations", [[this.line_id], [this.prepareDataForPersisting()]]).done(function() { - self.bowOut(self.animation_speed, true); - }).always(function() { - self.$(".button_ok").removeAttr("disabled"); - }); - }, - - getPostMortemProcess: function() { - var clone = _.clone(this); - var parent = this.getParent(); - var partner_id = this.partner_id; - var mv_lines_selected = this.get("mv_lines_selected"); - return function() { - parent.unexcludeMoveLines(undefined, partner_id, mv_lines_selected); - parent.childValidated(clone); - }; - }, -}); - -var manualReconciliation = abstractReconciliation.extend({ - className: abstractReconciliation.prototype.className + ' o_manual_reconciliation', - - events: _.defaults({ - "change input[name='modeselektor']": "modeSelektorHandler", - }, abstractReconciliation.prototype.events), - - init: function(parent, context) { - this.children_widget = manualReconciliationLine; - this.template_prefix = "manual_"; - this._super(parent, context); - - this.model_aml = new Model("account.move.line"); - this.model_partner = new Model("res.partner"); - this.model_account = new Model("account.account"); - this.title = _t("Journal Items to Reconcile"); - context.display_name = _t("Invoices & Payments Matching"); // Used by breadcrumb - this.max_move_lines_displayed = 20; - - this.mode = defaultIfUndef(context.context.mode, 'all'); - this.partner_ids = context.context.partner_ids; - this.account_ids = context.context.account_ids; - this.show_mode_selector = defaultIfUndef(context.context.show_mode_selector, true); - // If working on given accounts or partners, don't fetch reconciliations for the other type - if (this.partner_ids && ! this.account_ids) this.account_ids = []; - if (this.account_ids && ! this.partner_ids) this.partner_ids = []; - // If working on a single account or partner, hide the mode selector and progressbar - this.working_on_a_given_item = utils.xor((this.partner_ids && this.partner_ids.length === 1), (this.account_ids && this.account_ids.length === 1)); - - this.customers_data = { - show: true, - items: [], - num_total: 0, - num_done: 0, - }; - this.suppliers_data = { - show: true, - items: [], - num_total: 0, - num_done: 0, - }; - this.accounts_data = { - show: true, - items: [], - num_total: 0, - num_done: 0, - }; - this.create_form_fields = _.defaults({ - journal_id: { - id: "journal_id", - index: 2, - corresponding_property: "journal_id", // a account.move field name - label: _t("Journal"), - required: true, - constructor: FieldMany2One, - field_properties: { - relation: "account.journal", - string: _t("Journal"), - type: "many2one", - }, - } - }, this.create_form_fields); - }, - - start: function() { - var self = this; - return $.when(this._super()).then(function(){ - return self.model_aml.call("get_data_for_manual_reconciliation_widget", [self.partner_ids, self.account_ids]).then(function(data){ - var data_customers_len = data.customers ? data.customers.length : 0; - var data_suppliers_len = data.suppliers ? data.suppliers.length : 0; - var data_accounts_len = data.accounts ? data.accounts.length : 0; - - // If nothing to reconcile, stop here - if (data_customers_len + data_suppliers_len + data_accounts_len === 0) { - self.$el.prepend(QWeb.render("manual_reconciliation_nothing_to_reconcile")); - return; - } - - // If reconciling a specified account/partner, adapt title to the use case - if (self.working_on_a_given_item) { - self.title = _t("Reconciling "); - if (data_customers_len === 1) { self.title += data.customers[0].partner_name } - else if (data_suppliers_len === 1) { self.title += data.suppliers[0].partner_name } - else if (data_accounts_len === 1) { self.title += data.accounts[0].account_code } - } - - // Display only modes for which there are items to reconcile - var display_mode_customers = data_customers_len !== 0; - var display_mode_suppliers = data_suppliers_len !== 0; - var display_mode_others = data_accounts_len !== 0; - var mode_selector_relevant = (display_mode_customers + display_mode_suppliers + display_mode_others) > 1; - - // Display interface - self.$el.prepend(QWeb.render("manual_reconciliation", { - title: self.title, - total_lines: 0, - hide_progress: self.working_on_a_given_item, - mode: self.mode, - show_mode_selector: self.show_mode_selector && !self.working_on_a_given_item && mode_selector_relevant, - display_mode_customers: display_mode_customers, - display_mode_suppliers: display_mode_suppliers, - display_mode_others: display_mode_others, - })); - - // Process data - if (data_customers_len !== 0) { - self.customers_data.num_total = data_customers_len; - self.customers_data.items = _.each(data.customers, function(item){self.prepareReconciliationData(item, 'customer')}); - } - if (data_suppliers_len !== 0) { - self.suppliers_data.num_total = data_suppliers_len; - self.suppliers_data.items = _.each(data.suppliers, function(item){self.prepareReconciliationData(item, 'supplier')}); - } - if (data_accounts_len !== 0) { - self.accounts_data.num_total = data_accounts_len; - self.accounts_data.items = _.each(data.accounts, function(item){self.prepareReconciliationData(item, 'account')}); - } - - // Instanciate reconciliations - self.$(".reconciliation_lines_container").css("opacity", 0); - return $.when(self.updateProgress(false)).then(function(){ - return self.$(".reconciliation_lines_container").animate({opacity: 1}, self.aestetic_animation_speed); - }); - }).always(function() { - self.$(".button_ok").removeAttr("disabled"); - }); - }); - }, - - processReconciliations: function(reconciliations) { - if (reconciliations.length === 0) return; - var self = this; - var reconciliations_type_count = _.countBy(reconciliations, function(rec) { return rec.data.reconciliation_type; }); - var data = _.collect(reconciliations, function(rec) { - return { - id: rec.data.reconciliation_type === 'account' ? rec.data.account_id : rec.data.partner_id, - type: rec.data.reconciliation_type === 'account' ? 'account' : 'partner', - mv_line_ids: _.collect(rec.get("mv_lines_selected"), function(o){ return o.id }), - new_mv_line_dicts: rec.prepareCreatedMoveLinesForPersisting(rec.getCreatedLines()), - }; - }); - var deferred_animation = self.$(".reconciliation_lines_container, .show_more_container").fadeOut(self.aestetic_animation_speed); - var deferred_rpc = self.model_aml.call("process_reconciliations", [data]); - return $.when(deferred_animation, deferred_rpc) - .done(function() { - // Remove children - for (var i=0; i"); - this.$(".done_message").wrap(container).css("opacity", 0).css("position", "relative").css("left", "-50%"); - this.$(".done_message").animate({opacity: 1, left: 0}, this.aestetic_animation_speed*2, "easeOutCubic"); - this.$(".done_message").animate({opacity: 1}, this.aestetic_animation_speed*3, "easeOutCubic"); - }, - - hideDoneMessage: function() { - this.$(".done_message").remove(); - }, - - prepareReconciliationData: function(item, type) { - item.displayed = false; - item.reconciliation_type = type; - }, - - childValidated: function(reconciliation_type) { - if (reconciliation_type === "customer") - this.customers_data.num_done++; - else if (reconciliation_type === "supplier") - this.suppliers_data.num_done++; - else if (reconciliation_type === "account") - this.accounts_data.num_done++; - this.updateProgress(); - }, -}); - -var manualReconciliationLine = abstractReconciliationLine.extend({ - className: abstractReconciliationLine.prototype.className + ' o_manual_reconciliation_line', - - events: _.defaults({ - "click .accounting_view thead": "headerClickHandler", - "click .line_open_balance": "lineOpenBalanceClickHandler", - "click .button_reconcile": "buttonReconcileClickHandler", - }, abstractReconciliationLine.prototype.events), - - init: function(parent, context) { - this.template_prefix = "manual_"; - this._super(parent, context); - - this.working_on_a_given_item = this.getParent().working_on_a_given_item; - this.model_aml = this.getParent().model_aml; - this.data = context.data; - this.initial_currency = context.data.currency_id; - this.set("currency_id", context.data.currency_id); - this.on("change:currency_id", this, this.currencyChanged); - _.each(context.data.reconciliation_proposition, function(line) { - this.decorateMoveLine(line); - }, this); - this.set("mv_lines_selected", context.data.reconciliation_proposition); - }, - - render: function() { - var self = this; - self.$el.prepend(QWeb.render("manual_reconciliation_line", { - data: self.data, - presets: self.renderPresetsButtons(self.get("presets")), - })); - self.$(".match").slideUp(0); - self.$(".create").slideUp(0); - self.createFormWidgets(); - //Ugly fix to make the many2one dropdown work - self.$el.append($('ul.ui-autocomplete')); - self.updateBalance(); - self.updateAccountingViewMatchedLines(); - self.finishedLoadingMoveLines = $.Deferred(); - self.set("mode", "match"); - return self.finishedLoadingMoveLines; - }, - - islineCreatedBeingEditedValid: function() { - return this._super() && this.get("line_created_being_edited")[0].journal_id; - }, - - selectMoveLine: function(mv_line) { - var self = this; - var line = self._super(mv_line); - if (!line) return; - - $(mv_line).attr('data-selected','true'); - self.set("mv_lines_selected", self.get("mv_lines_selected").concat(line)); - }, - - mvLinesChanged: function() { - var self = this; - self._super(); - - // If we're not reconciling a specific account/partner, we're displaying all remaining - // move lines and there's not at least a debit and a credit, consider that the reconciliation is done - if (!self.working_on_a_given_item && self.get("pager_index") === 0 && !self.can_fetch_more_move_lines && self.get("mv_lines_selected").length === 0 && self.filter === '') { - var mkay, mmkay = false; - var lines = self.get("mv_lines").concat(self.mv_lines_deselected); - for (var i=0; i *").length + this.$(".tbody_created_lines > *").length === 0) - this.$(".tbody_matched_lines").append('\ - '); - }, - - updateAccountingViewMatchedLines: function() { - this._super(); - this.preventAccountingViewCollapse(); - }, - - updateAccountingViewCreatedLines: function() { - this._super(); - this.preventAccountingViewCollapse(); - }, - - balanceChanged: function() { - var self = this; - var balance = self.get("balance"); - - self.$(".button_reconcile").removeClass("btn-primary"); - self.$(".button_reconcile").text(_t("Reconcile")); - self.persist_action = "reconcile"; - if (self.get("mv_lines_selected").length < 2) { - self.$(".button_reconcile").text(_t("Skip")); - self.persist_action = "mark_as_reconciled"; - } else if (self.monetaryIsZero(balance)) { - self.$(".button_reconcile").addClass("btn-primary"); - } - - self.$(".tbody_open_balance").empty(); - if ((! self.monetaryIsZero(balance)) && self.get("mv_lines_selected").length > 1) { - var debit = (balance > 0 ? self.formatCurrencies(balance, self.get("currency_id")) : ""); - var credit = (balance < 0 ? self.formatCurrencies(-1*balance, self.get("currency_id")) : ""); - var $line = $(QWeb.render("manual_reconciliation_line_open_balance", { - debit: debit, - credit: credit, - label: _t("Create writeoff") - })); - self.$(".tbody_open_balance").append($line); - } - }, - - // If all selected lines have the same currency, set it as the widget's currency - mvLinesSelectedChanged: function(elt, val) { - var lines = this.get("mv_lines_selected"); - var new_currency = this.initial_currency; - if (lines.length > 0) { - var first_line_currency = lines[0].currency_id; - var same_currency = _.every(lines, function(line){ return line.currency_id === first_line_currency }); - if (same_currency) - new_currency = first_line_currency; - } - if (new_currency != this.get("currency_id")) - this.set("currency_id", new_currency); - else - this._super(elt, val); - }, - - currencyChanged: function() { - var self = this; - this.mv_lines_deselected = []; - this.updateMatches(); - var line_ids = _.collect(self.get("mv_lines_selected"), function(o){ return o.id }); - self.model_aml - .call("prepare_move_lines_for_reconciliation_widget", [line_ids, self.get("currency_id")]) - .then(function(lines) { - _.each(lines, function(line) { self.decorateMoveLine(line) }, self); - self.set("mv_lines_selected", lines); - }); - }, - - - /* Model */ - - loadReconciliationProposition: function() { - var self = this; - return self.model_aml - .call("get_reconciliation_proposition", [self.data.account_id, self.data.partner_id || undefined]) - .then(function(lines) { - _.each(lines, function(line) { self.decorateMoveLine(line) }, self); - self.set("mv_lines_selected", lines); - }); - }, - - updateMatchesGetMvLines: function(excluded_ids, offset, limit, callback) { - var self = this; - return self.model_aml - .call("get_move_lines_for_manual_reconciliation", [ - self.data.account_id, - self.data.partner_id || undefined, - excluded_ids, - self.filter, - offset, - limit, - self.get("currency_id") ]) - .then(function (lines) { - _.each(lines, function(line) { self.decorateMoveLine(line) }, self); - callback.call(self, lines); - }); - }, - - prepareCreatedMoveLinesForPersisting: function(lines) { - var dicts = this._super(lines); - for (var i=0; i= 1) - self.set("mode", "match"); - self.balanceChanged(); - }).always(function() { - self.$(".button_reconcile").removeAttr("disabled"); - }); - }, - - // Warning: _.throttle is an awkward workaround for a bug where markAsReconciled is called more than once too - // quickly, resulting in a concurrent update server-side. - markAsReconciled: _.throttle(function() { - var self = this; - var type = this.data.reconciliation_type; - var id = (type === "partner" ? this.data.partner_id : this.data.account_id); - var model = (type === "partner" ? this.getParent().model_partner : this.getParent().model_account); - model.call("mark_as_reconciled", [[id]]).then(function() { - self.bowOut(self.animation_speed, true); - }); - }, 100, {trailing: false}), - - getPostMortemProcess: function() { - var reconciliation_type = this.data.reconciliation_type; - var parent = this.getParent(); - return function() { - if (parent) - parent.childValidated(reconciliation_type); - }; - }, -}); - -core.action_registry.add('bank_statement_reconciliation_view', bankStatementReconciliation); -/* This widget takes its parameters from the action context. They are : - - statement_ids: list of bank statements to reconcile (if not passed, all unreconciled bank - statement lines will be displayed) - - notifications: list of { - type: one of bootstrap alert types (success, info, warning, danger) - message: the message to display, - details: a dict containing 'name', 'model' and 'ids' used to call a window action - } -*/ - -core.action_registry.add('manual_reconciliation_view', manualReconciliation); -/* This widget takes its parameters from the action context. They are : - - mode : 'customers', 'suppliers', 'others' or 'all' (default 'all') - - show_mode_selector : boolean (default true) - - partner_ids: list of ids - - account_ids: list of ids - - The mode sets which kind of reconciliations are displayed. Set show_mode_selector to false if the user - should not be able to change the mode. partner_ids and account_ids allow to specify items to reconcile, - otherwise all items for which there are entries to reconcile are displayed. -*/ - -return { - abstractReconciliation: abstractReconciliation, - abstractReconciliationLine: abstractReconciliationLine, - bankStatementReconciliation: bankStatementReconciliation, - manualReconciliation: manualReconciliation, - bankStatementReconciliationLine: bankStatementReconciliationLine, -}; - -}); diff --git a/addons/account/static/src/js/move_line_quickadd.js b/addons/account/static/src/js/move_line_quickadd.js index d60f0c3eaee..725440d9066 100644 --- a/addons/account/static/src/js/move_line_quickadd.js +++ b/addons/account/static/src/js/move_line_quickadd.js @@ -5,6 +5,7 @@ var core = require('web.core'); var data = require('web.data'); var ListView = require('web.ListView'); var Model = require('web.DataModel'); +var view_registry = require('web.view_registry'); var QWeb = core.qweb; @@ -99,6 +100,7 @@ var QuickAddListView = ListView.extend({ return self.old_search(compound_domain, self.last_context, self.last_group_by); }, }); -core.view_registry.add('tree_account_move_line_quickadd', QuickAddListView); + +view_registry.add('tree_account_move_line_quickadd', QuickAddListView); }); diff --git a/addons/account/static/src/js/reconciliation/reconciliation_action.js b/addons/account/static/src/js/reconciliation/reconciliation_action.js new file mode 100644 index 00000000000..f9f588a59df --- /dev/null +++ b/addons/account/static/src/js/reconciliation/reconciliation_action.js @@ -0,0 +1,285 @@ +odoo.define('account.ReconciliationClientAction', function (require) { +"use strict"; + +var ReconciliationModel = require('account.ReconciliationModel'); +var ReconciliationRenderer = require('account.ReconciliationRenderer'); +var ControlPanelMixin = require('web.ControlPanelMixin'); +var Widget = require('web.Widget'); +var core = require('web.core'); + + +/** + * Widget used as action for 'account.bank.statement' reconciliation + */ +var StatementAction = Widget.extend(ControlPanelMixin, { + title: core._t('Bank reconciliation'), + template: 'reconciliation', + custom_events: { + change_mode: '_onAction', + change_filter: '_onAction', + change_offset: '_onAction', + change_partner: '_onAction', + add_proposition: '_onAction', + remove_proposition: '_onAction', + update_proposition: '_onAction', + create_proposition: '_onAction', + quick_create_proposition: '_onAction', + toggle_partial_reconcile: '_onAction', + auto_reconciliation: '_onValidate', + validate: '_onValidate', + validate_all_balanced: '_onValidate', + change_name: '_onChangeName', + }, + config: { + // used to instanciate the model + Model: ReconciliationModel.StatementModel, + // used to instanciate the action interface + ActionRenderer: ReconciliationRenderer.StatementRenderer, + // used to instanciate each widget line + LineRenderer: ReconciliationRenderer.LineRenderer + }, + + /** + * @override + * @param {Object} params + * @param {Object} params.context + * + */ + init: function (parent, params) { + this._super.apply(this, arguments); + this.action_manager = parent; + this.params = params; + this.model = new this.config.Model(this, {modelName: "account.bank.statement.line"}); + this.widgets = []; + if (!this.action_manager) { + this.set_cp_bus(new Widget()); + } + }, + + /** + * instantiate the action renderer + * + * @override + */ + willStart: function () { + var self = this; + var def = this.model.load(this.params.context).then(this._super.bind(this)); + return def.then(function () { + self.title = self.model.bank_statement_id ? self.model.bank_statement_id.display_name : self.title; + self.renderer = new self.config.ActionRenderer(self, self.model, { + 'bank_statement_id': self.model.bank_statement_id, + 'valuenow': self.model.valuenow, + 'valuemax': self.model.valuemax, + 'title': self.title, + }); + }); + }, + + /** + * append the renderer and instantiate the line renderers + * + * @override + */ + start: function () { + var self = this; + + this.set("title", this.title); + var breadcrumbs = this.action_manager && this.action_manager.get_breadcrumbs() || [{ title: this.title, action: this }]; + this.update_control_panel({breadcrumbs: breadcrumbs, search_view_hidden: true}, {clear: true}); + + this.renderer.prependTo(self.$('.o_form_sheet')); + _.each(this.model.lines, function (line, handle) { + var widget = new self.config.LineRenderer(self, self.model, line); + widget.handle = handle; + self.widgets.push(widget); + widget.appendTo(self.$('.o_reconciliation_lines')); + }); + this._openFirstLine(); + }, + + /** + * update the control panel and breadcrumbs + * + * @override + */ + do_show: function () { + this._super.apply(this, arguments); + if (this.action_manager) { + var breadcrumbs = this.action_manager && this.action_manager.get_breadcrumbs() || [{ title: this.title, action: this }]; + while (breadcrumbs.length) { + if (breadcrumbs[breadcrumbs.length-1].action.widget === this) { + break; + } + breadcrumbs.pop(); + } + this.update_control_panel({breadcrumbs: breadcrumbs, search_view_hidden: true}, {clear: true}); + this.action_manager.do_push_state({ + action: this.params.tag, + active_id: this.params.res_id, + }); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @param {string} handle + * @returns {Widget} widget line + */ + _getWidget: function (handle) { + return _.find(this.widgets, function (widget) {return widget.handle===handle;}); + }, + + /** + * sitch to 'match' the first available line + * + * @private + */ + _openFirstLine: function () { + var self = this; + + var handle = _.compact(_.map(this.model.lines, function (line, handle) { + return line.reconciled ? null : handle; + }))[0]; + if (handle) { + var line = this.model.getLine(handle); + this.model.changeMode(handle, 'match').always(function () { + self._getWidget(handle).update(line); + }); + } + return handle; + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * dispatch on the camelcased event name to model method then update the + * line renderer with the new state. If the mode was switched from 'inactive' + * to 'create' or 'match', the other lines switch to 'inactive' mode + * + * @private + * @param {OdooEvent} event + */ + _onAction: function (event) { + var self = this; + var handle = event.target.handle; + var line = this.model.getLine(handle); + var mode = line.mode; + this.model[_.str.camelize(event.name)](handle, event.data.data).always(function () { + self._getWidget(handle).update(line); + if (mode === 'inactive' && line.mode !== 'inactive') { + _.each(self.model.lines, function (line, _handle) { + if (line.mode !== 'inactive' && _handle !== handle) { + self.model.changeMode(_handle, 'inactive'); + self._getWidget(_handle).update(line); + } + }); + } + }); + }, + + /** + * call 'changeName' model method + * + * @private + * @param {OdooEvent} event + */ + _onChangeName: function (event) { + var self = this; + var title = event.data.data; + this.model.changeName(title).then(function () { + self.title = title; + self.set("title", title); + self.renderer.update({ + 'valuenow': self.model.valuenow, + 'valuemax': self.model.valuemax, + 'title': title, + }); + }); + }, + + /** + * call 'validate' or 'autoReconciliation' model method then destroy the + * validated lines and update the action renderer with the new status bar + * values and notifications then open the first available line + * + * @private + * @param {OdooEvent} event + */ + _onValidate: function (event) { + var self = this; + var handle = event.target.handle; + var method = event.name.indexOf('auto_reconciliation') === -1 ? 'validate' : 'autoReconciliation'; + this.model[method](handle).then(function (result) { + self.renderer.update({ + 'valuenow': self.model.valuenow, + 'valuemax': self.model.valuemax, + 'title': self.title, + 'time': Date.now()-self.time, + 'notifications': result.notifications, + }); + _.each(result.handles, function (handle) { + self._getWidget(handle).destroy(); + }); + self._openFirstLine(); + }); + }, +}); + + +/** + * Widget used as action for 'account.move.line' and 'res.partner' for the + * manual reconciliation and mark data as reconciliate + */ +var ManualAction = StatementAction.extend({ + title: core._t('Journal Items to Reconcile'), + config: { + Model: ReconciliationModel.ManualModel, + ActionRenderer: ReconciliationRenderer.ManualRenderer, + LineRenderer: ReconciliationRenderer.ManualLineRenderer + }, + /** + * call 'validate' or 'autoReconciliation' model method then destroy the + * reconcilied lines, update the not reconcilied and update the action + * renderer with the new status bar values and notifications then open the + * first available line + * + * @private + * @param {OdooEvent} event + */ + _onValidate: function (event) { + var self = this; + var handle = event.target.handle; + var method = event.name.indexOf('auto_reconciliation') === -1 ? 'validate' : 'autoReconciliation'; + this.model[method](handle).then(function (result) { + self.renderer.update({ + 'valuenow': self.model.valuenow, + 'valuemax': self.model.valuemax, + 'title': self.title, + 'time': Date.now()-self.time, + 'notifications': result.notifications, + }); + _.each(result.reconciled, function (handle) { + self._getWidget(handle).destroy(); + }); + _.each(result.updated, function (handle) { + self._getWidget(handle).update(self.model.getLine(handle)); + }); + self._openFirstLine(); + }); + }, +}); + +core.action_registry.add('bank_statement_reconciliation_view', StatementAction); +core.action_registry.add('manual_reconciliation_view', ManualAction); + +return { + StatementAction: StatementAction, + ManualAction: ManualAction, +}; +}); diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js new file mode 100644 index 00000000000..4c5e3f6ec0e --- /dev/null +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -0,0 +1,1068 @@ +odoo.define('account.ReconciliationModel', function (require) { +"use strict"; + +var BasicModel = require('web.BasicModel'); +var field_utils = require('web.field_utils'); +var CrashManager = require('web.CrashManager'); +var core = require('web.core'); +var _t = core._t; + + +/** + * Model use to fetch, format and update 'account.bank.statement' and + * 'account.bank.statement.line' datas allowing reconciliation + * + * The statement internal structure: + * + * { + * valuenow: integer + * valuenow: valuemax + * [bank_statement_id]: { + * id: integer + * display_name: string + * } + * reconcileModels: [object] + * accounts: {id: code} + * } + * + * The internal structure of each line is: + * + * { + * balance: { + * type: number - show/hide action button + * amount: number - real amount + * amount_str: string - formated amount + * account_code: string + * }, + * st_line: { + * partner_id: integer + * partner_name: string + * } + * mode: string ('inactive', 'match', 'create') + * reconciliation_proposition: { + * id: number|string + * partial_reconcile: boolean + * invalid: boolean - through the invalid line (without account, label...) + * is_tax: boolean + * account_code: string + * date: string + * date_maturity: string + * label: string + * amount: number - real amount + * amount_str: string - formated amount + * [already_paid]: boolean + * [partner_id]: integer + * [partner_name]: string + * [account_code]: string + * [journal_id]: { + * id: integer + * display_name: string + * } + * [ref]: string + * [is_partially_reconciled]: boolean + * [amount_currency_str]: string|false (amount in record currency) + * } + * mv_lines: object - idem than reconciliation_proposition + * offset: integer + * filter: string + * [createForm]: { + * account_id: { + * id: integer + * display_name: string + * } + * tax_id: { + * id: integer + * display_name: string + * } + * analytic_account_id: { + * id: integer + * display_name: string + * } + * label: string + * amount: number, + * [journal_id]: { + * id: integer + * display_name: string + * } + * } + * } + */ +var StatementModel = BasicModel.extend({ + avoidCreate: false, + quickCreateFields: ['account_id', 'amount', 'analytic_account_id', 'label', 'tax_id'], + + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + this.reconcileModels = []; + this.lines = {}; + this.valuenow = 0; + this.valuemax = 0; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * add a reconciliation proposition from the matched lines + * We also display a warning if the user tries to add 2 line with different + * account type + * + * @param {string} handle + * @param {number} mv_lines id + * @returns {Deferred} + */ + addProposition: function (handle, mv_line_id) { + var line = this.getLine(handle); + var prop = _.clone(_.find(line.mv_lines, {'id': mv_line_id})); + + function checkAccountType (r) { + return !isNaN(r.id) && r.account_type !== prop.account_type; + } + if (_.any(line.reconciliation_proposition, checkAccountType)) { + new CrashManager().show_warning({data: { + exception_type: _t("Incorrect Operation"), + message: _t("You cannot mix items from receivable and payable accounts.") + }}); + return $.when(); + } + + line.reconciliation_proposition.push(prop); + _.each(line.reconciliation_proposition, function (line) { + line.partial_reconcile = false; + }); + return $.when(this._computeLine(line), this._performMoveLine(handle)); + }, + /** + * send information 'account.bank.statement.line' model to reconciliate + * lines, call rpc to 'reconciliation_widget_auto_reconcile' + * Update the number of validated line + * + * @returns {Deferred} resolved with an object who contains + * 'handles' key and 'notifications' + */ + autoReconciliation: function () { + var self = this; + var ids = _.pluck(_.filter(_.values(this.lines), {'reconciled': false}), 'id'); + return this.performModelRPC("account.bank.statement.line", 'reconciliation_widget_auto_reconcile', [ids, self.valuenow]).then(function (result) { + var reconciled_ids = _.difference(ids, result.st_lines_ids); + self.valuenow += reconciled_ids.length; + result.handles = []; + _.each(self.lines, function (line, handle) { + if (reconciled_ids.indexOf(line.id) !== -1) { + line.reconciled = true; + result.handles.push(handle); + } + }); + return result; + }); + }, + /** + * change the filter for the target line and fetch the new matched lines + * + * @param {string} handle + * @param {string} filter + * @returns {Deferred} + */ + changeFilter: function (handle, filter) { + this.getLine(handle).filter = filter; + return this._performMoveLine(handle); + }, + /** + * change the mode line ('inactive', 'match', 'create'), and fetch the new + * matched lines or prepare to create a new line + * + * 'match': display the matched lines, the user can select the lines to + * apply there as proposition + * 'create': display fields and quick create button to create a new + * proposition for the reconciliation + * + * @param {string} handle + * @param {string} mode + * @returns {Deferred} + */ + changeMode: function (handle, mode) { + var line = this.getLine(handle); + if (line.mode === 'create') { + this._blurProposition(handle); + line.createForm = null; + } + if (mode === 'create' && this.avoidCreate) { + mode = 'match'; + } + line.mode = mode; + if (mode === 'match') { + return this._performMoveLine(handle); + } + if (line.mode === 'create') { + return this.createProposition(handle); + } + return $.when(); + }, + /** + * call 'write' method on the 'account.bank.statement' + * + * @param {string} name + * @returns {Deferred} + */ + changeName: function (name) { + return this.performModelRPC("account.bank.statement", "write", + [this.bank_statement_id.id, {name: name}]); + }, + /** + * change the offset for the matched lines, and fetch the new matched lines + * + * @param {string} handle + * @param {number} offset + * @returns {Deferred} + */ + changeOffset: function (handle, offset) { + this.getLine(handle).offset += offset; + return this._performMoveLine(handle); + }, + /** + * change the partner on the line and fetch the new matched lines + * + * @param {string} handle + * @param {Object} partner + * @param {string} partner.display_name + * @param {number} partner.id + * @returns {Deferred} + */ + changePartner: function (handle, partner) { + var self = this; + var line = this.getLine(handle); + line.st_line.partner_id = partner && partner.id; + line.st_line.partner_name = partner && partner.display_name || ''; + return this._performMoveLine(handle).then(function () { + if (line.mode === 'create') { + return self.createProposition(handle); + } + }); + }, + /** + * + * then open the first available line + * + * @param {string} handle + * @returns {Deferred} + */ + createProposition: function (handle) { + var line = this.getLine(handle); + var prop = _.filter(line.reconciliation_proposition, '__focus'); + var last = prop[prop.length-1]; + if (last && (!last.account_id || !last.label.length || !last.amount)) { + return $.Deferred().reject(); + } + + prop = this._formatQuickCreate(line); + line.reconciliation_proposition.push(prop); + line.createForm = _.pick(prop, this.quickCreateFields); + return this._computeLine(line); + }, + /** + * get the line data for this handle + * + * @param {OdooEvent} event + * @returns {Object} + */ + getLine: function (handle) { + return this.lines[handle]; + }, + /** + * load data from + * - 'account.bank.statement' fetch the line id and bank_statement_id info + * - 'account.reconcile.model' fetch all reconcile model (for quick add) + * - 'account.account' fetch all account code + * - 'account.bank.statement.line' fetch each line data + * + * @param {Object} context + * @param {number[]} context.statement_ids + * @returns {Deferred} + */ + load: function (context) { + var self = this; + var statement_ids = context.statement_ids; + if (!statement_ids) { + return $.when(); + } + var def_statement = this.performModelRPC("account.bank.statement", 'reconciliation_widget_preprocess', [statement_ids]) + .then(function (statement) { + self.bank_statement_id = statement_ids.length === 1 ? {id: statement_ids[0], display_name: statement.statement_name} : false; + self.valuenow = 0; + self.valuemax = statement.st_lines_ids.length; + _.each(statement.st_lines_ids, function (id) { + self.lines[_.uniqueId('rline')] = { + 'id': id, + 'reconciled': false, + 'mode': 'inactive', + 'mv_lines': [], + 'offset': 0, + 'filter': "", + 'reconciliation_proposition': [], + 'reconcileModels': [] + }; + }); + }); + var def_reconcileModel = this.performRPC('/web/dataset/search_read', {model: 'account.reconcile.model', domain: []}).then(function (reconcileModels) { + self.reconcileModels = reconcileModels.records; + }); + var def_account = this.performRPC('/web/dataset/search_read', {model: 'account.account', domain: [], fields: ['code']}).then(function (accounts) { + self.accounts = _.object(_.pluck(accounts.records, 'id'), _.pluck(accounts.records, 'code')); + }); + return $.when(def_statement, def_reconcileModel, def_account).then(function () { + _.each(self.lines, function (line) { + line.reconcileModels = self.reconcileModels; + }); + var ids = _.pluck(self.lines, 'id'); + return self.performModelRPC("account.bank.statement.line", 'get_data_for_reconciliation_widget', [ids]) + .then(self._formatLine.bind(self)); + }); + }, + /** + * Add lines into the propositions from the reconcile model + * Can add 2 lines, and each with its taxes. The second line become editable + * in the create mode. + * + * @see 'updateProposition' method for more informations about the + * 'amount_type' + * + * @param {string} handle + * @param {integer} reconcile model id + * @returns {Deferred} + */ + quickCreateProposition: function (handle, reconcileModelId) { + var line = this.getLine(handle); + var reconcileModel = _.find(this.reconcileModels, function (r) {return r.id === reconcileModelId;}); + var fields = ['account_id', 'amount', 'amount_type', 'analytic_account_id', 'journal_id', 'label', 'tax_id']; + this._blurProposition(handle); + + var focus = this._formatQuickCreate(line, _.pick(reconcileModel, fields)); + focus.reconcileModelId = reconcileModelId; + line.reconciliation_proposition.push(focus); + + if (reconcileModel.has_second_line) { + var second = {}; + _.each(fields, function (key) { + second[key] = ("second_"+key) in reconcileModel ? reconcileModel["second_"+key] : reconcileModel[key]; + }); + focus = this._formatQuickCreate(line, second); + focus.reconcileModelId = reconcileModelId; + line.reconciliation_proposition.push(focus); + this._computeReconcileModels(handle, reconcileModelId); + } + line.createForm = _.pick(focus, this.quickCreateFields); + return this._computeLine(line); + }, + /** + * Remove a proposition and switch to an active mode ('create' or 'match') + * + * @param {string} handle + * @param {number} proposition id (move line id) + * @returns {Deferred} + */ + removeProposition: function (handle, id) { + var self = this; + var line = this.getLine(handle); + var prop = _.find(line.reconciliation_proposition, {'id' : id}); + line.reconciliation_proposition = _.filter(line.reconciliation_proposition, function (p) { + return p.id !== prop.id && p.id !== prop.link && p.link !== prop.id && (!p.link || p.link !== prop.link); + }); + line.mode = isNaN(id) && !this.avoidCreate ? 'create' : 'match'; + var def = this._computeLine(line); + if (line.mode === 'create') { + return def.then(function () { + return self.createProposition(handle); + }); + } else if (line.mode === 'match') { + return $.when(def, self._performMoveLine(handle)); + } + return def; + }, + /** + * Force the partial reconciliation to display the reconciliate button. + * This method should only be called when there is onely one proposition. + * + * @param {string} handle + * @returns {Deferred} + */ + togglePartialReconcile: function (handle) { + var line = this.getLine(handle); + var prop = _.find(line.reconciliation_proposition, {'invalid': false}); + prop.partial_reconcile = !prop.partial_reconcile; + return this._computeLine(line); + }, + /** + * Change the value of the editable proposition line or create a new one. + * + * If the editable line comes from a reconcile model with 2 lines + * and their 'amount_type' is "percent" + * and their total equals 100% (this doesn't take into account the taxes + * who can be included or not) + * Then the total is recomputed to have 100%. + * + * @param {string} handle + * @param {number[]} context.statement_ids + * @returns {Deferred} + */ + updateProposition: function (handle, values) { + var line = this.getLine(handle); + var prop = _.last(_.filter(line.reconciliation_proposition, '__focus')); + if (!prop) { + prop = this._formatQuickCreate(line); + line.reconciliation_proposition.push(prop); + } + _.each(values, function (value, fieldName) { + prop[fieldName] = values[fieldName]; + }); + if ('account_id' in values) { + prop.account_code = prop.account_id ? this.accounts[prop.account_id.id] : ''; + } + if ('amount' in values) { + prop.base_amount = values.amount; + if (prop.reconcileModelId) { + this._computeReconcileModels(handle, prop.reconcileModelId); + } + } + if ('account_id' in values || 'amount' in values || 'tax_id' in values) { + prop.__tax_to_recompute = true; + } + line.createForm = _.pick(prop, this.quickCreateFields); + return this._computeLine(line); + }, + /** + * Format the value and send it to 'account.bank.statement.line' model + * Update the number of validated lines + * + * @param {(string|string[])} handle + * @returns {Deferred} resolved with an object who contains + * 'handles' key + */ + validate: function (handle) { + var self = this; + var handles = []; + if (handle) { + handles = [handle]; + } else { + _.each(this.lines, function (line, handle) { + if (!line.reconciled && !line.balance.amount && line.reconciliation_proposition.length) { + handles.push(handle); + } + }); + } + var ids = []; + var values = []; + _.each(handles, function (handle) { + var line = self.getLine(handle); + var props = _.filter(line.reconciliation_proposition, function (prop) {return !prop.is_tax && !prop.invalid;}); + ids.push(line.id); + values.push({ + "partner_id": line.st_line.partner_id, + "counterpart_aml_dicts": _.map(_.filter(props, function (prop) {return !isNaN(prop.id) && !prop.already_paid;}), self._formatToProcessReconciliation), + "payment_aml_ids": _.map(_.filter(props, function (prop) {return !isNaN(prop.id) && prop.already_paid;}), self._formatToProcessReconciliation), + "new_aml_dicts": _.map(_.filter(props, function (prop) {return isNaN(prop.id);}), self._formatToProcessReconciliation), + }); + line.reconciled = true; + self.valuenow++; + }); + + return this.performModelRPC("account.bank.statement.line", 'process_reconciliations', [ids, values]).then(function () { + return {handles: handles}; + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * stop the editable proposition line and remove it if it's invalid then + * compute the line + * + * @see '_computeLine' + * + * @private + * @param {string} handle + * @returns {Deferred} + */ + _blurProposition: function (handle) { + var line = this.getLine(handle); + line.reconciliation_proposition = _.filter(line.reconciliation_proposition, function (l) { + l.__focus = false; + return !l.invalid; + }); + return this._computeLine(line); + }, + /** + * Calculates the balance; format each proposition amount_str and mark as + * invalid the line with empty account_id, amount or label + * Check the taxes server side for each updated propositions with tax_id + * + * @private + * @param {Object} + * @returns {Deferred} + */ + _computeLine: function (line) { + //balance_type + var self = this; + + // compute taxes + var tax_defs = []; + var reconciliation_proposition = []; + var format_options = { + currency_id: line.st_line.currency_id, + }; + _.each(line.reconciliation_proposition, function (prop) { + if (prop.is_tax) { + if (!_.find(line.reconciliation_proposition, {'id': prop.link}).__tax_to_recompute) { + reconciliation_proposition.push(prop); + } + return; + } + reconciliation_proposition.push(prop); + + if (prop.tax_id && prop.__tax_to_recompute && prop.base_amount) { + line.reconciliation_proposition = _.filter(line.reconciliation_proposition, function (p) { + return !p.is_tax || p.link !== prop.id; + }); + + var args = [[prop.tax_id.id], prop.base_amount, line.st_line.currency_id]; + tax_defs.push(self.performModelRPC("account.tax", 'json_friendly_compute_all', args).then(function (result) { + var tax = result.taxes[0]; + var tax_prop = self._formatQuickCreate(line, { + 'link': prop.id, + 'tax_id': tax.id, + 'amount': tax.amount, + 'label': tax.name, + 'account_id': tax.account_id ? [tax.account_id, null] : prop.account_id, + 'analytic': tax.analytic, + 'is_tax': true, + '__focus': false + }); + + prop.amount = tax.base; + prop.amount_str = field_utils.format_monetary(Math.abs(prop.amount), {}, format_options); + prop.invalid = !self._isValid(prop); + + tax_prop.amount_str = field_utils.format_monetary(Math.abs(tax_prop.amount), {}, format_options); + tax_prop.invalid = prop.invalid; + + reconciliation_proposition.push(tax_prop); + })); + } else { + var currencyData = { + currency_id: line.st_line.currency_id + }; + + prop.amount_str = field_utils.format_monetary(Math.abs(prop.amount), {}, currencyData); + prop.display = self._isDisplay(prop); + prop.invalid = !self._isValid(prop); + } + }); + + return $.when.apply($, tax_defs).then(function () { + _.each(reconciliation_proposition, function (prop) { + prop.__tax_to_recompute = false; + }); + line.reconciliation_proposition = reconciliation_proposition; + + var total = line.st_line.amount || 0; + _.each(line.reconciliation_proposition, function (prop) { + if (!prop.invalid) { + total -= prop.amount; + } + }); + total = Math.round(total*1000)/1000 || 0; + line.balance = { + 'amount': total, + 'amount_str': field_utils.format_monetary(Math.abs(total), {}, format_options), + 'account_code': self.accounts[line.st_line.open_balance_account_id], + }; + line.balance.type = line.balance.amount ? (line.balance.amount > 0 && line.st_line.partner_id ? 0 : -1) : 1; + + if (line.balance.type === -1) { + var props = _.filter(line.reconciliation_proposition, {'invalid': false}); + if (props.length === 1 && props[0].partial_reconcile) { + line.balance.type = 0; + } + } + }); + }, + /** + * + * + * @private + * @param {string} handle + * @param {integer} reconcile model id + */ + _computeReconcileModels: function (handle, reconcileModelId) { + var line = this.getLine(handle); + // if quick create with 2 lines who use 100%, change the both values in same time + var props = _.filter(line.reconciliation_proposition, {'reconcileModelId': reconcileModelId, '__focus': true}); + if (props.length === 2 && props[0].percent && props[1].percent) { + if (props[0].percent + props[1].percent === 100) { + props[0].base_amount = props[0].amount = line.st_line.amount - props[1].base_amount; + props[0].__tax_to_recompute = true; + } + } + }, + /** + * format a name_get into an object {id, display_name}, idempotent + * + * @private + * @param {Object|Array} data or name_get + * @param {Object|false} {id, display_name} + */ + _formatNameGet: function (value) { + return value ? (value.id ? value : {'id': value[0], 'display_name': value[1]}) : false; + }, + /** + * Format each propositions (amount, label, account_id) + * + * @private + * @param {Object} + */ + _formatLineProposition: function (line, props) { + var self = this; + if (props.length) { + _.each(props, function (prop) { + prop.amount = prop.debit || -prop.credit; + prop.label = prop.name; + prop.account_id = self._formatNameGet(prop.account_id || line.account_id); + }); + } + }, + /** + * Format each server lines and propositions and compute all lines + * + * @see '_computeLine' + * + * @private + * @param {Object[]} + * @returns {Deferred} + */ + _formatLine: function (lines) { + var self = this; + var defs = []; + _.each(lines, function (data) { + var line = _.find(self.lines, function (l) { + return l.id === data.st_line.id; + }); + _.extend(line, data); + self._formatLineProposition(line, line.reconciliation_proposition); + if (!line.reconciliation_proposition.length) { + delete line.reconciliation_proposition; + } + defs.push(self._computeLine(line)); + }); + return $.when.apply($, defs); + }, + /** + * Format the server value then compute the line + * + * @see '_computeLine' + * + * @private + * @param {string} handle + * @param {Object[]} + * @returns {Deferred} + */ + _formatMoveLine: function (handle, mv_lines) { + var self = this; + var line = this.getLine(handle); + _.extend(line, {'mv_lines': mv_lines}); + this._formatLineProposition(line, mv_lines); + if (line.mode !== 'create' && !mv_lines.length && !line.filter.length) { + line.mode = this.avoidCreate || !line.balance.amount ? 'inactive' : 'create'; + if (line.mode === 'create') { + return this._computeLine(line).then(function () { + return self.createProposition(handle); + }); + } + } else { + return this._computeLine(line); + } + }, + /** + * Apply default values for the proposition, format datas and format the + * base_amount with the decimal number from the currency + * + * @private + * @param {Object} + * @param {Object} + * @returns {Object} + */ + _formatQuickCreate: function (line, values) { + values = values || {}; + var account = this._formatNameGet(values.account_id); + var prop = { + 'id': _.uniqueId('createLine'), + 'label': values.label || line.st_line.name, + 'account_id': account, + 'account_code': account ? this.accounts[account.id] : '', + 'analytic_account_id': this._formatNameGet(values.analytic_account_id), + 'journal_id': this._formatNameGet(values.journal_id), + 'tax_id': this._formatNameGet(values.tax_id), + 'debit': 0, + 'credit': 0, + 'base_amount': values.amount_type !== "percentage" ? + (values.amount || line.balance.amount) : + line.st_line.amount * values.amount / 100, + 'percent': values.amount_type === "percentage" ? values.amount : null, + 'link': values.link, + 'display': true, + 'invalid': true, + '__tax_to_recompute': true, + 'is_tax': values.is_tax, + '__focus': '__focus' in values ? values.__focus : true, + }; + prop.base_amount = Math.sign(prop.base_amount) * + field_utils.parse_monetary( + field_utils.format_monetary(Math.abs(prop.base_amount), {}, line) + ); + prop.amount = prop.base_amount; + return prop; + }, + /** + * @private + * @param {object} prop + * @returns {Boolean} + */ + _isDisplay: function (prop) { + return !!prop.account_id; + }, + /** + * @private + * @param {object} prop + * @returns {Boolean} + */ + _isValid: function (prop) { + return prop.account_id && prop.amount && prop.label && !!prop.label.length; + }, + /** + * Fetch 'account.bank.statement.line' propositions. + * + * @see '_formatMoveLine' + * + * @private + * @param {string} handle + * @returns {Deferred} + */ + _performMoveLine: function (handle) { + var line = this.getLine(handle); + var excluded_ids = _.flatten(_.map(this.lines, function (line) { + return _.filter(_.pluck(line.reconciliation_proposition, 'id'), _.isNumber); + })); + var filter = line.filter || ""; + var offset = line.offset; + var limit = 6; + var args = [line.id, line.st_line.partner_id, excluded_ids, filter, offset, limit]; + return this.performModelRPC("account.bank.statement.line", 'get_move_lines_for_reconciliation_widget', args) + .then(this._formatMoveLine.bind(this, handle)); + }, + /** + * format the proposition to send information server side + * + * @private + * @param {object} proposition + * @returns {object} + */ + _formatToProcessReconciliation: function (prop) { + var result = { + 'name' : prop.label, + 'debit' : prop.amount > 0 ? prop.amount : 0, + 'credit' : prop.amount < 0 ? -prop.amount : 0, + 'account_id' : prop.account_id.id, + 'journal_id' : prop.journal_id && prop.journal_id.id, + }; + if (!isNaN(prop.id)) result.counterpart_aml_id = prop.id; + if (prop.analytic_account_id) result.analytic_account_id = prop.analytic_account_id.id; + if (prop.tax_id) result.tax_ids = [[4, prop.tax_id.id, null]]; + return result; + }, +}); + + +/** + * Model use to fetch, format and update 'account.move.line' and 'res.partner' + * datas allowing manual reconciliation + */ +var ManualModel = StatementModel.extend({ + quickCreateFields: ['account_id', 'journal_id', 'amount', 'analytic_account_id', 'label', 'tax_id'], + + /** + * load data from + * - 'account.move.line' fetch the lines to reconciliate + * - 'account.account' fetch all account code + * + * @param {Object} context + * @param {string} [context.mode] 'customers', 'suppliers' or 'accounts' + * @param {integer[]} [context.company_ids] + * @param {integer[]} [context.partner_ids] used for 'customers' and + * 'suppliers' mode + * @returns {Deferred} + */ + load: function (context) { + var self = this; + + var domain_account_id = []; + if (context && context.company_ids) { + domain_account_id.push(['company_id', 'in', context.company_ids]); + } + + var def_account = this.performRPC('/web/dataset/search_read', { + model: 'account.account', + domain: domain_account_id, + fields: ['code']}) + .then(function (accounts) { + self.account_ids = _.pluck(accounts.records, 'id'); + self.accounts = _.object(self.account_ids, _.pluck(accounts.records, 'code')); + }); + + return def_account.then(function () { + switch(context.mode) { + case 'customers': + case 'suppliers': + var mode = context.mode === 'customers' ? 'receivable' : 'payable'; + var args = ['partner', context.partner_ids || null, mode]; + return self.performModelRPC("account.move.line", 'get_data_for_manual_reconciliation', args ) + .then(function (result) { + var defs = _.map(result, self._formatLine.bind(self, context.mode)); + self.valuenow = 0; + self.valuemax = Object.keys(self.lines).length; + return $.when.apply($, defs); + }); + case 'accounts': + var args = ['account', self.account_ids]; + return self.performModelRPC("account.move.line", 'get_data_for_manual_reconciliation', args) + .then(function (result) { + var defs = _.map(result, self._formatLine.bind(self, 'accounts')); + self.valuenow = 0; + self.valuemax = Object.keys(self.lines).length; + return $.when.apply($, defs); + }); + default: + var partner_ids = context.partner_ids; + var account_ids = self.account_ids; + if (partner_ids && !account_ids) account_ids = []; + if (!partner_ids && account_ids) partner_ids = []; + account_ids = null; // TOFIX: REMOVE ME + partner_ids = null; // TOFIX: REMOVE ME + var args = [partner_ids, account_ids]; + return self.performModelRPC("account.move.line", 'get_data_for_manual_reconciliation_widget', args) + .then(function (result) { + var defs = _.map(result.accounts, self._formatLine.bind(self, 'accounts')); + defs = defs.concat(_.map(result.customers, self._formatLine.bind(self, 'customers'))); + defs = defs.concat(_.map(result.suppliers, self._formatLine.bind(self, 'suppliers'))); + self.valuenow = 0; + self.valuemax = Object.keys(self.lines).length; + return $.when.apply($, defs); + }); + } + }); + }, + /** + * Mark the account or the partner as reconciled + * + * @param {(string|string[])} handle + * @returns {Deferred} resolved with the handle array + */ + validate: function (handle) { + var self = this; + var handles = []; + if (handle) { + handles = [handle]; + } else { + _.each(this.lines, function (line, handle) { + if (!line.reconciled && !line.balance.amount && line.reconciliation_proposition.length) { + handles.push(handle); + } + }); + } + + var def = $.when(); + var process_reconciliations = []; + var reconciled = []; + _.each(handles, function (handle) { + var line = self.getLine(handle); + if(line.reconciled) { + return; + } + var props = line.reconciliation_proposition; + if (!props.length) { + self.valuenow++; + reconciled.push(handle); + line.reconciled = true; + process_reconciliations.push({ + id: line.type === 'accounts' ? line.account_id : line.partner_id, + type: line.type, + mv_line_ids: [], + new_mv_line_dicts: [], + }); + } else { + var mv_line_ids = _.pluck(_.filter(props, function (prop) {return !isNaN(prop.id);}), 'id'); + var new_mv_line_dicts = _.map(_.filter(props, function (prop) {return isNaN(prop.id);}), self._formatToProcessReconciliation); + process_reconciliations.push({ + id: null, + type: null, + mv_line_ids: mv_line_ids, + new_mv_line_dicts: new_mv_line_dicts + }); + } + line.reconciliation_proposition = []; + }); + + if (process_reconciliations.length) { + def = self.performModelRPC("account.move.line", 'process_reconciliations', [process_reconciliations]); + } + + return def.then(function() { + var defs = []; + var account_ids = []; + var partner_ids = []; + _.each(handles, function (handle) { + var line = self.getLine(handle); + if (line.reconciled) { + return; + } + defs.push(self._performMoveLine(handle).then(function () { + if(!line.mv_lines.length) { + self.valuenow++; + reconciled.push(handle); + line.reconciled = true; + if (line.type === 'accounts') { + account_ids.push(line.account_id.id); + } else { + partner_ids.push(line.partner_id.id); + } + } + })); + }); + return $.when(defs).then(function() { + if (account_ids.length) { + self.performModelRPC("account.account", 'mark_as_reconciled', [account_ids]); + } + if (partner_ids.length) { + self.performModelRPC("res.partner", 'mark_as_reconciled', [partner_ids]); + } + return {reconciled: reconciled, updated: _.difference(handles, reconciled)}; + }); + }); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * override change the balance type to display or not the reconcile button + * + * @override + * @private + * @param {Object} + * @returns {Deferred} + */ + _computeLine: function (line) { + return this._super(line).then(function () { + var props = line.reconciliation_proposition; + line.balance.type = -1; + if (!line.balance.amount && props.length) { + line.balance.type = 1; + } else if(_.any(props, function (prop) {return prop.amount > 0;}) && + _.any(props, function (prop) {return prop.amount < 0;})) { + line.balance.type = 0; + } + }); + }, + /** + * Format each server lines and propositions and compute all lines + * + * @see '_computeLine' + * + * @private + * @param {string} 'customers', 'suppliers' or 'accounts' + * @param {Object} + * @returns {Deferred} + */ + _formatLine: function (type, data) { + var line = this.lines[_.uniqueId('rline')] = _.extend(data, { + 'type': type, + 'reconciled': false, + 'mode': 'inactive', + 'offset': 0, + 'filter': "", + 'reconcileModels': [], + 'account_id': this._formatNameGet([data.account_id, data.account_name]), + 'st_line': data + }); + this._formatLineProposition(line, line.reconciliation_proposition); + if (!line.reconciliation_proposition.length) { + delete line.reconciliation_proposition; + } + return this._computeLine(line); + }, + /** + * override to add journal_id + * + * @override + * @private + * @param {Object} + * @param {Object} + */ + _formatLineProposition: function (line, props) { + var self = this; + this._super(line, props); + if (props.length) { + _.each(props, function (prop) { + prop.journal_id = self._formatNameGet(prop.journal_id || line.journal_id); + }); + } + }, + /** + * @override + * @param {object} prop + * @returns {Boolean} + */ + _isDisplay: function (prop) { + return !!prop.journal_id && this._super(prop); + }, + /** + * @override + * @param {object} prop + * @returns {Boolean} + */ + _isValid: function (prop) { + return prop.journal_id && this._super(prop); + }, + /** + * Fetch 'account.move.line' propositions. + * + * @see '_formatMoveLine' + * + * @override + * @private + * @param {string} handle + * @returns {Deferred} + */ + _performMoveLine: function (handle) { + var line = this.getLine(handle); + var excluded_ids = _.flatten(_.map(this.lines, function (line) { + return _.filter(_.pluck(line.reconciliation_proposition, 'id'), _.isNumber); + })); + var filter = line.filter || ""; + var offset = line.offset; + var limit = 6; + var args = [line.account_id.id, line.partner_id, excluded_ids, filter, offset, limit]; + return this.performModelRPC("account.move.line", 'get_move_lines_for_manual_reconciliation', args) + .then(this._formatMoveLine.bind(this, handle)); + }, +}); + +return { + StatementModel: StatementModel, + ManualModel: ManualModel, +}; +}); \ No newline at end of file diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js new file mode 100644 index 00000000000..3887bb1e737 --- /dev/null +++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js @@ -0,0 +1,631 @@ +odoo.define('account.ReconciliationRenderer', function (require) { +"use strict"; + +var Widget = require('web.Widget'); +var FieldManagerMixin = require('web.FieldManagerMixin'); +var relational_fields = require('web.relational_fields'); +var basic_fields = require('web.basic_fields'); +var core = require('web.core'); +var time = require('web.time'); +var qweb = core.qweb; +var _t = core._t; + + +/** + * rendering of the bank statement action contains progress bar, title and + * auto reconciliation button + */ +var StatementRenderer = Widget.extend(FieldManagerMixin, { + template: 'reconciliation.statement', + events: { + 'click div:first button.o_automatic_reconciliation': '_onAutoReconciliation', + 'click div:first h1.statement_name': '_onClickStatementName', + 'click div:first h1.statement_name_edition button': '_onValidateName', + "click *[rel='do_action']": "_onDoAction", + }, + /** + * @override + */ + init: function (parent, model, state) { + this._super(parent); + this.model = model; + this._initialState = state; + }, + /** + * display iniial state and create the name statement field + * + * @override + */ + start: function () { + this._super(); + this.time = Date.now(); + this.$progress = this.$('.progress'); + + if (this._initialState.bank_statement_id) { + this.handleNameRecord = this.model.makeRecord("account.bank.statement", [{ + type: 'char', + name: 'name', + attrs: {string: ""}, + value: this._initialState.bank_statement_id.display_name + }]); + this.name = new basic_fields.FieldChar(this, + 'name', this.model.get(this.handleNameRecord), + {mode: 'edit', required: true}); + + this.name.appendTo(this.$('.statement_name_edition')); + this.$('.statement_name').text(this._initialState.bank_statement_id.display_name); + } + + this.$('h1.statement_name').text(this._initialState.title); + + delete this._initialState; + + this.enterHandler = function (e) { + if ((e.which === 13 || e.which === 10) && (e.ctrlKey || e.metaKey)) { + this.trigger_up('validate_all_balanced'); + } + }.bind(this); + $('body').on('keyup', this.enterHandler); + }, + /** + * @override + */ + destroy: function () { + this._super(); + $('body').off('keyup', this.enterHandler); + }, + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + /** + * update the statement rendering + * + * @param {object} state - statement data + * @param {integer} state.valuenow - for the progress bar + * @param {integer} state.valuemax - for the progress bar + * @param {string} state.title - for the progress bar + * @param {[object]} [state.notifications] + */ + update: function (state) { + this.$progress.find('.valuenow').text(state.valuenow); + this.$progress.find('.valuemax').text(state.valuemax); + this.$progress.find('.progress-bar') + .attr('aria-valuenow', state.valuenow) + .attr('aria-valuemax', state.valuemax) + .css('width', (state.valuenow/state.valuemax*100) + '%'); + + if (state.valuenow === state.valuemax) { + var dt = Date.now()-this.time; + var $done = $(qweb.render("reconciliation.done", { + 'duration': moment(dt).utc().format(time.strftime_to_moment_format(_t.database.parameters.time_format)), + 'number': state.valuenow, + 'timePerTransaction': Math.round(dt/1000/state.valuemax) + })); + $done.appendTo(this.$el.first()); + this.$('.o_automatic_reconciliation').hide(); + } + + if (state.notifications) { + this._renderNotifications(state.notifications); + } + + this.$('h1.statement_name').text(state.title); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + /** + * render the notifications + * + * @param {[object]} notifications + */ + _renderNotifications: function(notifications) { + for (var i=0; i= 0); + + // parnter_id + var record = this._makePartnerRecord(state.st_line.partner_id, state.st_line.partner_name); + this.fields.partner_id.reset(record); + this.$el.attr('data-partner', state.st_line.partner_id); + + // mode + this.$('.create, .match').each(function () { + var $panel = $(this); + $panel.css('-webkit-transition', 'none'); + $panel.css('-moz-transition', 'none'); + $panel.css('-o-transition', 'none'); + $panel.css('transition', 'none'); + $panel.css('max-height', $panel.height()); + $panel.css('-webkit-transition', ''); + $panel.css('-moz-transition', ''); + $panel.css('-o-transition', ''); + $panel.css('transition', ''); + }); + this.$el.data('mode', state.mode).attr('data-mode', state.mode); + this.$('.create, .match').each(function () { + $(this).removeAttr('style'); + }); + + // reconciliation_proposition + var $props = this.$('.accounting_view tbody').empty(); + var props = _.filter(state.reconciliation_proposition, {'display': true}); + _.each(props, function (line) { + var $line = $(qweb.render("reconciliation.line.mv_line", {'line': line, 'state': state})); + if (!isNaN(line.id)) { + $('') + .appendTo($line.find('.cell_info_popover')) + .attr("data-content", qweb.render('reconciliation.line.mv_line.details', {'line': line})); + } + + if (state.balance.amount < 0 && props.length === 1) { + var text = !line.partial_reconcile ? + _t("This move's amount is higher than the transaction's amount. Click to register a partial payment and keep the payment balance open."): + _t("Undo the partial reconciliation."); + $('') + .prependTo($line.find('.cell_right')) + .attr("data-content", text); + } + $props.append($line); + }); + + // mv_lines + var $mv_lines = this.$('.match table tbody').empty(); + _.each(state.mv_lines.slice(0,5), function (line) { + var $line = $(qweb.render("reconciliation.line.mv_line", {'line': line, 'state': state})); + if (!isNaN(line.id)) { + $('') + .appendTo($line.find('.cell_info_popover')) + .attr("data-content", qweb.render('reconciliation.line.mv_line.details', {'line': line})); + } + $mv_lines.append($line); + }); + this.$('.match .fa-chevron-right').toggleClass('disabled', state.mv_lines.length <= 5); + this.$('.match .fa-chevron-left').toggleClass('disabled', !state.offset); + this.$('.match').css('max-height', !state.mv_lines.length && !state.filter.length ? '0px' : ''); + + // balance + this.$('table tfoot').html(qweb.render("reconciliation.line.balance", {'state': state})); + + // create form + if (state.createForm) { + if (!this.fields.account_id) { + this._renderCreate(state); + } + var data = this.model.get(this.handleCreateRecord).data; + this.model.notifyChanges(this.handleCreateRecord, state.createForm); + record = this.model.get(this.handleCreateRecord); + _.each(this.fields, function (field, fieldName) { + if (fieldName === "partner_id") return; + if ((data[fieldName] || state.createForm[fieldName]) && !_.isEqual(state.createForm[fieldName], data[fieldName])) { + field.reset(record); + } + }); + } + this.$('.create .add_line').toggle(!!state.balance.amount); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @param {integer} partnerID + * @param {string} partnerName + * @returns {object} dataPoint + */ + _makePartnerRecord: function (partnerID, partnerName) { + var recordID = this.model.makeRecord('account.bank.statement.line', [{ + relation: 'res.partner', + type: 'many2one', + name: 'partner_id', + value: [partnerID, partnerName] + }]); + return this.model.get(recordID); + }, + + /** + * create account_id, tax_id, analytic_account_id, label and amount field + * + * @private + * @param {object} state - statement line + */ + _renderCreate: function (state) { + this.handleCreateRecord = this.model.makeRecord('account.bank.statement.line', [{ + relation: 'account.account', + type: 'many2one', + name: 'account_id', + }, { + relation: 'account.journal', + type: 'many2one', + name: 'journal_id', + }, { + relation: 'account.tax', + type: 'many2one', + name: 'tax_id', + }, { + relation: 'account.analytic.account', + type: 'many2one', + name: 'analytic_account_id', + }, { + type: 'char', + name: 'label', + attrs: {string: ""} + }, { + type: 'float', + name: 'amount', + attrs: {string: ""}, + }]); + var record = this.model.get(this.handleCreateRecord); + + this.fields.account_id = new relational_fields.FieldMany2One(this, + 'account_id', record, {mode: 'edit', required: true}); + + this.fields.journal_id = new relational_fields.FieldMany2One(this, + 'journal_id', record, {mode: 'edit', required: true}); + + this.fields.tax_id = new relational_fields.FieldMany2One(this, + 'tax_id', record, {mode: 'edit'}); + + this.fields.analytic_account_id = new relational_fields.FieldMany2One(this, + 'analytic_account_id', record, {mode: 'edit'}); + + this.fields.label = new basic_fields.FieldChar(this, + 'label', record, {mode: 'edit', required: true}); + + this.fields.amount = new basic_fields.FieldFloat(this, + 'amount', record, {mode: 'edit', required: true}); + + var $create = $(qweb.render("reconciliation.line.create", {'state': state})); + this.fields.account_id.appendTo($create.find('.create_account_id .o_td_field')); + this.fields.journal_id.appendTo($create.find('.create_journal_id .o_td_field')); + this.fields.tax_id.appendTo($create.find('.create_tax_id .o_td_field')); + this.fields.analytic_account_id.appendTo($create.find('.create_analytic_account_id .o_td_field')); + this.fields.label.appendTo($create.find('.create_label .o_td_field')); + this.fields.amount.appendTo($create.find('.create_amount .o_td_field')); + this.$('.create').append($create); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {OdooEvent} event + */ + _onFieldChanged: function (event) { + event.stopPropagation(); + var fieldName = event.target.name; + if (fieldName === 'partner_id') { + var partner_id = event.data.changes.partner_id; + this.trigger_up('change_partner', {'data': partner_id}); + } else { + if (event.data.changes.amount && isNaN(event.data.changes.amount)) { + return; + } + this.trigger_up('update_proposition', {'data': event.data.changes}); + } + }, + /** + * @private + */ + _onTogglePanel: function () { + var mode = this.$el.data('mode') === 'inactive' ? 'match' : 'inactive'; + this.trigger_up('change_mode', {'data': mode}); + }, + /** + * @private + */ + _onShowPanel: function () { + var mode = (this.$el.data('mode') === 'inactive' || this.$el.data('mode') === 'match') ? 'create' : 'match'; + this.trigger_up('change_mode', {'data': mode}); + }, + /** + * @private + */ + _onFilterChange: function () { + this.trigger_up('change_filter', {'data': _.str.strip($(event.target).val())}); + }, + /** + * @private + */ + _onPrevious: function () { + this.trigger_up('change_offset', {'data': -5}); + }, + /** + * @private + */ + _onNext: function () { + this.trigger_up('change_offset', {'data': 5}); + }, + /** + * @private + * @param {MouseEvent} event + */ + _onSelectMoveLine: function (event) { + var mv_line_id = $(event.target).closest('.mv_line').data('line-id'); + this.trigger_up('add_proposition', {'data': mv_line_id}); + }, + /** + * @private + * @param {MouseEvent} event + */ + _onSelectProposition: function (event) { + var mv_line_id = $(event.target).closest('.mv_line').data('line-id'); + this.trigger_up('remove_proposition', {'data': mv_line_id}); + }, + /** + * @private + * @param {MouseEvent} event + */ + _onQuickCreateProposition: function (event) { + document.activeElement && document.activeElement.blur(); + this.trigger_up('quick_create_proposition', {'data': $(event.target).data('reconcile-model-id')}); + }, + /** + * @private + */ + _onCreateProposition: function () { + document.activeElement && document.activeElement.blur(); + this.trigger_up('create_proposition'); + }, + /** + * @private + */ + _onValidate: function () { + this.trigger_up('validate'); + }, + /** + * @private + */ + _onTogglePartialReconcile: function (e) { + e.stopPropagation(); + var popover = $(e.target).data('bs.popover'); + popover && popover.destroy(); + this.trigger_up('toggle_partial_reconcile'); + } +}); + + +/** + * rendering of the manual reconciliation action contains progress bar, title + * and auto reconciliation button + */ +var ManualRenderer = StatementRenderer.extend({ + template: "reconciliation.manual.statement", + + /** + * avoid statement name edition + * + * @override + * @private + */ + _onClickStatementName: function () {} +}); + + +/** + * rendering of the manual reconciliation, contains line data, proposition and + * view for 'match' mode + */ +var ManualLineRenderer = LineRenderer.extend({ + template: "reconciliation.manual.line", + + /** + * create partner_id field in readonly mode + * + * @override + */ + init: function (parent, model, state) { + this._super.apply(this, arguments); + if (state.partner_id) { + this.fields.partner_id = new relational_fields.FieldMany2One(this, + 'partner_id', + this._makePartnerRecord(state.partner_id, state.partner_name), + {mode: 'readonly'} + ); + } else { + this.fields.title_account_id = new relational_fields.FieldMany2One(this, + 'account_id', + this.model.get(this.model.makeRecord('account.move.line', [{ + relation: 'account.account', + type: 'many2one', + name: 'account_id', + value: [state.account_id.id, state.account_id.display_name] + }])), + {mode: 'readonly'} + ); + } + }, + /** + * @override + * @param {string} handle + * @param {number} proposition id (move line id) + * @returns {Deferred} + */ + removeProposition: function (handle, id) { + if (!id) { + return $.when(); + } + return this._super(handle, id); + }, + /** + * move the partner field + * + * @override + */ + start: function () { + this._super(); + if (!this.fields.title_account_id) { + this.fields.partner_id.$el.prependTo(this.$('.accounting_view thead td:eq(1) span:first')); + } else { + this.fields.partner_id.destroy(); + this.fields.title_account_id.appendTo(this.$('.accounting_view thead td:eq(1) span:first')); + } + }, + /** + * @override + */ + update: function (state) { + this._super(state); + var props = _.filter(state.reconciliation_proposition, {'display': true}); + if (!props.length) { + var $line = $(qweb.render("reconciliation.line.mv_line", {'line': {}, 'state': state})); + this.$('.accounting_view tbody').append($line); + } + }, + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + /** + * display journal_id field + * + * @override + */ + _renderCreate: function (state) { + this._super(state); + this.$('.create .create_journal_id').show(); + }, + +}); + + +return { + StatementRenderer: StatementRenderer, + ManualRenderer: ManualRenderer, + LineRenderer: LineRenderer, + ManualLineRenderer: ManualLineRenderer, +}; +}); diff --git a/addons/account/static/src/less/account_journal_dashboard.less b/addons/account/static/src/less/account_journal_dashboard.less index 564f39b4006..140395eb150 100644 --- a/addons/account/static/src/less/account_journal_dashboard.less +++ b/addons/account/static/src/less/account_journal_dashboard.less @@ -35,77 +35,8 @@ min-height: 120px; } - .o_kanban_graph_section { + .o_dashboard_graph { .o-position-absolute(auto, 0, 0, 0); - .o_graph_linechart { - > svg { - height: 75px; - - // X axis - g.nv-x.nv-axis { - transform: translateY(50px); - - // Remove max and min labels - g.nv-axisMaxMin { - display: none; - } - g.tick.major { - // Don't know why, but leads to strange bold text - // stroke: @odoo-brand-primary; - - line { - display: none; - } - } - } - - // !important are needed because nvd3 uses inline style on elements - - // Remove strokes - g.nv-linesWrap { - g.nv-group.nv-series-0 { - fill-opacity: 0.3 !important; - fill: @odoo-brand-primary !important; - stroke: @odoo-brand-primary !important; - - .nv-point { - visibility: hidden; - r: 3; - stroke: @odoo-brand-primary !important; - fill: white; - stroke-width: 2; - } - .nv-point:nth-child(5n+1) { - visibility: visible; - fill-opacity: .95 !important; - stroke-opacity: .95 !important; - } - } - } - } - } - .o_graph_barchart { - > svg { - height: 75px; - overflow: visible; - g.nv-barsWrap { - g.nv-group.nv-series-0 { - g.past { - opacity: 0.5; - fill: @odoo-brand-primary !important; - } - g.future { - opacity: 0.5; - fill: @odoo-brand-optional !important; - } - } - } - } - } - } - - .o_invisible .o_kanban_graph_section { - display: none; // This fix a bug on firefox } } @@ -113,3 +44,77 @@ min-width: inherit; } } + + +// Style for the widget "dashboard_graph" +.o_dashboard_graph { + &.o_graph_linechart { + > svg { + height: 75px; + + // X axis + g.nv-x.nv-axis { + transform: translateY(50px); + + // Remove max and min labels + g.nv-axisMaxMin { + display: none; + } + g.tick.major { + // Don't know why, but leads to strange bold text + // stroke: @odoo-brand-primary; + + line { + display: none; + } + } + } + + // !important are needed because nvd3 uses inline style on elements + + // Remove strokes + g.nv-linesWrap { + g.nv-group.nv-series-0 { + fill-opacity: 0.3 !important; + fill: @odoo-brand-primary !important; + stroke: @odoo-brand-primary !important; + + .nv-point { + visibility: hidden; + r: 3; + stroke: @odoo-brand-primary !important; + fill: white; + stroke-width: 2; + } + .nv-point:nth-child(5n+1) { + visibility: visible; + fill-opacity: .95 !important; + stroke-opacity: .95 !important; + } + } + } + } + } + &.o_graph_barchart { + > svg { + height: 75px; + overflow: visible; + g.nv-barsWrap { + g.nv-group.nv-series-0 { + g.past { + opacity: 0.5; + fill: @odoo-brand-primary !important; + } + g.future { + opacity: 0.5; + fill: @odoo-brand-optional !important; + } + } + } + } + } +} + +.o_invisible .o_dashboard_graph { + display: none; // This fix a bug on firefox +} diff --git a/addons/account/static/src/less/account_reconciliation.less b/addons/account/static/src/less/account_reconciliation.less index 841b1cc5896..5df97bb2e9f 100644 --- a/addons/account/static/src/less/account_reconciliation.less +++ b/addons/account/static/src/less/account_reconciliation.less @@ -4,641 +4,327 @@ @accountingBorder: 1px solid #000; @initialLineBackground: #f0f0f0; @infoColor: #44c; -// Warning, this value is also specified in the abstractReconciliation widget -@aestetic_animation_speed: 300ms; + .o_reconciliation { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - -o-user-select: none; - user-select: none; - cursor: default; - font-size: 12px; - - // Why on earth does bootstrap set font-size to 14px in popovers ? - .popover { - font-size: 12px; - } - - .oe_form_sheetbg { - padding-top: 0; - - .oe_form_sheet { - position: relative; - padding: 20px 15px 20px 15px; - border-top: 0; - height: 100%; - } - } - - .clearer { - clear: both; - } - - .oe_view_nocontent { - max-width: none; - - > p { - padding: 0 10%; - } - } - - h1 input, h2 input { - height: auto !important; - } - h1 { width: 48%; - padding: 0 0 0 @actionColWidth; + padding: 0 0 0 15px; margin: 0 0 25px 0; float: left; font-size: 2em; height: 1.5em; - } - - .statement_name span { - line-height: 1.5em; - cursor: pointer; - } - - .change_statement_name_container { - display: none; - width: 95%; - - td:first-child, td:first-child > input { - width: 99%; + &.statement_name_edition input { + width: 85%; } - td:last-child { - width: 1%; - padding-left: 3px; - - button { - height: 100%; - box-sizing: content-box; - } - } - } - - h2 { - font-size: 1.8em; } .progress { width: 49%; - margin: 10px @actionColWidth 0 0; + margin: 10px 15px 0 0; float: right; position: relative; display: inline-block; - + .progress-bar { + background-color: #7c7bad; + } .progress-text { text-align: center; position: absolute; width: 100%; - left: 0; - top: 1px; - z-index: 10; - text-shadow: - -1px -1px 0 #f5f5f5, - 1px -1px 0 #f5f5f5, - -1px 1px 0 #f5f5f5, - 1px 1px 0 #f5f5f5; - } - .progress-bar { - background-color: @odoo-brand-optional; - } - } - - .show_more_container { - text-align: center; - margin-bottom: 15px; - - .show_more { - display: none; - margin: auto; - } - } - .notification_area { - margin: 0 @actionColWidth; - .fa { - font-size: 20px; - margin-left: 7px; - line-height: 14px; - position: relative; - top: 3px; - } - } - - .interface_options { - margin-bottom: 25px; - padding-left: @actionColWidth; - - .oe_radio_input, .o_radio_input { - border: none; - display: inline; } } .done_message { + padding: 70px 20% 10px 20%; + text-align: center; + position: relative; + .fa { + position: absolute; + right: 20%; + } + } + + .notification_area { + clear: both; + } + + .o_view_nocontent { + max-width: none; + padding: 0 10%; + color: @odoo-main-color-muted; + font-size: 125%; + } + + .accounting_view { width: 100%; - padding: 0 20%; - margin-top: 50px; - margin-bottom: 50px; - h2 { - margin-bottom: 30px; - - .congrats_icon { - float: right; - font-size: 2em; - position: relative; - top: -0.25em; - } + .cell_left { + border-right: 1px solid #333; + padding-right: 5px; } - - .achievements { - margin-top: 30px; - text-align: center; - position: relative; - - .achievement { - font-size: 4em; - margin: 0 0.3em; - position: relative; - vertical-align: middle; - text-shadow: 2px 2px 0px rgba(0,0,0,0.2); - - i { - font-size: 0.5em; - color: white; - position: absolute; - top: 50%; - margin-top: -0.55em; - left: 0; - width: 100%; - text-align: center; - //text-shadow: 1px 1px 0 black; - } - } + .do_partial_reconcile_true { + color: #7c7bad; + padding-right: 5px; } - - .action_buttons { - text-align: center; + .do_partial_reconcile_false { + color: #f0eeee; + padding-right: 5px; } } - /* icons */ - .fa { - &.line_info_button { - color: #ccc !important; + .o_multi_currency { + margin-right: 5px; + &.o_multi_currency_color_0 { + color: #dd6666; + } + &.o_multi_currency_color_1 { + color: #aaaaaa; + } + &.o_multi_currency_color_2 { + color: #66dd66; + } + &.o_multi_currency_color_3 { + color: #6666dd; + } + &.o_multi_currency_color_4 { + color: #dddd66; + } + &.o_multi_currency_color_5 { + color: #dd66dd; + } + &.o_multi_currency_color_6 { + color: #66dddd; + } + &.o_multi_currency_color_7 { + color: #aaa333; } } - .accounting_view .fa-add-remove:before { - content: "\f068"; - } - .match .fa-add-remove:before { - content: "\f067"; - } - // reconciliationLine widget .o_reconciliation_line { margin-bottom: 30px; - table { width: 100%; + vertical-align: top; + } + tr { + cursor: pointer; + } + tr.already_reconciled { + color: @infoColor; + } + tr.invalid { + text-decoration: line-through; + } + thead td { + border-top: @lightBorder; + padding-top: 4px; + padding-bottom: 5px; + background-color: @initialLineBackground; + } + tfoot td { + color: #bbb; } - // modes : default + /* columns */ + + .cell_action { + width: 15px; + color: @gray; + background: #fff; + border: 0; + .fa-add-remove:before { + content: ""; + } + } + tr:hover .cell_action .fa-add-remove:before { + content: "\f068"; + } + .is_tax .cell_action .fa-add-remove:before { + position: relative; + top: -18px; + } + .cell_account_code { + width: 80px; + } + .cell_due_date { + width: 100px; + } + .cell_label { + width: auto; + } + .cell_left { + padding-right: 5px; + } + .cell_right, .cell_left { + text-align: right; + width: 120px; + } + .cell_info_popover { + text-align: right; + width: 15px; + color: #ccc; + background: #fff; + border: 0; + } + + /* info popover */ + + table.details { + vertical-align: top; + td:first-child { + vertical-align: top; + padding-right: 10px; + font-weight: bold; + } + } + + tr.one_line_info { + td { + padding-top: 10px; + text-align: center; + color: @infoColor; + } + } + + /* Icons */ + .toggle_match, .toggle_create { - -webkit-transition-property: -webkit-transform; - -moz-transition-property: -moz-transform; - -ms-transition-property: -ms-transform; - -o-transition-property: -o-transform; - transition-property: transform; - -webkit-transform: rotate(0deg); - -moz-transform: rotate(0deg); - -ms-transform: rotate(0deg); - -o-transform: rotate(0deg); - transform: rotate(0deg); - -webkit-transition-duration: @aestetic_animation_speed; - -moz-transition-duration: @aestetic_animation_speed; - -ms-transition-duration: @aestetic_animation_speed; - -o-transition-duration: @aestetic_animation_speed; - transition-duration: @aestetic_animation_speed; + .o-transform(rotate(0deg)); + .o-transition(transform, 300ms); } - .visible_toggle { visibility: visible !important; - -webkit-transform: rotate(90deg); - -moz-transform: rotate(90deg); - -ms-transform: rotate(90deg); - -o-transform: rotate(90deg); - transform: rotate(90deg); + .o-transform(rotate(90deg)); } - - .partner_name { - color: #444; - - .change_partner { - color: @odoo-brand-optional; - display: none; - cursor: pointer; - margin: 0 10px 0 5px; - } - &:hover .change_partner { - display: inline; - } - } - - .change_partner_container { - width: 200px; - display: none; - position: relative !important; - } - - // modes : specific &[data-mode="match"] { .toggle_match { .visible_toggle; } } - &[data-mode="create"] { .toggle_create { .visible_toggle; } - - &.no_partner { - .toggle_match { - .visible_toggle; - } - } } - &[data-mode="inactive"] { - .initial_line > td { - background-color: (@initialLineBackground + #080808) !important; - } + /* Match view & Create view */ + + > .match, > .create { + max-height: 0px; + overflow: hidden; + margin-top: 5px; + margin-bottom: 5px; + .o-transition(max-height, 250ms); } - - &.no_match:not(.no_partner) { - .initial_line { - cursor: default !important; - - .line_info_button { - cursor: pointer; - } - } - .toggle_match { - visibility: hidden !important; - } + &[data-mode="match"] > .match { + max-height: 130px; + .o-transition(max-height, 400ms); } - - /* gap between accounting_view and action view */ - > table > tbody > tr:nth-child(1) > td table { - margin-bottom: 10px; + &[data-mode="create"] > .create { + max-height: 210px; + .o-transition(max-height, 400ms); } + } + + /* Match view */ - /* popover */ - table.details { - td:first-child { - padding-right: 10px; - font-weight: bold; + .match { + > div { + padding: 1px; + border: 1px solid #eee; + border-radius: 5px; + } + .cell_action .fa-add-remove:before { + content: ""; + } + tr:hover .cell_action .fa-add-remove:before { + content: "\f067"; + } + .match_controls { + padding: 0 0 5px (@actionColWidth+@mainTableBordersPadding); + + .filter { + width: 240px; + display: inline-block; } - tr.one_line_info { - td { - padding-top: 10px; - text-align: center; - color: @infoColor; - } - } - } - - /* arrays of move lines */ - .accounting_view, .match table { - width: 100%; - - tbody tr { + .fa-chevron-left, .fa-chevron-right { + display: inline-block; cursor: pointer; } - tr.already_reconciled { - color: @infoColor; + .fa-chevron-left { + margin-right: 10px; } - td { - padding: 1px 8px; - vertical-align: middle; - } - - td.cell_action, td.cell_info_popover { - width: @actionColWidth; - padding: 0; - } - - td.cell_action { - text-align: left; - } - - td.cell_account_code { - width: 80px; - padding-left: @mainTableBordersPadding; - } - - td.cell_due_date { - width: 100px; - } - - td.cell_label { - - } - - td.cell_debit { - text-align: right; - width: 15%; - } - - td.cell_credit { - width: 15%; - text-align: right; - padding-right: @mainTableBordersPadding; - } - - td.cell_info_popover { - text-align: right; - } - - tr.line_open_balance { - color: #bbb; - } - - tr { - .toggle_create, .toggle_match, .fa-add-remove { - visibility: hidden; - color: #555; - } - - &:hover, &:active, &:focus { - .toggle_create, .toggle_match, .fa-add-remove { - visibility: visible; - } - } - } - - tr .do_partial_reconcile_button { - color: @odoo-brand-optional; - padding-right: 5px; - } - - tr .undo_partial_reconcile_button { - color: @odoo-brand-secondary; - padding-right: 5px; + .fa-chevron-left.disabled, .fa-chevron-right.disabled { + color: #ddd; + cursor: default; } } - - /* Partie infos */ - .accounting_view { - border-collapse: separate; - - .initial_line > td { - border-top: @lightBorder; - padding-top: 4px; - padding-bottom: 5px; - background-color: @initialLineBackground; - -webkit-transition-property: background-color; - -moz-transition-property: background-color; - -ms-transition-property: background-color; - transition-property: background-color; - -webkit-transition-duration: @aestetic_animation_speed; - -moz-transition-duration: @aestetic_animation_speed; - -ms-transition-duration: @aestetic_animation_speed; - -o-transition-duration: @aestetic_animation_speed; - transition-duration: @aestetic_animation_speed; - - &.cell_action, &.cell_info_popover { - border-top: none; - background: white !important; - // Hack pour l'alignement au px près - padding-top: 6px; - padding-bottom: 3px; - } - } - - caption { - text-align: left; - height: 26px; - margin: 0 @actionColWidth 4px @actionColWidth; - padding: 0; - - .partner_name { - font-size: 1.1em; - font-weight: bold; - } - - .tip_reconciliation_not_balanced { - color: red; - float: right; - padding-right: 7px; - } - - button { - float: right; - position: relative; - top: -4px; - - &:disabled { - opacity: 0.5; - } - } - - > span, > input { - position: relative; top: 7px; /* meh */ - } - } - - // accounting "T" - td.cell_credit { border-left: @accountingBorder; } + .show_more { + display: inline-block; + margin-left: (@actionColWidth+@mainTableBordersPadding); + margin-top: 5px; } + } - - /* Match view */ - .match { - display: none; - padding: 1px; - .match_controls { - padding: 0 0 5px (@actionColWidth+@mainTableBordersPadding); - - .filter { - width: 240px; - display: inline-block; - } - - .pager_control_left, .pager_control_right { - display: inline-block; - cursor: pointer; - } - - .pager_control_left { - margin-right: 10px; - } - - .pager_control_left.disabled, .pager_control_right.disabled { - color: #ddd; - cursor: default; - } - } - - .show_more { - display: inline-block; - margin-left: (@actionColWidth+@mainTableBordersPadding); - margin-top: 5px; - } - } - - - /* Action create */ - .create { - display: none; + /* Create view */ + .create { + > div { margin: 0 @actionColWidth; - border: 1px solid #d5d5d5; - border-radius: 5px; padding: 10px; overflow: auto; - - .quick_add { - position: relative; - margin-bottom: 10px; - clear: both; - - &:empty { - display: none; - } - - .presets_config { - display: none; - position: absolute; - top: 0; - right: 0; - z-index: 100; - - .dropdown-menu { - font-size: 1em; // Since there's no dropdown-menu-sm class in bootstrap - } - } - - &:hover .presets_config, .presets_config.open { - display: inline; - } - } - - .o_form > table { - width: 49%; - margin: 0; - vertical-align: top; - - &.create_group_left { float: left; } - &.create_group_right { float: right; } - - td { - width: 99%; - padding-left: 8px; - - .oe_m2o_cm_button { - line-height: 22px; - } - } - - td.o_td_label { - line-height: 26px; - font-weight: bold; - padding-right: 8px; - min-width: 100px; - border-right: 1px solid #ddd; - white-space: nowrap; - width: 1%; - } - - input, select { - width: 100%; - } - - } - - .add_line_container { - clear: both; - line-height: 26px; - text-align: center; - margin: 0; - padding-top: 7px; - } + border: 1px solid #ddd; + border-radius: 5px; + } + table { + width: 49%; + margin: 0; + vertical-align: top; + } + td { + width: 99%; + padding-left: 8px; + } + tr .o_td_label { + vertical-align: top; + line-height: 26px; + font-weight: bold; + padding-right: 8px; + min-width: 100px; + border-right: 1px solid #ddd; + white-space: nowrap; + width: 1%; + } + .btn { + padding-top: 0; + padding-bottom: 0; + } + .add_line_container { + text-align: center; + clear: both; + color: @infoColor; + cursor: pointer; } } } -.o_manual_reconciliation { - .o_manual_reconciliation_line { - .button_reconcile { - float: right; - position: relative; - top: 1.1em; +/*Manual Reconciliation*/ +.o_manual_statement { + .accounting_view { + td[colspan="3"] span:first-child { + width: 50%; + display: inline-block; } - - .accounting_view { - thead { - background: none; - - td { - vertical-align: bottom !important; - } - - td:nth-child(2) { - padding-left: 0; - } - - .cell_item_name { - display: inline-block; - min-width: 135px; - } - - .cell_last_time_entries_checked { - font-weight: normal; - display: inline-block; - } - .cell_item_name + .cell_last_time_entries_checked { - padding-left: 20px; - } - - .cell_title_account_code { - text-align: center; - } - - tr:first-child td { - border-bottom: @lightBorder; - - &.cell_action, &.cell_info_popover { - border-bottom: none; - } - - &.cell_title_account_code { - border-bottom: 1px solid #000 !important; - } - } - - } - - .filler_line td { - height: 22px; - cursor: default !important; - } - - tbody.tbody_matched_lines tr:first-child td { - padding-top: 4px; - } + td[colspan="2"] { + border-bottom: 1px solid #333; + text-align: center; + width: 240px; } - - td.cell_due_date { - padding-left: @mainTableBordersPadding !important; + .do_partial_reconcile_true { + display: none; } } } diff --git a/addons/account/static/src/xml/account_reconciliation.xml b/addons/account/static/src/xml/account_reconciliation.xml index 6796c7fb2f6..7949bb7fde5 100644 --- a/addons/account/static/src/xml/account_reconciliation.xml +++ b/addons/account/static/src/xml/account_reconciliation.xml @@ -2,369 +2,248 @@ - +
+
+
+
+
+
+
- -
-

-
-
0 /
-
+ +
+
+

+

+
+
+ /
+
- -
-
-
-
-
-

Tip: Hit CTRL-Enter to reconcile all the balanced items in the sheet.

-
- + +
+
+
+
Tip: Hit CTRL-Enter to reconcile all the balanced items in the sheet.
+
+
+

Good Job! Bank statements are fully reconciled.

+

This page displays all the bank transactions that are to be reconciled and provides with a neat interface to do so.

+
+ - -
-
-
-
+ + + o_manual_statement + + + +

Good Job! There is nothing to reconcile.

+

All invoices and payments have been matched, your accounts' balances are clean.

+
+
- -
-

-

+
+ +

Congrats, you're all done !

+

It took you to reconcile transactions.
That's on average seconds per transaction.

+
+ + + +
+ + + + + + + + + + + + + + + + + + + +
+
+ + + Select a partner or choose a counterpart +
+
()
+
+ +
+
+
+
+ + + +
+ + +
+ + + + + + + + + + Last Reconciliation: + + + + + - -
- - - - - + + + + + + Create Write-offOpen balanceChoose counterpart + + + + + + + +
+
+ + + +
+ + + +
+
+ + +
+
+
+ +
- - - - - - - - - - -
- - - - - -
-
-
-
- - - -
-
-
-
-
-

You did not configure any reconcile model yet, you can do it there.

-
-
-
- -
-
-
-
-

Save and New

-
-
-
-
- - - - - - -
- -
+ + + + - - - - - - - - - - - - - - - + + + - - - -
No result matching ''
Tax
- - - - - - - - - - + + + + +
Account
Journal
Date
Due Date
Total - () -
ResidualAmount - () -
This payment is registered but not reconciled.
+ + + + + -
-
+ + Label + + + + Amount + + + + + +
- - - - - - - - - + + + + + + + : : + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - +
Account
Journal
Label
Ref
Partner
Date
Due Date
ResidualAmount + () +
This payment is registered but not reconciled.
+
- + + + + + + + + + +
Date
Partner
Transaction
Description
Amount ()
Account
Note
+
- - - -

- - - - -
-

-
- -

-
+ +
+ + + + + - - - - -

Good Job! Bank statements are fully reconciled.

-

This page displays all the bank transactions that are to be reconciled and provides with a neat interface to do so.

-
-
- - - - -

It took you to reconcile transactions.
That's on average seconds per transaction.

- -

You validated reconciliations with the ctrl-enter shortcut.

-
-

- - - -

-
-
- - - - - - -
- - - - - Select a partner or choose a counterpart - - - - -
-
- - - - - - - - () - - - - - - - - - - - - - - - - - - - -
Date
Partner
Transaction
Description
Amount ()
Account
Note
-
- - - This move's amount is higher than the transaction's amount. Click to register a partial payment and keep the payment balance open. - - - - - Undo the partial reconciliation. - - - - - - -
- - -
-
- - - - - - - - -
- - - - - customersCustomers - suppliersVendors - othersOtherAccounts - allAll - - -
-
-
- - - -

Good Job! There is nothing to reconcile.

-

All invoices and payments have been matched, your accounts' balances are clean.

-
-
- - -
-

Good Job! There is nothing to reconcile.

-

All invoices and payments have been matched, your accounts' balances are clean.

-
-
- - - - - - - - - - - Last Reconciliation : - - - - - - - - - - - - - - - - - - - +
+
diff --git a/addons/account/static/test/account_reconciliation.js b/addons/account/static/test/account_reconciliation.js new file mode 100644 index 00000000000..ecd0686e676 --- /dev/null +++ b/addons/account/static/test/account_reconciliation.js @@ -0,0 +1,914 @@ +odoo.define('account.reconciliationModel_tests', function (require) { +"use strict"; + +var ReconciliationClientAction = require('account.ReconciliationClientAction'); +var testUtils = require('web.test_utils'); + +QUnit.module('account', { + beforeEach: function () { + this.data = { + "res.partner": { + fields: { + id: {string: "ID", type: "integer"}, + display_name: {string: "Displayed name", type: "char"}, + image: {string: "image", type: "integer"}, + }, + records: [ + {id: 1, display_name: "partner 1", image: 'AAA'}, + {id: 2, display_name: "partner 2", image: 'BBB'}, + {id: 3, display_name: "partner 3", image: 'CCC'}, + {id: 4, display_name: "partner 4", image: 'DDD'}, + {id: 8, display_name: "Agrolait", image: 'EEE'}, + {id: 12, display_name: "Camptocamp", image: 'FFF'}, + ] + }, + "account.account": { + fields: { + id: {string: "ID", type: "integer"}, + code: {string: "code", type: "integer"}, + display_name: {string: "Displayed name", type: "char"}, + }, + records: [ + {id: 282, code: 100000, display_name: "100000 Fixed Asset Account"}, + {id: 283, code: 101000, display_name: "101000 Current Assets"}, + {id: 284, code: 101110, display_name: "101110 Stock Valuation Account"}, + {id: 285, code: 101120, display_name: "101120 Stock Interim Account (Received)"}, + {id: 286, code: 101130, display_name: "101130 Stock Interim Account (Delivered)"}, + {id: 287, code: 101200, display_name: "101200 Account Receivable"}, + {id: 288, code: 101300, display_name: "101300 Tax Paid"}, + {id: 308, code: 101401, display_name: "101401 Bank"}, + ] + }, + "account.tax": { + fields: { + id: {string: "ID", type: "integer"}, + display_name: {string: "Displayed name", type: "char"}, + amount: {string: "amout", type: "float"}, + price_include: {string: "Included in Price", type: "boolean"}, + account_id: {string: "partner", type: 'many2one', relation: 'account.account'}, + }, + records: [ + {id: 6, display_name: "Tax 20.00%", amount: 20, price_include: false}, + {id: 7, display_name: "Tax 10.00% include", amount: 10, price_include: true, account_id: 288}, + ] + }, + "account.journal": { + fields: { + id: {string: "ID", type: "integer"}, + display_name: {string: "Displayed name", type: "char"}, + }, + records: [] + }, + "account.analytic.account": { + fields: { + id: {string: "ID", type: "integer"}, + display_name: {string: "Displayed name", type: "char"}, + }, + records: [ + {id: 16, display_name: "Administrative"}, + {id: 7, display_name: "Agrolait - Agrolait"}, + {id: 8, display_name: "Asustek - ASUSTeK"}, + {id: 15, display_name: "Camp to Camp - Camptocamp"}, + {id: 6, display_name: "CampToCamp - Camptocamp"}, + {id: 17, display_name: "Commercial & Marketing"}, + {id: 23, display_name: "Data Import/Export Plugin - Delta PC"}, + {id: 9, display_name: "Delta PC - Delta PC"}, + ] + }, + "account.bank.statement.line": { + fields: { + id: {string: "ID", type: "integer"}, + display_name: {string: "Displayed name", type: "char"}, + partner_id: {string: "partner", type: 'many2one', relation: 'res.partner'}, + }, + records: [ + {id: 5, display_name: "SAJ/2014/002 and SAJ/2014/003"}, + {id: 6, display_name: "Bank fees"}, + {id: 7, display_name: "Prepayment"}, + {id: 8, display_name: "First 2000 \u20ac of SAJ/2014/001"}, + ] + }, + "account.reconcile.model": { + fields: { + id: {string: "ID", type: "integer"}, + name: {string: "Button Label", type: "char"}, + has_second_line: {string: "Add a second line", type: "boolean"}, + account_id: {string: "Account", type: "many2one", relation:'account.account'}, + journal_id: {string: "Journal", type: "many2one", relation:'account.journal'}, + label: {string: "Journal Item Label", type: "char"}, + amount_type: {string: "amount_type", type: "selection", selection: [['fixed', 'Fixed'], ['percentage', 'Percentage of balance']], default:'percentage'}, + amount: {string: "Amount", type: "float", digits:0, help:"Fixed amount will count as a debit if it is negative, as a credit if it is positive.", default:100.0}, + tax_id: {string: "Tax", type: "many2one", relation:'account.tax', domain:[('type_tax_use', '=', 'purchase')]}, + analytic_account_id: {string: "Analytic Account", type: "many2one", relation:'account.analytic.account'}, + second_account_id: {string: "Second Account", type: "many2one", relation:'account.account', domain:[('deprecated', '=', false)]}, + second_journal_id: {string: "Second Journal", type: "many2one", relation:'account.journal', help:"This field is ignored in a bank statement reconciliation."}, + second_label: {string: "Second Journal Item Label", type: "char"}, + second_amount_type: {string: "Second amount_type", type: "selection", selection: [['fixed', 'Fixed'], ['percentage', 'Percentage of balance']], default:'percentage'}, + second_amount: {string: "Second Amount", type: "float", digits:0, help:"Fixed amount will count as a debit if it is negative, as a credit if it is positive.", default:100.0}, + second_tax_id: {string: "Second Tax", type: "many2one", relation:'account.tax', domain:[('type_tax_use', '=', 'purchase')]}, + second_analytic_account_id: {string: "Second Analytic Account", type: "many2one", relation:'account.analytic.account'}, + }, + records: [ + {"second_analytic_account_id": false, "second_amount_type": "percentage", "second_journal_id": false, "id": 4, "analytic_account_id": false, "display_name": "Int\u00e9rrets", "second_tax_id": false, "has_second_line": false, "journal_id": false, "label": false, "second_label": false, "second_account_id": false, "account_id": 282, "company_id": [1, "Demo SPRL"], "tax_id": false, "amount_type": "fixed", "name": "Int\u00e9rrets", "amount": 0.0, "second_amount": 100.0}, + {"second_analytic_account_id": false, "second_amount_type": "percentage", "second_journal_id": false, "id": 2, "analytic_account_id": false, "display_name": "Perte et Profit", "second_tax_id": false, "has_second_line": false, "journal_id": false, "label": false, "second_label": false, "second_account_id": false, "account_id": 283, "company_id": [1, "Demo SPRL"], "tax_id": false, "amount_type": "percentage", "name": "Perte et Profit", "amount": 100.0, "second_amount": 100.0}, + {"second_analytic_account_id": false, "second_amount_type": "percentage", "second_journal_id": false, "id": 5, "analytic_account_id": false, "display_name": "Fs bank", "second_tax_id": false, "has_second_line": false, "journal_id": false, "label": false, "second_label": false, "second_account_id": false, "account_id": 284, "company_id": [1, "Demo SPRL"], "tax_id": false, "amount_type": "percentage", "name": "Fs bank", "amount": 100.0, "second_amount": 100.0}, + {"second_analytic_account_id": false, "second_amount_type": "percentage", "second_journal_id": false, "id": 8, "analytic_account_id": false, "display_name": "Caisse Sand.", "second_tax_id": false, "has_second_line": false, "journal_id": false, "label": "Caisse Sand.", "second_label": false, "second_account_id": false, "account_id": 308, "company_id": [1, "Demo SPRL"], "tax_id": false, "amount_type": "percentage", "name": "Caisse Sand.", "amount": 100.0, "second_amount": 100.0}, + {"second_analytic_account_id": false, "second_amount_type": "percentage", "second_journal_id": false, "id": 3, "analytic_account_id": false, "display_name": "ATOS", "second_tax_id": 7, "has_second_line": true, "journal_id": false, "label": "ATOS Banque", "second_label": "ATOS Frais", "second_account_id": 286, "account_id": 285, "company_id": [1, "Demo SPRL"], "tax_id": 6, "amount_type": "percentage", "name": "ATOS", "amount": 97.5, "second_amount": 2.5}, + {"second_analytic_account_id": false, "second_amount_type": "percentage", "second_journal_id": false, "id": 10, "analytic_account_id": false, "display_name": "Double", "second_tax_id": false, "has_second_line": true, "journal_id": false, "label": "Double Banque", "second_label": "Double Frais", "second_account_id": 286, "account_id": 285, "company_id": [1, "Demo SPRL"], "tax_id": false, "amount_type": "percentage", "name": "Double", "amount": 97.5, "second_amount": 2.5}, + ] + } + }; + var data_preprocess = { + notifications: [], + num_already_reconciled_lines: 0, + st_lines_ids: [5, 6, 7, 8], + statement_name: "BNK/2014/001", + }; + var data_widget = [ + { + "st_line": { + "currency_id": 3, + "communication_partner_name": false, + "open_balance_account_id": 287, + "name": "SAJ/2014/002 and SAJ/2014/003", + "partner_name": "Agrolait", + "partner_id": 8, + "has_no_partner": false, + "journal_id": 84, + "account_name": "Bank", + "note": "", + "amount": 1175.0, + "amount_str": "$ 1,175.00", + "amount_currency_str": "", + "date": "2017-01-01", + "account_code": "101401", + "ref": "", + "id": 5, + "statement_id": 2 + }, + "reconciliation_proposition": [] + }, + { + "st_line": { + "currency_id": 3, + "communication_partner_name": false, + "name": "Bank fees", + "partner_name": false, + "partner_id": false, + "has_no_partner": true, + "journal_id": 84, + "account_name": "Bank", + "note": "", + "amount": -32.58, + "amount_str": "$ 32.58", + "amount_currency_str": "", + "date": "2017-01-01", + "account_code": "101401", + "ref": "", + "id": 6, + "statement_id": 2 + }, + "reconciliation_proposition": [] + }, + { + "st_line": { + "currency_id": 3, + "communication_partner_name": false, + "open_balance_account_id": 287, + "name": "Prepayment", + "partner_name": "Camptocamp", + "partner_id": 12, + "has_no_partner": false, + "journal_id": 84, + "account_name": "Bank", + "note": "", + "amount": 650.0, + "amount_str": "$ 650.00", + "amount_currency_str": "", + "date": "2017-01-01", + "account_code": "101401", + "ref": "", + "id": 7, + "statement_id": 2 + }, + "reconciliation_proposition": [ + { + "account_type": "receivable", + "amount_currency_str": "", + "currency_id": false, + "date_maturity": "2017-02-07", + "date": "2017-01-08", + "total_amount_str": "$ 650.00", + "partner_id": 12, + "account_name": "101200 Account Receivable", + "name": "INV/2017/0012", + "partner_name": "Camptocamp", + "total_amount_currency_str": "", + "id": 133, + "credit": 0.0, + "journal_id": [1, "Customer Invoices"], + "amount_str": "$ 650.00", + "debit": 650.0, + "account_id": [287, "101200 Account Receivable"], + "account_code": "101200", + "ref": "", + "already_paid": false + }, + ] + }, + { + "st_line": { + "currency_id": 3, + "communication_partner_name": false, + "open_balance_account_id": 285, + "name": "First 2000 \u20ac of SAJ/2014/001", + "partner_name": "Camptocamp", + "partner_id": 12, + "has_no_partner": false, + "journal_id": 84, + "account_name": "Bank", + "note": "", + "amount": 2000.0, + "amount_str": "$ 2,000.00", + "amount_currency_str": "", + "date": "2017-01-01", + "account_code": "101401", + "ref": "", + "id": 8, + "statement_id": 2 + }, + "reconciliation_proposition": [] + }, + ]; + var mv_lines = { + '[]': [], + '[5,"",0,6]': [ + {"account_type": "receivable", "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-02-07", "date": "2017-01-08", "total_amount_str": "$ 650.00", "partner_id": 8, "account_name": "101200 Account Receivable", "name": "INV/2017/0002", "partner_name": "Agrolait", "total_amount_currency_str": "", "id": 109, "credit": 0.0, "journal_id": [1, "Customer Invoices"], "amount_str": "$ 650.00", "debit": 650.0, "account_code": "101200", "ref": "", "already_paid": false}, + {"account_type": "receivable", "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-02-07", "date": "2017-01-08", "total_amount_str": "$ 525.00", "partner_id": 8, "account_name": "101200 Account Receivable", "name": "INV/2017/0003", "partner_name": "Agrolait", "total_amount_currency_str": "", "id": 112, "credit": 0.0, "journal_id": [1, "Customer Invoices"], "amount_str": "$ 525.00", "debit": 525.0, "account_code": "101200", "ref": "", "already_paid": false} + ], + '[5,"b",0,6]': [ + {"account_type": "liquidity", "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-01-23", "date": "2017-01-23", "total_amount_str": "$ 100.00", "partner_id": 8, "account_name": "Bank", "name": "BNK1/2017/0003: CUST.IN/2017/0001", "partner_name": "Agrolait", "total_amount_currency_str": "", "id": 394, "credit": 0.0, "journal_id": "Bank", "amount_str": "$ 100.00", "debit": 100.0, "account_code": "101401", "ref": "", "already_paid": true}, + {"account_type": "liquidity", "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-01-23", "date": "2017-01-23", "total_amount_str": "$ 525.50", "partner_id": 8, "account_name": "Bank", "name": "BNK1/2017/0004: CUST.IN/2017/0002", "partner_name": "Agrolait", "total_amount_currency_str": "", "id": 396, "credit": 0.0, "journal_id": "Bank", "amount_str": "$ 525.50", "debit": 525.5, "account_code": "101401", "ref": "INV/2017/0003", "already_paid": true}, + ], + '[6,"",0,6]': [ + {"account_type": "liquidity", "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-01-23", "date": "2017-01-23", "total_amount_str": "$ 376.00", "partner_id": 7, "account_name": "Bank", "name": "BNK1/2017/0002: SUPP.OUT/2017/0002", "partner_name": "ASUSTeK", "total_amount_currency_str": "", "id": 392, "credit": 376.0, "journal_id": "Bank", "amount_str": "$ 376.00", "debit": 0.0, "account_code": "101401", "ref": "BILL/2017/0003", "already_paid": true}, + {"account_type": "liquidity", "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-01-23", "date": "2017-01-23", "total_amount_str": "$ 100.00", "partner_id": 8, "account_name": "Bank", "name": "BNK1/2017/0003: CUST.IN/2017/0001", "partner_name": "Agrolait", "total_amount_currency_str": "", "id": 394, "credit": 0.0, "journal_id": "Bank", "amount_str": "$ 100.00", "debit": 100.0, "account_code": "101401", "ref": "", "already_paid": true}, + {"account_type": "liquidity", "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-01-23", "date": "2017-01-23", "total_amount_str": "$ 525.50", "partner_id": 8, "account_name": "Bank", "name": "BNK1/2017/0004: CUST.IN/2017/0002", "partner_name": "Agrolait", "total_amount_currency_str": "", "id": 396, "credit": 0.0, "journal_id": "Bank", "amount_str": "$ 525.50", "debit": 525.5, "account_code": "101401", "ref": "INV/2017/0003", "already_paid": true}, + {"account_type": "receivable", "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-02-07", "date": "2017-01-08", "total_amount_str": "$ 650.00", "partner_id": 8, "account_name": "101200 Account Receivable", "name": "INV/2017/0002", "partner_name": "Agrolait", "total_amount_currency_str": "", "id": 109, "credit": 0.0, "journal_id": [1, "Customer Invoices"], "amount_str": "$ 650.00", "debit": 650.0, "account_code": "101200", "ref": "", "already_paid": false}, + {"account_type": "receivable", "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-02-22", "date": "2017-01-23", "total_amount_str": "$ 525.00", "partner_id": 8, "account_name": "101200 Account Receivable", "name": "INV/2017/0004", "partner_name": "Agrolait", "total_amount_currency_str": "", "id": 399, "credit": 0.0, "journal_id": [1, "Customer Invoices"], "amount_str": "$ 525.00", "debit": 525.0, "account_code": "101200", "ref": "", "already_paid": false}, + {"account_type": "receivable", "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-02-28", "date": "2017-01-01", "total_amount_str": "$ 4,610.00", "partner_id": 12, "account_name": "101200 Account Receivable", "name": "INV/2017/0001", "partner_name": "Camptocamp", "total_amount_currency_str": "", "id": 106, "credit": 0.0, "journal_id": [1, "Customer Invoices"], "amount_str": "$ 4,610.00", "debit": 4610.0, "account_code": "101200", "ref": "", "already_paid": false} + ], + '[6,"",5,6]': [ + {"account_type": "receivable", "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-02-28", "date": "2017-01-01", "total_amount_str": "$ 4,610.00", "partner_id": 12, "account_name": "101200 Account Receivable", "name": "INV/2017/0001", "partner_name": "Camptocamp", "total_amount_currency_str": "", "id": 106, "credit": 0.0, "journal_id": [1, "Customer Invoices"], "amount_str": "$ 4,610.00", "debit": 4610.0, "account_code": "101200", "ref": "", "already_paid": false}, + {"account_type": "payable", "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-02-28", "date": "2017-01-01", "total_amount_str": "$ 10,000.00", "partner_id": 12, "account_name": "Account Payable", "name": "BILL/2017/0001", "partner_name": "Camptocamp", "total_amount_currency_str": "", "id": 114, "credit": 10000.0, "journal_id": [2, "Vendor Bills"], "amount_str": "$ 10,000.00", "debit": 0.0, "account_code": "111100", "ref": "", "already_paid": false}, + {"account_type": "payable", "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-02-28", "date": "2017-01-15", "total_amount_str": "$ 5,749.99", "partner_id": 7, "account_name": "Account Payable", "name": "BILL/2017/0002", "partner_name": "ASUSTeK", "total_amount_currency_str": "", "id": 117, "credit": 5749.99, "journal_id": [2, "Vendor Bills"], "amount_str": "$ 5,749.99", "debit": 0.0, "account_code": "111100", "ref": "", "already_paid": false} + ], + '[7,"",0,6]': [ + {"account_type": "receivable", "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-02-07", "date": "2017-01-08", "total_amount_str": "$ 650.00", "partner_id": 12, "account_name": "101200 Account Receivable", "name": "INV/2017/0012", "partner_name": "Camptocamp", "total_amount_currency_str": "", "id": 133, "credit": 0.0, "journal_id": [1, "Customer Invoices"], "amount_str": "$ 650.00", "debit": 650.0, "account_id": [287, "101200 Account Receivable"], "account_code": "101200", "ref": "", "already_paid": false}, + {"account_type": "receivable", "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-02-28", "date": "2017-01-01", "total_amount_str": "$ 4,610.00", "partner_id": 12, "account_name": "101200 Account Receivable", "name": "INV/2017/0001", "partner_name": "Camptocamp", "total_amount_currency_str": "", "id": 106, "credit": 0.0, "journal_id": [1, "Customer Invoices"], "amount_str": "$ 4,610.00", "debit": 4610.0, "account_id": [287, "101200 Account Receivable"], "account_code": "101200", "ref": "", "already_paid": false}, + {"account_type": "payable", "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-02-28", "date": "2017-01-01", "total_amount_str": "$ 10,000.00", "partner_id": 12, "account_name": "Account Payable", "name": "BILL/2017/0001", "partner_name": "Camptocamp", "total_amount_currency_str": "", "id": 114, "credit": 10000.0, "journal_id": [2, "Vendor Bills"], "amount_str": "$ 10,000.00", "debit": 0.0, "account_id": [284, "101110 Stock Valuation Account"], "account_code": "111100", "ref": "", "already_paid": false} + ], + '[8,"",0,6]': [], + }; + var auto_reconciliation = { + "num_already_reconciled_lines": 1, + "notifications": [{ + "message": "1 transaction was automatically reconciled.", + "type": "info", + "details": { + "model": "account.move", + "name": "Automatically reconciled items", + "ids": [143] + } + }], + "st_lines_ids": [5, 6, 8], + "statement_name": false + }; + var data_for_manual_reconciliation_widget = { + '[null,null]': { + "customers": [ + {"account_id": 287, "partner_name": "Agrolait", "reconciliation_proposition": [], "currency_id": 3, "max_date": "2017-02-14 12:30:31.256629", "last_time_entries_checked": null, "account_code": "101200", "partner_id": 8, "account_name": "101200 Account Receivable"}, + {"account_id": 7, "partner_name": "Camptocamp", "reconciliation_proposition": [], "currency_id": 3, "max_date": "2017-02-13 14:24:55.246469", "last_time_entries_checked": null, "account_code": "101200", "partner_id": 12, "account_name": "101200 Account Receivable"} + ], + "accounts": [ + { + "account_id": 283, "account_name": "101000 Current Assets", "currency_id": 3, "max_date": "2017-02-16 14:32:04.833986", "last_time_entries_checked": "2017-02-16", "account_code": "101000", + "reconciliation_proposition": [ + {"account_id": 283, "account_type": "other", "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-02-16", "date": "2017-02-16", "total_amount_str": "$ 1,000.00", "partner_id": 8, "account_name": "101000 Current Assets", "name": "BNK1/2017/0006: Customer Payment", "partner_name": "Agrolait", "total_amount_currency_str": "", "id": 399, "credit": 1000.0, "journal_id": [3, "Bank"], "amount_str": "$ 1,000.00", "debit": 0.0, "account_code": "101000", "ref": "", "already_paid": false}, + {"account_id": 283, "account_type": "other", "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-03-18", "date": "2017-02-16", "total_amount_str": "$ 1,000.00", "partner_id": 8, "account_name": "101000 Current Assets", "name": "INV/2017/0006", "partner_name": "Agrolait", "total_amount_currency_str": "", "id": 402, "credit": 0.0, "journal_id": [1, "Customer Invoices"], "amount_str": "$ 1,000.00", "debit": 1000.0, "account_code": "101000", "ref": "", "already_paid": false} + ] + } + ], + "suppliers": [ + { + "account_id": 284, "partner_name": "Agrolait", + "reconciliation_proposition": [ + {"account_id": 284, "account_type": "other", "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-02-16", "date": "2017-02-16", "total_amount_str": "$ 1,000.00", "partner_id": 8, "account_name": "101000 Current Assets", "name": "BNK1/999: Customer Payment", "partner_name": "Agrolait", "total_amount_currency_str": "", "id": 999, "credit": 1000.0, "journal_id": [3, "Bank"], "amount_str": "$ 1,000.00", "debit": 0.0, "account_code": "111100", "ref": "", "already_paid": false}, + {"account_id": 284, "account_type": "other", "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-03-18", "date": "2017-02-16", "total_amount_str": "$ 1,000.00", "partner_id": 8, "account_name": "101000 Current Assets", "name": "INV/998", "partner_name": "Agrolait", "total_amount_currency_str": "", "id": 998, "credit": 0.0, "journal_id": [1, "Customer Invoices"], "amount_str": "$ 1,000.00", "debit": 1000.0, "account_code": "111100", "ref": "", "already_paid": false} + ], + "currency_id": 3, "max_date": "2017-02-14 12:36:05.014302", "last_time_entries_checked": null, "account_code": "111100", "partner_id": 8, "account_name": "Account Payable" + }, { + "account_id": 284, "partner_name": "Camptocamp", + "reconciliation_proposition": [ + {"account_id": 284, "account_type": "other", "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-02-16", "date": "2017-02-16", "total_amount_str": "$ 1,000.00", "partner_id": 12, "account_name": "101000 Current Assets", "name": "BNK1/1999: Customer Payment", "partner_name": "Camptocamp", "total_amount_currency_str": "", "id": 1999, "credit": 1000.0, "journal_id": [3, "Bank"], "amount_str": "$ 1,000.00", "debit": 0.0, "account_code": "111100", "ref": "", "already_paid": false}, + {"account_id": 284, "account_type": "other", "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-03-18", "date": "2017-02-16", "total_amount_str": "$ 1,000.00", "partner_id": 12, "account_name": "101000 Current Assets", "name": "INV/1998", "partner_name": "Camptocamp", "total_amount_currency_str": "", "id": 1998, "credit": 0.0, "journal_id": [1, "Customer Invoices"], "amount_str": "$ 1,000.00", "debit": 1000.0, "account_code": "111100", "ref": "", "already_paid": false} + ], + "currency_id": 3, "max_date": "2017-02-14 12:36:05.014302", "last_time_entries_checked": null, "account_code": "111100", "partner_id": 12, "account_name": "Account Payable" + } + ] + }, + '["partner",null,"receivable"]': [ + {"account_id": 287, "partner_name": "Agrolait", "reconciliation_proposition": [], "currency_id": 3, "max_date": "2017-02-14 12:30:31.256629", "last_time_entries_checked": null, "account_code": "101200", "partner_id": 8, "account_name": "101200 Account Receivable"}, + {"account_id": 287, "partner_name": "Camptocamp", "reconciliation_proposition": [], "currency_id": 3, "max_date": "2017-02-13 14:24:55.246469", "last_time_entries_checked": null, "account_code": "101200", "partner_id": 12, "account_name": "101200 Account Receivable"} + ] + }; + var move_lines_for_manual_reconciliation = { + '[287,8,"",0,6]': [ + {"account_type": "receivable", "account_id": [287, "101200 Account Receivable"], "amount_currency_str": "10,222.00 €", "currency_id": 1, "date_maturity": "2017-02-08", "date": "2017-02-08", "total_amount_str": "$ 11,000.00", "partner_id": 8, "account_name": "101200 Account Receivable", "name": "INV/2017/0004: Customer Payment", "partner_name": "Agrolait", "total_amount_currency_str": "", "id": 17, "credit": 11000.0, "journal_id": [1, "Customer Invoices"], "amount_str": "$ 11,000.00", "debit": 0.0, "account_code": "101200", "ref": "", "already_paid": false}, + {"account_type": "receivable", "account_id": [7, "101200 Account Receivable"], "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-02-09", "date": "2017-02-09", "total_amount_str": "$ 1,000.00", "partner_id": 8, "account_name": "101200 Account Receivable", "name": "INV/2017/0005: Customer Payment", "partner_name": "Agrolait", "total_amount_currency_str": "", "id": 19, "credit": 1000.0, "journal_id": [1, "Customer Invoices"], "amount_str": "$ 1,000.00", "debit": 0.0, "account_code": "101200", "ref": "", "already_paid": false}, + {"account_type": "receivable", "account_id": [287, "101200 Account Receivable"], "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-02-09", "date": "2017-02-09", "total_amount_str": "$ 180.00", "partner_id": 8, "account_name": "101200 Account Receivable", "name": "BILL/2017/0003: Customer Payment", "partner_name": "Agrolait", "total_amount_currency_str": "", "id": 21, "credit": 180.0, "journal_id": [2, "Vendor Bills"], "amount_str": "$ 180.00", "debit": 0.0, "account_code": "101200", "ref": "fddfgfdgfdgsdfg", "already_paid": false}, + {"account_type": "receivable", "account_id": [287, "101200 Account Receivable"], "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-02-09", "date": "2017-02-09", "total_amount_str": "$ 90.00", "partner_id": 8, "account_name": "101200 Account Receivable", "name": "INV/2017/0006: Customer Payment", "partner_name": "Agrolait", "total_amount_currency_str": "", "id": 23, "credit": 90.0, "journal_id": [1, "Customer Invoices"], "amount_str": "$ 90.00", "debit": 0.0, "account_code": "101200", "ref": "", "already_paid": false}, + {"account_type": "receivable", "account_id": [287, "101200 Account Receivable"], "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-03-10", "date": "2017-02-08", "total_amount_str": "$ 650.00", "partner_id": 8, "account_name": "101200 Account Receivable", "name": "INV/2017/0012", "partner_name": "Agrolait", "total_amount_currency_str": "", "id": 6, "credit": 0.0, "journal_id": [1, "Customer Invoices"], "amount_str": "$ 1000.00", "debit": 1000.0, "account_code": "101200", "ref": "", "already_paid": false}, + {"account_type": "receivable", "account_id": [287, "101200 Account Receivable"], "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-03-10", "date": "2017-02-08", "total_amount_str": "$ 525.00", "partner_id": 8, "account_name": "101200 Account Receivable", "name": "INV/2017/0003", "partner_name": "Agrolait", "total_amount_currency_str": "", "id": 9, "credit": 0.0, "journal_id": [1, "Customer Invoices"], "amount_str": "$ 525.00", "debit": 525.0, "account_code": "101200", "ref": "", "already_paid": false} + ], + '[7,12,"",0,6]': [ + {"account_type": "receivable", "account_id": [287, "101200 Account Receivable"], "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-02-08", "date": "2017-02-08", "total_amount_str": "$ 11,000.00", "partner_id": 12, "account_name": "101200 Account Receivable", "name": "INV/2017/0004: Customer Payment", "partner_name": "Camptocamp", "total_amount_currency_str": "", "id": 17, "credit": 11000.0, "journal_id": [1, "Customer Invoices"], "amount_str": "$ 11,000.00", "debit": 0.0, "account_code": "101200", "ref": "", "already_paid": false}, + {"account_type": "receivable", "account_id": [7, "101200 Account Receivable"], "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-02-09", "date": "2017-02-09", "total_amount_str": "$ 1,000.00", "partner_id": 12, "account_name": "101200 Account Receivable", "name": "INV/2017/0005: Customer Payment", "partner_name": "Camptocamp", "total_amount_currency_str": "", "id": 19, "credit": 1000.0, "journal_id": [1, "Customer Invoices"], "amount_str": "$ 1,000.00", "debit": 0.0, "account_code": "101200", "ref": "", "already_paid": false}, + {"account_type": "receivable", "account_id": [287, "101200 Account Receivable"], "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-02-09", "date": "2017-02-09", "total_amount_str": "$ 180.00", "partner_id": 12, "account_name": "101200 Account Receivable", "name": "BILL/2017/0003: Customer Payment", "partner_name": "Camptocamp", "total_amount_currency_str": "", "id": 21, "credit": 180.0, "journal_id": [2, "Vendor Bills"], "amount_str": "$ 180.00", "debit": 0.0, "account_code": "101200", "ref": "fddfgfdgfdgsdfg", "already_paid": false}, + ], + '[284,8,"",0,6]': [ + {"account_type": "receivable", "account_id": [284, "111100 Account Payable"], "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-02-08", "date": "2017-02-08", "total_amount_str": "$ 11,000.00", "partner_id": 8, "account_name": "101200 Account Receivable", "name": "INV/2017/0004: Customer Payment", "partner_name": "Agrolait", "total_amount_currency_str": "", "id": 17, "credit": 11000.0, "journal_id": [1, "Customer Invoices"], "amount_str": "$ 11,000.00", "debit": 0.0, "account_code": "111100", "ref": "", "already_paid": false}, + {"account_type": "receivable", "account_id": [284, "111100 Account Payable"], "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-02-09", "date": "2017-02-09", "total_amount_str": "$ 1,000.00", "partner_id": 8, "account_name": "101200 Account Receivable", "name": "INV/2017/0005: Customer Payment", "partner_name": "Agrolait", "total_amount_currency_str": "", "id": 19, "credit": 1000.0, "journal_id": [1, "Customer Invoices"], "amount_str": "$ 1,000.00", "debit": 0.0, "account_code": "111100", "ref": "", "already_paid": false}, + {"account_type": "receivable", "account_id": [284, "111100 Account Payable"], "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-02-09", "date": "2017-02-09", "total_amount_str": "$ 180.00", "partner_id": 8, "account_name": "101200 Account Receivable", "name": "BILL/2017/0003: Customer Payment", "partner_name": "Agrolait", "total_amount_currency_str": "", "id": 21, "credit": 180.0, "journal_id": [2, "Vendor Bills"], "amount_str": "$ 180.00", "debit": 0.0, "account_code": "111100", "ref": "fddfgfdgfdgsdfg", "already_paid": false}, + ], + '[283,null,"",0,6]': [ + {"account_type": "other", "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-02-16", "date": "2017-02-16", "total_amount_str": "$ 1,000.00", "partner_id": 8, "account_name": "101000 Current Assets", "name": "BNK1/2017/0006: Customer Payment", "partner_name": "Agrolait", "total_amount_currency_str": "", "id": 399, "credit": 1000.0, "journal_id": [3, "Bank"], "amount_str": "$ 1,000.00", "debit": 0.0, "account_code": "101000", "ref": "", "already_paid": false}, + {"account_type": "other", "amount_currency_str": "", "currency_id": false, "date_maturity": "2017-03-18", "date": "2017-02-16", "total_amount_str": "$ 1,000.00", "partner_id": 8, "account_name": "101000 Current Assets", "name": "INV/2017/0006", "partner_name": "Agrolait", "total_amount_currency_str": "", "id": 402, "credit": 0.0, "journal_id": [1, "Customer Invoices"], "amount_str": "$ 1,000.00", "debit": 1000.0, "account_code": "101000", "ref": "", "already_paid": false} + ], + '[284,12,"",0,6]': [], + }; + this.session = { + currencies: { + 3: { + digits: [69, 2], + position: "before", + symbol: "$" + } + } + }; + this.mockRPC = function (route, args) { + var key; + var excluded_ids; + var ids; + if (args.method === "json_friendly_compute_all" && args.model === "account.tax") { + var tax = _.find(this.data["account.tax"].records, {'id': args.args[0][0]}); + var amount = args.args[1]; + var tax_base = tax.price_include ? amount*100/(100+tax.amount) : amount; + return $.when({ + "base": amount, + "taxes": [{ + "id": tax.id, + "amount": tax_base*tax.amount/100, + "base": tax_base, + "name": tax.display_name, + "analytic": false, + "refund_account_id": false, + "account_id": tax.account_id + }], + "total_excluded": amount/100*(100-tax.amount), + "total_included": amount, + }); + } + if (args.method === "reconciliation_widget_preprocess" && args.model === "account.bank.statement") { + return $.when(data_preprocess); + } + if (args.method === "get_data_for_reconciliation_widget" && args.model === "account.bank.statement.line") { + ids = args.args[0]; + return $.when(_.filter(data_widget, function (w) {return _.contains(ids, w.st_line.id);})); + } + if (args.method === "get_move_lines_for_reconciliation_widget" && args.model === "account.bank.statement.line") { + var partner_id = args.args.splice(1, 1)[0]; + excluded_ids = args.args.splice(1, 1)[0]; + key = JSON.stringify(args.args); + if (!mv_lines[key]) { + throw new Error("Unknown parameters for get_move_lines_for_reconciliation_widget: '"+ key + "'"); + } + return $.when(_.filter(mv_lines[key], function (line) { + return excluded_ids.indexOf(line.id) === -1 && (!partner_id || partner_id === line.partner_id); + })); + } + if (args.method === "reconciliation_widget_auto_reconcile" && args.model === "account.bank.statement.line") { + return $.when(auto_reconciliation); + } + if (args.method === "get_data_for_manual_reconciliation_widget" && args.model === "account.move.line") { + key = JSON.stringify(args.args); + if (!data_for_manual_reconciliation_widget[key]) { + throw new Error("Unknown parameters for get_data_for_manual_reconciliation_widget: '"+ key + "'"); + } + return $.when(data_for_manual_reconciliation_widget[key]); + } + if (args.method === "get_move_lines_for_manual_reconciliation" && args.model === "account.move.line") { + excluded_ids = args.args.splice(2, 1)[0]; + key = JSON.stringify(args.args); + if (!move_lines_for_manual_reconciliation[key]) { + throw new Error("Unknown parameters for get_move_lines_for_manual_reconciliation: '"+ key + "'"); + } + return $.when(_.filter(move_lines_for_manual_reconciliation[key], function (line) { + return excluded_ids.indexOf(line.id) === -1; + })); + } + if (args.method === "process_reconciliations" && args.model === "account.move.line") { + var datas = args.args[0]; + for (var i in datas) { + var data = datas[i]; + for (var key in move_lines_for_manual_reconciliation) { + move_lines_for_manual_reconciliation[key] = _.filter(move_lines_for_manual_reconciliation[key], function (mv_line) { + return data.mv_line_ids.indexOf(mv_line.id) === -1; + }); + } + } + return $.when(); + } + if (args.method === "mark_as_reconciled") { + return $.when(); + } + return this._super(route, args); + }; + this.options = { + context: { + statement_ids: [4] + } + }; + } +}, function () { + QUnit.module('Reconciliation'); + + QUnit.test('Reconciliation basic rendering', function (assert) { + assert.expect(11); + + var clientAction = new ReconciliationClientAction.StatementAction(null, this.options); + testUtils.addMockEnvironment(clientAction, { + 'data': this.data, + mockRPC: this.mockRPC, + }); + clientAction.appendTo($('#qunit-fixture')); + + var widget = clientAction.widgets[0]; + + assert.ok(widget.$el.hasClass('o_reconciliation_line'), "should instance of widget reconciliation"); + assert.strictEqual(widget.$('.accounting_view').length, 1, "should have one view"); + assert.strictEqual(widget.$('.match').length, 1, "should have 'match' panel"); + assert.strictEqual(widget.$('.create').length, 1, "should have 'create' panel"); + + assert.strictEqual(widget.$('thead').text().replace(/[\n\r\s]+/g, ' '), " 101401 2017-01-01 SAJ/2014/002 and SAJ/2014/003 $ 1,175.00 ", "should display the line information"); + assert.ok(widget.$('caption .o_form_field.o_form_field_many2one').length, "should display the many2one with to select a partner"); + + assert.strictEqual(clientAction.$('[data-mode="inactive"]').length, 3, "should be as 'inactive' mode by default"); + assert.strictEqual(widget.$el.data('mode'), 'match', "the first one should automatically switch to match mode"); + + widget.$('.accounting_view thead td:first').trigger('click'); + assert.strictEqual(widget.$el.data('mode'), 'inactive', "should switch to 'inactive' mode"); + + widget.$('.accounting_view tfoot td:first').trigger('click'); + assert.strictEqual(widget.$el.data('mode'), 'create', "should switch to 'create' mode"); + widget.$('.accounting_view tfoot td:first').trigger('click'); + assert.strictEqual(widget.$el.data('mode'), 'match', "should switch to 'match' mode"); + + clientAction.destroy(); + }); + + + QUnit.test('Reconciliation fields', function (assert) { + assert.expect(6); + + var clientAction = new ReconciliationClientAction.StatementAction(null, this.options); + testUtils.addMockEnvironment(clientAction, { + 'data': this.data, + mockRPC: this.mockRPC, + }); + clientAction.appendTo($('#qunit-fixture')); + + var widget = clientAction.widgets[0]; + + assert.strictEqual(widget.$('.o_form_input_dropdown input').val(), "Agrolait", "the partner many2one should display agrolait"); + assert.strictEqual(clientAction.widgets[2].$('.o_form_input_dropdown input').val(), "Camptocamp", "the partner many2one should display Camptocamp"); + widget.$('.accounting_view tfoot td:first').trigger('click'); + assert.strictEqual(widget.$('.create .o_form_input').length, 6, + "create panel should display 5 fields (account_id, tax_id, analytic_account_id, label, amount)"); + assert.strictEqual(widget.$('.create .create_account_id .o_form_required, .create .create_label .o_form_required, .create .create_amount .o_form_required').length, 3, + "account_id, label and amount should be required fields"); + assert.strictEqual(widget.$('.create .create_label input').val(), 'SAJ/2014/002 and SAJ/2014/003', + "should use the name of the reconciliation line for the default label value"); + assert.strictEqual(widget.$('.create .create_amount input').val(), '1175.00', + "should have the balance amout as default value for the amout field"); + + clientAction.destroy(); + }); + + + QUnit.test('Reconciliation basic data', function (assert) { + assert.expect(20); + + var clientAction = new ReconciliationClientAction.StatementAction(null, this.options); + testUtils.addMockEnvironment(clientAction, { + 'data': this.data, + mockRPC: this.mockRPC, + }); + + clientAction.appendTo($('#qunit-fixture')); + + var widget = clientAction.widgets[0]; + + assert.strictEqual(widget.$('.mv_line').length, 2, "should display 2 account move lines"); + assert.strictEqual(widget.$('.mv_line').text().replace(/[\n\r\s]+/g, ' '), + " 101200 2017-02-07 INV/2017/0002 $ 650.00 101200 2017-02-07 INV/2017/0003 $ 525.00 ", + "should display 4 account move lines who contains the account_code, due_date, label and the credit"); + assert.strictEqual(widget.$('.mv_line .cell_right:not(:empty)').length, 2, "should display only the credit account move lines (hide the debit)"); + + clientAction.widgets[1].$('.accounting_view thead td:first').trigger('click'); + assert.strictEqual(clientAction.widgets[1].$('.mv_line').length, 5, "should display 5 account move lines"); + assert.strictEqual(clientAction.widgets[1].$('.mv_line .cell_right:not(:empty)').length, 4, "should display only the credit account move lines (hide the debit)"); + assert.strictEqual(clientAction.widgets[1].$('.mv_line.already_reconciled').length, 3, "should display 3 already reconciled account move lines"); + assert.strictEqual(clientAction.widgets[1].$('.mv_line').text().replace(/[\n\r\s]+/g, ' '), + " 101401 2017-01-23 ASUSTeK: BNK1/2017/0002: SUPP.OUT/2017/0002 : BILL/2017/0003 $ 376.00 101401 2017-01-23 Agrolait: BNK1/2017/0003: CUST.IN/2017/0001 $ 100.00 101401 2017-01-23 Agrolait: BNK1/2017/0004: CUST.IN/2017/0002 : INV/2017/0003 $ 525.50 101200 2017-02-07 Agrolait: INV/2017/0002 $ 650.00 101200 2017-02-22 Agrolait: INV/2017/0004 $ 525.00 ", + "should display 4 account move lines who contains the account_code, due_date, label and the credit"); + assert.strictEqual(clientAction.widgets[1].$('.mv_line .cell_left:not(:empty)').length, 1, "should display only 1 debit account move lines"); + clientAction.widgets[1].$('.match_controls .fa-chevron-right').trigger('click'); + assert.strictEqual(clientAction.widgets[1].$('.mv_line').text().replace(/[\n\r\s]+/g, ' '), + " 101200 2017-02-28 Camptocamp: INV/2017/0001 $ 4,610.00 111100 2017-02-28 Camptocamp: BILL/2017/0001 $ 10,000.00 111100 2017-02-28 ASUSTeK: BILL/2017/0002 $ 5,749.99 ", + "should display the next 5 account move lines"); + + assert.ok(clientAction.widgets[0].$('caption button.btn-default:visible').length, "should display the 'validate' button"); + assert.ok(clientAction.widgets[1].$('caption .text-danger:visible').length, "should display the 'Select a partner or choose a counterpart' message"); + assert.ok(clientAction.widgets[2].$('caption button.btn-primary:visible').length, "should display the 'Reconcile' button"); + + clientAction.widgets[3].$('.accounting_view thead td:first').trigger('click'); + assert.strictEqual(clientAction.widgets[3].$el.data('mode'), 'create', "should switch to 'create' mode instead 'match' mode when 'match' mode is empty"); + + widget = clientAction.widgets[2]; + + assert.notOk( widget.$('.cell_right .line_info_button').length, "should not display the partial reconciliation alert"); + assert.ok( widget.$('button.btn-primary:not(hidden)').length, "should display the reconcile button"); + widget.$('.accounting_view thead td:first').trigger('click'); + widget.$('.match td.cell_label:first').trigger('click'); + assert.ok( widget.$('.text-danger:not(hidden)').length, "should display counterpart alert"); + widget.$('.accounting_view tbody td.cell_label:first').trigger('click'); + assert.ok( widget.$('.text-danger:not(hidden)').length, "should display counterpart alert"); + assert.strictEqual(widget.$('.cell_right .line_info_button').length, 1, "should display a partial reconciliation alert"); + widget.$('.cell_right .line_info_button').trigger('click'); + assert.ok(widget.$('.cell_right .line_info_button').hasClass('do_partial_reconcile_false'), "should display the partial reconciliation information"); + assert.ok( widget.$('button.btn-default:not(hidden)').length, "should display the validate button"); + + clientAction.destroy(); + }); + + + QUnit.test('Reconciliation title', function (assert) { + assert.expect(6); + + var clientAction = new ReconciliationClientAction.StatementAction(null, this.options); + testUtils.addMockEnvironment(clientAction, { + 'data': this.data, + mockRPC: this.mockRPC, + }); + + clientAction.appendTo($('#qunit-fixture')); + + assert.strictEqual(clientAction.$('h1.statement_name:visible').text(), "BNK/2014/001", "Should display the statement name"); + clientAction.$('h1.statement_name').trigger('click'); + assert.strictEqual(clientAction.$('h1.statement_name:visible').length, 0, "Should hide the statement name title to edit the content"); + assert.strictEqual(clientAction.$('h1.statement_name_edition:visible').length, 1, "Should show the edition field of statement name"); + clientAction.$('h1.statement_name_edition input').val('BNK/2014/001-BB').trigger('input'); + clientAction.$('h1.statement_name_edition button').trigger('click'); + assert.strictEqual(clientAction.$('h1.statement_name_edition:visible').length, 0, "Should hide the edition field of statement name"); + assert.strictEqual(clientAction.$('h1.statement_name:visible').length, 1, "Should show the statement name"); + assert.strictEqual(clientAction.$('h1.statement_name:visible').text(), "BNK/2014/001-BB", "Should update the statement name"); + + clientAction.destroy(); + }); + + + QUnit.test('Reconciliation currencies', function (assert) { + assert.expect(2); + + var clientAction = new ReconciliationClientAction.StatementAction(null, this.options); + + testUtils.addMockEnvironment(clientAction, { + 'data': this.data, + mockRPC: this.mockRPC, + session: this.session, + translateParameters: { + date_format: "%m/%d/%Y", + direction:"ltr", + name:"English", + thousands_sep: ",", + time_format: "%H:%M:%S", + decimal_point: ".", + id:1, + grouping: [3,0], + } + }); + clientAction.appendTo($('#qunit-fixture')); + + var widget = clientAction.widgets[0]; + + assert.strictEqual(clientAction.$('.accounting_view tfoot .cell_right, .accounting_view tfoot .cell_left').text().replace(/[\n\r\s]+/g, ' '), + "$ 1,175.00$ 32.58$ 2,000.00", "should display the different amounts with the currency"); + widget.$('.accounting_view thead .mv_line td:first').trigger('click'); + + assert.strictEqual(clientAction.$('.accounting_view tbody').text().replace(/[\n\r\s]+/g, ' '), + " 101200 2017-02-07 INV/2017/0012 $ 650.00 ", "should display the created reconciliation line with the currency"); + + clientAction.destroy(); + }); + + + QUnit.test('Reconciliation create line', function (assert) { + assert.expect(21); + + var clientAction = new ReconciliationClientAction.StatementAction(null, this.options); + + testUtils.addMockEnvironment(clientAction, { + 'data': this.data, + mockRPC: this.mockRPC, + }); + clientAction.appendTo($('#qunit-fixture')); + + assert.strictEqual(clientAction.$('.accounting_view tfoot .cell_right, .accounting_view tfoot .cell_left').text().replace(/[$, ]+/g, ''), "1175.0032.582000.00", "should display the open balance values"); + + var widget = clientAction.widgets[0]; + + assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), 'Open balance', "should display 'Open Balance' line with the rest to reconcile"); + + widget.$('.accounting_view tfoot td:first').trigger('click'); + widget.$('.create .create_account_id input').trigger('click'); + $('.ui-autocomplete .ui-menu-item a:contains(101200 Account Receivable)').trigger('mouseenter').trigger('click'); + + assert.notOk(widget.$('.accounting_view tfoot .cell_label').text(), "should not display 'Open Balance' line because the rest to reconcile is null"); + assert.strictEqual(widget.$('.accounting_view tbody tr').length, 1, "should have only the created reconcile line"); + assert.strictEqual(widget.$('.accounting_view tbody tr').text().replace(/[\n\r\s$,]+/g, ' '), " 101200 SAJ/2014/002 and SAJ/2014/003 1175.00 ", + "the new line should have the selected account, name and amout"); + assert.ok(widget.$('caption button.btn-primary:visible').length, "should display the 'Reconcile' button"); + + widget.$('.create .create_amount input').val('1100.00').trigger('input'); + + assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text(), "1100.00", "should display the value 1100.00 in right column"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "75.00", "should display 'Open Balance' line because the rest to reconcile is 75.00"); + assert.strictEqual(widget.$('.accounting_view tbody tr').length, 1, "should have ever only the created reconcile line"); + assert.strictEqual(widget.$('.accounting_view tbody tr').text().replace(/[\n\r\s$,]+/g, ' '), " 101200 SAJ/2014/002 and SAJ/2014/003 1100.00 ", + "the new line should be update the amout"); + assert.ok(widget.$('caption button.btn-default:visible').length, "should display the 'validate' button"); + + widget.$('.create .add_line').trigger('click').trigger('click'); + widget.$('.create .create_amount input').val('-100').trigger('input'); + widget.$('.create .create_account_id input').trigger('click'); + $('.ui-autocomplete .ui-menu-item a:contains(101000 Current Assets)').trigger('mouseenter').trigger('click'); + widget.$('.create .create_label input').val('test0').trigger('input'); + + assert.strictEqual(widget.$('.accounting_view tbody .cell_left:last').text(), "100.00", "should display the value 100.00 in left column"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open Balance'"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "175.00", "should display 'Open Balance' line because the rest to reconcile is 175.00"); + assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines"); + assert.strictEqual(widget.$('.accounting_view tbody tr:eq(1)').text().replace(/[\n\r\s$,]+/g, ' '), " 101000 test0 100.00 ", + "the new line should have the selected account, name and amout"); + + widget.$('.accounting_view tfoot td:first').trigger('click'); + widget.$('.accounting_view tfoot td:first').trigger('click'); + + assert.strictEqual(widget.$('.create .create_amount input').val(), "175.00", "should have '175.00' as default amount value"); + + widget.$('.create .create_amount input').val('200').trigger('input'); + widget.$('.create .create_account_id input').trigger('click'); + $('.ui-autocomplete .ui-menu-item a:contains(101000 Current Assets)').trigger('mouseenter').trigger('click'); + widget.$('.create .create_label input').val('test1').trigger('input'); + + assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text(), "200.00", "should display the value 100.00 in left column"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Create Write-off", "should display 'Create Write-off'"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "25.00", "should display 'Create Write-off' with 25.00 in left column"); + assert.strictEqual(widget.$('.accounting_view tbody tr').length, 3, "should have 3 created reconcile lines"); + + clientAction.destroy(); + }); + + + QUnit.test('Reconciliation create line with taxes', function (assert) { + assert.expect(13); + + var clientAction = new ReconciliationClientAction.StatementAction(null, this.options); + + testUtils.addMockEnvironment(clientAction, { + 'data': this.data, + mockRPC: this.mockRPC, + }); + clientAction.appendTo($('#qunit-fixture')); + + var widget = clientAction.widgets[0]; + + widget.$('.accounting_view tfoot td:first').trigger('click'); + widget.$('.create .create_account_id input').trigger('click'); + $('.ui-autocomplete .ui-menu-item a:contains(101000 Current Assets)').trigger('mouseenter').trigger('click'); + widget.$('.create .create_label input').val('test1').trigger('input'); + widget.$('.create .create_amount input').val('1100').trigger('input'); + + assert.strictEqual(widget.$('.accounting_view tbody .cell_right:last').text(), "1100.00", "should display the value 1100.00 in left column"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open Balance'"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "75.00", "should display 'Open Balance' with 75.00 in right column"); + assert.strictEqual(widget.$('.accounting_view tbody tr').length, 1, "should have 1 created reconcile lines"); + + widget.$('.create .create_tax_id input').trigger('click'); + $('.ui-autocomplete .ui-menu-item a:contains(10.00%)').trigger('mouseenter').trigger('click'); + + assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "1000.00100.00", "should have 2 created reconcile lines with right column values"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Open balance", "should display 'Open Balance'"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_right').text(), "75.00", "should display 'Open Balance' with 75.00 in right column"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "", "should display 'Open Balance' without any value in left column"); + assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines"); + + widget.$('.create .create_tax_id input').trigger('click'); + $('.ui-autocomplete .ui-menu-item a:contains(20.00%)').trigger('mouseenter').trigger('click'); + + assert.strictEqual(widget.$('.accounting_view tbody .cell_right').text().replace('$_', ''), "1100.00220.00", "should have 2 created reconcile lines with right column values"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_label').text(), "Create Write-off", "should display 'Create Write-off'"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_left').text(), "145.00", "should display 'Create Write-off' with 145.00 in right column"); + assert.strictEqual(widget.$('.accounting_view tbody tr').length, 2, "should have 2 created reconcile lines"); + + clientAction.destroy(); + }); + + + QUnit.test('Reconciliation create line from reconciliation model', function (assert) { + assert.expect(6); + + var clientAction = new ReconciliationClientAction.StatementAction(null, this.options); + + testUtils.addMockEnvironment(clientAction, { + 'data': this.data, + mockRPC: this.mockRPC, + }); + clientAction.appendTo($('#qunit-fixture')); + + var widget = clientAction.widgets[0]; + + widget.$('.accounting_view tfoot td:first').trigger('click'); + widget.$('.create .quick_add button:contains(ATOS)').trigger('click'); + + assert.strictEqual(widget.$('.accounting_view tbody .cell_label, .accounting_view tbody .cell_right').text().replace(/[\n\r\s$,]+/g, ' '), + "ATOS Banque1145.62Tax 20.00%229.12ATOS Frais26.71Tax 10.00% include2.67", "should display 4 lines"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_label, .accounting_view tfoot .cell_left').text().replace(/[\n\r\s$,]+/g, ' '), + "Create Write-off229.12", "should display the 'Create Write-off' line with value in left column"); + + widget.$('.create .create_amount input').val('100').trigger('input'); + + assert.strictEqual(widget.$('.accounting_view tbody').text().replace(/[\n\r\s$,]+/g, ' '), + " 101120 ATOS Banque 1075.00 101120 Tax 20.00% 215.00 101130 ATOS Frais 90.91 101300 Tax 10.00% include 9.09 ", + "should update the value of the 4 lines (because the line must have 100% of the value)"); + assert.strictEqual(widget.$('.accounting_view tfoot .cell_label, .accounting_view tfoot .cell_left').text().replace(/[\n\r\s$,]+/g, ' '), + "Create Write-off215.00", "should change the 'Create Write-off' line because the 20.00% tax is not an include tax"); + + widget.$('.accounting_view tbody .cell_account_code:first').trigger('click'); + widget.$('.accounting_view tbody .cell_label:first').trigger('click'); + + assert.strictEqual(widget.$('.accounting_view tbody').text().replace(/[\n\r\s$,]+/g, ' '), "", "should removed every line"); + + widget.$('.create .quick_add button:contains(Double)').trigger('click'); + + assert.strictEqual(widget.$('.accounting_view tbody').text().replace(/[\n\r\s$,]+/g, ' '), + " 101120 Double Banque 1145.62 101130 Double Frais 29.38 ", + "should have a sum of reconciliation proposition amounts equal to the line amount"); + + clientAction.destroy(); + }); + + + QUnit.test('Reconciliation auto reconciliation', function (assert) { + assert.expect(6); + + var clientAction = new ReconciliationClientAction.StatementAction(null, this.options); + + testUtils.addMockEnvironment(clientAction, { + 'data': this.data, + mockRPC: this.mockRPC, + }); + clientAction.appendTo($('body')); + + clientAction.$('button.o_automatic_reconciliation').trigger('click'); + + assert.strictEqual(clientAction.$('.progress .valuenow').text(), "1", "should reconciled one line"); + assert.strictEqual(clientAction.$('.progress .valuemax').text(), "4", "should have maximum 4 lines to reconciled"); + assert.strictEqual(clientAction.$('thead .cell_label').text().replace(/[\n\r\s$,]+/g, ' '), + "SAJ/2014/002 and SAJ/2014/003 Bank fees First 2000 € of SAJ/2014/001 ", "should rest 3 lines to reconciled"); + assert.ok(clientAction.$('.notification_area .notification').length, "should display the notification"); + var do_action = false; + clientAction.on('do_action', clientAction, function (data) { + do_action = data.data.action; + }); + clientAction.$('.notification_area a.fa-external-link').trigger('click'); + assert.deepEqual(_.pick(do_action, "res_model", "domain", "type"), + {"res_model":"account.move", "domain":[["id","in",[143]]], "type":"ir.actions.act_window"}, + "should try to open the record form view"); + clientAction.$('.notification_area .close').trigger('click'); + assert.notOk(clientAction.$('.notification_area .notification').length, "should close the notification"); + + clientAction.destroy(); + }); + + + QUnit.test('Reconciliation manual open record', function (assert) { + assert.expect(2); + + var clientAction = new ReconciliationClientAction.ManualAction(null, this.options); + + testUtils.addMockEnvironment(clientAction, { + 'data': this.data, + mockRPC: this.mockRPC, + }); + clientAction.appendTo($('#qunit-fixture')); + + testUtils.intercept(clientAction, 'call_service', function (event) { + if (event.data.args[0].indexOf('get_formview_action') !== -1) { + assert.equal(event.data.args[1].model, "account.account", "should open the account.account from view "); + event.data.callback($.when()); + } + }); + clientAction.$('.accounting_view:first thead .o_form_uri').trigger('click'); + + testUtils.intercept(clientAction, 'call_service', function (event) { + if (event.data.args[0].indexOf('get_formview_action') !== -1) { + assert.equal(event.data.args[1].model, "res.partner", "should open the res.partner from view "); + event.data.callback($.when()); + } + }); + clientAction.$('.accounting_view:eq(1) thead .o_form_uri').trigger('click'); + + clientAction.destroy(); + }); + + + QUnit.test('Reconciliation manual', function (assert) { + assert.expect(9); + + var clientAction = new ReconciliationClientAction.ManualAction(null, this.options); + + testUtils.addMockEnvironment(clientAction, { + 'data': this.data, + mockRPC: this.mockRPC, + session: this.session, + }); + clientAction.appendTo($('#qunit-fixture')); + + assert.strictEqual(clientAction.$('.accounting_view:first thead').text().replace(/[\n\r\s]+/g, ' '), + " 101000 Current AssetsLast Reconciliation: 2017-02-16 101000 ", + "should display the account as title"); + assert.strictEqual(clientAction.$('.o_reconciliation_line:first').data('mode'), "inactive", "should be in 'inactive' mode because no line to displayed and the balance amount is null"); + assert.strictEqual(clientAction.$('.accounting_view:first tbody tr').length, 2, "should have 2 propositions"); + assert.strictEqual(clientAction.$('.accounting_view:first .o_reconcile:visible').length, 1, "should display the reconcile button"); + + clientAction.$('.accounting_view:first .o_reconcile:visible').trigger('click'); + + assert.strictEqual(clientAction.$('.progress .progress-bar').attr('aria-valuenow'), "1", "should display a progress bar with 1 reconciled line"); + + assert.strictEqual(clientAction.$('.accounting_view:first thead').text().replace(/[\n\r\s]+/g, ' '), + " Agrolait 101200 ", + "should display the partner and the account code as title"); + + assert.strictEqual(clientAction.$('.o_reconciliation_line:first .match tr:first .cell_left').text(), + "$ 11,000.00", "sould display the line in $"); + assert.strictEqual(clientAction.$('.o_reconciliation_line:first .match tr:first .cell_left .o_multi_currency').data('content'), + "10,222.00 €", "sould display the monetary information in €"); + + assert.strictEqual(clientAction.$('.accounting_view:first .o_no_valid:visible').length, 1, "should display the skip button"); + + clientAction.destroy(); + }); + + + QUnit.test('Reconciliation manual validate all', function (assert) { + assert.expect(6); + + var clientAction = new ReconciliationClientAction.ManualAction(null, this.options); + + testUtils.addMockEnvironment(clientAction, { + 'data': this.data, + mockRPC: this.mockRPC, + session: this.session, + }); + + clientAction.prependTo($('#qunit-fixture')); + + assert.strictEqual(clientAction.$('.o_reconciliation_line[data-mode="match"]').length, 0, "should have every widgets in 'inactive' mode"); + assert.strictEqual(clientAction.$('.o_reconciliation_line').length, 5, "should have 5 widgets to reconciled"); + assert.strictEqual(clientAction.$('.o_reconciliation_line button.o_reconcile:visible').length, 3, "should have 3 widgets who display 'reconcile' button"); + + var e = $.Event("keyup"); + e.which = 13; // # Enter code value + e.ctrlKey = true; // Ctrl key pressed + $("body").trigger(e); + + assert.strictEqual(clientAction.$('.o_reconciliation_line').length, 3, "should have 2 already reconciled widgets"); + assert.strictEqual(clientAction.$('.progress .progress-bar').attr('aria-valuenow'), "2", "should display a progress bar with 2 reconciled line"); + assert.strictEqual(clientAction.$('.o_reconciliation_line[data-mode="match"]').length, 1, "should display the 'match' mode for the first line"); + + clientAction.destroy(); + }); +}); +}); diff --git a/addons/account/views/account.xml b/addons/account/views/account.xml index 47fe1f10667..a96fdd9ca4f 100644 --- a/addons/account/views/account.xml +++ b/addons/account/views/account.xml @@ -1,18 +1,25 @@ - - + + diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index 8cabb8da63f..fb0b2c9fa75 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -7,9 +7,10 @@ account.invoice.calendar account.invoice - + + @@ -173,7 +174,7 @@
-
diff --git a/addons/account/views/account_journal_dashboard_view.xml b/addons/account/views/account_journal_dashboard_view.xml index f295f26173f..3bae9462fd9 100644 --- a/addons/account/views/account_journal_dashboard_view.xml +++ b/addons/account/views/account_journal_dashboard_view.xml @@ -294,9 +294,7 @@
-
- -
+