class SyncJobQueue(models.Model): _name = "sync.job.queue" _order = "id asc" def _default_name(self): return str(uuid.uuid4()) def _default_user(self): return self.env.user name = fields.Char('Name', default=_default_name) user_id = fields.Many2one('res.users', 'User', default=_default_user) active = fields.Boolean('Active', default=True) state = fields.Selection( (('P', u'Paused'), ('Q', u'Queued'), ('R', u'Running'), ('D', u'Done'), ('F', u'Failed')), default='P', string='State', help='Process State') type = fields.Selection((('1', u'Export'), ('2', u'Import')), default='1', string='State', help='Type') source = fields.Reference([('res.users', 'User'), ('res.partner', 'Contact'), ('hr.payslip.run', 'Payslip Run')], string='Source') method = fields.Char('Method') params = fields.Char('Params dict') date_processed = fields.Datetime('Date Processed') @api.model def sync_job_cron(self): _logger.info("Sync Job Cron - Started by cron") #how many jobs will I process? sync_job_qty = 1 icp_obj = self.env['ir.config_parameter'] try: sync_ob_qty = int(icp_obj.get_param('sync_job_qty')) except Exception as e: _logger.info( "Joberror no se encuentra config param sync_job_qty , %s", str(e)) jobs = self.search([('state', '=', 'Q'), ('active', '=', True)], order='id asc', limit=sync_job_qty) for job in jobs: source = job.source method = job.method sync_method = getattr(source, method) try: job.state = 'R' res = sync_method(job.name, job.id) job.state = 'D' except Exception as e: _logger.info("Joberror Job name %s failed with message %s", job.name, str(e)) job.state = 'F' _logger.info("Sync Job Cron - Ended by cron")
class Image(models.Model): _name = "base_multi_image.image" _order = "sequence, owner_model, owner_id, id" _sql_constraints = [ ('uniq_name_owner', 'UNIQUE(owner_id, owner_model, name)', _('A document can have only one image with the same name.')), ] owner_id = fields.Integer( "Owner", required=True, ondelete="cascade", # This Integer is really a split Many2one ) owner_model = fields.Char(required=True) owner_ref_id = fields.Reference( selection="_selection_owner_ref_id", string="Referenced Owner", compute="_compute_owner_ref_id", store=True, ) storage = fields.Selection([('url', 'URL'), ('file', 'OS file'), ('db', 'Database'), ('filestore', 'Filestore')], required=True) name = fields.Char('Image title', translate=True) filename = fields.Char() extension = fields.Char('File extension', readonly=True) attachment_id = fields.Many2one('ir.attachment', string='Attachment', domain="[('index_content', '=', 'image')]") file_db_store = fields.Binary('Image stored in database', filters='*.png,*.jpg,*.gif') path = fields.Char("Image path", help="Image path") url = fields.Char('Image remote URL') image_main = fields.Binary("Full-sized image", compute="_get_image") image_medium = fields.Binary( "Medium-sized image", compute="_get_image_sizes", help="Medium-sized image. It is automatically resized as a " "128 x 128 px image, with aspect ratio preserved, only when the " "image exceeds one of those sizes. Use this field in form views " "or kanban views.") image_small = fields.Binary( "Small-sized image", compute="_get_image_sizes", help="Small-sized image. It is automatically resized as a 64 x 64 px " "image, with aspect ratio preserved. Use this field anywhere a " "small image is required.") comments = fields.Text('Comments', translate=True) sequence = fields.Integer(default=10) show_technical = fields.Boolean(compute="_show_technical") @api.model @tools.ormcache("self") def _selection_owner_ref_id(self): """Allow any model; after all, this field is readonly.""" return [(r.model, r.name) for r in self.env["ir.model"].search([])] @api.multi @api.depends("owner_model", "owner_id") def _compute_owner_ref_id(self): """Get a reference field based on the split model and id fields.""" for s in self: s.owner_ref_id = "{0.owner_model},{0.owner_id}".format(s) @api.multi @api.depends('storage', 'path', 'file_db_store', 'url') def _get_image(self): """Get image data from the right storage type.""" for s in self: s.image_main = getattr(s, "_get_image_from_%s" % s.storage)() @api.multi @api.depends("owner_id", "owner_model") def _show_technical(self): """Know if you need to show the technical fields.""" self.show_technical = all("default_owner_%s" % f not in self.env.context for f in ("id", "model")) @api.multi def _get_image_from_filestore(self): return self.attachment_id.datas @api.multi def _get_image_from_db(self): return self.file_db_store @api.multi def _get_image_from_file(self): if self.path and os.path.exists(self.path): try: with open(self.path, 'rb') as f: return base64.b64encode(f.read()) except Exception as e: _logger.error("Can not open the image %s, error : %s", self.path, e, exc_info=True) else: _logger.error("The image %s doesn't exist ", self.path) return False @api.multi def _get_image_from_url(self): return self._get_image_from_url_cached(self.url) @api.model @tools.ormcache("url") def _get_image_from_url_cached(self, url): """Allow to download an image and cache it by its URL.""" if url: try: (filename, header) = urllib.urlretrieve(url) with open(filename, 'rb') as f: return base64.b64encode(f.read()) except: _logger.error("URL %s cannot be fetched", url, exc_info=True) return False @api.multi @api.depends('image_main') def _get_image_sizes(self): for s in self: try: vals = tools.image_get_resized_images( s.with_context(bin_size=False).image_main) except: vals = {"image_medium": False, "image_small": False} s.update(vals) @api.model def _make_name_pretty(self, name): return name.replace('_', ' ').capitalize() @api.onchange('url') def _onchange_url(self): if self.url: filename = self.url.split('/')[-1] self.name, self.extension = os.path.splitext(filename) self.name = self._make_name_pretty(self.name) @api.onchange('path') def _onchange_path(self): if self.path: self.name, self.extension = os.path.splitext( os.path.basename(self.path)) self.name = self._make_name_pretty(self.name) @api.onchange('filename') def _onchange_filename(self): if self.filename: self.name, self.extension = os.path.splitext(self.filename) self.name = self._make_name_pretty(self.name) @api.onchange('attachment_id') def _onchange_attachmend_id(self): if self.attachment_id: self.name = self.attachment_id.res_name @api.constrains('storage', 'url') def _check_url(self): if self.storage == 'url' and not self.url: raise exceptions.ValidationError( _('You must provide an URL for the image.')) @api.constrains('storage', 'path') def _check_path(self): if self.storage == 'file' and not self.path: raise exceptions.ValidationError( _('You must provide a file path for the image.')) @api.constrains('storage', 'file_db_store') def _check_store(self): if self.storage == 'db' and not self.file_db_store: raise exceptions.ValidationError( _('You must provide an attached file for the image.')) @api.constrains('storage', 'attachment_id') def _check_attachment_id(self): if self.storage == 'filestore' and not self.attachment_id: raise exceptions.ValidationError( _('You must provide an attachment for the image.'))
class QueueJobChunk(models.Model): _name = "queue.job.chunk" _description = "Queue Job Chunk" _inherit = "collection.base" @api.model def _selection_target_model(self): models = self.env["ir.model"].search([]) return [(model.model, model.name) for model in models] @api.depends("model_name", "record_id") def _compute_reference(self): for rec in self: rec.company_id = self.env.user.company_id if rec.model_name and rec.record_id: rec.reference = "{},{}".format(rec.model_name, rec.record_id or 0) record = self.env[rec.model_name].browse(rec.record_id) if "company_id" in record._fields: rec.company_id = record.company_id else: rec.reference = False # component fields usage = fields.Char("Usage") apply_on_model = fields.Char("Apply on model") data_str = fields.Text(string="Editable data") state = fields.Selection( [("pending", "Pending"), ("done", "Done"), ("fail", "Failed")], default="pending", string="State", ) state_info = fields.Text("Additional state information") model_name = fields.Char("Model ID") record_id = fields.Integer("Record ID") reference = fields.Reference( string="Reference", selection="_selection_target_model", compute=_compute_reference, store=True, ) company_id = fields.Many2one("res.company", compute=_compute_reference, store=True) @api.model_create_multi def create(self, vals): result = super().create(vals) for rec in result: rec.enqueue_job() return result def button_retry(self): self.enqueue_job() def enqueue_job(self): if DEBUG_MODE: return self.process_chunk() else: return self.with_delay().process_chunk() def process_chunk(self): self.ensure_one() usage = self.usage apply_on = self.apply_on_model with self.work_on(apply_on) as work: if DEBUG_MODE: with self.env.cr.savepoint(): processor = work.component(usage=usage) result = processor.run() self.state_info = "" self.state = "done" return result else: try: with self.env.cr.savepoint(): processor = work.component(usage=usage) result = processor.run() except Exception as e: self.state = "fail" self.state_info = type(e).__name__ + str(e.args) return False self.state_info = "" self.state = "done" return result
class PRTMailMessage(models.Model): _name = "mail.message" _inherit = "mail.message" author_display = fields.Char(string="Author", compute="_author_display") # Fields to avoid access check issues author_allowed_id = fields.Many2one(string="Author", comodel_name='res.partner', compute='_get_author_allowed', search='_search_author_allowed') partner_allowed_ids = fields.Many2many(string="Recipients", comodel_name='res.partner', compute='_get_partners_allowed') subject_display = fields.Char(string="Subject", compute="_subject_display") partner_count = fields.Integer(string="Recipients count", compute='_partner_count') record_ref = fields.Reference(string="Message Record", selection='_referenceable_models', compute='_record_ref') attachment_count = fields.Integer(string="Attachments count", compute='_attachment_count') thread_messages_count = fields.Integer( string="Messages in thread", compute='_thread_messages_count', help="Total number of messages in thread") ref_partner_ids = fields.Many2many(string="Followers", comodel_name='res.partner', compute='_message_followers') ref_partner_count = fields.Integer(string="Followers", compute='_ref_partner_count') # -- Count ref Partners @api.multi def _ref_partner_count(self): for rec in self: rec.ref_partner_count = len(rec.ref_partner_ids) """ Sometimes user has access to record but does not have access to author or recipients. Below is a workaround for author, recipient and followers """ # -- Get allowed author @api.depends('author_id') @api.multi def _get_author_allowed(self): for rec in self: author_id = rec.author_id try: author_id.check_access_rule('read') rec.author_allowed_id = author_id except: continue # -- Get allowed recipients @api.depends('partner_ids') @api.multi def _get_partners_allowed(self): for rec in self: recipients_allowed = self.env['res.partner'] for partner in rec.partner_ids: try: partner.check_access_rule('read') recipients_allowed += partner except: continue rec.partner_allowed_ids = recipients_allowed # -- Search allowed authors @api.model def _search_author_allowed(self, operator, value): return [('author_id', operator, value)] # -- Get related record followers """ Check if model has 'followers' field and user has access to followers """ @api.depends('record_ref') @api.multi def _message_followers(self): for rec in self: if rec.record_ref: if 'message_partner_ids' in self.env[rec.model]._fields: followers_allowed = self.env['res.partner'] for follower in rec.record_ref.message_partner_ids: try: follower.check_access_rule('read') followers_allowed += follower except: continue rec.ref_partner_ids = followers_allowed # -- Dummy @api.multi def dummy(self): return # -- Get Subject for tree view @api.depends('subject') @api.multi def _subject_display(self): # Get model names first. Use this method to get translated values ir_models = self.env['ir.model'].search([ ('model', 'in', list(set(self.mapped('model')))) ]) model_dict = {} for model in ir_models: # Check if model has "name" field has_name = self.env['ir.model.fields'].sudo().search_count([ ('model_id', '=', model.id), ('name', '=', 'name') ]) model_dict.update({model.model: [model.name, has_name]}) # Compose subject for rec in self: if rec.subject: subject_display = rec.subject else: subject_display = '=== No Reference ===' # Has reference if rec.record_ref: subject_display = model_dict.get(rec.model)[0] # Has 'name' field if model_dict.get(rec.model, False)[1]: subject_display = "%s: %s" % (subject_display, rec.record_ref.sudo().name) # Has subject if rec.subject: subject_display = "%s => %s" % (subject_display, rec.subject) # Set subject rec.subject_display = subject_display # -- Get Author for tree view @api.depends('author_allowed_id') @api.multi def _author_display(self): for rec in self: rec.author_display = rec.author_allowed_id.name if rec.author_allowed_id else rec.email_from # -- Count recipients @api.depends('partner_allowed_ids') @api.multi def _partner_count(self): for rec in self: rec.partner_count = len(rec.partner_allowed_ids) # -- Count attachments @api.depends('attachment_ids') @api.multi def _attachment_count(self): for rec in self: rec.attachment_count = len(rec.attachment_ids) # -- Count messages in same thread @api.depends('res_id') @api.multi def _thread_messages_count(self): for rec in self: rec.thread_messages_count = self.search_count([ '&', '&', ('model', '=', rec.model), ('res_id', '=', rec.res_id), ('message_type', '!=', 'notification') ]) # -- Ref models @api.model def _referenceable_models(self): # return [(x.model, x.name) for x in self.env['ir.model'].sudo().search([('model', '!=', 'mail.channel')])] """ Mail channel is needed for legacy views (Settings->Technical Settings->Messages) """ return [(x.model, x.name) for x in self.env['ir.model'].sudo().search([('transient', '=', False)])] # -- Compose reference @api.depends('res_id') @api.multi def _record_ref(self): for rec in self: if rec.model: if rec.res_id: res = self.env[rec.model].sudo().search([("id", "=", rec.res_id)]) if res: rec.record_ref = res # -- Open messages of the same thread @api.multi def thread_messages(self): self.ensure_one() global TREE_VIEW_ID global FORM_VIEW_ID # Cache Tree View and Form View ids if not TREE_VIEW_ID: TREE_VIEW_ID = self.env.ref( 'prt_mail_messages.prt_mail_message_tree').id FORM_VIEW_ID = self.env.ref( 'prt_mail_messages.prt_mail_message_form').id return { 'name': _("Messages"), "views": [[TREE_VIEW_ID, "tree"], [FORM_VIEW_ID, "form"]], 'res_model': 'mail.message', 'type': 'ir.actions.act_window', 'target': 'current', 'domain': [('message_type', '!=', 'notification'), ('model', '=', self.model), ('res_id', '=', self.res_id)] } # -- Override _search """ mail.message overrides generic '_search' defined in 'model' to implement own logic for message access rights. However sometimes it does not work as expected. So we use generic method in 'model' and check access rights later in 'search' method. Following keys in context are used: - 'check_messages_access': if not set legacy 'search' is performed """ @api.model def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): if not self._context.get('check_messages_access', False): return super(PRTMailMessage, self)._search(args=args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid) if expression.is_false(self, args): # optimization: no need to query, as no record satisfies the domain return 0 if count else [] query = self._where_calc(args) order_by = self._generate_order_by(order, query) from_clause, where_clause, where_clause_params = query.get_sql() where_str = where_clause and (" WHERE %s" % where_clause) or '' if count: # Ignore order, limit and offset when just counting, they don't make sense and could # hurt performance query_str = 'SELECT count(1) FROM ' + from_clause + where_str self._cr.execute(query_str, where_clause_params) res = self._cr.fetchone() return res[0] limit_str = limit and ' limit %d' % limit or '' offset_str = offset and ' offset %d' % offset or '' query_str = 'SELECT "%s".id FROM ' % self._table + from_clause + where_str + order_by + limit_str + offset_str self._cr.execute(query_str, where_clause_params) res = self._cr.fetchall() # TDE note: with auto_join, we could have several lines about the same result # i.e. a lead with several unread messages; we uniquify the result using # a fast way to do it while preserving order (http://www.peterbe.com/plog/uniqifiers-benchmark) def _uniquify_list(seq): seen = set() return [x for x in seq if x not in seen and not seen.add(x)] return _uniquify_list([x[0] for x in res]) # -- Override read """ Avoid access rights check implemented in original mail.message Will check them later in "search" Using base model function instead Following keys in context are used: - 'check_messages_access': if not set legacy 'search' is performed """ @api.multi def read(self, fields=None, load='_classic_read'): if not self._context.get('check_messages_access', False): return super(PRTMailMessage, self).read(fields=fields, load=load) """ From here starts the original 'read' code """ # split fields into stored and computed fields stored, inherited, computed = [], [], [] for name in fields: field = self._fields.get(name) if field: if field.store: stored.append(name) elif field.base_field.store: inherited.append(name) else: computed.append(name) else: _logger.warning("%s.read() with unknown field '%s'", self._name, name) # fetch stored fields from the database to the cache; this should feed # the prefetching of secondary records self._read_from_database(stored, inherited) # retrieve results from records; this takes values from the cache and # computes remaining fields result = [] name_fields = [(name, self._fields[name]) for name in (stored + inherited + computed)] use_name_get = (load == '_classic_read') for record in self: try: values = {'id': record.id} for name, field in name_fields: values[name] = field.convert_to_read( record[name], record, use_name_get) result.append(values) except MissingError: pass return result # -- Override Search """ Mail message access rights/rules checked must be done based on the access rights/rules of the message record. As a workaround we are using 'search' method to filter messages from unavailable records. Display only messages where user has read access to related record. Following keys in context are used: - 'check_messages_access': if not set legacy 'search' is performed - 'force_record_reset': in case message refers to non-existing (e.g. removed) record model and res_id will be set NULL """ @api.model def search(self, args, offset=0, limit=None, order=None, count=False): if not self._context.get('check_messages_access', False): return super(PRTMailMessage, self).search(args=args, offset=offset, limit=limit, order=order, count=count) # Store context keys force_record_reset = self._context.get('force_record_reset', False) # Store initial args in case we need them later modded_args = args sort_order = 'desc' if order == 'date DESC, id DESC' else 'asc' # New query # Check model access 1st # Add mail.channel by default because we are not showing chat/channel messages forbidden_models = ['mail.channel'] # Get list of possible followed models self._cr.execute(""" SELECT model FROM ir_model mdl WHERE mdl.is_mail_thread = True AND name != 'mail.channel' """) # Check each model for msg_model in self._cr.fetchall(): if not self.env['ir.model.access'].check( msg_model[0], 'read', raise_exception=False): forbidden_models.append(msg_model[0]) # Add forbidden models condition to domain if len(forbidden_models) > 0: modded_args.append(['model', 'not in', forbidden_models]) # Return Count if count: return super(PRTMailMessage, self).search(args=modded_args, offset=offset, limit=limit, order=order, count=True) # Get records res_ids = self._search(args=modded_args, offset=offset, limit=limit, order=order, count=False) res = self.browse(res_ids) # Now check record rules res_allowed = self.env['mail.message'] len_initial = limit if limit else len(res) len_filtered = 0 last_val = False # Check records """ Check in we need include "lost" messages. These are messages with no model or res_id """ get_lost = self._context.get('get_lost', False) for rec in res: last_val = rec.date # No model if not rec.model: if get_lost: res_allowed += rec len_filtered += 1 continue # No id if not rec.res_id: if get_lost: res_allowed += rec len_filtered += 1 continue # Check access rules on record. Skip if refers to deleted record try: target_rec = self.env[rec.model].search([('id', '=', rec.res_id)]) if not target_rec: # Reset model and res_id if force_record_reset: rec.sudo().write({'model': False, 'res_id': False}) continue # Check message record target_rec.check_access_rule('read') except: continue res_allowed += rec len_filtered += 1 # We store ids of the record with the last date not to fetch them twice while fetching more recs last_ids = res.filtered(lambda d: d.date == last_val).ids del res # Hope Python will free memory asap!)) # Return if initially got less then limit if limit is None or len_initial < limit: return res_allowed len_remaining = len_initial - len_filtered # Return if all allowed if len_remaining == 0: return res_allowed # Get remaining recs while len_remaining > 0: if sort_order == 'desc': new_args = modded_args + [['date', '<=', last_val], ['id', 'not in', last_ids]] else: new_args = modded_args + [['date', '>=', last_val], ['id', 'not in', last_ids]] # Let's try!)) res_2_ids = self._search(args=new_args, offset=0, limit=len_remaining, order=order, count=False) res_2 = self.browse(res_2_ids) if len(res_2) < 1: break # Check records for rec in res_2: last_val = rec.date # No model if not rec.model: if get_lost: res_allowed += rec len_filtered += 1 continue # No res_id if not rec.res_id: if get_lost: res_allowed += rec len_filtered += 1 continue # Check access rules on record. Skip if refers to deleted record try: target_rec = self.env[rec.model].search([('id', '=', rec.res_id)]) if not target_rec: # Reset model and res_id if force_record_reset: rec.sudo().write({'model': False, 'res_id': False}) continue # Check message record target_rec.check_access_rule('read') except: continue res_allowed += rec len_remaining -= 1 if not len_remaining == 0: last_ids += res_2.filtered(lambda d: d.date == last_val).ids return res_allowed # -- Prepare context for reply or quote message @api.multi def reply_prep_context(self): self.ensure_one() body = False wizard_mode = self._context.get('wizard_mode', False) if wizard_mode in ['quote', 'forward']: body = (_( "<div font-style=normal;><br/></div><blockquote>----- Original message ----- <br/> Date: %s <br/> From: %s <br/> Subject: %s <br/><br/>%s</blockquote>" ) % (str(self.date), self.author_display, self.subject_display, self.body)) ctx = { 'default_res_id': self.res_id, 'default_parent_id': False if wizard_mode == 'forward' else self.id, 'default_model': self.model, 'default_partner_ids': [self.author_allowed_id.id] if self.author_allowed_id else [], 'default_attachment_ids': self.attachment_ids.ids if wizard_mode == 'forward' else [], 'default_is_log': False, 'default_body': body, 'default_wizard_mode': wizard_mode } return ctx # -- Reply or quote message @api.multi def reply(self): self.ensure_one() return { 'name': _("New message"), "views": [[False, "form"]], 'res_model': 'mail.compose.message', 'type': 'ir.actions.act_window', 'target': 'new', 'context': self.reply_prep_context() } # -- Move message @api.multi def move(self): self.ensure_one() return { 'name': _("Move messages"), "views": [[False, "form"]], 'res_model': 'prt.message.move.wiz', 'type': 'ir.actions.act_window', 'target': 'new' }
class IrServerObjectLines(models.Model): _name = 'ir.server.object.lines' _description = 'Server Action value mapping' _sequence = 'ir_actions_id_seq' server_id = fields.Many2one('ir.actions.server', string='Related Server Action', ondelete='cascade') col1 = fields.Many2one('ir.model.fields', string='Field', required=True) value = fields.Text( required=True, help="Expression containing a value specification. \n" "When Formula type is selected, this field may be a Python expression " " that can use the same values as for the code field on the server action.\n" "If Value type is selected, the value will be used directly without evaluation." ) type = fields.Selection([('value', 'Value'), ('reference', 'Reference'), ('equation', 'Python expression')], 'Evaluation Type', default='value', required=True, change_default=True) resource_ref = fields.Reference(string='Record', selection='_selection_target_model', compute='_compute_resource_ref', inverse='_set_resource_ref') @api.model def _selection_target_model(self): models = self.env['ir.model'].search([]) return [(model.model, model.name) for model in models] @api.depends('col1.relation', 'value', 'type') def _compute_resource_ref(self): for line in self: if line.type in ['reference', 'value' ] and line.col1 and line.col1.relation: value = line.value or '' try: value = int(value) if not self.env[line.col1.relation].browse(value).exists(): record = self.env[line.col1.relation]._search([], limit=1) value = record[0] if record else 0 except ValueError: record = self.env[line.col1.relation]._search([], limit=1) value = record[0] if record else 0 line.resource_ref = '%s,%s' % (line.col1.relation, value) else: line.resource_ref = False @api.onchange('resource_ref') def _set_resource_ref(self): for line in self.filtered(lambda line: line.type == 'reference'): if line.resource_ref: line.value = str(line.resource_ref.id) @api.multi def eval_value(self, eval_context=None): result = dict.fromkeys(self.ids, False) for line in self: expr = line.value if line.type == 'equation': expr = safe_eval(line.value, eval_context) elif line.col1.ttype in ['many2one', 'integer']: try: expr = int(line.value) except Exception: pass result[line.id] = expr return result
class ThemeView(models.Model): _name = 'theme.ir.ui.view' _description = 'Theme UI View' def compute_arch_fs(self): path_info = get_resource_from_path(self._context['install_filename']) if path_info: return '/'.join(path_info[0:2]) name = fields.Char(required=True) key = fields.Char() type = fields.Char() priority = fields.Integer(default=16, required=True) mode = fields.Selection([('primary', "Base view"), ('extension', "Extension View")]) active = fields.Boolean(default=True) arch = fields.Text(translate=xml_translate) arch_fs = fields.Char(default=compute_arch_fs) inherit_id = fields.Reference( selection=[('ir.ui.view', 'ir.ui.view'), ('theme.ir.ui.view', 'theme.ir.ui.view')]) copy_ids = fields.One2many('ir.ui.view', 'theme_template_id', 'Views using a copy of me', copy=False, readonly=True) # TODO master add missing field: customize_show @api.multi def _convert_to_base_model(self, website, **kwargs): self.ensure_one() inherit = self.inherit_id if self.inherit_id and self.inherit_id._name == 'theme.ir.ui.view': inherit = self.inherit_id.with_context( active_test=False).copy_ids.filtered( lambda x: x.website_id == website) if not inherit: # inherit_id not yet created, add to the queue return False if inherit and inherit.website_id != website: website_specific_inherit = self.env['ir.ui.view'].with_context( active_test=False).search([('key', '=', inherit.key), ('website_id', '=', website.id)], limit=1) if website_specific_inherit: inherit = website_specific_inherit new_view = { 'type': self.type or 'qweb', 'name': self.name, 'arch': self.arch, 'key': self.key, 'inherit_id': inherit and inherit.id, 'arch_fs': self.arch_fs, 'priority': self.priority, 'active': self.active, 'theme_template_id': self.id, 'website_id': website.id, } if self.mode: # if not provided, it will be computed automatically (if inherit_id or not) new_view['mode'] = self.mode return new_view
class CouponPromotion(models.Model): """ Promotions with coupons """ _name = "coupon.promotion" _description = __doc__ @api.multi @api.depends("name") def _get_number(self): for row in self: number = "" if row.name: number = row.name.split("-")[-1:][0] row.number = number return True name = fields.Char(_("Code")) number = fields.Char(_("Number"), compute="_get_number", store=True) partner_id = fields.Many2one("res.partner", _("Customer")) partner_name = fields.Char(_("Customer Name"), required=True) partner_street = fields.Char(_("Customer Address"), required=True) partner_phone = fields.Char(_("Customer Phone"), required=True) type_identifier = fields.Selection( [ ("04", "RUC"), ("05", "CEDULA"), ("06", "PASAPORTE"), ("07", "CONSUMIDOR FINAL"), ], string="Tipo ID", default="06", required=True, ) identification = fields.Char(_("Identification"), required=True) email = fields.Char(_("Email"), required=True) terms = fields.Boolean(_("Accept Terms and Conditions"), required=True) coupon_id = fields.Many2one("coupon", _("Coupon")) send = fields.Boolean("Email Send", default=False) currency_id = fields.Many2one("res.currency", related="coupon_id.currency_id", store=True) date_from = fields.Date(_("From"), related="coupon_id.date_from", store=True) date_to = fields.Date(_("To"), related="coupon_id.date_to", store=True) date_used = fields.Date(_("Date Used")) team_ids = fields.Many2many( "crm.team", "coupon_promotion_crm_team_rel", "coupon_id", "team_id", _("Sale Channels"), related="coupon_id.team_ids", store=True, ) value = fields.Monetary(_("Amount")) used = fields.Boolean(_("Used")) used_in = fields.Char(_("Used in")) reference = fields.Reference(string="Reference", selection="_get_reference_models") state = fields.Selection( [("draft", _("Draft")), ("confirm", _("Confirm")), ("cancel", _("Cancel"))], string=_("State"), related="coupon_id.state", store=True, ) @api.multi def _get_reference_models(self): refs = self.env["ir.model"].search([ "|", ("model", "=", "account.invoice"), ("model", "=", "pos.order") ]) return [(ref.model, ref.name) for ref in refs] + [("", "")] @api.multi def unlink(self): for row in self: if row.state != "draft": raise UserError( _("You cannot delete an Coupon which is not draft or cancelled" )) return super(Coupon, self).unlink() @api.model def fields_view_get(self, view_id=None, view_type="form", toolbar=False, submenu=False): res = models.Model.fields_view_get(self, view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) update = view_type in ["form", "tree"] if update: doc = etree.XML(res["arch"]) for t in doc.xpath("//" + view_type): t.attrib["create"] = "false" t.attrib["edit"] = "false" res["arch"] = etree.tostring(doc) return res @api.model def create(self, vals): today = fields.Date.context_today(self) partner_obj = self.env["res.partner"] coupon_obj = self.env["coupon"] promotion_obj = self.env["coupon.promotion"] res = self if self.env.context.get("website_id"): partner_id = partner_obj.search([("identification", "=", vals.get("identification"))]) if not partner_id: partner_id = partner_obj.create({ "name": vals.get("partner_name"), "type_identifier": vals.get("type_identifier"), "identification": vals.get("identification"), "email": vals.get("email"), "phone": vals.get("partner_phone"), "street": vals.get("partner_street"), "customer": True, }) coupon_id = coupon_obj.search([ ("date_from", "<=", today), ("date_to", ">=", today), ("coupon_left", ">", 0), ("state", "=", "confirm"), ]) last_coupon = partner_coupons = promotion_obj.search( [("partner_id", "=", partner_id.id)], limit=1) if not coupon_id: return last_coupon number = len(coupon_id.mapped("coupon_ids")) + 1 partner_coupons = promotion_obj.search([ ("partner_id", "=", partner_id.id), ("coupon_id", "=", coupon_id.id) ]) if len(partner_coupons) < coupon_id.coupon_partner: vals.update({ "name": "{}-{}-{:0>4}".format(coupon_id.code, partner_id.identification[-4:], number), "coupon_id": coupon_id.id, "partner_id": partner_id.id, "value": coupon_id.coupon_value, }) res = super(CouponPromotion, self).create(vals) self.coupon_notify("ok", res) else: res = last_coupon self.coupon_notify("fail", res) return res @api.model def coupon_notify(self, type, coupon_ids): for row in coupon_ids: mail_obj = self.env["mail.mail"] ean = BytesIO() generate("code128", u"{}".format(row.name), writer=ImageWriter(), output=ean) ean.seek(0) jpgdata = ean.read() imgdata = base64.encodestring(jpgdata) variables = { "name": row.coupon_id.name, "partner": row.partner_id.name_get()[0][1], "date_to": row.coupon_id.date_to, "code": row.name, "barcode": '<img src="data:image/jpeg;base64,{}" />'.format( imgdata.decode("utf-8")), "amount": row.value, "min_amount": row.coupon_id.min_amount, "coupon_limit": row.coupon_id.coupon_partner, } if type == "ok": template = Template( row.coupon_id.body_html, trim_blocks=True, lstrip_blocks=True, autoescape=True, ) elif type == "fail": template = Template( row.coupon_id.body_html_limit, trim_blocks=True, lstrip_blocks=True, autoescape=True, ) vals = { "state": "outgoing", "subject": _("{}: {}".format(self.env.user.company_id.name_get()[0][1], row.coupon_id.name)), "body_html": template.render(**variables), "email_to": row.email, "email_from": "{} <{}>".format(row.env.user.company_id.name, row.env.user.company_id.email), "author_id": row.env.user.company_id.partner_id.id, } mail = mail_obj.create(vals) mail.send() row.send = True return True @api.model def validate_coupon(self, coupon, partner_id, crm_team_id, due): def _validate(coupon): today = fields.Date.context_today(self) return { coupon.coupon_id.min_amount > due: "Valor minimo para aplicar el cupón es de {}".format( coupon.coupon_id.min_amount), not coupon.date_from <= today <= coupon.date_to: "Cupón expirado", coupon.used: "Cupon aplicado", coupon.partner_id.id != partner_id: "El cupón a aplicar no corresponde al cliente a facturar", }.get(True) or False coupon = self.search( [ ("name", "=", coupon), ("coupon_id.coupon_apply", "in", ("both", "pos")), ("team_ids.id", "=", crm_team_id), ("state", "=", "confirm"), ], limit=1, ) if not coupon.id: return "Cupón no valido", False return _validate(coupon), coupon.value
class File(models.Model): _name = "dms.file" _description = "File" _inherit = [ "portal.mixin", "dms.security.mixin", "dms.mixins.thumbnail", "mail.thread", "mail.activity.mixin", ] _order = "name asc" # ---------------------------------------------------------- # Database # ---------------------------------------------------------- name = fields.Char(string="Filename", required=True, index=True) active = fields.Boolean( string="Archived", default=True, help="If a file is set to archived, it is not displayed, but still exists.", ) directory_id = fields.Many2one( comodel_name="dms.directory", string="Directory", domain="[('permission_create', '=', True)]", context="{'dms_directory_show_path': True}", ondelete="restrict", auto_join=True, required=True, index=True, ) storage_id = fields.Many2one( related="directory_id.storage_id", comodel_name="dms.storage", string="Storage", auto_join=True, readonly=True, store=True, ) is_hidden = fields.Boolean( string="Storage is Hidden", related="storage_id.is_hidden", readonly=True ) company_id = fields.Many2one( related="storage_id.company_id", comodel_name="res.company", string="Company", readonly=True, store=True, index=True, ) path_names = fields.Char( compute="_compute_path", string="Path Names", readonly=True, store=False ) path_json = fields.Text( compute="_compute_path", string="Path Json", readonly=True, store=False ) color = fields.Integer(string="Color", default=0) category_id = fields.Many2one( comodel_name="dms.category", context="{'dms_category_show_path': True}", string="Category", ) tag_ids = fields.Many2many( comodel_name="dms.tag", relation="dms_file_tag_rel", column1="fid", column2="tid", string="Tags", ) content = fields.Binary( compute="_compute_content", inverse="_inverse_content", string="Content", attachment=False, prefetch=False, required=True, store=False, ) extension = fields.Char( compute="_compute_extension", string="Extension", readonly=True, store=True ) res_mimetype = fields.Char( compute="_compute_mimetype", string="Type", readonly=True, store=True ) size = fields.Integer(string="Size", readonly=True) checksum = fields.Char(string="Checksum/SHA1", readonly=True, size=40, index=True) content_binary = fields.Binary( string="Content Binary", attachment=False, prefetch=False, invisible=True ) save_type = fields.Char( compute="_compute_save_type", string="Current Save Type", invisible=True, prefetch=False, ) migration = fields.Char( compute="_compute_migration", string="Migration Status", readonly=True, prefetch=False, compute_sudo=True, ) require_migration = fields.Boolean( compute="_compute_migration", store=True, compute_sudo=True, ) content_file = fields.Binary( attachment=True, string="Content File", prefetch=False, invisible=True ) def check_access_token(self, access_token=False): res = False if access_token: if self.access_token and self.access_token == access_token: return True else: items = ( self.env["dms.directory"] .sudo() .search([("access_token", "=", access_token)]) ) if items: item = items[0] if self.directory_id.id == item.id: return True else: directory_item = self.directory_id while directory_item.parent_id: if directory_item.id == self.directory_id.id: return True directory_item = directory_item.parent_id # Fix last level if directory_item.id == self.directory_id.id: return True return res attachment_id = fields.Many2one( comodel_name="ir.attachment", string="Attachment File", prefetch=False, invisible=True, ondelete="cascade", ) res_model = fields.Char(string="Linked attachments model") res_id = fields.Integer(string="Linked attachments record ID") record_ref = fields.Reference( string="Record Referenced", compute="_compute_record_ref", selection=[], readonly=True, store=False, ) storage_id_save_type = fields.Selection(related="storage_id.save_type", store=False) @api.depends("res_model", "res_id") def _compute_record_ref(self): for record in self: record.record_ref = False if record.res_model and record.res_id: record.record_ref = "{},{}".format(record.res_model, record.res_id) def get_human_size(self): return human_size(self.size) def _get_share_url(self, redirect=False, signup_partner=False, pid=None): self.ensure_one() return "/my/dms/file/{}/download?access_token={}&db={}".format( self.id, self._portal_ensure_token(), self.env.cr.dbname, ) # ---------------------------------------------------------- # Helper # ---------------------------------------------------------- @api.model def _get_checksum(self, binary): return hashlib.sha1(binary or b"").hexdigest() @api.model def _get_content_inital_vals(self): return {"content_binary": False, "content_file": False} def _update_content_vals(self, vals, binary): new_vals = vals.copy() new_vals.update( { "checksum": self._get_checksum(binary), "size": binary and len(binary) or 0, } ) if self.storage_id.save_type in ["file", "attachment"]: new_vals["content_file"] = self.content else: new_vals["content_binary"] = self.content and binary return new_vals @api.model def _get_binary_max_size(self): return int( self.env["ir.config_parameter"] .sudo() .get_param("dms.binary_max_size", default=25) ) @api.model def _get_forbidden_extensions(self): get_param = self.env["ir.config_parameter"].sudo().get_param extensions = get_param("dms.forbidden_extensions", default="") return [extension.strip() for extension in extensions.split(",")] def _get_thumbnail_placeholder_name(self): return self.extension and "file_%s.svg" % self.extension or "" # ---------------------------------------------------------- # Actions # ---------------------------------------------------------- def action_migrate(self, logging=True): record_count = len(self) index = 1 for dms_file in self: if logging: info = (index, record_count, dms_file.migration) _logger.info(_("Migrate File %s of %s [ %s ]") % info) index += 1 dms_file.write({"content": dms_file.with_context({}).content}) def action_save_onboarding_file_step(self): self.env.user.company_id.set_onboarding_step_done( "documents_onboarding_file_state" ) # ---------------------------------------------------------- # SearchPanel # ---------------------------------------------------------- @api.model def _search_panel_directory(self, **kwargs): search_domain = (kwargs.get("search_domain", []),) category_domain = kwargs.get("category_domain", []) if category_domain and len(category_domain): return "=", category_domain[0][2] if search_domain and len(search_domain): for domain in search_domain[0]: if domain[0] == "directory_id": return domain[1], domain[2] return None, None @api.model def _search_panel_domain(self, field, operator, directory_id, comodel_domain=False): if not comodel_domain: comodel_domain = [] files_ids = self.search([("directory_id", operator, directory_id)]).ids return expression.AND([comodel_domain, [(field, "in", files_ids)]]) @api.model def search_panel_select_range(self, field_name, **kwargs): operator, directory_id = self._search_panel_directory(**kwargs) if directory_id and field_name == "directory_id": domain = [("parent_id", operator, directory_id)] values = ( self.env["dms.directory"] .with_context(directory_short_name=True) .search_read(domain, ["display_name", "parent_id"]) ) return { "parent_field": "parent_id", "values": values if len(values) > 1 else [], } context = {} if field_name == "directory_id": context["directory_short_name"] = True return super(File, self.with_context(**context)).search_panel_select_range( field_name, **kwargs ) @api.model def search_panel_select_multi_range(self, field_name, **kwargs): operator, directory_id = self._search_panel_directory(**kwargs) if field_name == "tag_ids": sql_query = """ SELECT t.name AS name, t.id AS id, c.name AS group_name, c.id AS group_id, COUNT(r.fid) AS count FROM dms_tag t JOIN dms_category c ON t.category_id = c.id LEFT JOIN dms_file_tag_rel r ON t.id = r.tid {directory_where_clause} GROUP BY c.name, c.id, t.name, t.id ORDER BY c.name, c.id, t.name, t.id; """ where_clause = "" params = [] if directory_id: file_ids = self.search([("directory_id", operator, directory_id)]).ids if file_ids: where_clause = "WHERE r.fid in %s" params.append(tuple(file_ids)) else: where_clause = "WHERE 1 = 0" # pylint: disable=sql-injection final_query = sql_query.format(directory_where_clause=where_clause) self.env.cr.execute(final_query, params) return self.env.cr.dictfetchall() if directory_id and field_name in ["directory_id", "category_id"]: comodel_domain = kwargs.pop("comodel_domain", []) directory_comodel_domain = self._search_panel_domain( "file_ids", operator, directory_id, comodel_domain ) return super( File, self.with_context(directory_short_name=True) ).search_panel_select_multi_range( field_name, comodel_domain=directory_comodel_domain, **kwargs ) return super( File, self.with_context(directory_short_name=True) ).search_panel_select_multi_range(field_name, **kwargs) # ---------------------------------------------------------- # Read # ---------------------------------------------------------- @api.depends("name", "directory_id", "directory_id.parent_path") def _compute_path(self): model = self.env["dms.directory"] data = {} for record in self: path_names = [] path_json = [] if record.directory_id.parent_path: for directory_id in reversed( list(map(int, record.directory_id.parent_path.split("/")[:-1])) ): if not directory_id: break if directory_id not in data: data[directory_id] = model.browse(directory_id) path_names.append(data[directory_id].name) path_json.append( { "model": model._name, "name": data[directory_id].name, "id": directory_id, } ) path_names.reverse() path_json.reverse() name = record.name_get() path_names.append(name[0][1]) path_json.append( { "model": record._name, "name": name[0][1], "id": isinstance(record.id, int) and record.id or 0, } ) record.update( { "path_names": "/".join(path_names), "path_json": json.dumps(path_json), } ) @api.depends("name") def _compute_extension(self): for record in self: record.extension = file.guess_extension(record.name) @api.depends("name", "content") def _compute_mimetype(self): for record in self: binary = base64.b64decode(record.with_context({}).content or "") record.res_mimetype = guess_mimetype( binary, default="application/octet-stream" ) @api.depends("content_binary", "content_file", "attachment_id") def _compute_content(self): bin_size = self.env.context.get("bin_size", False) for record in self: if record.content_file: context = {"human_size": True} if bin_size else {"base64": True} record.content = record.with_context(context).content_file elif record.content_binary: record.content = ( record.content_binary if bin_size else base64.b64encode(record.content_binary) ) elif record.attachment_id: context = {"human_size": True} if bin_size else {"base64": True} record.content = record.with_context(context).attachment_id.datas @api.depends("content_binary", "content_file") def _compute_save_type(self): for record in self: if record.content_file: record.save_type = "file" else: record.save_type = "database" @api.depends("storage_id", "storage_id.save_type") def _compute_migration(self): storage_model = self.env["dms.storage"] save_field = storage_model._fields["save_type"] values = save_field._description_selection(self.env) selection = {value[0]: value[1] for value in values} for record in self: storage_type = record.storage_id.save_type if storage_type == "attachment" or storage_type == record.save_type: record.migration = selection.get(storage_type) record.require_migration = False else: storage_label = selection.get(storage_type) file_label = selection.get(record.save_type) record.migration = "{} > {}".format(file_label, storage_label) record.require_migration = True def read(self, fields=None, load="_classic_read"): self.check_directory_access("read", {}, True) return super(File, self).read(fields, load=load) # ---------------------------------------------------------- # View # ---------------------------------------------------------- @api.onchange("category_id") def _change_category(self): res = {"domain": {"tag_ids": [("category_id", "=", False)]}} if self.category_id: res.update( { "domain": { "tag_ids": [ "|", ("category_id", "=", False), ("category_id", "child_of", self.category_id.id), ] } } ) tags = self.tag_ids.filtered( lambda rec: not rec.category_id or rec.category_id == self.category_id ) self.tag_ids = tags return res # ---------------------------------------------------------- # Security # ---------------------------------------------------------- @api.model def _get_directories_from_database(self, file_ids): if not file_ids: return self.env["dms.directory"] return self.env["dms.file"].browse(file_ids).mapped("directory_id") @api.model def _read_group_process_groupby(self, gb, query): if self.env.user.id == SUPERUSER_ID: return super(File, self)._read_group_process_groupby(gb, query) directories = ( self.env["dms.directory"].with_context(prefetch_fields=False).search([]) ) if directories: where_clause = '"{table}"."{field}" = ANY (VALUES {ids})'.format( table=self._table, field="directory_id", ids=", ".join(map(lambda id: "(%s)" % id, directories.ids)), ) query.where_clause += [where_clause] else: query.where_clause += ["0=1"] return super(File, self)._read_group_process_groupby(gb, query) @api.model def _search( self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None, ): result = super(File, self)._search( args, offset, limit, order, False, access_rights_uid ) if self.env.user.id == SUPERUSER_ID: return len(result) if count else result # Fix access files with share button (public) if self.env.user.has_group("base.group_public"): return len(result) if count else result # operations if not result: return 0 if count else [] file_ids = set(result) directories = self._get_directories_from_database(result) for directory in directories - directories._filter_access("read"): file_ids -= set(directory.sudo().mapped("file_ids").ids) return len(file_ids) if count else list(file_ids) def _filter_access(self, operation): records = super(File, self)._filter_access(operation) if self.env.user.id == SUPERUSER_ID: return records directories = self._get_directories_from_database(records.ids) for directory in directories - directories._filter_access("read"): records -= self.browse(directory.sudo().mapped("file_ids").ids) return records def check_access(self, operation, raise_exception=False): res = super(File, self).check_access(operation, raise_exception) try: if self.env.user.has_group("base.group_portal"): res_access = res and self.check_directory_access(operation) return res_access and ( self.directory_id.id not in self.directory_id._get_ids_without_access_groups(operation) ) else: return res and self.check_directory_access(operation) except AccessError: if raise_exception: raise return False def check_directory_access(self, operation, vals=False, raise_exception=False): if not vals: vals = {} if self.env.user.id == SUPERUSER_ID: return True if "directory_id" in vals and vals["directory_id"]: records = self.env["dms.directory"].browse(vals["directory_id"]) else: records = self._get_directories_from_database(self.ids) return records.check_access(operation, raise_exception) # ---------------------------------------------------------- # Constrains # ---------------------------------------------------------- @api.constrains("name") def _check_name(self): for record in self: if not file.check_name(record.name): raise ValidationError(_("The file name is invalid.")) files = record.sudo().directory_id.file_ids.name_get() if list( filter( lambda file: file[1] == record.name and file[0] != record.id, files ) ): raise ValidationError(_("A file with the same name already exists.")) @api.constrains("extension") def _check_extension(self): for record in self: if ( record.extension and record.extension in self._get_forbidden_extensions() ): raise ValidationError(_("The file has a forbidden file extension.")) @api.constrains("size") def _check_size(self): for record in self: if record.size and record.size > self._get_binary_max_size() * 1024 * 1024: raise ValidationError( _("The maximum upload size is %s MB).") % self._get_binary_max_size() ) @api.constrains("directory_id") def _check_directory_access(self): for record in self: if not record.directory_id.check_access("create", raise_exception=False): raise ValidationError( _("The directory has to have the permission to create files.") ) # ---------------------------------------------------------- # Create, Update, Delete # ---------------------------------------------------------- def _inverse_content(self): updates = defaultdict(set) for record in self: values = self._get_content_inital_vals() binary = base64.b64decode(record.content or "") values = record._update_content_vals(values, binary) updates[tools.frozendict(values)].add(record.id) with self.env.norecompute(): for vals, ids in updates.items(): self.browse(ids).write(dict(vals)) def _create_model_attachment(self, vals): res_vals = vals.copy() directory = self.env["dms.directory"].sudo().browse(res_vals["directory_id"]) if directory and directory.res_model and directory.res_id: attachment = ( self.env["ir.attachment"] .with_context(dms_file=True) .create( { "name": vals["name"], "datas": vals["content"], "res_model": directory.res_model, "res_id": directory.res_id, } ) ) res_vals["attachment_id"] = attachment.id res_vals["res_model"] = attachment.res_model res_vals["res_id"] = attachment.res_id del res_vals["content"] return res_vals def copy(self, default=None): self.ensure_one() default = dict(default or []) if "directory_id" in default: model = self.env["dms.directory"] directory = model.browse(default["directory_id"]) names = directory.sudo().file_ids.mapped("name") else: names = self.sudo().directory_id.file_ids.mapped("name") default.update({"name": file.unique_name(self.name, names, self.extension)}) self.check_directory_access("create", default, True) return super(File, self).copy(default) def write(self, vals): self.check_directory_access("write", vals, True) self.check_lock() return super(File, self).write(vals) def unlink(self): self.check_access_rights("unlink") self.check_directory_access("unlink", {}, True) self.check_lock() # We need to do sudo because we don't know when the related groups # will be deleted return super(File, self.sudo()).unlink() @api.model_create_multi def create(self, vals_list): new_vals_list = [] for vals in vals_list: if "res_model" not in vals and "res_id" not in vals: vals = self._create_model_attachment(vals) new_vals_list.append(vals) return super(File, self).create(new_vals_list) # ---------------------------------------------------------- # Locking fields and functions # ---------------------------------------------------------- locked_by = fields.Many2one(comodel_name="res.users", string="Locked by") is_locked = fields.Boolean(compute="_compute_locked", string="Locked") is_lock_editor = fields.Boolean(compute="_compute_locked", string="Editor") # ---------------------------------------------------------- # Locking # ---------------------------------------------------------- def lock(self): self.write({"locked_by": self.env.uid}) def unlock(self): self.write({"locked_by": None}) @api.model def _check_lock_editor(self, lock_uid): return lock_uid in (self.env.uid, SUPERUSER_ID) def check_lock(self): for record in self: if record.locked_by.exists() and not self._check_lock_editor( record.locked_by.id ): message = _("The record (%s [%s]) is locked, by an other user.") raise AccessError(message % (record._description, record.id)) # ---------------------------------------------------------- # Read, View # ---------------------------------------------------------- @api.depends("locked_by") def _compute_locked(self): for record in self: if record.locked_by.exists(): record.update( { "is_locked": True, "is_lock_editor": record.locked_by.id == record.env.uid, } ) else: record.update({"is_locked": False, "is_lock_editor": False})
class student_student(models.Model): _name = "student.student" _description = "Student Information" _inherit = ['mail.thread', 'mail.activity.mixin'] name = fields.Char(string="Name", required=True, index=True, translate=True) active = fields.Boolean(string="Active", default=True) image = fields.Binary("Image") uni_no = fields.Char(string="Ministry University No.", required=True, copy=False) seat_no = fields.Char(string="Seat No.") dob = fields.Date(string="Date of Birth", required=True) age = fields.Integer(string="Age",readonly=True,store=True,compute="_get_calc_age") @api.onchange('dob') def _get_calc_age(self): for rec in self: d1 = datetime.strptime(str(date.today()),"%Y-%m-%d") d2 = datetime.strptime(str(rec.dob),"%Y-%m-%d") rec.age = (d1-d2).days/365 gender = fields.Selection([("male", "Male"), ("female", "Female")], "Gender", default="male") fdate = fields.Date("First Registration Date") ldate = fields.Datetime("Last Registration Date", readonly=True) degree_id = fields.Many2one("degree.detail", "Degree to Register For") result_ids = fields.One2many("schoolresult.result", "student_id", "Subject's Results") hobbies_ids = fields.Many2many("hobbies.details", "student_hobbies_rel", "student_id", "hobbie_id", "Hobbies Information") regfees = fields.Float("Registration Fees", default="0.0") tutfees = fields.Float("Tuition Fees ($)", default="0.0") @api.onchange('degree_id') def _get_tutfees(self): for rec in self: self.tutfees = self.degree_id.degfees totfees = fields.Float(string="Total Fees ($)", store=True, readonly=True, compute='_get_total_fees') @api.depends('regfees', 'tutfees') def _get_total_fees(self): for record in self: if not record.regfees: self.totfees = record.regfees + record.tutfees else: self.totfees = record.regfees + record.tutfees ref = fields.Reference( selection=[("res.partner", "Partner"), ("res.users", "User"), ("student.student", "Student")], string="Reference") ref_link = fields.Char("External Link") responsible_id = fields.Many2one("res.partner", "Responsible Person / Next Of Kin") phone = fields.Char(related="responsible_id.phone", string="NOK Phone") email = fields.Char(related="responsible_id.email", string="NOK Email") health_issues = fields.Selection([("yes", "Yes"), ("no", "No")], "Health Issues", default="no") health_notes = fields.Text("Health Issue(s) Details", copy=False) template = fields.Html("Template") # Represents state of student state = fields.Selection(STATE, "Status", readonly=True, default="draft") quality_check = fields.Selection([('',''),('good','good'),('very_good','very_good'),('excellent','excellent')],string="Quality Check") student_sequence = fields.Char(string='Number', required=True, copy=False, readonly=True, index=True, default=lambda self: _('New')) @api.model def create(self, vals): if vals.get('student_sequence', _('New')) == _('New'): vals['student_sequence'] = self.env['ir.sequence'].next_by_code('student.Sequence') or _('New') result = super(student_student, self).create(vals) return result
class PRTMailMessageDraft(models.Model): _name = "prt.mail.message.draft" _description = "Draft Message" _order = 'write_date desc, id desc' _rec_name = 'subject' # -- Set domain for subtype_id def _get_subtypes(self): return [('id', 'in', [ self.env['ir.model.data'].xmlid_to_res_id('mail.mt_comment'), self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note') ])] subject = fields.Char(string="Subject") subject_display = fields.Char(string="Subject", compute="_subject_display") body = fields.Html(string="Contents", default="", sanitize_style=True, strip_classes=True) model = fields.Char(sting="Related Document Model", index=True) res_id = fields.Integer(string="Related Document ID", index=True) subtype_id = fields.Many2one(string="Message Type", comodel_name='mail.message.subtype', domain=_get_subtypes, default=lambda self: self.env['ir.model.data'] .xmlid_to_res_id('mail.mt_comment'), required=True) parent_id = fields.Integer(string="Parent Message") author_id = fields.Many2one( string="Author", comodel_name='res.partner', index=True, ondelete='set null', default=lambda self: self.env.user.partner_id.id) partner_ids = fields.Many2many(string="Recipients", comodel_name='res.partner') record_ref = fields.Reference(string="Message Record", selection='_referenceable_models', compute='_record_ref') attachment_ids = fields.Many2many( string="Attachments", comodel_name='ir.attachment', relation='prt_message_draft_attachment_rel', column1='message_id', column2='attachment_id') ref_partner_ids = fields.Many2many(string="Followers", comodel_name='res.partner', compute='_message_followers') ref_partner_count = fields.Integer(string="Followers", compute='_ref_partner_count') wizard_mode = fields.Char(string="Wizard Mode", default='composition') signature_location = fields.Selection( [('b', 'Before quote'), ('a', 'Message bottom'), ('n', 'No signature')], string='Signature Location', default='b', required=True, help='Whether to put signature before or after the quoted text.') # -- Count ref Partners def _ref_partner_count(self): for rec in self: rec.ref_partner_count = len(rec.ref_partner_ids) # -- Get related record followers @api.depends('record_ref') def _message_followers(self): for rec in self: if rec.record_ref: rec.ref_partner_ids = rec.record_ref.message_partner_ids # -- Get Subject for tree view @api.depends('subject') def _subject_display(self): # Get model names first. Use this method to get translated values ir_models = self.env['ir.model'].search([ ('model', 'in', list(set(self.mapped('model')))) ]) model_dict = {} for model in ir_models: # Check if model has "name" field has_name = self.env['ir.model.fields'].sudo().search_count([ ('model_id', '=', model.id), ('name', '=', 'name') ]) model_dict.update({model.model: [model.name, has_name]}) # Compose subject for rec in self: subject_display = '=== No Reference ===' # Has reference if rec.record_ref: subject_display = model_dict.get(rec.model)[0] # Has 'name' field if model_dict.get(rec.model, False)[1]: subject_display = "%s: %s" % (subject_display, rec.record_ref.name) # Has subject if rec.subject: subject_display = "%s => %s" % (subject_display, rec.subject) # Set subject rec.subject_display = subject_display # -- Ref models @api.model def _referenceable_models(self): return [(x.model, x.name) for x in self.env['ir.model'].sudo().search([('model', '!=', 'mail.channel')]) ] # -- Compose reference @api.depends('res_id') def _record_ref(self): for rec in self: if rec.res_id: if rec.model: res = self.env[rec.model].sudo().search([("id", "=", rec.res_id)]) if res: rec.record_ref = res # -- Send message def send_it(self): self.ensure_one() # Compose message body return _select_draft(self)
class ConnectorCheckpoint(models.Model): _name = "connector.checkpoint" _description = "Connector Checkpoint" _inherit = ["mail.thread", "mail.activity.mixin"] @api.model def _reference_models(self): models = self.env["ir.model"].search([("state", "!=", "manual")]) return [(model.model, model.name) for model in models if not model.model.startswith("ir.")] @api.depends("model_id", "record_id") def _compute_record(self): for check in self: check.record = check.model_id.model + "," + str(check.record_id) @api.depends("model_id", "record_id") def _compute_name(self): for check in self: model = self.env[check.model_id.model] check.name = model.browse(check.record_id).display_name @api.model def _search_record(self, operator, value): model_model = self.env["ir.model"] sql = "SELECT DISTINCT model_id FROM connector_checkpoint" self.env.cr.execute(sql) model_ids = [row[0] for row in self.env.cr.fetchall()] models = model_model.browse(model_ids) ids = set() for model in models: model_id = model.id model_name = model.model model_obj = self.env[model_name] results = model_obj.name_search(name=value, operator=operator) res_ids = [res[0] for res in results] checks = self.search([("model_id", "=", model_id), ("record_id", "in", res_ids)]) ids.update(checks.ids) if not ids: return [("id", "=", "0")] return [("id", "in", tuple(ids))] @api.depends("backend_id") def _compute_company(self): for check in self: if hasattr(check.backend_id, "company_id"): check.company_id = check.backend_id.company_id.id record = fields.Reference( compute="_compute_record", selection="_reference_models", help="The record to review.", readonly=True, ) name = fields.Char( compute="_compute_name", search="_search_record", string="Record Name", help="Name of the record to review", readonly=True, ) record_id = fields.Integer(string="Record ID", required=True, readonly=True) model_id = fields.Many2one(comodel_name="ir.model", string="Model", required=True, readonly=True) backend_id = fields.Reference( string="Imported from", selection="_reference_models", readonly=True, required=True, help="The record has been imported from this backend", index=True, ) state = fields.Selection( selection=[("need_review", "Need Review"), ("reviewed", "Reviewed")], string="Status", required=True, readonly=True, default="need_review", ) company_id = fields.Many2one( "res.company", string="Company", readonly=True, compute="_compute_company", store=True, ) def reviewed(self): return self.write({"state": "reviewed"}) def _subscribe_users(self): """ Subscribe all users having the 'Connector Manager' group """ group = self.env.ref("connector.group_connector_manager") if not group: return users = self.env["res.users"].search([("groups_id", "=", group.id)]) self.message_subscribe(partner_ids=users.mapped("partner_id").ids) @api.model def create(self, vals): record = super(ConnectorCheckpoint, self).create(vals) record._subscribe_users() msg = _("A %s needs a review.") % record.model_id.name record.message_post(body=msg, subtype="mail.mt_comment") return record @api.model def create_from_name(self, model_name, record_id, backend_model_name, backend_id): model_model = self.env["ir.model"] model = model_model.search([("model", "=", model_name)], limit=1) assert model, "The model %s does not exist" % model_name backend = backend_model_name + "," + str(backend_id) return self.create({ "model_id": model.id, "record_id": record_id, "backend_id": backend }) @api.model def _needaction_domain_get(self): """ Returns the domain to filter records that require an action :return: domain or False is no action """ return [("state", "=", "need_review")]
class StockInventoryLine(models.Model): _inherit = 'stock.inventory.line' calc_product_cost = fields.Float(compute='_compute_product_cost', string='Computed cost', store=True, digits=dp.get_precision('Account')) manual_product_cost = fields.Float(string='Manual cost', digits=dp.get_precision('Account')) value = fields.Float(compute='_compute_value', string='Value', store=True, digits=dp.get_precision('Account')) cost_origin = fields.Text( compute='_compute_product_cost', store=True, help='Explain computation method for each product') reference = fields.Reference(selection='_select_models', compute='_compute_product_cost', store=True) def _select_models(self): models = self.env['ir.model'].search([('model', 'in', self._get_models())]) return [(x['model'], x['name']) for x in models] + [('', '')] @api.multi @api.depends('product_id', 'product_qty', 'manual_product_cost', 'inventory_id.state', 'inventory_id.to_recompute') def _compute_product_cost(self): custom_data_source = self._get_custom_data_source() po_l_obj = self.env['purchase.order.line'] product_ids = [x.product_id.id for x in self] # product_ids == [False] for first created line if not product_ids or not product_ids[0]: return invoices = self._get_invoice_data(product_ids, company=self.env.user.company_id) for line in self: # DON'T FORGET TO update SELECTION_ORDER # when changes are done in the order of the blocks if line.inventory_id.state not in ('done') and \ not line.inventory_id.to_recompute: line.cost_origin = _("Click on 'Compute cost'") continue if not line.inventory_id or not line.product_id: continue # get cost from supplier invoice explanation = '' cost_price = 0 reference = False if not cost_price: # get cost price from supplier info sup_info = line.product_id.seller_ids if sup_info and sup_info[0].price: cost_price = sup_info[0].price explanation = _('Supplier info') reference = 'product.supplierinfo,%s' % sup_info[0].id if not cost_price and custom_data_source: cost_price, explanation, reference = self._get_custom_cost( custom_data_source, product_ids) if not cost_price and invoices: cost_price = invoices[line.product_id.id].get('price_unit') explanation = _('Supplier Invoice') if invoices[line.product_id.id].get('id'): reference = 'account.invoice,%s' % invoices[ line.product_id.id].get('id') if not cost_price: # get cost price from purchase po_line = po_l_obj.search( [('product_id', '=', line.product_id.id), ('order_id.state', '=', 'done')], limit=1) if po_line: cost_price = po_line.price_unit explanation = _('Purchase') reference = 'purchase.order,%s' % po_line.order_id.id if not cost_price: if line.product_id.standard_price_: cost_price = line.product_id.standard_price_ explanation = _('Product manual standard_price_') reference = 'product.product,%s' % line.product_id.id if not cost_price: if line.product_id.standard_price: cost_price = line.product_id.standard_price explanation = _('Product standard_price') reference = 'product.product,%s' % line.product_id.id if not cost_price: explanation = _('No Cost found') reference = '' line.calc_product_cost = cost_price line.cost_origin = explanation if reference: line.reference = reference def _get_custom_data_source(self): return None def _get_custom_cost(self, custom_data_source, product_ids): return (None, None, None) @api.multi @api.depends('calc_product_cost', 'manual_product_cost', 'product_qty') def _compute_value(self): for line in self: if line.manual_product_cost: line.value = line.manual_product_cost * line.product_qty else: line.value = line.calc_product_cost * line.product_qty @api.model def _get_invoice_data(self, product_ids, company): invoices = defaultdict(dict) query = """ SELECT l.product_id, max(l.create_date) AS date FROM account_invoice_line l LEFT JOIN account_invoice i ON i.id = l.invoice_id WHERE l.product_id IN %s AND i.company_id = %s AND i.type = 'in_invoice' AND i.state in ('open', 'paid') GROUP BY 1 ORDER BY date ASC LIMIT 1 """ self.env.cr.execute(query, (tuple(product_ids), company.id)) oldier = self.env.cr.fetchall() if oldier: query = """ SELECT l.product_id, l.price_unit, i.id, i.number FROM account_invoice_line l LEFT JOIN account_invoice i ON i.id = l.invoice_id WHERE l.product_id IN %s AND i.company_id = %s AND l.create_date >= %s AND i.type = 'in_invoice' AND i.state in ('open', 'paid') ORDER BY l.create_date ASC """ self.env.cr.execute(query, (tuple(product_ids), company.id, oldier[0][1])) res = self.env.cr.fetchall() invoices = defaultdict(dict) for elm in res: invoices[elm[0]].update({ 'price_unit': elm[1], 'id': elm[2], 'number': elm[3] }) return invoices @api.model def _get_models(self): return [ 'account.invoice', 'purchase.order', 'product.supplierinfo', 'product.product', ] @api.multi def button_info_origin(self): self.ensure_one() if self.reference: return { 'type': 'ir.actions.act_window', 'view_mode': 'form', 'res_model': self.reference._model._name, 'res_id': self.reference.id, 'target': 'new', 'name': _("%s: %s: '%s'" % (self.reference._model._description, self.product_id.display_name, self.value)), } return False
class Voucher(models.Model): '''新建凭证''' _name = 'voucher' _inherit = ['mail.thread'] _order = 'period_id, name desc' _description = u'会计凭证' @api.model def _default_voucher_date(self): return self._default_voucher_date_impl() @api.model def _default_voucher_date_impl(self): ''' 获得默认的凭证创建日期 ''' voucher_setting = self.env['ir.values'].get_default( 'finance.config.settings', 'default_voucher_date') now_date = fields.Date.context_today(self) if voucher_setting == 'last' and self.search([], limit=1): create_date = self.search([], limit=1).date else: create_date = now_date return create_date @api.model def _select_objects(self): records = self.env['business.data.table'].search([]) models = self.env['ir.model'].search([ ('model', 'in', [record.name for record in records]) ]) return [(model.model, model.name) for model in models] @api.one @api.depends('date') def _compute_period_id(self): self.period_id = self.env['finance.period'].get_period(self.date) document_word_id = fields.Many2one( 'document.word', u'凭证字', ondelete='restrict', required=True, default=lambda self: self.env.ref('finance.document_word_1')) date = fields.Date(u'凭证日期', required=True, default=_default_voucher_date, states=READONLY_STATES, track_visibility='always', help=u'本张凭证创建的时间', copy=False) name = fields.Char(u'凭证号', track_visibility='always', copy=False) att_count = fields.Integer(u'附单据', default=1, help=u'原始凭证的张数', states=READONLY_STATES) period_id = fields.Many2one('finance.period', u'会计期间', compute='_compute_period_id', ondelete='restrict', store=True, help=u'本张凭证发生日期对应的,会计期间') line_ids = fields.One2many( 'voucher.line', 'voucher_id', u'凭证明细', copy=True, states=READONLY_STATES, ) amount_text = fields.Float(u'总计', compute='_compute_amount', store=True, track_visibility='always', help=u'凭证金额') state = fields.Selection([('draft', u'草稿'), ('done', u'已确认'), ('cancel', u'已作废')], u'状态', default='draft', index=True, track_visibility='always', help=u'凭证所属状态!') is_checkout = fields.Boolean(u'结账凭证', help=u'是否是结账凭证') is_init = fields.Boolean(u'是否初始化凭证', help=u'是否是初始化凭证') company_id = fields.Many2one( 'res.company', string=u'公司', change_default=True, default=lambda self: self.env['res.company']._company_default_get()) ref = fields.Reference(string=u'前置单据', selection='_select_objects') @api.one def voucher_done(self): """ 确认 凭证按钮 所调用的方法 :return: 主要是把 凭证的 state改变 """ if self.state == 'done': raise UserError(u'凭证%s已经确认,请不要重复确认!' % self.name) if self.period_id.is_closed: raise UserError(u'该会计期间已结账!不能确认') if not self.line_ids: raise ValidationError(u'请输入凭证行') for line in self.line_ids: if line.debit + line.credit == 0: raise ValidationError(u'单行凭证行 %s 借和贷不能同时为0' % line.account_id.name) if line.debit * line.credit != 0: raise ValidationError( u'单行凭证行不能同时输入借和贷\n 摘要为%s的凭证行 借方为:%s 贷方为:%s' % (line.name, line.debit, line.credit)) debit_sum = sum([line.debit for line in self.line_ids]) credit_sum = sum([line.credit for line in self.line_ids]) precision = self.env['decimal.precision'].precision_get('Amount') debit_sum = round(debit_sum, precision) credit_sum = round(credit_sum, precision) if debit_sum != credit_sum: raise ValidationError(u'借贷方不平,无法确认!\n 借方合计:%s 贷方合计:%s' % (debit_sum, credit_sum)) self.state = 'done' ''' #xuan if self.is_checkout: # 月结凭证不做反转 return True for line in self.line_ids: if line.account_id.costs_types == 'out' and line.credit: # 费用类科目只能在借方记账,比如银行利息收入 line.debit = -line.credit line.credit = 0 if line.account_id.costs_types == 'in' and line.debit: # 收入类科目只能在贷方记账,比如退款给客户的情况 line.credit = -line.debit line.debit = 0 ''' @api.one def voucher_can_be_draft(self): if self.ref: raise UserError(u'不能撤销确认由其他单据生成的凭证!') self.voucher_draft() @api.one def voucher_draft(self): if self.state == 'draft': raise UserError(u'凭证%s已经撤销确认,请不要重复撤销!' % self.name) if self.period_id.is_closed: raise UserError(u'%s期 会计期间已结账!不能撤销确认' % self.period_id.name) self.state = 'draft' @api.one @api.depends('line_ids') def _compute_amount(self): # todo 实现明细行总金额 self.amount_text = str(sum([line.debit for line in self.line_ids])) # 重载write 方法 @api.multi def write(self, vals): for order in self: # 还需要进一步优化 if self.env.context.get('call_module', False) == "checkout_wizard": return super(Voucher, self).write(vals) if order.period_id.is_closed is True: raise UserError(u'%s期 会计期间已结账,凭证不能再修改!' % order.period_id.name) if len(vals) == 1 and vals.get('state', False): # 确认or撤销确认 return super(Voucher, self).write(vals) else: order = self.browse(order.id) if order.state == 'done': raise UserError(u'凭证%s已确认!修改请先撤销!' % order.name) return super(Voucher, self).write(vals)
class File(models.Model): _name = 'ag_dms.file' _description = "File" _inherit = [ 'ag_dms.locking', 'ag_dms.access', 'mail.thread', 'mail.activity.mixin' ] #---------------------------------------------------------- # Database #---------------------------------------------------------- name = fields.Char(string="Filename", required=True, index=True) file_name = fields.Char(string="ContentFilename", index=True) active = fields.Boolean( string="Archived", default=True, help= "If a file is set to archived, it is not displayed, but still exists.") is_top_file = fields.Boolean(string="Top Directory", compute='_compute_is_top_file', search='_search_is_top_file') settings = fields.Many2one(comodel_name='ag_dms.settings', string="Settings", store=True, auto_join=True, ondelete='restrict', compute='_compute_settings') show_tree = fields.Boolean(string="Show Structure", related="settings.show_tree", readonly=True) color = fields.Integer(string="Color") tags = fields.Many2many(comodel_name='ag_dms.tag', relation='ag_dms_file_tag_rel', column1='fid', column2='tid', string='Tags') category = fields.Many2one(string="Category", comodel_name='ag_dms.category', related="directory.category", readonly=True, store=True) content = fields.Binary(string='Content', required=True, compute='_compute_content', inverse='_inverse_content') reference = fields.Reference(selection=[('ag_dms.data', _('Data'))], string="Data Reference", readonly=True) directory = fields.Many2one(comodel_name='ag_dms.directory', string="Directory", domain="[('permission_create', '=', True)]", ondelete='restrict', auto_join=True, required=True) extension = fields.Char(string='Extension', compute='_compute_extension', readonly=True, store=True) mimetype = fields.Char(string='Type', compute='_compute_mimetype', readonly=True, store=True) size = fields.Integer(string='Size', readonly=True) path = fields.Char(string="Path", store=True, index=True, readonly=True, compute='_compute_path') relational_path = fields.Text(string="Path", store=True, readonly=True, compute='_compute_relational_path') custom_thumbnail = fields.Binary(string="Custom Thumbnail") custom_thumbnail_medium = fields.Binary(string="Medium Custom Thumbnail") custom_thumbnail_small = fields.Binary(string="Small Custom Thumbnail") thumbnail = fields.Binary(compute='_compute_thumbnail', string="Thumbnail") thumbnail_medium = fields.Binary(compute='_compute_thumbnail_medium', string="Medium Thumbnail") thumbnail_small = fields.Binary(compute='_compute_thumbnail_small', string="Small Thumbnail") #---------------------------------------------------------- # Functions #---------------------------------------------------------- @api.multi def notify_change(self, values, *largs, **kwargs): super(File, self).notify_change(values, *largs, **kwargs) if "save_type" in values: self._update_reference_type() @api.multi def trigger_computation_up(self, fields, *largs, **kwargs): if ("no_trigger_computation_up" not in self.env.context): self.mapped('directory').suspend_security().with_context( is_parent=True).trigger_computation(fields) @api.multi def trigger_computation(self, fields, *largs, **kwargs): super(File, self).trigger_computation(fields, refresh=True, *largs, **kwargs) for record in self: values = {} if "settings" in fields: values.update(record._compute_settings(write=False)) if "path" in fields: values.update(record._compute_path(write=False)) values.update(record._compute_relational_path(write=False)) if "extension" in fields: values.update(record._compute_extension(write=False)) if "mimetype" in fields: values.update(record._compute_mimetype(write=False)) if values: record.write(values) if "settings" in fields: record.notify_change( {'save_type': record.settings.save_type}) @api.model def max_upload_size(self): params = self.env['ir.config_parameter'].sudo() return int(params.get_param('ag_dms.max_upload_size', default=25)) @api.model def forbidden_extensions(self): params = self.env['ir.config_parameter'].sudo() forbidden_extensions = params.get_param('ag_dms.forbidden_extensions', default="") return [x.strip() for x in forbidden_extensions.split(',')] #---------------------------------------------------------- # Search #---------------------------------------------------------- @api.model def _search_permission_create(self, operator, operand): res = super(File, self)._search_permission_create(operator, operand) records = self.browse( res[0][2]).filtered(lambda r: r.directory.check_access('create')) if operator == '=' and operand: return [('id', 'in', records.mapped('id'))] return [('id', 'not in', records.mapped('id'))] @api.model def _search_is_top_file(self, operator, operand): files = self.search( []).filtered(lambda f: not f.directory.check_access('read')) if operator == '=' and operand: return [('id', 'in', files.mapped('id'))] return [('id', 'not in', files.mapped('id'))] #---------------------------------------------------------- # Read, View #---------------------------------------------------------- @api.multi def _compute_settings(self, write=True): if write: for record in self: record.settings = record.directory.settings else: self.ensure_one() return {'settings': self.directory.settings.id} @api.multi def _compute_extension(self, write=True): if write: for record in self: record.extension = os.path.splitext(record.name)[1] else: self.ensure_one() return {'extension': os.path.splitext(self.name)[1]} @api.multi def _compute_mimetype(self, write=True): def get_mimetype(record): mimetype = mimetypes.guess_type(record.name)[0] if (not mimetype or mimetype == 'application/octet-stream') and record.content: mimetype = guess_mimetype(base64.b64decode(record.content)) return mimetype or 'application/octet-stream' if write: for record in self: record.mimetype = get_mimetype(record) else: self.ensure_one() return {'mimetype': get_mimetype(self)} @api.multi def _compute_path(self, write=True): if write: for record in self: record.path = "%s%s" % (record.directory.path, record.name) else: self.ensure_one() return {'path': "%s%s" % (self.directory.path, self.name)} @api.multi def _compute_relational_path(self, write=True): def get_relational_path(record): path = json.loads(record.directory.relational_path) path.append({ 'model': record._name, 'id': record.id, 'name': record.name }) return json.dumps(path) if write: for record in self: record.relational_path = get_relational_path(record) else: self.ensure_one() return {'relational_path': get_relational_path(self)} @api.multi def _compute_content(self): for record in self: record.content = record._get_content() @api.multi def _compute_permissions(self): super(File, self)._compute_permissions() for record in self: record.permission_create = record.permission_create and record.directory.check_access( 'create') @api.depends('directory') def _compute_is_top_file(self): for record in self: record.is_top_file = record.directory.check_access('read') @api.depends('custom_thumbnail') def _compute_thumbnail(self): for record in self: if record.custom_thumbnail: record.thumbnail = record.with_context({}).custom_thumbnail else: extension = record.extension and record.extension.strip( ".") or "" path = os.path.join(_img_path, "file_%s.png" % extension) if not os.path.isfile(path): path = os.path.join(_img_path, "file_unkown.png") with open(path, "rb") as image_file: record.thumbnail = base64.b64encode(image_file.read()) @api.depends('custom_thumbnail_medium') def _compute_thumbnail_medium(self): for record in self: if record.custom_thumbnail_medium: record.thumbnail_medium = record.with_context( {}).custom_thumbnail_medium else: extension = record.extension and record.extension.strip( ".") or "" path = os.path.join(_img_path, "file_%s_128x128.png" % extension) if not os.path.isfile(path): path = os.path.join(_img_path, "file_unkown_128x128.png") with open(path, "rb") as image_file: record.thumbnail_medium = base64.b64encode( image_file.read()) @api.depends('custom_thumbnail_small') def _compute_thumbnail_small(self): for record in self: if record.custom_thumbnail_small: record.thumbnail_small = record.with_context( {}).custom_thumbnail_small else: extension = record.extension and record.extension.strip( ".") or "" path = os.path.join(_img_path, "file_%s_64x64.png" % extension) if not os.path.isfile(path): path = os.path.join(_img_path, "file_unkown_64x64.png") with open(path, "rb") as image_file: record.thumbnail_small = base64.b64encode( image_file.read()) #---------------------------------------------------------- # Create, Update, Delete #---------------------------------------------------------- @api.constrains('name') def _check_name(self): if not self.check_name(self.name): raise ValidationError(_("The file name is invalid.")) childs = self.sudo().directory.files.mapped( lambda rec: [rec.id, rec.name]) duplicates = [ rec for rec in childs if rec[1] == self.name and rec[0] != self.id ] if duplicates: raise ValidationError( _("A file with the same name already exists.")) @api.constrains('name') def _check_extension(self): forbidden_extensions = self.forbidden_extensions() file_extension = self._compute_extension(write=False)['extension'] if file_extension and file_extension in forbidden_extensions: raise ValidationError( _("The file has a forbidden file extension.")) @api.constrains('content') def _check_size(self): max_upload_size = self.max_upload_size() if (max_upload_size * 1024 * 1024) < len(base64.b64decode( self.content)): raise ValidationError( _("The maximum upload size is %s MB).") % max_upload_size) @api.constrains('directory') def _check_directory_access(self): for record in self: record.directory.check_access('create', raise_exception=True) @api.multi def _inverse_content(self): for record in self: if record.content: content = record.content size = len(base64.b64decode(content)) reference = record.reference if reference: record._update_reference_values({'content': content}) record.write({'size': size}) else: directory = record.directory settings = record.settings if record.settings else directory.settings save_type = settings and settings.read(['save_type']) reference = record._create_reference( settings, directory.path, record.name, content) if reference: reference = "%s,%s" % (reference._name, reference.id) record.write({'reference': reference, 'size': size}) else: _logger.warning( "Something went wrong %s not supported!" % save_type) else: record._unlink_reference() record.reference = None @api.model def _before_create(self, vals, *largs, **kwargs): tools.image_resize_images(vals, big_name='custom_thumbnail', medium_name='custom_thumbnail_medium', small_name='custom_thumbnail_small') return super(File, self)._before_create(vals, *largs, **kwargs) @api.multi def _before_write(self, vals, *largs, **kwargs): tools.image_resize_images(vals, big_name='custom_thumbnail', medium_name='custom_thumbnail_medium', small_name='custom_thumbnail_small') return super(File, self)._before_write(vals, *largs, **kwargs) @api.multi def _before_unlink(self, *largs, **kwargs): info = super(File, self)._before_unlink(*largs, **kwargs) records = self.filtered(lambda r: r.reference) references = [ list((k, list(map(lambda rec: rec.reference.id, v)))) for k, v in itertools.groupby( records.sorted(key=lambda rec: rec.reference._name), lambda rec: rec.reference._name) ] info['references'] = references return info @api.multi def _after_unlink(self, result, info, infos, *largs, **kwargs): super(File, self)._after_unlink(result, info, infos, *largs, **kwargs) if 'references' in info and info['references']: for tuple in info['references']: self.env[tuple[0]].sudo().browse(list(filter( None, tuple[1]))).unlink() @api.returns('self', lambda value: value.id) def copy(self, default=None): self.ensure_one() default = dict(default or []) names = [] if 'directory' in default: directory = self.env['ag_dms.directory'].sudo().browse( default['directory']) names = directory.files.mapped('name') else: names = self.sudo().directory.files.mapped('name') default.update( {'name': self.unique_name(self.name, names, self.extension)}) vals = self.copy_data(default)[0] if 'reference' in vals: del vals['reference'] if not 'content' in vals: vals.update({'content': self.content}) new = self.with_context(lang=None).create(vals) self.copy_translations(new) return new @api.multi def _check_recomputation(self, vals, olds, *largs, **kwargs): super(File, self)._check_recomputation(vals, olds, *largs, **kwargs) fields = [] if 'name' in vals: fields.extend(['extension', 'mimetype', 'path']) if 'directory' in vals: fields.extend(['settings', 'path']) if 'content' in vals: fields.extend(['index_content']) if fields: self.trigger_computation(fields) self._check_reference_values(vals) #---------------------------------------------------------- # Reference #---------------------------------------------------------- @api.multi def _create_reference(self, settings, path, filename, content): self.ensure_one() self.check_access('create', raise_exception=True) if settings.read(['save_type']) and settings.save_type == 'database': return self.env['ag_dms.data_database'].sudo().create( {'data': content}) return None @api.multi def _update_reference_values(self, values): self.check_access('write', raise_exception=True) references = self.sudo().mapped('reference') if references: references.sudo().update(values) @api.multi def _update_reference_type(self): self.check_access('write', raise_exception=True) for record in self: save_type = record.settings.read([ 'save_type' ]) and record.settings and record.settings.save_type if record.reference and save_type and save_type != record.reference.type( ): reference = record._create_reference(record.settings, record.directory.path, record.name, record.content) if reference: record._unlink_reference() record.reference = "%s,%s" % (reference._name, reference.id) else: _logger.warning("Something went wrong %s not supported!" % save_type) @api.multi def _check_reference_values(self, values): self.check_access('write', raise_exception=True) if 'content' in values: self._update_reference_values({'content': values['content']}) if 'settings' in values: self._update_reference_type() @api.multi def _get_content(self): self.ensure_one() self.check_access('read', raise_exception=True) return self.reference.sudo().content() if self.reference else None @api.multi def _unlink_reference(self): self.check_access('unlink', raise_exception=True) record = self.filtered(lambda r: r.reference) for tuple in [ list((k, list(map(lambda rec: rec.reference.id, v)))) for k, v in itertools.groupby( record.sorted(key=lambda rec: rec.reference._name), lambda rec: rec.reference._name) ]: self.env[tuple[0]].sudo().browse(list(filter(None, tuple[1]))).unlink()
class ExternalSystem(models.Model): _name = 'external.system' _description = 'External System' name = fields.Char( required=True, help='This is the canonical (humanized) name for the system.', ) host = fields.Char( help='This is the domain or IP address that the system can be reached ' 'at.', ) port = fields.Integer( help='This is the port number that the system is listening on.', ) username = fields.Char( help='This is the username that is used for authenticating to this ' 'system, if applicable.', ) password = fields.Char( help='This is the password that is used for authenticating to this ' 'system, if applicable.', ) private_key = fields.Text( help='This is the private key that is used for authenticating to ' 'this system, if applicable.', ) private_key_password = fields.Text( help='This is the password to unlock the private key that was ' 'provided for this sytem.', ) fingerprint = fields.Text( help='This is the fingerprint that is advertised by this system in ' 'order to validate its identity.', ) ignore_fingerprint = fields.Boolean( default=True, help='Set this to `True` in order to ignore an invalid/unknown ' 'fingerprint from the system.', ) remote_path = fields.Char( help='Restrict to this directory path on the remote, if applicable.', ) company_ids = fields.Many2many( string='Companies', comodel_name='res.company', required=True, default=lambda s: [(6, 0, s.env.user.company_id.ids)], help='Access to this system is restricted to these companies.', ) system_type = fields.Selection( selection='_get_system_types', required=True, ) interface = fields.Reference( selection='_get_system_types', readonly=True, help='This is the interface that this system represents. It is ' 'created automatically upon creation of the external system.', ) _sql_constraints = [ ('name_uniq', 'UNIQUE(name)', 'Connection name must be unique.'), ] @api.model def _get_system_types(self): """Return the adapter interface models that are installed.""" adapter = self.env['external.system.adapter'] return [(m, self.env[m]._description) for m in adapter._inherit_children] @api.multi @api.constrains('fingerprint', 'ignore_fingerprint') def check_fingerprint_ignore_fingerprint(self): """Do not allow a blank fingerprint if not set to ignore.""" for record in self: if not record.ignore_fingerprint and not record.fingerprint: raise ValidationError( _( 'Fingerprint cannot be empty when Ignore Fingerprint is ' 'not checked.', )) @api.multi @contextmanager def client(self): """Client object usable as a context manager to include destruction. Yields the result from ``external_get_client``, then calls ``external_destroy_client`` to cleanup the client. Yields: mixed: An object representing the client connection to the remote system. """ with self.interface.client() as client: yield client @api.model def create(self, vals): """Create the interface for the record and assign to ``interface``.""" record = super(ExternalSystem, self).create(vals) if not self.env.context.get('no_create_interface'): interface = self.env[vals['system_type']].create({ 'system_id': record.id, }) record.interface = interface return record @api.multi def action_test_connection(self): """Test the connection to the external system.""" self.ensure_one() self.interface.external_test_connection()
class SDP(models.Model): _name = 'wbs.cost.line' _rec_name = 'wbs_id' wbs_id = fields.Many2one('project.wbs', u"WBS", required=True) type_id = fields.Many2one('wbs.cost.type', u"Type", required=True) reference = fields.Reference(string='Document', selection='_reference_models', required=True) unit_cost = fields.Float(u'Coût unitaire') account_id = fields.Many2one('account.account', string=u'Compte comptable') product_id = fields.Many2one('product.product', string=u'Product') qty = fields.Float(u'Quatité', required=True) date = fields.Date(u'Date', required=True) cost = fields.Float(u'Coût total', compute="get_price") analytic_line_id = fields.Many2one('account.analytic.line', u'Analytical line', readonly=True) def name_get(self): res = [] for line in self: name = line.type_id.name if line.reference: name += '/' + str(line.reference.name) res.append((line.id, name)) return res def get_price(self): for record in self: record.cost = record.qty * record.unit_cost @api.model def _reference_models(self): types = self.env['wbs.cost.type'].search([]) models = [] for type in types: models.append((type.model_id.model, type.model_id.name)) return models @api.onchange('reference') def onchange_reference(self): if self.reference: type = self.env['wbs.cost.type'].search([('model_id.model', '=', self.reference._name)]) if type: account_id = False self.type_id = type.id if self.reference.read([type.field_id.name]): self.unit_cost = self.reference.read( [type.field_id.name])[0][type.field_id.name] print('ffffff', type.account_field_id.name) if type.account_field_id.name and self.reference.read( [type.account_field_id.name]): if self.reference.read([ type.account_field_id.name ])[0].get(type.account_field_id.name, False): account_id = self.reference.read([ type.account_field_id.name ])[0][type.account_field_id.name] if not account_id: if type.account_id: account_id = type.account_id self.account_id = account_id if type.product_field_id.name and self.reference.read([ type.product_field_id.name ])[0].get(type.product_field_id.name, False): self.product_id = self.reference.read([ type.product_field_id.name ])[0][type.product_field_id.name] @api.onchange('type_id') def onchange_type(self): if self.reference: type = self.env['wbs.cost.type'].search([('model_id.model', '=', self.reference._name)]) if type: self.type_id = type.id
class Subscription(models.Model): _name = "subscription.subscription" _description = "Subscription" name = fields.Char(required=True) active = fields.Boolean( help= "If the active field is set to False, it will allow you to hide the subscription without removing it.", default=True) partner_id = fields.Many2one('res.partner', string='Partner') notes = fields.Text(string='Internal Notes') user_id = fields.Many2one('res.users', string='User', required=True, default=lambda self: self.env.user) interval_number = fields.Integer(string='Internal Qty', default=1) interval_type = fields.Selection([('days', 'Days'), ('weeks', 'Weeks'), ('months', 'Months')], string='Interval Unit', default='months') exec_init = fields.Integer(string='Number of Documents') date_init = fields.Datetime(string='First Date', default=fields.Datetime.now) state = fields.Selection([('draft', 'Draft'), ('running', 'Running'), ('done', 'Done')], string='Status', copy=False, default='draft') doc_source = fields.Reference( selection=_get_document_types, string='Source Document', required=True, help= "User can choose the source document on which he wants to create documents" ) doc_lines = fields.One2many('subscription.subscription.history', 'subscription_id', string='Documents created', readonly=True) cron_id = fields.Many2one('ir.cron', string='Cron Job', help="Scheduler which runs on subscription", states={ 'running': [('readonly', True)], 'done': [('readonly', True)] }) note = fields.Text(string='Notes', help="Description or Summary of Subscription") @api.model def _auto_end(self): super(Subscription, self)._auto_end() # drop the FK from subscription to ir.cron, as it would cause deadlocks # during cron job execution. When model_copy() tries to write() on the subscription, # it has to wait for an ExclusiveLock on the cron job record, but the latter # is locked by the cron system for the duration of the job! # FIXME: the subscription module should be reviewed to simplify the scheduling process # and to use a unique cron job for all subscriptions, so that it never needs to # be updated during its execution. self.env.cr.execute("ALTER TABLE %s DROP CONSTRAINT %s" % (self._table, '%s_cron_id_fkey' % self._table)) @api.multi def set_process(self): model_subscription = self.env['ir.model'].search([('model', '=', self._name)]) for subscription in self: cron_data = { 'name': subscription.name, 'interval_number': subscription.interval_number, 'interval_type': subscription.interval_type, 'numbercall': subscription.exec_init, 'nextcall': subscription.date_init, 'model_id': model_subscription.id, 'state': 'code', 'code': 'model._cron_model_copy(' + str(self.id) + ')' } cron = self.env['ir.cron'].sudo().create(cron_data) subscription.write({'cron_id': cron.id, 'state': 'running'}) @api.model def _cron_model_copy(self, ids): self.browse(ids).model_copy() @api.multi def model_copy(self): for subscription in self.filtered(lambda sub: sub.cron_id): if not subscription.doc_source.exists(): raise UserError( _('Please provide another source document.\nThis one does not exist!' )) default = {'state': 'draft'} documents = self.env['subscription.document'].search( [('model.model', '=', subscription.doc_source._name)], limit=1) fieldnames = dict( (f.field.name, f.value == 'date' and fields.Date.today() or False) for f in documents.field_ids) default.update(fieldnames) # if there was only one remaining document to generate # the subscription is over and we mark it as being done if subscription.cron_id.numbercall == 1: subscription.write({'state': 'done'}) else: subscription.write({'state': 'running'}) copied_doc = subscription.doc_source.copy(default) self.env['subscription.subscription.history'].create({ 'subscription_id': subscription.id, 'date': fields.Datetime.now(), 'document_id': '%s,%s' % (subscription.doc_source._name, copied_doc.id) }) @api.multi def unlink(self): if any(self.filtered(lambda s: s.state == "running")): raise UserError(_('You cannot delete an active subscription!')) return super(Subscription, self).unlink() @api.multi def set_done(self): self.mapped('cron_id').write({'active': False}) self.write({'state': 'done'}) @api.multi def set_draft(self): self.write({'state': 'draft'})
class ImportSourceConsumerMixin(models.AbstractModel): """Source consumer mixin. Inheriting models can setup, configure and use import sources. Relation towards source records is generic to grant maximum freedom on which source type to use. """ _name = "import.source.consumer.mixin" _description = "Import source consumer" source_id = fields.Integer(string="Source ID", required=False, ondelete="cascade") source_model = fields.Selection(string="Source type", selection="_selection_source_ref_id") source_ref_id = fields.Reference( string="Source", compute="_compute_source_ref_id", selection="_selection_source_ref_id", # NOTE: do not store a computed fields.Reference, Odoo crashes # with an error message "Mixing appels and orange..." when performing # a self.recompute() on such fields. store=False, ) source_config_summary = fields.Html( compute="_compute_source_config_summary", readonly=True) @api.depends("source_model", "source_id") def _compute_source_ref_id(self): for item in self: item.source_ref_id = False if not item.source_id or not item.source_model: continue item.source_ref_id = "{0.source_model},{0.source_id}".format(item) @api.model def _selection_source_ref_id(self): return [("import.source.csv", "CSV"), ("import.source.csv.std", "Odoo CSV")] @api.depends("source_ref_id") def _compute_source_config_summary(self): for item in self: item.source_config_summary = False if not item.source_ref_id: continue item.source_config_summary = item.source_ref_id.config_summary def open_source_config(self): self.ensure_one() action = self.env[self.source_model].get_formview_action() action.update({ "views": [(self.env[self.source_model].get_config_view_id(), "form")], "res_id": self.source_id, "target": "new", }) return action def get_source(self): """Return the source to the consumer.""" return self.source_ref_id
class IrUiMenu(models.Model): _name = 'ir.ui.menu' _description = 'Menu' _order = "sequence,id" _parent_store = True def __init__(self, *args, **kwargs): super(IrUiMenu, self).__init__(*args, **kwargs) self.pool['ir.model.access'].register_cache_clearing_method( self._name, 'clear_caches') name = fields.Char(string='Menu', required=True, translate=True) active = fields.Boolean(default=True) sequence = fields.Integer(default=10) child_id = fields.One2many('ir.ui.menu', 'parent_id', string='Child IDs') parent_id = fields.Many2one('ir.ui.menu', string='Parent Menu', index=True, ondelete="restrict") parent_path = fields.Char(index=True) groups_id = fields.Many2many('res.groups', 'ir_ui_menu_group_rel', 'menu_id', 'gid', string='Groups', help="If you have groups, the visibility of this menu will be based on these groups. "\ "If this field is empty, Odoo will compute visibility based on the related object's read access.") complete_name = fields.Char(string='Full Path', compute='_compute_complete_name', recursive=True) web_icon = fields.Char(string='Web Icon File') action = fields.Reference(selection=[( 'ir.actions.report', 'ir.actions.report' ), ('ir.actions.act_window', 'ir.actions.act_window'), ( 'ir.actions.act_url', 'ir.actions.act_url'), ( 'ir.actions.server', 'ir.actions.server'), ('ir.actions.client', 'ir.actions.client')]) web_icon_data = fields.Binary(string='Web Icon Image', attachment=True) @api.depends('name', 'parent_id.complete_name') def _compute_complete_name(self): for menu in self: menu.complete_name = menu._get_full_name() def _get_full_name(self, level=6): """ Return the full name of ``self`` (up to a certain level). """ if level <= 0: return '...' if self.parent_id: return self.parent_id._get_full_name( level - 1) + MENU_ITEM_SEPARATOR + (self.name or "") else: return self.name def read_image(self, path): if not path: return False path_info = path.split(',') icon_path = get_module_resource(path_info[0], path_info[1]) icon_image = False if icon_path: with tools.file_open(icon_path, 'rb') as icon_file: icon_image = base64.encodebytes(icon_file.read()) return icon_image @api.constrains('parent_id') def _check_parent_id(self): if not self._check_recursion(): raise ValidationError( _('Error! You cannot create recursive menus.')) @api.model @tools.ormcache('frozenset(self.env.user.groups_id.ids)', 'debug') def _visible_menu_ids(self, debug=False): """ Return the ids of the menu items visible to the user. """ # retrieve all menus, and determine which ones are visible context = {'ir.ui.menu.full_list': True} menus = self.with_context(context).search([]).sudo() groups = self.env.user.groups_id if not debug: groups = groups - self.env.ref('base.group_no_one') # first discard all menus with groups the user does not have menus = menus.filtered( lambda menu: not menu.groups_id or menu.groups_id & groups) # take apart menus that have an action action_menus = menus.filtered(lambda m: m.action and m.action.exists()) folder_menus = menus - action_menus visible = self.browse() # process action menus, check whether their action is allowed access = self.env['ir.model.access'] MODEL_GETTER = { 'ir.actions.act_window': lambda action: action.res_model, 'ir.actions.report': lambda action: action.model, 'ir.actions.server': lambda action: action.model_id.model, } for menu in action_menus: get_model = MODEL_GETTER.get(menu.action._name) if not get_model or not get_model(menu.action) or \ access.check(get_model(menu.action), 'read', False): # make menu visible, and its folder ancestors, too visible += menu menu = menu.parent_id while menu and menu in folder_menus and menu not in visible: visible += menu menu = menu.parent_id return set(visible.ids) @api.returns('self') def _filter_visible_menus(self): """ Filter `self` to only keep the menu items that should be visible in the menu hierarchy of the current user. Uses a cache for speeding up the computation. """ visible_ids = self._visible_menu_ids( request.session.debug if request else False) return self.filtered(lambda menu: menu.id in visible_ids) @api.model def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): menu_ids = super(IrUiMenu, self)._search(args, offset=0, limit=None, order=order, count=False, access_rights_uid=access_rights_uid) menus = self.browse(menu_ids) if menus: # menu filtering is done only on main menu tree, not other menu lists if not self._context.get('ir.ui.menu.full_list'): menus = menus._filter_visible_menus() if offset: menus = menus[offset:] if limit: menus = menus[:limit] return len(menus) if count else menus.ids def name_get(self): return [(menu.id, menu._get_full_name()) for menu in self] @api.model_create_multi def create(self, vals_list): self.clear_caches() for values in vals_list: if 'web_icon' in values: values['web_icon_data'] = self._compute_web_icon_data( values.get('web_icon')) return super(IrUiMenu, self).create(vals_list) def write(self, values): self.clear_caches() if 'web_icon' in values: values['web_icon_data'] = self._compute_web_icon_data( values.get('web_icon')) return super(IrUiMenu, self).write(values) def _compute_web_icon_data(self, web_icon): """ Returns the image associated to `web_icon`. `web_icon` can either be: - an image icon [module, path] - a built icon [icon_class, icon_color, background_color] and it only has to call `read_image` if it's an image. """ if web_icon and len(web_icon.split(',')) == 2: return self.read_image(web_icon) def unlink(self): # Detach children and promote them to top-level, because it would be unwise to # cascade-delete submenus blindly. We also can't use ondelete=set null because # that is not supported when _parent_store is used (would silently corrupt it). # TODO: ideally we should move them under a generic "Orphans" menu somewhere? extra = {'ir.ui.menu.full_list': True, 'active_test': False} direct_children = self.with_context(**extra).search([('parent_id', 'in', self.ids)]) direct_children.write({'parent_id': False}) self.clear_caches() return super(IrUiMenu, self).unlink() def copy(self, default=None): record = super(IrUiMenu, self).copy(default=default) match = NUMBER_PARENS.search(record.name) if match: next_num = int(match.group(1)) + 1 record.name = NUMBER_PARENS.sub('(%d)' % next_num, record.name) else: record.name = record.name + '(1)' return record @api.model @api.returns('self') def get_user_roots(self): """ Return all root menu ids visible for the user. :return: the root menu ids :rtype: list(int) """ return self.search([('parent_id', '=', False)]) def _load_menus_blacklist(self): return [] @api.model @tools.ormcache_context('self._uid', keys=('lang', )) def load_menus_root(self): fields = ['name', 'sequence', 'parent_id', 'action', 'web_icon_data'] menu_roots = self.get_user_roots() menu_roots_data = menu_roots.read(fields) if menu_roots else [] menu_root = { 'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children': menu_roots_data, 'all_menu_ids': menu_roots.ids, } xmlids = menu_roots._get_menuitems_xmlids() for menu in menu_roots_data: menu['xmlid'] = xmlids[menu['id']] return menu_root @api.model @tools.ormcache_context('self._uid', 'debug', keys=('lang', )) def load_menus(self, debug): """ Loads all menu items (all applications and their sub-menus). :return: the menu root :rtype: dict('children': menu_nodes) """ fields = [ 'name', 'sequence', 'parent_id', 'action', 'web_icon', 'web_icon_data' ] menu_roots = self.get_user_roots() menu_roots_data = menu_roots.read(fields) if menu_roots else [] menu_root = { 'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children': [menu['id'] for menu in menu_roots_data], } all_menus = {'root': menu_root} if not menu_roots_data: return all_menus # menus are loaded fully unlike a regular tree view, cause there are a # limited number of items (752 when all 6.1 addons are installed) menus_domain = [('id', 'child_of', menu_roots.ids)] blacklisted_menu_ids = self._load_menus_blacklist() if blacklisted_menu_ids: menus_domain = expression.AND( [menus_domain, [('id', 'not in', blacklisted_menu_ids)]]) menus = self.search(menus_domain) menu_items = menus.read(fields) xmlids = (menu_roots + menus)._get_menuitems_xmlids() # add roots at the end of the sequence, so that they will overwrite # equivalent menu items from full menu read when put into id:item # mapping, resulting in children being correctly set on the roots. menu_items.extend(menu_roots_data) # set children ids and xmlids menu_items_map = { menu_item["id"]: menu_item for menu_item in menu_items } for menu_item in menu_items: menu_item.setdefault('children', []) parent = menu_item['parent_id'] and menu_item['parent_id'][0] menu_item['xmlid'] = xmlids.get(menu_item['id'], "") if parent in menu_items_map: menu_items_map[parent].setdefault('children', []).append(menu_item['id']) all_menus.update(menu_items_map) # sort by sequence for menu_id in all_menus: all_menus[menu_id]['children'].sort( key=lambda id: all_menus[id]['sequence']) # recursively set app ids to related children def _set_app_id(app_id, menu): menu['app_id'] = app_id for child_id in menu['children']: _set_app_id(app_id, all_menus[child_id]) for app in menu_roots_data: app_id = app['id'] _set_app_id(app_id, all_menus[app_id]) # filter out menus not related to an app (+ keep root menu) all_menus = { menu['id']: menu for menu in all_menus.values() if menu.get('app_id') } all_menus['root'] = menu_root return all_menus def _get_menuitems_xmlids(self): menuitems = self.env['ir.model.data'].sudo().search([ ('res_id', 'in', self.ids), ('model', '=', 'ir.ui.menu') ]) return {menu.res_id: menu.complete_name for menu in menuitems}
class MarketingCampaignTest(models.TransientModel): _name = 'marketing.campaign.test' _description = 'Marketing Campaign: Launch a Test' @api.model def default_get(self, default_fields): defaults = super(MarketingCampaignTest, self).default_get(default_fields) if not defaults.get('res_id'): model_name = defaults.get('model_name') if not model_name and defaults.get('campaign_id'): model_name = self.env['marketing.campaign'].browse( defaults['campaign_id']).model_name if model_name: resource = self.env[model_name].search([], limit=1) defaults['res_id'] = resource.id return defaults @api.model def _selection_target_model(self): models = self.env['ir.model'].search([('is_mail_thread', '=', True)]) return [(model.model, model.name) for model in models] campaign_id = fields.Many2one('marketing.campaign', string='Campaign', required=True) model_id = fields.Many2one('ir.model', string='Object', related='campaign_id.model_id', readonly=True) model_name = fields.Char('Record model', related='campaign_id.model_id.model', readonly=True) res_id = fields.Integer(string='Record ID', index=True) resource_ref = fields.Reference(string='Record', selection='_selection_target_model', compute='_compute_resource_ref', inverse='_set_resource_ref') @api.depends('model_name', 'res_id') def _compute_resource_ref(self): for participant in self: if participant.model_name: participant.resource_ref = '%s,%s' % (participant.model_name, participant.res_id or 0) def _set_resource_ref(self): for participant in self: if participant.resource_ref: participant.res_id = participant.resource_ref.id def action_launch_test(self): """ Create test participant based on user choice. """ participant = self.env['marketing.participant'].create({ 'campaign_id': self.campaign_id.id, 'res_id': self.res_id, 'is_test': True, }) return { 'name': _('Launch a Test'), 'type': 'ir.actions.act_window', 'view_mode': 'form', 'res_model': 'marketing.participant', 'res_id': participant.id, 'target': 'current', }
class MarketingParticipant(models.Model): _name = 'marketing.participant' _description = 'Marketing Participant' _rec_name = 'resource_ref' @api.model def default_get(self, default_fields): defaults = super(MarketingParticipant, self).default_get(default_fields) if 'res_id' in default_fields and not defaults.get('res_id'): model_name = defaults.get('model_name') if not model_name and defaults.get('campaign_id'): model_name = self.env['marketing.campaign'].browse( defaults['campaign_id']).model_name if model_name and model_name in self.env: resource = self.env[model_name].search([], limit=1) defaults['res_id'] = resource.id return defaults @api.model def _selection_target_model(self): models = self.env['ir.model'].search([('is_mail_thread', '=', True)]) return [(model.model, model.name) for model in models] def _search_resource_ref(self, operator, value): ir_models = set([ model['model_name'] for model in self.env['marketing.campaign'].search([]).read(['model_name']) ]) ir_model_ids = [] for model in ir_models: if model in self.env: ir_model_ids += self.env['marketing.participant'].search([ '&', ('model_name', '=', model), ('res_id', 'in', [ name[0] for name in self.env[model].name_search(name=value) ]) ]).ids operator = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in' return [('id', operator, ir_model_ids)] campaign_id = fields.Many2one('marketing.campaign', string='Campaign', index=True, ondelete='cascade', required=True) model_id = fields.Many2one('ir.model', string='Model', related='campaign_id.model_id', index=True, readonly=True, store=True) model_name = fields.Char(string='Record model', related='campaign_id.model_id.model', readonly=True, store=True) res_id = fields.Integer(string='Record ID', index=True) resource_ref = fields.Reference(string='Record', selection='_selection_target_model', compute='_compute_resource_ref', inverse='_set_resource_ref', search='_search_resource_ref') trace_ids = fields.One2many('marketing.trace', 'participant_id', string='Actions') state = fields.Selection( [ ('running', 'Running'), ('completed', 'Completed'), ('unlinked', 'Removed'), ], default='running', index=True, required=True, help='Removed means the related record does not exist anymore.') is_test = fields.Boolean('Test Record', default=False) @api.depends('model_name', 'res_id') def _compute_resource_ref(self): for participant in self: if participant.model_name and participant.model_name in self.env: participant.resource_ref = '%s,%s' % (participant.model_name, participant.res_id or 0) else: participant.resource_ref = None def _set_resource_ref(self): for participant in self: if participant.resource_ref: participant.res_id = participant.resource_ref.id def check_completed(self): existing_traces = self.env['marketing.trace'].search([ ('participant_id', 'in', self.ids), ('state', '=', 'scheduled'), ]) (self - existing_traces.mapped('participant_id')).write( {'state': 'completed'}) @api.model def create(self, values): res = super(MarketingParticipant, self).create(values) # prepare first traces related to begin activities primary_activities = res.campaign_id.marketing_activity_ids.filtered( lambda act: act.trigger_type == 'begin') now = Datetime.now() trace_ids = [(0, 0, { 'activity_id': activity.id, 'schedule_date': now + relativedelta( **{activity.interval_type: activity.interval_number}), }) for activity in primary_activities] res.write({'trace_ids': trace_ids}) return res def action_set_completed(self): ''' Manually mark as a completed and cancel every scheduled trace ''' # TDE TODO: delegate set Canceled to trace record self.write({'state': 'completed'}) self.env['marketing.trace'].search([('participant_id', 'in', self.ids), ('state', '=', 'scheduled') ]).write({ 'state': 'canceled', 'schedule_date': Datetime.now(), 'state_msg': _('Marked as completed') }) def action_set_running(self): self.write({'state': 'running'}) def action_set_unlink(self): self.write({'state': 'unlinked'}) self.env['marketing.trace'].search([('participant_id', 'in', self.ids), ('state', '=', 'scheduled') ]).write({ 'state': 'canceled', 'state_msg': _('Record deleted'), }) return True
class SeBackend(models.Model): _name = 'se.backend' _description = 'Se Backend' _inherit = ['connector.backend', 'keychain.backend'] _backend_type = 'se' _backend_name = 'search_engine_backend' # We can't leave this field required in database, because of strange # behaviors with name field defined already on connector.backend, which is # inherited by both se.backend and se.backend.spec.abstract. # In next version, the field is removed from connector.backend, so we # should probably just define the field here, with a required=True and # remove the related field defined on specific backend which will # be useless because it will be takend from se.backend... name = fields.Char(required=False) specific_model = fields.Selection( string='Type', selection=[], required=True, readonly=True ) index_ids = fields.One2many( 'se.index', 'backend_id', ) specific_backend = fields.Reference( string='Specific backend', compute='_compute_specific_backend', selection='_get_specific_backend_selection', readonly=True, ) @api.model def _get_specific_backend_selection(self): spec_backend_selection = self._fields['specific_model'].selection vals = [] s = self.with_context(active_test=False) for model, descr in spec_backend_selection: # We have to check if the table really exist. # Because in the case of the user uninstall a connector_XXX module # with a new se.backend (so who adds a new element into selection # field), no more se.backend will be available (because the # selection field still filled with the previous model and Odoo # try to load the model). if model in s.env and s.env[model]._table_exist(): records = s.env[model].search([]) for record in records: vals.append((model, record.id)) return vals @api.model def register_spec_backend(self, specific_backend_model): """ This function must be called by specific backend from the _register_hook method to register it into the allowed specific models :param self: :param specific_backend_model: """ self._fields['specific_model'].selection.append(( specific_backend_model._name, specific_backend_model._description)) @api.depends('specific_model') @api.multi def _compute_specific_backend(self): for specific_model in self.mapped('specific_model'): recs = self.filtered(lambda r, model=specific_model: r.specific_model == model) backends = self.env[specific_model].search([ ('se_backend_id', 'in', recs.ids)]) backend_by_rec = {r.se_backend_id: r for r in backends} for rec in recs: spec_backend = backend_by_rec.get(rec) rec.specific_backend = '%s,%s' % ( spec_backend._name, spec_backend.id)
class PRTMailComposer(models.TransientModel): _inherit = 'mail.compose.message' _name = 'mail.compose.message' wizard_mode = fields.Char(string="Wizard mode") forward_ref = fields.Reference(string="Attach to record", selection='_referenceable_models_fwd', readonly=False) # -- Ref models @api.model def _referenceable_models_fwd(self): return [(x.object, x.name) for x in self.env['res.request.link'].sudo().search([])] # -- Record ref change @api.onchange('forward_ref') @api.multi def ref_change(self): self.ensure_one() if self.forward_ref: self.update({ 'model': self.forward_ref._name, 'res_id': self.forward_ref.id }) # -- Get record data @api.model def get_record_data(self, values): """ Copy-pasted mail.compose.message original function so stay aware in case it is changed in Odoo core! Returns a defaults-like dict with initial values for the composition wizard when sending an email related a previous email (parent_id) or a document (model, res_id). This is based on previously computed default values. """ result = {} subj = self._context.get('default_subject', False) subject = tools.ustr(subj) if subj else False if not subject: if values.get('parent_id'): parent = self.env['mail.message'].browse( values.get('parent_id')) result['record_name'] = parent.record_name, subject = tools.ustr(parent.subject or parent.record_name or '') if not values.get('model'): result['model'] = parent.model if not values.get('res_id'): result['res_id'] = parent.res_id partner_ids = values.get('partner_ids', list()) +\ [(4, id) for id in parent.partner_ids.filtered(lambda rec: rec.email not in [self.env.user.email, self.env.user.company_id.email]).ids] if self._context.get( 'is_private' ) and parent.author_id: # check message is private then add author also in partner list. partner_ids += [(4, parent.author_id.id)] result['partner_ids'] = partner_ids elif values.get('model') and values.get('res_id'): doc_name_get = self.env[values.get('model')].browse( values.get('res_id')).name_get() result[ 'record_name'] = doc_name_get and doc_name_get[0][1] or '' subject = tools.ustr(result['record_name']) # Change prefix in case we are forwarding re_prefix = _('Fwd:') if self._context.get( 'default_wizard_mode', False) == 'forward' else _('Re:') if subject and not (subject.startswith('Re:') or subject.startswith(re_prefix)): subject = "%s %s" % (re_prefix, subject) result['subject'] = subject return result
class app_menu(models.Model): _name = 'app.menu' _order = 'id desc' name = fields.Char(string=u"菜单名称", required=True) complete_name = fields.Char(string=u"完整路径", compute="_compute_complete_name") sequence = fields.Integer(default=10, string=u"菜单顺序", help=u'数字越小优先级越高') parent_id = fields.Many2one(comodel_name='app.menu', string=u'上级菜单', default=None) action = fields.Reference(selection=[('app.act_window', 'APP窗口动作')], default=None) icon = fields.Char( string=u"菜单图标", help=u'填写Awesome图标的名称,如fa-check,仅一级菜单有效。支持4.7.0版本的Awesome图标') groups_id = fields.Many2many('res.groups', 'app_menu_group_rel', 'app_menu_id', 'gid', string='Groups', help=u"指定可访问该菜单的用户组列表,留空表示所有用户都可以访问") @api.depends('name', 'parent_id') def _compute_complete_name(self): for menu_item in self: complete_name = menu_item.name or '' parent_menu_item = menu_item.parent_id while parent_menu_item: complete_name = parent_menu_item.name + '/' + complete_name parent_menu_item = parent_menu_item.parent_id menu_item.complete_name = complete_name return 'test' @api.onchange('parent_id') def onchange_parent_id(self): if self.parent_id: self.icon = '' @api.model def get_user_menu_list(self): menus = self.search([('action', '!=', False)]) menus_has_rights = [] for menu_item in menus: try: self.env[menu_item.action.res_model].check_access_rights( 'read') menus_has_rights.append(menu_item) except: continue menus = [] for menu_item in menus_has_rights: while menu_item: menu = { 'id': menu_item.id, 'name': menu_item.name, 'sequence': menu_item.sequence, 'parent_id': menu_item.parent_id.id, 'action': self.env['app.act_window'].get_action_data( menu_item.action and menu_item.action.id), 'icon': menu_item.icon } if menu not in menus: menus.append(menu) menu_item = menu_item.parent_id return menus
class PrivacyLookupWizardLine(models.TransientModel): _name = 'privacy.lookup.wizard.line' _description = 'Privacy Lookup Wizard Line' _transient_max_count = 0 _transient_max_hours = 24 @api.model def _selection_target_model(self): return [(model.model, model.name) for model in self.env['ir.model'].sudo().search([])] wizard_id = fields.Many2one('privacy.lookup.wizard') res_id = fields.Integer(string="Resource ID", required=True) res_name = fields.Char(string='Resource name', compute='_compute_res_name', store=True) res_model_id = fields.Many2one('ir.model', 'Related Document Model', ondelete='cascade') res_model = fields.Char(string='Document Model', related='res_model_id.model', store=True, readonly=True) resource_ref = fields.Reference(string='Record', selection='_selection_target_model', compute='_compute_resource_ref', inverse='_set_resource_ref') has_active = fields.Boolean(compute='_compute_has_active', store=True) is_active = fields.Boolean() is_unlinked = fields.Boolean() execution_details = fields.Char(default='') @api.depends('res_model', 'res_id', 'is_unlinked') def _compute_resource_ref(self): for line in self: if line.res_model and line.res_model in self.env and not line.is_unlinked: # Exclude records that can't be read (eg: multi-company ir.rule) try: self.env[line.res_model].browse( line.res_id).check_access_rule('read') line.resource_ref = '%s,%s' % (line.res_model, line.res_id or 0) except Exception: line.resource_ref = None else: line.resource_ref = None def _set_resource_ref(self): for line in self: if line.resource_ref: line.res_id = line.resource_ref.id @api.depends('res_model_id') def _compute_has_active(self): for line in self: if not line.res_model_id: line.has_active = False continue line.has_active = 'active' in self.env[line.res_model] @api.depends('res_model', 'res_id') def _compute_res_name(self): for line in self: if not line.res_id or not line.res_model: continue record = self.env[line.res_model].sudo().browse(line.res_id) if not record.exists(): continue name = record.name_get() line.res_name = name[0][1] if name else ('%s/%s') % ( line.res_model_id.name, line.res_id) @api.onchange('is_active') def _onchange_is_active(self): for line in self: if not line.res_model_id or not line.res_id: continue action = _('Unarchived') if line.is_active else _('Archived') line.execution_details = '%s %s #%s' % ( action, line.res_model_id.name, line.res_id) self.env[line.res_model].sudo().browse(line.res_id).write( {'active': line.is_active}) def action_unlink(self): self.ensure_one() if self.is_unlinked: raise UserError(_('The record is already unlinked.')) self.env[self.res_model].sudo().browse(self.res_id).unlink() self.execution_details = '%s %s #%s' % ( _('Deleted'), self.res_model_id.name, self.res_id) self.is_unlinked = True def action_archive_all(self): for line in self: if not line.has_active or not line.is_active: continue line.is_active = False line._onchange_is_active() def action_unlink_all(self): for line in self: if line.is_unlinked: continue line.action_unlink() def action_open_record(self): self.ensure_one() return { 'type': 'ir.actions.act_window', 'view_mode': 'form', 'res_id': self.res_id, 'res_model': self.res_model, }
class EventMailScheduler(models.Model): """ Event automated mailing. This model replaces all existing fields and configuration allowing to send emails on events since Odoo 9. A cron exists that periodically checks for mailing to run. """ _name = 'event.mail' _rec_name = 'event_id' _description = 'Event Automated Mailing' @api.model def _selection_template_model(self): return [('mail.template', 'Mail')] event_id = fields.Many2one('event.event', string='Event', required=True, ondelete='cascade') sequence = fields.Integer('Display order') notification_type = fields.Selection([('mail', 'Mail')], string='Send', default='mail', required=True) interval_nbr = fields.Integer('Interval', default=1) interval_unit = fields.Selection([('now', 'Immediately'), ('hours', 'Hours'), ('days', 'Days'), ('weeks', 'Weeks'), ('months', 'Months')], string='Unit', default='hours', required=True) interval_type = fields.Selection([('after_sub', 'After each registration'), ('before_event', 'Before the event'), ('after_event', 'After the event')], string='Trigger ', default="before_event", required=True) scheduled_date = fields.Datetime('Schedule Date', compute='_compute_scheduled_date', store=True) # contact and status mail_registration_ids = fields.One2many( 'event.mail.registration', 'scheduler_id', help='Communication related to event registrations') mail_done = fields.Boolean("Sent", copy=False, readonly=True) mail_state = fields.Selection([('running', 'Running'), ('scheduled', 'Scheduled'), ('sent', 'Sent')], string='Global communication Status', compute='_compute_mail_state') mail_count_done = fields.Integer('# Sent', copy=False, readonly=True) template_model_id = fields.Many2one('ir.model', string='Template Model', compute='_compute_template_model_id', compute_sudo=True) template_ref = fields.Reference(string='Template', selection='_selection_template_model', required=True) @api.depends('notification_type') def _compute_template_model_id(self): mail_model = self.env['ir.model']._get('mail.template') for mail in self: mail.template_model_id = mail_model if mail.notification_type == 'mail' else False @api.depends('event_id.date_begin', 'event_id.date_end', 'interval_type', 'interval_unit', 'interval_nbr') def _compute_scheduled_date(self): for scheduler in self: if scheduler.interval_type == 'after_sub': date, sign = scheduler.event_id.create_date, 1 elif scheduler.interval_type == 'before_event': date, sign = scheduler.event_id.date_begin, -1 else: date, sign = scheduler.event_id.date_end, 1 scheduler.scheduled_date = date + _INTERVALS[ scheduler.interval_unit]( sign * scheduler.interval_nbr) if date else False @api.depends('interval_type', 'scheduled_date', 'mail_done') def _compute_mail_state(self): for scheduler in self: # registrations based if scheduler.interval_type == 'after_sub': scheduler.mail_state = 'running' # global event based elif scheduler.mail_done: scheduler.mail_state = 'sent' elif scheduler.scheduled_date: scheduler.mail_state = 'scheduled' else: scheduler.mail_state = 'running' def execute(self): for scheduler in self: now = fields.Datetime.now() if scheduler.interval_type == 'after_sub': new_registrations = scheduler.event_id.registration_ids.filtered_domain( [('state', 'not in', ('cancel', 'draft')) ]) - scheduler.mail_registration_ids.registration_id scheduler._create_missing_mail_registrations(new_registrations) # execute scheduler on registrations scheduler.mail_registration_ids.execute() total_sent = len( scheduler.mail_registration_ids.filtered( lambda reg: reg.mail_sent)) scheduler.update({ 'mail_done': total_sent >= (scheduler.event_id.seats_reserved + scheduler.event_id.seats_used), 'mail_count_done': total_sent, }) else: # before or after event -> one shot email if scheduler.mail_done or scheduler.notification_type != 'mail': continue # no template -> ill configured, skip and avoid crash if not scheduler.template_ref: continue # do not send emails if the mailing was scheduled before the event but the event is over if scheduler.scheduled_date <= now and ( scheduler.interval_type != 'before_event' or scheduler.event_id.date_end > now): scheduler.event_id.mail_attendees( scheduler.template_ref.id) scheduler.update({ 'mail_done': True, 'mail_count_done': scheduler.event_id.seats_reserved + scheduler.event_id.seats_used, }) return True def _create_missing_mail_registrations(self, registrations): new = [] for scheduler in self: new += [{ 'registration_id': registration.id, 'scheduler_id': scheduler.id, } for registration in registrations] if new: return self.env['event.mail.registration'].create(new) return self.env['event.mail.registration'] @api.model def _warn_template_error(self, scheduler, exception): # We warn ~ once by hour ~ instead of every 10 min if the interval unit is more than 'hours'. if random.random() < 0.1666 or scheduler.interval_unit in ('now', 'hours'): ex_s = exception_to_unicode(exception) try: event, template = scheduler.event_id, scheduler.template_ref emails = list( set([ event.organizer_id.email, event.user_id.email, template.write_uid.email ])) subject = _("WARNING: Event Scheduler Error for event: %s", event.name) body = _("""Event Scheduler for: - Event: %(event_name)s (%(event_id)s) - Scheduled: %(date)s - Template: %(template_name)s (%(template_id)s) Failed with error: - %(error)s You receive this email because you are: - the organizer of the event, - or the responsible of the event, - or the last writer of the template. """, event_name=event.name, event_id=event.id, date=scheduler.scheduled_date, template_name=template.name, template_id=template.id, error=ex_s) email = self.env['ir.mail_server'].build_email( email_from=self.env.user.email, email_to=emails, subject=subject, body=body, ) self.env['ir.mail_server'].send_email(email) except Exception as e: _logger.error( "Exception while sending traceback by email: %s.\n Original Traceback:\n%s", e, exception) pass @api.model def run(self, autocommit=False): """ Backward compatible method, notably if crons are not updated when migrating for some reason. """ return self.schedule_communications(autocommit=autocommit) @api.model def schedule_communications(self, autocommit=False): schedulers = self.search([('mail_done', '=', False), ('scheduled_date', '<=', fields.Datetime.now())]) for scheduler in schedulers: try: # Prevent a mega prefetch of the registration ids of all the events of all the schedulers self.browse(scheduler.id).execute() except Exception as e: _logger.exception(e) self.invalidate_cache() self._warn_template_error(scheduler, e) else: if autocommit and not getattr(threading.currentThread(), 'testing', False): self.env.cr.commit() return True
class CrmClaim(models.Model): """ Crm claim """ _name = "crm.claim" _description = "Claim" _order = "priority,date desc" _inherit = ['mail.thread'] @api.model def _get_default_stage_id(self): """ Gives default stage_id """ team_id = self.env['crm.team']._get_default_team_id() return self.stage_find(team_id.id, [('sequence', '=', '1')]) @api.model def _get_default_team(self): return self.env['crm.team']._get_default_team_id() name = fields.Char( string='Claim Subject', required=True, ) active = fields.Boolean(default=True, ) action_next = fields.Char(string='Next Action', ) date_action_next = fields.Datetime(string='Next Action Date', ) description = fields.Text() resolution = fields.Text() create_date = fields.Datetime( string='Creation Date', readonly=True, ) write_date = fields.Datetime( string='Update Date', readonly=True, ) date_deadline = fields.Date(string='Deadline', ) date_closed = fields.Datetime( string='Closed', readonly=True, ) date = fields.Datetime( string='Claim Date', index=True, detault=fields.Datetime.now, ) model_ref_id = fields.Reference( selection=referenceable_models, string='Reference', oldname='ref', ) categ_id = fields.Many2one( comodel_name='crm.claim.category', string='Category', ) priority = fields.Selection( selection=[ ('0', 'Low'), ('1', 'Normal'), ('2', 'High'), ], default='1', ) type_action = fields.Selection( selection=[ ('correction', 'Corrective Action'), ('prevention', 'Preventive Action'), ], string='Action Type', ) user_id = fields.Many2one( comodel_name='res.users', string='Responsible', track_visibility='always', default=lambda self: self.env.user, ) user_fault = fields.Char(string='Trouble Responsible', ) team_id = fields.Many2one( comodel_name='crm.team', string='Sales Team', index=True, default=_get_default_team, help="Responsible sales team. Define Responsible user and Email " "account for mail gateway.", ) company_id = fields.Many2one( comodel_name='res.company', string='Company', default=lambda self: self.env.user.company_id, ) partner_id = fields.Many2one( comodel_name='res.partner', string='Partner', ) email_cc = fields.Text( string='Watchers Emails', help="These email addresses will be added to the CC field of all " "inbound and outbound emails for this record before being sent. " "Separate multiple email addresses with a comma", ) email_from = fields.Char( string='Email', help="Destination email for email gateway.", ) partner_phone = fields.Char(string='Phone', ) stage_id = fields.Many2one( comodel_name='crm.claim.stage', string='Stage', track_visibility='onchange', default=_get_default_stage_id, domain="['|', ('team_ids', '=', team_id), ('case_default', '=', True)]" ) cause = fields.Text(string='Root Cause', ) def stage_find(self, team_id, domain=None, order='sequence'): """ Override of the base.stage method Parameter of the stage search taken from the lead: - team_id: if set, stages must belong to this team or be a default case """ if domain is None: # pragma: no cover domain = [] # collect all team_ids team_ids = [] if team_id: team_ids.append(team_id) team_ids.extend(self.mapped('team_id').ids) search_domain = [] if team_ids: search_domain += ['|'] * len(team_ids) for team_id in team_ids: search_domain.append(('team_ids', '=', team_id)) search_domain.append(('case_default', '=', True)) # AND with the domain in parameter search_domain += list(domain) # perform search, return the first found return self.env['crm.claim.stage'].search(search_domain, order=order, limit=1).id @api.onchange('partner_id') def onchange_partner_id(self): """This function returns value of partner address based on partner :param email: ignored """ if self.partner_id: self.email_from = self.partner_id.email self.partner_phone = self.partner_id.phone @api.onchange('categ_id') def onchange_categ_id(self): if self.stage_id: self.team_id = self.categ_id.team_id @api.model def create(self, values): ctx = self.env.context.copy() if values.get('team_id') and not ctx.get('default_team_id'): ctx['default_team_id'] = values.get('team_id') return super(CrmClaim, self.with_context(context=ctx)).create(values) @api.multi def copy(self, default=None): default = dict( default or {}, stage_id=self._get_default_stage_id(), name=_('%s (copy)') % self.name, ) return super(CrmClaim, self).copy(default) # ------------------------------------------------------- # Mail gateway # ------------------------------------------------------- @api.model def message_new(self, msg, custom_values=None): """ Overrides mail_thread message_new that is called by the mailgateway through message_process. This override updates the document according to the email. """ if custom_values is None: custom_values = {} desc = html2plaintext(msg.get('body')) if msg.get('body') else '' defaults = { 'name': msg.get('subject') or _("No Subject"), 'description': desc, 'email_from': msg.get('from'), 'email_cc': msg.get('cc'), 'partner_id': msg.get('author_id', False), } if msg.get('priority'): defaults['priority'] = msg.get('priority') defaults.update(custom_values) return super(CrmClaim, self).message_new(msg, custom_values=defaults)
class DmsSecurityMixin(models.AbstractModel): _name = "dms.security.mixin" _description = "DMS Security Mixin" # Submodels must define this field that points to the owner dms.directory _directory_field = "directory_id" res_model = fields.Char(string="Linked attachments model", index=True, store=True) res_id = fields.Integer( string="Linked attachments record ID", index=True, store=True ) record_ref = fields.Reference( string="Record Referenced", compute="_compute_record_ref", selection=lambda self: self._get_ref_selection(), ) permission_read = fields.Boolean( compute="_compute_permissions", search="_search_permission_read", string="Read Access", ) permission_create = fields.Boolean( compute="_compute_permissions", search="_search_permission_create", string="Create Access", ) permission_write = fields.Boolean( compute="_compute_permissions", search="_search_permission_write", string="Write Access", ) permission_unlink = fields.Boolean( compute="_compute_permissions", search="_search_permission_unlink", string="Delete Access", ) @api.model def _get_ref_selection(self): models = self.env["ir.model"].search([]) return [(model.model, model.name) for model in models] @api.depends("res_model", "res_id") def _compute_record_ref(self): for record in self: record.record_ref = False if record.res_model and record.res_id: record.record_ref = "{},{}".format(record.res_model, record.res_id) def _compute_permissions(self): """Get permissions for the current record. ⚠Not very performant; only display field on form views. """ # Superuser unrestricted 🦸 if self.env.su: self.update( { "permission_create": True, "permission_read": True, "permission_unlink": True, "permission_write": True, } ) return # Update according to presence when applying ir.rule creatable = self._filter_access_rules("create") readable = self._filter_access_rules("read") unlinkable = self._filter_access_rules("unlink") writeable = self._filter_access_rules("write") for one in self: one.update( { "permission_create": bool(one & creatable), "permission_read": bool(one & readable), "permission_unlink": bool(one & unlinkable), "permission_write": bool(one & writeable), } ) @api.model def _get_domain_by_inheritance(self, operation): """Get domain for inherited accessible records.""" if self.env.su: return [] inherited_access_field = "storage_id_inherit_access_from_parent_record" if self._name != "dms.directory": inherited_access_field = "{}.{}".format( self._directory_field, inherited_access_field, ) inherited_access_domain = [(inherited_access_field, "=", True)] domains = [] # Get all used related records related_groups = self.sudo().read_group( domain=inherited_access_domain + [("res_model", "!=", False)], fields=["res_id:array_agg"], groupby=["res_model"], ) for group in related_groups: try: model = self.env[group["res_model"]] except KeyError: # Model not registered. This is normal if you are upgrading the # database. Otherwise, you probably have garbage DMS data. # These records will be accessible by DB users only. domains.append( [ ("res_model", "=", group["res_model"]), (True, "=", self.env.user.has_group("base.group_user")), ] ) continue # Check model access only once per batch if not model.check_access_rights(operation, raise_exception=False): continue domains.append([("res_model", "=", model._name), ("res_id", "=", False)]) # Check record access in batch too group_ids = [i for i in group["res_id"] if i] # Hack to remove None res_id related_ok = model.browse(group_ids)._filter_access_rules_python(operation) if not related_ok: continue domains.append( [("res_model", "=", model._name), ("res_id", "in", related_ok.ids)] ) result = inherited_access_domain + OR(domains) return result @api.model def _get_access_groups_query(self, operation): """Return the query to select access groups.""" operation_check = { "create": "AND dag.perm_inclusive_create", "read": "", "unlink": "AND dag.perm_inclusive_unlink", "write": "AND dag.perm_inclusive_write", }[operation] select = """ SELECT dir_group_rel.aid FROM dms_directory_complete_groups_rel AS dir_group_rel INNER JOIN dms_access_group AS dag ON dir_group_rel.gid = dag.id INNER JOIN dms_access_group_users_rel AS users ON users.gid = dag.id WHERE users.uid = %s {} """.format( operation_check ) return (select, (self.env.uid,)) @api.model def _get_domain_by_access_groups(self, operation): """Get domain for records accessible applying DMS access groups.""" result = [ ( "%s.storage_id_inherit_access_from_parent_record" % self._directory_field, "=", False, ), ( self._directory_field, "inselect", self._get_access_groups_query(operation), ), ] return result @api.model def _get_permission_domain(self, operator, value, operation): """Abstract logic for searching computed permission fields.""" _self = self # HACK ir.rule domain is always computed with sudo, so if this check is # true, we can assume safely that you're checking permissions if self.env.su and value == self.env.uid: _self = self.sudo(False) value = bool(value) # Tricky one, to know if you want to search # positive or negative access positive = (operator not in NEGATIVE_TERM_OPERATORS) == bool(value) if _self.env.su: # You're SUPERUSER_ID return TRUE_DOMAIN if positive else FALSE_DOMAIN # Obtain and combine domains result = OR( [ _self._get_domain_by_access_groups(operation), _self._get_domain_by_inheritance(operation), ] ) if not positive: result.insert(0, "!") return result @api.model def _search_permission_create(self, operator, value): return self._get_permission_domain(operator, value, "create") @api.model def _search_permission_read(self, operator, value): return self._get_permission_domain(operator, value, "read") @api.model def _search_permission_unlink(self, operator, value): return self._get_permission_domain(operator, value, "unlink") @api.model def _search_permission_write(self, operator, value): return self._get_permission_domain(operator, value, "write") def _filter_access_rules_python(self, operation): # Only kept to not break inheritance; see next comment result = super()._filter_access_rules_python(operation) # HACK Always fall back to applying rules by SQL. # Upstream `_filter_acccess_rules_python()` doesn't use computed fields # search methods. Thus, it will take the `[('permission_{operation}', # '=', user.id)]` rule literally. Obviously that will always fail # because `self[f"permission_{operation}"]` will always be a `bool`, # while `user.id` will always be an `int`. result |= self._filter_access_rules(operation) return result @api.model_create_multi def create(self, vals_list): # Create as sudo to avoid testing creation permissions before DMS security # groups are attached (otherwise nobody would be able to create) res = super(DmsSecurityMixin, self.sudo()).create(vals_list) # Need to flush now, so all groups are stored in DB and the SELECT used # to check access works res.flush() # Go back to original sudo state and check we really had creation permission res = res.sudo(self.env.su) res.check_access_rights("create") res.check_access_rule("create") return res
class Rating(models.Model): _name = "rating.rating" _description = "Rating" _order = 'write_date desc' _rec_name = 'res_name' _sql_constraints = [ ('rating_range', 'check(rating >= 0 and rating <= 5)', 'Rating should be between 0 and 5'), ] @api.depends('res_model', 'res_id') def _compute_res_name(self): for rating in self: name = self.env[rating.res_model].sudo().browse( rating.res_id).name_get() rating.res_name = name and name[0][1] or ('%s/%s') % ( rating.res_model, rating.res_id) @api.model def _default_access_token(self): return uuid.uuid4().hex @api.model def _selection_target_model(self): return [(model.model, model.name) for model in self.env['ir.model'].sudo().search([])] create_date = fields.Datetime(string="Submitted on") res_name = fields.Char(string='Resource name', compute='_compute_res_name', store=True, help="The name of the rated resource.") res_model_id = fields.Many2one('ir.model', 'Related Document Model', index=True, ondelete='cascade', help='Model of the followed resource') res_model = fields.Char(string='Document Model', related='res_model_id.model', store=True, index=True, readonly=True) res_id = fields.Integer(string='Document', required=True, help="Identifier of the rated object", index=True) resource_ref = fields.Reference(string='Resource Ref', selection='_selection_target_model', compute='_compute_resource_ref', readonly=True) parent_res_name = fields.Char('Parent Document Name', compute='_compute_parent_res_name', store=True) parent_res_model_id = fields.Many2one('ir.model', 'Parent Related Document Model', index=True, ondelete='cascade') parent_res_model = fields.Char('Parent Document Model', store=True, related='parent_res_model_id.model', index=True, readonly=False) parent_res_id = fields.Integer('Parent Document', index=True) parent_ref = fields.Reference(string='Parent Ref', selection='_selection_target_model', compute='_compute_parent_ref', readonly=True) rated_partner_id = fields.Many2one('res.partner', string="Rated Operator", help="Owner of the rated resource") rated_partner_name = fields.Char(related="rated_partner_id.name") partner_id = fields.Many2one('res.partner', string='Customer', help="Author of the rating") rating = fields.Float(string="Rating Value", group_operator="avg", default=0, help="Rating value: 0=Unhappy, 5=Happy") rating_image = fields.Binary('Image', compute='_compute_rating_image') rating_text = fields.Selection([('top', 'Satisfied'), ('ok', 'Okay'), ('ko', 'Dissatisfied'), ('none', 'No Rating yet')], string='Rating', store=True, compute='_compute_rating_text', readonly=True) feedback = fields.Text('Comment', help="Reason of the rating") message_id = fields.Many2one( 'mail.message', string="Message", index=True, ondelete='cascade', help= "Associated message when posting a review. Mainly used in website addons." ) is_internal = fields.Boolean('Visible Internally Only', readonly=False, related='message_id.is_internal', store=True) access_token = fields.Char( 'Security Token', default=_default_access_token, help="Access token to set the rating of the value") consumed = fields.Boolean(string="Filled Rating", help="Enabled if the rating has been filled.") @api.depends('res_model', 'res_id') def _compute_resource_ref(self): for rating in self: if rating.res_model and rating.res_model in self.env: rating.resource_ref = '%s,%s' % (rating.res_model, rating.res_id or 0) else: rating.resource_ref = None @api.depends('parent_res_model', 'parent_res_id') def _compute_parent_ref(self): for rating in self: if rating.parent_res_model and rating.parent_res_model in self.env: rating.parent_ref = '%s,%s' % (rating.parent_res_model, rating.parent_res_id or 0) else: rating.parent_ref = None @api.depends('parent_res_model', 'parent_res_id') def _compute_parent_res_name(self): for rating in self: name = False if rating.parent_res_model and rating.parent_res_id: name = self.env[rating.parent_res_model].sudo().browse( rating.parent_res_id).name_get() name = name and name[0][1] or ('%s/%s') % ( rating.parent_res_model, rating.parent_res_id) rating.parent_res_name = name def _get_rating_image_filename(self): self.ensure_one() if self.rating >= RATING_LIMIT_SATISFIED: rating_int = 5 elif self.rating >= RATING_LIMIT_OK: rating_int = 3 elif self.rating >= RATING_LIMIT_MIN: rating_int = 1 else: rating_int = 0 return 'rating_%s.png' % rating_int def _compute_rating_image(self): for rating in self: try: image_path = get_resource_path( 'rating', 'static/src/img', rating._get_rating_image_filename()) rating.rating_image = base64.b64encode( open(image_path, 'rb').read()) if image_path else False except (IOError, OSError): rating.rating_image = False @api.depends('rating') def _compute_rating_text(self): for rating in self: if rating.rating >= RATING_LIMIT_SATISFIED: rating.rating_text = 'top' elif rating.rating >= RATING_LIMIT_OK: rating.rating_text = 'ok' elif rating.rating >= RATING_LIMIT_MIN: rating.rating_text = 'ko' else: rating.rating_text = 'none' @api.model_create_multi def create(self, vals_list): for values in vals_list: if values.get('res_model_id') and values.get('res_id'): values.update(self._find_parent_data(values)) return super().create(vals_list) def write(self, values): if values.get('res_model_id') and values.get('res_id'): values.update(self._find_parent_data(values)) return super(Rating, self).write(values) def unlink(self): # OPW-2181568: Delete the chatter message too self.env['mail.message'].search([('rating_ids', 'in', self.ids) ]).unlink() return super(Rating, self).unlink() def _find_parent_data(self, values): """ Determine the parent res_model/res_id, based on the values to create or write """ current_model_name = self.env['ir.model'].sudo().browse( values['res_model_id']).model current_record = self.env[current_model_name].browse(values['res_id']) data = { 'parent_res_model_id': False, 'parent_res_id': False, } if hasattr(current_record, '_rating_get_parent_field_name'): current_record_parent = current_record._rating_get_parent_field_name( ) if current_record_parent: parent_res_model = getattr(current_record, current_record_parent) data['parent_res_model_id'] = self.env['ir.model']._get( parent_res_model._name).id data['parent_res_id'] = parent_res_model.id return data def reset(self): for record in self: record.write({ 'rating': 0, 'access_token': record._default_access_token(), 'feedback': False, 'consumed': False, }) def action_open_rated_object(self): self.ensure_one() return { 'type': 'ir.actions.act_window', 'res_model': self.res_model, 'res_id': self.res_id, 'views': [[False, 'form']] }
class AccountCheckOperation(models.Model): _name = 'account.check.operation' _rec_name = 'operation' _order = 'date desc, id desc' # _order = 'create_date desc' # al final usamos solo date y no datetime porque el otro dato ya lo tenemos # en create_date. ademas el orden es una mezcla de la fecha el id # y entonces la fecha la solemos computar con el payment date para que # sea igual a la fecha contable (payment date va al asiento) # date = fields.Datetime( date = fields.Date( default=fields.Date.context_today, # default=lambda self: fields.Datetime.now(), required=True, index=True, ) check_id = fields.Many2one( 'account.check', 'Check', required=True, ondelete='cascade', auto_join=True, index=True, ) operation = fields.Selection( [ # from payments ('holding', 'Receive'), ('deposited', 'Deposit'), ('selled', 'Sell'), ('delivered', 'Deliver'), # usado para hacer transferencias internas, es lo mismo que delivered # (endosado) pero no queremos confundir con terminos, a la larga lo # volvemos a poner en holding ('transfered', 'Transfer'), ('handed', 'Hand'), ('withdrawed', 'Withdrawal'), # from checks ('reclaimed', 'Claim'), ('rejected', 'Rejection'), ('debited', 'Debit'), ('returned', 'Return'), # al final no vamos a implemnetar esto ya que habria que hacer muchas # cosas hasta actualizar el asiento, mejor se vuelve atras y se # vuelve a generar deuda y listo, igualmente lo dejamos por si se # quiere usar de manera manual ('changed', 'Change'), ('cancel', 'Cancel'), ], required=True, index=True, ) origin_name = fields.Char(compute='_compute_origin_name') origin = fields.Reference(string='Origin Document', selection='_reference_models') partner_id = fields.Many2one( 'res.partner', string='Partner', ) notes = fields.Text() @api.multi def unlink(self): for rec in self: if rec.origin: raise ValidationError( _('You can not delete a check operation that has an origin.' '\nYou can delete the origin reference and unlink after.' )) return super(AccountCheckOperation, self).unlink() @api.multi @api.depends('origin') def _compute_origin_name(self): """ We add this computed method because an error on tree view displaying reference field when destiny record is deleted. As said in this post (last answer) we should use name_get instead of display_name https://www.odoo.com/es_ES/forum/ayuda-1/question/ how-to-override-name-get-method-in-new-api-61228 """ for rec in self: try: if rec.origin: id, name = rec.origin.name_get()[0] origin_name = name # origin_name = rec.origin.display_name else: origin_name = False except Exception as e: _logger.exception("Compute origin on checks exception: %s" % e) # if we can get origin we clean it rec.write({'origin': False}) origin_name = False rec.origin_name = origin_name @api.model def _reference_models(self): return [ ('account.payment', 'Payment'), ('account.check', 'Check'), ('account.invoice', 'Invoice'), ('account.move', 'Journal Entry'), ('account.move.line', 'Journal Item'), ('account.bank.statement.line', 'Statement Line'), ]