Initial commit: OCA Technical packages (595 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:03 +02:00
commit 2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions

View file

@ -0,0 +1,3 @@
from . import date_range_type
from . import date_range
from . import date_range_search_mixin

View file

@ -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)]

View file

@ -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

View file

@ -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)
)