mirror of
https://github.com/bringout/oca-mrp.git
synced 2026-04-23 17:52:00 +02:00
Initial commit: OCA Mrp packages (117 packages)
This commit is contained in:
commit
277e84fd7a
4403 changed files with 395154 additions and 0 deletions
|
|
@ -0,0 +1,9 @@
|
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import res_company
|
||||
from . import res_config
|
||||
from . import account_analytic_account
|
||||
from . import account_analytic_line
|
||||
from . import hr_timesheet_sheet
|
||||
from . import hr_department
|
||||
from . import hr_employee
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# Copyright 2019 Onestein (<https://www.onestein.eu>)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class AccountAnalyticAccount(models.Model):
|
||||
_inherit = "account.analytic.account"
|
||||
|
||||
@api.constrains("company_id")
|
||||
def _check_timesheet_sheet_company_id(self):
|
||||
for rec in self.sudo():
|
||||
sheets = rec.line_ids.mapped("sheet_id").filtered(
|
||||
lambda s: s.company_id and s.company_id != rec.company_id
|
||||
)
|
||||
if sheets:
|
||||
raise ValidationError(
|
||||
_(
|
||||
"You cannot change the company, "
|
||||
"as this %(rec_name)s (%(rec_display_name)s) "
|
||||
"is assigned to %(current_name)s (%(current_display_name)s).",
|
||||
rec_name=rec._name,
|
||||
rec_display_name=rec.display_name,
|
||||
current_name=sheets[0]._name,
|
||||
current_display_name=sheets[0].display_name,
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
# Copyright 2018 ForgeFlow, S.L.
|
||||
# Copyright 2018-2019 Brainbean Apps (https://brainbeanapps.com)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
|
||||
class AccountAnalyticLine(models.Model):
|
||||
_inherit = "account.analytic.line"
|
||||
|
||||
sheet_id = fields.Many2one(
|
||||
comodel_name="hr_timesheet.sheet", string="Sheet", copy=False
|
||||
)
|
||||
sheet_state = fields.Selection(string="Sheet State", related="sheet_id.state")
|
||||
|
||||
def _get_sheet_domain(self):
|
||||
"""Hook for extensions"""
|
||||
self.ensure_one()
|
||||
return [
|
||||
("date_end", ">=", self.date),
|
||||
("date_start", "<=", self.date),
|
||||
("employee_id", "=", self.employee_id.id),
|
||||
("company_id", "in", [self.company_id.id, False]),
|
||||
("state", "in", ["new", "draft"]),
|
||||
]
|
||||
|
||||
def _determine_sheet(self):
|
||||
"""Hook for extensions"""
|
||||
self.ensure_one()
|
||||
return self.env["hr_timesheet.sheet"].search(self._get_sheet_domain(), limit=1)
|
||||
|
||||
def _compute_sheet(self):
|
||||
"""Links the timesheet line to the corresponding sheet"""
|
||||
for timesheet in self.filtered("project_id"):
|
||||
sheet = timesheet._determine_sheet()
|
||||
if timesheet.sheet_id != sheet:
|
||||
timesheet.sheet_id = sheet
|
||||
|
||||
@api.constrains("company_id", "sheet_id")
|
||||
def _check_company_id_sheet_id(self):
|
||||
for aal in self.sudo():
|
||||
if (
|
||||
aal.company_id
|
||||
and aal.sheet_id.company_id
|
||||
and aal.company_id != aal.sheet_id.company_id
|
||||
):
|
||||
raise ValidationError(
|
||||
_(
|
||||
"You cannot create a timesheet of a different company "
|
||||
"than the one of the timesheet sheet:"
|
||||
"\n - %(sheet_name)s of %(sheet_company)s"
|
||||
"\n - %(name)s of %(company)s",
|
||||
sheet_name=aal.sheet_id.complete_name,
|
||||
sheet_company=aal.sheet_id.company_id.name,
|
||||
name=aal.name,
|
||||
company=aal.company_id.name,
|
||||
)
|
||||
)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, values):
|
||||
if not self.env.context.get("sheet_create") and "sheet_id" in values:
|
||||
del values["sheet_id"]
|
||||
res = super().create(values)
|
||||
res._compute_sheet()
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _sheet_create(self, values):
|
||||
return self.with_context(sheet_create=True).create(values)
|
||||
|
||||
def write(self, values):
|
||||
self._check_state_on_write(values)
|
||||
res = super().write(values)
|
||||
if self._timesheet_should_compute_sheet(values):
|
||||
self._compute_sheet()
|
||||
return res
|
||||
|
||||
def unlink(self):
|
||||
self._check_state()
|
||||
return super().unlink()
|
||||
|
||||
def _check_state_on_write(self, values):
|
||||
"""Hook for extensions"""
|
||||
if self._timesheet_should_check_write(values):
|
||||
self._check_state()
|
||||
|
||||
@api.model
|
||||
def _timesheet_should_check_write(self, values):
|
||||
"""Hook for extensions"""
|
||||
return bool(set(self._get_timesheet_protected_fields()) & set(values.keys()))
|
||||
|
||||
@api.model
|
||||
def _timesheet_should_compute_sheet(self, values):
|
||||
"""Hook for extensions"""
|
||||
return any(f in self._get_sheet_affecting_fields() for f in values)
|
||||
|
||||
@api.model
|
||||
def _get_timesheet_protected_fields(self):
|
||||
"""Hook for extensions"""
|
||||
return [
|
||||
"name",
|
||||
"date",
|
||||
"unit_amount",
|
||||
"user_id",
|
||||
"employee_id",
|
||||
"department_id",
|
||||
"company_id",
|
||||
"task_id",
|
||||
"project_id",
|
||||
"sheet_id",
|
||||
]
|
||||
|
||||
@api.model
|
||||
def _get_sheet_affecting_fields(self):
|
||||
"""Hook for extensions"""
|
||||
return ["date", "employee_id", "project_id", "company_id"]
|
||||
|
||||
def _check_state(self):
|
||||
if self.env.context.get("skip_check_state"):
|
||||
return
|
||||
for line in self.exists().filtered("sheet_id"):
|
||||
if line.sheet_id.state not in ["new", "draft"]:
|
||||
raise UserError(
|
||||
_(
|
||||
"You cannot modify an entry in a confirmed timesheet sheet"
|
||||
": %(names)s",
|
||||
names=line.sheet_id.complete_name,
|
||||
)
|
||||
)
|
||||
|
||||
def merge_timesheets(self):
|
||||
unit_amount = sum(t.unit_amount for t in self)
|
||||
amount = sum(t.amount for t in self)
|
||||
self[0].write({"unit_amount": unit_amount, "amount": amount})
|
||||
self[1:].unlink()
|
||||
return self[0]
|
||||
|
||||
def _check_can_update_timesheet(self):
|
||||
return super()._check_can_update_timesheet() or not self.filtered("sheet_id")
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
# Copyright 2018 ForgeFlow, S.L.
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class HrDepartment(models.Model):
|
||||
_inherit = "hr.department"
|
||||
|
||||
timesheet_sheet_to_approve_count = fields.Integer(
|
||||
compute="_compute_timesheet_to_approve", string="Timesheet Sheets to Approve"
|
||||
)
|
||||
|
||||
def _compute_timesheet_to_approve(self):
|
||||
timesheet_data = self.env["hr_timesheet.sheet"].read_group(
|
||||
[("department_id", "in", self.ids), ("state", "=", "confirm")],
|
||||
["department_id"],
|
||||
["department_id"],
|
||||
)
|
||||
result = {
|
||||
data["department_id"][0]: data["department_id_count"]
|
||||
for data in timesheet_data
|
||||
}
|
||||
for department in self:
|
||||
department.timesheet_sheet_to_approve_count = result.get(department.id, 0)
|
||||
|
||||
@api.constrains("company_id")
|
||||
def _check_company_id(self):
|
||||
for rec in self.sudo().filtered("company_id"):
|
||||
for field in [
|
||||
rec.env["hr_timesheet.sheet"].search(
|
||||
[
|
||||
("department_id", "=", rec.id),
|
||||
("company_id", "!=", rec.company_id.id),
|
||||
("company_id", "!=", False),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
]:
|
||||
if (
|
||||
rec.company_id
|
||||
and field.company_id
|
||||
and rec.company_id != field.company_id
|
||||
):
|
||||
raise ValidationError(
|
||||
_(
|
||||
"You cannot change the company, "
|
||||
"as this %(rec_name)s (%(rec_display_name)s) "
|
||||
"is assigned to %(current_name)s (%(current_display_name)s).",
|
||||
rec_name=rec._name,
|
||||
rec_display_name=rec.display_name,
|
||||
current_name=field._name,
|
||||
current_display_name=field.display_name,
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
# Copyright 2018 ForgeFlow, S.L.
|
||||
# Copyright 2019 Brainbean Apps (https://brainbeanapps.com)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class HrEmployee(models.Model):
|
||||
_inherit = "hr.employee"
|
||||
|
||||
timesheet_sheet_ids = fields.One2many(
|
||||
comodel_name="hr_timesheet.sheet",
|
||||
inverse_name="employee_id",
|
||||
string="Timesheet Sheets",
|
||||
)
|
||||
timesheet_sheet_count = fields.Integer(
|
||||
compute="_compute_timesheet_sheet_count", string="Timesheet Sheets Count"
|
||||
)
|
||||
|
||||
def _compute_timesheet_sheet_count(self):
|
||||
Sheet = self.env["hr_timesheet.sheet"]
|
||||
for employee in self:
|
||||
employee.timesheet_sheet_count = Sheet.search_count(
|
||||
[("employee_id", "=", employee.id)]
|
||||
)
|
||||
|
||||
@api.constrains("company_id")
|
||||
def _check_company_id(self):
|
||||
for rec in self.sudo().filtered("company_id"):
|
||||
for field in [
|
||||
rec.env["hr_timesheet.sheet"].search(
|
||||
[
|
||||
("employee_id", "=", rec.id),
|
||||
("company_id", "!=", rec.company_id.id),
|
||||
("company_id", "!=", False),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
]:
|
||||
if (
|
||||
rec.company_id
|
||||
and field.company_id
|
||||
and rec.company_id != field.company_id
|
||||
):
|
||||
raise ValidationError(
|
||||
_(
|
||||
"You cannot change the company, "
|
||||
"as this %(rec_name)s (%(rec_display_name)s) "
|
||||
"is assigned to %(current_name)s (%(current_display_name)s).",
|
||||
rec_name=rec._name,
|
||||
rec_display_name=rec.display_name,
|
||||
current_name=field._name,
|
||||
current_display_name=field.display_name,
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,922 @@
|
|||
# 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,
|
||||
readonly=True,
|
||||
states={"new": [("readonly", False)]},
|
||||
)
|
||||
user_id = fields.Many2one(
|
||||
comodel_name="res.users",
|
||||
related="employee_id.user_id",
|
||||
string="User",
|
||||
store=True,
|
||||
readonly=True,
|
||||
)
|
||||
date_start = fields.Date(
|
||||
string="Date From",
|
||||
default=lambda self: self._default_date_start(),
|
||||
required=True,
|
||||
index=True,
|
||||
readonly=True,
|
||||
states={"new": [("readonly", False)]},
|
||||
)
|
||||
date_end = fields.Date(
|
||||
string="Date To",
|
||||
default=lambda self: self._default_date_end(),
|
||||
required=True,
|
||||
index=True,
|
||||
readonly=True,
|
||||
states={"new": [("readonly", False)]},
|
||||
)
|
||||
timesheet_ids = fields.One2many(
|
||||
comodel_name="account.analytic.line",
|
||||
inverse_name="sheet_id",
|
||||
string="Timesheets",
|
||||
readonly=True,
|
||||
states={"new": [("readonly", False)], "draft": [("readonly", False)]},
|
||||
)
|
||||
line_ids = fields.One2many(
|
||||
comodel_name="hr_timesheet.sheet.line",
|
||||
compute="_compute_line_ids",
|
||||
string="Timesheet Sheet Lines",
|
||||
readonly=True,
|
||||
states={"new": [("readonly", False)], "draft": [("readonly", False)]},
|
||||
)
|
||||
new_line_ids = fields.One2many(
|
||||
comodel_name="hr_timesheet.sheet.new.analytic.line",
|
||||
inverse_name="sheet_id",
|
||||
string="Temporary Timesheets",
|
||||
readonly=True,
|
||||
states={"new": [("readonly", False)], "draft": [("readonly", False)]},
|
||||
)
|
||||
state = fields.Selection(
|
||||
[
|
||||
("new", "New"),
|
||||
("draft", "Open"),
|
||||
("confirm", "Waiting Review"),
|
||||
("done", "Approved"),
|
||||
],
|
||||
default="new",
|
||||
tracking=True,
|
||||
string="Status",
|
||||
required=True,
|
||||
readonly=True,
|
||||
index=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
comodel_name="res.company",
|
||||
string="Company",
|
||||
default=lambda self: self.env.company,
|
||||
required=True,
|
||||
readonly=True,
|
||||
)
|
||||
review_policy = fields.Selection(
|
||||
selection=lambda self: self._selection_review_policy(),
|
||||
default=lambda self: self._default_review_policy(),
|
||||
required=True,
|
||||
readonly=True,
|
||||
)
|
||||
department_id = fields.Many2one(
|
||||
comodel_name="hr.department",
|
||||
string="Department",
|
||||
default=lambda self: self._default_department_id(),
|
||||
readonly=True,
|
||||
states={"new": [("readonly", False)]},
|
||||
)
|
||||
reviewer_id = fields.Many2one(
|
||||
comodel_name="hr.employee", string="Reviewer", readonly=True, 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 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 = _("Week %(end)s", end=period_end)
|
||||
else:
|
||||
sheet.name = _(
|
||||
"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(
|
||||
_("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.name_get()[0][1]]
|
||||
|
||||
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(
|
||||
_(
|
||||
"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(
|
||||
_(
|
||||
"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(
|
||||
_(
|
||||
"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(
|
||||
_(
|
||||
"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(
|
||||
_(
|
||||
"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._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 = [(6, 0, 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.name_get()
|
||||
value = name[0][1] 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 = [(6, 0, 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 > 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(
|
||||
_(
|
||||
"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(_("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: [(6, 0, 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(
|
||||
_(
|
||||
"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(_("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(_("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(_("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(
|
||||
_(
|
||||
"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(_("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 "{} - {}".format(
|
||||
project_id.name_get()[0][1], task_id.name_get()[0][1]
|
||||
)
|
||||
|
||||
return project_id.name_get()[0][1]
|
||||
|
||||
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 l: l.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: 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 l: self._is_compatible_new_line(l, 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": _("Warning"),
|
||||
"message": _("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)
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
# Copyright 2018 ForgeFlow, S.L.
|
||||
# Copyright 2019 Brainbean Apps (https://brainbeanapps.com)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
from odoo import fields, models
|
||||
|
||||
_WEEKDAYS = [
|
||||
("0", "Monday"),
|
||||
("1", "Tuesday"),
|
||||
("2", "Wednesday"),
|
||||
("3", "Thursday"),
|
||||
("4", "Friday"),
|
||||
("5", "Saturday"),
|
||||
("6", "Sunday"),
|
||||
]
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = "res.company"
|
||||
|
||||
sheet_range = fields.Selection(
|
||||
[("MONTHLY", "Month"), ("WEEKLY", "Week"), ("DAILY", "Day")],
|
||||
string="Timesheet Sheet Range",
|
||||
default="WEEKLY",
|
||||
help="The range of your Timesheet Sheet.",
|
||||
)
|
||||
|
||||
timesheet_week_start = fields.Selection(
|
||||
selection=_WEEKDAYS, string="Week start day", default="0"
|
||||
)
|
||||
|
||||
timesheet_sheet_review_policy = fields.Selection(
|
||||
selection=[
|
||||
("hr", "By HR Officers"),
|
||||
("hr_manager", "By HR Managers"),
|
||||
("timesheet_manager", "By Timesheets Managers"),
|
||||
],
|
||||
default="hr",
|
||||
help="How Timesheet Sheets review is performed.",
|
||||
)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# Copyright 2018 ForgeFlow, S.L.
|
||||
# Copyright 2019 Brainbean Apps (https://brainbeanapps.com)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfig(models.TransientModel):
|
||||
_inherit = "res.config.settings"
|
||||
|
||||
sheet_range = fields.Selection(
|
||||
related="company_id.sheet_range",
|
||||
string="Timesheet Sheet Range",
|
||||
help="The range of your Timesheet Sheet.",
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
timesheet_week_start = fields.Selection(
|
||||
related="company_id.timesheet_week_start",
|
||||
string="Week Start Day",
|
||||
help="Starting day for Timesheet Sheets.",
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
timesheet_sheet_review_policy = fields.Selection(
|
||||
related="company_id.timesheet_sheet_review_policy", readonly=False
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue