mirror of
https://github.com/bringout/oca-technical.git
synced 2026-04-18 16:52:05 +02:00
Initial commit: OCA Technical packages (595 packages)
This commit is contained in:
commit
2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions
|
|
@ -0,0 +1,3 @@
|
|||
from . import date_range_type
|
||||
from . import date_range
|
||||
from . import date_range_search_mixin
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class DateRange(models.Model):
|
||||
_name = "date.range"
|
||||
_description = "Date Range"
|
||||
_order = "type_name, date_start"
|
||||
_check_company_auto = True
|
||||
|
||||
@api.model
|
||||
def _default_company(self):
|
||||
return self.env.company
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
date_start = fields.Date(string="Start date", required=True)
|
||||
date_end = fields.Date(string="End date", required=True)
|
||||
type_id = fields.Many2one(
|
||||
comodel_name="date.range.type",
|
||||
string="Type",
|
||||
index=1,
|
||||
required=True,
|
||||
ondelete="restrict",
|
||||
domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]",
|
||||
check_company=True,
|
||||
)
|
||||
type_name = fields.Char(related="type_id.name", store=True, string="Type Name")
|
||||
company_id = fields.Many2one(
|
||||
comodel_name="res.company", string="Company", index=1, default=_default_company
|
||||
)
|
||||
active = fields.Boolean(
|
||||
help="The active field allows you to hide the date range without "
|
||||
"removing it.",
|
||||
default=True,
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"date_range_uniq",
|
||||
"unique (name,type_id, company_id)",
|
||||
"A date range must be unique per company !",
|
||||
)
|
||||
]
|
||||
|
||||
@api.constrains("type_id", "date_start", "date_end", "company_id")
|
||||
def _validate_range(self):
|
||||
for this in self:
|
||||
if this.date_start > this.date_end:
|
||||
raise ValidationError(
|
||||
_("%(name)s is not a valid range (%(date_start)s > %(date_end)s)")
|
||||
% {
|
||||
"name": this.name,
|
||||
"date_start": this.date_start,
|
||||
"date_end": this.date_end,
|
||||
}
|
||||
)
|
||||
if this.type_id.allow_overlap:
|
||||
continue
|
||||
# here we use a plain SQL query to benefit of the daterange
|
||||
# function available in PostgresSQL
|
||||
# (http://www.postgresql.org/docs/current/static/rangetypes.html)
|
||||
SQL = """
|
||||
SELECT
|
||||
id
|
||||
FROM
|
||||
date_range dt
|
||||
WHERE
|
||||
DATERANGE(dt.date_start, dt.date_end, '[]') &&
|
||||
DATERANGE(%s::date, %s::date, '[]')
|
||||
AND dt.id != %s
|
||||
AND dt.active
|
||||
AND dt.company_id = %s
|
||||
AND dt.type_id=%s;"""
|
||||
self.env.cr.execute(
|
||||
SQL,
|
||||
(
|
||||
this.date_start,
|
||||
this.date_end,
|
||||
this.id,
|
||||
this.company_id.id or None,
|
||||
this.type_id.id,
|
||||
),
|
||||
)
|
||||
res = self.env.cr.fetchall()
|
||||
if res:
|
||||
dt = self.browse(res[0][0])
|
||||
raise ValidationError(
|
||||
_("%(thisname)s overlaps %(dtname)s")
|
||||
% {"thisname": this.name, "dtname": dt.name}
|
||||
)
|
||||
|
||||
def get_domain(self, field_name):
|
||||
self.ensure_one()
|
||||
return [(field_name, ">=", self.date_start), (field_name, "<=", self.date_end)]
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
# Copyright 2021 Opener B.V. <stefan@opener.amsterdam>
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
from lxml import etree
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.osv.expression import FALSE_DOMAIN, NEGATIVE_TERM_OPERATORS, TRUE_DOMAIN
|
||||
|
||||
|
||||
class DateRangeSearchMixin(models.AbstractModel):
|
||||
_name = "date.range.search.mixin"
|
||||
_description = "Mixin class to add a Many2one style period search field"
|
||||
_date_range_search_field = "date"
|
||||
|
||||
date_range_search_id = fields.Many2one(
|
||||
comodel_name="date.range",
|
||||
string="Filter by period (technical field)",
|
||||
compute="_compute_date_range_search_id",
|
||||
search="_search_date_range_search_id",
|
||||
)
|
||||
|
||||
def _compute_date_range_search_id(self):
|
||||
"""Assign a dummy value for this search field"""
|
||||
for record in self:
|
||||
record.date_range_search_id = False
|
||||
|
||||
@api.model
|
||||
def _search_date_range_search_id(self, operator, value):
|
||||
"""Map the selected date ranges to the model's date field"""
|
||||
# Deal with some bogus values
|
||||
if not value:
|
||||
if operator in NEGATIVE_TERM_OPERATORS:
|
||||
return TRUE_DOMAIN
|
||||
return FALSE_DOMAIN
|
||||
if value is True:
|
||||
if operator in NEGATIVE_TERM_OPERATORS:
|
||||
return FALSE_DOMAIN
|
||||
return TRUE_DOMAIN
|
||||
# Assume from here on that the value is a string,
|
||||
# a single id or a list of ids
|
||||
ranges = self.env["date.range"]
|
||||
if isinstance(value, str):
|
||||
ranges = self.env["date.range"].search([("name", operator, value)])
|
||||
else:
|
||||
if isinstance(value, int):
|
||||
value = [value]
|
||||
sub_op = "not in" if operator in NEGATIVE_TERM_OPERATORS else "in"
|
||||
ranges = self.env["date.range"].search([("id", sub_op, value)])
|
||||
if not ranges:
|
||||
return FALSE_DOMAIN
|
||||
domain = (len(ranges) - 1) * ["|"] + sum(
|
||||
(
|
||||
[
|
||||
"&",
|
||||
(self._date_range_search_field, ">=", date_range.date_start),
|
||||
(self._date_range_search_field, "<=", date_range.date_end),
|
||||
]
|
||||
for date_range in ranges
|
||||
),
|
||||
[],
|
||||
)
|
||||
return domain
|
||||
|
||||
@api.model
|
||||
def get_view(self, view_id=None, view_type="form", **options):
|
||||
"""Inject the dummy Many2one field in the search view"""
|
||||
result = super().get_view(view_id=view_id, view_type=view_type, **options)
|
||||
if view_type != "search":
|
||||
return result
|
||||
root = etree.fromstring(result["arch"])
|
||||
if root.xpath("//field[@name='date_range_search_id']"):
|
||||
# Field was inserted explicitely
|
||||
return result
|
||||
separator = etree.Element("separator")
|
||||
field = etree.Element(
|
||||
"field",
|
||||
attrib={
|
||||
"name": "date_range_search_id",
|
||||
"string": _("Period"),
|
||||
},
|
||||
)
|
||||
groups = root.xpath("/search/group")
|
||||
if groups:
|
||||
groups[0].addprevious(separator)
|
||||
groups[0].addprevious(field)
|
||||
else:
|
||||
search = root.xpath("/search")
|
||||
search[0].append(separator)
|
||||
search[0].append(field)
|
||||
result["arch"] = etree.tostring(root, encoding="unicode")
|
||||
return result
|
||||
|
||||
@api.model
|
||||
def get_views(self, views, options=None):
|
||||
"""Adapt the label of the dummy search field
|
||||
|
||||
Ensure the technical name does not show up in the Custom Filter
|
||||
fields list (while still showing up in the Export widget)
|
||||
"""
|
||||
result = super().get_views(views, options=options)
|
||||
if "date_range_search_id" in result["models"][self._name]:
|
||||
result["models"][self._name]["date_range_search_id"]["string"] = _("Period")
|
||||
return result
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
# Copyright 2016 ACSONE SA/NV (<http://acsone.eu>)
|
||||
# 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
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class DateRangeType(models.Model):
|
||||
_name = "date.range.type"
|
||||
_description = "Date Range Type"
|
||||
|
||||
@api.model
|
||||
def _default_company(self):
|
||||
return self.env.company
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
allow_overlap = fields.Boolean(
|
||||
help="If set, date ranges of same type must not overlap.", default=False
|
||||
)
|
||||
active = fields.Boolean(
|
||||
help="The active field allows you to hide the date range type "
|
||||
"without removing it.",
|
||||
default=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
comodel_name="res.company", string="Company", index=1, default=_default_company
|
||||
)
|
||||
date_range_ids = fields.One2many("date.range", "type_id", string="Ranges")
|
||||
date_ranges_exist = fields.Boolean(compute="_compute_date_ranges_exist")
|
||||
|
||||
# Defaults for generating date ranges
|
||||
name_expr = fields.Text(
|
||||
"Range name expression",
|
||||
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."
|
||||
),
|
||||
)
|
||||
range_name_preview = fields.Char(compute="_compute_range_name_preview", store=True)
|
||||
name_prefix = fields.Char("Range name prefix")
|
||||
duration_count = fields.Integer("Duration")
|
||||
unit_of_time = fields.Selection(
|
||||
[
|
||||
(str(YEARLY), "years"),
|
||||
(str(MONTHLY), "months"),
|
||||
(str(WEEKLY), "weeks"),
|
||||
(str(DAILY), "days"),
|
||||
]
|
||||
)
|
||||
autogeneration_date_start = fields.Date(
|
||||
string="Autogeneration Start Date",
|
||||
help="Only applies when there are no date ranges of this type yet",
|
||||
)
|
||||
autogeneration_count = fields.Integer()
|
||||
autogeneration_unit = fields.Selection(
|
||||
[
|
||||
(str(YEARLY), "years"),
|
||||
(str(MONTHLY), "months"),
|
||||
(str(WEEKLY), "weeks"),
|
||||
(str(DAILY), "days"),
|
||||
]
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"date_range_type_uniq",
|
||||
"unique (name,company_id)",
|
||||
"A date range type must be unique per company !",
|
||||
)
|
||||
]
|
||||
|
||||
@api.constrains("company_id")
|
||||
def _check_company_id(self):
|
||||
if not self.env.context.get("bypass_company_validation", False):
|
||||
for rec in self.sudo():
|
||||
if not rec.company_id:
|
||||
continue
|
||||
if bool(
|
||||
rec.date_range_ids.filtered(
|
||||
lambda r: r.company_id and r.company_id != rec.company_id
|
||||
)
|
||||
):
|
||||
raise ValidationError(
|
||||
_(
|
||||
"You cannot change the company, as this "
|
||||
"Date Range Type is assigned to Date Range '%s'."
|
||||
)
|
||||
% (rec.date_range_ids.display_name)
|
||||
)
|
||||
|
||||
@api.depends("name_expr", "name_prefix")
|
||||
def _compute_range_name_preview(self):
|
||||
year_start = fields.Datetime.now().replace(day=1, month=1)
|
||||
next_year = year_start + relativedelta(years=1)
|
||||
for dr_type in self:
|
||||
if dr_type.name_expr or dr_type.name_prefix:
|
||||
names = self.env["date.range.generator"]._generate_names(
|
||||
[year_start, next_year], dr_type.name_expr, dr_type.name_prefix
|
||||
)
|
||||
dr_type.range_name_preview = names[0]
|
||||
else:
|
||||
dr_type.range_name_preview = False
|
||||
|
||||
@api.depends("date_range_ids")
|
||||
def _compute_date_ranges_exist(self):
|
||||
for dr_type in self:
|
||||
dr_type.date_ranges_exist = bool(dr_type.date_range_ids)
|
||||
|
||||
@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.model
|
||||
def autogenerate_ranges(self):
|
||||
"""Generate ranges for types with autogeneration settings"""
|
||||
logger = logging.getLogger(__name__)
|
||||
for dr_type in self.search(
|
||||
[
|
||||
("autogeneration_count", "!=", False),
|
||||
("autogeneration_unit", "!=", False),
|
||||
("duration_count", "!=", False),
|
||||
("unit_of_time", "!=", False),
|
||||
]
|
||||
):
|
||||
try:
|
||||
wizard = self.env["date.range.generator"].new({"type_id": dr_type.id})
|
||||
if not wizard.date_end:
|
||||
# Nothing to generate
|
||||
continue
|
||||
with self.env.cr.savepoint():
|
||||
wizard.action_apply(batch=True)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Error autogenerating ranges for date range type "
|
||||
"%s: %s" % (dr_type.name, e)
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue