You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
307 lines
13 KiB
307 lines
13 KiB
# -*- coding: utf-8 -*-
|
|
"""This model is used to detect, which all options want to hide from the
|
|
specified group and model"""
|
|
#############################################################################
|
|
#
|
|
# Cybrosys Technologies Pvt. Ltd.
|
|
#
|
|
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
|
|
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>)
|
|
#
|
|
# You can modify it under the terms of the GNU LESSER
|
|
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details.
|
|
#
|
|
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
|
|
# (LGPL v3) along with this program.
|
|
# If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
#############################################################################
|
|
from collections import defaultdict
|
|
from operator import attrgetter
|
|
from odoo import api, _
|
|
from odoo.exceptions import UserError
|
|
from odoo.models import BaseModel, _unlink, LOG_ACCESS_COLUMNS, \
|
|
INSERT_BATCH_SIZE, SQL_DEFAULT
|
|
from odoo.tools import OrderedSet, split_every, clean_context, SQL
|
|
|
|
|
|
@api.model
|
|
def _create(self, data_list):
|
|
""" Create records from the stored field values in ``data_list``. """
|
|
assert data_list
|
|
cr = self.env.cr
|
|
|
|
# insert rows in batches of maximum INSERT_BATCH_SIZE
|
|
ids = [] # ids of created records
|
|
other_fields = OrderedSet() # non-column fields
|
|
|
|
for data_sublist in split_every(INSERT_BATCH_SIZE, data_list):
|
|
stored_list = [data['stored'] for data in data_sublist]
|
|
fnames = sorted({name for stored in stored_list for name in stored})
|
|
|
|
columns = []
|
|
rows = [[] for _ in stored_list]
|
|
for fname in fnames:
|
|
field = self._fields[fname]
|
|
if field.column_type:
|
|
columns.append(fname)
|
|
for stored, row in zip(stored_list, rows):
|
|
if fname in stored:
|
|
colval = field.convert_to_column(stored[fname], self, stored)
|
|
if field.translate is True and colval:
|
|
if 'en_US' not in colval.adapted:
|
|
colval.adapted['en_US'] = next(iter(colval.adapted.values()))
|
|
row.append(colval)
|
|
else:
|
|
row.append(SQL_DEFAULT)
|
|
else:
|
|
other_fields.add(field)
|
|
|
|
if field.type == 'properties':
|
|
# force calling fields.create for properties field because
|
|
# we might want to update the parent definition
|
|
other_fields.add(field)
|
|
|
|
if not columns:
|
|
# manage the case where we create empty records
|
|
columns = ['id']
|
|
for row in rows:
|
|
row.append(SQL_DEFAULT)
|
|
|
|
cr.execute(SQL(
|
|
'INSERT INTO %s (%s) VALUES %s RETURNING "id"',
|
|
SQL.identifier(self._table),
|
|
SQL(', ').join(map(SQL.identifier, columns)),
|
|
SQL(', ').join(tuple(row) for row in rows),
|
|
))
|
|
ids.extend(id_ for id_, in cr.fetchall())
|
|
|
|
# put the new records in cache, and update inverse fields, for many2one
|
|
#
|
|
# cachetoclear is an optimization to avoid modified()'s cost until other_fields are processed
|
|
cachetoclear = []
|
|
records = self.browse(ids)
|
|
inverses_update = defaultdict(list) # {(field, value): ids}
|
|
common_set_vals = set(LOG_ACCESS_COLUMNS + ['id', 'parent_path'])
|
|
for data, record in zip(data_list, records):
|
|
data['record'] = record
|
|
# DLE P104: test_inherit.py, test_50_search_one2many
|
|
vals = dict({k: v for d in data['inherited'].values() for k, v in d.items()}, **data['stored'])
|
|
set_vals = common_set_vals.union(vals)
|
|
for field in self._fields.values():
|
|
if field.type in ('one2many', 'many2many'):
|
|
self.env.cache.set(record, field, ())
|
|
elif field.related and not field.column_type:
|
|
self.env.cache.set(record, field, field.convert_to_cache(None, record))
|
|
# DLE P123: `test_adv_activity`, `test_message_assignation_inbox`, `test_message_log`, `test_create_mail_simple`, ...
|
|
# Set `mail.message.parent_id` to False in cache so it doesn't do the useless SELECT when computing the modified of `child_ids`
|
|
# in other words, if `parent_id` is not set, no other message `child_ids` are impacted.
|
|
# + avoid the fetch of fields which are False. e.g. if a boolean field is not passed in vals and as no default set in the field attributes,
|
|
# then we know it can be set to False in the cache in the case of a create.
|
|
elif field.store and field.name not in set_vals and not field.compute:
|
|
self.env.cache.set(record, field, field.convert_to_cache(None, record))
|
|
for fname, value in vals.items():
|
|
field = self._fields[fname]
|
|
if field.type in ('one2many', 'many2many'):
|
|
cachetoclear.append((record, field))
|
|
else:
|
|
cache_value = field.convert_to_cache(value, record)
|
|
self.env.cache.set(record, field, cache_value)
|
|
if field.type in ('many2one', 'many2one_reference') and self.pool.field_inverses[field]:
|
|
inverses_update[(field, cache_value)].append(record.id)
|
|
|
|
for (field, value), record_ids in inverses_update.items():
|
|
field._update_inverses(self.browse(record_ids), value)
|
|
|
|
# update parent_path
|
|
records._parent_store_create()
|
|
|
|
# protect fields being written against recomputation
|
|
protected = [(data['protected'], data['record']) for data in data_list]
|
|
with self.env.protecting(protected):
|
|
# mark computed fields as todo
|
|
records.modified(self._fields, create=True)
|
|
|
|
if other_fields:
|
|
# discard default values from context for other fields
|
|
others = records.with_context(clean_context(self._context))
|
|
for field in sorted(other_fields, key=attrgetter('_sequence')):
|
|
field.create([
|
|
(other, data['stored'][field.name])
|
|
for other, data in zip(others, data_list)
|
|
if field.name in data['stored']
|
|
])
|
|
|
|
# mark fields to recompute
|
|
records.modified([field.name for field in other_fields], create=True)
|
|
|
|
# if value in cache has not been updated by other_fields, remove it
|
|
for record, field in cachetoclear:
|
|
if self.env.cache.contains(record, field) and not self.env.cache.get(record, field):
|
|
self.env.cache.remove(record, field)
|
|
|
|
# check Python constraints for stored fields
|
|
records._validate_fields(name for data in data_list for name in data['stored'])
|
|
records.check_access_rule('create')
|
|
# This is used to restrict the access right to create a record
|
|
current_model_id = self.env['ir.model'].sudo().search(
|
|
[('model', '=', self._name)]).id
|
|
access_right_rec = self.env['access.right'].sudo().search_read(
|
|
[('model_id', '=', current_model_id)],
|
|
['model_id', 'is_create_or_update',
|
|
'groups_id'])
|
|
if access_right_rec and not self.env.is_admin():
|
|
for rec in access_right_rec:
|
|
group_name = self.env['ir.model.data'].sudo().search([
|
|
('model', '=', 'res.groups'),
|
|
('res_id', '=', rec['groups_id'][0])
|
|
]).name
|
|
module_name = self.env['ir.model.data'].sudo().search([
|
|
('model', '=', 'res.groups'),
|
|
('res_id', '=', rec['groups_id'][0])
|
|
]).module
|
|
group = module_name + "." + group_name
|
|
if self.env.user.has_group(group):
|
|
if rec['is_create_or_update']:
|
|
raise UserError('You are restricted from performing this'
|
|
' operation. Please contact the'
|
|
' administrator.')
|
|
return records
|
|
|
|
|
|
@api.model
|
|
def unlink(self):
|
|
""" unlink()
|
|
|
|
Deletes the records in ``self``.
|
|
|
|
:raise AccessError: if the user is not allowed to delete all the given records
|
|
:raise UserError: if the record is default property for other records
|
|
"""
|
|
if not self:
|
|
return True
|
|
|
|
self.check_access_rights('unlink')
|
|
self.check_access_rule('unlink')
|
|
|
|
from odoo.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG
|
|
for func in self._ondelete_methods:
|
|
# func._ondelete is True if it should be called during uninstallation
|
|
if func._ondelete or not self._context.get(MODULE_UNINSTALL_FLAG):
|
|
func(self)
|
|
|
|
# TOFIX: this avoids an infinite loop when trying to recompute a
|
|
# field, which triggers the recomputation of another field using the
|
|
# same compute function, which then triggers again the computation
|
|
# of those two fields
|
|
for field in self._fields.values():
|
|
self.env.remove_to_compute(field, self)
|
|
|
|
self.env.flush_all()
|
|
|
|
cr = self._cr
|
|
Data = self.env['ir.model.data'].sudo().with_context({})
|
|
Defaults = self.env['ir.default'].sudo()
|
|
Property = self.env['ir.property'].sudo()
|
|
Attachment = self.env['ir.attachment'].sudo()
|
|
ir_property_unlink = Property
|
|
ir_model_data_unlink = Data
|
|
ir_attachment_unlink = Attachment
|
|
|
|
# mark fields that depend on 'self' to recompute them after 'self' has
|
|
# been deleted (like updating a sum of lines after deleting one line)
|
|
with self.env.protecting(self._fields.values(), self):
|
|
self.modified(self._fields, before=True)
|
|
|
|
for sub_ids in cr.split_for_in_conditions(self.ids):
|
|
records = self.browse(sub_ids)
|
|
|
|
# Check if the records are used as default properties.
|
|
refs = [f'{self._name},{id_}' for id_ in sub_ids]
|
|
default_properties = Property.search(
|
|
[('res_id', '=', False), ('value_reference', 'in', refs)])
|
|
if not self._context.get(MODULE_UNINSTALL_FLAG) and default_properties:
|
|
raise UserError(
|
|
_('Unable to delete this document because it is used as a default property'))
|
|
else:
|
|
ir_property_unlink |= default_properties
|
|
|
|
# Delete the records' properties.
|
|
ir_property_unlink |= Property.search([('res_id', 'in', refs)])
|
|
|
|
cr.execute(SQL(
|
|
"DELETE FROM %s WHERE id IN %s",
|
|
SQL.identifier(self._table), sub_ids,
|
|
))
|
|
|
|
# Removing the ir_model_data reference if the record being deleted
|
|
# is a record created by xml/csv file, as these are not connected
|
|
# with real database foreign keys, and would be dangling references.
|
|
#
|
|
# Note: the following steps are performed as superuser to avoid
|
|
# access rights restrictions, and with no context to avoid possible
|
|
# side-effects during admin calls.
|
|
data = Data.search(
|
|
[('model', '=', self._name), ('res_id', 'in', sub_ids)])
|
|
ir_model_data_unlink |= data
|
|
|
|
# For the same reason, remove the defaults having some of the
|
|
# records as value
|
|
Defaults.discard_records(records)
|
|
|
|
# For the same reason, remove the relevant records in ir_attachment
|
|
# (the search is performed with sql as the search method of
|
|
# ir_attachment is overridden to hide attachments of deleted
|
|
# records)
|
|
cr.execute(SQL(
|
|
"SELECT id FROM ir_attachment WHERE res_model=%s AND res_id IN %s",
|
|
self._name, sub_ids,
|
|
))
|
|
ir_attachment_unlink |= Attachment.browse(
|
|
row[0] for row in cr.fetchall())
|
|
|
|
# invalidate the *whole* cache, since the orm does not handle all
|
|
# changes made in the database, like cascading delete!
|
|
self.env.invalidate_all(flush=False)
|
|
if ir_property_unlink:
|
|
ir_property_unlink.unlink()
|
|
if ir_model_data_unlink:
|
|
ir_model_data_unlink.unlink()
|
|
if ir_attachment_unlink:
|
|
ir_attachment_unlink.unlink()
|
|
|
|
# auditing: deletions are infrequent and leave no trace in the database
|
|
_unlink.info('User #%s deleted %s records with IDs: %r', self._uid,
|
|
self._name, self.ids)
|
|
# This is used to restrict the access right to unlink a record
|
|
current_model_id = self.env['ir.model'].sudo().search(
|
|
[('model', '=', self._name)]).id
|
|
access_right_rec = self.env['access.right'].sudo().search_read(
|
|
[('model_id', '=', current_model_id)], ['model_id', 'is_delete',
|
|
'groups_id'])
|
|
if access_right_rec and not self.env.is_admin():
|
|
for rec in access_right_rec:
|
|
group_name = self.env['ir.model.data'].sudo().search([
|
|
('model', '=', 'res.groups'),
|
|
('res_id', '=', rec['groups_id'][0])
|
|
]).name
|
|
module_name = self.env['ir.model.data'].sudo().search([
|
|
('model', '=', 'res.groups'),
|
|
('res_id', '=', rec['groups_id'][0])
|
|
]).module
|
|
group = module_name + "." + group_name
|
|
if self.env.user.has_group(group):
|
|
if rec['is_delete']:
|
|
raise UserError(_('You are restricted from performing this'
|
|
' operation. Please contact the'
|
|
' administrator.'))
|
|
return True
|
|
|
|
|
|
BaseModel._create = _create
|
|
BaseModel.unlink = unlink
|
|
|