import copy

from psycopg2 import sql

from odoo import _, api, models
from odoo.exceptions import UserError
from odoo.osv import expression
from odoo.tools.misc import format_date, formatLang, parse_date


class AccountReconciliation(models.AbstractModel):
    _name = "account.reconciliation.widget"
    _description = "Account Reconciliation widget"

    ####################################################
    # Public
    ####################################################

    @api.model
    def process_bank_statement_line(self, st_line_ids, data):
        """Handles data sent from the bank statement reconciliation widget
        (and can otherwise serve as an old-API bridge)

        :param st_line_ids
        :param list of dicts data: must contains the keys
            'counterpart_aml_dicts', 'payment_aml_ids' and 'new_aml_dicts',
            whose value is the same as described in process_reconciliation
            except that ids are used instead of recordsets.
        :returns dict: used as a hook to add additional keys.
        """
        st_lines = self.env["account.bank.statement.line"].browse(st_line_ids)
        AccountMoveLine = self.env["account.move.line"]
        ctx = dict(self._context, force_price_include=False)

        processed_moves = self.env["account.move"]
        for st_line, datum in zip(st_lines, copy.deepcopy(data)):
            payment_aml_rec = AccountMoveLine.browse(datum.get("payment_aml_ids", []))

            for aml_dict in datum.get("counterpart_aml_dicts", []):
                aml_dict["move_line"] = AccountMoveLine.browse(
                    aml_dict["counterpart_aml_id"]
                )
                del aml_dict["counterpart_aml_id"]

            vals = {}
            if datum.get("partner_id") is not None:
                vals["partner_id"] = datum["partner_id"]
            if datum.get("ref") is not None:
                vals["ref"] = datum["ref"]
            if vals:
                st_line.write(vals)

            ctx["default_to_check"] = datum.get("to_check")
            moves = st_line.with_context(ctx).process_reconciliation(
                datum.get("counterpart_aml_dicts", []),
                payment_aml_rec,
                datum.get("new_aml_dicts", []),
            )
            processed_moves = processed_moves | moves
        return {
            "moves": processed_moves.ids,
            "statement_line_ids": processed_moves.mapped(
                "line_ids.statement_line_id"
            ).ids,
        }

    @api.model
    def get_move_lines_for_bank_statement_line(
        self,
        st_line_id,
        partner_id=None,
        excluded_ids=None,
        search_str=False,
        offset=0,
        limit=None,
        mode=None,
    ):
        """Returns move lines for the bank statement reconciliation widget,
        formatted as a list of dicts

        :param st_line_id: ids of the statement lines
        :param partner_id: optional partner id to select only the moves
            line corresponding to the partner
        :param excluded_ids: optional move lines ids excluded from the
            result
        :param search_str: optional search (can be the amout, display_name,
            partner name, move line name)
        :param offset: useless but kept in stable to preserve api
        :param limit: number of the result to search
        :param mode: 'rp' for receivable/payable or 'other'
        """
        st_line = self.env["account.bank.statement.line"].browse(st_line_id)

        # Blue lines = payment on bank account not assigned to a statement yet
        aml_accounts = [
            st_line.journal_id.default_account_id.id,
        ]

        if partner_id is None:
            partner_id = st_line.partner_id.id

        domain = self._domain_move_lines_for_reconciliation(
            st_line,
            aml_accounts,
            partner_id,
            excluded_ids=excluded_ids,
            search_str=search_str,
            mode=mode,
        )

        from_clause, where_clause, where_clause_params = (
            self.env["account.move.line"]._where_calc(domain).get_sql()
        )
        query_str = sql.SQL(
            """
            SELECT "account_move_line".id, COUNT(*) OVER() FROM {from_clause}
            {where_str}
            ORDER BY ("account_move_line".debit -
                      "account_move_line".credit) = {amount} DESC,
                "account_move_line".date_maturity ASC,
                "account_move_line".id ASC
            {limit_str}
        """.format(
                from_clause=from_clause,
                where_str=where_clause and (" WHERE %s" % where_clause) or "",
                amount=st_line.amount,
                limit_str=limit and " LIMIT %s" or "",
            )
        )
        params = where_clause_params + (limit and [limit] or [])
        self.env["account.move"].flush()
        self.env["account.move.line"].flush()
        self.env["account.bank.statement"].flush()
        self._cr.execute(query_str, params)
        res = self._cr.fetchall()
        try:
            # All records will have the same count value, just get the 1st one
            recs_count = res[0][1]
        except IndexError:
            recs_count = 0
        aml_recs = self.env["account.move.line"].browse([i[0] for i in res])
        target_currency = (
            st_line.foreign_currency_id
            or st_line.journal_id.currency_id
            or st_line.journal_id.company_id.currency_id
        )
        return self._prepare_move_lines(
            aml_recs,
            target_currency=target_currency,
            target_date=st_line.date,
            recs_count=recs_count,
        )

    @api.model
    def _get_bank_statement_line_partners(self, st_lines):
        params = []

        # Add the res.partner.ban's IR rules. In case partners are not shared
        # between companies, identical bank accounts may exist in a company we
        # don't have access to.
        ir_rules_query = self.env["res.partner.bank"]._where_calc([])
        self.env["res.partner.bank"]._apply_ir_rules(ir_rules_query, "read")
        from_clause, where_clause, where_clause_params = ir_rules_query.get_sql()
        if where_clause:
            where_bank = ("AND %s" % where_clause).replace(
                '"res_partner_bank"', '"bank"'
            )
            params += where_clause_params
        else:
            where_bank = ""

        # Add the res.partner's IR rules. In case partners are not shared
        # between companies, identical partners may exist in a company we don't
        # have access to.
        ir_rules_query = self.env["res.partner"]._where_calc([])
        self.env["res.partner"]._apply_ir_rules(ir_rules_query, "read")
        from_clause, where_clause, where_clause_params = ir_rules_query.get_sql()
        if where_clause:
            where_partner = ("AND %s" % where_clause).replace('"res_partner"', '"p3"')
            params += where_clause_params
        else:
            where_partner = ""

        query = """
            SELECT
                st_line.id                          AS id,
                COALESCE(p1.id,p2.id,p3.id)         AS partner_id
            FROM account_bank_statement_line st_line
        """
        query += "INNER JOIN account_move m ON m.id = st_line.move_id \n"
        query += (
            "LEFT JOIN res_partner_bank bank ON bank.id = m.partner_bank_id OR "
            "bank.sanitized_acc_number "
            "ILIKE regexp_replace(st_line.account_number, '\\W+', '', 'g') %s\n"
            % (where_bank)
        )
        query += "LEFT JOIN res_partner p1 ON st_line.partner_id=p1.id \n"
        query += "LEFT JOIN res_partner p2 ON bank.partner_id=p2.id \n"
        # By definition the commercial partner_id doesn't have a parent_id set
        query += (
            "LEFT JOIN res_partner p3 ON p3.name ILIKE st_line.partner_name %s "
            "AND p3.parent_id is NULL \n" % (where_partner)
        )
        query += "WHERE st_line.id IN %s"

        params += [tuple(st_lines.ids)]

        self._cr.execute(query, params)

        result = {}
        for res in self._cr.dictfetchall():
            result[res["id"]] = res["partner_id"]
        return result

    @api.model
    def get_bank_statement_line_data(self, st_line_ids, excluded_ids=None):
        """Returns the data required to display a reconciliation widget, for
        each statement line in self

        :param st_line_id: ids of the statement lines
        :param excluded_ids: optional move lines ids excluded from the
            result
        """
        results = {
            "lines": [],
            "value_min": 0,
            "value_max": 0,
            "reconciled_aml_ids": [],
        }

        if not st_line_ids:
            return results

        excluded_ids = excluded_ids or []

        # Make a search to preserve the table's order.
        bank_statement_lines = self.env["account.bank.statement.line"].search(
            [("id", "in", st_line_ids)]
        )
        results["value_max"] = len(bank_statement_lines)
        reconcile_model = self.env["account.reconcile.model"].search(
            [("rule_type", "!=", "writeoff_button")]
        )

        # Search for missing partners when opening the reconciliation widget.
        if bank_statement_lines:
            partner_map = self._get_bank_statement_line_partners(bank_statement_lines)
            matching_amls = reconcile_model._apply_rules(
                bank_statement_lines, excluded_ids=excluded_ids, partner_map=partner_map
            )

        # Iterate on st_lines to keep the same order in the results list.
        bank_statements_left = self.env["account.bank.statement"]
        for line in bank_statement_lines:
            if matching_amls[line.id].get("status") == "reconciled":
                reconciled_move_lines = matching_amls[line.id].get("reconciled_lines")
                results["value_min"] += 1
                results["reconciled_aml_ids"] += (
                    reconciled_move_lines and reconciled_move_lines.ids or []
                )
            else:
                aml_ids = matching_amls[line.id]["aml_ids"]
                bank_statements_left += line.statement_id
                target_currency = (
                    line.foreign_currency_id
                    or line.journal_id.currency_id
                    or line.journal_id.company_id.currency_id
                )

                amls = aml_ids and self.env["account.move.line"].browse(aml_ids)
                line_vals = {
                    "st_line": self._get_statement_line(line),
                    "reconciliation_proposition": aml_ids
                    and self._prepare_move_lines(
                        amls, target_currency=target_currency, target_date=line.date
                    )
                    or [],
                    "model_id": matching_amls[line.id].get("model")
                    and matching_amls[line.id]["model"].id,
                    "write_off": matching_amls[line.id].get("status") == "write_off",
                }
                if not line.partner_id:
                    partner = False
                    if matching_amls[line.id].get("partner"):
                        partner = matching_amls[line.id]["partner"]
                    elif partner_map.get(line.id):
                        partner = self.env["res.partner"].browse(partner_map[line.id])
                    if partner:
                        line_vals.update(
                            {
                                "partner_id": partner.id,
                                "partner_name": partner.name,
                            }
                        )
                results["lines"].append(line_vals)

        return results

    @api.model
    def get_bank_statement_data(self, bank_statement_line_ids, srch_domain=None):
        """Get statement lines of the specified statements or all unreconciled
        statement lines and try to automatically reconcile them / find them
        a partner.
        Return ids of statement lines left to reconcile and other data for
        the reconciliation widget.

        :param bank_statement_line_ids: ids of the bank statement lines
        """
        if not bank_statement_line_ids:
            return {}
        bank_statements = (
            self.env["account.bank.statement.line"]
            .browse(bank_statement_line_ids)
            .mapped("statement_id")
        )

        query = """
             SELECT line.id
             FROM account_bank_statement_line line
             LEFT JOIN res_partner p on p.id = line.partner_id
             INNER JOIN account_bank_statement st ON line.statement_id = st.id
                AND st.state = 'posted'
             WHERE line.is_reconciled = FALSE
             AND line.amount != 0.0
             AND line.id IN %(ids)s
             GROUP BY line.id
        """
        self.env.cr.execute(query, {"ids": tuple(bank_statement_line_ids)})

        domain = [["id", "in", [line.get("id") for line in self.env.cr.dictfetchall()]]]
        if srch_domain is not None:
            domain += srch_domain
        bank_statement_lines = self.env["account.bank.statement.line"].search(domain)

        results = self.get_bank_statement_line_data(
            bank_statement_lines.ids,
            excluded_ids=bank_statement_lines.move_id.line_ids.ids,
        )
        bank_statement_lines_left = self.env["account.bank.statement.line"].browse(
            [line["st_line"]["id"] for line in results["lines"]]
        )
        bank_statements_left = bank_statement_lines_left.mapped("statement_id")

        results.update(
            {
                "statement_id": len(bank_statements_left) == 1
                and bank_statements_left.id
                or False,
                "statement_name": len(bank_statements_left) == 1
                and bank_statements_left.name
                or False,
                "journal_id": bank_statements
                and bank_statements[0].journal_id.id
                or False,
                "notifications": [],
            }
        )

        if len(results["lines"]) < len(bank_statement_lines):
            results["notifications"].append(
                {
                    "type": "info",
                    "template": "reconciliation.notification.reconciled",
                    "reconciled_aml_ids": results["reconciled_aml_ids"],
                    "nb_reconciled_lines": results["value_min"],
                    "details": {
                        "name": _("Journal Items"),
                        "model": "account.move.line",
                        "ids": results["reconciled_aml_ids"],
                    },
                }
            )

        return results

    @api.model
    def get_move_lines_for_manual_reconciliation(
        self,
        account_id,
        partner_id=False,
        excluded_ids=None,
        search_str=False,
        offset=0,
        limit=None,
        target_currency_id=False,
    ):
        """Returns unreconciled move lines for an account or a partner+account,
        formatted for the manual reconciliation widget"""

        Account_move_line = self.env["account.move.line"]
        Account = self.env["account.account"]
        Currency = self.env["res.currency"]

        domain = self._domain_move_lines_for_manual_reconciliation(
            account_id, partner_id, excluded_ids, search_str
        )
        recs_count = Account_move_line.search_count(domain)
        lines = Account_move_line.search(
            domain, limit=limit, order="date_maturity desc, id desc"
        )
        if target_currency_id:
            target_currency = Currency.browse(target_currency_id)
        else:
            account = Account.browse(account_id)
            target_currency = account.currency_id or account.company_id.currency_id
        return self._prepare_move_lines(
            lines, target_currency=target_currency, recs_count=recs_count
        )

    @api.model
    def get_all_data_for_manual_reconciliation(self, partner_ids, account_ids):
        """Returns the data required for the invoices & payments matching of
        partners/accounts.
        If an argument is None, fetch all related reconciliations. Use [] to
        fetch nothing.
        """
        MoveLine = self.env["account.move.line"]
        aml_ids = (
            self._context.get("active_ids")
            and self._context.get("active_model") == "account.move.line"
            and tuple(self._context.get("active_ids"))
        )
        if aml_ids:
            aml = MoveLine.browse(aml_ids)
            account = aml[0].account_id
            currency = account.currency_id or account.company_id.currency_id
            return {
                "accounts": [
                    {
                        "reconciliation_proposition": self._prepare_move_lines(
                            aml, target_currency=currency
                        ),
                        "company_id": account.company_id.id,
                        "currency_id": currency.id,
                        "mode": "accounts",
                        "account_id": account.id,
                        "account_name": account.name,
                        "account_code": account.code,
                    }
                ],
                "customers": [],
                "suppliers": [],
            }
        # If we have specified partner_ids, don't return the list of
        # reconciliation for specific accounts as it will show entries that are
        # not reconciled with other partner. Asking for a specific partner on a
        # specific account is never done.
        accounts_data = []
        if not partner_ids or not any(partner_ids):
            accounts_data = self.get_data_for_manual_reconciliation(
                "account", account_ids
            )
        return {
            "customers": self.get_data_for_manual_reconciliation(
                "partner", partner_ids, "receivable"
            ),
            "suppliers": self.get_data_for_manual_reconciliation(
                "partner", partner_ids, "payable"
            ),
            "accounts": accounts_data,
        }

    @api.model
    def get_data_for_manual_reconciliation(
        self, res_type, res_ids=None, account_type=None
    ):
        """Returns the data required for the invoices & payments matching of
         partners/accounts (list of dicts).
         If no res_ids is passed, returns data for all partners/accounts that can
         be reconciled.

        :param res_type: either 'partner' or 'account'
        :param res_ids: ids of the partners/accounts to reconcile, use None to
            fetch data indiscriminately of the id, use [] to prevent from
            fetching any data at all.
        :param account_type: if a partner is both customer and vendor, you can
            use 'payable' to reconcile the vendor-related journal entries and
            'receivable' for the customer-related entries.
        """

        Account = self.env["account.account"]
        Partner = self.env["res.partner"]

        if res_ids is not None and len(res_ids) == 0:
            # Note : this short-circuiting is better for performances, but also
            # required since postgresql doesn't implement empty list (so 'AND id
            # in ()' is useless)
            return []
        res_ids = res_ids and tuple(res_ids)

        assert res_type in ("partner", "account")
        assert account_type in ("payable", "receivable", None)
        is_partner = res_type == "partner"
        res_alias = is_partner and "p" or "a"
        aml_ids = (
            self._context.get("active_ids")
            and self._context.get("active_model") == "account.move.line"
            and tuple(self._context.get("active_ids"))
        )
        all_entries = self._context.get("all_entries", False)
        all_entries_query = """
            AND EXISTS (
                SELECT NULL
                FROM account_move_line l
                JOIN account_move move ON l.move_id = move.id
                JOIN account_journal journal ON l.journal_id = journal.id
                WHERE l.account_id = a.id
                {inner_where}
                AND l.amount_residual != 0
                AND move.state = 'posted'
            )
        """.format(
            inner_where=is_partner and "AND l.partner_id = p.id" or " "
        )
        only_dual_entries_query = """
            AND EXISTS (
                SELECT NULL
                FROM account_move_line l
                JOIN account_move move ON l.move_id = move.id
                JOIN account_journal journal ON l.journal_id = journal.id
                WHERE l.account_id = a.id
                {inner_where}
                AND l.amount_residual > 0
                AND move.state = 'posted'
            )
            AND EXISTS (
                SELECT NULL
                FROM account_move_line l
                JOIN account_move move ON l.move_id = move.id
                JOIN account_journal journal ON l.journal_id = journal.id
                WHERE l.account_id = a.id
                {inner_where}
                AND l.amount_residual < 0
                AND move.state = 'posted'
            )
        """.format(
            inner_where=is_partner and "AND l.partner_id = p.id" or " "
        )
        query = sql.SQL(
            """
            SELECT {select} account_id, account_name, account_code, max_date
            FROM (
                    SELECT {inner_select}
                        a.id AS account_id,
                        a.name AS account_name,
                        a.code AS account_code,
                        MAX(l.write_date) AS max_date
                    FROM
                        account_move_line l
                        RIGHT JOIN account_account a ON (a.id = l.account_id)
                        RIGHT JOIN account_account_type at
                            ON (at.id = a.user_type_id)
                        {inner_from}
                    WHERE
                        a.reconcile IS TRUE
                        AND l.full_reconcile_id is NULL
                        {where1}
                        {where2}
                        {where3}
                        AND l.company_id = {company_id}
                        {where4}
                        {where5}
                    GROUP BY {group_by1} a.id, a.name, a.code {group_by2}
                    {order_by}
                ) as s
            {outer_where}
        """.format(
                select=is_partner
                and "partner_id, partner_name, to_char(last_time_entries_checked, "
                "'YYYY-MM-DD') AS last_time_entries_checked,"
                or " ",
                inner_select=is_partner
                and "p.id AS partner_id, p.name AS partner_name, "
                "p.last_time_entries_checked AS last_time_entries_checked,"
                or " ",
                inner_from=is_partner
                and "RIGHT JOIN res_partner p ON (l.partner_id = p.id)"
                or " ",
                where1=is_partner
                and " "
                or "AND ((at.type <> 'payable' AND at.type <> 'receivable') "
                "OR l.partner_id IS NULL)",
                where2=account_type and "AND at.type = %(account_type)s" or "",
                where3=res_ids and "AND " + res_alias + ".id in %(res_ids)s" or "",
                company_id=self.env.company.id,
                where4=aml_ids and "AND l.id IN %(aml_ids)s" or " ",
                where5=all_entries and all_entries_query or only_dual_entries_query,
                group_by1=is_partner and "l.partner_id, p.id," or " ",
                group_by2=is_partner and ", p.last_time_entries_checked" or " ",
                order_by=is_partner
                and "ORDER BY p.last_time_entries_checked"
                or "ORDER BY a.code",
                outer_where=is_partner
                and "WHERE (last_time_entries_checked IS NULL "
                "OR max_date > last_time_entries_checked)"
                or " ",
            )
        )
        self.env["account.move.line"].flush()
        self.env["account.account"].flush()
        self.env.cr.execute(query, locals())

        # Apply ir_rules by filtering out
        rows = self.env.cr.dictfetchall()
        ids = [x["account_id"] for x in rows]
        allowed_ids = set(Account.browse(ids).ids)
        rows = [row for row in rows if row["account_id"] in allowed_ids]
        if is_partner:
            ids = [x["partner_id"] for x in rows]
            allowed_ids = set(Partner.browse(ids).ids)
            rows = [row for row in rows if row["partner_id"] in allowed_ids]

        # Keep mode for future use in JS
        if res_type == "account":
            mode = "accounts"
        else:
            mode = "customers" if account_type == "receivable" else "suppliers"

        # Fetch other data
        for row in rows:
            account = Account.browse(row["account_id"])
            currency = account.currency_id or account.company_id.currency_id
            row["currency_id"] = currency.id
            partner_id = is_partner and row["partner_id"] or None
            rec_prop = (
                aml_ids
                and self.env["account.move.line"].browse(aml_ids)
                or self._get_move_line_reconciliation_proposition(
                    account.id, partner_id
                )
            )
            row["reconciliation_proposition"] = self._prepare_move_lines(
                rec_prop, target_currency=currency
            )
            row["mode"] = mode
            row["company_id"] = account.company_id.id

        # Return the partners with a reconciliation proposition first, since
        # they are most likely to be reconciled.
        return [r for r in rows if r["reconciliation_proposition"]] + [
            r for r in rows if not r["reconciliation_proposition"]
        ]

    @api.model
    def process_move_lines(self, data):
        """Used to validate a batch of reconciliations in a single call
        :param data: list of dicts containing:
            - 'type': either 'partner' or 'account'
            - 'id': id of the affected res.partner or account.account
            - 'mv_line_ids': ids of existing account.move.line to reconcile
            - 'new_mv_line_dicts': list of dicts containing values suitable for
              account_move_line.create()
        """

        Partner = self.env["res.partner"]

        for datum in data:
            if (
                len(datum["mv_line_ids"]) >= 1
                or len(datum["mv_line_ids"]) + len(datum["new_mv_line_dicts"]) >= 2
            ):
                self._process_move_lines(
                    datum["mv_line_ids"], datum["new_mv_line_dicts"]
                )

            if datum["type"] == "partner":
                partners = Partner.browse(datum["id"])
                partners.mark_as_reconciled()

    ####################################################
    # Private
    ####################################################

    def _str_domain_for_mv_line(self, search_str):
        return [
            "|",
            ("account_id.code", "ilike", search_str),
            "|",
            ("move_id.name", "ilike", search_str),
            "|",
            ("move_id.ref", "ilike", search_str),
            "|",
            ("date_maturity", "like", parse_date(self.env, search_str)),
            "&",
            ("name", "!=", "/"),
            ("name", "ilike", search_str),
        ]

    @api.model
    def _domain_move_lines(self, search_str):
        """Returns the domain from the search_str search
        :param search_str: search string
        """
        if not search_str:
            return []
        str_domain = self._str_domain_for_mv_line(search_str)
        if search_str[0] in ["-", "+"]:
            try:
                amounts_str = search_str.split("|")
                for amount_str in amounts_str:
                    amount = (
                        amount_str[0] == "-"
                        and float(amount_str)
                        or float(amount_str[1:])
                    )
                    amount_domain = [
                        "|",
                        ("amount_residual", "=", amount),
                        "|",
                        ("amount_residual_currency", "=", amount),
                        "|",
                        (
                            amount_str[0] == "-" and "credit" or "debit",
                            "=",
                            float(amount_str[1:]),
                        ),
                        ("amount_currency", "=", amount),
                    ]
                    str_domain = expression.OR([str_domain, amount_domain])
            except Exception:
                pass
        else:
            try:
                amount = float(search_str)
                amount_domain = [
                    "|",
                    ("amount_residual", "=", amount),
                    "|",
                    ("amount_residual_currency", "=", amount),
                    "|",
                    ("amount_residual", "=", -amount),
                    "|",
                    ("amount_residual_currency", "=", -amount),
                    "&",
                    ("account_id.internal_type", "=", "liquidity"),
                    "|",
                    "|",
                    "|",
                    ("debit", "=", amount),
                    ("credit", "=", amount),
                    ("amount_currency", "=", amount),
                    ("amount_currency", "=", -amount),
                ]
                str_domain = expression.OR([str_domain, amount_domain])
            except Exception:
                pass
        return str_domain

    @api.model
    def _domain_move_lines_for_reconciliation(
        self,
        st_line,
        aml_accounts,
        partner_id,
        excluded_ids=None,
        search_str=False,
        mode="rp",
    ):
        """Return the domain for account.move.line records which can be used for
        bank statement reconciliation.

        :param aml_accounts:
        :param partner_id:
        :param excluded_ids:
        :param search_str:
        :param mode: 'rp' for receivable/payable or 'other'
        """
        AccountMoveLine = self.env["account.move.line"]

        # Always exclude the journal items that have been marked as
        # 'to be checked' in a former bank statement reconciliation
        to_check_excluded = AccountMoveLine.search(
            AccountMoveLine._get_suspense_moves_domain()
        ).ids
        if excluded_ids is None:
            excluded_ids = []
        excluded_ids.extend(to_check_excluded)

        domain_reconciliation = [
            "&",
            "&",
            ("statement_line_id", "=", False),
            ("account_id", "in", aml_accounts),
            ("balance", "!=", 0.0),
        ]
        if st_line.company_id.account_bank_reconciliation_start:
            domain_reconciliation = expression.AND(
                [
                    domain_reconciliation,
                    [
                        (
                            "date",
                            ">=",
                            st_line.company_id.account_bank_reconciliation_start,
                        )
                    ],
                ]
            )
        # default domain matching
        domain_matching = [
            "&",
            "&",
            "&",
            "&",
            ("id", "not in", st_line.move_id.line_ids.ids),
            ("reconciled", "=", False),
            ("account_id.reconcile", "=", True),
            ("balance", "!=", 0.0),
            ("parent_state", "=", "posted"),
        ]

        domain = expression.OR([domain_reconciliation, domain_matching])
        if partner_id:
            domain = expression.AND([domain, [("partner_id", "=", partner_id)]])
        if mode == "rp":
            domain = expression.AND(
                [
                    domain,
                    [
                        (
                            "account_id.internal_type",
                            "in",
                            ["receivable", "payable", "liquidity"],
                        )
                    ],
                ]
            )
        else:
            domain = expression.AND(
                [
                    domain,
                    [
                        (
                            "account_id.internal_type",
                            "not in",
                            ["receivable", "payable", "liquidity"],
                        )
                    ],
                ]
            )

        # Domain factorized for all reconciliation use cases
        if search_str:
            str_domain = self._domain_move_lines(search_str=search_str)
            str_domain = expression.OR(
                [str_domain, [("partner_id.name", "ilike", search_str)]]
            )
            domain = expression.AND([domain, str_domain])

        if excluded_ids:
            domain = expression.AND([[("id", "not in", excluded_ids)], domain])
        # filter on account.move.line having the same company as the statement
        # line
        domain = expression.AND([domain, [("company_id", "=", st_line.company_id.id)]])
        return domain

    @api.model
    def _domain_move_lines_for_manual_reconciliation(
        self, account_id, partner_id=False, excluded_ids=None, search_str=False
    ):
        """Create domain criteria that are relevant to manual reconciliation."""
        domain = [
            "&",
            "&",
            ("reconciled", "=", False),
            ("account_id", "=", account_id),
            ("move_id.state", "=", "posted"),
        ]
        domain = expression.AND([domain, [("balance", "!=", 0.0)]])
        if partner_id:
            domain = expression.AND([domain, [("partner_id", "=", partner_id)]])
        if excluded_ids:
            domain = expression.AND([[("id", "not in", excluded_ids)], domain])
        if search_str:
            str_domain = self._domain_move_lines(search_str=search_str)
            domain = expression.AND([domain, str_domain])
        # filter on account.move.line having the same company as the given account
        account = self.env["account.account"].browse(account_id)
        domain = expression.AND([domain, [("company_id", "=", account.company_id.id)]])
        return domain

    @api.model
    def _prepare_move_lines(
        self, move_lines, target_currency=False, target_date=False, recs_count=0
    ):
        """Returns move lines formatted for the manual/bank reconciliation
         widget

        :param move_line_ids:
        :param target_currency: currency (browse) you want the move line
            debit/credit converted into
        :param target_date: date to use for the monetary conversion
        """
        ret = []

        for line in move_lines:
            company_currency = line.company_id.currency_id
            line_currency = (
                (line.currency_id and line.amount_currency)
                and line.currency_id
                or company_currency
            )
            ret_line = {
                "id": line.id,
                "name": line.name
                and line.name != "/"
                and line.move_id.name != line.name
                and line.move_id.name + ": " + line.name
                or line.move_id.name,
                "ref": line.move_id.ref or "",
                # For reconciliation between statement transactions and already
                # registered payments (eg. checks)
                # NB : we don't use the 'reconciled' field because the line
                # we're selecting is not the one that gets reconciled
                "account_id": [line.account_id.id, line.account_id.display_name],
                "already_paid": line.account_id.internal_type == "liquidity",
                "account_code": line.account_id.code,
                "account_name": line.account_id.name,
                "analytic_account_code": line.analytic_account_id.display_name or "",
                "account_type": line.account_id.internal_type,
                "date_maturity": format_date(self.env, line.date_maturity),
                "date": format_date(self.env, line.date),
                "journal_id": [line.journal_id.id, line.journal_id.display_name],
                "partner_id": line.partner_id.id,
                "partner_name": line.partner_id.name,
                "currency_id": line_currency.id,
            }

            debit = line.debit
            credit = line.credit
            amount = line.amount_residual
            amount_currency = line.amount_residual_currency

            # For already reconciled lines, don't use amount_residual(_currency)
            if line.account_id.internal_type == "liquidity":
                amount = debit - credit
                amount_currency = line.amount_currency

            target_currency = target_currency or company_currency

            # Use case:
            # Let's assume that company currency is in USD and that we have the
            # 3 following move lines
            #      Debit  Credit  Amount currency  Currency
            # 1)    25      0            0            NULL
            # 2)    17      0           25             EUR
            # 3)    33      0           25             YEN
            #
            # If we ask to see the information in the reconciliation widget in
            # company currency, we want to see The following information
            # 1) 25 USD (no currency information)
            # 2) 17 USD [25 EUR] (show 25 euro in currency information,
            #    in the little bill)
            # 3) 33 USD [25 YEN] (show 25 yen in currency information)
            #
            # If we ask to see the information in another currency than the
            # company let's say EUR
            # 1) 35 EUR [25 USD]
            # 2) 25 EUR (no currency information)
            # 3) 50 EUR [25 YEN]
            # In that case, we have to convert the debit-credit to the currency
            # we want and we show next to it the value of the amount_currency or
            # the debit-credit if no amount currency
            if target_currency == company_currency:
                if line_currency == target_currency:
                    amount = amount
                    amount_currency = ""
                    total_amount = debit - credit
                    total_amount_currency = ""
                else:
                    amount = amount
                    amount_currency = amount_currency
                    total_amount = debit - credit
                    total_amount_currency = line.amount_currency

            if target_currency != company_currency:
                if line_currency == target_currency:
                    amount = amount_currency
                    amount_currency = ""
                    total_amount = line.amount_currency
                    total_amount_currency = ""
                else:
                    amount_currency = line.currency_id and amount_currency or amount
                    company = line.account_id.company_id
                    date = target_date or line.date
                    amount = company_currency._convert(
                        amount, target_currency, company, date
                    )
                    total_amount = company_currency._convert(
                        (line.debit - line.credit), target_currency, company, date
                    )
                    total_amount_currency = (
                        line.currency_id
                        and line.amount_currency
                        or (line.debit - line.credit)
                    )

            ret_line["recs_count"] = recs_count
            ret_line["debit"] = amount > 0 and amount or 0
            ret_line["credit"] = amount < 0 and -amount or 0
            ret_line["amount_currency"] = amount_currency
            ret_line["amount_str"] = formatLang(
                self.env, abs(amount), currency_obj=target_currency
            )
            ret_line["total_amount_str"] = formatLang(
                self.env, abs(total_amount), currency_obj=target_currency
            )
            ret_line["amount_currency_str"] = (
                amount_currency
                and formatLang(
                    self.env, abs(amount_currency), currency_obj=line_currency
                )
                or ""
            )
            ret_line["total_amount_currency_str"] = (
                total_amount_currency
                and formatLang(
                    self.env, abs(total_amount_currency), currency_obj=line_currency
                )
                or ""
            )
            ret.append(ret_line)
        return ret

    @api.model
    def _get_statement_line(self, st_line):
        """Returns the data required by the bank statement reconciliation
        widget to display a statement line"""

        group_analytic = self.env.user.has_group("analytic.group_analytic_accounting")
        group_analytic_tags = self.env.user.has_group("analytic.group_analytic_tags")
        statement_currency = (
            st_line.journal_id.currency_id or st_line.journal_id.company_id.currency_id
        )
        if st_line.amount_currency and st_line.foreign_currency_id:
            amount = st_line.amount_currency
            amount_currency = st_line.amount
            amount_currency_str = formatLang(
                self.env, abs(amount_currency), currency_obj=statement_currency
            )
        else:
            amount = st_line.amount
            amount_currency = amount
            amount_currency_str = ""
        amount_str = formatLang(
            self.env,
            abs(amount),
            currency_obj=st_line.foreign_currency_id or statement_currency,
        )

        data = {
            "id": st_line.id,
            "ref": st_line.ref,
            "narration": st_line.narration or "",
            "name": st_line.name,
            "payment_ref": st_line.payment_ref,
            "date": format_date(self.env, st_line.date),
            "amount": amount,
            "amount_str": amount_str,  # Amount in the statement line currency
            "currency_id": st_line.foreign_currency_id.id or statement_currency.id,
            "partner_id": st_line.partner_id.id,
            "journal_id": st_line.journal_id.id,
            "statement_id": st_line.statement_id.id,
            "account_id": [
                st_line.journal_id.default_account_id.id,
                st_line.journal_id.default_account_id.display_name,
            ],
            "account_code": st_line.journal_id.default_account_id.code,
            "account_name": st_line.journal_id.default_account_id.name,
            "partner_name": st_line.partner_id.name,
            "communication_partner_name": st_line.partner_name,
            # Amount in the statement currency
            "amount_currency_str": amount_currency_str,
            # Amount in the statement currency
            "amount_currency": amount_currency,
            "has_no_partner": not st_line.partner_id.id,
            "company_id": st_line.company_id.id,
            "group_analytic_accounting": group_analytic,
            "group_analytic_tags": group_analytic_tags,
        }
        if st_line.partner_id:
            data["open_balance_account_id"] = (
                amount > 0
                and st_line.partner_id.property_account_receivable_id.id
                or st_line.partner_id.property_account_payable_id.id
            )

        return data

    @api.model
    def _get_move_line_reconciliation_proposition(self, account_id, partner_id=None):
        """Returns two lines whose amount are opposite"""

        Account_move_line = self.env["account.move.line"]

        ir_rules_query = Account_move_line._where_calc([])
        Account_move_line._apply_ir_rules(ir_rules_query, "read")
        from_clause, where_clause, where_clause_params = ir_rules_query.get_sql()
        where_str = where_clause and (" WHERE %s" % where_clause) or ""

        # Get pairs
        query = sql.SQL(
            """
            SELECT a.id, b.id
            FROM account_move_line a, account_move_line b,
                 account_move move_a, account_move move_b,
                 account_journal journal_a, account_journal journal_b
            WHERE a.id != b.id
            AND move_a.id = a.move_id
            AND move_a.state = 'posted'
            AND move_a.journal_id = journal_a.id
            AND move_b.id = b.move_id
            AND move_b.journal_id = journal_b.id
            AND move_b.state = 'posted'
            AND a.amount_residual = -b.amount_residual
            AND a.balance != 0.0
            AND b.balance != 0.0
            AND NOT a.reconciled
            AND a.account_id = %s
            AND (%s IS NULL AND b.account_id = %s)
            AND (%s IS NULL AND NOT b.reconciled OR b.id = %s)
            AND (%s is NULL OR (a.partner_id = %s AND b.partner_id = %s))
            AND a.id IN (SELECT "account_move_line".id FROM {0})
            AND b.id IN (SELECT "account_move_line".id FROM {0})
            ORDER BY a.date desc
            LIMIT 1
            """.format(
                from_clause + where_str
            )
        )
        move_line_id = self.env.context.get("move_line_id") or None
        params = (
            [
                account_id,
                move_line_id,
                account_id,
                move_line_id,
                move_line_id,
                partner_id,
                partner_id,
                partner_id,
            ]
            + where_clause_params
            + where_clause_params
        )
        self.env.cr.execute(query, params)

        pairs = self.env.cr.fetchall()

        if pairs:
            return Account_move_line.browse(pairs[0])
        return Account_move_line

    @api.model
    def _process_move_lines(self, move_line_ids, new_mv_line_dicts):
        """Create new move lines from new_mv_line_dicts (if not empty) then call
         reconcile_partial on self and new move lines

        :param new_mv_line_dicts: list of dicts containing values suitable for
            account_move_line.create()
        """
        if len(move_line_ids) < 1 or len(move_line_ids) + len(new_mv_line_dicts) < 2:
            raise UserError(_("A reconciliation must involve at least 2 move lines."))
        AccountMoveLine = self.env["account.move.line"].with_context(
            skip_account_move_synchronization=True
        )
        account_move_line = AccountMoveLine.browse(move_line_ids)
        writeoff_lines = AccountMoveLine
        # Create writeoff move lines
        if len(new_mv_line_dicts) > 0:
            company_currency = account_move_line[0].account_id.company_id.currency_id
            same_currency = False
            currencies = list(
                {aml.currency_id or company_currency for aml in account_move_line}
            )
            if len(currencies) == 1 and currencies[0] != company_currency:
                same_currency = True
            # We don't have to convert debit/credit to currency as all values in
            # the reconciliation widget are displayed in company currency
            # If all the lines are in the same currency, create writeoff entry
            # with same currency also
            for mv_line_dict in new_mv_line_dicts:
                if not same_currency:
                    mv_line_dict["amount_currency"] = False
                writeoff_lines += account_move_line._create_writeoff([mv_line_dict])

            (account_move_line + writeoff_lines).reconcile()
        else:
            account_move_line.reconcile()
