odoo.define("account.ReconciliationModel", function (require) {
    "use strict";

    var BasicModel = require("web.BasicModel");
    var field_utils = require("web.field_utils");
    var utils = require("web.utils");
    var session = require("web.session");
    var core = require("web.core");
    var _t = core._t;

    /**
     * Model use to fetch, format and update 'account.reconciliation.widget',
     * datas allowing reconciliation
     *
     * The statement internal structure::
     *
     *  {
     *      valuenow: integer
     *      valuenow: valuemax
     *      [bank_statement_line_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_rp', 'match_other', 'create')
     *      reconciliation_proposition: {
     *          id: number|string
     *          partial_amount: number
     *          invalid: boolean - through the invalid line (without account, label...)
     *          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
     *          [to_check]: boolean
     *          [amount_currency_str]: string|false (amount in record currency)
     *      }
     *      mv_lines_match_rp: object - idem than reconciliation_proposition
     *      mv_lines_match_other: object - idem than reconciliation_proposition
     *      limitMoveLines: integer
     *      filter: string
     *      [createForm]: {
     *          account_id: {
     *              id: integer
     *              display_name: string
     *          }
     *          tax_ids: {
     *              id: integer
     *              display_name: string
     *          }
     *          analytic_account_id: {
     *              id: integer
     *              display_name: string
     *          }
     *          analytic_tag_ids: {
     *          }
     *          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_ids",
            "force_tax_included",
            "analytic_tag_ids",
            "to_check",
        ],

        // Overridden in ManualModel
        modes: ["create", "match_rp", "match_other"],

        /**
         * @override
         *
         * @param {Widget} parent
         * @param {Object} options
         */
        init: function (parent, options) {
            this._super.apply(this, arguments);
            this.reconcileModels = [];
            this.lines = {};
            this.valuenow = 0;
            this.valuemax = 0;
            this.alreadyDisplayed = [];
            this.domain = [];
            this.defaultDisplayQty = (options && options.defaultDisplayQty) || 10;
            this.limitMoveLines = (options && options.limitMoveLines) || 15;
            this.display_context = "init";
        },

        // --------------------------------------------------------------------------
        // 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_line_id
         * @returns {Promise}
         */
        addProposition: function (handle, mv_line_id) {
            var line = this.getLine(handle);
            var prop = _.clone(_.find(line["mv_lines_" + line.mode], {id: mv_line_id}));
            this._addProposition(line, prop);
            line["mv_lines_" + line.mode] = _.filter(
                line["mv_lines_" + line.mode],
                (l) => l.id != mv_line_id
            );

            // Remove all non valid lines
            line.reconciliation_proposition = _.filter(
                line.reconciliation_proposition,
                function (prop) {
                    return prop && !prop.invalid;
                }
            );

            // Onchange the partner if not already set on the statement line.
            if (
                !line.st_line.partner_id &&
                line.reconciliation_proposition &&
                line.reconciliation_proposition.length == 1 &&
                prop.partner_id &&
                line.type === undefined
            ) {
                return this.changePartner(
                    handle,
                    {id: prop.partner_id, display_name: prop.partner_name},
                    true
                );
            }

            return Promise.all([
                this._computeLine(line),
                this._performMoveLine(
                    handle,
                    "match_rp",
                    line.mode == "match_rp" ? 1 : 0
                ),
                this._performMoveLine(
                    handle,
                    "match_other",
                    line.mode == "match_other" ? 1 : 0
                ),
            ]);
        },
        /**
         * Change the filter for the target line and fetch the new matched lines
         *
         * @param {String} handle
         * @param {String} filter
         * @returns {Promise}
         */
        changeFilter: function (handle, filter) {
            var line = this.getLine(handle);
            line["filter_" + line.mode] = filter;
            line["mv_lines_" + line.mode] = [];
            return this._performMoveLine(handle, line.mode);
        },
        /**
         * Change the mode line ('inactive', 'match_rp', 'match_other', 'create'),
         * and fetch the new matched lines or prepare to create a new line
         *
         * ``match_rp``
         *   display the matched lines from receivable/payable accounts, the user
         *   can select the lines to apply there as proposition
         * ``match_other``
         *   display the other 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 {'inactive' | 'match_rp' | 'create'} mode
         * @returns {Promise}
         */
        changeMode: function (handle, mode) {
            var self = this;
            var line = this.getLine(handle);
            if (mode === "default") {
                var match_requests = self.modes
                    .filter((x) => x.startsWith("match"))
                    .map((x) => this._performMoveLine(handle, x));
                return Promise.all(match_requests).then(function () {
                    return self.changeMode(handle, self._getDefaultMode(handle));
                });
            }
            if (mode === "next") {
                var available_modes = self._getAvailableModes(handle);
                mode =
                    available_modes[
                        (available_modes.indexOf(line.mode) + 1) %
                            available_modes.length
                    ];
            }
            line.mode = mode;
            if (["match_rp", "match_other"].includes(line.mode)) {
                if (
                    !(
                        line["mv_lines_" + line.mode] &&
                        line["mv_lines_" + line.mode].length
                    )
                ) {
                    return this._performMoveLine(handle, line.mode);
                }
                return this._formatMoveLine(handle, line.mode, []);
            }
            if (line.mode === "create") {
                return this.createProposition(handle);
            }
            return Promise.resolve();
        },
        /**
         * Fetch the more matched lines
         *
         * @param {String} handle
         * @returns {Promise}
         */
        changeOffset: function (handle) {
            var line = this.getLine(handle);
            return this._performMoveLine(handle, line.mode);
        },
        /**
         * Change the partner on the line and fetch the new matched lines
         *
         * @param {String} handle
         * @param {Boolean} preserveMode
         * @param {Object} partner
         * @param {String} partner.display_name
         * @param {Number} partner.id
         * @returns {Promise}
         */
        changePartner: function (handle, partner, preserveMode) {
            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) || "";
            line.mv_lines_match_rp = [];
            line.mv_lines_match_other = [];
            return Promise.resolve(partner && this._changePartner(handle, partner.id))
                .then(function () {
                    if (line.st_line.partner_id) {
                        _.each(line.reconciliation_proposition, function (prop) {
                            if (prop.partner_id != line.st_line.partner_id) {
                                line.reconciliation_proposition = [];
                                return false;
                            }
                        });
                    }
                    return self._computeLine(line);
                })
                .then(function () {
                    return self.changeMode(
                        handle,
                        preserveMode ? line.mode : "default",
                        true
                    );
                });
        },
        // eslint-disable-next-line no-unused-vars
        changeRef: function (handle, ref, preserveMode) {
            var line = this.getLine(handle);
            line.st_line.ref = ref;
            return Promise.resolve();
        },

        /**
         * Close the statement
         * @returns {Promise<Number>} resolves to the res_id of the closed statements
         */
        closeStatement: function () {
            var self = this;
            return this._rpc({
                model: "account.bank.statement",
                method: "button_validate",
                args: [self.statement.statement_id],
            }).then(function () {
                return self.statement.statement_id;
            });
        },
        /**
         *
         * Then open the first available line
         *
         * @param {String} handle
         * @returns {Promise}
         */
        createProposition: function (handle) {
            var line = this.getLine(handle);
            var prop = _.filter(line.reconciliation_proposition, "__focus");
            prop = this._formatQuickCreate(line);
            line.reconciliation_proposition.push(prop);
            line.createForm = _.pick(prop, this.quickCreateFields);
            return this._computeLine(line);
        },
        /**
         * Return context information and journal_id
         * @returns {Object} context
         */
        getContext: function () {
            return this.context;
        },
        /**
         * Return the lines that needs to be displayed by the widget
         *
         * @returns {Object} lines that are loaded and not yet displayed
         */
        getStatementLines: function () {
            var self = this;
            var linesToDisplay = _.pick(this.lines, function (value, key, object) {
                if (
                    value.visible === true &&
                    self.alreadyDisplayed.indexOf(key) === -1
                ) {
                    self.alreadyDisplayed.push(key);
                    return object;
                }
            });
            return linesToDisplay;
        },
        /**
         * Return a boolean telling if load button needs to be displayed or not
         * overridden in ManualModel
         *
         * @returns {Boolean} true if load more button needs to be displayed
         */
        hasMoreLines: function () {
            var notDisplayed = _.filter(this.lines, function (line) {
                return !line.visible;
            });
            if (notDisplayed.length > 0) {
                return true;
            }
            return false;
        },
        /**
         * Get the line data for this handle
         *
         * @param {Object} handle
         * @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.reconciliation.widget' fetch each line data
         *
         * overridden in ManualModel
         * @param {Object} context
         * @param {Number[]} context.statement_line_ids
         * @returns {Promise}
         */
        load: function (context) {
            var self = this;
            this.context = context;
            this.statement_line_ids = context.statement_line_ids;
            if (this.statement_line_ids === undefined) {
                // This could be undefined if the user pressed F5, take everything as fallback instead of rainbowman
                return self
                    ._rpc({
                        model: "account.bank.statement.line",
                        method: "search_read",
                        fields: ["id"],
                        domain: [["journal_id", "=?", context.active_id]],
                    })
                    .then(function (result) {
                        self.statement_line_ids = result.map((r) => r.id);
                        return self.reload();
                    });
            }
            return self.reload();
        },
        /**
         * Load more bank statement line
         *
         * @param {integer} qty quantity to load
         * @returns {Promise}
         */
        loadMore: function (qty) {
            if (qty === undefined) {
                qty = this.defaultDisplayQty;
            }
            var ids = _.pluck(this.lines, "id");
            ids = ids.splice(this.pagerIndex, qty);
            this.pagerIndex += qty;
            return this.loadData(ids, this._getExcludedIds());
        },
        /**
         * RPC method to load informations on lines
         * overridden in ManualModel
         *
         * @param {Array} ids ids of bank statement line passed to rpc call
         * @param {Array} excluded_ids list of move_line ids that needs to be excluded from search
         * @returns {Promise}
         */
        loadData: function (ids) {
            var self = this;
            var excluded_ids = this._getExcludedIds();
            return self
                ._rpc({
                    model: "account.reconciliation.widget",
                    method: "get_bank_statement_line_data",
                    args: [ids, excluded_ids],
                    context: self.context,
                })
                .then(function (res) {
                    return self._formatLine(res.lines);
                });
        },
        /**
         * Reload all data
         */
        reload: function () {
            var self = this;
            self.alreadyDisplayed = [];
            self.lines = {};
            self.pagerIndex = 0;
            var def_statement = this._rpc({
                model: "account.reconciliation.widget",
                method: "get_bank_statement_data",
                kwargs: {
                    bank_statement_line_ids: self.statement_line_ids,
                    srch_domain: self.domain,
                },
                context: self.context,
            }).then(function (statement) {
                self.statement = statement;
                self.bank_statement_line_id =
                    self.statement_line_ids.length === 1
                        ? {
                              id: self.statement_line_ids[0],
                              display_name: statement.statement_name,
                          }
                        : false;
                self.valuenow = self.valuenow || statement.value_min;
                self.valuemax = self.valuemax || statement.value_max;
                self.context.journal_id = statement.journal_id;
                _.each(statement.lines, function (res) {
                    var handle = _.uniqueId("rline");
                    self.lines[handle] = {
                        id: res.st_line.id,
                        partner_id: res.st_line.partner_id,
                        ref: res.st_line.ref,
                        handle: handle,
                        reconciled: false,
                        mode: "inactive",
                        mv_lines_match_rp: [],
                        mv_lines_match_other: [],
                        filter_match_rp: "",
                        filter_match_other: "",
                        reconciliation_proposition: [],
                        reconcileModels: [],
                    };
                });
            });
            var domainReconcile = [];
            if (self.context && self.context.company_ids) {
                domainReconcile.push(["company_id", "in", self.context.company_ids]);
            }
            if (
                self.context &&
                self.context.active_model === "account.journal" &&
                self.context.active_ids
            ) {
                domainReconcile.push("|");
                domainReconcile.push(["match_journal_ids", "=", false]);
                domainReconcile.push([
                    "match_journal_ids",
                    "in",
                    self.context.active_ids,
                ]);
            }
            var def_reconcileModel = this._loadReconciliationModel({
                domainReconcile: domainReconcile,
            });
            var def_account = this._rpc({
                model: "account.account",
                method: "search_read",
                fields: ["code"],
            }).then(function (accounts) {
                self.accounts = _.object(
                    _.pluck(accounts, "id"),
                    _.pluck(accounts, "code")
                );
            });
            var def_analytic_account = this._rpc({
                model: "account.analytic.account",
                method: "search_read",
                fields: ["display_name"],
            }).then(function (accounts) {
                self.analytic_accounts = _.object(
                    _.pluck(accounts, "id"),
                    _.pluck(accounts, "display_name")
                );
            });
            var def_taxes = self._loadTaxes();
            return Promise.all([
                def_statement,
                def_reconcileModel,
                def_account,
                def_analytic_account,
                def_taxes,
            ]).then(function () {
                _.each(self.lines, function (line) {
                    line.reconcileModels = self.reconcileModels;
                });
                var ids = _.pluck(self.lines, "id");
                ids = ids.splice(0, self.defaultDisplayQty);
                self.pagerIndex = ids.length;
                return self._formatLine(self.statement.lines);
            });
        },
        _readAnalyticTags: function (params) {
            var self = this;
            this.analyticTags = {};
            if (!params || !params.res_ids || !params.res_ids.length) {
                return $.when();
            }
            var fields = ((params && params.fields) || []).concat([
                "id",
                "display_name",
            ]);
            return this._rpc({
                model: "account.analytic.tag",
                method: "read",
                args: [params.res_ids, fields],
            }).then(function (tags) {
                for (var i = 0; i < tags.length; i++) {
                    var tag = tags[i];
                    self.analyticTags[tag.id] = tag;
                }
            });
        },
        _loadReconciliationModel: function (params) {
            const def = $.Deferred();
            this._rpc({
                model: "account.reconcile.model",
                method: "search_read",
                domain: params.domainReconcile || [],
            }).then((reconcileModels) => {
                const recIds = _.map(reconcileModels, "id");
                // Clean
                for (const reconcileModel of reconcileModels) {
                    reconcileModel.line_ids = [];
                }
                this._rpc({
                    model: "account.reconcile.model.line",
                    method: "search_read",
                    domain: [["model_id", "in", recIds]],
                }).then((reconcileModelLines) => {
                    let analyticTagIds = [];
                    for (const reconcileModelLine of reconcileModelLines) {
                        const reconcileModelByLine = _.findWhere(reconcileModels, {
                            id: reconcileModelLine.model_id[0],
                        });
                        reconcileModelByLine.line_ids.push(reconcileModelLine);
                        analyticTagIds = analyticTagIds.concat(
                            reconcileModelLine.analytic_tag_ids || []
                        );
                    }
                    analyticTagIds = _.unique(analyticTagIds);
                    this._readAnalyticTags({res_ids: analyticTagIds}).then(() => {
                        for (const reconcileModel of reconcileModels) {
                            var analyticTagData = [];
                            var modelTags = reconcileModel.analytic_tag_ids || [];
                            for (const tagId of modelTags) {
                                analyticTagData.push([
                                    tagId,
                                    this.analyticTags[tagId].display_name,
                                ]);
                            }
                            reconcileModel.analytic_tag_ids = analyticTagData;
                        }

                        this.reconcileModels = reconcileModels;
                        return def.resolve();
                    });
                });
            });

            return def;
        },
        _loadTaxes: function () {
            var self = this;
            self.taxes = {};
            return this._rpc({
                model: "account.tax",
                method: "search_read",
                fields: ["price_include", "name"],
            }).then(function (taxes) {
                _.each(taxes, function (tax) {
                    self.taxes[tax.id] = {
                        price_include: tax.price_include,
                        display_name: tax.name,
                    };
                });
                return taxes;
            });
        },
        /**
         * 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} reconcileModelId
         * @returns {Promise}
         */
        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",
                "force_tax_included",
                "tax_ids",
                "analytic_tag_ids",
                "to_check",
                "amount_string",
                "decimal_separator",
            ];
            this._blurProposition(handle);
            let focus = null;
            const defs = [];
            for (const reconcileModelLineIndex in reconcileModel.line_ids) {
                const reconcileModelLine =
                    reconcileModel.line_ids[reconcileModelLineIndex];
                if (reconcileModelLineIndex === "0") {
                    focus = this._formatQuickCreate(
                        line,
                        _.pick(reconcileModelLine, fields),
                        reconcileModel
                    );
                    focus.reconcileModelId = reconcileModelId;
                    line.reconciliation_proposition.push(focus);
                } else {
                    defs.push(
                        this._computeLine(line).then(() => {
                            const second_focus = this._formatQuickCreate(
                                line,
                                _.pick(reconcileModelLine, fields),
                                reconcileModel
                            );
                            second_focus.reconcileModelId = reconcileModelId;
                            line.reconciliation_proposition.push(second_focus);
                            this._computeReconcileModels(handle, reconcileModelId);
                        })
                    );
                }
            }
            return Promise.all(defs).then(() => {
                line.createForm = _.pick(focus, this.quickCreateFields);
                return this._computeLine(line);
            });
        },
        /**
         * Remove a proposition and switch to an active mode ('create' or 'match_rp' or 'match_other')
         * overridden in ManualModel
         *
         * @param {String} handle
         * @param {Number} id (move line id)
         * @returns {Promise}
         */
        removeProposition: function (handle, id) {
            var self = this;
            var line = this.getLine(handle);
            var defs = [];
            var prop = _.find(line.reconciliation_proposition, {id: id});
            if (prop) {
                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)
                        );
                    }
                );
                if (prop.reconcileModelId === undefined) {
                    if (
                        ["receivable", "payable", "liquidity"].includes(
                            prop.account_type
                        )
                    ) {
                        line.mv_lines_match_rp.unshift(prop);
                    } else {
                        line.mv_lines_match_other.unshift(prop);
                    }
                }

                // No proposition left and then, reset the st_line partner.
                if (
                    line.reconciliation_proposition.length == 0 &&
                    line.st_line.has_no_partner
                )
                    defs.push(self.changePartner(line.handle));
            }
            line.mode =
                (id || line.mode !== "create") && isNaN(id) ? "create" : "match_rp";
            defs.push(this._computeLine(line));
            return Promise.all(defs).then(function () {
                return self.changeMode(handle, line.mode, true);
            });
        },
        getPartialReconcileAmount: function (handle, data) {
            var line = this.getLine(handle);
            var formatOptions = {
                currency_id: line.st_line.currency_id,
                noSymbol: true,
            };
            var prop = _.find(line.reconciliation_proposition, {id: data.data});
            if (prop) {
                var amount = prop.partial_amount || prop.amount;
                // Check if we can get a partial amount that would directly set balance to zero
                var partial = Math.abs(line.balance.amount + amount);
                if (Math.abs(line.balance.amount) >= Math.abs(amount)) {
                    amount = Math.abs(amount);
                } else if (partial <= Math.abs(prop.amount) && partial >= 0) {
                    amount = partial;
                } else {
                    amount = Math.abs(amount);
                }
                return field_utils.format.monetary(amount, {}, formatOptions);
            }
        },
        /**
         * Force the partial reconciliation to display the reconciliate button.
         *
         * @param {String} handle
         * @returns {Promise}
         */
        partialReconcile: function (handle, data) {
            var line = this.getLine(handle);
            var prop = _.find(line.reconciliation_proposition, {id: data.mvLineId});
            if (prop) {
                var amount = data.amount;
                try {
                    amount = field_utils.parse.float(data.amount);
                } catch (err) {
                    amount = NaN;
                }
                // Amount can't be greater than line.amount and can not be negative and must be a number
                // the amount we receive will be a string, so take sign of previous line amount in consideration in order to put
                // the amount in the correct left or right column
                if (amount >= Math.abs(prop.amount) || amount <= 0 || isNaN(amount)) {
                    delete prop.partial_amount_str;
                    delete prop.partial_amount;
                    if (isNaN(amount) || amount < 0) {
                        this.do_warn(
                            _.str.sprintf(
                                _t("The amount %s is not a valid partial amount"),
                                data.amount
                            )
                        );
                    }
                    return this._computeLine(line);
                }
                var format_options = {currency_id: line.st_line.currency_id};
                prop.partial_amount = (prop.amount > 0 ? 1 : -1) * amount;
                prop.partial_amount_str = field_utils.format.monetary(
                    Math.abs(prop.partial_amount),
                    {},
                    format_options
                );
            }
            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 {*} values
         * @returns {Promise}
         */
        updateProposition: function (handle, values) {
            var self = this;
            var line = this.getLine(handle);
            var prop = _.last(_.filter(line.reconciliation_proposition, "__focus"));
            if ("to_check" in values && values.to_check === false) {
                // Check if we have another line with to_check and if yes don't change value of this proposition
                prop.to_check = line.reconciliation_proposition.some(function (
                    rec_prop,
                    // eslint-disable-next-line no-unused-vars
                    index
                ) {
                    return rec_prop.id !== prop.id && rec_prop.to_check;
                });
            }
            if (!prop) {
                prop = this._formatQuickCreate(line);
                line.reconciliation_proposition.push(prop);
            }
            _.each(values, function (value, fieldName) {
                if (fieldName === "analytic_tag_ids") {
                    switch (value.operation) {
                        case "ADD_M2M":
                            // Handle analytic_tag selection via drop down (single dict) and
                            // full widget (array of dict)
                            var vids = _.isArray(value.ids) ? value.ids : [value.ids];
                            _.each(vids, function (val) {
                                if (!_.findWhere(prop.analytic_tag_ids, {id: val.id})) {
                                    prop.analytic_tag_ids.push(val);
                                }
                            });
                            break;
                        case "FORGET":
                            var id = self.localData[value.ids[0]].ref;
                            prop.analytic_tag_ids = _.filter(
                                prop.analytic_tag_ids,
                                function (val) {
                                    return val.id !== id;
                                }
                            );
                            break;
                    }
                } else if (fieldName === "tax_ids") {
                    switch (value.operation) {
                        case "ADD_M2M":
                            prop.__tax_to_recompute = true;
                            var vids = _.isArray(value.ids) ? value.ids : [value.ids];
                            _.each(vids, function (val) {
                                if (!_.findWhere(prop.tax_ids, {id: val.id})) {
                                    value.ids.price_include = self.taxes[val.id]
                                        ? self.taxes[val.id].price_include
                                        : false;
                                    prop.tax_ids.push(val);
                                }
                            });
                            break;
                        case "FORGET":
                            prop.__tax_to_recompute = true;
                            var id = self.localData[value.ids[0]].ref;
                            prop.tax_ids = _.filter(prop.tax_ids, function (val) {
                                return val.id !== id;
                            });
                            break;
                    }
                } else {
                    prop[fieldName] = values[fieldName];
                }
            });
            if ("account_id" in values) {
                prop.account_code = prop.account_id
                    ? this.accounts[prop.account_id.id]
                    : "";
            }
            if ("analytic_account_id" in values) {
                prop.analytic_account_code = prop.analytic_account_id
                    ? this.analytic_accounts[prop.analytic_account_id.id]
                    : "";
            }

            if ("amount" in values) {
                prop.base_amount = values.amount;
                if (prop.reconcileModelId) {
                    this._computeReconcileModels(handle, prop.reconcileModelId);
                }
            }
            if (
                "force_tax_included" in values ||
                "amount" in values ||
                "account_id" in values
            ) {
                prop.__tax_to_recompute = true;
            }
            line.createForm = _.pick(prop, this.quickCreateFields);
            // If you check/uncheck the force_tax_included box, reset the createForm amount.
            if (prop.base_amount) line.createForm.amount = prop.base_amount;
            if (prop.tax_ids.length !== 1) {
                // When we have 0 or more than 1 taxes, reset the base_amount and force_tax_included, otherwise weird behavior can happen
                prop.amount = prop.base_amount;
                line.createForm.force_tax_included = false;
            }
            return this._computeLine(line);
        },
        /**
         * Format the value and send it to 'account.reconciliation.widget' model
         * Update the number of validated lines
         * overridden in ManualModel
         *
         * @param {(String|String[])} handle
         * @returns {Promise<Object>} resolved with an object who contains
         *   'handles' key
         */
        validate: function (handle) {
            var self = this;
            this.display_context = "validate";
            var handles = [];
            if (handle) {
                handles = [handle];
            } else {
                _.each(this.lines, function (line, handle) {
                    if (
                        !line.reconciled &&
                        line.balance &&
                        !line.balance.amount &&
                        line.reconciliation_proposition.length
                    ) {
                        handles.push(handle);
                    }
                });
            }
            var ids = [];
            var values = [];
            var handlesPromises = [];
            _.each(handles, function (handle) {
                var line = self.getLine(handle);
                var props = _.filter(line.reconciliation_proposition, function (prop) {
                    return !prop.invalid;
                });
                var computeLinePromise = null;
                if (props.length === 0) {
                    // Usability: if user has not chosen any lines and click validate, it has the same behavior
                    // as creating a write-off of the same amount.
                    props.push(
                        self._formatQuickCreate(line, {
                            account_id: [
                                line.st_line.open_balance_account_id,
                                self.accounts[line.st_line.open_balance_account_id],
                            ],
                        })
                    );
                    // Update balance of line otherwise it won't be to zero and another line will be added
                    line.reconciliation_proposition.push(props[0]);
                    computeLinePromise = self._computeLine(line);
                }
                ids.push(line.id);
                handlesPromises.push(
                    Promise.resolve(computeLinePromise).then(function () {
                        var values_dict = {
                            partner_id: line.st_line.partner_id,
                            ref: line.st_line.ref,
                            counterpart_aml_dicts: _.map(
                                _.filter(props, function (prop) {
                                    return !isNaN(prop.id) && !prop.already_paid;
                                }),
                                self._formatToProcessReconciliation.bind(self, line)
                            ),
                            payment_aml_ids: _.pluck(
                                _.filter(props, function (prop) {
                                    return !isNaN(prop.id) && prop.already_paid;
                                }),
                                "id"
                            ),
                            new_aml_dicts: _.map(
                                _.filter(props, function (prop) {
                                    return isNaN(prop.id) && prop.display;
                                }),
                                self._formatToProcessReconciliation.bind(self, line)
                            ),
                            to_check: line.to_check,
                        };

                        // If the lines are not fully balanced, create an unreconciled amount.
                        // line.st_line.currency_id is never false here because its equivalent to
                        // statement_line.currency_id or statement_line.journal_id.currency_id or statement_line.journal_id.company_id.currency_id (Python-side).
                        // see: get_statement_line_for_reconciliation_widget method in account/models/account_bank_statement.py for more details
                        var currency = session.get_currency(line.st_line.currency_id);
                        var balance = line.balance.amount;
                        if (!utils.float_is_zero(balance, currency.digits[1])) {
                            var unreconciled_amount_dict = {
                                account_id: line.st_line.open_balance_account_id,
                                credit: balance > 0 ? balance : 0,
                                debit: balance < 0 ? -balance : 0,
                                name: line.st_line.name + " : " + _t("Open balance"),
                            };
                            values_dict.new_aml_dicts.push(unreconciled_amount_dict);
                        }
                        values.push(values_dict);
                        line.reconciled = true;
                    })
                );

                _.each(self.lines, function (other_line) {
                    if (other_line != line) {
                        var filtered_prop = other_line.reconciliation_proposition.filter(
                            (p) =>
                                !line.reconciliation_proposition
                                    .map((l) => l.id)
                                    .includes(p.id)
                        );
                        if (
                            filtered_prop.length !=
                            other_line.reconciliation_proposition.length
                        ) {
                            other_line.need_update = true;
                            other_line.reconciliation_proposition = filtered_prop;
                        }
                        self._computeLine(line);
                    }
                });
            });

            return Promise.all(handlesPromises).then(function () {
                return self
                    ._rpc({
                        model: "account.reconciliation.widget",
                        method: "process_bank_statement_line",
                        args: [ids, values],
                        context: self.context,
                    })
                    .then(self._validatePostProcess.bind(self))
                    .then(function () {
                        self.valuenow += handles.length;
                        return {handles: handles};
                    });
            });
        },

        // --------------------------------------------------------------------------
        // Private
        // --------------------------------------------------------------------------

        /**
         * add a line proposition after checking receivable and payable accounts constraint
         *
         * @private
         * @param {Object} line
         * @param {Object} prop
         */
        _addProposition: function (line, prop) {
            line.reconciliation_proposition.push(prop);
        },
        /**
         * Stop the editable proposition line and remove it if it's invalid then
         * compute the line
         *
         * See :func:`_computeLine`
         *
         * @private
         * @param {String} handle
         * @returns {Promise}
         */
        _blurProposition: function (handle) {
            var line = this.getLine(handle);
            line.reconciliation_proposition = _.filter(
                line.reconciliation_proposition,
                function (l) {
                    l.__focus = false;
                    return !l.invalid;
                }
            );
        },
        /**
         * When changing partner, read property_account_receivable and payable
         * of that partner because the counterpart account might cahnge depending
         * on the partner
         *
         * @private
         * @param {String} handle
         * @param {integer} partner_id
         * @returns {Promise}
         */
        _changePartner: function (handle, partner_id) {
            var self = this;
            return this._rpc({
                model: "res.partner",
                method: "read",
                args: [
                    partner_id,
                    ["property_account_receivable_id", "property_account_payable_id"],
                ],
            }).then(function (result) {
                if (result.length > 0) {
                    var line = self.getLine(handle);
                    self.lines[handle].st_line.open_balance_account_id =
                        line.balance.amount < 0
                            ? result[0].property_account_payable_id[0]
                            : result[0].property_account_receivable_id[0];
                }
            });
        },
        /**
         * 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_ids
         * extended by ManualModel
         *
         * @private
         * @param {Object} line
         * @returns {Promise}
         */
        _computeLine: function (line) {
            // Balance_type
            var self = this;

            // Compute taxes
            var tax_defs = [];
            var reconciliation_proposition = [];
            var formatOptions = {
                currency_id: line.st_line.currency_id,
            };
            line.to_check = false;
            _.each(line.reconciliation_proposition, function (prop) {
                if (prop.to_check) {
                    // If one of the proposition is to_check, set the global to_check flag to true
                    line.to_check = true;
                }
                if (prop.tax_repartition_line_id) {
                    if (
                        !_.find(line.reconciliation_proposition, {id: prop.link})
                            .__tax_to_recompute
                    ) {
                        reconciliation_proposition.push(prop);
                    }
                    return;
                }
                if (!prop.already_paid && parseInt(prop.id)) {
                    prop.is_move_line = true;
                }
                reconciliation_proposition.push(prop);

                if (
                    prop.tax_ids &&
                    prop.tax_ids.length &&
                    prop.__tax_to_recompute &&
                    prop.base_amount
                ) {
                    // (OZM) REVISAR
                    reconciliation_proposition = _.filter(
                        reconciliation_proposition,
                        function (p) {
                            return !p.tax_repartition_line_id || p.link !== prop.id;
                        }
                    );
                    var args = [
                        prop.tax_ids.map(function (el) {
                            return el.id;
                        }),
                        prop.base_amount,
                        formatOptions.currency_id,
                    ];
                    var add_context = {round: true};
                    if (
                        prop.tax_ids.length === 1 &&
                        line.createForm &&
                        line.createForm.force_tax_included
                    )
                        add_context.force_price_include = true;
                    tax_defs.push(
                        self
                            ._rpc({
                                model: "account.tax",
                                method: "json_friendly_compute_all",
                                args: args,
                                context: $.extend({}, self.context || {}, add_context),
                            })
                            .then(function (result) {
                                _.each(result.taxes, function (tax) {
                                    var tax_prop = self._formatQuickCreate(line, {
                                        link: prop.id,
                                        tax_ids: tax.tax_ids,
                                        tax_repartition_line_id:
                                            tax.tax_repartition_line_id,
                                        tax_tag_ids: tax.tag_ids,
                                        amount: tax.amount,
                                        name: prop.name
                                            ? prop.name + " " + tax.name
                                            : tax.name,
                                        date: prop.date,
                                        account_id: tax.account_id
                                            ? [tax.account_id, null]
                                            : prop.account_id,
                                        analytic: tax.analytic,
                                        __focus: false,
                                    });

                                    prop.tax_exigible =
                                        tax.tax_exigibility === "on_payment"
                                            ? true
                                            : undefined;
                                    prop.amount = tax.base;
                                    prop.amount_str = field_utils.format.monetary(
                                        Math.abs(prop.amount),
                                        {},
                                        formatOptions
                                    );
                                    prop.invalid = !self._isValid(prop);

                                    tax_prop.amount_str = field_utils.format.monetary(
                                        Math.abs(tax_prop.amount),
                                        {},
                                        formatOptions
                                    );
                                    tax_prop.invalid = prop.invalid;

                                    reconciliation_proposition.push(tax_prop);
                                });

                                prop.tax_tag_ids = self._formatMany2ManyTagsTax(
                                    result.base_tags || []
                                );
                            })
                    );
                } else {
                    prop.amount_str = field_utils.format.monetary(
                        Math.abs(prop.amount),
                        {},
                        formatOptions
                    );
                    prop.display = self._isDisplayedProposition(prop);
                    prop.invalid = !self._isValid(prop);
                }
            });

            return Promise.all(tax_defs).then(function () {
                _.each(reconciliation_proposition, function (prop) {
                    prop.__tax_to_recompute = false;
                });
                line.reconciliation_proposition = reconciliation_proposition;

                var amount_currency = 0;
                var total = line.st_line.amount || 0;
                var isOtherCurrencyId = _.uniq(
                    _.pluck(
                        _.reject(reconciliation_proposition, "invalid"),
                        "currency_id"
                    )
                );
                isOtherCurrencyId =
                    isOtherCurrencyId.length === 1 &&
                    !total &&
                    isOtherCurrencyId[0] !== formatOptions.currency_id
                        ? isOtherCurrencyId[0]
                        : false;

                _.each(reconciliation_proposition, function (prop) {
                    if (!prop.invalid) {
                        total -= prop.partial_amount || prop.amount;
                        if (isOtherCurrencyId) {
                            amount_currency -=
                                (prop.amount < 0 ? -1 : 1) *
                                Math.abs(prop.amount_currency);
                        }
                    }
                });
                var company_currency = session.get_currency(line.st_line.currency_id);
                var company_precision =
                    (company_currency && company_currency.digits[1]) || 2;
                total = utils.round_decimals(total, company_precision) || 0;
                if (isOtherCurrencyId) {
                    var other_currency = session.get_currency(isOtherCurrencyId);
                    var other_precision =
                        (other_currency && other_currency.digits[1]) || 2;
                    amount_currency = utils.round_decimals(
                        amount_currency,
                        other_precision
                    );
                }
                line.balance = {
                    amount: total,
                    amount_str: field_utils.format.monetary(
                        Math.abs(total),
                        {},
                        formatOptions
                    ),
                    currency_id: isOtherCurrencyId,
                    amount_currency: isOtherCurrencyId ? amount_currency : total,
                    amount_currency_str: isOtherCurrencyId
                        ? field_utils.format.monetary(
                              Math.abs(amount_currency),
                              {},
                              {
                                  currency_id: isOtherCurrencyId,
                              }
                          )
                        : false,
                    account_code: self.accounts[line.st_line.open_balance_account_id],
                };
                line.balance.show_balance = line.balance.amount_currency != 0;
                line.balance.type = line.balance.amount_currency
                    ? line.st_line.partner_id
                        ? 0
                        : -1
                    : 1;
            });
        },
        /**
         *
         *
         * @private
         * @param {String} handle
         * @param {integer} reconcileModelId
         */
        _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} [value] data or name_get
         */
        _formatNameGet: function (value) {
            return value
                ? value.id
                    ? value
                    : {id: value[0], display_name: value[1]}
                : false;
        },
        _formatMany2ManyTags: function (value) {
            var res = [];
            for (var i = 0, len = value.length; i < len; i++) {
                res.push({
                    id: value[i],
                    display_name: this.analyticTags[value[i]]
                        ? this.analyticTags[value[i]].display_name
                        : "",
                });
            }
            return res;
        },
        _formatMany2ManyTagsTax: function (value) {
            var res = [];
            for (var i = 0; i < value.length; i++) {
                res.push({
                    id: value[i],
                    display_name: this.taxes[value[i]]
                        ? this.taxes[value[i]].display_name
                        : "",
                });
            }
            return res;
        },
        /**
         * Format each propositions (amount, label, account_id)
         * extended in ManualModel
         *
         * @private
         * @param {Object} line
         * @param {Object[]} props
         */
        _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
                    );
                    prop.is_partially_reconciled =
                        prop.amount_str !== prop.total_amount_str;
                    prop.to_check = Boolean(prop.to_check);
                });
            }
        },
        /**
         * Format each server lines and propositions and compute all lines
         * overridden in ManualModel
         *
         * @see '_computeLine'
         *
         * @private
         * @param {Object[]} lines
         * @returns {Promise}
         */
        _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;
                });
                line.visible = true;
                line.limitMoveLines = self.limitMoveLines;
                _.extend(line, data);
                self._formatLineProposition(line, line.reconciliation_proposition);
                if (!line.reconciliation_proposition.length) {
                    delete line.reconciliation_proposition;
                }

                // No partner set on st_line and all matching amls have the same one: set it on the st_line.
                defs.push(
                    self
                        ._computeLine(line)
                        .then(function () {
                            if (
                                !line.st_line.partner_id &&
                                line.reconciliation_proposition.length > 0
                            ) {
                                var hasDifferentPartners = function (prop) {
                                    return (
                                        !prop.partner_id ||
                                        prop.partner_id !=
                                            line.reconciliation_proposition[0]
                                                .partner_id
                                    );
                                };

                                if (
                                    !_.any(
                                        line.reconciliation_proposition,
                                        hasDifferentPartners
                                    )
                                ) {
                                    return self.changePartner(
                                        line.handle,
                                        {
                                            id:
                                                line.reconciliation_proposition[0]
                                                    .partner_id,
                                            display_name:
                                                line.reconciliation_proposition[0]
                                                    .partner_name,
                                        },
                                        true
                                    );
                                }
                            } else if (
                                !line.st_line.partner_id &&
                                line.partner_id &&
                                line.partner_name
                            ) {
                                return self.changePartner(
                                    line.handle,
                                    {
                                        id: line.partner_id,
                                        display_name: line.partner_name,
                                    },
                                    true
                                );
                            }
                            return true;
                        })
                        .then(function () {
                            return data.write_off
                                ? self.quickCreateProposition(
                                      line.handle,
                                      data.model_id
                                  )
                                : true;
                        })
                        .then(function () {
                            // If still no partner set, take the one from context, if it exists
                            if (
                                !line.st_line.partner_id &&
                                self.context.partner_id &&
                                self.context.partner_name
                            ) {
                                return self.changePartner(
                                    line.handle,
                                    {
                                        id: self.context.partner_id,
                                        display_name: self.context.partner_name,
                                    },
                                    true
                                );
                            }
                            return true;
                        })
                );
            });
            return Promise.all(defs);
        },
        /**
         * Format the server value then compute the line
         * overridden in ManualModel
         *
         * @see '_computeLine'
         *
         * @private
         * @param {String} handle
         * @param {Object[]} mv_lines
         * @returns {Promise}
         */
        _formatMoveLine: function (handle, mode, mv_lines) {
            var self = this;
            var line = this.getLine(handle);
            line["mv_lines_" + mode] = _.uniq(
                line["mv_lines_" + mode].concat(mv_lines),
                (l) => l.id
            );
            if (mv_lines[0]) {
                line["remaining_" + mode] = mv_lines[0].recs_count - mv_lines.length;
            } else if (line["mv_lines_" + mode].lenght == 0) {
                line["remaining_" + mode] = 0;
            }
            this._formatLineProposition(line, mv_lines);

            if (
                (line.mode == "match_other" || line.mode == "match_rp") &&
                !line["mv_lines_" + mode].length &&
                !line["filter_" + mode].length
            ) {
                line.mode = self._getDefaultMode(handle);
                if (
                    line.mode !== "match_rp" &&
                    line.mode !== "match_other" &&
                    line.mode !== "inactive"
                ) {
                    return this._computeLine(line).then(function () {
                        return self.createProposition(handle);
                    });
                }
            } else {
                return this._computeLine(line);
            }
        },
        /**
         * Overridden in ManualModel
         */
        _getDefaultMode: function (handle) {
            var line = this.getLine(handle);
            if (
                line.balance.amount === 0 &&
                (!line.st_line.mv_lines_match_rp ||
                    line.st_line.mv_lines_match_rp.length === 0) &&
                (!line.st_line.mv_lines_match_other ||
                    line.st_line.mv_lines_match_other.length === 0)
            ) {
                return "inactive";
            }
            if (line.mv_lines_match_rp && line.mv_lines_match_rp.length) {
                return "match_rp";
            }
            if (line.mv_lines_match_other && line.mv_lines_match_other.length) {
                return "match_other";
            }
            return "create";
        },
        _getAvailableModes: function (handle) {
            var line = this.getLine(handle);
            var modes = [];
            if (line.mv_lines_match_rp && line.mv_lines_match_rp.length) {
                modes.push("match_rp");
            }
            if (line.mv_lines_match_other && line.mv_lines_match_other.length) {
                modes.push("match_other");
            }
            modes.push("create");
            return modes;
        },
        /**
         * Apply default values for the proposition, format datas and format the
         * base_amount with the decimal number from the currency
         * extended in ManualModel
         *
         * @private
         * @param {Object} line
         * @param {Object} values
         * @returns {Object}
         */
        _formatQuickCreate: function (line, values, reconcileModel) {
            values = values || {};
            var today = new moment().utc().format();
            var account = this._formatNameGet(values.account_id);
            var analytic_account = this._formatNameGet(values.analytic_account_id);
            var formatOptions = {
                currency_id: line.st_line.currency_id,
            };
            var amount = 0;
            switch (values.amount_type) {
                case "percentage":
                    amount = (line.balance.amount * values.amount) / 100;
                    break;
                case "regex":
                    var matching = line.st_line.payment_ref.match(
                        new RegExp(values.amount_string)
                    );
                    if (matching && matching.length == 2) {
                        matching = matching[1].replace(
                            new RegExp(
                                "[^-\\d" + reconcileModel.decimal_separator + "]",
                                "g"
                            ),
                            ""
                        );
                        matching = matching.replace(
                            reconcileModel.decimal_separator,
                            "."
                        );
                        amount = parseFloat(matching) || 0;
                        amount = line.balance.amount > 0 ? amount : -amount;
                    }
                    break;
                case "fixed":
                    amount = values.amount;
                    break;
                default:
                    amount =
                        values.amount !== undefined
                            ? values.amount
                            : line.balance.amount;
            }

            var prop = {
                id: _.uniqueId("createLine"),
                label: values.label || line.st_line.payment_ref,
                account_id: account,
                account_code: account ? this.accounts[account.id] : "",
                analytic_account_id: analytic_account,
                analytic_account_code: analytic_account
                    ? this.analytic_accounts[analytic_account.id]
                    : "",
                analytic_tag_ids: this._formatMany2ManyTags(
                    values.analytic_tag_ids || []
                ),
                journal_id: this._formatNameGet(values.journal_id),
                tax_ids: this._formatMany2ManyTagsTax(values.tax_ids || []),
                tax_tag_ids: this._formatMany2ManyTagsTax(values.tax_tag_ids || []),
                tax_repartition_line_id: values.tax_repartition_line_id,
                debit: 0,
                credit: 0,
                date: values.date
                    ? values.date
                    : field_utils.parse.date(today, {}, {isUTC: true}),
                force_tax_included: values.force_tax_included || false,
                base_amount: amount,
                percent: values.amount_type === "percentage" ? values.amount : null,
                link: values.link,
                display: true,
                invalid: true,
                to_check: Boolean(values.to_check),
                __tax_to_recompute: true,
                __focus: "__focus" in values ? values.__focus : true,
            };
            if (prop.base_amount) {
                // Call to format and parse needed to round the value to the currency precision
                var sign = prop.base_amount < 0 ? -1 : 1;
                var amount = field_utils.format.monetary(
                    Math.abs(prop.base_amount),
                    {},
                    formatOptions
                );
                prop.base_amount =
                    sign * field_utils.parse.monetary(amount, {}, formatOptions);
            }

            prop.amount = prop.base_amount;
            return prop;
        },
        /**
         * Return list of account_move_line that has been selected and needs to be removed
         * from other calls.
         *
         * @private
         * @returns {Array} list of excluded ids
         */
        _getExcludedIds: function () {
            var excludedIds = [];
            _.each(this.lines, function (line) {
                if (line.reconciliation_proposition) {
                    _.each(line.reconciliation_proposition, function (prop) {
                        if (parseInt(prop.id)) {
                            excludedIds.push(prop.id);
                        }
                    });
                }
            });
            return excludedIds;
        },
        /**
         * Defined whether the line is to be displayed or not. Here, we only display
         * the line if it comes from the server or if an account is defined when it
         * is created
         * extended in ManualModel
         *
         * @private
         * @param {Object} prop
         * @returns {Boolean}
         */
        _isDisplayedProposition: function (prop) {
            return !isNaN(prop.id) || Boolean(prop.account_id);
        },
        /**
         * Extended in ManualModel
         * @private
         * @param {Object} prop
         * @returns {Boolean}
         */
        _isValid: function (prop) {
            return (
                !isNaN(prop.id) ||
                (prop.account_id &&
                    prop.amount &&
                    prop.label &&
                    Boolean(prop.label.length))
            );
        },
        /**
         * Fetch 'account.reconciliation.widget' propositions.
         * overridden in ManualModel
         *
         * @see '_formatMoveLine'
         *
         * @private
         * @param {String} handle
         * @returns {Promise}
         */
        _performMoveLine: function (handle, mode, limit) {
            limit = limit || this.limitMoveLines;
            var line = this.getLine(handle);
            var excluded_ids = _.map(
                _.union(
                    line.reconciliation_proposition,
                    line.mv_lines_match_rp,
                    line.mv_lines_match_other
                ),
                function (prop) {
                    return _.isNumber(prop.id) ? prop.id : null;
                }
            ).filter((id) => id !== null);
            var filter = line["filter_" + mode] || "";
            return this._rpc({
                model: "account.reconciliation.widget",
                method: "get_move_lines_for_bank_statement_line",
                args: [
                    line.id,
                    line.st_line.partner_id,
                    excluded_ids,
                    filter,
                    0,
                    limit,
                    mode === "match_rp" ? "rp" : "other",
                ],
                context: this.context,
            }).then(this._formatMoveLine.bind(this, handle, mode));
        },
        /**
         * Format the proposition to send information server side
         * extended in ManualModel
         *
         * @private
         * @param {Object} line
         * @param {Object} prop
         * @returns {Object}
         */
        _formatToProcessReconciliation: function (line, prop) {
            var amount = -prop.amount;
            if (prop.partial_amount) {
                amount = -prop.partial_amount;
            }

            var result = {
                name: prop.label,
                debit: amount > 0 ? amount : 0,
                credit: amount < 0 ? -amount : 0,
                tax_exigible: prop.tax_exigible,
                analytic_tag_ids: [[6, null, _.pluck(prop.analytic_tag_ids, "id")]],
            };
            if (!isNaN(prop.id)) {
                result.counterpart_aml_id = prop.id;
            } else {
                result.account_id = prop.account_id.id;
                if (prop.journal_id) {
                    result.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_ids && prop.tax_ids.length)
                result.tax_ids = [[6, null, _.pluck(prop.tax_ids, "id")]];

            if (prop.tax_tag_ids && prop.tax_tag_ids.length)
                result.tax_tag_ids = [[6, null, _.pluck(prop.tax_tag_ids, "id")]];
            if (prop.tax_repartition_line_id)
                result.tax_repartition_line_id = prop.tax_repartition_line_id;
            if (prop.reconcileModelId)
                result.reconcile_model_id = prop.reconcileModelId;
            return result;
        },
        /**
         * Hook to handle return values of the validate's line process.
         *
         * @private
         * @param {Object} data
         * @param {Object[]} data.moves list of processed account.move
         * @returns {Deferred}
         */
        // eslint-disable-next-line no-unused-vars
        _validatePostProcess: function (data) {
            return Promise.resolve();
        },
    });

    /**
     * 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_ids",
            "force_tax_included",
            "analytic_tag_ids",
            "date",
            "to_check",
        ],

        modes: ["create", "match"],

        // --------------------------------------------------------------------------
        // Public
        // --------------------------------------------------------------------------

        /**
         * Return a boolean telling if load button needs to be displayed or not
         *
         * @returns {Boolean} true if load more button needs to be displayed
         */
        hasMoreLines: function () {
            if (this.manualLines.length > this.pagerIndex) {
                return true;
            }
            return false;
        },
        /**
         * Load data from
         * - 'account.reconciliation.widget' 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 {Promise}
         */
        load: function (context) {
            var self = this;
            this.context = context;

            var domain_account_id = [];
            if (context && context.company_ids) {
                domain_account_id.push(["company_id", "in", context.company_ids]);
            }

            var def_account = this._rpc({
                model: "account.account",
                method: "search_read",
                domain: domain_account_id,
                fields: ["code"],
            }).then(function (accounts) {
                self.account_ids = _.pluck(accounts, "id");
                self.accounts = _.object(self.account_ids, _.pluck(accounts, "code"));
            });

            var domainReconcile = [];
            var session_allowed_company_ids =
                session.user_context.allowed_company_ids || [];
            var company_ids =
                (context && context.company_ids) ||
                session_allowed_company_ids.slice(0, 1);

            if (company_ids) {
                domainReconcile.push(["company_id", "in", company_ids]);
            }
            var def_reconcileModel = this._loadReconciliationModel({
                domainReconcile: domainReconcile,
            });
            var def_taxes = this._loadTaxes();

            return Promise.all([def_reconcileModel, def_account, def_taxes]).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
                                ._rpc({
                                    model: "account.reconciliation.widget",
                                    method: "get_data_for_manual_reconciliation",
                                    args: args,
                                    context: context,
                                })
                                .then(function (result) {
                                    self.manualLines = result;
                                    self.valuenow = 0;
                                    self.valuemax = Object.keys(
                                        self.manualLines
                                    ).length;
                                    var lines = self.manualLines.slice(
                                        0,
                                        self.defaultDisplayQty
                                    );
                                    self.pagerIndex = lines.length;
                                    return self.loadData(lines);
                                });
                        case "accounts":
                            return self
                                ._rpc({
                                    model: "account.reconciliation.widget",
                                    method: "get_data_for_manual_reconciliation",
                                    args: [
                                        "account",
                                        context.account_ids || self.account_ids,
                                    ],
                                    context: context,
                                })
                                .then(function (result) {
                                    self.manualLines = result;
                                    self.valuenow = 0;
                                    self.valuemax = Object.keys(
                                        self.manualLines
                                    ).length;
                                    var lines = self.manualLines.slice(
                                        0,
                                        self.defaultDisplayQty
                                    );
                                    self.pagerIndex = lines.length;
                                    return self.loadData(lines);
                                });
                        default:
                            var partner_ids = context.partner_ids || null;
                            var account_ids =
                                context.account_ids || self.account_ids || null;
                            return self
                                ._rpc({
                                    model: "account.reconciliation.widget",
                                    method: "get_all_data_for_manual_reconciliation",
                                    args: [partner_ids, account_ids],
                                    context: context,
                                })
                                .then(function (result) {
                                    // Flatten the result
                                    self.manualLines = [].concat(
                                        result.accounts,
                                        result.customers,
                                        result.suppliers
                                    );
                                    self.valuenow = 0;
                                    self.valuemax = Object.keys(
                                        self.manualLines
                                    ).length;
                                    var lines = self.manualLines.slice(
                                        0,
                                        self.defaultDisplayQty
                                    );
                                    self.pagerIndex = lines.length;
                                    return self.loadData(lines);
                                });
                    }
                }
            );
        },

        /**
         * Reload data by calling load
         * It overrides super.reload() because
         * it is not adapted for this model.
         *
         * Use case: coming back to manual reconcilation
         *           in breadcrumb
         */
        reload: function () {
            this.lines = {};
            return this.load(this.context);
        },

        /**
         * Load more partners/accounts
         * overridden in ManualModel
         *
         * @param {integer} qty quantity to load
         * @returns {Promise}
         */
        loadMore: function (qty) {
            if (qty === undefined) {
                qty = this.defaultDisplayQty;
            }
            var lines = this.manualLines.slice(this.pagerIndex, this.pagerIndex + qty);
            this.pagerIndex += qty;
            return this.loadData(lines);
        },
        /**
         * Method to load informations on lines
         *
         * @param {Array} lines manualLines to load
         * @returns {Promise}
         */
        loadData: function (lines) {
            var self = this;
            var defs = [];
            _.each(lines, function (l) {
                defs.push(self._formatLine(l.mode, l));
            });
            return Promise.all(defs);
        },
        /**
         * Mark the account or the partner as reconciled
         *
         * @param {(String|String[])} handle
         * @returns {Promise<Array>} 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 = Promise.resolve();
            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) && prop.display;
                        }),
                        self._formatToProcessReconciliation.bind(self, line)
                    );
                    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._rpc({
                    model: "account.reconciliation.widget",
                    method: "process_move_lines",
                    args: [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;
                    }
                    line.filter_match = "";
                    defs.push(
                        self._performMoveLine(handle, "match").then(function () {
                            if (!line.mv_lines_match.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);
                                }
                            }
                        })
                    );
                });
                return Promise.all(defs).then(function () {
                    if (partner_ids.length) {
                        self._rpc({
                            model: "res.partner",
                            method: "mark_as_reconciled",
                            args: [partner_ids],
                        });
                    }
                    return {
                        reconciled: reconciled,
                        updated: _.difference(handles, reconciled),
                    };
                });
            });
        },
        removeProposition: function (handle, id) {
            var self = this;
            var line = this.getLine(handle);
            var defs = [];
            var prop = _.find(line.reconciliation_proposition, {id: id});
            if (prop) {
                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.mv_lines_match = line.mv_lines_match || [];
                line.mv_lines_match.unshift(prop);

                // No proposition left and then, reset the st_line partner.
                if (
                    line.reconciliation_proposition.length == 0 &&
                    line.st_line.has_no_partner
                )
                    defs.push(self.changePartner(line.handle));
            }
            line.mode =
                (id || line.mode !== "create") && isNaN(id) ? "create" : "match";
            defs.push(this._computeLine(line));
            return Promise.all(defs).then(function () {
                return self.changeMode(handle, line.mode, true);
            });
        },

        // --------------------------------------------------------------------------
        // Private
        // --------------------------------------------------------------------------

        /**
         * override change the balance type to display or not the reconcile button
         *
         * @override
         * @private
         * @param {Object} line
         * @returns {Promise}
         */
        _computeLine: function (line) {
            return this._super(line).then(function () {
                var props = _.reject(line.reconciliation_proposition, "invalid");
                _.each(line.reconciliation_proposition, function (p) {
                    delete p.is_move_line;
                });
                line.balance.type = -1;
                if (!line.balance.amount_currency && 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 {'customers' | 'suppliers' | 'accounts'} type
         * @param {Object} data
         * @returns {Promise}
         */
        _formatLine: function (type, data) {
            var line = (this.lines[_.uniqueId("rline")] = _.extend(data, {
                type: type,
                reconciled: false,
                mode: "inactive",
                limitMoveLines: this.limitMoveLines,
                filter_match: "",
                reconcileModels: this.reconcileModels,
                account_id: this._formatNameGet([data.account_id, data.account_name]),
                st_line: data,
                visible: true,
            }));
            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} line
         * @param {Object} props
         */
        _formatLineProposition: function (line, props) {
            var self = this;
            this._super(line, props);
            if (props.length) {
                _.each(props, function (prop) {
                    var tmp_value = prop.debit || prop.credit;
                    prop.credit = prop.credit !== 0 ? 0 : tmp_value;
                    prop.debit = prop.debit !== 0 ? 0 : tmp_value;
                    prop.amount = -prop.amount;
                    prop.journal_id = self._formatNameGet(
                        prop.journal_id || line.journal_id
                    );
                    prop.to_check = Boolean(prop.to_check);
                });
            }
        },
        /**
         * Override to add journal_id on tax_created_line
         *
         * @private
         * @param {Object} line
         * @param {Object} values
         * @returns {Object}
         */
        _formatQuickCreate: function (line, values) {
            // Add journal to created line
            if (
                values &&
                values.journal_id === undefined &&
                line &&
                line.createForm &&
                line.createForm.journal_id
            ) {
                values.journal_id = line.createForm.journal_id;
            }
            return this._super(line, values);
        },
        /**
         * @override
         * @param {Object} prop
         * @returns {Boolean}
         */
        _isDisplayedProposition: function (prop) {
            return Boolean(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 {Promise}
         */
        _performMoveLine: function (handle, mode, limit) {
            limit = limit || this.limitMoveLines;
            var line = this.getLine(handle);
            var excluded_ids = _.map(
                _.union(line.reconciliation_proposition, line.mv_lines_match),
                function (prop) {
                    return _.isNumber(prop.id) ? prop.id : null;
                }
            ).filter((id) => id !== null);
            var filter = line.filter_match || "";
            var args = [
                line.account_id.id,
                line.partner_id,
                excluded_ids,
                filter,
                0,
                limit,
            ];
            return this._rpc({
                model: "account.reconciliation.widget",
                method: "get_move_lines_for_manual_reconciliation",
                args: args,
                context: this.context,
            }).then(this._formatMoveLine.bind(this, handle, ""));
        },

        _formatToProcessReconciliation: function (line, prop) {
            var result = this._super(line, prop);
            result.date = prop.date;
            return result;
        },
        _getDefaultMode: function (handle) {
            var line = this.getLine(handle);
            if (
                line.balance.amount === 0 &&
                (!line.st_line.mv_lines_match ||
                    line.st_line.mv_lines_match.length === 0)
            ) {
                return "inactive";
            }
            return line.mv_lines_match.length > 0 ? "match" : "create";
        },
        _formatMoveLine: function (handle, mode, mv_lines) {
            var self = this;
            var line = this.getLine(handle);
            line.mv_lines_match = _.uniq(
                (line.mv_lines_match || []).concat(mv_lines),
                (l) => l.id
            );
            this._formatLineProposition(line, mv_lines);

            if (
                line.mode !== "create" &&
                !line.mv_lines_match.length &&
                !line.filter_match.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);
            }
        },
    });

    return {
        StatementModel: StatementModel,
        ManualModel: ManualModel,
    };
});
