# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import collections from ast import literal_eval from collections import defaultdict from lxml import etree from random import randint from odoo import api, fields, models, tools, _ from odoo.exceptions import ValidationError, UserError from odoo.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG class WorksheetTemplate(models.Model): _name = 'worksheet.template' _description = 'Worksheet Template' _order = 'sequence, name' def _get_default_color(self): return randint(1, 11) name = fields.Char(string='Name', required=True) sequence = fields.Integer() worksheet_count = fields.Integer(compute='_compute_worksheet_count', compute_sudo=True) model_id = fields.Many2one('ir.model', ondelete='cascade', readonly=True, domain=[('state', '=', 'manual')]) action_id = fields.Many2one('ir.actions.act_window', readonly=True) company_ids = fields.Many2many('res.company', string='Companies', domain=lambda self: [('id', 'in', self.env.companies.ids)]) report_view_id = fields.Many2one('ir.ui.view', domain=[('type', '=', 'qweb')], readonly=True) color = fields.Integer('Color', default=_get_default_color) active = fields.Boolean(default=True) res_model = fields.Char('Host Model', help="The model that is using this template") def _compute_worksheet_count(self): for record in self: record.worksheet_count = record.model_id and self.env[record.model_id.model].search_count([]) or 0 @api.constrains('report_view_id', 'model_id') def _check_report_view_type(self): for worksheet_template in self: if worksheet_template.model_id and worksheet_template.report_view_id: if worksheet_template.report_view_id.type != 'qweb': raise ValidationError(_('The template to print this worksheet template should be a QWeb template.')) @api.constrains('res_model') def _check_res_model_exists(self): res_models = self.mapped('res_model') ir_model_names = [res['model'] for res in self.env['ir.model'].sudo().search_read([('model', 'in', res_models)], ['model'])] if any(model_name not in ir_model_names for model_name in res_models): raise ValidationError(_('The host model name should be an existing model.')) @api.model_create_multi def create(self, vals_list): templates = super().create(vals_list) if not self.env.context.get('worksheet_no_generation'): for template in templates: template._generate_worksheet_model() return templates def write(self, vals): old_company_ids = self.company_ids res = super().write(vals) if 'company_ids' in vals and self.company_ids: update_company_ids = old_company_ids - self.company_ids template_dict = defaultdict(lambda: self.env['worksheet.template']) for template in self: template_dict[template.res_model] |= template for res_model, templates in template_dict.items(): for model, name in self._get_models_to_check_dict()[res_model]: records = self.env[model].search([('worksheet_template_id', 'in', templates.ids)]) for record in records: if record.company_id not in record.worksheet_template_id.company_ids: if update_company_ids: company_names = ', '.join(update_company_ids.mapped('name')) raise UserError(_("Unfortunately, you cannot unlink this worksheet template from %s because the template is still connected to tasks within the company.", company_names)) else: company_names = ', '.join(record.worksheet_template_id.company_ids.mapped('name')) raise UserError(_("You can't restrict this worksheet template to '%s' because it's still connected to tasks in '%s' (and potentially other companies). Please either unlink those tasks from this worksheet template, " "move them to a project for the right company, or keep this worksheet template open to all companies.", company_names, record.company_id.name)) return res def unlink(self): # When uninstalling module, let the ORM take care of everything. As the # xml ids are correctly generated, all data will be properly removed. if self.env.context.get(MODULE_UNINSTALL_FLAG): return super().unlink() # When manual deletion of worksheet, we need to handle explicitly the removal of depending data models_ids = self.mapped('model_id.id') self.env['ir.ui.view'].search([('model', 'in', self.mapped('model_id.model'))]).unlink() # backend views (form, pivot, ...) self.mapped('report_view_id').unlink() # qweb templates self.env['ir.model.access'].search([('model_id', 'in', models_ids)]).unlink() x_name_fields = self.env['ir.model.fields'].search([('model_id', 'in', models_ids), ('name', '=', 'x_name')]) x_name_fields.write({'related': False}) # we need to manually remove relation to allow the deletion of fields self.env['ir.rule'].search([('model_id', 'in', models_ids)]).unlink() self.mapped('action_id').unlink() # context needed to avoid "manual" removal of related fields self.mapped('model_id').with_context(**{MODULE_UNINSTALL_FLAG: True}).unlink() return super(WorksheetTemplate, self.exists()).unlink() @api.returns('self', lambda value: value.id) def copy(self, default=None): if default is None: default = {} if not default.get('name'): default['name'] = _("%s (copy)", self.name) # force no model default['model_id'] = False template = super(WorksheetTemplate, self.with_context(worksheet_no_generation=True)).copy(default) template._generate_worksheet_model() return template def _generate_worksheet_model(self): self.ensure_one() res_model = self.res_model.replace('.', '_') name = 'x_%s_worksheet_template_%d' % (res_model, self.id) # create access rights and rules if not hasattr(self, f'_get_{res_model}_manager_group'): raise NotImplementedError(f'Method _get_{res_model}_manager_group not implemented on {res_model}') if not hasattr(self, f'_get_{res_model}_user_group'): raise NotImplementedError(f'Method _get_{res_model}_user_group not implemented on {res_model}') if not hasattr(self, f'_get_{res_model}_access_all_groups'): raise NotImplementedError(f'Method _get_{res_model}_access_all_groups not implemented on {res_model}') # while creating model it will initialize the init_models method from create of ir.model # and there is related field of model_id in mail template so it's going to recursive loop while recompute so used flush self.env.flush_all() # Generate xml ids for some records: views, actions and models. This will let the ORM handle # the module uninstallation (removing all data belonging to the module using their xml ids). # NOTE: this is not needed for ir.model.fields, ir.model.access and ir.rule, as they are in # delete 'cascade' mode, so their database entries will removed (no need their xml id). module_name = getattr(self, f'_get_{res_model}_module_name')() xid_values = [] model_counter = collections.Counter() def register_xids(records): for record in records: model_counter[record._name] += 1 xid_values.append({ 'name': "{}_{}_{}".format( name, record._name.replace('.', '_'), model_counter[record._name], ), 'module': module_name, 'model': record._name, 'res_id': record.id, 'noupdate': True, }) return records # generate the ir.model (and so the SQL table) model = register_xids(self.env['ir.model'].sudo().create({ 'name': self.name, 'model': name, 'field_id': self._prepare_default_fields_values() + [ (0, 0, { 'name': 'x_name', 'field_description': 'Name', 'ttype': 'char', 'related': 'x_%s_id.name' % res_model, }), ] })) self.env['ir.model.access'].sudo().create([{ 'name': name + '_manager_access', 'model_id': model.id, 'group_id': getattr(self, '_get_%s_manager_group' % res_model)().id, 'perm_create': True, 'perm_write': True, 'perm_read': True, 'perm_unlink': True, }, { 'name': name + '_user_access', 'model_id': model.id, 'group_id': getattr(self, '_get_%s_user_group' % res_model)().id, 'perm_create': True, 'perm_write': True, 'perm_read': True, 'perm_unlink': True, }]) self.env['ir.rule'].create([{ 'name': name + '_own', 'model_id': model.id, 'domain_force': "[('create_uid', '=', user.id)]", 'groups': [(6, 0, [getattr(self, '_get_%s_user_group' % res_model)().id])] }, { 'name': name + '_all', 'model_id': model.id, 'domain_force': [(1, '=', 1)], 'groups': [(6, 0, getattr(self, '_get_%s_access_all_groups' % res_model)().ids)], }]) # create the view to extend by 'studio' and add the user custom fields __, __, search_view = register_xids(self.env['ir.ui.view'].sudo().create([ self._prepare_default_form_view_values(model), self._prepare_default_tree_view_values(model), self._prepare_default_search_view_values(model) ])) action = register_xids(self.env['ir.actions.act_window'].sudo().create({ 'name': 'Worksheets', 'res_model': model.model, 'search_view_id': search_view.id, 'context': { 'edit': False, 'create': False, 'delete': False, 'duplicate': False, } })) self.env['ir.model.data'].sudo().create(xid_values) # link the worksheet template to its generated model and action self.write({ 'action_id': action.id, 'model_id': model.id, }) # this must be done after form view creation and filling the 'model_id' field self.sudo()._generate_qweb_report_template() # Add unique constraint on the x_model_id field since we want one worksheet per host record conname = '%s_x_%s_id_uniq' % (name, res_model) concode = 'unique(x_%s_id)' % (res_model) tools.add_constraint(self.env.cr, name, conname, concode) def _prepare_default_fields_values(self): """Prepare a list that contains the data to create the default fields for the model created from the template. Fields related to these fields shouldn't be put here, they should be created after the creation of these fields. """ res_model_name = self.res_model.replace('.', '_') fields_func = getattr(self, '_default_%s_template_fields' % res_model_name, False) return [ (0, 0, { 'name': 'x_%s_id' % (res_model_name), 'field_description': self.env[self.res_model]._description, 'ttype': 'many2one', 'relation': self.res_model, 'required': True, 'on_delete': 'cascade', }), (0, 0, { 'name': 'x_comments', 'ttype': 'html', 'field_description': 'Comments', }), ] + (fields_func and fields_func() or []) def _prepare_default_form_view_values(self, model): """Create a default form view for the model created from the template. """ res_model_name = self.res_model.replace('.', '_') form_arch_func = getattr(self, '_default_%s_worksheet_form_arch' % res_model_name, False) return { 'type': 'form', 'name': 'template_view_' + "_".join(self.name.split(' ')), 'model': model.model, 'arch': form_arch_func and form_arch_func() or """
""" % (res_model_name, res_model_name) } def _prepare_default_tree_view_values(self, model): """Create a default list view for the model created from the template.""" res_model_name = self.res_model.replace('.', '_') tree_arch_func = getattr(self, f'_default_{res_model_name}_worksheet_tree_arch', False) return { 'type': 'tree', 'name': 'tree_view_' + self.name.replace(' ', '_'), 'model': model.model, 'arch': tree_arch_func and tree_arch_func() or """