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 @@
-
- -
+