class HrEquipmentCategory(models.Model): _name = 'hr.equipment.category' _inherits = {"mail.alias": "alias_id"} _inherit = ['mail.thread'] _description = 'Asset Category' @api.one @api.depends('equipment_ids') def _compute_fold(self): self.fold = False if self.equipment_count else True name = fields.Char('Category Name', required=True, translate=True) user_id = fields.Many2one('res.users', 'Responsible', track_visibility='onchange', default=lambda self: self.env.uid) color = fields.Integer('Color Index') note = fields.Text('Comments', translate=True) equipment_ids = fields.One2many('hr.equipment', 'category_id', string='Equipments', copy=False) equipment_count = fields.Integer(string="Equipment", compute='_compute_equipment_count') maintenance_ids = fields.One2many('hr.equipment.request', 'category_id', copy=False) maintenance_count = fields.Integer(string="Maintenance", compute='_compute_maintenance_count') alias_id = fields.Many2one( 'mail.alias', 'Alias', ondelete='cascade', required=True, help="Email alias for this equipment category. New emails will automatically " "create new maintenance request for this equipment category.") fold = fields.Boolean(string='Folded in Maintenance Pipe', compute='_compute_fold', store=True) @api.multi def _compute_equipment_count(self): equipment_data = self.env['hr.equipment'].read_group([('category_id', 'in', self.ids)], ['category_id'], ['category_id']) mapped_data = dict([(m['category_id'][0], m['category_id_count']) for m in equipment_data]) for category in self: category.equipment_count = mapped_data.get(category.id, 0) @api.multi def _compute_maintenance_count(self): maintenance_data = self.env['hr.equipment.request'].read_group([('category_id', 'in', self.ids)], ['category_id'], ['category_id']) mapped_data = dict([(m['category_id'][0], m['category_id_count']) for m in maintenance_data]) for category in self: category.maintenance_count = mapped_data.get(category.id, 0) @api.model def create(self, vals): self = self.with_context(alias_model_name='hr.equipment.request', alias_parent_model_name=self._name) category_id = super(HrEquipmentCategory, self).create(vals) category_id.alias_id.write({'alias_parent_thread_id': category_id.id, 'alias_defaults': {'category_id': category_id.id}}) return category_id @api.multi def unlink(self): for category in self: if category.equipment_ids or category.maintenance_ids: raise UserError(_("You cannot delete an equipment category containing equipments or maintenance requests.")) res = super(HrEquipmentCategory, self).unlink() return res
class EventEvent(models.Model): """ Override Event model to add optional questions when buying tickets. """ _inherit = 'event.event' question_ids = fields.One2many('event.question', 'event_id', 'Questions') general_question_ids = fields.One2many('event.question', 'event_id', 'Questions', domain=[('is_individual', '=', False)]) specific_question_ids = fields.One2many('event.question', 'event_id', 'Questions', domain=[('is_individual', '=', True)])
class OauthApplication(models.Model): _inherit = 'oauth.application' client_id = fields.Char('Database UUID') last_connection = fields.Char(compute='_get_last_connection', string='Last Connection', size=64) server_db_ids = fields.One2many('saas_portal.server', 'oauth_application_id', string='Server Database') template_db_ids = fields.One2many('saas_portal.database', 'oauth_application_id', string='Template Database') client_db_ids = fields.One2many('saas_portal.client', 'oauth_application_id', string='Client Database') @api.one def _get_last_connection(self): oat = self.pool.get('oauth.access_token') to_search = [('application_id', '=', self.id)] access_token_ids = oat.search(self.env.cr, self.env.uid, to_search) if access_token_ids: access_token = oat.browse(self.env.cr, self.env.uid, access_token_ids[0]) self.last_connection = access_token.user_id.login_date
class SaasPortalCategory(models.Model): @api.multi def name_get(self): res = [] for record in self: res.append((record.id, record.display_name)) return res @api.one @api.depends('name') def _name_get_fnc(self): name = self.name if self.parent_id: name = self.parent_id.name + ' / ' + name self.display_name = name _name = "saas.portal.category" _description = "SaaS Client Category" name = fields.Char( "Employee Tag", required=True ) display_name = fields.Char( 'Name', compute='_name_get_fnc', store=True, readonly=True ) parent_id = fields.Many2one( 'saas.portal.category', 'Parent Employee Tag', index=True ) child_ids = fields.One2many( 'saas.portal.category', 'parent_id', 'Child Categories' ) @api.constrains('parent_id') @api.multi def _check_recursion(self): level = 100 cr = self.env.cr ids = self.ids while len(ids): cr.execute('select distinct parent_id from saas_portal_category where id IN %s', (tuple(ids), )) ids = filter(None, map(lambda x:x[0], cr.fetchall())) if not level: raise Warning('Error! You cannot create recursive Categories') level -= 1 return True
class OauthApplication(models.Model): CLIENT_ID_CHARACTER_SET = r'_-.:;=?!@0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' _name = 'oauth.application' _rec_name = 'client_id' def generate_client_id(self): return str(uuid.uuid1()) client_id = fields.Char('Client ID', select=True, required=True, default=generate_client_id) token_ids = fields.One2many('oauth.access_token', 'application_id', 'Tokens') _sql_constraints = [ ('client_id_uniq', 'unique (client_id)', 'client_id should be unique!'), ] @api.multi def _get_access_token(self, user_id=None, create=False): self.ensure_one() if not user_id: user_id = self.env.user.id access_token = self.env['oauth.access_token'].sudo().search( [('application_id', '=', self.id), ('user_id', '=', user_id)], order='id DESC', limit=1) if access_token: access_token = access_token[0] if access_token.is_expired(): access_token = None if not access_token and create: expires = datetime.now() + timedelta(seconds=60 * 60) vals = { 'user_id': user_id, 'scope': 'userinfo', 'expires': expires.strftime(DEFAULT_SERVER_DATETIME_FORMAT), 'token': oauthlib_common.generate_token(), 'application_id': self.id, } access_token = self.env['oauth.access_token'].create(vals) # we have to commit now, because /oauth2/tokeninfo could # be called before we finish current transaction. self._cr.commit() if not access_token: return None return access_token.token
class HtmlFormHistory(models.Model): _name = "html.form.history" _description = "HTML Form History" html_id = fields.Many2one('html.form', ondelete='cascade', string="HTML Form", readonly=True) form_name = fields.Char(related="html_id.name", string="Form Name") ref_url = fields.Char(string="Reference URL", readonly=True) record_id = fields.Integer(string="Record ID", readonly=True) insert_data = fields.One2many('html.form.history.field', 'html_id', string="HTML Fields", readonly=True)
class ResPartnerBank(models.Model): _inherit = "res.partner.bank" journal_id = fields.One2many( 'account.journal', 'bank_account_id', domain=[('type', '=', 'bank')], string='Account Journal', readonly=True, help="The accounting journal corresponding to this bank account.") @api.one @api.constrains('journal_id') def _check_journal_id(self): if len(self.journal_id) > 1: raise ValidationError( _('A bank account can anly belong to one journal.'))
class Category(models.Model): """ Channel contain various categories to manage its slides """ _name = 'slide.category' _description = "Slides Category" _order = "sequence, id" name = fields.Char('Name', translate=True, required=True) channel_id = fields.Many2one('slide.channel', string="Channel", required=True, ondelete='cascade') sequence = fields.Integer(default=10, help='Display order') slide_ids = fields.One2many('slide.slide', 'category_id', string="Slides") nbr_presentations = fields.Integer("Number of Presentations", compute='_count_presentations', store=True) nbr_documents = fields.Integer("Number of Documents", compute='_count_presentations', store=True) nbr_videos = fields.Integer("Number of Videos", compute='_count_presentations', store=True) nbr_infographics = fields.Integer("Number of Infographics", compute='_count_presentations', store=True) total = fields.Integer(compute='_count_presentations', store=True) @api.depends('slide_ids.slide_type', 'slide_ids.website_published') def _count_presentations(self): result = dict.fromkeys(self.ids, dict()) res = self.env['slide.slide'].read_group( [('website_published', '=', True), ('category_id', 'in', self.ids)], ['category_id', 'slide_type'], ['category_id', 'slide_type'], lazy=False) for res_group in res: result[res_group['category_id'][0]][res_group[ 'slide_type']] = result[res_group['category_id'][0]].get( res_group['slide_type'], 0) + res_group['__count'] for record in self: record.nbr_presentations = result[record.id].get('presentation', 0) record.nbr_documents = result[record.id].get('document', 0) record.nbr_videos = result[record.id].get('video', 0) record.nbr_infographics = result[record.id].get('infographic', 0) record.total = record.nbr_presentations + record.nbr_documents + record.nbr_videos + record.nbr_infographics
class crm_lead(models.Model): _inherit = ['crm.lead'] @api.one @api.depends('order_ids') def _get_sale_amount_total(self): total = 0.0 nbr = 0 for order in self.order_ids: if order.state == 'draft': nbr += 1 if order.state not in ('draft', 'cancel'): total += order.currency_id.compute(order.amount_untaxed, self.company_currency) self.sale_amount_total = total self.sale_number = nbr sale_amount_total= fields.Float(compute='_get_sale_amount_total', string="Sum of Orders", readonly=True, digits=0) sale_number = fields.Integer(compute='_get_sale_amount_total', string="Number of Quotations", readonly=True) order_ids = fields.One2many('sale.order', 'opportunity_id', string='Orders') def retrieve_sales_dashboard(self, cr, uid, context=None): res = super(crm_lead, self).retrieve_sales_dashboard(cr, uid, context=None) res['invoiced'] = { 'this_month': 0, 'last_month': 0, } account_invoice_domain = [ ('state', 'in', ['open', 'paid']), ('user_id', '=', uid), ('date', '>=', date.today().replace(day=1) - relativedelta(months=+1)), ('type', 'in', ['out_invoice', 'out_refund']) ] invoice_ids = self.pool.get('account.invoice').search_read(cr, uid, account_invoice_domain, ['date', 'amount_untaxed_signed'], context=context) for inv in invoice_ids: if inv['date']: inv_date = datetime.strptime(inv['date'], tools.DEFAULT_SERVER_DATE_FORMAT).date() if inv_date <= date.today() and inv_date >= date.today().replace(day=1): res['invoiced']['this_month'] += inv['amount_untaxed_signed'] elif inv_date < date.today().replace(day=1) and inv_date >= date.today().replace(day=1) - relativedelta(months=+1): res['invoiced']['last_month'] += inv['amount_untaxed_signed'] res['invoiced']['target'] = self.pool('res.users').browse(cr, uid, uid, context=context).target_sales_invoiced return res
class pos_config(models.Model): _inherit = 'pos.config' @api.one @api.depends('cache_ids') def _get_oldest_cache_time(self): pos_cache = self.env['pos.cache'] oldest_cache = pos_cache.search([('config_id', '=', self.id)], order='write_date', limit=1) if oldest_cache: self.oldest_cache_time = oldest_cache.write_date # Use a related model to avoid the load of the cache when the pos load his config cache_ids = fields.One2many('pos.cache', 'config_id') oldest_cache_time = fields.Datetime(compute='_get_oldest_cache_time', string='Oldest cache time', readonly=True) def _get_cache_for_user(self): pos_cache = self.env['pos.cache'] cache_for_user = pos_cache.search([('id', 'in', self.cache_ids.ids), ('compute_user_id', '=', self.env.uid)]) if cache_for_user: return cache_for_user[0] else: return None @api.multi def get_products_from_cache(self, fields, domain): cache_for_user = self._get_cache_for_user() if cache_for_user: return cache_for_user.get_cache(domain, fields) else: pos_cache = self.env['pos.cache'] pos_cache.create({ 'config_id': self.id, 'product_domain': str(domain), 'product_fields': str(fields), 'compute_user_id': self.env.uid }) new_cache = self._get_cache_for_user() return new_cache.get_cache(domain, fields) @api.one def delete_cache(self): # throw away the old caches self.cache_ids.unlink()
class EventQuestion(models.Model): _name = 'event.question' _rec_name = 'title' _order = 'sequence,id' title = fields.Char(required=True, translate=True) event_id = fields.Many2one('event.event', required=True, ondelete='cascade') answer_ids = fields.One2many('event.answer', 'question_id', "Answers", required=True) sequence = fields.Integer(default=10) is_individual = fields.Boolean( 'Ask each attendee', help= "If True, this question will be asked for every attendee of a reservation. If " "not it will be asked only once and its value propagated to every attendees." )
class event_event(models.Model): _inherit = 'event.event' event_ticket_ids = fields.One2many( 'event.event.ticket', 'event_id', string='Event Ticket', default=lambda rec: rec._default_tickets(), copy=True) @api.model def _default_tickets(self): try: product = self.env.ref('event_sale.product_product_event') return [{ 'name': _('Subscription'), 'product_id': product.id, 'price': 0, }] except ValueError: return self.env['event.event.ticket']
class SaasPortalClient(models.Model): _inherit = 'saas_portal.client' subscription_start = fields.Datetime(string="Subscription start", track_visibility='onchange') expiration_datetime = fields.Datetime( string="Expiration", compute='_handle_paid_invoices', store=True, help='Subscription start plus all paid days from related invoices') invoice_lines = fields.One2many('account.invoice.line', 'saas_portal_client_id') trial = fields.Boolean('Trial', help='indication of trial clients', default=False, store=True, readonly=True, compute='_handle_paid_invoices') @api.multi @api.depends('invoice_lines.invoice_id.state') def _handle_paid_invoices(self): for client_obj in self: client_obj.expiration_datetime = datetime.strptime( client_obj.create_date, DEFAULT_SERVER_DATETIME_FORMAT) + timedelta( hours=client_obj.plan_id.expiration) # for trial days = 0 for line in self.env['account.invoice.line'].search([ ('saas_portal_client_id', '=', client_obj.id), ('invoice_id.state', '=', 'paid') ]): days += line.period if days != 0: client_obj.expiration_datetime = datetime.strptime( client_obj.subscription_start or client_obj.create_date, DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(days=days)
class Applicant(models.Model): _name = "hr.applicant" _description = "Applicant" _order = "priority desc, id desc" _inherit = ['mail.thread', 'ir.needaction_mixin', 'utm.mixin'] _mail_mass_mailing = _('Applicants') def _default_stage_id(self): if self._context.get('default_job_id'): ids = self.env['hr.recruitment.stage'].search( [('job_ids', '=', self._context['default_job_id']), ('fold', '=', False)], order='sequence asc', limit=1).ids if ids: return ids[0] return False def _default_company_id(self): company_id = False if self._context.get('default_department_id'): department = self.env['hr.department'].browse( self._context['default_department_id']) company_id = department.company_id.id if not company_id: company_id = self.env['res.company']._company_default_get( 'hr.applicant') return company_id name = fields.Char("Subject / Application Name", required=True) active = fields.Boolean( "Active", default=True, help= "If the active field is set to false, it will allow you to hide the case without removing it." ) description = fields.Text("Description") email_from = fields.Char("Email", size=128, help="These people will receive email.") email_cc = fields.Text( "Watchers Emails", size=252, 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" ) probability = fields.Float("Probability") partner_id = fields.Many2one('res.partner', "Contact") create_date = fields.Datetime("Creation Date", readonly=True, select=True) write_date = fields.Datetime("Update Date", readonly=True) stage_id = fields.Many2one('hr.recruitment.stage', 'Stage', track_visibility='onchange', domain="[('job_ids', '=', job_id)]", copy=False, select=1, default=_default_stage_id) last_stage_id = fields.Many2one( 'hr.recruitment.stage', "Last Stage", help= "Stage of the applicant before being in the current stage. Used for lost cases analysis." ) categ_ids = fields.Many2many('hr.applicant.category', string="Tags") company_id = fields.Many2one('res.company', "Company", default=_default_company_id) user_id = fields.Many2one('res.users', "Responsible", track_visibility="onchange", default=lambda self: self.env.uid) date_closed = fields.Datetime("Closed", readonly=True, select=True) date_open = fields.Datetime("Assigned", readonly=True, select=True) date_last_stage_update = fields.Datetime("Last Stage Update", select=True, default=fields.Datetime.now) date_action = fields.Date("Next Action Date") title_action = fields.Char("Next Action", size=64) priority = fields.Selection(AVAILABLE_PRIORITIES, "Appreciation", default='0') job_id = fields.Many2one('hr.job', "Applied Job") salary_proposed_extra = fields.Char( "Proposed Salary Extra", help="Salary Proposed by the Organisation, extra advantages") salary_expected_extra = fields.Char( "Expected Salary Extra", help="Salary Expected by Applicant, extra advantages") salary_proposed = fields.Float("Proposed Salary", help="Salary Proposed by the Organisation") salary_expected = fields.Float("Expected Salary", help="Salary Expected by Applicant") availability = fields.Date( "Availability", help= "The date at which the applicant will be available to start working") partner_name = fields.Char("Applicant's Name") partner_phone = fields.Char("Phone", size=32) partner_mobile = fields.Char("Mobile", size=32) type_id = fields.Many2one('hr.recruitment.degree', "Degree") department_id = fields.Many2one('hr.department', "Department") survey = fields.Many2one('survey.survey', related='job_id.survey_id', string="Survey") # TDE FIXME: rename to survey_id response_id = fields.Many2one('survey.user_input', "Response", ondelete="set null", oldname="response") reference = fields.Char("Referred By") day_open = fields.Float(compute='_compute_day', string="Days to Open") day_close = fields.Float(compute='_compute_day', string="Days to Close") color = fields.Integer("Color Index", default=0) emp_id = fields.Many2one('hr.employee', string="Employee", track_visibility="onchange", help="Employee linked to the applicant.") user_email = fields.Char(related='user_id.email', type="char", string="User Email", readonly=True) attachment_number = fields.Integer(compute='_get_attachment_number', string="Number of Attachments") employee_name = fields.Char(related='emp_id.name', string="Employee Name") attachment_ids = fields.One2many('ir.attachment', 'res_id', domain=[('res_model', '=', 'hr.applicant') ], string='Attachments') @api.depends('date_open', 'date_closed') @api.one def _compute_day(self): if self.date_open: date_create = datetime.strptime( self.create_date, tools.DEFAULT_SERVER_DATETIME_FORMAT) date_open = datetime.strptime(self.date_open, tools.DEFAULT_SERVER_DATETIME_FORMAT) self.day_open = (date_open - date_create).total_seconds() / (24.0 * 3600) if self.date_closed: date_create = datetime.strptime( self.create_date, tools.DEFAULT_SERVER_DATETIME_FORMAT) date_closed = datetime.strptime( self.date_closed, tools.DEFAULT_SERVER_DATETIME_FORMAT) self.day_close = (date_closed - date_create).total_seconds() / (24.0 * 3600) @api.multi def _get_attachment_number(self): read_group_res = self.env['ir.attachment'].read_group( [('res_model', '=', 'hr.applicant'), ('res_id', 'in', self.ids)], ['res_id'], ['res_id']) attach_data = dict( (res['res_id'], res['res_id_count']) for res in read_group_res) for record in self: record.attachment_number = attach_data.get(record.id, 0) @api.model def _read_group_stage_ids(self, ids, domain, read_group_order=None, access_rights_uid=None): access_rights_uid = access_rights_uid or self.env.uid Stage = self.env['hr.recruitment.stage'] order = Stage._order # lame hack to allow reverting search, should just work in the trivial case if read_group_order == 'stage_id desc': order = "%s desc" % order # retrieve job_id from the context and write the domain: ids + contextual columns (job or default) job_id = self._context.get('default_job_id') department_id = self._context.get('default_department_id') search_domain = [] if job_id: search_domain = [('job_ids', '=', job_id)] if department_id: if search_domain: search_domain = [ '|', ('job_ids.department_id', '=', department_id) ] + search_domain else: search_domain = [('job_ids.department_id', '=', department_id)] if self.ids: if search_domain: search_domain = ['|', ('id', 'in', self.ids)] + search_domain else: search_domain = [('id', 'in', self.ids)] stage_ids = Stage._search(search_domain, order=order, access_rights_uid=access_rights_uid) stages = Stage.sudo(access_rights_uid).browse(stage_ids) result = stages.name_get() # restore order of the search result.sort( lambda x, y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0]))) fold = {} for stage in stages: fold[stage.id] = stage.fold or False return result, fold _group_by_full = {'stage_id': _read_group_stage_ids} @api.onchange('job_id') def onchange_job_id(self): vals = self._onchange_job_id_internal(self.job_id.id) self.department_id = vals['value']['department_id'] self.user_id = vals['value']['user_id'] self.stage_id = vals['value']['stage_id'] def _onchange_job_id_internal(self, job_id): department_id = False user_id = False stage_id = self.stage_id.id if job_id: job = self.env['hr.job'].browse(job_id) department_id = job.department_id.id user_id = job.user_id.id if not self.stage_id: stage_ids = self.env['hr.recruitment.stage'].search( [('job_ids', '=', job.id), ('fold', '=', False)], order='sequence asc', limit=1).ids stage_id = stage_ids[0] if stage_ids else False return { 'value': { 'department_id': department_id, 'user_id': user_id, 'stage_id': stage_id } } @api.onchange('partner_id') def onchange_partner_id(self): self.partner_phone = self.partner_id.phone self.partner_mobile = self.partner_id.mobile self.email_from = self.partner_id.email @api.onchange('stage_id') def onchange_stage_id(self): vals = self._onchange_stage_id_internal(self.stage_id.id) if vals['value'].get('date_closed'): self.date_closed = vals['value']['date_closed'] def _onchange_stage_id_internal(self, stage_id): if not stage_id: return {'value': {}} stage = self.env['hr.recruitment.stage'].browse(stage_id) if stage.fold: return {'value': {'date_closed': fields.datetime.now()}} return {'value': {'date_closed': False}} @api.model def create(self, vals): if vals.get('department_id' ) and not self._context.get('default_department_id'): self = self.with_context( default_department_id=vals.get('department_id')) if vals.get('job_id') or self._context.get('default_job_id'): job_id = vals.get('job_id') or self._context.get('default_job_id') for key, value in self._onchange_job_id_internal( job_id)['value'].iteritems(): if key not in vals: vals[key] = value if vals.get('user_id'): vals['date_open'] = fields.Datetime.now() if 'stage_id' in vals: vals.update( self._onchange_stage_id_internal( vals.get('stage_id'))['value']) return super(Applicant, self.with_context(mail_create_nolog=True)).create(vals) @api.multi def write(self, vals): # user_id change: update date_open if vals.get('user_id'): vals['date_open'] = fields.Datetime.now() # stage_id: track last stage before update if 'stage_id' in vals: vals['date_last_stage_update'] = fields.Datetime.now() vals.update( self._onchange_stage_id_internal( vals.get('stage_id'))['value']) for applicant in self: vals['last_stage_id'] = applicant.stage_id.id res = super(Applicant, self).write(vals) else: res = super(Applicant, self).write(vals) # post processing: if stage changed, post a message in the chatter if vals.get('stage_id'): if self.stage_id.template_id: self.message_post_with_template(self.stage_id.template_id.id, notify=True, composition_mode='mass_mail') return res @api.model def get_empty_list_help(self, help): return super( Applicant, self.with_context( empty_list_help_model='hr.job', empty_list_help_id=self.env.context.get('default_job_id'), empty_list_help_document_name=_( "job applicants"))).get_empty_list_help(help) @api.multi def action_get_created_employee(self): self.ensure_one() action = self.env['ir.actions.act_window'].for_xml_id( 'hr', 'open_view_employee_list') action['res_id'] = self.mapped('emp_id').ids[0] return action @api.multi def action_makeMeeting(self): """ This opens Meeting's calendar view to schedule meeting on current applicant @return: Dictionary value for created Meeting view """ self.ensure_one() partners = self.partner_id | self.user_id.partner_id | self.department_id.manager_id.user_id.partner_id category = self.env.ref('hr_recruitment.categ_meet_interview') res = self.env['ir.actions.act_window'].for_xml_id( 'calendar', 'action_calendar_event') res['context'] = { 'search_default_partner_ids': self.partner_id.name, 'default_partner_ids': partners.ids, 'default_user_id': self.env.uid, 'default_name': self.name, 'default_categ_ids': category and [category.id] or False, } return res @api.multi def action_start_survey(self): self.ensure_one() # create a response and link it to this applicant if not self.response_id: response = self.env['survey.user_input'].create({ 'survey_id': self.survey.id, 'partner_id': self.partner_id.id }) self.response_id = response.id else: response = self.response_id # grab the token of the response and start surveying return self.survey.with_context( survey_token=response.token).action_start_survey() @api.multi def action_print_survey(self): """ If response is available then print this response otherwise print survey form (print template of the survey) """ self.ensure_one() if not self.response_id: return self.survey.action_print_survey() else: response = self.response_id return self.survey.with_context( survey_token=response.token).action_print_survey() @api.multi def action_get_attachment_tree_view(self): attachment_action = self.env.ref('base.action_attachment') action = attachment_action.read()[0] action['context'] = { 'default_res_model': self._name, 'default_res_id': self.ids[0] } action['domain'] = str( ['&', ('res_model', '=', self._name), ('res_id', 'in', self.ids)]) return action @api.multi def _track_subtype(self, init_values): record = self[0] if 'emp_id' in init_values and record.emp_id: return 'hr_recruitment.mt_applicant_hired' elif 'stage_id' in init_values and record.stage_id and record.stage_id.sequence <= 1: return 'hr_recruitment.mt_applicant_new' elif 'stage_id' in init_values and record.stage_id and record.stage_id.sequence > 1: return 'hr_recruitment.mt_applicant_stage_changed' return super(Applicant, self)._track_subtype(init_values) @api.model def message_get_reply_to(self, ids, default=None): """ Override to get the reply_to of the parent project. """ applicants = self.sudo().browse(ids) aliases = self.env['hr.job'].message_get_reply_to( applicants.mapped('job_id').ids, default=default) return dict( (applicant.id, aliases.get(applicant.job_id and applicant.job_id.id or 0, False)) for applicant in applicants) @api.multi def message_get_suggested_recipients(self): recipients = super(Applicant, self).message_get_suggested_recipients() for applicant in self: if applicant.partner_id: applicant._message_add_suggested_recipient( recipients, partner=applicant.partner_id, reason=_('Contact')) elif applicant.email_from: applicant._message_add_suggested_recipient( recipients, email=applicant.email_from, reason=_('Contact Email')) return recipients @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. """ val = msg.get('from').split('<')[0] defaults = { 'name': msg.get('subject') or _("No Subject"), 'partner_name': val, 'email_from': msg.get('from'), 'email_cc': msg.get('cc'), 'user_id': False, 'partner_id': msg.get('author_id', False), } if msg.get('priority'): defaults['priority'] = msg.get('priority') if custom_values: defaults.update(custom_values) return super(Applicant, self).message_new(msg, custom_values=defaults) @api.multi def create_employee_from_applicant(self): """ Create an hr.employee from the hr.applicants """ employee = False for applicant in self: address_id = contact_name = False if applicant.partner_id: address_id = applicant.partner_id.address_get(['contact' ])['contact'] contact_name = applicant.partner_id.name_get()[0][1] if applicant.job_id and (applicant.partner_name or contact_name): applicant.job_id.write({ 'no_of_hired_employee': applicant.job_id.no_of_hired_employee + 1 }) employee = self.env['hr.employee'].create({ 'name': applicant.partner_name or contact_name, 'job_id': applicant.job_id.id, 'address_home_id': address_id, 'department_id': applicant.department_id.id or False, 'address_id': applicant.company_id and applicant.company_id.partner_id and applicant.company_id.partner_id.id or False, 'work_email': applicant.department_id and applicant.department_id.company_id and applicant.department_id.company_id.email or False, 'work_phone': applicant.department_id and applicant.department_id.company_id and applicant.department_id.company_id.phone or False }) applicant.write({'emp_id': employee.id}) applicant.job_id.message_post( body=_('New Employee %s Hired') % applicant.partner_name if applicant.partner_name else applicant.name, subtype="hr_recruitment.mt_job_applicant_hired") employee._broadcast_welcome() else: raise UserError( _('You must define an Applied Job and a Contact Name for this applicant.' )) employee_action = self.env.ref('hr.open_view_employee_list') dict_act_window = employee_action.read([])[0] if employee: dict_act_window['res_id'] = employee.id dict_act_window['view_mode'] = 'form,tree' return dict_act_window @api.multi def archive_applicant(self): self.write({'active': False}) @api.multi def reset_applicant(self): """ Reinsert the applicant into the recruitment pipe in the first stage""" for applicant in self: first_stage_obj = self.env['hr.recruitment.stage'].search( [('job_ids', 'in', applicant.job_id.id)], order="sequence asc", limit=1) applicant.write({'active': True, 'stage_id': first_stage_obj.id})
class AccountAssetAsset(models.Model): _name = 'account.asset.asset' _description = 'Asset/Revenue Recognition' _inherit = ['mail.thread', 'ir.needaction_mixin'] account_move_ids = fields.One2many('account.move', 'asset_id', string='Entries', readonly=True, states={'draft': [('readonly', False)]}) entry_count = fields.Integer(compute='_entry_count', string='# Asset Entries') name = fields.Char(string='Asset Name', required=True, readonly=True, states={'draft': [('readonly', False)]}) code = fields.Char(string='Reference', size=32, readonly=True, states={'draft': [('readonly', False)]}) value = fields.Float(string='Gross Value', required=True, readonly=True, digits=0, states={'draft': [('readonly', False)]}, oldname='purchase_value') currency_id = fields.Many2one('res.currency', string='Currency', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env.user.company_id.currency_id.id) company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env['res.company']._company_default_get('account.asset.asset')) note = fields.Text() category_id = fields.Many2one('account.asset.category', string='Category', required=True, change_default=True, readonly=True, states={'draft': [('readonly', False)]}) date = fields.Date(string='Date', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=fields.Date.context_today, oldname="purchase_date") state = fields.Selection([('draft', 'Draft'), ('open', 'Running'), ('close', 'Close')], 'Status', required=True, copy=False, default='draft', help="When an asset is created, the status is 'Draft'.\n" "If the asset is confirmed, the status goes in 'Running' and the depreciation lines can be posted in the accounting.\n" "You can manually close an asset when the depreciation is over. If the last line of depreciation is posted, the asset automatically goes in that status.") active = fields.Boolean(default=True) partner_id = fields.Many2one('res.partner', string='Partner', readonly=True, states={'draft': [('readonly', False)]}) method = fields.Selection([('linear', 'Linear'), ('degressive', 'Degressive')], string='Computation Method', required=True, readonly=True, states={'draft': [('readonly', False)]}, default='linear', help="Choose the method to use to compute the amount of depreciation lines.\n * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n" " * Degressive: Calculated on basis of: Residual Value * Degressive Factor") method_number = fields.Integer(string='Number of Depreciations', readonly=True, states={'draft': [('readonly', False)]}, default=5, help="The number of depreciations needed to depreciate your asset") method_period = fields.Integer(string='Number of Months in a Period', required=True, readonly=True, default=12, states={'draft': [('readonly', False)]}, help="The amount of time between two depreciations, in months") method_end = fields.Date(string='Ending Date', readonly=True, states={'draft': [('readonly', False)]}) method_progress_factor = fields.Float(string='Degressive Factor', readonly=True, default=0.3, states={'draft': [('readonly', False)]}) value_residual = fields.Float(compute='_amount_residual', method=True, digits=0, string='Residual Value') method_time = fields.Selection([('number', 'Number of Depreciations'), ('end', 'Ending Date')], string='Time Method', required=True, readonly=True, default='number', states={'draft': [('readonly', False)]}, help="Choose the method to use to compute the dates and number of depreciation lines.\n" " * Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n" " * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond.") prorata = fields.Boolean(string='Prorata Temporis', readonly=True, states={'draft': [('readonly', False)]}, help='Indicates that the first depreciation entry for this asset have to be done from the purchase date instead of the first January / Start date of fiscal year') depreciation_line_ids = fields.One2many('account.asset.depreciation.line', 'asset_id', string='Depreciation Lines', readonly=True, states={'draft': [('readonly', False)], 'open': [('readonly', False)]}) salvage_value = fields.Float(string='Salvage Value', digits=0, readonly=True, states={'draft': [('readonly', False)]}, help="It is the amount you plan to have that you cannot depreciate.") invoice_id = fields.Many2one('account.invoice', string='Invoice', states={'draft': [('readonly', False)]}, copy=False) type = fields.Selection(related="category_id.type", string='Type', required=True) @api.multi def unlink(self): for asset in self: if asset.state in ['open', 'close']: raise UserError(_('You cannot delete a document is in %s state.') % (asset.state,)) if asset.account_move_ids: raise UserError(_('You cannot delete a document that contains posted entries.')) return super(AccountAssetAsset, self).unlink() @api.multi def _get_last_depreciation_date(self): """ @param id: ids of a account.asset.asset objects @return: Returns a dictionary of the effective dates of the last depreciation entry made for given asset ids. If there isn't any, return the purchase date of this asset """ self.env.cr.execute(""" SELECT a.id as id, COALESCE(MAX(m.date),a.date) AS date FROM account_asset_asset a LEFT JOIN account_move m ON (m.asset_id = a.id) WHERE a.id IN %s GROUP BY a.id, a.date """, (tuple(self.ids),)) result = dict(self.env.cr.fetchall()) return result @api.model def _cron_generate_entries(self): assets = self.env['account.asset.asset'].search([('state', '=', 'open')]) assets._compute_entries(datetime.today()) def _compute_board_amount(self, sequence, residual_amount, amount_to_depr, undone_dotation_number, posted_depreciation_line_ids, total_days, depreciation_date): amount = 0 if sequence == undone_dotation_number: amount = residual_amount else: if self.method == 'linear': amount = amount_to_depr / (undone_dotation_number - len(posted_depreciation_line_ids)) if self.prorata and self.category_id.type == 'purchase': amount = amount_to_depr / self.method_number days = total_days - float(depreciation_date.strftime('%j')) if sequence == 1: amount = (amount_to_depr / self.method_number) / total_days * days elif sequence == undone_dotation_number: amount = (amount_to_depr / self.method_number) / total_days * (total_days - days) elif self.method == 'degressive': amount = residual_amount * self.method_progress_factor if self.prorata: days = total_days - float(depreciation_date.strftime('%j')) if sequence == 1: amount = (residual_amount * self.method_progress_factor) / total_days * days elif sequence == undone_dotation_number: amount = (residual_amount * self.method_progress_factor) / total_days * (total_days - days) return amount def _compute_board_undone_dotation_nb(self, depreciation_date, total_days): undone_dotation_number = self.method_number if self.method_time == 'end': end_date = datetime.strptime(self.method_end, DF).date() undone_dotation_number = 0 while depreciation_date <= end_date: depreciation_date = date(depreciation_date.year, depreciation_date.month, depreciation_date.day) + relativedelta(months=+self.method_period) undone_dotation_number += 1 if self.prorata and self.category_id.type == 'purchase': undone_dotation_number += 1 return undone_dotation_number @api.multi def compute_depreciation_board(self): self.ensure_one() posted_depreciation_line_ids = self.depreciation_line_ids.filtered(lambda x: x.move_check) unposted_depreciation_line_ids = self.depreciation_line_ids.filtered(lambda x: not x.move_check) # Remove old unposted depreciation lines. We cannot use unlink() with One2many field commands = [(2, line_id.id, False) for line_id in unposted_depreciation_line_ids] if self.value_residual != 0.0: amount_to_depr = residual_amount = self.value_residual if self.prorata: depreciation_date = datetime.strptime(self._get_last_depreciation_date()[self.id], DF).date() else: # depreciation_date = 1st of January of purchase year asset_date = datetime.strptime(self.date, DF).date() # if we already have some previous validated entries, starting date isn't 1st January but last entry + method period if posted_depreciation_line_ids and posted_depreciation_line_ids[0].depreciation_date: last_depreciation_date = datetime.strptime(posted_depreciation_line_ids[0].depreciation_date, DF).date() depreciation_date = last_depreciation_date + relativedelta(months=+self.method_period) else: depreciation_date = asset_date day = depreciation_date.day month = depreciation_date.month year = depreciation_date.year total_days = (year % 4) and 365 or 366 undone_dotation_number = self._compute_board_undone_dotation_nb(depreciation_date, total_days) for x in range(len(posted_depreciation_line_ids), undone_dotation_number): sequence = x + 1 amount = self._compute_board_amount(sequence, residual_amount, amount_to_depr, undone_dotation_number, posted_depreciation_line_ids, total_days, depreciation_date) amount = self.currency_id.round(amount) residual_amount -= amount vals = { 'amount': amount, 'asset_id': self.id, 'sequence': sequence, 'name': (self.code or '') + '/' + str(sequence), 'remaining_value': residual_amount, 'depreciated_value': self.value - (self.salvage_value + residual_amount), 'depreciation_date': depreciation_date.strftime(DF), } commands.append((0, False, vals)) # Considering Depr. Period as months depreciation_date = date(year, month, day) + relativedelta(months=+self.method_period) day = depreciation_date.day month = depreciation_date.month year = depreciation_date.year self.write({'depreciation_line_ids': commands}) return True @api.multi def validate(self): self.write({'state': 'open'}) fields = [ 'method', 'method_number', 'method_period', 'method_end', 'method_progress_factor', 'method_time', 'salvage_value', 'invoice_id', ] ref_tracked_fields = self.env['account.asset.asset'].fields_get(fields) for asset in self: tracked_fields = ref_tracked_fields.copy() if asset.method == 'linear': del(tracked_fields['method_progress_factor']) if asset.method_time != 'end': del(tracked_fields['method_end']) else: del(tracked_fields['method_number']) dummy, tracking_value_ids = asset._message_track(tracked_fields, dict.fromkeys(fields)) asset.message_post(subject=_('Asset created'), tracking_value_ids=tracking_value_ids) @api.multi def set_to_close(self): move_ids = [] for asset in self: unposted_depreciation_line_ids = asset.depreciation_line_ids.filtered(lambda x: not x.move_check) if unposted_depreciation_line_ids: old_values = { 'method_end': asset.method_end, 'method_number': asset.method_number, } # Remove all unposted depr. lines commands = [(2, line_id.id, False) for line_id in unposted_depreciation_line_ids] # Create a new depr. line with the residual amount and post it sequence = len(asset.depreciation_line_ids) - len(unposted_depreciation_line_ids) + 1 today = datetime.today().strftime(DF) vals = { 'amount': asset.value_residual, 'asset_id': asset.id, 'sequence': sequence, 'name': (asset.code or '') + '/' + str(sequence), 'remaining_value': 0, 'depreciated_value': asset.value - asset.salvage_value, # the asset is completely depreciated 'depreciation_date': today, } commands.append((0, False, vals)) asset.write({'depreciation_line_ids': commands, 'method_end': today, 'method_number': sequence}) tracked_fields = self.env['account.asset.asset'].fields_get(['method_number', 'method_end']) changes, tracking_value_ids = asset._message_track(tracked_fields, old_values) if changes: asset.message_post(subject=_('Asset sold or disposed. Accounting entry awaiting for validation.'), tracking_value_ids=tracking_value_ids) move_ids += asset.depreciation_line_ids[-1].create_move(post_move=False) if move_ids: name = _('Disposal Move') view_mode = 'form' if len(move_ids) > 1: name = _('Disposal Moves') view_mode = 'tree,form' return { 'name': name, 'view_type': 'form', 'view_mode': view_mode, 'res_model': 'account.move', 'type': 'ir.actions.act_window', 'target': 'current', 'res_id': move_ids[0], } @api.multi def set_to_draft(self): self.write({'state': 'draft'}) @api.one @api.depends('value', 'salvage_value', 'depreciation_line_ids') def _amount_residual(self): total_amount = 0.0 for line in self.depreciation_line_ids: if line.move_check: total_amount += line.amount self.value_residual = self.value - total_amount - self.salvage_value @api.onchange('company_id') def onchange_company_id(self): self.currency_id = self.company_id.currency_id.id @api.multi @api.depends('account_move_ids') def _entry_count(self): for asset in self: asset.entry_count = self.env['account.move'].search_count([('asset_id', '=', asset.id)]) @api.one @api.constrains('prorata', 'method_time') def _check_prorata(self): if self.prorata and self.method_time != 'number': raise ValidationError(_('Prorata temporis can be applied only for time method "number of depreciations".')) @api.onchange('category_id') def onchange_category_id(self): vals = self.onchange_category_id_values(self.category_id.id) # We cannot use 'write' on an object that doesn't exist yet if vals: for k, v in vals['value'].iteritems(): setattr(self, k, v) def onchange_category_id_values(self, category_id): if category_id: category = self.env['account.asset.category'].browse(category_id) return { 'value': { 'method': category.method, 'method_number': category.method_number, 'method_time': category.method_time, 'method_period': category.method_period, 'method_progress_factor': category.method_progress_factor, 'method_end': category.method_end, 'prorata': category.prorata, } } @api.onchange('method_time') def onchange_method_time(self): if self.method_time != 'number': self.prorata = False @api.multi def copy_data(self, default=None): if default is None: default = {} default['name'] = self.name + _(' (copy)') return super(AccountAssetAsset, self).copy_data(default)[0] @api.multi def _compute_entries(self, date): depreciation_ids = self.env['account.asset.depreciation.line'].search([ ('asset_id', 'in', self.ids), ('depreciation_date', '<=', date), ('move_check', '=', False)]) return depreciation_ids.create_move() @api.model def create(self, vals): asset = super(AccountAssetAsset, self.with_context(mail_create_nolog=True)).create(vals) asset.compute_depreciation_board() return asset @api.multi def write(self, vals): res = super(AccountAssetAsset, self).write(vals) if 'depreciation_line_ids' not in vals: self.compute_depreciation_board() return res @api.multi def open_entries(self): return { 'name': _('Journal Entries'), 'view_type': 'form', 'view_mode': 'tree,form', 'res_model': 'account.move', 'view_id': False, 'type': 'ir.actions.act_window', 'context': dict(self.env.context or {}, search_default_asset_id=self.id, default_asset_id=self.id), }
class Slide(models.Model): """ This model represents actual presentations. Those must be one of four types: - Presentation - Document - Infographic - Video Slide has various statistics like view count, embed count, like, dislikes """ _name = 'slide.slide' _inherit = [ 'mail.thread', 'website.seo.metadata', 'website.published.mixin' ] _description = 'Slides' _PROMOTIONAL_FIELDS = [ '__last_update', 'name', 'image_thumb', 'image_medium', 'slide_type', 'total_views', 'category_id', 'channel_id', 'description', 'tag_ids', 'write_date', 'create_date', 'website_published', 'website_url', 'website_meta_title', 'website_meta_description', 'website_meta_keywords' ] _sql_constraints = [('name_uniq', 'UNIQUE(channel_id, name)', 'The slide name must be unique within a channel')] # description name = fields.Char('Title', required=True, translate=True) description = fields.Text('Description', translate=True) channel_id = fields.Many2one('slide.channel', string="Channel", required=True) category_id = fields.Many2one('slide.category', string="Category", domain="[('channel_id', '=', channel_id)]") tag_ids = fields.Many2many('slide.tag', 'rel_slide_tag', 'slide_id', 'tag_id', string='Tags') download_security = fields.Selection([('none', 'No One'), ('user', 'Authentified Users Only'), ('public', 'Everyone')], string='Download Security', required=True, default='user') image = fields.Binary('Image', attachment=True) image_medium = fields.Binary('Medium', compute="_get_image", store=True, attachment=True) image_thumb = fields.Binary('Thumbnail', compute="_get_image", store=True, attachment=True) @api.depends('image') def _get_image(self): for record in self: if record.image: record.image_medium = image.crop_image(record.image, type='top', ratio=(4, 3), thumbnail_ratio=4) record.image_thumb = image.crop_image(record.image, type='top', ratio=(4, 3), thumbnail_ratio=6) else: record.image_medium = False record.iamge_thumb = False # content slide_type = fields.Selection( [('infographic', 'Infographic'), ('presentation', 'Presentation'), ('document', 'Document'), ('video', 'Video')], string='Type', required=True, default='document', help= "Document type will be set automatically depending on file type, height and width." ) index_content = fields.Text('Transcript') datas = fields.Binary('Content') url = fields.Char('Document URL', help="Youtube or Google Document URL") document_id = fields.Char('Document ID', help="Youtube or Google Document ID") mime_type = fields.Char('Mime-type') @api.onchange('url') def on_change_url(self): self.ensure_one() if self.url: res = self._parse_document_url(self.url) if res.get('error'): raise Warning( _('Could not fetch data from url. Document or access right not available:\n%s' ) % res['error']) values = res['values'] if not values.get('document_id'): raise Warning( _('Please enter valid Youtube or Google Doc URL')) for key, value in values.iteritems(): setattr(self, key, value) # website date_published = fields.Datetime('Publish Date') website_message_ids = fields.One2many( 'mail.message', 'res_id', domain=lambda self: [('model', '=', self._name), ('message_type', '=', 'comment')], string='Website Messages', help="Website communication history") likes = fields.Integer('Likes') dislikes = fields.Integer('Dislikes') # views embedcount_ids = fields.One2many('slide.embed', 'slide_id', string="Embed Count") slide_views = fields.Integer('# of Website Views') embed_views = fields.Integer('# of Embedded Views') total_views = fields.Integer("Total # Views", default="0", compute='_compute_total', store=True) @api.depends('slide_views', 'embed_views') def _compute_total(self): for record in self: record.total_views = record.slide_views + record.embed_views embed_code = fields.Text('Embed Code', readonly=True, compute='_get_embed_code') def _get_embed_code(self): base_url = self.env['ir.config_parameter'].get_param('web.base.url') for record in self: if record.datas and not record.document_id: record.embed_code = '<iframe src="%s/slides/embed/%s?page=1" allowFullScreen="true" height="%s" width="%s" frameborder="0"></iframe>' % ( base_url, record.id, 315, 420) elif record.slide_type == 'video' and record.document_id: if not record.mime_type: # embed youtube video record.embed_code = '<iframe src="//www.youtube.com/embed/%s?theme=light" allowFullScreen="true" frameborder="0"></iframe>' % ( record.document_id) else: # embed google doc video record.embed_code = '<embed src="https://video.google.com/get_player?ps=docs&partnerid=30&docid=%s" type="application/x-shockwave-flash"></embed>' % ( record.document_id) else: record.embed_code = False @api.multi @api.depends('name') def _website_url(self, name, arg): res = super(Slide, self)._website_url(name, arg) base_url = self.env['ir.config_parameter'].get_param('web.base.url') #link_tracker is not in dependencies, so use it to shorten url only if installed. if self.env.registry.get('link.tracker'): LinkTracker = self.env['link.tracker'] res.update({(slide.id, LinkTracker.sudo().create({ 'url': '%s/slides/slide/%s' % (base_url, slug(slide)) }).short_url) for slide in self}) else: res.update({(slide.id, '%s/slides/slide/%s' % (base_url, slug(slide))) for slide in self}) return res @api.model def create(self, values): if not values.get('index_content'): values['index_content'] = values.get('description') if values.get( 'slide_type') == 'infographic' and not values.get('image'): values['image'] = values['datas'] if values.get( 'website_published') and not values.get('date_published'): values['date_published'] = datetime.datetime.now() if values.get('url'): doc_data = self._parse_document_url(values['url']).get( 'values', dict()) for key, value in doc_data.iteritems(): values.setdefault(key, value) # Do not publish slide if user has not publisher rights if not self.user_has_groups('base.group_website_publisher'): values['website_published'] = False slide = super(Slide, self).create(values) slide.channel_id.message_subscribe_users() slide._post_publication() return slide @api.multi def write(self, values): if values.get('url'): doc_data = self._parse_document_url(values['url']).get( 'values', dict()) for key, value in doc_data.iteritems(): values.setdefault(key, value) res = super(Slide, self).write(values) if values.get('website_published'): self.date_published = datetime.datetime.now() self._post_publication() return res @api.model def check_field_access_rights(self, operation, fields): """ As per channel access configuration (visibility) - public ==> no restriction on slides access - private ==> restrict all slides of channel based on access group defined on channel group_ids field - partial ==> show channel, but presentations based on groups means any user can see channel but not slide's content. For private: implement using record rule For partial: user can see channel, but channel gridview have slide detail so we have to implement partial field access mechanism for public user so he can have access of promotional field (name, view_count) of slides, but not all fields like data (actual pdf content) all fields should be accessible only for user group defined on channel group_ids """ if self.env.uid == SUPERUSER_ID: return fields or list(self._fields) fields = super(Slide, self).check_field_access_rights(operation, fields) # still read not perform so we can not access self.channel_id if self.ids: self.env.cr.execute( 'SELECT DISTINCT channel_id FROM ' + self._table + ' WHERE id IN %s', (tuple(self.ids), )) channel_ids = [x[0] for x in self.env.cr.fetchall()] channels = self.env['slide.channel'].sudo().browse(channel_ids) limited_access = all( channel.visibility == 'partial' and not len(channel.group_ids & self.env.user.groups_id) for channel in channels) if limited_access: fields = [ field for field in fields if field in self._PROMOTIONAL_FIELDS ] return fields def get_related_slides(self, limit=20): domain = [('website_published', '=', True), ('channel_id.visibility', '!=', 'private'), ('id', '!=', self.id)] if self.category_id: domain += [('category_id', '=', self.category_id.id)] for record in self.search(domain, limit=limit): yield record def get_most_viewed_slides(self, limit=20): for record in self.search([('website_published', '=', True), ('channel_id.visibility', '!=', 'private'), ('id', '!=', self.id)], limit=limit, order='total_views desc'): yield record def _post_publication(self): base_url = self.env['ir.config_parameter'].get_param('web.base.url') for slide in self.filtered(lambda slide: slide.website_published): publish_template = slide.channel_id.publish_template_id html_body = publish_template.with_context({ 'base_url': base_url }).render_template(publish_template.body_html, 'slide.slide', slide.id) slide.channel_id.message_post( body=html_body, subtype='website_slides.mt_channel_slide_published') return True @api.one def send_share_email(self, email): base_url = self.env['ir.config_parameter'].get_param('web.base.url') return self.channel_id.share_template_id.with_context({ 'email': email, 'base_url': base_url }).send_mail(self.id) # -------------------------------------------------- # Parsing methods # -------------------------------------------------- @api.model def _fetch_data(self, base_url, data, content_type=False): result = {'values': dict()} try: if data: base_url = base_url + '?%s' % urlencode(data) req = urllib2.Request(base_url) content = urllib2.urlopen(req).read() if content_type == 'json': result['values'] = json.loads(content) elif content_type in ('image', 'pdf'): result['values'] = content.encode('base64') else: result['values'] = content except urllib2.HTTPError as e: result['error'] = e.read() e.close() except urllib2.URLError as e: result['error'] = e.reason return result def _find_document_data_from_url(self, url): expr = re.compile( r'^.*((youtu.be/)|(v/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*' ) arg = expr.match(url) document_id = arg and arg.group(7) or False if document_id: return ('youtube', document_id) expr = re.compile( r'(^https:\/\/docs.google.com|^https:\/\/drive.google.com).*\/d\/([^\/]*)' ) arg = expr.match(url) document_id = arg and arg.group(2) or False if document_id: return ('google', document_id) return (None, False) def _parse_document_url(self, url, only_preview_fields=False): document_source, document_id = self._find_document_data_from_url(url) if document_source and hasattr(self, '_parse_%s_document' % document_source): return getattr(self, '_parse_%s_document' % document_source)( document_id, only_preview_fields) return {'error': _('Unknown document')} def _parse_youtube_document(self, document_id, only_preview_fields): key = self.env['ir.config_parameter'].sudo().get_param( 'website_slides.google_app_key') fetch_res = self._fetch_data( 'https://www.googleapis.com/youtube/v3/videos', { 'id': document_id, 'key': key, 'part': 'snippet', 'fields': 'items(id,snippet)' }, 'json') if fetch_res.get('error'): return fetch_res values = {'slide_type': 'video', 'document_id': document_id} youtube_values = fetch_res['values'].get('items', list(dict()))[0] if youtube_values.get('snippet'): snippet = youtube_values['snippet'] if only_preview_fields: values.update({ 'url_src': snippet['thumbnails']['high']['url'], 'title': snippet['title'], 'description': snippet['description'] }) return values values.update({ 'name': snippet['title'], 'image': self._fetch_data(snippet['thumbnails']['high']['url'], {}, 'image')['values'], 'description': snippet['description'], }) return {'values': values} @api.model def _parse_google_document(self, document_id, only_preview_fields): def get_slide_type(vals): # TDE FIXME: WTF ?? image = Image.open(io.BytesIO(vals['image'].decode('base64'))) width, height = image.size if height > width: return 'document' else: return 'presentation' key = self.env['ir.config_parameter'].sudo().get_param( 'website_slides.google_app_key') fetch_res = self._fetch_data( 'https://www.googleapis.com/drive/v2/files/%s' % document_id, { 'projection': 'BASIC', 'key': key }, "json") if fetch_res.get('error'): return fetch_res google_values = fetch_res['values'] if only_preview_fields: return { 'url_src': google_values['thumbnailLink'], 'title': google_values['title'], } values = { 'name': google_values['title'], 'image': self._fetch_data( google_values['thumbnailLink'].replace('=s220', ''), {}, 'image')['values'], 'mime_type': google_values['mimeType'], 'document_id': document_id, } if google_values['mimeType'].startswith('video/'): values['slide_type'] = 'video' elif google_values['mimeType'].startswith('image/'): values['datas'] = values['image'] values['slide_type'] = 'infographic' elif google_values['mimeType'].startswith( 'application/vnd.google-apps'): values['datas'] = self._fetch_data( google_values['exportLinks']['application/pdf'], {}, 'pdf')['values'] values['slide_type'] = get_slide_type(values) if google_values['exportLinks'].get('text/plain'): values['index_content'] = self._fetch_data( google_values['exportLinks']['text/plain'], {})['values'] if google_values['exportLinks'].get('text/csv'): values['index_content'] = self._fetch_data( google_values['exportLinks']['text/csv'], {})['values'] elif google_values['mimeType'] == 'application/pdf': # TODO: Google Drive PDF document doesn't provide plain text transcript values['datas'] = self._fetch_data(google_values['webContentLink'], {}, 'pdf')['values'] values['slide_type'] = get_slide_type(values) return {'values': values}
class Channel(models.Model): """ A channel is a container of slides. It has group-based access configuration allowing to configure slide upload and access. Slides can be promoted in channels. """ _name = 'slide.channel' _description = 'Channel for Slides' _inherit = [ 'mail.thread', 'website.seo.metadata', 'website.published.mixin' ] _order = 'sequence, id' _order_by_strategy = { 'most_viewed': 'total_views desc', 'most_voted': 'likes desc', 'latest': 'date_published desc', } name = fields.Char('Name', translate=True, required=True) description = fields.Html('Description', translate=True) sequence = fields.Integer(default=10, help='Display order') category_ids = fields.One2many('slide.category', 'channel_id', string="Categories") slide_ids = fields.One2many('slide.slide', 'channel_id', string="Slides") promote_strategy = fields.Selection([('none', 'No Featured Presentation'), ('latest', 'Latest Published'), ('most_voted', 'Most Voted'), ('most_viewed', 'Most Viewed'), ('custom', 'Featured Presentation')], string="Featuring Policy", default='most_voted', required=True) custom_slide_id = fields.Many2one('slide.slide', string='Slide to Promote') promoted_slide_id = fields.Many2one('slide.slide', string='Featured Slide', compute='_compute_promoted_slide_id', store=True) @api.depends('custom_slide_id', 'promote_strategy', 'slide_ids.likes', 'slide_ids.total_views', "slide_ids.date_published") def _compute_promoted_slide_id(self): for record in self: if record.promote_strategy == 'none': record.promoted_slide_id = False elif record.promote_strategy == 'custom': record.promoted_slide_id = record.custom_slide_id elif record.promote_strategy: slides = self.env['slide.slide'].search( [('website_published', '=', True), ('channel_id', '=', record.id)], limit=1, order=self._order_by_strategy[record.promote_strategy]) record.promoted_slide_id = slides and slides[0] or False nbr_presentations = fields.Integer('Number of Presentations', compute='_count_presentations', store=True) nbr_documents = fields.Integer('Number of Documents', compute='_count_presentations', store=True) nbr_videos = fields.Integer('Number of Videos', compute='_count_presentations', store=True) nbr_infographics = fields.Integer('Number of Infographics', compute='_count_presentations', store=True) total = fields.Integer(compute='_count_presentations', store=True) @api.depends('slide_ids.slide_type', 'slide_ids.website_published') def _count_presentations(self): result = dict.fromkeys(self.ids, dict()) res = self.env['slide.slide'].read_group( [('website_published', '=', True), ('channel_id', 'in', self.ids)], ['channel_id', 'slide_type'], ['channel_id', 'slide_type'], lazy=False) for res_group in res: result[res_group['channel_id'][0]][res_group[ 'slide_type']] = result[res_group['channel_id'][0]].get( res_group['slide_type'], 0) + res_group['__count'] for record in self: record.nbr_presentations = result[record.id].get('presentation', 0) record.nbr_documents = result[record.id].get('document', 0) record.nbr_videos = result[record.id].get('video', 0) record.nbr_infographics = result[record.id].get('infographic', 0) record.total = record.nbr_presentations + record.nbr_documents + record.nbr_videos + record.nbr_infographics publish_template_id = fields.Many2one( 'mail.template', string='Published Template', help="Email template to send slide publication through email", default=lambda self: self.env['ir.model.data'].xmlid_to_res_id( 'website_slides.slide_template_published')) share_template_id = fields.Many2one( 'mail.template', string='Shared Template', help="Email template used when sharing a slide", default=lambda self: self.env['ir.model.data'].xmlid_to_res_id( 'website_slides.slide_template_shared')) visibility = fields.Selection( [('public', 'Public'), ('private', 'Private'), ('partial', 'Show channel but restrict presentations')], default='public', required=True) group_ids = fields.Many2many( 'res.groups', 'rel_channel_groups', 'channel_id', 'group_id', string='Channel Groups', help="Groups allowed to see presentations in this channel") access_error_msg = fields.Html( 'Error Message', help="Message to display when not accessible due to access rights", default= "<p>This channel is private and its content is restricted to some users.</p>", translate=True) upload_group_ids = fields.Many2many( 'res.groups', 'rel_upload_groups', 'channel_id', 'group_id', string='Upload Groups', help= "Groups allowed to upload presentations in this channel. If void, every user can upload." ) # not stored access fields, depending on each user can_see = fields.Boolean('Can See', compute='_compute_access') can_see_full = fields.Boolean('Full Access', compute='_compute_access') can_upload = fields.Boolean('Can Upload', compute='_compute_access') @api.one @api.depends('visibility', 'group_ids', 'upload_group_ids') def _compute_access(self): self.can_see = self.visibility in [ 'public', 'private' ] or bool(self.group_ids & self.env.user.groups_id) self.can_see_full = self.visibility == 'public' or bool( self.group_ids & self.env.user.groups_id) self.can_upload = self.can_see and (not self.upload_group_ids or bool(self.upload_group_ids & self.env.user.groups_id)) @api.multi @api.depends('name') def _website_url(self, name, arg): res = super(Channel, self)._website_url(name, arg) base_url = self.env['ir.config_parameter'].get_param('web.base.url') res.update({(channel.id, '%s/slides/%s' % (base_url, slug(channel))) for channel in self}) return res @api.onchange('visibility') def change_visibility(self): if self.visibility == 'public': self.group_ids = False
class account_analytic_account(models.Model): _name = 'account.analytic.account' _inherit = ['mail.thread'] _description = 'Analytic Account' _order = 'code, name asc' @api.multi def _compute_debit_credit_balance(self): analytic_line_obj = self.env['account.analytic.line'] domain = [('account_id', 'in', self.mapped('id'))] if self._context.get('from_date', False): domain.append(('date', '>=', self._context['from_date'])) if self._context.get('to_date', False): domain.append(('date', '<=', self._context['to_date'])) account_amounts = analytic_line_obj.search_read(domain, ['account_id', 'amount']) account_ids = set([line['account_id'][0] for line in account_amounts]) data_debit = {account_id: 0.0 for account_id in account_ids} data_credit = {account_id: 0.0 for account_id in account_ids} for account_amount in account_amounts: if account_amount['amount'] < 0.0: data_debit[account_amount['account_id'][0]] += account_amount['amount'] else: data_credit[account_amount['account_id'][0]] += account_amount['amount'] for account in self: account.debit = abs(data_debit.get(account.id, 0.0)) account.credit = data_credit.get(account.id, 0.0) account.balance = account.credit - account.debit name = fields.Char(string='Analytic Account', index=True, required=True, track_visibility='onchange') code = fields.Char(string='Reference', index=True, track_visibility='onchange') # FIXME: account_type is probably not necessary anymore, could be removed in v10 account_type = fields.Selection([ ('normal', 'Analytic View') ], string='Type of Account', required=True, default='normal') tag_ids = fields.Many2many('account.analytic.tag', 'account_analytic_account_tag_rel', 'account_id', 'tag_id', string='Tags', copy=True) line_ids = fields.One2many('account.analytic.line', 'account_id', string="Analytic Lines") company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.user.company_id) # use auto_join to speed up name_search call partner_id = fields.Many2one('res.partner', string='Customer', auto_join=True) balance = fields.Monetary(compute='_compute_debit_credit_balance', string='Balance') debit = fields.Monetary(compute='_compute_debit_credit_balance', string='Debit') credit = fields.Monetary(compute='_compute_debit_credit_balance', string='Credit') currency_id = fields.Many2one(related="company_id.currency_id", string="Currency", readonly=True) @api.multi def name_get(self): res = [] for analytic in self: name = analytic.name if analytic.code: name = '['+analytic.code+'] '+name if analytic.partner_id: name = name +' - '+analytic.partner_id.commercial_partner_id.name res.append((analytic.id, name)) return res @api.model def name_search(self, name='', args=None, operator='ilike', limit=100): args = args or [] domain = ['|', ('code', operator, name), ('name', operator, name)] partners = self.env['res.partner'].search([('name', operator, name)], limit=limit) if partners: domain = ['|'] + domain + [('partner_id', 'in', partners.ids)] recs = self.search(domain + args, limit=limit) return recs.name_get()
class Users(models.Model): _inherit = 'res.users' def __init__(self, pool, cr): init_res = super(Users, self).__init__(pool, cr) self.SELF_WRITEABLE_FIELDS = list( set(self.SELF_WRITEABLE_FIELDS + [ 'country_id', 'city', 'website', 'website_description', 'website_published' ])) return init_res create_date = fields.Datetime('Create Date', readonly=True, copy=False, select=True) karma = fields.Integer('Karma', default=0) badge_ids = fields.One2many('gamification.badge.user', 'user_id', string='Badges', copy=False) gold_badge = fields.Integer('Gold badges count', compute="_get_user_badge_level") silver_badge = fields.Integer('Silver badges count', compute="_get_user_badge_level") bronze_badge = fields.Integer('Bronze badges count', compute="_get_user_badge_level") @api.multi @api.depends('badge_ids') def _get_user_badge_level(self): """ Return total badge per level of users TDE CLEANME: shouldn't check type is forum ? """ for user in self: user.gold_badge = 0 user.silver_badge = 0 user.bronze_badge = 0 self.env.cr.execute( """ SELECT bu.user_id, b.level, count(1) FROM gamification_badge_user bu, gamification_badge b WHERE bu.user_id IN %s AND bu.badge_id = b.id AND b.level IS NOT NULL GROUP BY bu.user_id, b.level ORDER BY bu.user_id; """, [tuple(self.ids)]) for (user_id, level, count) in self.env.cr.fetchall(): # levels are gold, silver, bronze but fields have _badge postfix self.browse(user_id)['{}_badge'.format(level)] = count @api.model def _generate_forum_token(self, user_id, email): """Return a token for email validation. This token is valid for the day and is a hash based on a (secret) uuid generated by the forum module, the user_id, the email and currently the day (to be updated if necessary). """ forum_uuid = self.env['ir.config_parameter'].sudo().get_param( 'website_forum.uuid') return hashlib.sha256( '%s-%s-%s-%s' % (datetime.now().replace(hour=0, minute=0, second=0, microsecond=0), forum_uuid, user_id, email)).hexdigest() @api.one def send_forum_validation_email(self, forum_id=None): if not self.email: return False token = self._generate_forum_token(self.id, self.email) activation_template = self.env.ref('website_forum.validation_email') if activation_template: params = {'token': token, 'id': self.id, 'email': self.email} if forum_id: params['forum_id'] = forum_id base_url = self.env['ir.config_parameter'].get_param( 'web.base.url') token_url = base_url + '/forum/validate_email?%s' % urlencode( params) activation_template.sudo().with_context( token_url=token_url).send_mail(self.id, force_send=True) return True @api.one def process_forum_validation_token(self, token, email, forum_id=None, context=None): validation_token = self._generate_forum_token(self.id, email) if token == validation_token and self.karma == 0: karma = 3 forum = None if forum_id: forum = self.env['forum.forum'].browse(forum_id) else: forum_ids = self.env['forum.forum'].search([], limit=1) if forum_ids: forum = forum_ids[0] if forum: # karma gained: karma to ask a question and have 2 downvotes karma = forum.karma_ask + (-2 * forum.karma_gen_question_downvote) return self.write({'karma': karma}) return False @api.multi def add_karma(self, karma): for user in self: user.karma += karma return True @api.model def get_serialised_gamification_summary(self, excluded_categories=None): if isinstance(excluded_categories, list): if 'forum' not in excluded_categories: excluded_categories.append('forum') else: excluded_categories = ['forum'] return super(Users, self).get_serialised_gamification_summary( excluded_categories=excluded_categories) # Wrapper for call_kw with inherits @api.multi def open_website_url(self): return self.mapped('partner_id').open_website_url()
class HrEquipment(models.Model): _name = 'hr.equipment' _inherit = ['mail.thread'] _description = 'Equipment' @api.multi def _track_subtype(self, init_values): self.ensure_one() if ('employee_id' in init_values and self.employee_id) or ('department_id' in init_values and self.department_id): return 'hr_equipment.mt_mat_assign' return super(HrEquipment, self)._track_subtype(init_values) @api.multi def name_get(self): result = [] for record in self: if record.name and record.serial_no: result.append((record.id, record.name + '/' + record.serial_no)) if record.name and not record.serial_no: result.append((record.id, record.name)) return result @api.model def name_search(self, name, args=None, operator='ilike', limit=100): args = args or [] recs = self.browse() if name: recs = self.search([('name', '=', name)] + args, limit=limit) if not recs: recs = self.search([('name', operator, name)] + args, limit=limit) return recs.name_get() name = fields.Char('Asset Name', required=True, translate=True) user_id = fields.Many2one('res.users', string='Technician', track_visibility='onchange') employee_id = fields.Many2one('hr.employee', string='Assigned to Employee', track_visibility='onchange') department_id = fields.Many2one('hr.department', string='Assigned to Department', track_visibility='onchange') category_id = fields.Many2one('hr.equipment.category', string='Asset Category', track_visibility='onchange') partner_id = fields.Many2one('res.partner', string='Vendor', domain="[('supplier', '=', 1)]") partner_ref = fields.Char('Vendor Reference') location = fields.Char('Location') model = fields.Char('Model') serial_no = fields.Char('Serial Number', copy=False) assign_date = fields.Date('Assigned Date', track_visibility='onchange') cost = fields.Float('Cost') note = fields.Text('Note') warranty = fields.Date('Warranty') color = fields.Integer('Color Index') scrap_date = fields.Date('Scrap Date') equipment_assign_to = fields.Selection( [('department', 'Department'), ('employee', 'Employee')], string='Used By', required=True, default='employee') maintenance_ids = fields.One2many('hr.equipment.request', 'equipment_id') maintenance_count = fields.Integer(compute='_compute_maintenance_count', string="Maintenance", store=True) maintenance_open_count = fields.Integer(compute='_compute_maintenance_count', string="Current Maintenance", store=True) @api.one @api.depends('maintenance_ids.stage_id.done') def _compute_maintenance_count(self): self.maintenance_count = len(self.maintenance_ids) self.maintenance_open_count = len(self.maintenance_ids.filtered(lambda x: not x.stage_id.done)) @api.onchange('equipment_assign_to') def _onchange_equipment_assign_to(self): if self.equipment_assign_to == 'employee': self.department_id = False if self.equipment_assign_to == 'department': self.employee_id = False self.assign_date = fields.Date.context_today(self) @api.onchange('category_id') def _onchange_category_id(self): self.user_id = self.category_id.user_id _sql_constraints = [ ('serial_no', 'unique(serial_no)', "Another asset already exists with this serial number!"), ] @api.model def create(self, vals): equipment = super(HrEquipment, self).create(vals) # subscribe employee or department manager when equipment assign to him. user_ids = [] if equipment.employee_id and equipment.employee_id.user_id: user_ids.append(equipment.employee_id.user_id.id) if equipment.department_id and equipment.department_id.manager_id and equipment.department_id.manager_id.user_id: user_ids.append(equipment.department_id.manager_id.user_id.id) if user_ids: equipment.message_subscribe_users(user_ids=user_ids) return equipment @api.multi def write(self, vals): user_ids = [] # subscribe employee or department manager when equipment assign to employee or department. if vals.get('employee_id'): user_id = self.env['hr.employee'].browse(vals['employee_id'])['user_id'] if user_id: user_ids.append(user_id.id) if vals.get('department_id'): department = self.env['hr.department'].browse(vals['department_id']) if department and department.manager_id and department.manager_id.user_id: user_ids.append(department.manager_id.user_id.id) if user_ids: self.message_subscribe_users(user_ids=user_ids) return super(HrEquipment, self).write(vals) @api.multi def _read_group_category_ids(self, domain, read_group_order=None, access_rights_uid=None): """ Read group customization in order to display all the category in the kanban view, even if they are empty """ category_obj = self.env['hr.equipment.category'] order = category_obj._order access_rights_uid = access_rights_uid or self._uid if read_group_order == 'category_id desc': order = '%s desc' % order category_ids = category_obj._search([], order=order, access_rights_uid=access_rights_uid) result = [category.name_get()[0] for category in category_obj.browse(category_ids)] # restore order of the search result.sort(lambda x, y: cmp(category_ids.index(x[0]), category_ids.index(y[0]))) fold = {} for category in category_obj.browse(category_ids): fold[category.id] = category.fold return result, fold _group_by_full = { 'category_id': _read_group_category_ids }
class account_financial_report(models.Model): _name = "account.financial.report" _description = "Account Report" @api.multi @api.depends('parent_id', 'parent_id.level') def _get_level(self): '''Returns a dictionary with key=the ID of a record and value = the level of this record in the tree structure.''' for report in self: level = 0 if report.parent_id: level = report.parent_id.level + 1 report.level = level def _get_children_by_order(self): '''returns a recordset of all the children computed recursively, and sorted by sequence. Ready for the printing''' res = self children = self.search([('parent_id', 'in', self.ids)], order='sequence ASC') if children: res += children._get_children_by_order() return res name = fields.Char('Report Name', required=True, translate=True) parent_id = fields.Many2one('account.financial.report', 'Parent') children_ids = fields.One2many('account.financial.report', 'parent_id', 'Account Report') sequence = fields.Integer('Sequence') level = fields.Integer(compute='_get_level', string='Level', store=True) type = fields.Selection([ ('sum', 'View'), ('accounts', 'Accounts'), ('account_type', 'Account Type'), ('account_report', 'Report Value'), ], 'Type', default='sum') account_ids = fields.Many2many('account.account', 'account_account_financial_report', 'report_line_id', 'account_id', 'Accounts') account_report_id = fields.Many2one('account.financial.report', 'Report Value') account_type_ids = fields.Many2many( 'account.account.type', 'account_account_financial_report_type', 'report_id', 'account_type_id', 'Account Types') sign = fields.Selection( [(-1, 'Reverse balance sign'), (1, 'Preserve balance sign')], 'Sign on Reports', required=True, default=1, help= 'For accounts that are typically more debited than credited and that you would like to print as negative amounts in your reports, you should reverse the sign of the balance; e.g.: Expense account. The same applies for accounts that are typically more credited than debited and that you would like to print as positive amounts in your reports; e.g.: Income account.' ) display_detail = fields.Selection( [('no_detail', 'No detail'), ('detail_flat', 'Display children flat'), ('detail_with_hierarchy', 'Display children with hierarchy')], 'Display details', default='detail_flat') style_overwrite = fields.Selection( [ (0, 'Automatic formatting'), (1, 'Main Title 1 (bold, underlined)'), (2, 'Title 2 (bold)'), (3, 'Title 3 (bold, smaller)'), (4, 'Normal Text'), (5, 'Italic Text (smaller)'), (6, 'Smallest Text'), ], 'Financial Report Style', default=0, help= "You can set up here the format you want this record to be displayed. If you leave the automatic formatting, it will be computed based on the financial reports hierarchy (auto-computed field 'level')." )
class Channel(models.Model): """ A mail.channel is a discussion group that may behave like a listener on documents. """ _description = 'Discussion channel' _name = 'mail.channel' _mail_flat_thread = False _mail_post_access = 'read' _inherit = ['mail.thread'] _inherits = {'mail.alias': 'alias_id'} def _get_default_image(self): image_path = modules.get_module_resource('mail', 'static/src/img', 'groupdefault.png') return tools.image_resize_image_big(open(image_path, 'rb').read().encode('base64')) name = fields.Char('Name', required=True, translate=True) channel_type = fields.Selection([ ('chat', 'Chat Discussion'), ('channel', 'Channel')], 'Channel Type', default='channel') description = fields.Text('Description') uuid = fields.Char('UUID', size=50, select=True, default=lambda self: '%s' % uuid.uuid4()) email_send = fields.Boolean('Send messages by email', default=False) # multi users channel channel_last_seen_partner_ids = fields.One2many('mail.channel.partner', 'channel_id', string='Last Seen') channel_partner_ids = fields.Many2many('res.partner', 'mail_channel_partner', 'channel_id', 'partner_id', string='Listeners') channel_message_ids = fields.Many2many('mail.message', 'mail_message_mail_channel_rel') message_is_follower = fields.Boolean('Is a member', compute='_compute_message_is_follower') # access public = fields.Selection([ ('public', 'Everyone'), ('private', 'Invited people only'), ('groups', 'Selected group of users')], 'Privacy', required=True, default='groups', help='This group is visible by non members. Invisible groups can add members through the invite button.') group_public_id = fields.Many2one('res.groups', string='Authorized Group', default=lambda self: self.env.ref('base.group_user')) group_ids = fields.Many2many( 'res.groups', rel='mail_channel_res_group_rel', id1='mail_channel_id', id2='groups_id', string='Auto Subscription', help="Members of those groups will automatically added as followers. " "Note that they will be able to manage their subscription manually " "if necessary.") # image: all image fields are base64 encoded and PIL-supported image = fields.Binary("Photo", default=_get_default_image, attachment=True, help="This field holds the image used as photo for the group, limited to 1024x1024px.") image_medium = fields.Binary('Medium-sized photo', compute='_get_image', inverse='_set_image_medium', store=True, attachment=True, help="Medium-sized photo of the group. It is automatically " "resized as a 128x128px image, with aspect ratio preserved. " "Use this field in form views or some kanban views.") image_small = fields.Binary('Small-sized photo', compute='_get_image', inverse='_set_image_small', store=True, attachment=True, help="Small-sized photo of the group. It is automatically " "resized as a 64x64px image, with aspect ratio preserved. " "Use this field anywhere a small image is required.") alias_id = fields.Many2one( 'mail.alias', 'Alias', ondelete="restrict", required=True, help="The email address associated with this group. New emails received will automatically create new topics.") @api.multi def _compute_message_is_follower(self): memberships = self.env['mail.channel.partner'].sudo().search([ ('channel_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id), ]) membership_ids = memberships.mapped('channel_id') for record in self: record.message_is_follower = record in membership_ids @api.one @api.depends('image') def _get_image(self): self.image_medium = tools.image_resize_image_medium(self.image) self.image_small = tools.image_resize_image_small(self.image) def _set_image_medium(self): self.image = tools.image_resize_image_big(self.image_medium) def _set_image_small(self): self.image = tools.image_resize_image_big(self.image_small) @api.model def create(self, vals): # Create channel and alias channel = super(Channel, self.with_context( alias_model_name=self._name, alias_parent_model_name=self._name, mail_create_nolog=True, mail_create_nosubscribe=True) ).create(vals) channel.alias_id.write({"alias_force_thread_id": channel.id, 'alias_parent_thread_id': channel.id}) if vals.get('group_ids'): channel._subscribe_users() # make channel listen itself: posting on a channel notifies the channel if not self._context.get('mail_channel_noautofollow'): channel.message_subscribe(channel_ids=[channel.id]) return channel @api.multi def unlink(self): aliases = self.mapped('alias_id') # Delete mail.channel try: all_emp_group = self.env.ref('mail.channel_all_employees') except ValueError: all_emp_group = None if all_emp_group and all_emp_group in self: raise UserError(_('You cannot delete those groups, as the Whole Company group is required by other modules.')) res = super(Channel, self).unlink() # Cascade-delete mail aliases as well, as they should not exist without the mail.channel. aliases.sudo().unlink() return res @api.multi def write(self, vals): result = super(Channel, self).write(vals) if vals.get('group_ids'): self._subscribe_users() return result def _subscribe_users(self): for mail_channel in self: mail_channel.write({'channel_partner_ids': [(4, pid) for pid in mail_channel.mapped('group_ids').mapped('users').mapped('partner_id').ids]}) @api.multi def action_follow(self): self.ensure_one() channel_partner = self.mapped('channel_last_seen_partner_ids').filtered(lambda cp: cp.partner_id == self.env.user.partner_id) if not channel_partner: return self.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': self.env.user.partner_id.id})]}) @api.multi def action_unfollow(self): partner_id = self.env.user.partner_id.id channel_info = self.channel_info('unsubscribe')[0] # must be computed before leaving the channel (access rights) result = self.write({'channel_partner_ids': [(3, partner_id)]}) self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', partner_id), channel_info) if not self.email_send: notification = _('<div class="o_mail_notification">left <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (self.id, self.name,) # post 'channel left' message as root since the partner just unsubscribed from the channel self.sudo().message_post(body=notification, message_type="notification", subtype="mail.mt_comment", author_id=partner_id) return result @api.multi def _notification_group_recipients(self, message, recipients, done_ids, group_data): """ All recipients of a message on a channel are considered as partners. This means they will receive a minimal email, without a link to access in the backend. Mailing lists should indeed send minimal emails to avoid the noise. """ for recipient in recipients: group_data['partner'] |= recipient done_ids.add(recipient.id) return super(Channel, self)._notification_group_recipients(message, recipients, done_ids, group_data) @api.multi def message_get_email_values(self, notif_mail=None): self.ensure_one() res = super(Channel, self).message_get_email_values(notif_mail=notif_mail) headers = {} if res.get('headers'): try: headers.update(eval(res['headers'])) except Exception: pass headers['Precedence'] = 'list' # avoid out-of-office replies from MS Exchange # http://blogs.technet.com/b/exchange/archive/2006/10/06/3395024.aspx headers['X-Auto-Response-Suppress'] = 'OOF' if self.alias_domain and self.alias_name: headers['List-Id'] = '%s.%s' % (self.alias_name, self.alias_domain) headers['List-Post'] = '<mailto:%s@%s>' % (self.alias_name, self.alias_domain) # Avoid users thinking it was a personal message # X-Forge-To: will replace To: after SMTP envelope is determined by ir.mail.server list_to = '"%s" <%s@%s>' % (self.name, self.alias_name, self.alias_domain) headers['X-Forge-To'] = list_to res['headers'] = repr(headers) return res @api.multi def message_get_recipient_values(self, notif_message=None, recipient_ids=None): # real mailing list: multiple recipients (hidden by X-Forge-To) if self.alias_domain and self.alias_name: return { 'email_to': ','.join(formataddr((partner.name, partner.email)) for partner in self.env['res.partner'].sudo().browse(recipient_ids)), 'recipient_ids': [], } return super(Channel, self).message_get_recipient_values(notif_message=notif_message, recipient_ids=recipient_ids) @api.multi @api.returns('self', lambda value: value.id) def message_post(self, body='', subject=None, message_type='notification', subtype=None, parent_id=False, attachments=None, content_subtype='html', **kwargs): # auto pin 'direct_message' channel partner self.filtered(lambda channel: channel.channel_type == 'chat').mapped('channel_last_seen_partner_ids').write({'is_pinned': True}) # apply shortcode (text only) subsitution body = self.env['mail.shortcode'].apply_shortcode(body, shortcode_type='text') message = super(Channel, self.with_context(mail_create_nosubscribe=True)).message_post(body=body, subject=subject, message_type=message_type, subtype=subtype, parent_id=parent_id, attachments=attachments, content_subtype=content_subtype, **kwargs) return message #------------------------------------------------------ # Instant Messaging API #------------------------------------------------------ # A channel header should be broadcasted: # - when adding user to channel (only to the new added partners) # - when folding/minimizing a channel (only to the user making the action) # A message should be broadcasted: # - when a message is posted on a channel (to the channel, using _notify() method) # Anonymous method @api.multi def _broadcast(self, partner_ids): """ Broadcast the current channel header to the given partner ids :param partner_ids : the partner to notify """ notifications = self._channel_channel_notifications(partner_ids) self.env['bus.bus'].sendmany(notifications) @api.multi def _channel_channel_notifications(self, partner_ids): """ Generate the bus notifications of current channel for the given partner ids :param partner_ids : the partner to send the current channel header :returns list of bus notifications (tuple (bus_channe, message_content)) """ notifications = [] for partner in self.env['res.partner'].browse(partner_ids): user_id = partner.user_ids and partner.user_ids[0] or False if user_id: for channel_info in self.sudo(user_id).channel_info(): notifications.append([(self._cr.dbname, 'res.partner', partner.id), channel_info]) return notifications @api.multi def _notify(self, message): """ Broadcast the given message on the current channels. Send the message on the Bus Channel (uuid for public mail.channel, and partner private bus channel (the tuple)). A partner will receive only on message on its bus channel, even if this message belongs to multiple mail channel. Then 'channel_ids' field of the received message indicates on wich mail channel the message should be displayed. :param : mail.message to broadcast """ message.ensure_one() notifications = self._channel_message_notifications(message) self.env['bus.bus'].sendmany(notifications) @api.multi def _channel_message_notifications(self, message): """ Generate the bus notifications for the given message :param message : the mail.message to sent :returns list of bus notifications (tuple (bus_channe, message_content)) """ message_values = message.message_format()[0] notifications = [] for channel in self: notifications.append([(self._cr.dbname, 'mail.channel', channel.id), dict(message_values)]) # add uuid to allow anonymous to listen if channel.public == 'public': notifications.append([channel.uuid, dict(message_values)]) return notifications @api.multi def channel_info(self, extra_info = False): """ Get the informations header for the current channels :returns a list of channels values :rtype : list(dict) """ channel_infos = [] partner_channels = self.env['mail.channel.partner'] # find the channel partner state, if logged user if self.env.user and self.env.user.partner_id: partner_channels = self.env['mail.channel.partner'].search([('partner_id', '=', self.env.user.partner_id.id), ('channel_id', 'in', self.ids)]) # for each channel, build the information header and include the logged partner information for channel in self: info = { 'id': channel.id, 'name': channel.name, 'uuid': channel.uuid, 'state': 'open', 'is_minimized': False, 'channel_type': channel.channel_type, 'public': channel.public, 'mass_mailing': channel.email_send, } if extra_info: info['info'] = extra_info # add the partner for 'direct mesage' channel if channel.channel_type == 'chat': info['direct_partner'] = (channel.sudo() .with_context(active_test=False) .channel_partner_ids .filtered(lambda p: p.id != self.env.user.partner_id.id) .read(['id', 'name', 'im_status'])) # add user session state, if available and if user is logged if partner_channels.ids: partner_channel = partner_channels.filtered(lambda c: channel.id == c.channel_id.id) if len(partner_channel) >= 1: partner_channel = partner_channel[0] info['state'] = partner_channel.fold_state or 'open' info['is_minimized'] = partner_channel.is_minimized info['seen_message_id'] = partner_channel.seen_message_id.id # add needaction and unread counter, since the user is logged info['message_needaction_counter'] = channel.message_needaction_counter info['message_unread_counter'] = channel.message_unread_counter channel_infos.append(info) return channel_infos @api.multi def channel_fetch_message(self, last_id=False, limit=20): """ Return message values of the current channel. :param last_id : last message id to start the research :param limit : maximum number of messages to fetch :returns list of messages values :rtype : list(dict) """ self.ensure_one() domain = [("channel_ids", "in", self.ids)] if last_id: domain.append(("id", "<", last_id)) return self.env['mail.message'].message_fetch(domain=domain, limit=limit) # User methods @api.model def channel_get(self, partners_to, pin=True): """ Get the canonical private channel between some partners, create it if needed. To reuse an old channel (conversation), this one must be private, and contains only the given partners. :param partners_to : list of res.partner ids to add to the conversation :param pin : True if getting the channel should pin it for the current user :returns a channel header, or False if the users_to was False :rtype : dict """ if partners_to: partners_to.append(self.env.user.partner_id.id) # determine type according to the number of partner in the channel self.env.cr.execute(""" SELECT P.channel_id as channel_id FROM mail_channel C, mail_channel_partner P WHERE P.channel_id = C.id AND C.public LIKE 'private' AND P.partner_id IN %s AND channel_type LIKE 'chat' GROUP BY P.channel_id HAVING COUNT(P.partner_id) = %s """, (tuple(partners_to), len(partners_to),)) result = self.env.cr.dictfetchall() if result: # get the existing channel between the given partners channel = self.browse(result[0].get('channel_id')) # pin up the channel for the current partner if pin: self.env['mail.channel.partner'].search([('partner_id', '=', self.env.user.partner_id.id), ('channel_id', '=', channel.id)]).write({'is_pinned': True}) else: # create a new one channel = self.create({ 'channel_partner_ids': [(4, partner_id) for partner_id in partners_to], 'public': 'private', 'channel_type': 'chat', 'email_send': False, 'name': ', '.join(self.env['res.partner'].sudo().browse(partners_to).mapped('name')), }) # broadcast the channel header to the other partner (not me) channel._broadcast(partners_to) return channel.channel_info()[0] return False @api.model def channel_get_and_minimize(self, partners_to): channel = self.channel_get(partners_to) if channel: self.channel_minimize(channel['uuid']) return channel @api.model def channel_fold(self, uuid, state=None): """ Update the fold_state of the given session. In order to syncronize web browser tabs, the change will be broadcast to himself (the current user channel). Note: the user need to be logged :param state : the new status of the session for the current user. """ domain = [('partner_id', '=', self.env.user.partner_id.id), ('channel_id.uuid', '=', uuid)] for session_state in self.env['mail.channel.partner'].search(domain): if not state: state = session_state.fold_state if session_state.fold_state == 'open': state = 'folded' else: state = 'open' session_state.write({ 'fold_state': state, 'is_minimized': bool(state != 'closed'), }) self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), session_state.channel_id.channel_info()[0]) @api.model def channel_minimize(self, uuid, minimized=True): values = { 'fold_state': minimized and 'open' or 'closed', 'is_minimized': minimized } domain = [('partner_id', '=', self.env.user.partner_id.id), ('channel_id.uuid', '=', uuid)] channel_partners = self.env['mail.channel.partner'].search(domain) channel_partners.write(values) self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_partners.channel_id.channel_info()[0]) @api.model def channel_pin(self, uuid, pinned=False): # add the person in the channel, and pin it (or unpin it) channel = self.search([('uuid', '=', uuid)]) channel_partners = self.env['mail.channel.partner'].search([('partner_id', '=', self.env.user.partner_id.id), ('channel_id', '=', channel.id)]) if not pinned: self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel.channel_info('unsubscribe')[0]) if channel_partners: channel_partners.write({'is_pinned': pinned}) @api.multi def channel_seen(self): self.ensure_one() if self.channel_message_ids.ids: last_message_id = self.channel_message_ids.ids[0] # zero is the index of the last message self.env['mail.channel.partner'].search([('channel_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id)]).write({'seen_message_id': last_message_id}) self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), {'info': 'channel_seen', 'id': self.id, 'last_message_id': last_message_id}) return last_message_id @api.multi def channel_invite(self, partner_ids): """ Add the given partner_ids to the current channels and broadcast the channel header to them. :param partner_ids : list of partner id to add """ partners = self.env['res.partner'].browse(partner_ids) # add the partner for channel in self: partners_to_add = partners - channel.channel_partner_ids channel.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': partner_id}) for partner_id in partners_to_add.ids]}) for partner in partners_to_add: notification = _('<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (self.id, self.name,) self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment", author_id=partner.id) # broadcast the channel header to the added partner self._broadcast(partner_ids) #------------------------------------------------------ # Instant Messaging View Specific (Slack Client Action) #------------------------------------------------------ @api.model def get_init_notifications(self): """ Get unread messages and old messages received less than AWAY_TIMER ago of minimized channel ONLY. This aims to set the minimized channel when refreshing the page. Note : the user need to be logged """ # get current user's minimzed channel minimized_channels = self.env['mail.channel.partner'].search([('is_minimized', '=', True), ('partner_id', '=', self.env.user.partner_id.id)]).mapped('channel_id') # get the message since the AWAY_TIMER threshold = datetime.datetime.now() - datetime.timedelta(seconds=AWAY_TIMER) threshold = threshold.strftime(DEFAULT_SERVER_DATETIME_FORMAT) domain = [('channel_ids', 'in', minimized_channels.ids), ('create_date', '>', threshold)] # get the message since the last poll of the user presence = self.env['bus.presence'].search([('user_id', '=', self._uid)], limit=1) if presence: domain.append(('create_date', '>', presence.last_poll)) # do the message search message_values = self.env['mail.message'].message_fetch(domain=domain) # create the notifications (channel infos first, then messages) notifications = [] for channel_info in minimized_channels.channel_info(): notifications.append([(self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_info]) for message_value in message_values: for channel_id in message_value['channel_ids']: if channel_id in minimized_channels.ids: message_value['channel_ids'] = [channel_id] notifications.append([(self._cr.dbname, 'mail.channel', channel_id), dict(message_value)]) return notifications @api.model def channel_fetch_slot(self): """ Return the channels of the user grouped by 'slot' (channel, direct_message or private_group), and the mapping between partner_id/channel_id for direct_message channels. :returns dict : the grouped channels and the mapping """ values = {} my_partner_id = self.env.user.partner_id.id pinned_channels = self.env['mail.channel.partner'].search([('partner_id', '=', my_partner_id), ('is_pinned', '=', True)]).mapped('channel_id') # get the group/public channels values['channel_channel'] = self.search([('channel_type', '=', 'channel'), ('public', 'in', ['public', 'groups']), ('channel_partner_ids', 'in', [my_partner_id])]).channel_info() # get the pinned 'direct message' channel direct_message_channels = self.search([('channel_type', '=', 'chat'), ('id', 'in', pinned_channels.ids)]) values['channel_direct_message'] = direct_message_channels.channel_info() # get the private group values['channel_private_group'] = self.search([('channel_type', '=', 'channel'), ('public', '=', 'private'), ('channel_partner_ids', 'in', [my_partner_id])]).channel_info() return values @api.model def channel_search_to_join(self, name=None, domain=None): """ Return the channel info of the channel the current partner can join :param name : the name of the researched channels :param domain : the base domain of the research :returns dict : channel dict """ if not domain: domain = [] domain = expression.AND([ [('channel_type', '=', 'channel')], [('channel_partner_ids', 'not in', [self.env.user.partner_id.id])], [('public', '!=', 'private')], domain ]) if name: domain = expression.AND([domain, [('name', 'ilike', '%'+name+'%')]]) return self.search(domain).read(['name', 'public', 'uuid', 'channel_type']) @api.multi def channel_join_and_get_info(self): self.ensure_one() if self.channel_type == 'channel' and not self.email_send: notification = _('<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (self.id, self.name,) self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment") self.action_follow() channel_info = self.channel_info()[0] self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_info) return channel_info @api.model def channel_create(self, name, privacy='public'): """ Create a channel and add the current partner, broadcast it (to make the user directly listen to it when polling) :param name : the name of the channel to create :param privacy : privacy of the channel. Should be 'public' or 'private'. :return dict : channel header """ # create the channel new_channel = self.create({ 'name': name, 'public': privacy, 'email_send': False, 'channel_partner_ids': [(4, self.env.user.partner_id.id)] }) channel_info = new_channel.channel_info('creation')[0] notification = _('<div class="o_mail_notification">created <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (new_channel.id, new_channel.name,) new_channel.message_post(body=notification, message_type="notification", subtype="mail.mt_comment") self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_info) return channel_info @api.model def get_mention_suggestions(self, search, limit=8): """ Return 'limit'-first channels' id, name and public fields such that the name matches a 'search' string. Exclude channels of type chat (DM), and private channels the current user isn't registered to. """ domain = expression.AND([ [('name', 'ilike', search)], [('channel_type', '=', 'channel')], expression.OR([ [('public', '!=', 'private')], [('channel_partner_ids', 'in', [self.env.user.partner_id.id])] ]) ]) return self.search_read(domain, ['id', 'name', 'public'], limit=limit) @api.model def channel_fetch_listeners(self, uuid): """ Return the id, name and email of partners listening to the given channel """ self._cr.execute(""" SELECT P.id, P.name, P.email FROM mail_channel_partner CP INNER JOIN res_partner P ON CP.partner_id = P.id INNER JOIN mail_channel C ON CP.channel_id = C.id WHERE C.uuid = %s""", (uuid,)) return self._cr.dictfetchall() @api.multi def channel_fetch_preview(self): """ Return the last message of the given channels """ self._cr.execute(""" SELECT mail_channel_id AS id, MAX(mail_message_id) AS message_id FROM mail_message_mail_channel_rel WHERE mail_channel_id IN %s GROUP BY mail_channel_id """, (tuple(self.ids),)) channels_preview = dict((r['message_id'], r) for r in self._cr.dictfetchall()) last_messages = self.env['mail.message'].browse(channels_preview.keys()).message_format() for message in last_messages: channel = channels_preview[message['id']] del(channel['message_id']) channel['last_message'] = message return channels_preview.values()
class SaasConfig(models.TransientModel): _name = 'saas.config' def _default_database_id(self): return self._context.get('active_id') action = fields.Selection([('edit', 'Edit'), ('upgrade', 'Configure'), ('delete', 'Delete')], 'Action') database_id = fields.Many2one('saas_portal.client', string='Database', default=_default_database_id) server_id = fields.Many2one('saas_portal.server', string='Server', related='database_id.server_id', readonly=True) update_addons_list = fields.Boolean('Update Addon List', default=True) update_addons = fields.Char('Update Addons', size=256) install_addons = fields.Char('Install Addons', size=256) uninstall_addons = fields.Char('Uninstall Addons', size=256) access_owner_add = fields.Char('Grant access to Owner') access_remove = fields.Char( 'Restrict access', help='Restrict access for all users except super-user.\nNote, that ') fix_ids = fields.One2many('saas.config.fix', 'config_id', 'Fixes') param_ids = fields.One2many('saas.config.param', 'config_id', 'Parameters') description = fields.Text('Result') @api.multi def execute_action(self): res = False method = '%s_database' % self.action if hasattr(self, method): res = getattr(self, method)() return res @api.multi def delete_database(self): return self.database_id.delete_database() @api.multi def upgrade_database(self): self.ensure_one() obj = self[0] scheme = request.httprequest.scheme payload = { 'update_addons_list': (obj.update_addons_list or ''), 'update_addons': obj.update_addons.split(',') if obj.update_addons else [], 'install_addons': obj.install_addons.split(',') if obj.install_addons else [], 'uninstall_addons': obj.uninstall_addons.split(',') if obj.uninstall_addons else [], 'access_owner_add': obj.access_owner_add.split(',') if obj.access_owner_add else [], 'access_remove': obj.access_remove.split(',') if obj.access_remove else [], 'fixes': [[x.model, x.method] for x in obj.fix_ids], 'params': [{ 'key': x.key, 'value': x.value, 'hidden': x.hidden } for x in obj.param_ids], } res_text = self.do_upgrade_database(payload, self.database_id.id) obj.write({'description': res_text}) return { 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'saas.config', 'res_id': obj.id, 'target': 'new' } @api.model def do_upgrade_database(self, payload, saas_portal_client_id): client = self.env['saas_portal.client'].browse(saas_portal_client_id) state = { 'data': payload, } url = client.server_id._request_server( path='/saas_server/upgrade_database', client_id=client.client_id, state=state, )[0] res = requests.get(url, verify=(self.server_id.request_scheme == 'https' and self.server_id.verify_ssl)) if res.ok != True: raise Warning('Reason: %s \n Message: %s' % (res.reason, res.content)) return res.text
class SaleOrderEventRegistration(models.TransientModel): _name = "registration.editor" sale_order_id = fields.Many2one('sale.order', 'Sale Order', required=True) event_registration_ids = fields.One2many('registration.editor.line', 'editor_id', string='Registrations to Edit') @api.model def default_get(self, fields): res = super(SaleOrderEventRegistration, self).default_get(fields) if not res.get('sale_order_id'): sale_order_id = res.get('sale_order_id', self._context.get('active_id')) res['sale_order_id'] = sale_order_id sale_order = self.env['sale.order'].browse(res.get('sale_order_id')) registrations = self.env['event.registration'].search([ ('sale_order_id', '=', sale_order.id), ('event_ticket_id', 'in', sale_order.mapped('order_line.event_ticket_id').ids), ('state', '!=', 'cancel') ]) attendee_list = [] for so_line in [l for l in sale_order.order_line if l.event_ticket_id]: existing_registrations = [ r for r in registrations if r.event_ticket_id == so_line.event_ticket_id ] for reg in existing_registrations: attendee_list.append({ 'event_id': reg.event_id.id, 'event_ticket_id': reg.event_ticket_id.id, 'registration_id': reg.id, 'name': reg.name, 'email': reg.email, 'phone': reg.phone, 'sale_order_line_id': so_line.id, }) for count in range( int(so_line.product_uom_qty) - len(existing_registrations)): attendee_list.append([ 0, 0, { 'event_id': so_line.event_id.id, 'event_ticket_id': so_line.event_ticket_id.id, 'sale_order_line_id': so_line.id, } ]) res['event_registration_ids'] = attendee_list res = self._convert_to_cache(res, validate=False) res = self._convert_to_write(res) return res @api.multi def action_make_registration(self): Registration = self.env['event.registration'] for wizard in self: for wiz_registration in wizard.event_registration_ids: if wiz_registration.registration_id: wiz_registration.registration_id.write( wiz_registration.get_registration_data()[0]) else: Registration.create( wiz_registration.get_registration_data()[0]) return {'type': 'ir.actions.act_window_close'}
class HtmlForm(models.Model): _name = "html.form" _description = "HTML Form" def _default_return_url(self): return request.httprequest.host_url + "form/thankyou" def _default_submit_url(self): return request.httprequest.host_url + "form/insert" name = fields.Char(string="Form Name", required=True) model_id = fields.Many2one('ir.model', string="Model", required=True) fields_ids = fields.One2many('html.form.field', 'html_id', string="HTML Fields") output_html = fields.Text(string='Embed Code', readonly=True) required_fields = fields.Text(readonly=True, string="Required Fields") defaults_values = fields.One2many( 'html.form.defaults', 'html_id', string="Default Values", help= "Sets the value of an field before it gets inserted into the database") return_url = fields.Char( string="Return URL", default=_default_return_url, help= "The URL that the user will be redirected to after submitting the form", required=True) submit_url = fields.Char(string="Submit URL", default=_default_submit_url) submit_action = fields.One2many('html.form.action', 'hf_id', string="Submit Actions") captcha = fields.Many2one('html.form.captcha', string="Captcha") captcha_client_key = fields.Char(string="Captcha Client Key") captcha_secret_key = fields.Char(string="Captcha Secret Key") @api.onchange('model_id') def _onchange_model_id(self): #delete all existing fields for field_entry in self.fields_ids: field_entry.unlink() required_string = "" for model_field in self.env['ir.model.fields'].search([ ('model_id', '=', self.model_id.id), ('required', '=', True) ]): required_string += model_field.field_description.encode( "utf-8") + " (" + model_field.name.encode("utf-8") + ")\n" self.required_fields = required_string @api.one def generate_form(self): html_output = "" html_output += "<form method=\"POST\" action=\"" + self.submit_url + "\" enctype=\"multipart/form-data\">\n" html_output += " <h1>" + self.name.encode("utf-8") + "</h1>\n" for fe in self.fields_ids: #each field type has it's own function that way we can make plugin modules with new field types method = '_generate_html_%s' % (fe.field_type.html_type, ) action = getattr(self, method, None) if not action: raise NotImplementedError( 'Method %r is not implemented on %r object.' % (method, self)) html_output += action(fe) html_output += " <input type=\"hidden\" name=\"form_id\" value=\"" + str( self.id) + "\"/>\n" html_output += " <input type=\"submit\" value=\"Send\"/>\n" html_output += "</form>\n" self.output_html = html_output def _generate_html_file_select(self, fe): html_output = "" html_output += " <label for='" + fe.html_name.encode( "utf-8") + "'>" + fe.field_label + "</label>\n" html_output += " <input type=\"file\" id=\"" + fe.html_name.encode( "utf-8") + "\" name=\"" + fe.html_name.encode("utf-8") + "\"" if fe.field_id.required == True: html_output += " required" html_output += "/><br/>\n" return html_output def _generate_html_textbox(self, fe): html_output = "" html_output += " <label for='" + fe.html_name.encode( "utf-8") + "'>" + fe.field_label + "</label>\n" html_output += " <input type=\"text\" id=\"" + fe.html_name.encode( "utf-8") + "\" name=\"" + fe.html_name.encode("utf-8") + "\"" if fe.field_id.required == True: html_output += " required" html_output += "/><br/>\n" return html_output def _generate_html_textarea(self, fe): html_output = "" html_output += " <label for='" + fe.html_name.encode( "utf-8") + "'>" + fe.field_label + "</label>" html_output += " <textarea id=\"" + fe.html_name.encode( "utf-8") + "\" name=\"" + fe.html_name.encode("utf-8") + "\"" if fe.field_id.required == True: html_output += " required" html_output += "/><br/>\n" return html_output
class ir_sequence(models.Model): """ Sequence model. The sequence model allows to define and use so-called sequence objects. Such objects are used to generate unique identifiers in a transaction-safe way. """ _name = 'ir.sequence' _order = 'name' def _get_number_next_actual(self): '''Return number from ir_sequence row when no_gap implementation, and number from postgres sequence when standard implementation.''' for element in self: if element.implementation != 'standard': element.number_next_actual = element.number_next else: # get number from postgres sequence. Cannot use currval, because that might give an error when # not having used nextval before. query = "SELECT last_value, increment_by, is_called FROM ir_sequence_%03d" % element.id self.env.cr.execute(query) (last_value, increment_by, is_called) = self.env.cr.fetchone() if is_called: element.number_next_actual = last_value + increment_by else: element.number_next_actual = last_value def _set_number_next_actual(self): for record in self: record.write({'number_next': record.number_next_actual or 0}) name = fields.Char(required=True) code = fields.Char('Sequence Code') implementation = fields.Selection( [('standard', 'Standard'), ('no_gap', 'No gap')], 'Implementation', required=True, default='standard', help="Two sequence object implementations are offered: Standard " "and 'No gap'. The later is slower than the former but forbids any" " gap in the sequence (while they are possible in the former).") active = fields.Boolean(default=True) prefix = fields.Char(help="Prefix value of the record for the sequence") suffix = fields.Char(help="Suffix value of the record for the sequence") number_next = fields.Integer('Next Number', required=True, default=1, help="Next number of this sequence") number_next_actual = fields.Integer( compute='_get_number_next_actual', inverse='_set_number_next_actual', required=True, string='Next Number', default=1, help="Next number that will be used. This number can be incremented " "frequently so the displayed value might already be obsolete") number_increment = fields.Integer( 'Step', required=True, default=1, help= "The next number of the sequence will be incremented by this number") padding = fields.Integer( 'Sequence Size', required=True, default=0, help="eCore will automatically adds some '0' on the left of the " "'Next Number' to get the required padding size.") company_id = fields.Many2one('res.company', 'Company', default=lambda s: s.env['res.company']. _company_default_get('ir.sequence')) use_date_range = fields.Boolean('Use subsequences per date_range') date_range_ids = fields.One2many('ir.sequence.date_range', 'sequence_id', 'Subsequences') def init(self, cr): return # Don't do the following index yet. # CONSTRAINT/UNIQUE INDEX on (code, company_id) # /!\ The unique constraint 'unique_name_company_id' is not sufficient, because SQL92 # only support field names in constraint definitions, and we need a function here: # we need to special-case company_id to treat all NULL company_id as equal, otherwise # we would allow duplicate (code, NULL) ir_sequences. self.env.cr.execute(""" SELECT indexname FROM pg_indexes WHERE indexname = 'ir_sequence_unique_code_company_id_idx'""") if not self.env.cr.fetchone(): self.env.cr.execute(""" CREATE UNIQUE INDEX ir_sequence_unique_code_company_id_idx ON ir_sequence (code, (COALESCE(company_id,-1)))""") @api.model def create(self, values): """ Create a sequence, in implementation == standard a fast gaps-allowed PostgreSQL sequence is used. """ seq = super(ir_sequence, self).create(values) if values.get('implementation', 'standard') == 'standard': _create_sequence(self.env.cr, "ir_sequence_%03d" % seq.id, values.get('number_increment', 1), values.get('number_next', 1)) return seq @api.multi def unlink(self): _drop_sequence(self.env.cr, ["ir_sequence_%03d" % x.id for x in self]) return super(ir_sequence, self).unlink() @api.multi def write(self, values): new_implementation = values.get('implementation') for seq in self: # 4 cases: we test the previous impl. against the new one. i = values.get('number_increment', seq.number_increment) n = values.get('number_next', seq.number_next) if seq.implementation == 'standard': if new_implementation in ('standard', None): # Implementation has NOT changed. # Only change sequence if really requested. if values.get('number_next'): _alter_sequence(self.env.cr, "ir_sequence_%03d" % seq.id, number_next=n) if seq.number_increment != i: _alter_sequence(self.env.cr, "ir_sequence_%03d" % seq.id, number_increment=i) seq.date_range_ids._alter_sequence(number_increment=i) else: _drop_sequence(self.env.cr, ["ir_sequence_%03d" % seq.id]) for sub_seq in seq.date_range_ids: _drop_sequence( self.env.cr, ["ir_sequence_%03d_%03d" % (seq.id, sub_seq.id)]) else: if new_implementation in ('no_gap', None): pass else: _create_sequence(self.env.cr, "ir_sequence_%03d" % seq.id, i, n) for sub_seq in seq.date_range_ids: _create_sequence( self.env.cr, "ir_sequence_%03d_%03d" % (seq.id, sub_seq.id), i, n) return super(ir_sequence, self).write(values) def _next_do(self): if self.implementation == 'standard': number_next = _select_nextval(self.env.cr, 'ir_sequence_%03d' % self.id) else: number_next = _update_nogap(self, self.number_increment) return self.get_next_char(number_next) def get_next_char(self, number_next): def _interpolate(s, d): if s: return s % d return '' def _interpolation_dict(): now = datetime.now( pytz.timezone(self.env.context.get('tz') or 'UTC')) if self.env.context.get('ir_sequence_date'): t = datetime.strptime(self.env.context.get('ir_sequence_date'), '%Y-%m-%d') else: t = now sequences = { 'year': '%Y', 'month': '%m', 'day': '%d', 'y': '%y', 'doy': '%j', 'woy': '%W', 'weekday': '%w', 'h24': '%H', 'h12': '%I', 'min': '%M', 'sec': '%S' } res = {} for key, sequence in sequences.iteritems(): res[key] = now.strftime(sequence) res['range_' + key] = t.strftime(sequence) return res d = _interpolation_dict() try: interpolated_prefix = _interpolate(self.prefix, d) interpolated_suffix = _interpolate(self.suffix, d) except ValueError: raise UserError( _('Invalid prefix or suffix for sequence \'%s\'') % (self.get('name'))) return interpolated_prefix + '%%0%sd' % self.padding % number_next + interpolated_suffix def _create_date_range_seq(self, date): year = fields.Date.from_string(date).strftime('%Y') date_from = '{}-01-01'.format(year) date_to = '{}-12-31'.format(year) date_range = self.env['ir.sequence.date_range'].search( [('sequence_id', '=', self.id), ('date_from', '>=', date), ('date_from', '<=', date_to)], order='date_from desc') if date_range: date_to = datetime.strptime(date_range.date_from, '%Y-%m-%d') + timedelta(days=-1) date_to = date_to.strftime('%Y-%m-%d') date_range = self.env['ir.sequence.date_range'].search( [('sequence_id', '=', self.id), ('date_to', '>=', date_from), ('date_to', '<=', date)], order='date_to desc') if date_range: date_from = datetime.strptime(date_range.date_to, '%Y-%m-%d') + timedelta(days=1) date_from = date_from.strftime('%Y-%m-%d') seq_date_range = self.env['ir.sequence.date_range'].sudo().create({ 'date_from': date_from, 'date_to': date_to, 'sequence_id': self.id, }) return seq_date_range def _next(self): """ Returns the next number in the preferred sequence in all the ones given in self.""" if not self.use_date_range: return self._next_do() # date mode dt = fields.Date.today() if self.env.context.get('ir_sequence_date'): dt = self.env.context.get('ir_sequence_date') seq_date = self.env['ir.sequence.date_range'].search( [('sequence_id', '=', self.id), ('date_from', '<=', dt), ('date_to', '>=', dt)], limit=1) if not seq_date: seq_date = self._create_date_range_seq(dt) return seq_date._next() @api.multi def next_by_id(self): """ Draw an interpolated string using the specified sequence.""" self.check_access_rights('read') return self._next() @api.model def next_by_code(self, sequence_code): """ Draw an interpolated string using a sequence with the requested code. If several sequences with the correct code are available to the user (multi-company cases), the one from the user's current company will be used. :param dict context: context dictionary may contain a ``force_company`` key with the ID of the company to use instead of the user's current company for the sequence selection. A matching sequence for that specific company will get higher priority. """ self.check_access_rights('read') company_ids = self.env['res.company'].search([]).ids + [False] seq_ids = self.search([ '&', ('code', '=', sequence_code), ('company_id', 'in', company_ids) ]) if not seq_ids: return False force_company = self.env.context.get('force_company') if not force_company: force_company = self.env.user.company_id.id preferred_sequences = [ s for s in seq_ids if s.company_id and s.company_id.id == force_company ] seq_id = preferred_sequences[0] if preferred_sequences else seq_ids[0] return seq_id._next() @api.model def get_id(self, sequence_code_or_id, code_or_id='id'): """ Draw an interpolated string using the specified sequence. The sequence to use is specified by the ``sequence_code_or_id`` argument, which can be a code or an id (as controlled by the ``code_or_id`` argument. This method is deprecated. """ _logger.warning( "ir_sequence.get() and ir_sequence.get_id() are deprecated. " "Please use ir_sequence.next_by_code() or ir_sequence.next_by_id()." ) if code_or_id == 'id': return self.browse(sequence_code_or_id).next_by_id() else: return self.next_by_code(sequence_code_or_id) @api.model def get(self, code): """ Draw an interpolated string using the specified sequence. The sequence to use is specified by its code. This method is deprecated. """ return self.get_id(code, 'code')
class event_ticket(models.Model): _name = 'event.event.ticket' _description = 'Event Ticket' name = fields.Char('Name', required=True, translate=True) event_id = fields.Many2one('event.event', "Event", required=True, ondelete='cascade') product_id = fields.Many2one( 'product.product', 'Product', required=True, domain=[("event_type_id", "!=", False)], default=lambda self: self._default_product_id()) registration_ids = fields.One2many('event.registration', 'event_ticket_id', 'Registrations') price = fields.Float('Price', digits=dp.get_precision('Product Price')) deadline = fields.Date("Sales End") is_expired = fields.Boolean('Is Expired', compute='_is_expired') @api.model def _default_product_id(self): try: product = self.env['ir.model.data'].get_object( 'event_sale', 'product_product_event') return product.id except ValueError: return False @api.one @api.depends('deadline') def _is_expired(self): if self.deadline: current_date = fields.Date.context_today( self.with_context({'tz': self.event_id.date_tz})) self.is_expired = self.deadline < current_date else: self.is_expired = False # FIXME non-stored fields wont ends up in _columns (and thus _all_columns), which forbid them # to be used in qweb views. Waiting a fix, we create an old function field directly. """ price_reduce = fields.Float("Price Reduce", compute="_get_price_reduce", store=False, digits=dp.get_precision('Product Price')) @api.one @api.depends('price', 'product_id.lst_price', 'product_id.price') def _get_price_reduce(self): product = self.product_id discount = product.lst_price and (product.lst_price - product.price) / product.lst_price or 0.0 self.price_reduce = (1.0 - discount) * self.price """ def _get_price_reduce(self, cr, uid, ids, field_name, arg, context=None): res = dict.fromkeys(ids, 0.0) for ticket in self.browse(cr, uid, ids, context=context): product = ticket.product_id discount = product.lst_price and ( product.lst_price - product.price) / product.lst_price or 0.0 res[ticket.id] = (1.0 - discount) * ticket.price return res _columns = { 'price_reduce': old_fields.function(_get_price_reduce, type='float', string='Price Reduce', digits_compute=dp.get_precision('Product Price')), } # seats fields seats_availability = fields.Selection([('limited', 'Limited'), ('unlimited', 'Unlimited')], 'Available Seat', required=True, store=True, compute='_compute_seats', default="limited") seats_max = fields.Integer( 'Maximum Available Seats', help= "Define the number of available tickets. If you have too much registrations you will " "not be able to sell tickets anymore. Set 0 to ignore this rule set as unlimited." ) seats_reserved = fields.Integer(string='Reserved Seats', compute='_compute_seats', store=True) seats_available = fields.Integer(string='Available Seats', compute='_compute_seats', store=True) seats_unconfirmed = fields.Integer(string='Unconfirmed Seat Reservations', compute='_compute_seats', store=True) seats_used = fields.Integer(compute='_compute_seats', store=True) @api.multi @api.depends('seats_max', 'registration_ids.state') def _compute_seats(self): """ Determine reserved, available, reserved but unconfirmed and used seats. """ # initialize fields to 0 + compute seats availability for ticket in self: ticket.seats_availability = 'unlimited' if ticket.seats_max == 0 else 'limited' ticket.seats_unconfirmed = ticket.seats_reserved = ticket.seats_used = ticket.seats_available = 0 # aggregate registrations by ticket and by state if self.ids: state_field = { 'draft': 'seats_unconfirmed', 'open': 'seats_reserved', 'done': 'seats_used', } query = """ SELECT event_ticket_id, state, count(event_id) FROM event_registration WHERE event_ticket_id IN %s AND state IN ('draft', 'open', 'done') GROUP BY event_ticket_id, state """ self._cr.execute(query, (tuple(self.ids), )) for event_ticket_id, state, num in self._cr.fetchall(): ticket = self.browse(event_ticket_id) ticket[state_field[state]] += num # compute seats_available for ticket in self: if ticket.seats_max > 0: ticket.seats_available = ticket.seats_max - ( ticket.seats_reserved + ticket.seats_used) @api.one @api.constrains('registration_ids', 'seats_max') def _check_seats_limit(self): if self.seats_max and self.seats_available < 0: raise UserError(_('No more available seats for the ticket')) @api.onchange('product_id') def onchange_product_id(self): price = self.product_id.list_price if self.product_id else 0 return {'value': {'price': price}}
class res_partner(models.Model): _inherit = 'res.partner' # define a one2many field based on the inherited field partner_id daughter_ids = fields.One2many('test.inherit.daughter', 'partner_id')
class Post(models.Model): _name = 'forum.post' _description = 'Forum Post' _inherit = ['mail.thread', 'website.seo.metadata'] _order = "is_correct DESC, vote_count DESC, write_date DESC" name = fields.Char('Title') forum_id = fields.Many2one('forum.forum', string='Forum', required=True) content = fields.Html('Content', strip_style=True) plain_content = fields.Text('Plain Content', compute='_get_plain_content', store=True) content_link = fields.Char('URL', help="URL of Link Articles") tag_ids = fields.Many2many('forum.tag', 'forum_tag_rel', 'forum_id', 'forum_tag_id', string='Tags') state = fields.Selection([('active', 'Active'), ('pending', 'Waiting Validation'), ('close', 'Close'), ('offensive', 'Offensive'), ('flagged', 'Flagged')], string='Status', default='active') views = fields.Integer('Number of Views', default=0) active = fields.Boolean('Active', default=True) post_type = fields.Selection([('question', 'Question'), ('link', 'Article'), ('discussion', 'Discussion')], string='Type', default='question', required=True) website_message_ids = fields.One2many( 'mail.message', 'res_id', domain=lambda self: [ '&', ('model', '=', self._name), ('message_type', 'in', ['email', 'comment']) ], string='Post Messages', help="Comments on forum post", ) # history create_date = fields.Datetime('Asked on', select=True, readonly=True) create_uid = fields.Many2one('res.users', string='Created by', select=True, readonly=True) write_date = fields.Datetime('Update on', select=True, readonly=True) bump_date = fields.Datetime( 'Bumped on', readonly=True, help= "Technical field allowing to bump a question. Writing on this field will trigger " "a write on write_date and therefore bump the post. Directly writing on write_date " "is currently not supported and this field is a workaround.") write_uid = fields.Many2one('res.users', string='Updated by', select=True, readonly=True) relevancy = fields.Float('Relevance', compute="_compute_relevancy", store=True) # vote vote_ids = fields.One2many('forum.post.vote', 'post_id', string='Votes') user_vote = fields.Integer('My Vote', compute='_get_user_vote') vote_count = fields.Integer('Votes', compute='_get_vote_count', store=True) # favorite favourite_ids = fields.Many2many('res.users', string='Favourite') user_favourite = fields.Boolean('Is Favourite', compute='_get_user_favourite') favourite_count = fields.Integer('Favorite Count', compute='_get_favorite_count', store=True) # hierarchy is_correct = fields.Boolean('Correct', help='Correct answer or answer accepted') parent_id = fields.Many2one('forum.post', string='Question', ondelete='cascade') self_reply = fields.Boolean('Reply to own question', compute='_is_self_reply', store=True) child_ids = fields.One2many('forum.post', 'parent_id', string='Answers') child_count = fields.Integer('Number of answers', compute='_get_child_count', store=True) uid_has_answered = fields.Boolean('Has Answered', compute='_get_uid_has_answered') has_validated_answer = fields.Boolean('Is answered', compute='_get_has_validated_answer', store=True) # offensive moderation tools flag_user_id = fields.Many2one('res.users', string='Flagged by') moderator_id = fields.Many2one('res.users', string='Reviewed by', readonly=True) # closing closed_reason_id = fields.Many2one('forum.post.reason', string='Reason') closed_uid = fields.Many2one('res.users', string='Closed by', select=1) closed_date = fields.Datetime('Closed on', readonly=True) # karma calculation and access karma_accept = fields.Integer('Convert comment to answer', compute='_get_post_karma_rights') karma_edit = fields.Integer('Karma to edit', compute='_get_post_karma_rights') karma_close = fields.Integer('Karma to close', compute='_get_post_karma_rights') karma_unlink = fields.Integer('Karma to unlink', compute='_get_post_karma_rights') karma_comment = fields.Integer('Karma to comment', compute='_get_post_karma_rights') karma_comment_convert = fields.Integer( 'Karma to convert comment to answer', compute='_get_post_karma_rights') karma_flag = fields.Integer('Flag a post as offensive', compute='_get_post_karma_rights') can_ask = fields.Boolean('Can Ask', compute='_get_post_karma_rights') can_answer = fields.Boolean('Can Answer', compute='_get_post_karma_rights') can_accept = fields.Boolean('Can Accept', compute='_get_post_karma_rights') can_edit = fields.Boolean('Can Edit', compute='_get_post_karma_rights') can_close = fields.Boolean('Can Close', compute='_get_post_karma_rights') can_unlink = fields.Boolean('Can Unlink', compute='_get_post_karma_rights') can_upvote = fields.Boolean('Can Upvote', compute='_get_post_karma_rights') can_downvote = fields.Boolean('Can Downvote', compute='_get_post_karma_rights') can_comment = fields.Boolean('Can Comment', compute='_get_post_karma_rights') can_comment_convert = fields.Boolean('Can Convert to Comment', compute='_get_post_karma_rights') can_view = fields.Boolean('Can View', compute='_get_post_karma_rights') can_display_biography = fields.Boolean( "Is the author's biography visible from his post", compute='_get_post_karma_rights') can_post = fields.Boolean('Can Automatically be Validated', compute='_get_post_karma_rights') can_flag = fields.Boolean('Can Flag', compute='_get_post_karma_rights') can_moderate = fields.Boolean('Can Moderate', compute='_get_post_karma_rights') @api.one @api.depends('content') def _get_plain_content(self): self.plain_content = tools.html2plaintext( self.content)[0:500] if self.content else False @api.one @api.depends('vote_count', 'forum_id.relevancy_post_vote', 'forum_id.relevancy_time_decay') def _compute_relevancy(self): if self.create_date: days = (datetime.today() - datetime.strptime( self.create_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)).days self.relevancy = math.copysign(1, self.vote_count) * ( abs(self.vote_count - 1)**self.forum_id.relevancy_post_vote / (days + 2)**self.forum_id.relevancy_time_decay) else: self.relevancy = 0 @api.multi def _get_user_vote(self): votes = self.env['forum.post.vote'].search_read( [('post_id', 'in', self._ids), ('user_id', '=', self._uid)], ['vote', 'post_id']) mapped_vote = dict([(v['post_id'][0], v['vote']) for v in votes]) for vote in self: vote.user_vote = mapped_vote.get(vote.id, 0) @api.multi @api.depends('vote_ids.vote') def _get_vote_count(self): read_group_res = self.env['forum.post.vote'].read_group( [('post_id', 'in', self._ids)], ['post_id', 'vote'], ['post_id', 'vote'], lazy=False) result = dict.fromkeys(self._ids, 0) for data in read_group_res: result[data['post_id'][0]] += data['__count'] * int(data['vote']) for post in self: post.vote_count = result[post.id] @api.one def _get_user_favourite(self): self.user_favourite = self._uid in self.favourite_ids.ids @api.one @api.depends('favourite_ids') def _get_favorite_count(self): self.favourite_count = len(self.favourite_ids) @api.one @api.depends('create_uid', 'parent_id') def _is_self_reply(self): self.self_reply = self.parent_id.create_uid.id == self._uid @api.one @api.depends('child_ids.create_uid', 'website_message_ids') def _get_child_count(self): def process(node): total = len(node.website_message_ids) + len(node.child_ids) for child in node.child_ids: total += process(child) return total self.child_count = process(self) @api.one def _get_uid_has_answered(self): self.uid_has_answered = any(answer.create_uid.id == self._uid for answer in self.child_ids) @api.one @api.depends('child_ids.is_correct') def _get_has_validated_answer(self): self.has_validated_answer = any(answer.is_correct for answer in self.child_ids) @api.multi def _get_post_karma_rights(self): user = self.env.user is_admin = user.id == SUPERUSER_ID # sudoed recordset instead of individual posts so values can be # prefetched in bulk for post, post_sudo in itertools.izip(self, self.sudo()): is_creator = post.create_uid == user post.karma_accept = post.forum_id.karma_answer_accept_own if post.parent_id.create_uid == user else post.forum_id.karma_answer_accept_all post.karma_edit = post.forum_id.karma_edit_own if is_creator else post.forum_id.karma_edit_all post.karma_close = post.forum_id.karma_close_own if is_creator else post.forum_id.karma_close_all post.karma_unlink = post.forum_id.karma_unlink_own if is_creator else post.forum_id.karma_unlink_all post.karma_comment = post.forum_id.karma_comment_own if is_creator else post.forum_id.karma_comment_all post.karma_comment_convert = post.forum_id.karma_comment_convert_own if is_creator else post.forum_id.karma_comment_convert_all post.can_ask = is_admin or user.karma >= post.forum_id.karma_ask post.can_answer = is_admin or user.karma >= post.forum_id.karma_answer post.can_accept = is_admin or user.karma >= post.karma_accept post.can_edit = is_admin or user.karma >= post.karma_edit post.can_close = is_admin or user.karma >= post.karma_close post.can_unlink = is_admin or user.karma >= post.karma_unlink post.can_upvote = is_admin or user.karma >= post.forum_id.karma_upvote post.can_downvote = is_admin or user.karma >= post.forum_id.karma_downvote post.can_comment = is_admin or user.karma >= post.karma_comment post.can_comment_convert = is_admin or user.karma >= post.karma_comment_convert post.can_view = is_admin or user.karma >= post.karma_close or post_sudo.create_uid.karma > 0 post.can_display_biography = is_admin or post_sudo.create_uid.karma >= post.forum_id.karma_user_bio post.can_post = is_admin or user.karma >= post.forum_id.karma_post post.can_flag = is_admin or user.karma >= post.forum_id.karma_flag post.can_moderate = is_admin or user.karma >= post.forum_id.karma_moderate @api.one @api.constrains('post_type', 'forum_id') def _check_post_type(self): if (self.post_type == 'question' and not self.forum_id.allow_question) \ or (self.post_type == 'discussion' and not self.forum_id.allow_discussion) \ or (self.post_type == 'link' and not self.forum_id.allow_link): raise UserError(_('This forum does not allow %s' % self.post_type)) def _update_content(self, content, forum_id): forum = self.env['forum.forum'].browse(forum_id) if content and self.env.user.karma < forum.karma_dofollow: for match in re.findall(r'<a\s.*href=".*?">', content): content = re.sub(match, match[:3] + 'rel="nofollow" ' + match[3:], content) if self.env.user.karma <= forum.karma_editor: filter_regexp = r'(<img.*?>)|(<a[^>]*?href[^>]*?>)|(<[a-z|A-Z]+[^>]*style\s*=\s*[\'"][^\'"]*\s*background[^:]*:[^url;]*url)' content_match = re.search(filter_regexp, content, re.I) if content_match: raise KarmaError( 'User karma not sufficient to post an image or link.') return content @api.model def create(self, vals): if 'content' in vals and vals.get('forum_id'): vals['content'] = self._update_content(vals['content'], vals['forum_id']) post = super(Post, self.with_context(mail_create_nolog=True)).create(vals) # deleted or closed questions if post.parent_id and (post.parent_id.state == 'close' or post.parent_id.active is False): raise UserError( _('Posting answer on a [Deleted] or [Closed] question is not possible' )) # karma-based access if not post.parent_id and not post.can_ask: raise KarmaError('Not enough karma to create a new question') elif post.parent_id and not post.can_answer: raise KarmaError('Not enough karma to answer to a question') if not post.parent_id and not post.can_post: post.state = 'pending' # add karma for posting new questions if not post.parent_id and post.state == 'active': self.env.user.sudo().add_karma( post.forum_id.karma_gen_question_new) post.post_notification() return post @api.model def check_mail_message_access(self, res_ids, operation, model_name=None): if operation in ('write', 'unlink') and (not model_name or model_name == 'forum.post'): # Make sure only author or moderator can edit/delete messages if any(not post.can_edit for post in self.browse(res_ids)): raise KarmaError('Not enough karma to edit a post.') return super(Post, self).check_mail_message_access(res_ids, operation, model_name=model_name) @api.multi @api.depends('name', 'post_type') def name_get(self): result = [] for post in self: if post.post_type == 'discussion' and post.parent_id and not post.name: result.append( (post.id, '%s (%s)' % (post.parent_id.name, post.id))) else: result.append((post.id, '%s' % (post.name))) return result @api.multi def write(self, vals): if 'content' in vals: vals['content'] = self._update_content(vals['content'], self.forum_id.id) if 'state' in vals: if vals['state'] in ['active', 'close'] and any(not post.can_close for post in self): raise KarmaError('Not enough karma to close or reopen a post.') if 'active' in vals: if any(not post.can_unlink for post in self): raise KarmaError( 'Not enough karma to delete or reactivate a post') if 'is_correct' in vals: if any(not post.can_accept for post in self): raise KarmaError( 'Not enough karma to accept or refuse an answer') # update karma except for self-acceptance mult = 1 if vals['is_correct'] else -1 for post in self: if vals['is_correct'] != post.is_correct and post.create_uid.id != self._uid: post.create_uid.sudo().add_karma( post.forum_id.karma_gen_answer_accepted * mult) self.env.user.sudo().add_karma( post.forum_id.karma_gen_answer_accept * mult) if any(key not in [ 'state', 'active', 'is_correct', 'closed_uid', 'closed_date', 'closed_reason_id' ] for key in vals.keys()) and any(not post.can_edit for post in self): raise KarmaError('Not enough karma to edit a post.') res = super(Post, self).write(vals) # if post content modify, notify followers if 'content' in vals or 'name' in vals: for post in self: if post.parent_id: body, subtype = _( 'Answer Edited'), 'website_forum.mt_answer_edit' obj_id = post.parent_id else: body, subtype = _( 'Question Edited'), 'website_forum.mt_question_edit' obj_id = post obj_id.message_post(body=body, subtype=subtype) return res @api.multi def post_notification(self): base_url = self.env['ir.config_parameter'].get_param('web.base.url') for post in self: if post.state == 'active' and post.parent_id: body = _( '<p>A new answer for <i>%s</i> has been posted. <a href="%s/forum/%s/question/%s">Click here to access the post.</a></p>' % (post.parent_id.name, base_url, slug(post.parent_id.forum_id), slug(post.parent_id))) post.parent_id.message_post( subject=_('Re: %s') % post.parent_id.name, body=body, subtype='website_forum.mt_answer_new') elif post.state == 'active' and not post.parent_id: body = _( '<p>A new question <i>%s</i> has been asked on %s. <a href="%s/forum/%s/question/%s">Click here to access the question.</a></p>' % (post.name, post.forum_id.name, base_url, slug(post.forum_id), slug(post))) post.message_post(subject=post.name, body=body, subtype='website_forum.mt_question_new') elif post.state == 'pending' and not post.parent_id: # TDE FIXME: in master, you should probably use a subtype; # however here we remove subtype but set partner_ids partners = post.sudo().message_partner_ids.filtered( lambda partner: partner.user_ids and partner.user_ids.karma >= post.forum_id.karma_moderate) note_subtype = self.sudo().env.ref('mail.mt_note') body = _( '<p>A new question <i>%s</i> has been asked on %s and require your validation. <a href="%s/forum/%s/question/%s">Click here to access the question.</a></p>' % (post.name, post.forum_id.name, base_url, slug(post.forum_id), slug(post))) post.message_post(subject=post.name, body=body, subtype_id=note_subtype.id, partner_ids=partners.ids) return True @api.multi def reopen(self): if any(post.parent_id or post.state != 'close' for post in self): return False reason_offensive = self.env.ref('website_forum.reason_7') reason_spam = self.env.ref('website_forum.reason_8') for post in self: if post.closed_reason_id in (reason_offensive, reason_spam): _logger.info( 'Upvoting user <%s>, reopening spam/offensive question', post.create_uid) post.create_uid.sudo().add_karma( post.forum_id.karma_gen_answer_flagged * -1) self.sudo().write({'state': 'active'}) @api.multi def close(self, reason_id): if any(post.parent_id for post in self): return False reason_offensive = self.env.ref('website_forum.reason_7').id reason_spam = self.env.ref('website_forum.reason_8').id if reason_id in (reason_offensive, reason_spam): for post in self: _logger.info( 'Downvoting user <%s> for posting spam/offensive contents', post.create_uid) post.create_uid.sudo().add_karma( post.forum_id.karma_gen_answer_flagged) self.write({ 'state': 'close', 'closed_uid': self._uid, 'closed_date': datetime.today().strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT), 'closed_reason_id': reason_id, }) return True @api.one def validate(self): if not self.can_moderate: raise KarmaError('Not enough karma to validate a post') # if state == pending, no karma previously added for the new question if self.state == 'pending': self.create_uid.sudo().add_karma( self.forum_id.karma_gen_question_new) self.write({ 'state': 'active', 'active': True, 'moderator_id': self.env.user.id, }) self.post_notification() return True @api.one def refuse(self): if not self.can_moderate: raise KarmaError('Not enough karma to refuse a post') self.moderator_id = self.env.user return True @api.one def flag(self): if not self.can_flag: raise KarmaError('Not enough karma to flag a post') if (self.state == 'flagged'): return {'error': 'post_already_flagged'} elif (self.state == 'active'): self.write({ 'state': 'flagged', 'flag_user_id': self.env.user.id, }) return self.can_moderate and { 'success': 'post_flagged_moderator' } or { 'success': 'post_flagged_non_moderator' } else: return {'error': 'post_non_flaggable'} @api.one def mark_as_offensive(self, reason_id): if not self.can_moderate: raise KarmaError('Not enough karma to mark a post as offensive') # remove some karma _logger.info( 'Downvoting user <%s> for posting spam/offensive contents', self.create_uid) self.create_uid.sudo().add_karma( self.forum_id.karma_gen_answer_flagged) self.write({ 'state': 'offensive', 'moderator_id': self.env.user.id, 'closed_date': datetime.today().strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT), 'closed_reason_id': reason_id, 'active': False, }) return True @api.multi def unlink(self): if any(not post.can_unlink for post in self): raise KarmaError('Not enough karma to unlink a post') # if unlinking an answer with accepted answer: remove provided karma for post in self: if post.is_correct: post.create_uid.sudo().add_karma( post.forum_id.karma_gen_answer_accepted * -1) self.env.user.sudo().add_karma( post.forum_id.karma_gen_answer_accepted * -1) return super(Post, self).unlink() @api.multi def bump(self): """ Bump a question: trigger a write_date by writing on a dummy bump_date field. One cannot bump a question more than once every 10 days. """ self.ensure_one() if self.forum_id.allow_bump and not self.child_ids and (datetime.today( ) - datetime.strptime(self.write_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)).days > 9: # write through super to bypass karma; sudo to allow public user to bump any post return self.sudo().write({'bump_date': fields.Datetime.now()}) return False @api.multi def vote(self, upvote=True): Vote = self.env['forum.post.vote'] vote_ids = Vote.search([('post_id', 'in', self._ids), ('user_id', '=', self._uid)]) new_vote = '1' if upvote else '-1' voted_forum_ids = set() if vote_ids: for vote in vote_ids: if upvote: new_vote = '0' if vote.vote == '-1' else '1' else: new_vote = '0' if vote.vote == '1' else '-1' vote.vote = new_vote voted_forum_ids.add(vote.post_id.id) for post_id in set(self._ids) - voted_forum_ids: for post_id in self._ids: Vote.create({'post_id': post_id, 'vote': new_vote}) return {'vote_count': self.vote_count, 'user_vote': new_vote} @api.one def convert_answer_to_comment(self): """ Tools to convert an answer (forum.post) to a comment (mail.message). The original post is unlinked and a new comment is posted on the question using the post create_uid as the comment's author. """ if not self.parent_id: return False # karma-based action check: use the post field that computed own/all value if not self.can_comment_convert: raise KarmaError( 'Not enough karma to convert an answer to a comment') # post the message question = self.parent_id values = { 'author_id': self.sudo().create_uid.partner_id. id, # use sudo here because of access to res.users model 'body': tools.html_sanitize(self.content, strict=True, strip_style=True, strip_classes=True), 'message_type': 'comment', 'subtype': 'mail.mt_comment', 'date': self.create_date, } new_message = self.browse(question.id).with_context( mail_create_nosubscribe=True).message_post(**values) # unlink the original answer, using SUPERUSER_ID to avoid karma issues self.sudo().unlink() return new_message @api.model def convert_comment_to_answer(self, message_id, default=None): """ Tool to convert a comment (mail.message) into an answer (forum.post). The original comment is unlinked and a new answer from the comment's author is created. Nothing is done if the comment's author already answered the question. """ comment = self.env['mail.message'].sudo().browse(message_id) post = self.browse(comment.res_id) if not comment.author_id or not comment.author_id.user_ids: # only comment posted by users can be converted return False # karma-based action check: must check the message's author to know if own / all karma_convert = comment.author_id.id == self.env.user.partner_id.id and post.forum_id.karma_comment_convert_own or post.forum_id.karma_comment_convert_all can_convert = self.env.user.karma >= karma_convert if not can_convert: raise KarmaError( 'Not enough karma to convert a comment to an answer') # check the message's author has not already an answer question = post.parent_id if post.parent_id else post post_create_uid = comment.author_id.user_ids[0] if any(answer.create_uid.id == post_create_uid.id for answer in question.child_ids): return False # create the new post post_values = { 'forum_id': question.forum_id.id, 'content': comment.body, 'parent_id': question.id, } # done with the author user to have create_uid correctly set new_post = self.sudo(post_create_uid.id).create(post_values) # delete comment comment.unlink() return new_post @api.one def unlink_comment(self, message_id): user = self.env.user comment = self.env['mail.message'].sudo().browse(message_id) if not comment.model == 'forum.post' or not comment.res_id == self.id: return False # karma-based action check: must check the message's author to know if own or all karma_unlink = comment.author_id.id == user.partner_id.id and self.forum_id.karma_comment_unlink_own or self.forum_id.karma_comment_unlink_all can_unlink = user.karma >= karma_unlink if not can_unlink: raise KarmaError('Not enough karma to unlink a comment') return comment.unlink() @api.multi def set_viewed(self): self._cr.execute( """UPDATE forum_post SET views = views+1 WHERE id IN %s""", (self._ids, )) return True @api.multi def get_access_action(self): """ Override method that generated the link to access the document. Instead of the classic form view, redirect to the post on the website directly """ self.ensure_one() return { 'type': 'ir.actions.act_url', 'url': '/forum/%s/question/%s' % (self.forum_id.id, self.id), 'target': 'self', 'res_id': self.id, } @api.multi def _notification_get_recipient_groups(self, message, recipients): """ Override to set the access button: everyone can see an access button on their notification email. It will lead on the website view of the post. """ res = super(Post, self)._notification_get_recipient_groups( message, recipients) access_action = self._notification_link_helper('view', model=message.model, res_id=message.res_id) for category, data in res.iteritems(): res[category]['button_access'] = { 'url': access_action, 'title': '%s %s' % (_('View'), self.post_type) } return res @api.cr_uid_ids_context def message_post(self, cr, uid, thread_id, message_type='notification', subtype=None, context=None, **kwargs): if thread_id and message_type == 'comment': # user comments have a restriction on karma if isinstance(thread_id, (list, tuple)): post_id = thread_id[0] else: post_id = thread_id post = self.browse(cr, uid, post_id, context=context) # TDE FIXME: trigger browse because otherwise the function field is not compted - check with RCO tmp1, tmp2 = post.karma_comment, post.can_comment user = self.pool['res.users'].browse(cr, uid, uid) tmp3 = user.karma # TDE END FIXME if not post.can_comment: raise KarmaError('Not enough karma to comment') return super(Post, self).message_post(cr, uid, thread_id, message_type=message_type, subtype=subtype, context=context, **kwargs)
class event(models.Model): _name = 'event.event' _inherit = ['event.event', 'website.seo.metadata', 'website.published.mixin'] twitter_hashtag = fields.Char('Twitter Hashtag', default=lambda self: self._default_hashtag()) website_published = fields.Boolean(track_visibility='onchange') # TDE TODO FIXME: when website_mail/mail_thread.py inheritance work -> this field won't be necessary website_message_ids = fields.One2many( 'mail.message', 'res_id', domain=lambda self: [ '&', ('model', '=', self._name), ('message_type', '=', 'comment') ], string='Website Messages', help="Website communication history", ) @api.multi @api.depends('name') def _website_url(self, name, arg): res = super(event, self)._website_url(name, arg) res.update({(e.id, '/event/%s' % slug(e)) for e in self}) return res def _default_hashtag(self): return re.sub("[- \\.\\(\\)\\@\\#\\&]+", "", self.env.user.company_id.name).lower() show_menu = fields.Boolean('Dedicated Menu', compute='_get_show_menu', inverse='_set_show_menu', help="Creates menus Introduction, Location and Register on the page " " of the event on the website.", store=True) menu_id = fields.Many2one('website.menu', 'Event Menu') @api.one def _get_new_menu_pages(self): todo = [ (_('Introduction'), 'website_event.template_intro'), (_('Location'), 'website_event.template_location') ] result = [] for name, path in todo: complete_name = name + ' ' + self.name newpath = self.env['website'].new_page(complete_name, path, ispage=False) url = "/event/" + slug(self) + "/page/" + newpath result.append((name, url)) result.append((_('Register'), '/event/%s/register' % slug(self))) return result @api.one def _set_show_menu(self): if self.menu_id and not self.show_menu: self.menu_id.unlink() elif self.show_menu and not self.menu_id: root_menu = self.env['website.menu'].create({'name': self.name}) to_create_menus = self._get_new_menu_pages()[0] # TDE CHECK api.one -> returns a list with one item ? seq = 0 for name, url in to_create_menus: self.env['website.menu'].create({ 'name': name, 'url': url, 'parent_id': root_menu.id, 'sequence': seq, }) seq += 1 self.menu_id = root_menu @api.one def _get_show_menu(self): self.show_menu = bool(self.menu_id) def google_map_img(self, cr, uid, ids, zoom=8, width=298, height=298, context=None): event = self.browse(cr, uid, ids[0], context=context) if event.address_id: return self.browse(cr, SUPERUSER_ID, ids[0], context=context).address_id.google_map_img() return None def google_map_link(self, cr, uid, ids, zoom=8, context=None): event = self.browse(cr, uid, ids[0], context=context) if event.address_id: return self.browse(cr, SUPERUSER_ID, ids[0], context=context).address_id.google_map_link() return None @api.multi def _track_subtype(self, init_values): self.ensure_one() if 'website_published' in init_values and self.website_published: return 'website_event.mt_event_published' elif 'website_published' in init_values and not self.website_published: return 'website_event.mt_event_unpublished' return super(event, self)._track_subtype(init_values) @api.multi def action_open_badge_editor(self): """ open the event badge editor : redirect to the report page of event badge report """ self.ensure_one() return { 'type': 'ir.actions.act_url', 'target': 'new', 'url': '/report/html/%s/%s?enable_editor' % ('event.event_event_report_template_badge', self.id), }