# Copyright 2017-2018  Alexandre Díaz
# Copyright 2017  Dario Lodeiros
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

import datetime
import logging
from itertools import groupby

from odoo import _, api, fields, models
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tools import float_compare, float_is_zero
from odoo.tools.misc import get_lang

_logger = logging.getLogger(__name__)


class PmsFolio(models.Model):
    _name = "pms.folio"
    _description = "PMS Folio"
    _inherit = ["mail.thread", "mail.activity.mixin", "portal.mixin"]
    _order = "date_order"
    _check_company_auto = True
    _check_pms_properties_auto = True

    name = fields.Char(
        string="Folio Number",
        help="Folio name. When creating a folio the "
        "name is automatically formed with a sequence",
        readonly=True,
        index=True,
        default=lambda self: _("New"),
    )
    external_reference = fields.Char(
        string="External Reference",
        help="Reference of this folio in an external system",
        compute="_compute_external_reference",
        readonly=False,
        store=True,
    )
    pms_property_id = fields.Many2one(
        string="Property",
        help="The property for folios",
        comodel_name="pms.property",
        required=True,
        index=True,
        default=lambda self: self.env.user.get_active_property_ids()[0],
        check_pms_properties=True,
    )
    partner_id = fields.Many2one(
        string="Partner",
        help="The folio customer",
        readonly=False,
        store=True,
        tracking=True,
        index=True,
        compute="_compute_partner_id",
        comodel_name="res.partner",
        ondelete="restrict",
        check_pms_properties=True,
    )
    reservation_ids = fields.One2many(
        string="Reservations",
        help="Room reservation detail",
        readonly=False,
        states={"done": [("readonly", True)]},
        comodel_name="pms.reservation",
        inverse_name="folio_id",
        check_company=True,
        check_pms_properties=True,
    )
    number_of_rooms = fields.Integer(
        string="Number of Rooms",
        help="Number of rooms in folio. Canceled rooms do not count.",
        store="True",
        compute="_compute_number_of_rooms",
    )
    number_of_cancelled_rooms = fields.Integer(
        string="Number of Cancelled Rooms",
        help="Number of cancelled rooms in folio.",
        store="True",
        compute="_compute_number_of_cancelled_rooms",
    )
    number_of_services = fields.Integer(
        string="Number of Services",
        help="Number of services in the folio",
        store="True",
        compute="_compute_number_of_services",
    )
    service_ids = fields.One2many(
        string="Service",
        help="Services detail provide to customer and it will "
        "include in main Invoice.",
        readonly=False,
        states={"done": [("readonly", True)]},
        comodel_name="pms.service",
        inverse_name="folio_id",
        check_company=True,
        check_pms_properties=True,
    )
    sale_line_ids = fields.One2many(
        string="Sale lines",
        help="Sale lines in folio. It correspond with reservation nights",
        store="True",
        compute="_compute_sale_line_ids",
        compute_sudo=True,
        comodel_name="folio.sale.line",
        inverse_name="folio_id",
    )
    invoice_count = fields.Integer(
        string="Invoice Count",
        help="The amount of invoices in out invoice and out refund status",
        readonly=True,
        compute="_compute_get_invoiced",
    )
    company_id = fields.Many2one(
        string="Company",
        help="The company for folio",
        store=True,
        index=True,
        comodel_name="res.company",
        compute="_compute_company_id",
    )
    analytic_account_id = fields.Many2one(
        string="Analytic Account",
        help="The analytic account related to a folio.",
        readonly=True,
        index=True,
        states={"draft": [("readonly", False)], "sent": [("readonly", False)]},
        copy=False,
        comodel_name="account.analytic.account",
    )
    currency_id = fields.Many2one(
        string="Currency",
        help="The currency of the property location",
        readonly=True,
        required=True,
        index=True,
        related="pricelist_id.currency_id",
        ondelete="restrict",
    )
    pricelist_id = fields.Many2one(
        string="Pricelist",
        help="Pricelist for current folio.",
        readonly=False,
        store=True,
        index=True,
        comodel_name="product.pricelist",
        ondelete="restrict",
        check_pms_properties=True,
        compute="_compute_pricelist_id",
    )
    commission = fields.Float(
        string="Commission",
        readonly=True,
        store=True,
        compute="_compute_commission",
    )
    user_id = fields.Many2one(
        string="Reception Manager",
        help="The reception manager in the folio",
        readonly=False,
        index=True,
        store=True,
        comodel_name="res.users",
        ondelete="restrict",
        compute="_compute_user_id",
        tracking=True,
    )
    revenue_user_id = fields.Many2one(
        string="Revenue Manager",
        help="The revenue manager in the folio",
        readonly=False,
        index=True,
        store=True,
        comodel_name="res.users",
        ondelete="restrict",
        compute="_compute_revenue_user_id",
        tracking=True,
    )
    administrative_user_id = fields.Many2one(
        string="Administrative Manager",
        help="The administrative manager in the folio",
        readonly=False,
        index=True,
        store=True,
        comodel_name="res.users",
        ondelete="restrict",
        compute="_compute_administrative_user_id",
        tracking=True,
    )
    manager_user_id = fields.Many2one(
        string="Main Manager",
        help="The main manager in the folio",
        readonly=False,
        index=True,
        store=True,
        comodel_name="res.users",
        ondelete="restrict",
        compute="_compute_manager_user_id",
        tracking=True,
    )
    agency_id = fields.Many2one(
        string="Agency",
        help="Only allowed if the field of partner is_agency is True",
        comodel_name="res.partner",
        domain=[("is_agency", "=", True)],
        ondelete="restrict",
        index=True,
        check_pms_properties=True,
    )
    sale_channel_ids = fields.Many2many(
        string="Sale Channels",
        help="Sale Channels through which reservations were managed",
        store=True,
        compute="_compute_sale_channel_ids",
        comodel_name="pms.sale.channel",
    )
    sale_channel_origin_id = fields.Many2one(
        string="Sale Channel Origin",
        help="Sale Channel through which folio was created, the original",
        comodel_name="pms.sale.channel",
        index=True,
    )

    transaction_ids = fields.Many2many(
        string="Transactions",
        help="Payments made through payment acquirer",
        readonly=True,
        copy=False,
        comodel_name="payment.transaction",
        relation="payment_transaction_folio_rel",
        column1="folio_id",
        column2="payment_transaction_id",
    )
    payment_ids = fields.Many2many(
        string="Bank Payments",
        help="Payments",
        readonly=True,
        copy=False,
        comodel_name="account.payment",
        relation="account_payment_folio_rel",
        column1="folio_id",
        column2="payment_id",
    )
    statement_line_ids = fields.Many2many(
        string="Cash Payments",
        help="Statement lines",
        readonly=True,
        copy=False,
        comodel_name="account.bank.statement.line",
        relation="account_bank_statement_folio_rel",
        column1="folio_id",
        column2="account_journal_id",
    )
    payment_term_id = fields.Many2one(
        string="Payment Terms",
        help="Payment terms for current folio.",
        readonly=False,
        store=True,
        index=True,
        comodel_name="account.payment.term",
        ondelete="restrict",
        compute="_compute_payment_term_id",
    )
    checkin_partner_ids = fields.One2many(
        string="Checkin Partners",
        help="The checkin partners on a folio",
        comodel_name="pms.checkin.partner",
        inverse_name="folio_id",
    )
    count_rooms_pending_arrival = fields.Integer(
        string="Pending Arrival",
        help="The number of rooms left to occupy.",
        store=True,
        compute="_compute_count_rooms_pending_arrival",
    )
    pending_checkin_data = fields.Integer(
        string="Checkin Data",
        compute="_compute_pending_checkin_data",
        store=True,
    )
    ratio_checkin_data = fields.Integer(
        string="Pending Checkin Data",
        help="Field that stores the number of checkin partners pending "
        "to checkin (with the state = draft)",
        compute="_compute_ratio_checkin_data",
    )
    move_ids = fields.Many2many(
        string="Invoices",
        help="Folio invoices related to account move.",
        readonly=True,
        copy=False,
        comodel_name="account.move",
        compute="_compute_get_invoiced",
        search="_search_invoice_ids",
    )
    untaxed_amount_to_invoice = fields.Monetary(
        string="Amount to invoice",
        help="The amount to invoice",
        readonly=True,
        store=True,
        compute="_compute_untaxed_amount_to_invoice",
    )
    payment_state = fields.Selection(
        string="Payment Status",
        help="The state of the payment",
        copy=False,
        readonly=True,
        store=True,
        selection=[
            ("not_paid", "Not Paid"),
            ("paid", "Paid"),
            ("partial", "Partially Paid"),
            ("overpayment", "Overpayment"),
            ("nothing_to_pay", "Nothing to pay"),
        ],
        compute="_compute_amount",
        tracking=True,
    )
    partner_invoice_ids = fields.Many2many(
        string="Billing addresses",
        help="Invoice address for current group.",
        readonly=False,
        store=True,
        comodel_name="res.partner",
        relation="pms_folio_partner_rel",
        column1="folio",
        column2="partner",
        compute="_compute_partner_invoice_ids",
        check_pms_properties=True,
    )
    # REVIEW THIS
    # partner_invoice_state_id = fields.Many2one(related="partner_invoice_id.state_id")
    # partner_invoice_country_id = fields.Many2one(
    #     related="partner_invoice_id.country_id"
    # )
    fiscal_position_id = fields.Many2one(
        string="Fiscal Position",
        help="The fiscal position depends on the location of the client",
        comodel_name="account.fiscal.position",
        index=True,
    )
    closure_reason_id = fields.Many2one(
        string="Closure Reason",
        help="The closure reason for a closure room",
        comodel_name="room.closure.reason",
        index=True,
        check_pms_properties=True,
    )
    out_service_description = fields.Text(
        string="Cause of out of service",
        help="Indicates the cause of out of service",
    )
    segmentation_ids = fields.Many2many(
        string="Segmentation",
        help="Segmentation tags to classify folios",
        comodel_name="res.partner.category",
        ondelete="restrict",
        domain="[('is_used_in_checkin', '=', True)]",
    )
    reservation_type = fields.Selection(
        string="Type",
        help="The type of the reservation. "
        "Can be 'Normal', 'Staff' or 'Out of Service'",
        default=lambda *a: "normal",
        selection=[("normal", "Normal"), ("staff", "Staff"), ("out", "Out of Service")],
    )
    date_order = fields.Datetime(
        string="Order Date",
        help="Date on which folio is sold",
        readonly=True,
        required=True,
        index=True,
        default=fields.Datetime.now,
        states={"draft": [("readonly", False)], "sent": [("readonly", False)]},
        copy=False,
    )
    confirmation_date = fields.Datetime(
        string="Confirmation Date",
        help="Date on which the folio is confirmed.",
        readonly=True,
        index=True,
        copy=False,
    )
    state = fields.Selection(
        string="Status",
        help="Folio status; it can be Quotation, "
        "Quotation Sent, Confirmed, Locked or Cancelled",
        readonly=True,
        index=True,
        default="draft",
        copy=False,
        selection=[
            ("draft", "Quotation"),
            ("sent", "Quotation Sent"),
            ("confirm", "Confirmed"),
            ("done", "Locked"),
            ("cancel", "Cancelled"),
        ],
        tracking=True,
    )
    partner_name = fields.Char(
        string="Customer Name",
        help="In the name of whom the reservation is made",
        store=True,
        readonly=False,
        compute="_compute_partner_name",
    )
    email = fields.Char(
        string="E-mail",
        help="Customer E-mail",
        store=True,
        readonly=False,
        compute="_compute_email",
    )
    mobile = fields.Char(
        string="Mobile",
        help="Customer Mobile",
        store=True,
        readonly=False,
        compute="_compute_mobile",
    )
    partner_incongruences = fields.Char(
        string="partner_incongruences",
        help="indicates that some partner fields \
            on the folio do not correspond to that of \
            the associated partner",
        compute="_compute_partner_incongruences",
    )
    partner_internal_comment = fields.Text(
        string="Internal Partner Notes",
        help="Internal notes of the partner",
        related="partner_id.comment",
        store=True,
        readonly=False,
    )
    credit_card_details = fields.Text(
        string="Credit Card Details",
        help="Details of partner credit card",
    )

    pending_amount = fields.Monetary(
        string="Pending Amount",
        help="The amount that remains to be paid",
        store=True,
        compute="_compute_amount",
    )
    # refund_amount = fields.Monetary(
    #     compute="_compute_amount", store=True, string="Payment Returns"
    # )
    invoices_paid = fields.Monetary(
        string="Paid Out",
        help="Amount of invoices paid",
        store=True,
        compute="_compute_amount",
        tracking=True,
    )
    payment_multi = fields.Boolean(
        string="Folio paid with payments assigned to other folios",
        help="Technical field for manage payments with multiple folios assigned",
        readonly=True,
        store=True,
        compute="_compute_amount",
    )
    amount_untaxed = fields.Monetary(
        string="Untaxed Amount",
        help="The price without taxes on a folio",
        readonly=True,
        store=True,
        compute="_compute_amount_all",
        tracking=True,
    )
    amount_tax = fields.Monetary(
        string="Taxes",
        help="Price with taxes on a folio",
        readonly=True,
        store=True,
        compute="_compute_amount_all",
    )
    amount_total = fields.Monetary(
        string="Total",
        help="Total amount to be paid",
        readonly=True,
        store=True,
        compute="_compute_amount_all",
        tracking=True,
    )
    max_reservation_priority = fields.Integer(
        string="Max reservation priority on the entire folio",
        help="Max reservation priority on the entire folio",
        compute="_compute_max_reservation_priority",
        store=True,
    )
    invoice_status = fields.Selection(
        string="Invoice Status",
        help="Invoice Status; it can be: invoiced, to invoice, to confirm, no",
        readonly=True,
        default="no",
        store=True,
        selection=[
            ("invoiced", "Fully Invoiced"),
            ("to_invoice", "To Invoice"),
            ("to_confirm", "To Confirm"),
            ("no", "Nothing to Invoice"),
        ],
        compute="_compute_get_invoice_status",
        compute_sudo=True,
    )
    force_nothing_to_invoice = fields.Boolean(
        string="Force no invoice",
        help="When you set this field, the folio will be considered as "
        "nothin to invoice, even when there may be ordered "
        "quantities pending to invoice.",
        copy=False,
        compute="_compute_force_nothing_to_invoice",
        readonly=False,
        store=True,
    )
    internal_comment = fields.Text(
        string="Internal Folio Notes",
        help="Internal Folio notes for Staff",
    )
    cancelled_reason = fields.Text(
        string="Cause of cancelled",
        help="Indicates cause of cancelled",
    )
    prepaid_warning_days = fields.Integer(
        string="Prepaid Warning Days",
        help="Margin in days to create a notice if a payment \
                advance has not been recorded",
    )
    sequence = fields.Integer(
        string="Sequence",
        help="Sequence used to form the name of the folio",
        default=10,
    )
    note = fields.Text(
        string="Terms and conditions",
        help="Folio billing terms and conditions",
        default=lambda self: self._default_note(),
    )
    reference = fields.Char(
        string="Payment Ref.",
        help="The payment communication of this sale order.",
        copy=False,
    )

    possible_existing_customer_ids = fields.One2many(
        string="Possible existing customer",
        compute="_compute_possible_existing_customer_ids",
        comodel_name="res.partner",
        inverse_name="folio_possible_customer_id",
    )
    first_checkin = fields.Date(
        string="First Folio Checkin",
        readonly=False,
        store=True,
        compute="_compute_first_checkin",
    )
    days_to_checkin = fields.Integer(
        string="Days to Checkin",
        help="""Technical field to facilitate
            filtering by dates related to checkin""",
        compute="_compute_days_to_checkin",
        search="_search_days_to_checkin",
    )
    last_checkout = fields.Date(
        string="Last Folio Checkout",
        readonly=False,
        store=True,
        compute="_compute_last_checkout",
    )
    days_to_checkout = fields.Integer(
        string="Days to Checkout",
        help="""Technical field to facilitate
            filtering by dates related to checkout""",
        compute="_compute_days_to_checkout",
        search="_search_days_to_checkout",
    )
    invoice_to_agency = fields.Boolean(
        string="Invoice Agency",
        help="""Indicates if agency invoices partner
            (it only affects those nights/services sold through the agency)""",
        compute="_compute_invoice_to_agengy",
        store=True,
        readonly=False,
    )
    lang = fields.Selection(
        selection=lambda self: self._get_lang_selection_options(),
        string="Language",
        help="Language used for the folio",
        compute="_compute_lang",
        store=True,
        readonly=False,
    )

    @api.model
    def _get_lang_selection_options(self):
        """Gets the available languages for the selection."""
        langs = self.env["res.lang"].search([])
        return [(lang.code, lang.name) for lang in langs]

    def name_get(self):
        result = []
        for folio in self:
            name = folio.name
            if len(folio.reservation_ids) > 1:
                name += " (%s)" % len(folio.reservation_ids)
            result.append((folio.id, name))
        return result

    def _default_note(self):
        return (
            self.env["ir.config_parameter"]
            .sudo()
            .get_param("account.use_invoice_terms")
            and self.env.company.invoice_terms
            or ""
        )

    def _get_report_base_filename(self):
        self.ensure_one()
        return "Folio %s" % self.name

    def _get_invoice_grouping_keys(self):
        return ["company_id", "partner_id", "currency_id"]

    def get_invoice_vals_list(
        self, final=False, lines_to_invoice=False, partner_invoice_id=False
    ):
        precision = self.env["decimal.precision"].precision_get(
            "Product Unit of Measure"
        )
        invoice_vals_list = []
        invoice_item_sequence = 0
        for folio in self:
            folio_lines_to_invoice = folio.sale_line_ids.filtered(
                lambda l: l.id in list(lines_to_invoice.keys())
            )
            groups_invoice_lines = folio._get_groups_invoice_lines(
                lines_to_invoice=folio_lines_to_invoice,
                partner_invoice_id=partner_invoice_id,
            )
            for group in groups_invoice_lines:
                folio = folio.with_company(folio.company_id)
                down_payments = folio.env["folio.sale.line"]

                # Invoice values.
                invoice_vals = folio._prepare_invoice(
                    partner_invoice_id=group["partner_id"]
                )
                # Invoice line values (keep only necessary sections).
                current_section_vals = None
                invoice_lines_vals = []
                for line in group["lines"]:
                    if line.display_type == "line_section":
                        current_section_vals = line._prepare_invoice_line(
                            sequence=invoice_item_sequence
                            + folio.sale_line_ids.ids.index(line.id)
                        )
                        continue
                    if line.display_type != "line_note" and float_is_zero(
                        line.qty_to_invoice, precision_digits=precision
                    ):
                        continue
                    if (
                        line.qty_to_invoice > 0
                        or (line.qty_to_invoice < 0 and final)
                        or line.display_type == "line_note"
                    ):
                        if line.is_downpayment:
                            down_payments += line
                            continue
                        if current_section_vals:
                            invoice_lines_vals.append(current_section_vals)
                            current_section_vals = None
                        prepared_line = line._prepare_invoice_line(
                            sequence=invoice_item_sequence
                            + folio.sale_line_ids.ids.index(line.id),
                            qty=lines_to_invoice[line.id],
                        )
                        invoice_lines_vals.append(prepared_line)

                # If down payments are present in SO, group them under common section
                if down_payments:
                    down_payments_section = folio._prepare_down_payment_section_line(
                        sequence=invoice_item_sequence
                    )
                    invoice_lines_vals.append(down_payments_section)
                    for down_payment in down_payments:
                        # If the down payment is not for the current partner, skip it
                        # it will be managed manually or by the automatic invoice cron
                        if down_payment.default_invoice_to.id != group["partner_id"]:
                            continue
                        invoice_item_sequence += 1
                        invoice_down_payment_vals = down_payment._prepare_invoice_line(
                            sequence=invoice_item_sequence
                            + folio.sale_line_ids.ids.index(down_payment.id)
                        )
                        invoice_lines_vals.append(invoice_down_payment_vals)

                if not any(
                    new_line["display_type"] is False for new_line in invoice_lines_vals
                ):
                    raise self._nothing_to_invoice_error()
                invoice_vals["invoice_line_ids"] = [
                    (0, 0, invoice_line_id) for invoice_line_id in invoice_lines_vals
                ]

                invoice_vals_list.append(invoice_vals)
            invoice_item_sequence += 1000
        return invoice_vals_list

    def _get_groups_invoice_lines(self, lines_to_invoice, partner_invoice_id=False):
        self.ensure_one()
        groups_invoice_lines = []
        if partner_invoice_id:
            groups_invoice_lines.append(
                {
                    "partner_id": partner_invoice_id,
                    "lines": lines_to_invoice,
                }
            )
        else:
            partners = lines_to_invoice.mapped("default_invoice_to")
            for partner in partners:
                groups_invoice_lines.append(
                    {
                        "partner_id": partner.id,
                        "lines": lines_to_invoice.filtered(
                            lambda l: l.default_invoice_to == partner
                        ),
                    }
                )
            if any(not line.default_invoice_to for line in lines_to_invoice):
                groups_invoice_lines.append(
                    {
                        "partner_id": self.env.ref("pms.various_pms_partner").id,
                        "lines": lines_to_invoice.filtered(
                            lambda l: not l.default_invoice_to
                        ),
                    }
                )
        return groups_invoice_lines

    def _get_tax_amount_by_group(self):
        self.ensure_one()
        res = {}
        for line in self.sale_line_ids:
            price_reduce = line.price_total
            product = line.product_id
            taxes = line.tax_ids.compute_all(price_reduce, quantity=1, product=product)[
                "taxes"
            ]
            for tax in line.tax_ids:
                group = tax.tax_group_id
                res.setdefault(group, {"amount": 0.0, "base": 0.0})
                for t in taxes:
                    if t["id"] == tax.id or t["id"] in tax.children_tax_ids.ids:
                        res[group]["amount"] += t["amount"]
                        res[group]["base"] += t["base"]
        res = sorted(res.items(), key=lambda line: line[0].sequence)
        res = [
            (line[0].name, line[1]["amount"], line[1]["base"], len(res)) for line in res
        ]
        return res

    @api.depends("reservation_ids", "reservation_ids.external_reference")
    def _compute_external_reference(self):
        for folio in self:
            folio.external_reference = folio._get_folio_external_reference()

    def _get_folio_external_reference(self):
        self.ensure_one()
        references = list(set(self.reservation_ids.mapped("external_reference")))
        references = list(filter(bool, references))
        if references:
            return ",".join(references)
        else:
            return False

    @api.depends("reservation_ids", "reservation_ids.state")
    def _compute_number_of_rooms(self):
        for folio in self:
            folio.number_of_rooms = len(
                folio.reservation_ids.filtered(lambda a: a.state != "cancel")
            )

    @api.depends("reservation_ids", "reservation_ids.state")
    def _compute_number_of_cancelled_rooms(self):
        for folio in self:
            folio.number_of_cancelled_rooms = len(
                folio.reservation_ids.filtered(lambda a: a.state == "cancel")
            )

    @api.depends("service_ids", "service_ids.product_qty")
    def _compute_number_of_services(self):
        for folio in self:
            folio.number_of_services = sum(folio.service_ids.mapped("product_qty"))

    @api.depends(
        "reservation_ids",
        "service_ids",
        "service_ids.reservation_id",
        "service_ids.default_invoice_to",
        "service_ids.service_line_ids.price_day_total",
        "service_ids.service_line_ids.discount",
        "service_ids.service_line_ids.cancel_discount",
        "service_ids.service_line_ids.day_qty",
        "service_ids.service_line_ids.tax_ids",
        "reservation_ids.reservation_line_ids",
        "reservation_ids.reservation_line_ids.price",
        "reservation_ids.reservation_line_ids.discount",
        "reservation_ids.reservation_line_ids.cancel_discount",
        "reservation_ids.reservation_line_ids.default_invoice_to",
        "reservation_ids.tax_ids",
        "reservation_ids.state",
    )
    def _compute_sale_line_ids(self):
        for folio in self.filtered(lambda f: isinstance(f.id, int)):
            sale_lines_vals = []
            if folio.reservation_type in ("normal", "staff"):
                sale_lines_vals_to_drop = []
                seq = 0
                for reservation in sorted(
                    folio.reservation_ids.filtered(lambda r: isinstance(r.id, int)),
                    key=lambda r: r.folio_sequence,
                ):
                    seq += reservation.folio_sequence
                    # RESERVATION LINES
                    reservation_sale_lines = []
                    reservation_sale_lines_to_drop = []
                    if reservation.reservation_line_ids:
                        (
                            reservation_sale_lines,
                            reservation_sale_lines_to_drop,
                        ) = self._get_reservation_sale_lines(
                            folio, reservation, sequence=seq
                        )
                    if reservation_sale_lines:
                        sale_lines_vals.extend(reservation_sale_lines)
                    if reservation_sale_lines_to_drop:
                        sale_lines_vals_to_drop.extend(reservation_sale_lines_to_drop)
                    seq += len(reservation_sale_lines)
                    # RESERVATION SERVICES
                    service_sale_lines = []
                    service_sale_lines_to_drop = []
                    if reservation.service_ids:
                        (
                            service_sale_lines,
                            service_sale_lines_to_drop,
                        ) = self._get_service_sale_lines(
                            folio,
                            reservation,
                            sequence=seq,
                        )
                        if service_sale_lines:
                            sale_lines_vals.extend(service_sale_lines)
                        if service_sale_lines_to_drop:
                            sale_lines_vals_to_drop.extend(service_sale_lines_to_drop)
                    seq += len(service_sale_lines)
                # FOLIO SERVICES
                if folio.service_ids.filtered(lambda r: not r.reservation_id):
                    service_sale_lines = False
                    service_sale_lines_to_drop = False
                    (
                        service_sale_lines,
                        service_sale_lines_to_drop,
                    ) = self._get_folio_services_sale_lines(folio, sequence=seq + 1)
                    if service_sale_lines:
                        sale_lines_vals.extend(service_sale_lines)
                    if service_sale_lines_to_drop:
                        sale_lines_vals_to_drop.extend(service_sale_lines_to_drop)
                if sale_lines_vals:
                    folio.sale_line_ids = sale_lines_vals
                if sale_lines_vals_to_drop:
                    self.env["folio.sale.line"].browse(sale_lines_vals_to_drop).unlink()
            if not sale_lines_vals:
                folio.sale_line_ids = False

    @api.depends("pms_property_id")
    def _compute_company_id(self):
        for record in self:
            record.company_id = record.pms_property_id.company_id

    @api.depends(
        "partner_id", "agency_id", "reservation_ids", "reservation_ids.pricelist_id"
    )
    def _compute_pricelist_id(self):
        for folio in self:
            is_new = not folio.pricelist_id or isinstance(folio.id, models.NewId)
            if folio.reservation_type == "out":
                folio.pricelist_id = False
            elif len(folio.reservation_ids.pricelist_id) == 1:
                folio.pricelist_id = folio.reservation_ids.pricelist_id
            elif is_new and folio.agency_id and folio.agency_id.apply_pricelist:
                folio.pricelist_id = folio.agency_id.property_product_pricelist
            elif (
                is_new
                and folio.partner_id
                and folio.partner_id.property_product_pricelist
                and folio.partner_id.property_product_pricelist.is_pms_available
            ):
                folio.pricelist_id = folio.partner_id.property_product_pricelist
            elif not folio.pricelist_id:
                folio.pricelist_id = folio.pms_property_id.default_pricelist_id

    @api.depends(
        "agency_id",
        "reservation_type",
        "partner_name",
        "email",
        "mobile",
    )
    def _compute_partner_id(self):
        for folio in self:
            if folio.reservation_type == "out":
                folio.partner_id = False
            elif folio.agency_id and folio.invoice_to_agency:
                folio.partner_id = folio.agency_id
            elif not folio.partner_id:
                folio.partner_id = False

    @api.depends("pms_property_id")
    def _compute_user_id(self):
        active_user_id = self.env.uid
        for folio in self:
            if not folio.user_id:
                property_users = folio.pms_property_id.member_ids.filtered(
                    lambda u: u.pms_role == "reception"
                ).mapped("user_id")
                if property_users:
                    if active_user_id in property_users.ids:
                        folio.user_id = active_user_id
                    elif property_users:
                        folio.user_id = property_users[0]
                    else:
                        folio.user_id = active_user_id or folio.pms_property_id.user_id

    @api.depends("pms_property_id")
    def _compute_revenue_user_id(self):
        for folio in self:
            revenue_users = folio.pms_property_id.member_ids.filtered(
                lambda u: u.pms_role == "revenue"
            ).mapped("user_id")
            if revenue_users:
                folio.revenue_user_id = revenue_users[0]
            else:
                folio.revenue_user_id = False

    @api.depends("pms_property_id")
    def _compute_administrative_user_id(self):
        for folio in self:
            administrative_users = folio.pms_property_id.member_ids.filtered(
                lambda u: u.pms_role == "administrative"
            ).mapped("user_id")
            if administrative_users:
                folio.administrative_user_id = administrative_users[0]
            else:
                folio.administrative_user_id = False

    @api.depends("pms_property_id")
    def _compute_manager_user_id(self):
        for folio in self:
            manager_users = folio.pms_property_id.member_ids.filtered(
                lambda u: u.pms_role == "manager"
            ).mapped("user_id")
            if manager_users:
                folio.manager_user_id = manager_users[0]
            else:
                folio.manager_user_id = False

    @api.depends(
        "partner_id",
        "reservation_ids",
        "reservation_ids.partner_id",
        "reservation_ids.checkin_partner_ids",
        "reservation_ids.checkin_partner_ids.partner_id",
    )
    def _compute_partner_invoice_ids(self):
        for folio in self:
            if folio.partner_id:
                addr = folio.partner_id.address_get(["invoice"])
                if not addr["invoice"] in folio.partner_invoice_ids.ids:
                    folio.partner_invoice_ids = [(4, addr["invoice"])]
            for reservation in folio.reservation_ids:
                if reservation.partner_id:
                    addr = reservation.partner_id.address_get(["invoice"])
                    if not addr["invoice"] in folio.partner_invoice_ids.ids:
                        folio.partner_invoice_ids = [(4, addr["invoice"])]
                for checkin in reservation.checkin_partner_ids:
                    if checkin.partner_id:
                        addr = checkin.partner_id.address_get(["invoice"])
                        if not addr["invoice"] in folio.partner_invoice_ids.ids:
                            folio.partner_invoice_ids = [(4, addr["invoice"])]
        self.filtered(lambda f: not f.partner_invoice_ids).partner_invoice_ids = False

    @api.depends("partner_id")
    def _compute_payment_term_id(self):
        self.payment_term_id = False
        for folio in self:
            folio.payment_term_id = (
                folio.partner_id.property_payment_term_id
                and folio.partner_id.property_payment_term_id.id
                or False
            )

    @api.depends("reservation_ids", "reservation_ids.commission_amount")
    def _compute_commission(self):
        for folio in self:
            folio.commission = 0
            for reservation in folio.reservation_ids:
                if reservation.commission_amount != 0:
                    folio.commission = folio.commission + reservation.commission_amount

    @api.depends(
        "reservation_ids",
        "reservation_ids.sale_channel_ids",
        "service_ids",
        "service_ids.sale_channel_origin_id",
    )
    def _compute_sale_channel_ids(self):
        for record in self:
            sale_channel_ids = []
            if record.reservation_ids:
                for sale in record.reservation_ids.mapped("sale_channel_ids.id"):
                    sale_channel_ids.append(sale)
            if record.service_ids:
                for sale in record.service_ids.mapped("sale_channel_origin_id.id"):
                    sale_channel_ids.append(sale)
            sale_channel_ids = list(set(sale_channel_ids))
            record.sale_channel_ids = [(6, 0, sale_channel_ids)]

    @api.depends("sale_line_ids.invoice_lines")
    def _compute_get_invoiced(self):
        # The invoice_ids are obtained thanks to the invoice lines of the SO
        # lines, and we also search for possible refunds created directly from
        # existing invoices. This is necessary since such a refund is not
        # directly linked to the SO.
        for order in self:
            invoices = order.sale_line_ids.invoice_lines.move_id.filtered(
                lambda r: r.move_type
                in ("out_invoice", "out_refund", "out_receipt", "in_receipt")
            )
            order.move_ids = invoices
            order.invoice_count = len(invoices)

    # @api.depends(
    #     "reservation_ids",
    #     "reservation_ids.currency_id"
    # )
    # def _compute_currency_id(self):
    #     if len(self.reservation_ids.mapped("currency_id")) == 1:
    #         self.currency_id = self.reservation_ids.mapped("currency_id")
    #     else:
    #         raise UserError(_("Some reservations have different currency"))

    # is_checkin = fields.Boolean()

    def _compute_access_url(self):
        super(PmsFolio, self)._compute_access_url()
        for folio in self:
            folio.access_url = "/my/folios/%s" % (folio.id)

    @api.depends("state", "sale_line_ids.invoice_status", "force_nothing_to_invoice")
    def _compute_get_invoice_status(self):
        """
        Compute the invoice status of a Folio. Possible statuses:
        - no: if the Folio is in status 'draft', we consider that there is nothing to
          invoice. This is also the default value if the conditions of no
          other status is met.
        - to_invoice: if any SO line is 'to_invoice', the whole SO is 'to_invoice'
        - invoiced: if all SO lines are invoiced, the SO is invoiced.
        """
        unconfirmed_orders = self.filtered(lambda folio: folio.state in ["draft"])
        unconfirmed_orders.invoice_status = "no"
        zero_orders = self.filtered(lambda folio: folio.amount_total == 0)
        confirmed_orders = self - unconfirmed_orders - zero_orders
        if not confirmed_orders:
            return
        line_invoice_status_all = [
            (d["folio_id"][0], d["invoice_status"])
            for d in self.env["folio.sale.line"].read_group(
                [
                    ("folio_id", "in", confirmed_orders.ids),
                    ("is_downpayment", "=", False),
                    ("display_type", "=", False),
                ],
                ["folio_id", "invoice_status"],
                ["folio_id", "invoice_status"],
                lazy=False,
            )
        ]
        for order in confirmed_orders:
            line_invoice_status = [
                d[1] for d in line_invoice_status_all if d[0] == order.id
            ]
            if not order.force_nothing_to_invoice and any(
                invoice_status == "to_invoice" for invoice_status in line_invoice_status
            ):
                order.invoice_status = "to_invoice"
            elif any(inv.state == "draft" for inv in order.move_ids):
                order.invoice_status = "to_confirm"
            elif line_invoice_status and any(
                invoice_status == "invoiced" for invoice_status in line_invoice_status
            ):
                order.invoice_status = "invoiced"
            else:
                order.invoice_status = "no"

    @api.depends("untaxed_amount_to_invoice")
    def _compute_force_nothing_to_invoice(self):
        # If the invoice amount and amount total are the same,
        # and the qty to invoice is not 0, we force nothing to invoice
        for order in self:
            if (
                order.untaxed_amount_to_invoice <= 0
                and sum(order.sale_line_ids.mapped("qty_to_invoice")) != 0
            ):
                order.force_nothing_to_invoice = True
            else:
                order.force_nothing_to_invoice = False

    @api.depends("partner_id", "partner_id.name", "agency_id", "reservation_type")
    def _compute_partner_name(self):
        for record in self:
            if record.partner_id and record.partner_id != record.agency_id:
                record.partner_name = record.partner_id.name
            elif record.agency_id and not record.partner_name:
                # if the customer not is the agency but we dont know the customer's name,
                # set the name provisional
                record.partner_name = _("Reservation from ") + record.agency_id.name
            elif not record.partner_name:
                record.partner_name = False

    @api.depends("partner_id", "partner_id.email", "agency_id")
    def _compute_email(self):
        for record in self:
            self._apply_email(record)

    @api.depends("partner_id", "partner_id.mobile", "agency_id")
    def _compute_mobile(self):
        for record in self:
            self._apply_mobile(record)

    @api.depends(
        "partner_name",
        "email",
        "mobile",
        "partner_id",
    )
    def _compute_partner_incongruences(self):
        fields_mapping = {
            "partner_name": "name",
            "email": "email",
            "mobile": "mobile",
        }
        for record in self:
            incongruous_fields = False
            if record.partner_id:
                for k, v in fields_mapping.items():
                    if record.partner_id[v] and record.partner_id[v] != record[k]:
                        if not incongruous_fields:
                            incongruous_fields = v
                        else:
                            incongruous_fields += ", " + v
                if incongruous_fields:
                    record.partner_incongruences = (
                        incongruous_fields + " field/s don't correspond to saved host"
                    )
                else:
                    record.partner_incongruences = False
            else:
                record.partner_incongruences = False

    @api.depends("sale_line_ids.price_total")
    def _compute_amount_all(self):
        """
        Compute the total amounts of the SO.
        """
        for folio in self:
            amount_untaxed = amount_tax = 0.0
            for line in folio.sale_line_ids:
                amount_untaxed += line.price_subtotal
                amount_tax += line.price_tax
            folio.update(
                {
                    "amount_untaxed": amount_untaxed,
                    "amount_tax": amount_tax,
                    "amount_total": amount_untaxed + amount_tax,
                }
            )

    @api.depends("reservation_ids", "reservation_ids.state")
    def _compute_count_rooms_pending_arrival(self):
        self.count_rooms_pending_arrival = 0
        for folio in self.filtered("reservation_ids"):
            folio.count_rooms_pending_arrival = len(
                folio.reservation_ids.filtered(
                    lambda c: c.state in ("draf", "confirm", "arrival_delayed")
                )
            )

    @api.depends("checkin_partner_ids", "checkin_partner_ids.state")
    def _compute_pending_checkin_data(self):
        for folio in self:
            if folio.reservation_type != "out":
                folio.pending_checkin_data = len(
                    folio.checkin_partner_ids.filtered(lambda c: c.state == "draft")
                )

    @api.depends("pending_checkin_data")
    def _compute_ratio_checkin_data(self):
        self.ratio_checkin_data = 0
        for folio in self.filtered("reservation_ids"):
            if folio.reservation_type != "out":
                folio.ratio_checkin_data = (
                    (
                        sum(folio.reservation_ids.mapped("adults"))
                        - folio.pending_checkin_data
                    )
                    * 100
                    / sum(folio.reservation_ids.mapped("adults"))
                )

    @api.depends("sale_line_ids.untaxed_amount_to_invoice")
    def _compute_untaxed_amount_to_invoice(self):
        for folio in self:
            folio.untaxed_amount_to_invoice = sum(
                folio.sale_line_ids.filtered(lambda l: not l.is_downpayment).mapped(
                    "untaxed_amount_to_invoice"
                )
            )

    # TODO: Add return_ids to depends
    @api.depends(
        "amount_total",
        "currency_id",
        "company_id",
        "reservation_type",
        "state",
        "payment_ids.state",
        "payment_ids.move_id",
        "payment_ids.move_id.line_ids",
        "payment_ids.move_id.line_ids.date",
        "payment_ids.move_id.line_ids.debit",
        "payment_ids.move_id.line_ids.credit",
        "payment_ids.move_id.line_ids.currency_id",
        "payment_ids.move_id.line_ids.amount_currency",
        "move_ids.amount_residual",
    )
    def _compute_amount(self):
        for record in self:
            if record.reservation_type == "out":
                record.amount_total = 0
                vals = {
                    "payment_state": "nothing_to_pay",
                    "pending_amount": 0,
                    "invoices_paid": 0,
                }
                record.update(vals)
            else:
                # first attempt compute amount search payments refs with only one folio
                mls_one_folio = (
                    record.payment_ids.filtered(lambda pay: len(pay.folio_ids) == 1)
                    .mapped("move_id.line_ids")
                    .filtered(
                        lambda x: x.account_id.internal_type == "receivable"
                        and x.parent_state == "posted"
                    )
                )
                advance_amount = record._get_advance_amount(mls_one_folio)
                # Compute 'payment_state'.
                vals = record._get_amount_vals(mls_one_folio, advance_amount)
                # If folio its not paid, search payments refs with more than one folio
                folio_ids = record.payment_ids.mapped("folio_ids.id")
                if vals["pending_amount"] > 0 and len(folio_ids) > 1:
                    folios = self.env["pms.folio"].browse(folio_ids)
                    mls_multi_folio = folios.payment_ids.mapped(
                        "move_id.line_ids"
                    ).filtered(
                        lambda x: x.account_id.internal_type == "receivable"
                        and x.parent_state == "posted"
                    )
                    if mls_multi_folio:
                        advance_amount = record._get_advance_amount(mls_multi_folio)
                        vals = record._get_amount_vals(
                            mls_multi_folio, advance_amount, folio_ids
                        )

                record.update(vals)

    def _get_advance_amount(self, mls):
        self.ensure_one()
        advance_amount = 0.0
        for line in mls:
            line_currency = line.currency_id or line.company_id.currency_id
            line_amount = line.amount_currency if line.currency_id else line.balance
            line_amount *= -1
            if line_currency != self.currency_id:
                advance_amount += line.currency_id._convert(
                    line_amount,
                    self.currency_id,
                    self.company_id,
                    line.date or fields.Date.today(),
                )
            else:
                advance_amount += line_amount
        return advance_amount

    def _get_amount_vals(self, mls, advance_amount, folio_ids=False):
        self.ensure_one()
        folios = self
        if folio_ids:
            folios = self.env["pms.folio"].browse(folio_ids)
            mls_one_folio = (
                self.payment_ids.filtered(lambda pay: len(pay.folio_ids) == 1)
                .mapped("move_id.line_ids")
                .filtered(
                    lambda x: x.account_id.internal_type == "receivable"
                    and x.parent_state == "posted"
                )
            )
            amount_folio_residual = self.amount_total - self._get_advance_amount(
                mls_one_folio
            )
            amount_total_residual = sum(folios.mapped("amount_total")) - advance_amount
        else:
            amount_folio_residual = amount_total_residual = (
                sum(folios.mapped("amount_total")) - advance_amount
            )
        total = sum(folios.mapped("amount_total"))

        # REVIEW: Must We ignored services in cancelled folios
        # pending amount?
        # for folio in folios:
        #     if folio.state == "cancel":
        #         total = total - sum(folio.service_ids.mapped("price_total"))
        payment_state = "not_paid"
        if (
            mls
            and float_compare(
                amount_total_residual,
                total,
                precision_rounding=self.currency_id.rounding,
            )
            != 0
        ):
            has_due_amount = float_compare(
                amount_total_residual,
                0.0,
                precision_rounding=self.currency_id.rounding,
            )
            if has_due_amount == 0:
                payment_state = "paid"
            elif has_due_amount > 0:
                payment_state = "partial"
            elif has_due_amount < 0:
                payment_state = "overpayment"
        elif total == 0:
            payment_state = "nothing_to_pay"

        vals = {
            "payment_multi": len(folios) > 1,
            "pending_amount": min(amount_total_residual, amount_folio_residual),
            "invoices_paid": advance_amount,
            "payment_state": payment_state,
        }
        return vals

    @api.depends("reservation_ids", "reservation_ids.priority")
    def _compute_max_reservation_priority(self):
        for record in self.filtered("reservation_ids"):
            reservation_priors = record.reservation_ids.mapped("priority")
            record.max_reservation_priority = max(reservation_priors)

    def _compute_checkin_partner_count(self):
        for record in self:
            if (
                record.reservation_type in ("normal", "staff")
                and record.reservation_ids
            ):
                filtered_reservs = record.reservation_ids.filtered(
                    lambda x: x.state != "cancel"
                )
                mapped_checkin_partner = filtered_reservs.mapped(
                    "checkin_partner_ids.id"
                )
                record.checkin_partner_count = len(mapped_checkin_partner)
                mapped_checkin_partner_count = filtered_reservs.mapped(
                    lambda x: (x.adults + x.children) - len(x.checkin_partner_ids)
                )
                record.checkin_partner_pending_count = sum(mapped_checkin_partner_count)

    @api.depends("email", "mobile", "partner_name")
    def _compute_possible_existing_customer_ids(self):
        for record in self:
            record.possible_existing_customer_ids = False
            if record.partner_name:
                possible_customer = self._apply_possible_existing_customer_ids(
                    record.email, record.mobile, record.partner_id
                )
                if possible_customer:
                    record.possible_existing_customer_ids = possible_customer

    @api.depends("reservation_ids", "reservation_ids.checkin")
    def _compute_first_checkin(self):
        for record in self:
            if record.reservation_ids:
                checkins = record.reservation_ids.mapped("checkin")
                record.first_checkin = min(checkins)

    def _compute_days_to_checkin(self):
        for record in self:
            record.days_to_checkin = (record.first_checkin - fields.Date.today()).days

    def _search_days_to_checkin(self, operator, value):
        target_date = fields.Date.today() + datetime.timedelta(days=value)
        if operator in ("=", ">=", ">", "<=", "<"):
            return [("first_checkin", operator, target_date)]
        raise UserError(
            _("Unsupported operator %s for searching on date") % (operator,)
        )

    @api.depends("reservation_ids", "reservation_ids.checkout")
    def _compute_last_checkout(self):
        for record in self:
            if record.reservation_ids:
                checkouts = record.reservation_ids.mapped("checkout")
                record.last_checkout = max(checkouts)

    def _compute_days_to_checkout(self):
        for record in self:
            record.days_to_checkout = (record.last_checkout - fields.Date.today()).days

    def _search_days_to_checkout(self, operator, value):
        target_date = fields.Date.today() + datetime.timedelta(days=value)
        if operator in ("=", ">=", ">", "<=", "<"):
            return [("last_checkout", operator, target_date)]
        raise UserError(
            _("Unsupported operator %s for searching on date") % (operator,)
        )

    @api.depends("agency_id")
    def _compute_invoice_to_agengy(self):
        for record in self:
            if not record.agency_id or record.agency_id.invoice_to_agency == "never":
                record.invoice_to_agency = False
            elif record.agency_id.invoice_to_agency == "always":
                record.invoice_to_agency = True
            elif not record.invoice_to_agency:
                record.invoice_to_agency = False

    @api.depends("partner_id")
    def _compute_lang(self):
        for record in self.filtered("partner_id"):
            record.lang = record.partner_id.lang

    def _search_invoice_ids(self, operator, value):
        if operator == "in" and value:
            self.env.cr.execute(
                """
                SELECT array_agg(fo.id)
                    FROM pms_folio fo
                    JOIN folio_sale_line fol ON fol.folio_id = fo.id
                    JOIN folio_sale_line_invoice_rel foli_rel ON foli_rel.sale_line_id = fol.id
                    JOIN account_move_line aml ON aml.id = foli_rel.invoice_line_id
                    JOIN account_move am ON am.id = aml.move_id
                WHERE
                    am.move_type in ('out_invoice', 'out_refund', 'in_receipt') AND
                    am.id = ANY(%s)
            """,
                (list(value),),
            )
            so_ids = self.env.cr.fetchone()[0] or []
            return [("id", "in", so_ids)]
        return [
            "&",
            (
                "sale_line_ids.invoice_lines.move_id.move_type",
                "in",
                ("out_invoice", "out_refund", "in_receipt"),
            ),
            ("sale_line_ids.invoice_lines.move_id", operator, value),
        ]

    @api.constrains("name")
    def _check_required_partner_name(self):
        for record in self:
            if not record.partner_name and record.reservation_type != "out":
                raise models.ValidationError(_("You must assign a customer name"))

    @api.model
    def create(self, vals):
        if vals.get("name", _("New")) == _("New") or "name" not in vals:
            pms_property_id = (
                self.env.user.get_active_property_ids()[0]
                if "pms_property_id" not in vals
                else vals["pms_property_id"]
            )
            pms_property = self.env["pms.property"].browse(pms_property_id)
            vals["name"] = pms_property.folio_sequence_id._next_do()
        result = super(PmsFolio, self).create(vals)
        result.access_token = result._portal_ensure_token()
        return result

    def write(self, vals):
        reservations_to_update = self.env["pms.reservation"]
        services_to_update = self.env["pms.service"]
        if "sale_channel_origin_id" in vals:
            reservations_to_update = self.get_reservations_to_update_channel(vals)
            services_to_update = self.get_services_to_update_channel(vals)

        res = super(PmsFolio, self).write(vals)
        if reservations_to_update:
            reservations_to_update.sale_channel_origin_id = vals[
                "sale_channel_origin_id"
            ]

        if services_to_update:
            services_to_update.sale_channel_origin_id = vals["sale_channel_origin_id"]

        return res

    @api.model
    def _get_languages(self):
        return self.env["res.lang"].get_installed()

    def get_reservations_to_update_channel(self, vals):
        reservations_to_update = self.env["pms.reservation"]
        for record in self:
            for reservation in record.reservation_ids:
                if (
                    reservation.sale_channel_origin_id == self.sale_channel_origin_id
                ) and (
                    vals["sale_channel_origin_id"]
                    != reservation.sale_channel_origin_id.id
                ):
                    reservations_to_update += reservation
        return reservations_to_update

    def get_services_to_update_channel(self, vals):
        services_to_update = self.env["pms.service"]
        for record in self:
            for service in record.service_ids:
                if (
                    not service.reservation_id
                    and (service.sale_channel_origin_id == self.sale_channel_origin_id)
                    and (
                        vals["sale_channel_origin_id"]
                        != service.sale_channel_origin_id.id
                    )
                ):
                    services_to_update += service
        return services_to_update

    def action_pay(self):
        self.ensure_one()
        self.ensure_one()
        partner = self.partner_id.id
        amount = self.pending_amount
        view_id = self.env.ref("pms.wizard_payment_folio_view_form").id
        return {
            "name": _("Register Payment"),
            "view_type": "form",
            "view_mode": "form",
            "res_model": "wizard.payment.folio",
            "type": "ir.actions.act_window",
            "view_id": view_id,
            "context": {
                "default_folio_id": self.id,
                "default_amount": amount,
                "default_partner_id": partner,
            },
            "target": "new",
        }

    def open_partner(self):
        """Utility method used to add an "View Customer" button in folio views"""
        self.ensure_one()
        partner_form_id = self.env.ref("pms.view_partner_data_form").id
        return {
            "type": "ir.actions.act_window",
            "res_model": "res.partner",
            "view_mode": "form",
            "views": [(partner_form_id, "form")],
            "res_id": self.partner_id.id,
            "target": "new",
            "flags": {"form": {"action_buttons": True}},
        }

    def open_moves_folio(self):
        invoices = self.mapped("move_ids")
        action = self.env.ref("account.action_move_out_invoice_type").sudo().read()[0]
        if len(invoices) > 1:
            action["domain"] = [("id", "in", invoices.ids)]
        elif len(invoices) == 1:
            action["views"] = [(self.env.ref("account.view_move_form").id, "form")]
            action["res_id"] = invoices.ids[0]
        else:
            action = {"type": "ir.actions.act_window_close"}
        return action

    def folio_multi_changes(self):
        self.ensure_one()
        reservation_ids = self.reservation_ids.ids
        action = self.env.ref("pms.action_folio_changes").sudo().read()[0]
        action["context"] = ({"default_reservation_ids": [(6, 0, reservation_ids)]},)
        return action

    def action_checks(self):
        self.ensure_one()
        rooms = self.mapped("reservation_ids.id")
        return {
            "name": _("Checkins"),
            "view_type": "form",
            "view_mode": "tree,form",
            "res_model": "pms.checkin.partner",
            "type": "ir.actions.act_window",
            "domain": [("reservation_id", "in", rooms)],
            "search_view_id": [
                self.env.ref("pms.pms_checkin_partner_view_folio_search").id,
                "search",
            ],
            "target": "new",
        }

    def action_to_arrive(self):
        self.ensure_one()
        reservations = self.reservation_ids.filtered(
            lambda c: c.state in ("draf", "confirm", "arrival_delayed")
        )
        action = self.env.ref("pms.open_pms_reservation_form_tree_all").read()[0]
        action["domain"] = [("id", "in", reservations.ids)]
        return action

    def action_done(self):
        reservation_ids = self.mapped("reservation_ids")
        for line in reservation_ids:
            if line.state == "onboard":
                line.action_reservation_checkout()

    def action_cancel(self):
        for folio in self:
            for reservation in folio.reservation_ids.filtered(
                lambda res: res.state != "cancel"
            ):
                reservation.action_cancel()
            self.write(
                {
                    "state": "cancel",
                }
            )
        return True

    def action_confirm(self):
        self.filtered(lambda x: x.state != "confirm").write(
            {"state": "confirm", "confirmation_date": fields.Datetime.now()}
        )

        if self.env.context.get("confirm_all_reservations"):
            self.reservation_ids.action_confirm()

        return True

    # MAIL FLOWS

    def action_open_confirmation_mail_composer(self):
        self.ensure_one()
        res_id = False
        res_ids = []
        partner_ids = []
        if self.pms_property_id.property_confirmed_template:
            template = self.pms_property_id.property_confirmed_template
        else:
            raise ValidationError(
                _(
                    "You must select a confirmation template "
                    "in the email configuration menu of the property"
                )
            )
        model = "pms.folio"
        partner_ids = [self.partner_id.id]
        res_id = self.id
        composition_mode = "comment"
        ctx = dict(
            model=model,
            default_model=model,
            default_template_id=template and template.id or False,
            default_composition_mode=composition_mode,
            partner_ids=partner_ids,
            force_email=True,
        )
        return self.action_open_mail_composer(ctx, res_id=res_id, res_ids=res_ids)

    def action_open_modification_mail_composer(self):
        self.ensure_one()
        res_id = False
        res_ids = []
        partner_ids = []
        if self.pms_property_id.property_modified_template:
            template = self.pms_property_id.property_modified_template
        else:
            raise ValidationError(
                _(
                    "You must select a modification template "
                    "in the email configuration menu of the property"
                )
            )
        model = "pms.folio"
        partner_ids = [self.partner_id.id]
        res_id = self.id
        composition_mode = "comment"

        ctx = dict(
            model=model,
            default_model=model,
            default_template_id=template and template.id or False,
            default_composition_mode=composition_mode,
            partner_ids=partner_ids,
            force_email=True,
        )
        return self.action_open_mail_composer(ctx, res_id=res_id, res_ids=res_ids)

    def action_open_exit_mail_composer(self):
        self.ensure_one()
        res_id = False
        res_ids = []
        partner_ids = []

        if self.pms_property_id.property_exit_template:
            template = self.pms_property_id.property_exit_template
        else:
            raise ValidationError(
                _(
                    "You must select a exit template in "
                    "the email configuration menu of the property"
                )
            )
        model = "pms.checkin.partner"
        composition_mode = "mass_mail"
        for checkin_partner in self.checkin_partner_ids:
            if checkin_partner.state == "done":
                partner_ids.append(checkin_partner.partner_id.id)
                res_ids.append(checkin_partner.id)
        ctx = dict(
            model=model,
            default_model=model,
            default_template_id=template and template.id or False,
            default_composition_mode=composition_mode,
            partner_ids=partner_ids,
            force_email=True,
        )
        return self.action_open_mail_composer(ctx, res_id=res_id, res_ids=res_ids)

    def action_open_cancelation_mail_composer(self):
        self.ensure_one()
        res_id = False
        res_ids = []
        partner_ids = []
        if self.pms_property_id.property_canceled_template:
            template = self.pms_property_id.property_canceled_template
        else:
            raise ValidationError(
                _(
                    "You must select a cancelation template "
                    "in the email configuration menu of the property"
                )
            )
        model = "pms.reservation"
        composition_mode = "mass_mail"
        for reservation in self.reservation_ids:
            if reservation.state == "cancel":
                partner_ids.append(reservation.partner_id.id)
                res_ids.append(reservation.id)
        ctx = dict(
            model=model,
            default_model=model,
            default_template_id=template and template.id or False,
            default_composition_mode=composition_mode,
            partner_ids=partner_ids,
            force_email=True,
        )
        return self.action_open_mail_composer(ctx, res_id=res_id, res_ids=res_ids)

    def action_open_mail_composer(self, ctx, res_id=False, res_ids=False):
        compose_form = self.env.ref(
            "mail.email_compose_message_wizard_form", raise_if_not_found=False
        )
        composition_mode = ctx.get("default_composition_mode")
        if composition_mode == "comment":
            ctx.update(
                default_res_id=res_id,
                record_id=res_id,
            )
        else:
            ctx.update(
                active_ids=res_ids,
            )
        return {
            "name": _("Send Mail "),
            "type": "ir.actions.act_window",
            "view_type": "form",
            "view_mode": "form",
            "res_model": "mail.compose.message",
            "views": [(compose_form.id, "form")],
            "view_id": compose_form.id,
            "target": "new",
            "context": ctx,
        }

    def _message_post_after_hook(self, message, msg_vals):
        res = super(PmsFolio, self)._message_post_after_hook(message, msg_vals)
        for folio in self:
            for follower in folio.message_follower_ids:
                follower.sudo().unlink()
        return res

    def action_view_invoice(self):
        invoices = self.mapped("move_ids")
        action = self.env["ir.actions.actions"]._for_xml_id(
            "account.action_move_out_invoice_type"
        )
        if len(invoices) > 1:
            action["domain"] = [("id", "in", invoices.ids)]
        elif len(invoices) == 1:
            form_view = [(self.env.ref("account.view_move_form").id, "form")]
            if "views" in action:
                action["views"] = form_view + [
                    (state, view) for state, view in action["views"] if view != "form"
                ]
            else:
                action["views"] = form_view
            action["res_id"] = invoices.id
        else:
            action = {"type": "ir.actions.act_window_close"}

        context = {
            "default_move_type": "out_invoice",
        }
        if len(self) == 1:
            context.update(
                {
                    "default_partner_id": self.partner_id.id,
                    "default_invoice_payment_term_id": self.payment_term_id.id
                    or self.partner_id.property_payment_term_id.id
                    or self.env["account.move"]
                    .default_get(["invoice_payment_term_id"])
                    .get("invoice_payment_term_id"),
                    "default_invoice_origin": self.mapped("name"),
                    "default_user_id": self.user_id.id,
                }
            )
        action["context"] = context
        return action

    def preview_folio(self):
        self.ensure_one()
        return {
            "type": "ir.actions.act_url",
            "target": "self",
            "url": self.get_portal_url(),
        }

    # flake8:noqa=C901
    def _create_invoices(
        self,
        grouped=False,
        final=False,
        date=None,
        lines_to_invoice=False,
        partner_invoice_id=False,
    ):
        """
        Create the invoice associated to the Folio.
        :param grouped: if True, invoices are grouped by Folio id.
        If False, invoices are grouped by
                        (partner_invoice_ids, currency)
        :param final: if True, refunds will be generated if necessary
        :param lines_to_invoice: invoice specific lines dict(key=id, value=qty).
            if False, invoice all
        :returns: list of created invoices
        """
        if not self.env["account.move"].check_access_rights("create", False):
            try:
                self.check_access_rights("write")
                self.check_access_rule("write")
            except AccessError:
                return self.env["account.move"]
        # 1) Create invoices.
        if not lines_to_invoice:
            self = self.with_context(lines_auto_add=True)
            lines_to_invoice = dict()
            for line in self.sale_line_ids.filtered(
                lambda l: l.qty_to_invoice > 0
                or (l.qty_to_invoice < 0 and final)
                or l.display_type == "line_note"
            ):
                if not self._context.get("autoinvoice"):
                    lines_to_invoice[line.id] = (
                        0 if line.display_type else line.qty_to_invoice
                    )
                elif (
                    line.autoinvoice_date
                    and line.autoinvoice_date <= fields.Date.today()
                ):
                    lines_to_invoice[line.id] = (
                        0 if line.display_type else line.qty_to_invoice
                    )
        invoice_vals_list = self.get_invoice_vals_list(
            final=final,
            lines_to_invoice=lines_to_invoice,
            partner_invoice_id=partner_invoice_id,
        )
        if not invoice_vals_list:
            raise self._nothing_to_invoice_error()

        # 2) Manage 'grouped' parameter: group by (partner_id, currency_id).
        if not grouped:
            invoice_vals_list = self._get_group_vals_list(invoice_vals_list)

        partner_invoice = self.env["res.partner"].browse(partner_invoice_id)
        partner_invoice_policy = self.pms_property_id.default_invoicing_policy
        if partner_invoice and partner_invoice.invoicing_policy != "property":
            partner_invoice_policy = partner_invoice.invoicing_policy
        invoice_date = False
        if date:
            invoice_date = date
        if partner_invoice_policy == "checkout":
            margin_days_autoinvoice = (
                self.pms_property_id.margin_days_autoinvoice
                if partner_invoice.margin_days_autoinvoice == 0
                else partner_invoice.margin_days_autoinvoice
            )
            invoice_date = max(
                self.env["pms.reservation"]
                .search([("sale_line_ids", "in", lines_to_invoice.keys())])
                .mapped("checkout")
            ) + datetime.timedelta(days=margin_days_autoinvoice)
        if partner_invoice_policy == "month_day":
            month_day = (
                self.pms_property_id.invoicing_month_day
                if partner_invoice.invoicing_month_day == 0
                else partner_invoice.invoicing_month_day
            )
            invoice_date = datetime.date(
                datetime.date.today().year,
                datetime.date.today().month,
                month_day,
            )
            if invoice_date < datetime.date.today():
                invoice_date = datetime.date(
                    datetime.date.today().year,
                    datetime.date.today().month + 1,
                    month_day,
                )
        if invoice_date:
            if (
                self.company_id.period_lock_date
                and invoice_date < self.company_id.period_lock_date
                and not self.user_has_groups("account.group_account_manager")
            ):
                raise UserError(
                    _(
                        "The period to create this invoice is locked. "
                        "Please contact your administrator to unlock it."
                    )
                )
            if invoice_date < datetime.date.today() and not self._context.get(
                "autoinvoice"
            ):
                invoice_date = datetime.date.today()
            key_field = (
                "invoice_date"
                if invoice_date <= fields.Date.today()
                else "invoice_date_due"
            )
            for vals in invoice_vals_list:
                vals["date"] = invoice_date
                vals[key_field] = invoice_date

        # 3) Create invoices.

        # As part of the invoice creation, we make sure the
        # sequence of multiple SO do not interfere
        # in a single invoice. Example:
        # Folio 1:
        # - Section A (sequence: 10)
        # - Product A (sequence: 11)
        # Folio 2:
        # - Section B (sequence: 10)
        # - Product B (sequence: 11)
        #
        # If Folio 1 & 2 are grouped in the same invoice,
        # the result will be:
        # - Section A (sequence: 10)
        # - Section B (sequence: 10)
        # - Product A (sequence: 11)
        # - Product B (sequence: 11)
        #
        # Resequencing should be safe, however we resequence only
        # if there are less invoices than orders, meaning a grouping
        # might have been done. This could also mean that only a part
        # of the selected SO are invoiceable, but resequencing
        # in this case shouldn't be an issue.
        if len(invoice_vals_list) < len(self):
            FolioSaleLine = self.env["folio.sale.line"]
            for invoice in invoice_vals_list:
                sequence = 1
                for line in invoice["invoice_line_ids"]:
                    line[2]["sequence"] = FolioSaleLine._get_invoice_line_sequence(
                        new=sequence, old=line[2]["sequence"]
                    )
                    sequence += 1

        # Manage the creation of invoices in sudo because
        # a salesperson must be able to generate an invoice from a
        # sale order without "billing" access rights.
        # However, he should not be able to create an invoice from scratch.
        moves = self._create_account_moves(invoice_vals_list)

        # 4) Some moves might actually be refunds: convert
        # them if the total amount is negative
        # We do this after the moves have been created
        # since we need taxes, etc. to know if the total
        # is actually negative or not
        if final:
            moves.sudo().filtered(
                lambda m: m.amount_total < 0
            ).action_switch_invoice_into_refund_credit_note()
        for move in moves:
            move.sudo().message_post_with_view(
                "mail.message_origin_link",
                values={
                    "self": move,
                    "origin": move.line_ids.mapped("folio_line_ids.folio_id"),
                },
                subtype_id=self.env.ref("mail.mt_note").id,
            )
        return moves

    def _create_account_moves(self, invoice_vals_list):
        moves = self.env["account.move"]
        for invoice_vals in invoice_vals_list:
            if invoice_vals["move_type"] == "out_invoice":
                move = (
                    self.env["account.move"]
                    .sudo()
                    .with_context(default_move_type="out_invoice")
                    .create(invoice_vals)
                )
            moves += move
        return moves

    def _get_group_vals_list(self, invoice_vals_list):
        new_invoice_vals_list = []
        invoice_grouping_keys = self._get_invoice_grouping_keys()
        for _grouping_keys, invoices in groupby(
            invoice_vals_list,
            key=lambda x: [
                x.get(grouping_key) for grouping_key in invoice_grouping_keys
            ],
        ):
            origins = set()
            payment_refs = set()
            refs = set()
            ref_invoice_vals = None
            for invoice_vals in invoices:
                if not ref_invoice_vals:
                    ref_invoice_vals = invoice_vals
                else:
                    ref_invoice_vals["invoice_line_ids"] += invoice_vals[
                        "invoice_line_ids"
                    ]
                origins.add(invoice_vals["invoice_origin"])
                payment_refs.add(invoice_vals["payment_reference"])
                refs.add(invoice_vals["ref"])
            ref_invoice_vals.update(
                {
                    "ref": ", ".join(refs)[:2000],
                    "invoice_origin": ", ".join(origins),
                    "payment_reference": len(payment_refs) == 1
                    and payment_refs.pop()
                    or False,
                }
            )
            new_invoice_vals_list.append(ref_invoice_vals)
        return new_invoice_vals_list

    def _prepare_invoice(self, partner_invoice_id=False):
        """
        Prepare the dict of values to create the new invoice for a folio.
        This method may be overridden to implement custom invoice generation
        (making sure to call super() to establish a clean extension chain).
        """
        self.ensure_one()
        journal = self.pms_property_id._get_folio_default_journal(partner_invoice_id)
        if not journal:
            journal = (
                self.env["account.move"]
                .with_context(
                    default_move_type="out_invoice",
                    default_company_id=self.company_id.id,
                    default_pms_property_id=self.pms_property_id.id,
                )
                ._get_default_journal()
            )
        if not journal:
            raise UserError(
                _("Please define an accounting sales journal for the company %s (%s).")
                % (self.company_id.name, self.company_id.id)
            )
        ref = ""
        if self.name:
            ref = self.name
        if self.external_reference:
            ref += " - " + self.external_reference
        invoice_vals = {
            "name": "/",
            "ref": ref,
            "move_type": "out_invoice",
            "narration": self.note,
            "currency_id": self.pricelist_id.currency_id.id,
            # 'campaign_id': self.campaign_id.id,
            # 'medium_id': self.medium_id.id,
            # 'source_id': self.source_id.id,
            "invoice_user_id": self.user_id and self.user_id.id,
            "partner_id": partner_invoice_id,
            "partner_bank_id": self.company_id.partner_id.bank_ids[:1].id,
            "journal_id": journal.id,  # company comes from the journal
            "invoice_origin": self.name,
            "invoice_payment_term_id": self.payment_term_id.id,
            "transaction_ids": [(6, 0, self.transaction_ids.ids)],
            "invoice_line_ids": [],
            "company_id": self.company_id.id,
            "payment_reference": self.name,
            "fiscal_position_id": self.env["account.fiscal.position"]
            .with_company(self.company_id.id)
            .get_fiscal_position(partner_invoice_id),
        }
        return invoice_vals

    def do_payment(
        self,
        journal,
        receivable_account,
        user,
        amount,
        folio,
        reservations=False,
        services=False,
        partner=False,
        date=False,
        pay_type=False,
        ref=False,
    ):
        """
        create folio payment
        type: set cash to use statement or bank to use account.payment,
        by default, use the journal type
        """
        if not pay_type:
            pay_type = journal.type

        reference = folio.name
        if folio.external_reference:
            reference += " - " + folio.external_reference
        if ref and not ref in reference:
            reference += ": " + ref
        vals = {
            "journal_id": journal.id,
            "partner_id": partner.id if partner else False,
            "amount": amount,
            "date": date or fields.Date.today(),
            "ref": reference,
            "folio_ids": [(6, 0, [folio.id])],
            "payment_type": "inbound",
            "partner_type": "customer",
            "state": "draft",
            "origin_reference": folio.external_reference,
        }
        pay = self.env["account.payment"].create(vals)
        pay.sudo().message_post_with_view(
            "mail.message_origin_link",
            values={
                "self": pay,
                "origin": folio,
            },
            subtype_id=self.env.ref("mail.mt_note").id,
            email_from=user.partner_id.email_formatted
            or folio.pms_property_id.email_formatted,
        )

        pay.action_post()

        # Review: force to autoreconcile payment with invoices already created
        pay.flush()
        for move in folio.move_ids:
            move._autoreconcile_folio_payments()

        # Automatic register payment in cash register
        # TODO: cash_register to avoid flow in the new api (delete it in the future)
        if pay_type == "cash" and self.env.context.get("cash_register"):
            line = self._get_statement_line_vals(
                journal=journal,
                receivable_account=receivable_account,
                user=user,
                amount=amount,
                folios=folio,
                reservations=reservations,
                services=services,
                partner=partner,
                date=date,
            )
            self.env["account.bank.statement.line"].sudo().create(line)
        folio.sudo().message_post(
            body=_(
                """Payment: <b>%s</b> by <b>%s</b>""",
                amount,
                journal.display_name,
            ),
            email_from=user.partner_id.email_formatted
            or folio.pms_property_id.email_formatted,
        )
        for reservation in folio.reservation_ids:
            reservation.sudo().message_post(
                body=_(
                    """Payment: <b>%s</b> by <b>%s</b>""",
                    amount,
                    journal.display_name,
                ),
                email_from=user.partner_id.email_formatted
                or folio.pms_property_id.email_formatted,
            )
        return True

    def do_refund(
        self,
        journal,
        receivable_account,
        user,
        amount,
        folio,
        reservations=False,
        services=False,
        partner=False,
        date=False,
        pay_type=False,
        ref=False,
    ):
        """
        create folio refund
        type: set cash to use statement or bank to use account.payment,
        by default, use the journal type
        """
        if not pay_type:
            pay_type = journal.type
        reference = folio.name
        if folio.external_reference:
            reference += " - " + folio.external_reference
        if ref and not ref in reference:
            reference += ": " + ref
        vals = {
            "journal_id": journal.id,
            "partner_id": partner.id if partner else False,
            "amount": amount if amount > 0 else -amount,
            "date": date or fields.Date.today(),
            "ref": reference,
            "folio_ids": [(6, 0, [folio.id])],
            "payment_type": "outbound",
            "partner_type": "customer",
            "state": "draft",
        }
        pay = self.env["account.payment"].create(vals)
        pay.sudo().message_post_with_view(
            "mail.message_origin_link",
            values={
                "self": pay,
                "origin": folio,
            },
            subtype_id=self.env.ref("mail.mt_note").id,
            email_from=user.partner_id.email_formatted
            or folio.pms_property_id.email_formatted,
        )
        pay.action_post()

        # Automatic register refund in cash register
        # TODO: cash_register to avoid flow in the new api (delete it in the future)
        if pay_type == "cash" and self.env.context.get("cash_register"):
            line = self._get_statement_line_vals(
                journal=journal,
                receivable_account=receivable_account,
                user=user,
                amount=amount if amount < 0 else -amount,
                folios=folio,
                reservations=reservations,
                services=services,
                partner=partner,
                date=date,
            )
            self.env["account.bank.statement.line"].sudo().create(line)

        folio.sudo().message_post(
            body=_(
                """Refund: <b>%s</b> by <b>%s</b>""",
                amount,
                journal.display_name,
            ),
            email_from=user.partner_id.email_formatted
            or folio.pms_property_id.email_formatted,
        )
        for reservation in folio.reservation_ids:
            reservation.sudo().message_post(
                body=_(
                    """Refund: <b>%s</b> by <b>%s</b>""",
                    amount,
                    journal.display_name,
                ),
                email_from=user.partner_id.email_formatted
                or folio.pms_property_id.email_formatted,
            )
        return True

    def open_wizard_several_partners(self):
        ctx = dict(
            folio_id=self.id,
            possible_existing_customer_ids=self.possible_existing_customer_ids.ids,
        )
        return {
            "view_type": "form",
            "view_mode": "form",
            "name": "Several Customers",
            "res_model": "pms.several.partners.wizard",
            "target": "new",
            "type": "ir.actions.act_window",
            "context": ctx,
        }

    @api.model
    def _get_statement_line_vals(
        self,
        journal,
        receivable_account,
        user,
        amount,
        folios,
        reservations=False,
        services=False,
        partner=False,
        date=False,
    ):
        property_folio_id = folios.mapped("pms_property_id.id")
        if len(property_folio_id) != 1:
            raise ValidationError(_("Only can payment by property"))
        ctx = dict(self.env.context, company_id=folios[0].company_id.id)
        if not date:
            date = fields.Date.today()
        domain = [
            ("journal_id", "=", journal.id),
            ("pms_property_id", "=", property_folio_id[0]),
            ("state", "=", "open"),
            ("date", "=", date),
        ]
        statement = self.env["account.bank.statement"].sudo().search(domain, limit=1)
        reservation_ids = reservations.ids if reservations else []
        service_ids = services.ids if services else []
        if not statement:
            # TODO: cash control option
            st_values = {
                "journal_id": journal.id,
                "user_id": self.env.user.id,
                "pms_property_id": property_folio_id[0],
                "name": datetime.datetime.today().strftime(
                    get_lang(self.env).date_format
                ),
            }
            statement = (
                self.env["account.bank.statement"]
                .with_context(ctx)
                .sudo()
                .create(st_values)
            )
        return {
            "date": date,
            "amount": amount,
            "partner_id": partner.id if partner else False,
            "folio_ids": [(6, 0, folios.ids)],
            "reservation_ids": [(6, 0, reservation_ids)],
            "service_ids": [(6, 0, service_ids)],
            "payment_ref": ", ".join(folios.mapped("name")),
            "statement_id": statement.id,
            "journal_id": statement.journal_id.id,
            "counterpart_account_id": receivable_account.id,
        }

    @api.model
    def _get_reservation_sale_lines(self, folio, reservation, sequence):
        sale_reservation_vals = []
        if not reservation.sale_line_ids.filtered(lambda x: x.name == reservation.name):
            sale_reservation_vals.append(
                (
                    0,
                    0,
                    {
                        "name": reservation.name,
                        "display_type": "line_section",
                        "product_id": False,
                        "product_uom_qty": 0,
                        "discount": 0,
                        "price_unit": 0,
                        "tax_ids": False,
                        "folio_id": folio.id,
                        "reservation_id": reservation.id,
                        "sequence": sequence,
                    },
                )
            )
        else:
            sequence += 1
            sale_reservation_vals.append(
                (
                    1,
                    reservation.sale_line_ids.filtered(
                        lambda x: x.name == reservation.name
                    ).id,
                    {
                        "sequence": sequence,
                    },
                )
            )
        expected_reservation_lines = self.env["pms.reservation.line"].read_group(
            [
                ("reservation_id", "=", reservation.id),
                ("cancel_discount", "<", 100),
            ],
            ["price", "discount", "cancel_discount", "default_invoice_to"],
            ["price", "discount", "cancel_discount", "default_invoice_to"],
            lazy=False,
        )
        current_sale_line_ids = reservation.sale_line_ids.filtered(
            lambda x: x.reservation_id.id == reservation.id
            and not x.display_type
            and not x.service_id
        )

        for index, item in enumerate(expected_reservation_lines):
            sequence += 1
            lines_to = self.env["pms.reservation.line"].search(item["__domain"])
            final_discount = self.concat_discounts(
                item["discount"], item["cancel_discount"]
            )
            partner_invoice = lines_to.mapped("default_invoice_to")
            if current_sale_line_ids and index <= (len(current_sale_line_ids) - 1):
                current = {
                    "price_unit": item["price"],
                    "discount": final_discount,
                    "reservation_line_ids": [(6, 0, lines_to.ids)],
                    "sequence": sequence,
                    "default_invoice_to": partner_invoice[0].id
                    if partner_invoice
                    else current_sale_line_ids[index].default_invoice_to,
                }
                sale_reservation_vals.append(
                    (1, current_sale_line_ids[index].id, current)
                )
            else:
                new = {
                    "reservation_id": reservation.id,
                    "price_unit": item["price"],
                    "discount": final_discount,
                    "folio_id": folio.id,
                    "product_id": reservation.room_type_id.product_id.id,
                    "tax_ids": [(6, 0, reservation.tax_ids.ids)],
                    "reservation_line_ids": [(6, 0, lines_to.ids)],
                    "sequence": sequence,
                    "default_invoice_to": partner_invoice[0].id
                    if partner_invoice
                    else False,
                }
                sale_reservation_vals.append((0, 0, new))
        folio_sale_lines_to_remove = []
        if len(expected_reservation_lines) < len(current_sale_line_ids):
            folio_sale_lines_to_remove = [
                value.id
                for index, value in enumerate(current_sale_line_ids)
                if index > (len(expected_reservation_lines) - 1)
            ]
        return sale_reservation_vals, folio_sale_lines_to_remove

    @api.model
    def _get_service_sale_lines(self, folio, reservation, sequence):
        sale_service_vals = []
        folio_sale_lines_to_remove = []
        for service in reservation.service_ids:
            expected_reservation_services = self.env["pms.service.line"].read_group(
                [
                    ("reservation_id", "=", reservation.id),
                    ("service_id", "=", service.id),
                    ("cancel_discount", "<", 100),
                ],
                ["price_unit", "discount", "cancel_discount", "default_invoice_to"],
                ["price_unit", "discount", "cancel_discount", "default_invoice_to"],
                lazy=False,
            )
            current_sale_service_ids = reservation.sale_line_ids.filtered(
                lambda x: x.reservation_id.id == reservation.id
                and not x.display_type
                and x.service_id.id == service.id
            )

            for index, item in enumerate(expected_reservation_services):
                lines_to = self.env["pms.service.line"].search(item["__domain"])
                final_discount = self.concat_discounts(
                    item["discount"], item["cancel_discount"]
                )
                partner_invoice = lines_to.mapped("default_invoice_to")
                if current_sale_service_ids and index <= (
                    len(current_sale_service_ids) - 1
                ):
                    current = {
                        "price_unit": item["price_unit"],
                        "discount": final_discount,
                        "service_line_ids": [(6, 0, lines_to.ids)],
                        "sequence": sequence,
                        "default_invoice_to": partner_invoice[0].id
                        if partner_invoice
                        else current_sale_service_ids[index].default_invoice_to,
                    }
                    sale_service_vals.append(
                        (1, current_sale_service_ids[index].id, current)
                    )
                else:
                    new = {
                        "service_id": service.id,
                        "price_unit": item["price_unit"],
                        "discount": final_discount,
                        "folio_id": folio.id,
                        "reservation_id": reservation.id,
                        "service_line_ids": [(6, 0, lines_to.ids)],
                        "product_id": service.product_id.id,
                        "tax_ids": [(6, 0, service.tax_ids.ids)],
                        "sequence": sequence,
                        "default_invoice_to": partner_invoice[0].id
                        if partner_invoice
                        else False,
                    }
                    sale_service_vals.append((0, 0, new))
                sequence = sequence + 1
            if len(expected_reservation_services) < len(current_sale_service_ids):
                folio_sale_lines_to_remove = [
                    value.id
                    for index, value in enumerate(current_sale_service_ids)
                    if index > (len(expected_reservation_services) - 1)
                ]
        return sale_service_vals, folio_sale_lines_to_remove

    @api.model
    def _get_folio_services_sale_lines(self, folio, sequence):
        folio_services = folio.service_ids.filtered(lambda x: not x.reservation_id)
        sale_folio_lines = []
        sale_folio_lines_to_remove = []
        if folio_services:
            if not folio.sale_line_ids.filtered(lambda x: x.name == _("Others")):
                folio.sale_line_ids = [
                    (
                        0,
                        False,
                        {
                            "display_type": "line_section",
                            "product_id": False,
                            "product_uom_qty": 0,
                            "discount": 0,
                            "price_unit": 0,
                            "tax_ids": False,
                            "name": _("Others"),
                            "sequence": sequence,
                        },
                    )
                ]
            for folio_service in folio_services:
                sequence += 1
                expected_folio_services = self.env["pms.service.line"].read_group(
                    [
                        ("service_id.folio_id", "=", folio.id),
                        ("service_id", "=", folio_service.id),
                        ("reservation_id", "=", False),
                        ("cancel_discount", "<", 100),
                    ],
                    ["price_unit", "discount", "cancel_discount"],
                    ["price_unit", "discount", "cancel_discount"],
                    lazy=False,
                )
                current_folio_service_ids = folio.sale_line_ids.filtered(
                    lambda x: x.service_id.folio_id.id == folio.id
                    and not x.display_type
                    and not x.reservation_id
                    and x.service_id.id == folio_service.id
                )

                for index, item in enumerate(expected_folio_services):
                    lines_to = self.env["pms.service.line"].search(item["__domain"])
                    final_discount = self.concat_discounts(
                        item["discount"], item["cancel_discount"]
                    )
                    if current_folio_service_ids and index <= (
                        len(current_folio_service_ids) - 1
                    ):
                        current = {
                            "price_unit": item["price_unit"],
                            "discount": final_discount,
                            "service_line_ids": [(6, 0, lines_to.ids)],
                            "sequence": sequence,
                        }
                        sale_folio_lines.append(
                            (1, current_folio_service_ids[index].id, current)
                        )
                    else:
                        new = {
                            "service_id": folio_service.id,
                            "price_unit": item["price_unit"],
                            "discount": final_discount,
                            "folio_id": folio.id,
                            "service_line_ids": [(6, 0, lines_to.ids)],
                            "product_id": folio_service.product_id.id,
                            "tax_ids": [(6, 0, folio_service.tax_ids.ids)],
                            "sequence": sequence,
                        }
                        sale_folio_lines.append((0, 0, new))
                if len(expected_folio_services) < len(current_folio_service_ids):
                    sale_folio_lines_to_remove = [
                        value.id
                        for index, value in enumerate(current_folio_service_ids)
                        if index > (len(expected_folio_services) - 1)
                    ]
        else:
            sale_folio_lines_to_remove = folio.sale_line_ids.filtered(
                lambda x: x.name == _("Others")
            )
        return sale_folio_lines, sale_folio_lines_to_remove

    @api.model
    def _prepare_down_payment_section_line(self, **optional_values):
        """
        Prepare the dict of values to create a new down
        payment section for a sales order line.
        :param optional_values: any parameter that should
        be added to the returned down payment section
        """
        down_payments_section_line = {
            "display_type": "line_section",
            "name": _("Down Payments"),
            "product_id": False,
            "product_uom_id": False,
            "quantity": 0,
            "discount": 0,
            "price_unit": 0,
            "account_id": False,
        }
        if optional_values:
            down_payments_section_line.update(optional_values)
        return down_payments_section_line

    @api.model
    def _nothing_to_invoice_error(self):
        msg = _(
            """There is nothing to invoice!\n
        Reason(s) of this behavior could be:
        - You should deliver your products before invoicing them: Click on the "truck"
        icon (top-right of your screen) and follow instructions.
        - You should modify the invoicing policy of your product: Open the product,
        go to the "Sales tab" and modify invoicing policy from "delivered quantities"
        to "ordered quantities".
        """
        )
        return UserError(msg)

    @api.model
    def concat_discounts(self, discount, cancel_discount):
        discount_factor = 1.0
        for discount in [discount, cancel_discount]:
            discount_factor = discount_factor * ((100.0 - discount) / 100.0)
        final_discount = 100.0 - (discount_factor * 100.0)
        return final_discount

    @api.model
    def _apply_mobile(self, record):
        if record.partner_id and not record.mobile:
            record.mobile = record.partner_id.mobile
        elif not record.mobile:
            record.mobile = False

    @api.model
    def _apply_email(self, record):
        if record.partner_id and not record.email:
            record.email = record.partner_id.email
        elif not record.email:
            record.email = False

    @api.model
    def _apply_possible_existing_customer_ids(
        self, email=False, mobile=False, partner=False
    ):
        possible_customer = False
        if email and not partner:
            possible_customer = self.env["res.partner"].search([("email", "=", email)])
        if mobile and not partner:
            possible_customer = self.env["res.partner"].search(
                [("mobile", "=", mobile)]
            )
        return possible_customer

    def _create_payment_transaction(self, vals):
        # Ensure the currencies are the same.
        currency = self[0].currency_id
        if any(folio.currency_id != currency for folio in self):
            raise ValidationError(
                _(
                    "A transaction can't be linked to folios having different currencies."
                )
            )

        # Ensure the partner are the same.
        partner = self[0].partner_id
        if any(folio.partner_id != partner for folio in self):
            raise ValidationError(
                _("A transaction can't be linked to folios having different partners.")
            )

        # Try to retrieve the acquirer. However, fallback to the token's acquirer.
        acquirer_id = vals.get("acquirer_id")
        acquirer = None
        payment_token_id = vals.get("payment_token_id")

        if payment_token_id:
            payment_token = self.env["payment.token"].sudo().browse(payment_token_id)

            # Check payment_token/acquirer matching or take the acquirer from token
            if acquirer_id:
                acquirer = self.env["payment.acquirer"].browse(acquirer_id)
                if payment_token and payment_token.acquirer_id != acquirer:
                    raise ValidationError(
                        _("Invalid token found! Token acquirer %s != %s")
                        % (payment_token.acquirer_id.name, acquirer.name)
                    )
                if payment_token and payment_token.partner_id != partner:
                    raise ValidationError(
                        _("Invalid token found! Token partner %s != %s")
                        % (payment_token.partner.name, partner.name)
                    )
            else:
                acquirer = payment_token.acquirer_id

        # Check an acquirer is there.
        if not acquirer_id and not acquirer:
            raise ValidationError(
                _("A payment acquirer is required to create a transaction.")
            )

        if not acquirer:
            acquirer = self.env["payment.acquirer"].browse(acquirer_id)

        # Check a journal is set on acquirer.
        if not acquirer.journal_id:
            raise ValidationError(
                _("A journal must be specified for the acquirer %s.", acquirer.name)
            )

        if not acquirer_id and acquirer:
            vals["acquirer_id"] = acquirer.id

        vals.update(
            {
                "amount": sum(self.mapped("amount_total")),
                "currency_id": currency.id,
                "partner_id": partner.id,
                "folio_ids": [(6, 0, self.ids)],
            }
        )
        transaction = self.env["payment.transaction"].create(vals)

        # Process directly if payment_token
        if transaction.payment_token_id:
            transaction.s2s_do_transaction()

        return transaction
