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,8 @@
from . import dms_field_mixin
from . import ir_actions_act_window_view
from . import ir_ui_view
from . import dms_access_group
from . import dms_storage
from . import dms_directory
from . import dms_field_template
from . import res_partner

View file

@ -0,0 +1,55 @@
# Copyright 2024 Tecnativa - Víctor Martínez
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class DmsAccessGroups(models.Model):
_inherit = "dms.access.group"
dms_field_ref = fields.Reference(
selection="_selection_reference_value",
string="DMS field reference",
)
company_id = fields.Many2one(
compute="_compute_company_id",
comodel_name="res.company",
string="Company",
store=True,
)
@api.model
def _selection_reference_value(self):
models = (
self.env["ir.model"]
.sudo()
.search([("transient", "=", False)], order="name asc")
)
return [(model.model, model.name) for model in models]
@api.depends("dms_field_ref")
def _compute_company_id(self):
self.company_id = False
for item in self.filtered("dms_field_ref"):
item.company_id = (
item.dms_field_ref.company_id
if "company_id" in item.dms_field_ref._fields
else False
)
def _get_item_from_dms_field_ref(self, record):
return self.env["dms.access.group"].search(
[("dms_field_ref", "=", "%s,%s" % (record._name, record.id))]
)
@api.constrains("dms_field_ref")
def _check_dms_field_ref(self):
for item in self.filtered("dms_field_ref"):
dms_field_ref = "%s,%s" % (item.dms_field_ref._name, item.dms_field_ref.id)
if self.search(
[("dms_field_ref", "=", dms_field_ref), ("id", "!=", item.id)]
):
raise UserError(
_("There is already an access group created for this record.")
)

View file

@ -0,0 +1,166 @@
# Copyright 2020 Creu Blanca
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.osv import expression
class DmsDirectory(models.Model):
_inherit = "dms.directory"
parent_id = fields.Many2one(default=lambda self: self._default_parent())
@api.model
def _default_parent(self):
return self.env.context.get("default_parent_directory_id", False)
@api.constrains("res_id", "is_root_directory", "storage_id", "res_model")
def _check_resource(self):
for directory in self:
if directory.storage_id.save_type == "attachment":
continue
if (
directory.is_root_directory
and directory.storage_id.model_ids
and not directory.res_id
):
raise ValidationError(
_("Directories of this storage must be related to a record")
)
if not directory.res_id:
continue
if self.search(
[
("storage_id", "=", directory.storage_id.id),
("id", "!=", directory.id),
("res_id", "=", directory.res_id),
("res_model", "=", directory.res_model),
],
limit=1,
):
raise ValidationError(
_("This record is already related in this storage")
)
@api.model
def _build_documents_view_directory(self, directory):
return {
"id": "directory_%s" % directory.id,
"text": directory.name,
"icon": "fa fa-folder-o",
"type": "directory",
"data": {"odoo_id": directory.id, "odoo_model": "dms.directory"},
"children": directory.count_elements > 0,
}
@api.model
def _check_parent_field(self):
if self._parent_name not in self._fields:
raise TypeError("The parent (%s) field does not exist." % self._parent_name)
@api.model
def search_read_parents(
self, domain=False, fields=None, offset=0, limit=None, order=None
):
"""This method finds the top level elements of the hierarchy
for a given search query.
:param domain: a search domain <reference/orm/domains> (default: empty list)
:param fields: a list of fields to read (default: all fields of the model)
:param offset: the number of results to ignore (default: none)
:param limit: maximum number of records to return (default: all)
:param order: a string to define the sort order of the query
(default: none)
:returns: the top level elements for the given search query
"""
if not domain:
domain = []
records = self.search_parents(
domain=domain, offset=offset, limit=limit, order=order
)
if not records:
return []
if fields and fields == ["id"]:
return [{"id": record.id} for record in records]
result = records.read(fields)
if len(result) <= 1:
return result
index = {vals["id"]: vals for vals in result}
return [index[record.id] for record in records if record.id in index]
@api.model
def search_parents(
self, domain=False, offset=0, limit=None, order=None, count=False
):
"""This method finds the top level elements of the
hierarchy for a given search query.
:param domain: a search domain <reference/orm/domains> (default: empty list)
:param offset: the number of results to ignore (default: none)
:param limit: maximum number of records to return (default: all)
:param order: a string to define the sort order of the query
(default: none)
:param count: counts and returns the number of matching records
(default: False)
:returns: the top level elements for the given search query
"""
if not domain:
domain = []
res = self._search_parents(
domain=domain, offset=offset, limit=limit, order=order, count=count
)
return res if count else self.browse(res)
@api.model
def _search_parents(
self, domain=False, offset=0, limit=None, order=None, count=False
):
if not domain:
domain = []
self._check_parent_field()
self.check_access_rights("read")
if expression.is_false(self, domain):
return []
query = self._where_calc(domain)
self._apply_ir_rules(query, "read")
from_clause, where_clause, where_clause_arguments = query.get_sql()
parent_where = where_clause and (" WHERE %s" % where_clause) or ""
parent_query = 'SELECT "%s".id FROM ' % self._table + from_clause + parent_where
no_parent_clause = '"{table}"."{field}" IS NULL'.format(
table=self._table, field=self._parent_name
)
no_access_clause = '"{table}"."{field}" NOT IN ({query})'.format(
table=self._table, field=self._parent_name, query=parent_query
)
parent_clause = "({} OR {})".format(no_parent_clause, no_access_clause)
order_by = self._generate_order_by(order, query)
from_clause, where_clause, where_clause_params = query.get_sql()
where_str = (
where_clause
and (" WHERE {} AND {}".format(where_clause, parent_clause))
or (" WHERE %s" % parent_clause)
)
if count:
# pylint: disable=sql-injection
query_str = "SELECT count(1) FROM " + from_clause + where_str
self._cr.execute(query_str, where_clause_params)
return self._cr.fetchone()[0]
limit_str = limit and " limit %s" or ""
offset_str = offset and " offset %s" or ""
query_str = (
'SELECT "%s".id FROM ' % (self._table)
+ from_clause
+ where_str
+ order_by
+ limit_str
+ offset_str
)
complete_where_clause_params = where_clause_params + where_clause_arguments
if limit:
complete_where_clause_params.append(limit)
if offset:
complete_where_clause_params.append(offset)
# pylint: disable=sql-injection
self._cr.execute(query_str, complete_where_clause_params)
return list({x[0] for x in self._cr.fetchall()})

View file

@ -0,0 +1,80 @@
# Copyright 2020 Creu Blanca
# Copyright 2024 Tecnativa - Víctor Martínez
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo import api, fields, models
from odoo.tools import config
class DMSFieldMixin(models.AbstractModel):
_name = "dms.field.mixin"
_description = "Mixin to use DMS Field"
dms_directory_ids = fields.One2many(
"dms.directory",
"res_id",
string="DMS Directories",
domain=lambda self: [
("res_model", "=", self._name),
("storage_id.save_type", "!=", "attachment"),
],
auto_join=True,
)
@api.model
def models_to_track_dms_field_template(self):
"""Models to be tracked for dms field templates
:args:
:returns: list of models
"""
return self.env["dms.field.template"].sudo().search([]).mapped("model_id.model")
@api.model_create_multi
def create(self, vals_list):
"""Create a dms directory when creating the record if exist a template.
We need to avoid applying a template except when testing functionality
with dms_field* modules to avoid the error that a directory with the same
name already exists (example: create partner).
"""
result = super().create(vals_list)
test_condition = not config["test_enable"] or self.env.context.get(
"test_dms_field"
)
if (
test_condition
and not self.env.context.get("skip_track_dms_field_template")
and self._name in self.models_to_track_dms_field_template()
):
template = self.env["dms.field.template"].with_context(res_model=self._name)
for item in result:
template.with_context(res_id=item.id).create_dms_directory()
return result
def write(self, vals):
"""When modifying a record that has linked directories and changing the
user_id field it is necessary to update the auto-generated access group
(name and explicit_user_ids).
"""
res = super().write(vals)
# Apply sudo() in case the user does not have access to the directory
for item in self.sudo().filtered("dms_directory_ids"):
if "user_id" in vals:
template = self.env["dms.field.template"]._get_template_from_model(
item._name
)
if template:
template.sudo()._get_autogenerated_group(item)
return res
def unlink(self):
"""When deleting a record, we also delete the linked directories and the
auto-generated access group.
"""
# Apply sudo() in case the user does not have access to the directory
for record in self.sudo().filtered("dms_directory_ids"):
group = (
self.env["dms.access.group"].sudo()._get_item_from_dms_field_ref(record)
)
record.sudo().dms_directory_ids.unlink()
group.unlink()
return super().unlink()

View file

@ -0,0 +1,202 @@
# Copyright 2024 Tecnativa - Víctor Martínez
# 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 DmsFieldTemplate(models.Model):
_name = "dms.field.template"
_inherit = "dms.field.mixin"
_description = "Dms Field Template"
name = fields.Char(required=True)
company_id = fields.Many2one(
comodel_name="res.company",
string="Company",
store=True,
index=True,
)
storage_id = fields.Many2one(
comodel_name="dms.storage",
domain=[("save_type", "!=", "attachment")],
string="Storage",
)
parent_directory_id = fields.Many2one(
comodel_name="dms.directory",
domain="[('storage_id', '=', storage_id)]",
string="Parent directory",
)
model_id = fields.Many2one(
comodel_name="ir.model",
string="Model",
domain=[("transient", "=", False), ("model", "!=", "dms.field.template")],
index=True,
)
model = fields.Char(
compute="_compute_model", compute_sudo=True, store=True, string="Model name"
)
group_ids = fields.Many2many(
comodel_name="dms.access.group",
string="Groups",
)
user_field_id = fields.Many2one(
comodel_name="ir.model.fields",
domain="[('model_id', '=', model_id),('relation', '=', 'res.users')]",
string="User field",
)
directory_format_name = fields.Char(
string="Directory format name",
default="{{object.display_name}}",
help="""You can set expressions to be used for the directory name,
e.g.: {{object.name}}""",
)
@api.depends("model_id")
def _compute_model(self):
for item in self:
item.model = item.model_id.model
def _get_template_from_model(self, model):
return self.search([("model", "=", model)], limit=1)
@api.model_create_multi
def create(self, vals_list):
"""Create dms directory automatically in the creation in install mode."""
result = super().create(vals_list)
if self.env.context.get("install_mode"):
for item in result:
item_ctx = item.with_context(res_model=item._name, res_id=item.id)
item_ctx.create_dms_directory()
return result
@api.model
def create_dms_directory(self):
"""According to the model, create the directory linked to that record
and the subdirectories."""
res_model = self.env.context.get("res_model")
res_id = self.env.context.get("res_id")
record = self.env[res_model].browse(res_id)
directory_model = self.env["dms.directory"].sudo()
if res_model == "dms.field.template":
return directory_model.create(
{
"storage_id": record.storage_id.id,
"res_id": record.id,
"res_model": record._name,
"is_root_directory": True,
"name": record.display_name,
"group_ids": record.group_ids.ids,
}
)
template = self._get_template_from_model(res_model).sudo()
if not template:
raise ValidationError(_("There is no template linked to this model"))
total_directories = directory_model.search_count(
[
("parent_id", "=", self.parent_directory_id.id),
("res_model", "=", res_model),
("res_id", "=", res_id),
]
)
if total_directories > 0:
raise ValidationError(_("There is already a linked directory created."))
# Create root directory + files
dms_directory_ids = template.dms_directory_ids
new_directory = directory_model.create(
template._prepare_directory_vals(dms_directory_ids, record)
)
self._copy_files_from_directory(dms_directory_ids, new_directory)
# Create child directories
self._create_child_directories(new_directory, dms_directory_ids)
return new_directory
def _copy_files_from_directory(self, directory, new_directory):
for file in directory.file_ids:
file.copy({"directory_id": new_directory.id})
def _prepare_autogenerated_group(self, record):
group_name = _("Autogenerated group from %(model)s (%(name)s) #%(id)s") % {
"model": record._description,
"name": record.display_name,
"id": record.id,
}
vals = {
"name": group_name,
# We need to set all the permissions so that the user can manage their
# documents (directories and files)
"perm_create": True,
"perm_write": True,
"perm_unlink": True,
"dms_field_ref": "%s,%s" % (record._name, record.id),
"explicit_user_ids": [(5, 0)],
}
# Apply sudo() because the user may not have permissions to access
# ir.model.fields.
user_field = self.sudo().user_field_id
if user_field:
user = record[user_field.name]
if user:
vals["explicit_user_ids"] += [(4, user.id)]
return vals
def _get_autogenerated_group(self, record):
"""Get the existing auto-generated group or create a new one.
The permissions of the auto-generated group should be changed
to make sure you have the correct data.
"""
group_model = self.env["dms.access.group"]
group_ref = group_model._get_item_from_dms_field_ref(record)
if group_ref:
group_ref.write(self._prepare_autogenerated_group(record))
return group_ref
# Create the autogenerated group linked to the record
return group_model.create(self._prepare_autogenerated_group(record))
def _create_child_directories(self, parent, directory):
# Create child directories (all leves) + files
directory_model = self.env["dms.directory"].sudo()
for child_directory in directory.child_directory_ids:
child = directory_model.create(
{
"name": child_directory.name,
"is_root_directory": False,
"parent_id": parent.id,
}
)
self._copy_files_from_directory(child_directory, child)
self._create_child_directories(child, child_directory)
def _prepare_directory_vals(self, directory, record):
# Groups of the new directory will be those of the template + auto-generate
groups = directory.group_ids
groups += self._get_autogenerated_group(record)
directory_name = self.env["mail.render.mixin"]._render_template(
self.directory_format_name,
record._name,
record.ids,
engine="inline_template",
)[record.id]
vals = {
"storage_id": directory.storage_id.id,
"res_id": record.id,
"res_model": record._name,
"name": directory_name,
"group_ids": [(4, group.id) for group in groups],
}
if not self.parent_directory_id:
vals.update({"is_root_directory": True})
else:
vals.update(
{"parent_id": self.parent_directory_id.id, "inherit_group_ids": False}
)
return vals
@api.constrains("model_id")
def _check_model_id(self):
for template in self:
if self.env["dms.field.template"].search(
[("model_id", "=", template.model_id.id), ("id", "!=", template.id)]
):
raise UserError(
_("There is already a template created for this model.")
)

View file

@ -0,0 +1,66 @@
# Copyright 2020 Creu Blanca
# Copyright 2024 Tecnativa - Víctor Martínez
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class DmsStorage(models.Model):
_inherit = "dms.storage"
field_template_ids = fields.One2many(
comodel_name="dms.field.template",
inverse_name="storage_id",
string="File templated ids",
)
@api.model
def _build_documents_storage(self, storage):
storage_directories = []
model = self.env["dms.directory"]
directories = model.search_parents([["storage_id", "=", storage.id]])
for record in directories:
storage_directories.append(model._build_documents_view_directory(record))
return {
"id": "storage_%s" % storage.id,
"text": storage.name,
"icon": "fa fa-database",
"type": "storage",
"data": {"odoo_id": storage.id, "odoo_model": "dms.storage"},
"children": storage_directories,
}
@api.model
def get_js_tree_data(self):
return [record._build_documents_storage(record) for record in self.search([])]
@api.constrains("model_ids", "save_type")
def _constrain_model_ids(self):
for storage in self:
if storage.save_type == "attachment":
continue
if self.env["dms.directory"].search(
[
("storage_id", "=", storage.id),
("is_root_directory", "=", True),
(
"res_model",
"not in",
storage.mapped("model_ids.model"),
),
]
):
raise ValidationError(
_("Some directories are inconsistent with the storage models")
)
if storage.model_ids and self.env["dms.directory"].search(
[
("storage_id", "=", storage.id),
("is_root_directory", "=", True),
("res_model", "=", False),
]
):
raise ValidationError(
_("There are directories not associated to a record")
)

View file

@ -0,0 +1,13 @@
# Copyright 2020 Creu Blanca
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo import fields, models
class IrActionsActWindowView(models.Model):
_inherit = "ir.actions.act_window.view"
view_mode = fields.Selection(
selection_add=[("dms_list", "DMS Tree")], ondelete={"dms_list": "cascade"}
)

View file

@ -0,0 +1,38 @@
# Copyright 2020 Creu Blanca
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo import _, fields, models
from odoo.addons.base.models.ir_ui_view import NameManager
class IrUiView(models.Model):
_inherit = "ir.ui.view"
type = fields.Selection(selection_add=[("dms_list", "DMS Tree")])
def _postprocess_tag_dms_list(self, node, name_manager, node_info):
parent = node.getparent()
if parent_name := parent and parent.get("name"):
field = name_manager.model._fields.get(parent_name)
if field:
model_name = field.comodel_name
if model_name not in self.env:
self._raise_view_error(
_("Model not found: %(model)s", model=model_name), node
)
model = self.env[model_name]
new_name_manager = NameManager(model, parent=name_manager)
root_info = {
"view_type": node.tag,
"view_editable": self._editable_node(node, name_manager),
"view_modifiers_from_model": self._modifiers_from_model(node),
}
new_node_info = dict(
root_info,
modifiers={},
editable=self._editable_node(node, new_name_manager),
)
for child in node:
self._postprocess_tag_field(child, new_name_manager, new_node_info)

View file

@ -0,0 +1,9 @@
# Copyright 2024 Tecnativa - Carlos Roca
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from odoo import models
class ResPartner(models.Model):
_name = "res.partner"
_inherit = ["res.partner", "dms.field.mixin"]