mirror of
https://gitlab.com/flectra-community/server-ux.git
synced 2024-11-15 02:32:06 +00:00
324 lines
11 KiB
Python
324 lines
11 KiB
Python
|
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
|
||
|
# Copyright 2021 Opener B.V. (<https://opener.amsterdam>)
|
||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||
|
import logging
|
||
|
|
||
|
from dateutil.relativedelta import relativedelta
|
||
|
from dateutil.rrule import DAILY, MONTHLY, WEEKLY, YEARLY, rrule
|
||
|
|
||
|
from flectra import _, api, fields, models
|
||
|
from flectra.exceptions import UserError, ValidationError
|
||
|
from flectra.tools.safe_eval import safe_eval
|
||
|
|
||
|
_logger = logging.getLogger(__name__)
|
||
|
|
||
|
|
||
|
class DateRangeGenerator(models.TransientModel):
|
||
|
_name = "date.range.generator"
|
||
|
_description = "Date Range Generator"
|
||
|
|
||
|
name_expr = fields.Text(
|
||
|
"Range name expression",
|
||
|
compute="_compute_name_expr",
|
||
|
readonly=False,
|
||
|
store=True,
|
||
|
help=(
|
||
|
"Evaluated expression. E.g. "
|
||
|
"\"'FY%s' % date_start.strftime('%Y%m%d')\"\nYou can "
|
||
|
"use the Date types 'date_end' and 'date_start', as well as "
|
||
|
"the 'index' variable."
|
||
|
),
|
||
|
)
|
||
|
name_prefix = fields.Char(
|
||
|
"Range name prefix",
|
||
|
compute="_compute_name_prefix",
|
||
|
readonly=False,
|
||
|
store=True,
|
||
|
)
|
||
|
range_name_preview = fields.Char(compute="_compute_range_name_preview")
|
||
|
date_start = fields.Date(
|
||
|
"Start date",
|
||
|
compute="_compute_date_start",
|
||
|
readonly=False,
|
||
|
store=True,
|
||
|
required=True,
|
||
|
)
|
||
|
date_end = fields.Date("End date", compute="_compute_date_end", readonly=False)
|
||
|
type_id = fields.Many2one(
|
||
|
comodel_name="date.range.type",
|
||
|
string="Type",
|
||
|
required=True,
|
||
|
domain="['|', ('company_id', '=', company_id), " "('company_id', '=', False)]",
|
||
|
ondelete="cascade",
|
||
|
store=True,
|
||
|
compute="_compute_type_id",
|
||
|
readonly=False,
|
||
|
)
|
||
|
company_id = fields.Many2one(
|
||
|
comodel_name="res.company",
|
||
|
string="Company",
|
||
|
compute="_compute_company_id",
|
||
|
readonly=False,
|
||
|
store=True,
|
||
|
)
|
||
|
unit_of_time = fields.Selection(
|
||
|
[
|
||
|
(str(YEARLY), "years"),
|
||
|
(str(MONTHLY), "months"),
|
||
|
(str(WEEKLY), "weeks"),
|
||
|
(str(DAILY), "days"),
|
||
|
],
|
||
|
compute="_compute_unit_of_time",
|
||
|
readonly=False,
|
||
|
store=True,
|
||
|
required=True,
|
||
|
)
|
||
|
duration_count = fields.Integer(
|
||
|
"Duration",
|
||
|
compute="_compute_duration_count",
|
||
|
readonly=False,
|
||
|
store=True,
|
||
|
required=True,
|
||
|
)
|
||
|
count = fields.Integer(
|
||
|
string="Number of ranges to generate",
|
||
|
)
|
||
|
|
||
|
@api.onchange("date_end")
|
||
|
def onchange_date_end(self):
|
||
|
if self.date_end and self.count:
|
||
|
self.count = 0
|
||
|
|
||
|
@api.onchange("count")
|
||
|
def onchange_count(self):
|
||
|
if self.count and self.date_end:
|
||
|
self.date_end = False
|
||
|
|
||
|
@api.onchange("name_expr")
|
||
|
def onchange_name_expr(self):
|
||
|
"""Wipe the prefix if an expression is entered.
|
||
|
|
||
|
The reverse is not implemented because we don't want to wipe the
|
||
|
users' painstakingly crafted expressions by accident.
|
||
|
"""
|
||
|
if self.name_expr and self.name_prefix:
|
||
|
self.name_prefix = False
|
||
|
|
||
|
@api.depends("company_id", "type_id.company_id")
|
||
|
def _compute_type_id(self):
|
||
|
if (
|
||
|
self.company_id
|
||
|
and self.type_id.company_id
|
||
|
and self.type_id.company_id != self.company_id
|
||
|
):
|
||
|
self.type_id = self.env["date.range.type"]
|
||
|
|
||
|
def _generate_intervals(self, batch=False):
|
||
|
"""Generate a list of dates representing the intervals.
|
||
|
|
||
|
The last date only serves to compute the end date of the last interval.
|
||
|
|
||
|
:param batch: When true, don't raise when there are no ranges to
|
||
|
generate.
|
||
|
"""
|
||
|
if not self.date_end and not self.count:
|
||
|
if batch:
|
||
|
return []
|
||
|
raise ValidationError(
|
||
|
_("Please enter an end date, or the number of ranges to " "generate.")
|
||
|
)
|
||
|
kwargs = dict(
|
||
|
freq=int(self.unit_of_time),
|
||
|
interval=self.duration_count,
|
||
|
dtstart=self.date_start,
|
||
|
)
|
||
|
if self.date_end:
|
||
|
kwargs["until"] = self.date_end
|
||
|
else:
|
||
|
kwargs["count"] = self.count
|
||
|
vals = list(rrule(**kwargs))
|
||
|
if not vals:
|
||
|
raise UserError(_("No ranges to generate with these settings"))
|
||
|
# Generate another interval to fetch the last end date from
|
||
|
vals.append(
|
||
|
list(
|
||
|
rrule(
|
||
|
freq=int(self.unit_of_time),
|
||
|
interval=self.duration_count,
|
||
|
dtstart=vals[-1].date(),
|
||
|
count=2,
|
||
|
)
|
||
|
)[-1]
|
||
|
)
|
||
|
return vals
|
||
|
|
||
|
def generate_names(self, vals):
|
||
|
"""Generate the names for the given intervals"""
|
||
|
self.ensure_one()
|
||
|
return self._generate_names(vals, self.name_expr, self.name_prefix)
|
||
|
|
||
|
@staticmethod
|
||
|
def _generate_names(vals, name_expr, name_prefix):
|
||
|
"""Generate the names for the given intervals and naming parameters"""
|
||
|
names = []
|
||
|
count_digits = len(str(len(vals) - 1))
|
||
|
for idx, dt_start in enumerate(vals[:-1]):
|
||
|
date_start = dt_start.date()
|
||
|
# always remove 1 day for the date_end since range limits are
|
||
|
# inclusive
|
||
|
date_end = vals[idx + 1].date() - relativedelta(days=1)
|
||
|
index = "%0*d" % (count_digits, idx + 1)
|
||
|
if name_expr:
|
||
|
try:
|
||
|
names.append(
|
||
|
safe_eval(
|
||
|
name_expr,
|
||
|
{
|
||
|
"date_end": date_end,
|
||
|
"date_start": date_start,
|
||
|
"index": index,
|
||
|
},
|
||
|
)
|
||
|
)
|
||
|
except (SyntaxError, ValueError) as e:
|
||
|
raise ValidationError(_("Invalid name expression: %s") % e) from e
|
||
|
elif name_prefix:
|
||
|
names.append(name_prefix + index)
|
||
|
else:
|
||
|
raise ValidationError(
|
||
|
_(
|
||
|
"Please set a prefix or an expression to generate "
|
||
|
"the range names."
|
||
|
)
|
||
|
)
|
||
|
return names
|
||
|
|
||
|
@api.depends("name_expr", "name_prefix")
|
||
|
def _compute_range_name_preview(self):
|
||
|
for wiz in self:
|
||
|
preview = False
|
||
|
if wiz.name_expr or wiz.name_prefix:
|
||
|
vals = False
|
||
|
try:
|
||
|
vals = wiz._generate_intervals()
|
||
|
except Exception:
|
||
|
_logger.exception("Something happened generating intervals")
|
||
|
if vals:
|
||
|
names = wiz.generate_names(vals)
|
||
|
if names:
|
||
|
preview = names[0]
|
||
|
wiz.range_name_preview = preview
|
||
|
|
||
|
def _generate_date_ranges(self, batch=False):
|
||
|
"""Actually generate the date ranges."""
|
||
|
self.ensure_one()
|
||
|
vals = self._generate_intervals(batch=batch)
|
||
|
if not vals:
|
||
|
return []
|
||
|
date_ranges = []
|
||
|
names = self.generate_names(vals)
|
||
|
for idx, dt_start in enumerate(vals[:-1]):
|
||
|
date_start = dt_start.date()
|
||
|
date_end = vals[idx + 1].date() - relativedelta(days=1)
|
||
|
date_ranges.append(
|
||
|
{
|
||
|
"name": names[idx],
|
||
|
"date_start": date_start,
|
||
|
"date_end": date_end,
|
||
|
"type_id": self.type_id.id,
|
||
|
"company_id": self.company_id.id,
|
||
|
}
|
||
|
)
|
||
|
return date_ranges
|
||
|
|
||
|
@api.depends("type_id")
|
||
|
def _compute_company_id(self):
|
||
|
if self.type_id:
|
||
|
self.company_id = self.type_id.company_id
|
||
|
else:
|
||
|
self.company_id = self.env.company
|
||
|
|
||
|
@api.depends("type_id")
|
||
|
def _compute_name_expr(self):
|
||
|
if self.type_id.name_expr:
|
||
|
self.name_expr = self.type_id.name_expr
|
||
|
|
||
|
@api.depends("type_id")
|
||
|
def _compute_name_prefix(self):
|
||
|
if self.type_id.name_prefix:
|
||
|
self.name_prefix = self.type_id.name_prefix
|
||
|
|
||
|
@api.depends("type_id")
|
||
|
def _compute_duration_count(self):
|
||
|
if self.type_id.duration_count:
|
||
|
self.duration_count = self.type_id.duration_count
|
||
|
|
||
|
@api.depends("type_id")
|
||
|
def _compute_unit_of_time(self):
|
||
|
if self.type_id.unit_of_time:
|
||
|
self.unit_of_time = self.type_id.unit_of_time
|
||
|
|
||
|
@api.depends("type_id")
|
||
|
def _compute_date_start(self):
|
||
|
if not self.type_id:
|
||
|
return
|
||
|
last = self.env["date.range"].search(
|
||
|
[("type_id", "=", self.type_id.id)], order="date_end desc", limit=1
|
||
|
)
|
||
|
today = fields.Date.context_today(self)
|
||
|
if last:
|
||
|
self.date_start = last.date_end + relativedelta(days=1)
|
||
|
elif self.type_id.autogeneration_date_start:
|
||
|
self.date_start = self.type_id.autogeneration_date_start
|
||
|
else: # default to the beginning of the current year
|
||
|
self.date_start = today.replace(day=1, month=1)
|
||
|
|
||
|
@api.depends("date_start")
|
||
|
def _compute_date_end(self):
|
||
|
if not self.type_id or not self.date_start:
|
||
|
return
|
||
|
if self.type_id.autogeneration_unit and self.type_id.autogeneration_count:
|
||
|
key = {
|
||
|
str(YEARLY): "years",
|
||
|
str(MONTHLY): "months",
|
||
|
str(WEEKLY): "weeks",
|
||
|
str(DAILY): "days",
|
||
|
}[self.type_id.autogeneration_unit]
|
||
|
today = fields.Date.context_today(self)
|
||
|
date_end = today + relativedelta(**{key: self.type_id.autogeneration_count})
|
||
|
if date_end > self.date_start:
|
||
|
self.date_end = date_end
|
||
|
|
||
|
@api.onchange("company_id")
|
||
|
def _onchange_company_id(self):
|
||
|
if (
|
||
|
self.company_id
|
||
|
and self.type_id.company_id
|
||
|
and self.type_id.company_id != self.company_id
|
||
|
):
|
||
|
self._cache.update(self._convert_to_cache({"type_id": False}, update=True))
|
||
|
|
||
|
@api.constrains("company_id", "type_id")
|
||
|
def _check_company_id_type_id(self):
|
||
|
for rec in self.sudo():
|
||
|
if (
|
||
|
rec.company_id
|
||
|
and rec.type_id.company_id
|
||
|
and rec.company_id != rec.type_id.company_id
|
||
|
):
|
||
|
raise ValidationError(
|
||
|
_(
|
||
|
"The Company in the Date Range Generator and in "
|
||
|
"Date Range Type must be the same."
|
||
|
)
|
||
|
)
|
||
|
|
||
|
def action_apply(self, batch=False):
|
||
|
date_ranges = self._generate_date_ranges(batch=batch)
|
||
|
if date_ranges:
|
||
|
for dr in date_ranges:
|
||
|
self.env["date.range"].create(dr)
|
||
|
return self.env["ir.actions.actions"]._for_xml_id(
|
||
|
"date_range.date_range_action"
|
||
|
)
|