# Copyright (C) 2019-Today: Odoo Community Association (OCA)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from odoo import _, api, fields, models
from odoo.exceptions import UserError

JOURNAL_TYPE_MAP = {
    ("outgoing", "customer"): ["sale"],
    ("outgoing", "supplier"): ["purchase"],
    ("outgoing", "transit"): ["sale", "purchase"],
    ("incoming", "supplier"): ["purchase"],
    ("incoming", "customer"): ["sale"],
    ("incoming", "transit"): ["purchase", "sale"],
}

INVOICE_TYPE_MAP = {
    # Picking Type Code | Local Origin Usage | Local Dest Usage
    ("outgoing", "internal", "customer"): "out_invoice",
    ("incoming", "customer", "internal"): "out_refund",
    ("incoming", "customer", "customer"): "out_refund",
    ("incoming", "supplier", "internal"): "in_invoice",
    ("incoming", "supplier", "customer"): "in_invoice",
    ("outgoing", "internal", "supplier"): "in_refund",
    ("outgoing", "internal", "internal"): "in_refund",
    ("incoming", "transit", "internal"): "in_invoice",
    ("outgoing", "transit", "supplier"): "in_refund",
    ("outgoing", "transit", "customer"): "out_invoice",
}


class StockInvoiceOnshipping(models.TransientModel):
    _name = "stock.invoice.onshipping"
    _description = "Stock Invoice Onshipping"

    @api.model
    def _get_journal_type(self):
        active_ids = self.env.context.get("active_ids", [])
        if active_ids:
            active_ids = active_ids[0]
        pick_obj = self.env["stock.picking"]
        picking = pick_obj.browse(active_ids)
        if not picking or not picking.move_ids:
            return "sale"
        pick_type_code = picking.picking_type_id.code
        line = fields.first(picking.move_ids)
        if pick_type_code == "incoming":
            usage = line.location_id.usage
        else:
            usage = line.location_dest_id.usage
        return JOURNAL_TYPE_MAP.get((pick_type_code, usage), ["sale"])[0]

    journal_type = fields.Selection(
        selection=[
            ("purchase", "Create Supplier Invoice"),
            ("sale", "Create Customer Invoice"),
        ],
        default=_get_journal_type,
        readonly=True,
    )
    group = fields.Selection(
        selection=[
            ("picking", "Picking"),
            ("partner", "Partner"),
            ("partner_product", "Partner/Product"),
        ],
        default="picking",
        help="Group pickings/moves to create invoice(s):\n"
        "Picking: One invoice per picking;\n"
        "Partner: One invoice for each picking's partner;\n"
        "Partner/Product: One invoice per picking's partner and group "
        "product into a single invoice line.",
        required=True,
    )
    invoice_date = fields.Date()
    sale_journal = fields.Many2one(
        comodel_name="account.journal",
        domain="[('type', '=', 'sale')]",
        default=lambda self: self._default_journal("sale"),
        ondelete="cascade",
    )
    purchase_journal = fields.Many2one(
        comodel_name="account.journal",
        domain="[('type', '=', 'purchase')]",
        default=lambda self: self._default_journal("purchase"),
        ondelete="cascade",
    )
    show_sale_journal = fields.Boolean()
    show_purchase_journal = fields.Boolean()

    @api.model
    def default_get(self, fields_list):
        """
        Inherit to add default invoice_date
        :param fields_list: list of str
        :return: dict
        """
        result = super().default_get(fields_list)
        result.update({"invoice_date": fields.Date.today()})
        return result

    @api.onchange("group")
    def onchange_group(self):
        self.ensure_one()
        (
            sale_pickings,
            sale_refund_pickings,
            purchase_pickings,
            purchase_refund_pickings,
        ) = self.get_split_pickings()
        self.show_sale_journal = bool(sale_pickings)
        self.show_purchase_journal = bool(purchase_pickings)

    def get_partner_sum(self, pickings, partner, inv_type, picking_type, usage):
        pickings = pickings.filtered(
            lambda x: x.picking_type_id.code == picking_type and x.partner_id == partner
        )
        lines = pickings.mapped("move_ids")
        if picking_type == "outgoing":
            moves = lines.filtered(lambda x: x.location_dest_id.usage == usage)
        else:
            moves = lines.filtered(lambda x: x.location_id.usage == usage)
        total = sum(
            (
                m._get_price_unit_invoice(inv_type, m.picking_id.partner_id)
                * m.product_uom_qty
            )
            for m in moves
        )
        return total, moves.mapped("picking_id")

    def get_split_pickings(self):
        self.ensure_one()
        picking_obj = self.env["stock.picking"]
        pickings = picking_obj.browse(self.env.context.get("active_ids", []))
        if self.group != "picking":
            return self.get_split_pickings_grouped(pickings)
        return self.get_split_pickings_nogrouped(pickings)

    def get_split_pickings_grouped(self, pickings):
        sale_pickings = self.env["stock.picking"].browse()
        sale_refund_pickings = self.env["stock.picking"].browse()
        purchase_pickings = self.env["stock.picking"].browse()
        purchase_refund_pickings = self.env["stock.picking"].browse()

        for partner in pickings.mapped("partner_id"):
            so_sum, so_pickings = self.get_partner_sum(
                pickings, partner, "out_invoice", "outgoing", "customer"
            )
            si_sum, si_pickings = self.get_partner_sum(
                pickings, partner, "out_invoice", "incoming", "customer"
            )
            if (so_sum - si_sum) >= 0:
                sale_pickings |= so_pickings | si_pickings
            else:
                sale_refund_pickings |= so_pickings | si_pickings
            pi_sum, pi_pickings = self.get_partner_sum(
                pickings, partner, "in_invoice", "incoming", "supplier"
            )
            po_sum, po_pickings = self.get_partner_sum(
                pickings, partner, "in_invoice", "outgoing", "supplier"
            )
            if (pi_sum - po_sum) >= 0:
                purchase_pickings |= pi_pickings | po_pickings
            else:
                purchase_refund_pickings |= pi_pickings | po_pickings

        return (
            sale_pickings,
            sale_refund_pickings,
            purchase_pickings,
            purchase_refund_pickings,
        )

    def get_split_pickings_nogrouped(self, pickings):
        first = fields.first
        sale_pickings = pickings.filtered(
            lambda x: x.picking_type_id.code == "outgoing"
            and first(x.move_ids).location_dest_id.usage == "customer"
        )
        sale_refund_pickings = pickings.filtered(
            lambda x: x.picking_type_id.code == "incoming"
            and first(x.move_ids).location_id.usage == "customer"
        )
        purchase_pickings = pickings.filtered(
            lambda x: x.picking_type_id.code == "incoming"
            and first(x.move_ids).location_id.usage == "supplier"
        )
        purchase_refund_pickings = pickings.filtered(
            lambda x: x.picking_type_id.code == "outgoing"
            and first(x.move_ids).location_dest_id.usage == "supplier"
        )

        return (
            sale_pickings,
            sale_refund_pickings,
            purchase_pickings,
            purchase_refund_pickings,
        )

    @api.model
    def _default_journal(self, journal_type):
        """
        Get the default journal based on the given type
        :param journal_type: str
        :return: account.journal recordset
        """
        default_journal = self.env["account.journal"].search(
            [
                ("type", "=", journal_type),
                ("company_id", "=", self.env.company.id),
            ],
            limit=1,
        )
        return default_journal

    def action_generate(self):
        """
        Launch the invoice generation
        :return:
        """
        self.ensure_one()
        invoices = self._action_generate_invoices()
        if not invoices:
            raise UserError(_("No invoice created!"))

        # Update the state on pickings related to new invoices only
        self._update_picking_invoice_status(invoices.mapped("picking_ids"))

        inv_type = self._get_invoice_type()
        if inv_type in ["out_invoice", "out_refund"]:
            xmlid = "account.action_move_out_invoice_type"
        else:
            xmlid = "account.action_move_in_invoice_type"
        action_dict = self.env["ir.actions.act_window"]._for_xml_id(xmlid)

        if len(invoices) > 1:
            action_dict["domain"] = [("id", "in", invoices.ids)]
        elif len(invoices) == 1:
            if inv_type in ["out_invoice", "out_refund"]:
                form_view = [(self.env.ref("account.view_move_form").id, "form")]
            else:
                form_view = [(self.env.ref("account.view_move_form").id, "form")]
            if "views" in action_dict:
                action_dict["views"] = form_view + [
                    (state, view)
                    for state, view in action_dict["views"]
                    if view != "form"
                ]
            else:
                action_dict["views"] = form_view
            action_dict["res_id"] = invoices.ids[0]

        return action_dict

    def _load_pickings(self):
        """
        Load pickings from context
        :return: stock.picking recordset
        """
        picking_obj = self.env["stock.picking"]
        active_ids = self.env.context.get("active_ids", [])
        pickings = picking_obj.browse(active_ids)
        pickings = pickings.filtered(lambda p: p.invoice_state == "2binvoiced")
        return pickings

    def _get_journal(self):
        """
        Get the journal depending on the journal_type
        :return: account.journal recordset
        """
        self.ensure_one()
        journal_field = f"{self.journal_type}_journal"
        journal = self[journal_field]
        return journal

    def _get_invoice_type(self):
        """
        Get the invoice type
        :return: str
        """
        self.ensure_one()

        active_ids = self.env.context.get("active_ids", [])
        if active_ids:
            active_ids = active_ids[0]
        picking = self.env["stock.picking"].browse(active_ids)

        inv_type = (
            INVOICE_TYPE_MAP.get(
                (
                    picking.picking_type_code,
                    picking.location_id.usage,
                    picking.location_dest_id.usage,
                )
            )
            or "out_invoice"
        )

        return inv_type

    @api.model
    def _get_picking_key(self, picking):
        """
        Get the key for the given picking.
        By default, it's based on the invoice partner and the picking_type_id
        of the picking
        :param picking: stock.picking recordset
        :return: key (tuple,...)
        """
        key = (picking,)
        if self.group in ["partner", "partner_product"]:
            # Pickings with same Partner to create Invoice but the
            # Partner to Shipping is different should not be grouping.
            key = (
                picking._get_partner_to_invoice(),
                picking.picking_type_id,
                picking.partner_id,
            )
        return key

    def _group_pickings(self, pickings):
        """
        Group given picking
        :param pickings:
        :return: list of stock.picking recordset
        """
        grouped_picking = {}
        pickings = pickings.filtered(lambda p: p.invoice_state == "2binvoiced")
        for picking in pickings:
            key = self._get_picking_key(picking)
            picks_grouped = grouped_picking.get(key, self.env["stock.picking"].browse())
            picks_grouped |= picking
            grouped_picking.update({key: picks_grouped})
        return grouped_picking.values()

    def _simulate_invoice_onchange(self, values):
        """
        Simulate onchange for invoice
        :param values: dict
        :return: dict
        """
        invoice = self.env["account.move"].new(values.copy())
        invoice._onchange_partner_id()
        invoice._inverse_company_id()
        invoice._inverse_currency_id()
        invoice._onchange_date()
        new_values = invoice._convert_to_write(invoice._cache)
        # Ensure basic values are not updated
        values.update(new_values)
        return invoice, values

    def _build_invoice_values_from_pickings(self, pickings):
        """
        Build dict to create a new invoice from given pickings
        :param pickings: stock.picking recordset
        :return: dict
        """
        picking = fields.first(pickings)
        partner_id = picking._get_partner_to_invoice()
        partner = self.env["res.partner"].browse(partner_id)
        inv_type = self._get_invoice_type()
        currency = self.env.company.currency_id
        if partner:
            code = picking.picking_type_id.code
            if partner.property_product_pricelist and code == "outgoing":
                currency = partner.property_product_pricelist.currency_id
        journal = self._get_journal()
        invoice_obj = self.env["account.move"]
        values = invoice_obj.default_get(invoice_obj.fields_get().keys())
        values.update(
            {
                "invoice_origin": ", ".join(pickings.mapped("name")),
                "partner_id": partner_id,
                "move_type": inv_type,
                "currency_id": currency.id,
                "journal_id": journal.id,
            }
        )

        invoice, values = self._simulate_invoice_onchange(values)
        return invoice, values

    def _get_move_key(self, move):
        """
        Get the key based on the given move
        :param move: stock.move recordset
        :return: (key,)
        """
        key = (move,)
        if self.group == "partner_product":
            key = (move.product_id,)
        return key

    def _group_moves(self, moves):
        """
        Possibility to group moves (to create 1 invoice line with many moves)
        :param moves: stock.move recordset
        :return: list of stock.move recordset
        """
        grouped_moves = {}
        moves = moves.filtered(lambda m: m.invoice_state == "2binvoiced")
        for move in moves:
            key = self._get_move_key(move)
            move_grouped = grouped_moves.get(key, self.env["stock.move"].browse())
            move_grouped |= move
            grouped_moves.update({key: move_grouped})
        return grouped_moves.values()

    def _simulate_invoice_line_onchange(self, values, price_unit=None):
        """
        Simulate onchange for invoice line
        :param values: dict
        :return: dict
        """
        line = self.env["account.move.line"].new(values.copy())
        line._inverse_partner_id()
        line._inverse_product_id()
        line._inverse_account_id()
        line._inverse_amount_currency()
        new_values = line._convert_to_write(line._cache)
        if price_unit:
            new_values["price_unit"] = price_unit
        # Ensure basic values are not updated
        values.update(new_values)
        return values

    def _get_invoice_line_values(self, moves, invoice_values, invoice):
        """
        Create invoice line values from given moves
        :param moves: stock.move
        :param invoice: account.move
        :return: dict
        """
        move = fields.first(moves)
        product = move.product_id
        partner_id = self.env["res.partner"].browse(invoice_values["partner_id"])
        inv_type = invoice_values["move_type"]
        quantity = 0
        move_line_ids = []
        for move in moves:
            qty = move.product_uom_qty
            loc = move.location_id
            loc_dst = move.location_dest_id
            # Better to understand with IF/ELIF than many OR
            if inv_type == "out_invoice" and loc.usage == "customer":
                qty *= -1
            elif inv_type == "out_refund" and loc_dst.usage == "customer":
                qty *= -1
            elif inv_type == "in_invoice" and loc_dst.usage == "supplier":
                qty *= -1
            elif inv_type == "in_refund" and loc.usage == "supplier":
                qty *= -1
            quantity += qty
            move_line_ids.append((4, move.id))
        price = moves._get_price_unit_invoice(inv_type, partner_id, quantity)
        line_obj = self.env["account.move.line"]
        values = line_obj.default_get(line_obj.fields_get().keys())
        values.update(
            {
                "name": move.name,
                "product_id": product.id,
                "quantity": quantity,
                "price_unit": price,
                "move_line_ids": move_line_ids,
                "move_id": invoice.id,
            }
        )
        values = self._simulate_invoice_line_onchange(values, price_unit=price)
        return values

    def _update_picking_invoice_status(self, pickings):
        """
        Update invoice_state on pickings
        :param pickings: stock.picking recordset
        :return: stock.picking recordset
        """
        return pickings.set_as_invoiced()

    def ungroup_moves(self, grouped_moves_list):
        """Ungroup your moves, split them again, grouping by
        fiscal position, max itens per invoice and etc
        :param grouped_moves_list:
        :return: list of grouped moves list
        """
        return [grouped_moves_list]

    def _create_invoice(self, invoice_values):
        """Override this method if you need to change any values of the
        invoice and the lines before the invoice creation
        :param invoice_values: dict with the invoice and its lines
        :return: invoice
        """
        return self.env["account.move"].create(invoice_values)

    def _action_generate_invoices(self):
        """
        Action to generate invoices based on pickings
        :return: account.move recordset
        """
        pickings = self._load_pickings()
        company = pickings.mapped("company_id")
        if company and company != self.env.company:
            raise UserError(_("All pickings are not related to your company!"))
        pick_list = self._group_pickings(pickings)
        invoices = self.env["account.move"].browse()
        for pickings in pick_list:
            moves = pickings.mapped("move_ids")
            grouped_moves_list = self._group_moves(moves)
            parts = self.ungroup_moves(grouped_moves_list)
            for moves_list in parts:
                invoice, invoice_values = self._build_invoice_values_from_pickings(
                    pickings
                )
                lines = [(5, 0, {})]
                line_values = False
                for moves in moves_list:
                    line_values = self._get_invoice_line_values(
                        moves, invoice_values, invoice
                    )
                    if line_values:
                        lines.append((0, 0, line_values))
                if line_values:  # Only create the invoice if it has lines
                    invoice_values["invoice_line_ids"] = lines
                    # Necessary inform line_ids with empty list to avoid error
                    # addons/account/models/account_move.py", line 2251,
                    # in _sanitize_vals
                    # assert command not in (Command.SET, Command.CLEAR)
                    invoice_values["line_ids"] = []
                    invoice_values["invoice_date"] = self.invoice_date
                    invoice = self._create_invoice(invoice_values)
                    invoice._compute_amount()
                    invoices |= invoice
        return invoices
