# Copyright 2018-2020 ForgeFlow, S.L.
# Copyright 2018-2020 Brainbean Apps (https://brainbeanapps.com)
# Copyright 2018-2019 Onestein (<https://www.onestein.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

import logging
import re
from collections import namedtuple
from datetime import datetime, time

import babel.dates
from dateutil.relativedelta import SU, relativedelta

from odoo import SUPERUSER_ID, api, fields, models
from odoo.exceptions import UserError, ValidationError

_logger = logging.getLogger(__name__)

empty_name = "/"


class Sheet(models.Model):
    _name = "hr_timesheet.sheet"
    _description = "Timesheet Sheet"
    _inherit = ["mail.thread", "mail.activity.mixin", "portal.mixin"]
    _table = "hr_timesheet_sheet"
    _order = "id desc"
    _rec_name = "complete_name"

    def _default_date_start(self):
        return self._get_period_start(
            self.env.user.company_id, fields.Date.context_today(self)
        )

    def _default_date_end(self):
        return self._get_period_end(
            self.env.user.company_id, fields.Date.context_today(self)
        )

    def _selection_review_policy(self):
        ResCompany = self.env["res.company"]
        return ResCompany._fields["timesheet_sheet_review_policy"].selection

    def _default_review_policy(self):
        company = self.env.company
        return company.timesheet_sheet_review_policy

    def _default_employee(self):
        company = self.env.company
        return self.env["hr.employee"].search(
            [("user_id", "=", self.env.uid), ("company_id", "in", [company.id, False])],
            limit=1,
            order="company_id ASC",
        )

    def _default_department_id(self):
        return self._default_employee().department_id

    name = fields.Char(compute="_compute_name")
    employee_id = fields.Many2one(
        comodel_name="hr.employee",
        string="Employee",
        default=lambda self: self._default_employee(),
        required=True,
    )
    user_id = fields.Many2one(
        comodel_name="res.users",
        related="employee_id.user_id",
        string="User",
        store=True,
    )
    date_start = fields.Date(
        string="Date From",
        default=lambda self: self._default_date_start(),
        required=True,
        index=True,
    )
    date_end = fields.Date(
        string="Date To",
        default=lambda self: self._default_date_end(),
        required=True,
        index=True,
    )
    timesheet_ids = fields.One2many(
        comodel_name="account.analytic.line",
        inverse_name="sheet_id",
        string="Timesheets",
    )
    line_ids = fields.One2many(
        comodel_name="hr_timesheet.sheet.line",
        compute="_compute_line_ids",
        string="Timesheet Sheet Lines",
    )
    new_line_ids = fields.One2many(
        comodel_name="hr_timesheet.sheet.new.analytic.line",
        inverse_name="sheet_id",
        string="Temporary Timesheets",
    )
    state = fields.Selection(
        [
            ("new", "New"),
            ("draft", "Open"),
            ("confirm", "Waiting Review"),
            ("done", "Approved"),
        ],
        default="new",
        tracking=True,
        string="Status",
        required=True,
        index=True,
    )
    company_id = fields.Many2one(
        comodel_name="res.company",
        string="Company",
        default=lambda self: self.env.company,
        required=True,
    )
    review_policy = fields.Selection(
        selection=lambda self: self._selection_review_policy(),
        default=lambda self: self._default_review_policy(),
        required=True,
    )
    department_id = fields.Many2one(
        comodel_name="hr.department",
        string="Department",
        default=lambda self: self._default_department_id(),
    )
    reviewer_id = fields.Many2one(
        comodel_name="hr.employee", string="Reviewer", tracking=True
    )
    add_line_project_id = fields.Many2one(
        comodel_name="project.project",
        string="Select Project",
        domain="[('company_id', '=', company_id), ('allow_timesheets', '=', True)]",
        help="If selected, the associated project is added "
        "to the timesheet sheet when clicked the button.",
    )
    add_line_task_id = fields.Many2one(
        comodel_name="project.task",
        string="Select Task",
        domain="[('id', 'in', available_task_ids)]",
        help="If selected, the associated task is added "
        "to the timesheet sheet when clicked the button.",
    )
    available_task_ids = fields.Many2many(
        comodel_name="project.task",
        string="Available Tasks",
        compute="_compute_available_task_ids",
    )
    total_time = fields.Float(compute="_compute_total_time", store=True)
    can_review = fields.Boolean(
        compute="_compute_can_review", search="_search_can_review"
    )
    complete_name = fields.Char(compute="_compute_complete_name")

    @api.depends("date_start", "date_end")
    def _compute_name(self):
        locale = self.env.context.get("lang") or self.env.user.lang or "en_US"
        for sheet in self:
            if not sheet.date_start or not sheet.date_end:
                raise UserError(sheet.env._("Please enter start and end date"))
            if sheet.date_start == sheet.date_end:
                sheet.name = babel.dates.format_skeleton(
                    skeleton="MMMEd",
                    datetime=datetime.combine(sheet.date_start, time.min),
                    locale=locale,
                )
                continue

            period_start = sheet.date_start.strftime("%V, %Y")
            period_end = sheet.date_end.strftime("%V, %Y")

            if sheet.date_end <= sheet.date_start + relativedelta(weekday=SU):
                sheet.name = sheet.env._("Week %(end)s", end=period_end)
            else:
                sheet.name = sheet.env._(
                    "Weeks %(start)s - %(end)s", start=period_start, end=period_end
                )

    @api.depends("timesheet_ids.unit_amount")
    def _compute_total_time(self):
        for sheet in self:
            sheet.total_time = sum(sheet.mapped("timesheet_ids.unit_amount"))

    @api.depends("review_policy")
    def _compute_can_review(self):
        for sheet in self:
            sheet.can_review = self.env.user in sheet._get_possible_reviewers()

    @api.model
    def _search_can_review(self, operator, value):
        def check_in(users):
            return self.env.user in users

        def check_not_in(users):
            return self.env.user not in users

        if (operator == "=" and value) or (operator in ["<>", "!="] and not value):
            check = check_in
        else:
            check = check_not_in

        sheets = self.search([]).filtered(
            lambda sheet: check(sheet._get_possible_reviewers())
        )
        return [("id", "in", sheets.ids)]

    @api.depends("name", "employee_id")
    def _compute_complete_name(self):
        for sheet in self:
            complete_name = sheet.name
            complete_name_components = sheet._get_complete_name_components()
            if complete_name_components:
                complete_name = "{} ({})".format(
                    complete_name,
                    ", ".join(complete_name_components),
                )
            sheet.complete_name = complete_name

    @api.constrains("date_start", "date_end")
    def _check_start_end_dates(self):
        for sheet in self:
            if sheet.date_start > sheet.date_end:
                raise ValidationError(
                    sheet.env._("The start date cannot be later than the end date.")
                )

    def _get_complete_name_components(self):
        """Hook for extensions"""
        self.ensure_one()
        return [self.employee_id.display_name]

    def _get_overlapping_sheet_domain(self):
        """Hook for extensions"""
        self.ensure_one()
        return [
            ("id", "!=", self.id),
            ("date_start", "<=", self.date_end),
            ("date_end", ">=", self.date_start),
            ("employee_id", "=", self.employee_id.id),
            ("company_id", "=", self._get_timesheet_sheet_company().id),
        ]

    @api.constrains(
        "date_start", "date_end", "company_id", "employee_id", "review_policy"
    )
    def _check_overlapping_sheets(self):
        for sheet in self:
            overlapping_sheets = self.search(sheet._get_overlapping_sheet_domain())
            if overlapping_sheets:
                raise ValidationError(
                    sheet.env._(
                        "You cannot have 2 or more sheets that overlap!\n"
                        'Please use the menu "Timesheet Sheet" '
                        "to avoid this problem.\nConflicting sheets:\n - %(names)s",
                        names=(
                            "\n - ".join(overlapping_sheets.mapped("complete_name")),
                        ),
                    )
                )

    @api.constrains("company_id", "employee_id")
    def _check_company_id_employee_id(self):
        for rec in self.sudo():
            if (
                rec.company_id
                and rec.employee_id.company_id
                and rec.company_id != rec.employee_id.company_id
            ):
                raise ValidationError(
                    rec.env._(
                        "The Company in the Timesheet Sheet and in "
                        "the Employee must be the same."
                    )
                )

    @api.constrains("company_id", "department_id")
    def _check_company_id_department_id(self):
        for rec in self.sudo():
            if (
                rec.company_id
                and rec.department_id.company_id
                and rec.company_id != rec.department_id.company_id
            ):
                raise ValidationError(
                    rec.env._(
                        "The Company in the Timesheet Sheet and in "
                        "the Department must be the same."
                    )
                )

    @api.constrains("company_id", "add_line_project_id")
    def _check_company_id_add_line_project_id(self):
        for rec in self.sudo():
            if (
                rec.company_id
                and rec.add_line_project_id.company_id
                and rec.company_id != rec.add_line_project_id.company_id
            ):
                raise ValidationError(
                    rec.env._(
                        "The Company in the Timesheet Sheet and in "
                        "the Project must be the same."
                    )
                )

    @api.constrains("company_id", "add_line_task_id")
    def _check_company_id_add_line_task_id(self):
        for rec in self.sudo():
            if (
                rec.company_id
                and rec.add_line_task_id.company_id
                and rec.company_id != rec.add_line_task_id.company_id
            ):
                raise ValidationError(
                    rec.env._(
                        "The Company in the Timesheet Sheet and in "
                        "the Task must be the same."
                    )
                )

    def _get_possible_reviewers(self):
        self.ensure_one()
        res = self.env["res.users"].browse(SUPERUSER_ID)
        if self.review_policy == "hr":
            res |= self.env.ref("hr.group_hr_user").users
        elif self.review_policy == "hr_manager":
            res |= self.env.ref("hr.group_hr_manager").users
        elif self.review_policy == "timesheet_manager":
            res |= self.env.ref("hr_timesheet.group_hr_timesheet_approver").users
        return res

    def _get_timesheet_sheet_company(self):
        self.ensure_one()
        employee = self.employee_id
        company = employee.company_id or employee.department_id.company_id
        if not company:
            company = employee.user_id.company_id
        return company

    @api.onchange("employee_id")
    def _onchange_employee_id(self):
        if self.employee_id:
            company = self.sudo()._get_timesheet_sheet_company()
            self.company_id = company
            self.review_policy = company.timesheet_sheet_review_policy
            self.department_id = self.employee_id.department_id

    def _get_timesheet_sheet_lines_domain(self):
        self.ensure_one()
        return [
            ("date", "<=", self.date_end),
            ("date", ">=", self.date_start),
            ("employee_id", "=", self.employee_id.id),
            ("company_id", "=", self._get_timesheet_sheet_company().id),
            ("project_id", "!=", False),
        ]

    @api.depends("date_start", "date_end")
    def _compute_line_ids(self):
        SheetLine = self.env["hr_timesheet.sheet.line"]
        for sheet in self:
            if not all([sheet.date_start, sheet.date_end]):
                continue
            matrix = sheet._get_data_matrix()
            vals_list = []
            for key in sorted(matrix, key=lambda key: sheet._get_matrix_sortby(key)):
                vals_list.append(sheet._get_default_sheet_line(matrix, key))
                if sheet.state in ["new", "draft"] and self.env.context.get(
                    "hr_timesheet_sheet_clean_timesheets", True
                ):
                    sheet.clean_timesheets(matrix[key])
            sheet.line_ids = [fields.Command.set(SheetLine.create(vals_list).ids)]

    @api.model
    def _matrix_key_attributes(self):
        """Hook for extensions"""
        return ["date", "project_id", "task_id"]

    @api.model
    def _matrix_key(self):
        return namedtuple("MatrixKey", self._matrix_key_attributes())

    @api.model
    def _get_matrix_key_values_for_line(self, aal):
        """Hook for extensions"""
        return {"date": aal.date, "project_id": aal.project_id, "task_id": aal.task_id}

    @api.model
    def _get_matrix_sortby(self, key):
        res = []
        for attribute in key:
            if hasattr(attribute, "name_get"):
                name = attribute.display_name
                value = name if name else ""
            else:
                value = attribute
            res.append(value)
        return res

    def _get_data_matrix(self):
        self.ensure_one()
        MatrixKey = self._matrix_key()
        matrix = {}
        empty_line = self.env["account.analytic.line"]
        for line in self.timesheet_ids:
            key = MatrixKey(**self._get_matrix_key_values_for_line(line))
            if key not in matrix:
                matrix[key] = empty_line
            matrix[key] += line
        for date in self._get_dates():
            for key in matrix.copy():
                key = MatrixKey(**{**key._asdict(), "date": date})
                if key not in matrix:
                    matrix[key] = empty_line
        return matrix

    def _compute_timesheet_ids(self):
        AccountAnalyticLines = self.env["account.analytic.line"]
        for sheet in self:
            domain = sheet._get_timesheet_sheet_lines_domain()
            timesheets = AccountAnalyticLines.search(domain)
            sheet.link_timesheets_to_sheet(timesheets)
            sheet.timesheet_ids = [fields.Command.set(timesheets.ids)]

    @api.onchange("date_start", "date_end", "employee_id")
    def _onchange_scope(self):
        self._compute_timesheet_ids()

    @api.onchange("date_start", "date_end")
    def _onchange_dates(self):
        if self.date_start and self.date_end:
            if self.date_start > self.date_end:
                self.date_end = self.date_start

    @api.onchange("timesheet_ids")
    def _onchange_timesheets(self):
        self._compute_line_ids()

    @api.depends(
        "add_line_project_id", "company_id", "timesheet_ids", "timesheet_ids.task_id"
    )
    def _compute_available_task_ids(self):
        project_task_obj = self.env["project.task"]
        for rec in self:
            if rec.add_line_project_id:
                rec.available_task_ids = project_task_obj.search(
                    [
                        ("project_id", "=", rec.add_line_project_id.id),
                        ("company_id", "=", rec.company_id.id),
                        ("id", "not in", rec.timesheet_ids.mapped("task_id").ids),
                    ]
                ).ids
            else:
                rec.available_task_ids = []

    @api.model
    def _check_employee_user_link(self, vals):
        if vals.get("employee_id"):
            employee = self.env["hr.employee"].sudo().browse(vals["employee_id"])
            if not employee.user_id:
                raise UserError(
                    employee.env._(
                        "In order to create a sheet for this employee, you must"
                        " link him/her to an user: %s"
                    )
                    % (employee.name,)
                )
            return employee.user_id.id
        return False

    def copy(self, default=None):
        if not self.env.context.get("allow_copy_timesheet"):
            raise UserError(self.env._("You cannot duplicate a sheet."))
        return super().copy(default=default)

    @api.model_create_multi
    def create(self, vals_list):
        for vals in vals_list:
            self._check_employee_user_link(vals)
        res = super().create(vals_list)
        res.write({"state": "draft"})
        return res

    def _sheet_write(self, field, recs):
        self.with_context(sheet_write=True).write({field: [fields.Command.set(recs.ids)]})

    def write(self, vals):
        self._check_employee_user_link(vals)
        res = super().write(vals)
        for rec in self:
            if rec.state == "draft" and not self.env.context.get("sheet_write"):
                rec._update_analytic_lines_from_new_lines(vals)
                if "add_line_project_id" not in vals:
                    rec.delete_empty_lines(True)
        return res

    def unlink(self):
        for sheet in self:
            if sheet.state in ("confirm", "done"):
                raise UserError(
                    sheet.env._(
                        "You cannot delete a timesheet sheet which is already"
                        " submitted or confirmed: %s",
                        sheet.complete_name,
                    )
                )
        return super().unlink()

    def onchange(self, values, field_name, field_onchange):
        """
        Pass a flag for _compute_line_ids not to clean timesheet lines to be (kind of)
        idempotent during onchange
        """
        return super(
            Sheet, self.with_context(hr_timesheet_sheet_clean_timesheets=False)
        ).onchange(values, field_name, field_onchange)

    def _get_informables(self):
        """Hook for extensions"""
        self.ensure_one()
        return self.employee_id.parent_id.user_id.partner_id

    def _get_subscribers(self):
        """Hook for extensions"""
        self.ensure_one()
        subscribers = self._get_possible_reviewers().mapped("partner_id")
        subscribers |= self._get_informables()
        return subscribers

    def _timesheet_subscribe_users(self):
        for sheet in self.sudo():
            subscribers = sheet._get_subscribers()
            if subscribers:
                sheet.message_subscribe(partner_ids=subscribers.ids)

    def action_timesheet_draft(self):
        if self.filtered(lambda sheet: sheet.state != "done"):
            raise UserError(self.env._("Cannot revert to draft a non-approved sheet."))
        self._check_can_review()
        self.write({"state": "draft", "reviewer_id": False})

    def action_timesheet_confirm(self):
        self._timesheet_subscribe_users()
        self.reset_add_line()
        self.write({"state": "confirm"})

    def action_timesheet_done(self):
        if self.filtered(lambda sheet: sheet.state != "confirm"):
            raise UserError(self.env._("Cannot approve a non-submitted sheet."))
        self._check_can_review()
        self.write({"state": "done", "reviewer_id": self._get_current_reviewer().id})

    def action_timesheet_refuse(self):
        if self.filtered(lambda sheet: sheet.state != "confirm"):
            raise UserError(self.env._("Cannot reject a non-submitted sheet."))
        self._check_can_review()
        self.write({"state": "draft", "reviewer_id": False})

    @api.model
    def _get_current_reviewer(self):
        reviewer = self.env["hr.employee"].search(
            [("user_id", "=", self.env.uid)], limit=1
        )
        if not reviewer:
            raise UserError(
                reviewer.env._(
                    "In order to review a timesheet sheet, your user needs to be"
                    " linked to an employee."
                )
            )
        return reviewer

    def _check_can_review(self):
        if self.filtered(lambda x: not x.can_review and x.review_policy == "hr"):
            raise UserError(
                self.env._("Only a HR Officer or Manager can review the sheet.")
            )

    def button_add_line(self):
        for rec in self:
            if rec.state in ["new", "draft"]:
                rec.add_line()
                rec.reset_add_line()

    def reset_add_line(self):
        self.write({"add_line_project_id": False, "add_line_task_id": False})

    def _get_date_name(self, date):
        name = babel.dates.format_skeleton(
            skeleton="MMMEd",
            datetime=datetime.combine(date, time.min),
            locale=(self.env.context.get("lang") or self.env.user.lang or "en_US"),
        )
        name = re.sub(r"(\s*[^\w\d\s])\s+", r"\1\n", name)
        name = re.sub(r"([\w\d])\s([\w\d])", "\\1\u00a0\\2", name)
        return name

    def _get_dates(self):
        start = self.date_start
        end = self.date_end
        if end < start:
            return []
        dates = [start]
        while start != end:
            start += relativedelta(days=1)
            dates.append(start)
        return dates

    def _get_line_name(self, project_id, task_id=None, **kwargs):
        self.ensure_one()
        if task_id:
            return f"{project_id.display_name} - {task_id.display_name}"

        return project_id.display_name

    def _get_new_line_unique_id(self):
        """Hook for extensions"""
        self.ensure_one()
        return {
            "project_id": self.add_line_project_id,
            "task_id": self.add_line_task_id,
        }

    def _get_default_sheet_line(self, matrix, key):
        self.ensure_one()
        values = {
            "value_x": self._get_date_name(key.date),
            "value_y": self._get_line_name(**key._asdict()),
            "date": key.date,
            "project_id": key.project_id.id,
            "task_id": key.task_id.id,
            "unit_amount": sum(t.unit_amount for t in matrix[key]),
            "employee_id": self.employee_id.id,
            "company_id": self.company_id.id,
        }
        if self.id:
            values.update({"sheet_id": self.id})
        return values

    @api.model
    def _prepare_empty_analytic_line(self):
        return {
            "name": empty_name,
            "employee_id": self.employee_id.id,
            "date": self.date_start,
            "project_id": self.add_line_project_id.id,
            "task_id": self.add_line_task_id.id,
            "sheet_id": self.id,
            "unit_amount": 0.0,
            "company_id": self.company_id.id,
        }

    def add_line(self):
        if not self.add_line_project_id:
            return
        values = self._prepare_empty_analytic_line()
        new_line_unique_id = self._get_new_line_unique_id()
        existing_unique_ids = list(
            {frozenset(line.get_unique_id().items()) for line in self.line_ids}
        )
        if existing_unique_ids:
            self.delete_empty_lines(False)
        if frozenset(new_line_unique_id.items()) not in existing_unique_ids:
            new_line = self.env["account.analytic.line"]._sheet_create(values)
            self.write({"timesheet_ids": [(4, new_line.id)]})

    def link_timesheets_to_sheet(self, timesheets):
        self.ensure_one()
        if self.id and self.state in ["new", "draft"]:
            for aal in timesheets.filtered(lambda a: not a.sheet_id):
                aal.write({"sheet_id": self.id})

    def clean_timesheets(self, timesheets):
        repeated = timesheets.filtered(
            lambda t: t.name == empty_name and not t.timesheet_invoice_id
        )
        if len(repeated) > 1 and self.id:
            return repeated.merge_timesheets()
        return timesheets

    def _is_add_line(self, row):
        """Hook for extensions"""
        self.ensure_one()
        return (
            self.add_line_project_id == row.project_id
            and self.add_line_task_id == row.task_id
        )

    @api.model
    def _is_line_of_row(self, aal, row):
        """Hook for extensions"""
        return (
            aal.project_id.id == row.project_id.id and aal.task_id.id == row.task_id.id
        )

    def delete_empty_lines(self, delete_empty_rows=False):
        self.ensure_one()
        for name in list(set(self.line_ids.mapped("value_y"))):
            rows = self.line_ids.filtered(lambda line, name=name: line.value_y == name)
            if not rows:
                continue
            row = fields.first(rows)
            if delete_empty_rows and self._is_add_line(row):
                check = any([line.unit_amount for line in rows])
            else:
                check = not all([line.unit_amount for line in rows])
            if not check:
                continue
            row_lines = self.timesheet_ids.filtered(
                lambda aal, row=row: self._is_line_of_row(aal, row)
            )
            row_lines.filtered(
                lambda t: t.name == empty_name
                and not t.unit_amount
                and not t.timesheet_invoice_id
            ).unlink()
            if self.timesheet_ids != self.timesheet_ids.exists():
                self._sheet_write("timesheet_ids", self.timesheet_ids.exists())

    def _update_analytic_lines_from_new_lines(self, vals):
        self.ensure_one()
        new_line_ids_list = []
        for line in vals.get("line_ids", []):
            # Every time we change a value in the grid a new line in line_ids
            # is created with the proposed changes, even though the line_ids
            # is a computed field. We capture the value of 'new_line_ids'
            # in the proposed dict before it disappears.
            # This field holds the ids of the transient records
            # of model 'hr_timesheet.sheet.new.analytic.line'.
            if line[0] == 1 and line[2] and line[2].get("new_line_id"):
                new_line_ids_list += [line[2].get("new_line_id")]
        for new_line in self.new_line_ids.exists():
            if new_line.id in new_line_ids_list:
                new_line._update_analytic_lines()
        self.new_line_ids.exists().unlink()
        self._sheet_write("new_line_ids", self.new_line_ids.exists())

    @api.model
    def _prepare_new_line(self, line):
        """Hook for extensions"""
        return {
            "sheet_id": line.sheet_id.id,
            "date": line.date,
            "project_id": line.project_id.id,
            "task_id": line.task_id.id,
            "unit_amount": line.unit_amount,
            "company_id": line.company_id.id,
            "employee_id": line.employee_id.id,
        }

    def _is_compatible_new_line(self, line_a, line_b):
        """Hook for extensions"""
        self.ensure_one()
        return (
            line_a.project_id.id == line_b.project_id.id
            and line_a.task_id.id == line_b.task_id.id
            and line_a.date == line_b.date
        )

    def add_new_line(self, line):
        self.ensure_one()
        new_line_model = self.env["hr_timesheet.sheet.new.analytic.line"]
        new_line = self.new_line_ids.filtered(
            lambda lin: self._is_compatible_new_line(lin, line)
        )
        if new_line:
            new_line.write({"unit_amount": line.unit_amount})
        else:
            vals = self._prepare_new_line(line)
            new_line = new_line_model.create(vals)
        self._sheet_write("new_line_ids", self.new_line_ids | new_line)
        line.new_line_id = new_line.id

    @api.model
    def _get_period_start(self, company, date):
        r = company and company.sheet_range or "WEEKLY"
        if r == "WEEKLY":
            if company.timesheet_week_start:
                delta = relativedelta(weekday=int(company.timesheet_week_start), days=6)
            else:
                delta = relativedelta(days=date.weekday())
            return date - delta
        elif r == "MONTHLY":
            return date + relativedelta(day=1)
        return date

    @api.model
    def _get_period_end(self, company, date):
        r = company and company.sheet_range or "WEEKLY"
        if r == "WEEKLY":
            if company.timesheet_week_start:
                delta = relativedelta(
                    weekday=(int(company.timesheet_week_start) + 6) % 7
                )
            else:
                delta = relativedelta(days=6 - date.weekday())
            return date + delta
        elif r == "MONTHLY":
            return date + relativedelta(months=1, day=1, days=-1)
        return date

    # ------------------------------------------------
    # OpenChatter methods and notifications
    # ------------------------------------------------

    def _track_subtype(self, init_values):
        if self:
            record = self[0]
            if "state" in init_values and record.state == "confirm":
                return self.env.ref("hr_timesheet_sheet.mt_timesheet_confirmed")
            elif "state" in init_values and record.state == "done":
                return self.env.ref("hr_timesheet_sheet.mt_timesheet_approved")
        return super()._track_subtype(init_values)


class AbstractSheetLine(models.AbstractModel):
    _name = "hr_timesheet.sheet.line.abstract"
    _description = "Abstract Timesheet Sheet Line"

    sheet_id = fields.Many2one(comodel_name="hr_timesheet.sheet", ondelete="cascade")
    date = fields.Date()
    project_id = fields.Many2one(comodel_name="project.project", string="Project")
    task_id = fields.Many2one(comodel_name="project.task", string="Task")
    unit_amount = fields.Float(string="Quantity", default=0.0)
    company_id = fields.Many2one(comodel_name="res.company", string="Company")
    employee_id = fields.Many2one(comodel_name="hr.employee", string="Employee")

    def get_unique_id(self):
        """Hook for extensions"""
        self.ensure_one()
        return {"project_id": self.project_id, "task_id": self.task_id}


class SheetLine(models.TransientModel):
    _name = "hr_timesheet.sheet.line"
    _inherit = "hr_timesheet.sheet.line.abstract"
    _description = "Timesheet Sheet Line"

    value_x = fields.Char(string="Date Name")
    value_y = fields.Char(string="Project Name")
    new_line_id = fields.Integer(default=0)

    @api.onchange("unit_amount")
    def onchange_unit_amount(self):
        """This method is called when filling a cell of the matrix."""
        self.ensure_one()
        sheet = self._get_sheet()
        if not sheet:
            return {
                "warning": {
                    "title": self.env._("Warning"),
                    "message": self.env._("Save the Timesheet Sheet first."),
                }
            }
        sheet.add_new_line(self)

    @api.model
    def _get_sheet(self):
        sheet = (self._origin or self).sheet_id
        if not sheet:
            model = self.env.context.get("params", {}).get("model", "")
            obj_id = self.env.context.get("params", {}).get("id")
            if model == "hr_timesheet.sheet" and isinstance(obj_id, int):
                sheet = self.env["hr_timesheet.sheet"].browse(obj_id)
        return sheet


class SheetNewAnalyticLine(models.TransientModel):
    _name = "hr_timesheet.sheet.new.analytic.line"
    _inherit = "hr_timesheet.sheet.line.abstract"
    _description = "Timesheet Sheet New Analytic Line"

    @api.model
    def _is_similar_analytic_line(self, aal):
        """Hook for extensions"""
        return (
            aal.date == self.date
            and aal.project_id.id == self.project_id.id
            and aal.task_id.id == self.task_id.id
        )

    @api.model
    def _update_analytic_lines(self):
        sheet = self.sheet_id
        timesheets = sheet.timesheet_ids.filtered(
            lambda aal: self._is_similar_analytic_line(aal)
        )
        new_ts = timesheets.filtered(lambda t: t.name == empty_name)
        amount = sum(t.unit_amount for t in timesheets)
        diff_amount = self.unit_amount - amount
        if len(new_ts) > 1:
            new_ts = new_ts.merge_timesheets()
            sheet._sheet_write("timesheet_ids", sheet.timesheet_ids.exists())
        if not diff_amount:
            return
        if new_ts:
            unit_amount = new_ts.unit_amount + diff_amount
            if unit_amount:
                new_ts.write({"unit_amount": unit_amount})
            else:
                new_ts.unlink()
                sheet._sheet_write("timesheet_ids", sheet.timesheet_ids.exists())
        else:
            new_ts_values = sheet._prepare_new_line(self)
            new_ts_values.update({"name": empty_name, "unit_amount": diff_amount})
            self.env["account.analytic.line"]._sheet_create(new_ts_values)
