class BusPresence(models.Model): """ User Presence Its status is 'online', 'away' or 'offline'. This model should be a one2one, but is not attached to res_users to avoid database concurrence errors. Since the 'update' method is executed at each poll, if the user have multiple opened tabs, concurrence errors can happend, but are 'muted-logged'. """ _name = 'bus.presence' _description = 'User Presence' _log_access = False _sql_constraints = [('bus_user_presence_unique', 'unique(user_id)', 'A user can only have one IM status.')] user_id = fields.Many2one('res.users', 'Users', required=True, index=True, ondelete='cascade') last_poll = fields.Datetime('Last Poll', default=lambda self: fields.Datetime.now()) last_presence = fields.Datetime('Last Presence', default=lambda self: fields.Datetime.now()) status = fields.Selection([('online', 'Online'), ('away', 'Away'), ('offline', 'Offline')], 'IM Status', default='offline') @api.model def update(self, inactivity_period): """ Updates the last_poll and last_presence of the current user :param inactivity_period: duration in milliseconds """ presence = self.search([('user_id', '=', self._uid)], limit=1) # compute last_presence timestamp last_presence = datetime.datetime.now() - datetime.timedelta( milliseconds=inactivity_period) values = { 'last_poll': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT), } # update the presence or a create a new one if not presence: # create a new presence for the user values['user_id'] = self._uid values['last_presence'] = last_presence.strftime( DEFAULT_SERVER_DATETIME_FORMAT) self.create(values) else: # update the last_presence if necessary, and write values if datetime.datetime.strptime( presence.last_presence, DEFAULT_SERVER_DATETIME_FORMAT) < last_presence: values['last_presence'] = last_presence.strftime( DEFAULT_SERVER_DATETIME_FORMAT) # Hide transaction serialization errors, which can be ignored, the presence update is not essential with tools.mute_logger('ecore.sql_db'): presence.write(values) # avoid TransactionRollbackError self.env.cr.commit() # TODO : check if still necessary
class EventMailRegistration(models.Model): _name = 'event.mail.registration' _description = 'Registration Mail Scheduler' _rec_name = 'scheduler_id' _order = 'scheduled_date DESC' scheduler_id = fields.Many2one('event.mail', 'Mail Scheduler', required=True, ondelete='cascade') registration_id = fields.Many2one('event.registration', 'Attendee', required=True, ondelete='cascade') scheduled_date = fields.Datetime('Scheduled Time', compute='_compute_scheduled_date', store=True) mail_sent = fields.Boolean('Mail Sent') @api.one def execute(self): if self.registration_id.state in ['open', 'done'] and not self.mail_sent: self.scheduler_id.template_id.send_mail(self.registration_id.id) self.write({'mail_sent': True}) @api.one @api.depends('registration_id', 'scheduler_id.interval_unit', 'scheduler_id.interval_type') def _compute_scheduled_date(self): if self.registration_id: date_open = self.registration_id.date_open date_open_datetime = date_open and datetime.strptime(date_open, tools.DEFAULT_SERVER_DATETIME_FORMAT) or fields.datetime.now() self.scheduled_date = date_open_datetime + _INTERVALS[self.scheduler_id.interval_unit](self.scheduler_id.interval_nbr) else: self.scheduled_date = False
class DocumentPageHistory(models.Model): """This model is necessary to manage a document history.""" _name = "document.page.history" _description = "Document Page History" _order = 'id DESC' _rec_name = "create_date" page_id = fields.Many2one('document.page', 'Page') summary = fields.Char('Summary', select=True) content = fields.Text("Content") create_date = fields.Datetime("Date") create_uid = fields.Many2one('res.users', "Modified By") def getDiff(self, v1, v2): """Return the difference between two version of document version.""" text1 = self.browse(v1).content text2 = self.browse(v2).content line1 = line2 = '' if text1: line1 = text1.splitlines(1) if text2: line2 = text2.splitlines(1) if (not line1 and not line2) or (line1 == line2): return _('There are no changes in revisions.') else: diff = difflib.HtmlDiff() return diff.make_table( line1, line2, "Revision-{}".format(v1), "Revision-{}".format(v2), context=True )
class SaasClient(models.AbstractModel): _name = 'saas_base.client' users_len = fields.Integer('Count users', readonly=True) max_users = fields.Char('Max users allowed', readonly=True) file_storage = fields.Integer('File storage (MB)', readonly=True) db_storage = fields.Integer('DB storage (MB)', readonly=True) total_storage_limit = fields.Integer('Total storage limit (MB)', readonly=True, default=0) expiration_datetime = fields.Datetime('Expiration', track_visibility='onchange') trial = fields.Boolean('Trial', help='indication of trial clients', default=False, readonly=True)
class ImLivechatReportOperator(models.Model): """ Livechat Support Report on the Operator """ _name = "im_livechat.report.operator" _description = "Livechat Support Report" _order = 'livechat_channel_id, partner_id' _auto = False partner_id = fields.Many2one('res.partner', 'Operator', readonly=True) livechat_channel_id = fields.Many2one('im_livechat.channel', 'Channel', readonly=True) nbr_channel = fields.Integer('# of channel', readonly=True, group_operator="sum", help="Number of conversation") channel_id = fields.Many2one('mail.channel', 'Conversation', readonly=True) start_date = fields.Datetime('Start Date of session', readonly=True, help="Start date of the conversation") time_to_answer = fields.Float( 'Time to answer', digits=(16, 2), readonly=True, group_operator="avg", help="Average time to give the first answer to the visitor") duration = fields.Float('Average duration', digits=(16, 2), readonly=True, group_operator="avg", help="Duration of the conversation (in seconds)") def init(self, cr): # Note : start_date_hour must be remove when the read_group will allow grouping on the hour of a datetime. Don't forget to change the view ! tools.drop_view_if_exists(cr, 'im_livechat_report_operator') cr.execute(""" CREATE OR REPLACE VIEW im_livechat_report_operator AS ( SELECT row_number() OVER () AS id, P.id as partner_id, L.id as livechat_channel_id, count(C.id) as nbr_channel, C.id as channel_id, C.create_date as start_date, EXTRACT('epoch' FROM (max((SELECT (max(M.create_date)) FROM mail_message M JOIN mail_message_mail_channel_rel R ON (R.mail_message_id = M.id) WHERE R.mail_channel_id = C.id))-C.create_date)) as duration, EXTRACT('epoch' from ((SELECT min(M.create_date) FROM mail_message M, mail_message_mail_channel_rel R WHERE M.author_id=P.id AND R.mail_channel_id = C.id AND R.mail_message_id = M.id)-(SELECT min(M.create_date) FROM mail_message M, mail_message_mail_channel_rel R WHERE M.author_id IS NULL AND R.mail_channel_id = C.id AND R.mail_message_id = M.id))) as time_to_answer FROM im_livechat_channel_im_user O JOIN res_users U ON (O.user_id = U.id) JOIN res_partner P ON (U.partner_id = P.id) LEFT JOIN im_livechat_channel L ON (L.id = O.channel_id) LEFT JOIN mail_channel C ON (C.livechat_channel_id = L.id) GROUP BY P.id, L.id, C.id, C.create_date ) """)
class crm_activity_report(models.Model): """ CRM Lead Analysis """ _name = "crm.activity.report" _auto = False _description = "CRM Activity Analysis" _rec_name = 'id' date = fields.Datetime('Date', readonly=True) author_id = fields.Many2one('res.partner', 'Author', readonly=True) user_id = fields.Many2one('res.users', 'Responsible', readonly=True) team_id = fields.Many2one('crm.team', 'Sales Team', readonly=True) subtype_id = fields.Many2one('mail.message.subtype', 'Activity', readonly=True) country_id = fields.Many2one('res.country', 'Country', readonly=True) company_id = fields.Many2one('res.company', 'Company', readonly=True) stage_id = fields.Many2one('crm.stage', 'Stage', readonly=True) partner_id = fields.Many2one('res.partner', 'Partner/Customer', readonly=True) lead_type = fields.Char( string='Type', selection=[('lead', 'Lead'), ('opportunity', 'Opportunity')], help="Type is used to separate Leads and Opportunities") def init(self, cr): tools.drop_view_if_exists(cr, 'crm_activity_report') cr.execute(""" CREATE OR REPLACE VIEW crm_activity_report AS ( select m.id, m.subtype_id, m.author_id, m.date, l.user_id, l.team_id, l.country_id, l.company_id, l.stage_id, l.partner_id, l.type as lead_type from "mail_message" m left join "crm_lead" l on (m.res_id = l.id) inner join "crm_activity" a on (m.subtype_id = a.subtype_id) WHERE (m.model = 'crm.lead') )""")
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 OauthAccessToken(models.Model): _name = 'oauth.access_token' application_id = fields.Many2one('oauth.application', string='Application') token = fields.Char('Access Token', required=True) user_id = fields.Many2one('res.users', string='User', required=True) expires = fields.Datetime('Expires', required=True) scope = fields.Char('Scope') def is_valid(self, cr, uid, ids, scopes=None, context=None): """ Checks if the access token is valid. :param scopes: An iterable containing the scopes to check or None """ res = {} for t in self.browse(cr, uid, ids, context=context): res[t.id] = t.is_expired() and self._allow_scopes( cr, uid, t, scopes) return res def is_expired(self, cr, uid, ids, context=None): res = {} for t in self.browse(cr, uid, ids, context=context): res[t.id] = datetime.now() < datetime.strptime( t.expires, DEFAULT_SERVER_DATETIME_FORMAT) return res def _allow_scopes(self, cr, uid, token, scopes, context=None): if not scopes: return True provided_scopes = set(self.scope.split()) resource_scopes = set(scopes) return resource_scopes.issubset(provided_scopes) def allow_scopes(self, cr, uid, ids, scopes, context=None): """ Check if the token allows the provided scopes :param scopes: An iterable containing the scopes to check """ res = {} for t in self.browse(cr, uid, ids, context=context): res[t.id] = self._allow_scopes(cr, uid, t, scopes, context=context) return res
class ImLivechatReportChannel(models.Model): """ Livechat Support Report on the Channels """ _name = "im_livechat.report.channel" _description = "Livechat Support Report" _order = 'start_date, technical_name' _auto = False uuid = fields.Char('UUID', readonly=True) channel_id = fields.Many2one('mail.channel', 'Conversation', readonly=True) channel_name = fields.Char('Channel Name', readonly=True) technical_name = fields.Char('Code', readonly=True) livechat_channel_id = fields.Many2one('im_livechat.channel', 'Channel', readonly=True) start_date = fields.Datetime('Start Date of session', readonly=True, help="Start date of the conversation") start_date_hour = fields.Char('Hour of start Date of session', readonly=True) duration = fields.Float('Average duration', digits=(16, 2), readonly=True, group_operator="avg", help="Duration of the conversation (in seconds)") nbr_speaker = fields.Integer('# of speakers', readonly=True, group_operator="avg", help="Number of different speakers") nbr_message = fields.Integer('Average message', readonly=True, group_operator="avg", help="Number of message in the conversation") partner_id = fields.Many2one('res.partner', 'Opertor', readonly=True) def init(self, cr): # Note : start_date_hour must be remove when the read_group will allow grouping on the hour of a datetime. Don't forget to change the view ! tools.drop_view_if_exists(cr, 'im_livechat_report_channel') cr.execute(""" CREATE OR REPLACE VIEW im_livechat_report_channel AS ( SELECT C.id as id, C.uuid as uuid, C.id as channel_id, C.name as channel_name, CONCAT(L.name, ' / ', C.id) as technical_name, C.livechat_channel_id as livechat_channel_id, C.create_date as start_date, to_char(date_trunc('hour', C.create_date), 'YYYY-MM-DD HH24:MI:SS') as start_date_hour, EXTRACT('epoch' FROM (max((SELECT (max(M.create_date)) FROM mail_message M JOIN mail_message_mail_channel_rel R ON (R.mail_message_id = M.id) WHERE R.mail_channel_id = C.id))-C.create_date)) as duration, count(distinct P.id) as nbr_speaker, count(distinct M.id) as nbr_message, MAX(S.partner_id) as partner_id FROM mail_channel C JOIN mail_message_mail_channel_rel R ON (C.id = R.mail_channel_id) JOIN mail_message M ON (M.id = R.mail_message_id) JOIN mail_channel_partner S ON (S.channel_id = C.id) JOIN im_livechat_channel L ON (L.id = C.livechat_channel_id) LEFT JOIN res_partner P ON (M.author_id = P.id) GROUP BY C.id, C.name, C.livechat_channel_id, L.name, C.create_date, C.uuid ) """)
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 event_track(models.Model): _name = "event.track" _description = 'Event Track' _order = 'priority, date' _inherit = [ 'mail.thread', 'ir.needaction_mixin', 'website.seo.metadata', 'website.published.mixin' ] name = fields.Char('Title', required=True, translate=True) user_id = fields.Many2one('res.users', 'Responsible', track_visibility='onchange', default=lambda self: self.env.user) partner_id = fields.Many2one('res.partner', 'Proposed by') partner_name = fields.Char('Partner Name') partner_email = fields.Char('Partner Email') partner_phone = fields.Char('Partner Phone') partner_biography = fields.Html('Partner Biography') speaker_ids = fields.Many2many('res.partner', string='Speakers') tag_ids = fields.Many2many('event.track.tag', string='Tags') state = fields.Selection([('draft', 'Proposal'), ('confirmed', 'Confirmed'), ('announced', 'Announced'), ('published', 'Published'), ('refused', 'Refused'), ('cancel', 'Cancelled')], 'Status', default='draft', required=True, copy=False, track_visibility='onchange') description = fields.Html('Track Description', translate=True) date = fields.Datetime('Track Date') duration = fields.Float('Duration', digits=(16, 2), default=1.5) location_id = fields.Many2one('event.track.location', 'Room') event_id = fields.Many2one('event.event', 'Event', required=True) color = fields.Integer('Color Index') priority = fields.Selection([('0', 'Low'), ('1', 'Medium'), ('2', 'High'), ('3', 'Highest')], 'Priority', required=True, default='1') image = fields.Binary('Image', compute='_compute_image', store=True, attachment=True) @api.one @api.depends('speaker_ids.image') def _compute_image(self): if self.speaker_ids: self.image = self.speaker_ids[0].image else: self.image = False @api.model def create(self, vals): res = super(event_track, self).create(vals) res.message_subscribe(res.speaker_ids.ids) res.event_id.message_post(body="""<h3>%(header)s</h3> <ul> <li>%(proposed_by)s</li> <li>%(mail)s</li> <li>%(phone)s</li> <li>%(title)s</li> <li>%(speakers)s</li> <li>%(introduction)s</li> </ul>""" % { 'header': _('New Track Proposal'), 'proposed_by': '<b>%s</b>: %s' % (_('Proposed By'), (res.partner_id.name or res.partner_name or res.partner_email)), 'mail': '<b>%s</b>: %s' % (_('Mail'), '<a href="mailto:%s">%s</a>' % (res.partner_email, res.partner_email)), 'phone': '<b>%s</b>: %s' % (_('Phone'), res.partner_phone), 'title': '<b>%s</b>: %s' % (_('Title'), res.name), 'speakers': '<b>%s</b>: %s' % (_('Speakers Biography'), res.partner_biography), 'introduction': '<b>%s</b>: %s' % (_('Talk Introduction'), res.description), }, subtype='event.mt_event_track') return res @api.multi def write(self, vals): if vals.get('state') == 'published': vals.update({'website_published': True}) res = super(event_track, self).write(vals) if vals.get('speaker_ids'): self.message_subscribe([ speaker['id'] for speaker in self.resolve_2many_commands( 'speaker_ids', vals['speaker_ids'], ['id']) ]) return res @api.multi @api.depends('name') def _website_url(self, field_name, arg): res = super(event_track, self)._website_url(field_name, arg) res.update({ (track.id, '/event/%s/track/%s' % (slug(track.event_id), slug(track))) for track in self }) return res def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True): """ Override read_group to always display all states. """ if groupby and groupby[0] == "state": # Default result structure # states = self._get_state_list(cr, uid, context=context) states = [('draft', 'Proposal'), ('confirmed', 'Confirmed'), ('announced', 'Announced'), ('published', 'Published'), ('cancel', 'Cancelled')] read_group_all_states = [{ '__context': { 'group_by': groupby[1:] }, '__domain': domain + [('state', '=', state_value)], 'state': state_value, 'state_count': 0, } for state_value, state_name in states] # Get standard results read_group_res = super(event_track, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby) # Update standard results with default results result = [] for state_value, state_name in states: res = filter(lambda x: x['state'] == state_value, read_group_res) if not res: res = filter(lambda x: x['state'] == state_value, read_group_all_states) if state_value == 'cancel': res[0]['__fold'] = True res[0]['state'] = [state_value, state_name] result.append(res[0]) return result else: return super(event_track, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby) def open_track_speakers_list(self, cr, uid, track_id, context=None): track_id = self.browse(cr, uid, track_id, context=context) return { 'name': _('Speakers'), 'domain': [('id', 'in', [partner.id for partner in track_id.speaker_ids])], 'view_type': 'form', 'view_mode': 'kanban,form', 'res_model': 'res.partner', 'view_id': False, 'type': 'ir.actions.act_window', }
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 MailTracking(models.Model): _name = 'mail.tracking.value' _description = 'Mail Tracking Value' field = fields.Char('Changed Field', required=True, readonly=1) field_desc = fields.Char('Field Description', required=True, readonly=1) field_type = fields.Char('Field Type') old_value_integer = fields.Integer('Old Value Integer', readonly=1) old_value_float = fields.Float('Old Value Float', readonly=1) old_value_monetary = fields.Float('Old Value Monetary', readonly=1) old_value_char = fields.Char('Old Value Char', readonly=1) old_value_text = fields.Text('Old Value Text', readonly=1) old_value_datetime = fields.Datetime('Old Value DateTime', readonly=1) new_value_integer = fields.Integer('New Value Integer', readonly=1) new_value_float = fields.Float('New Value Float', readonly=1) new_value_monetary = fields.Float('New Value Monetary', readonly=1) new_value_char = fields.Char('New Value Char', readonly=1) new_value_text = fields.Text('New Value Text', readonly=1) new_value_datetime = fields.Datetime('New Value Datetime', readonly=1) mail_message_id = fields.Many2one('mail.message', 'Message ID', required=True, select=True, ondelete='cascade') @api.model def create_tracking_values(self, initial_value, new_value, col_name, col_info): tracked = True values = { 'field': col_name, 'field_desc': col_info['string'], 'field_type': col_info['type'] } if col_info['type'] in [ 'integer', 'float', 'char', 'text', 'datetime', 'monetary' ]: values.update({ 'old_value_%s' % col_info['type']: initial_value, 'new_value_%s' % col_info['type']: new_value }) elif col_info['type'] == 'date': values.update({ 'old_value_datetime': initial_value and datetime.strftime( datetime.combine( datetime.strptime(initial_value, tools.DEFAULT_SERVER_DATE_FORMAT), datetime.min.time()), tools.DEFAULT_SERVER_DATETIME_FORMAT) or False, 'new_value_datetime': new_value and datetime.strftime( datetime.combine( datetime.strptime(new_value, tools.DEFAULT_SERVER_DATE_FORMAT), datetime.min.time()), tools.DEFAULT_SERVER_DATETIME_FORMAT) or False, }) elif col_info['type'] == 'boolean': values.update({ 'old_value_integer': initial_value, 'new_value_integer': new_value }) elif col_info['type'] == 'selection': values.update({ 'old_value_char': initial_value and dict(col_info['selection'])[initial_value] or '', 'new_value_char': new_value and dict(col_info['selection'])[new_value] or '' }) elif col_info['type'] == 'many2one': values.update({ 'old_value_integer': initial_value and initial_value.id or 0, 'new_value_integer': new_value and new_value.id or 0, 'old_value_char': initial_value and initial_value.name_get()[0][1] or '', 'new_value_char': new_value and new_value.name_get()[0][1] or '' }) else: tracked = False if tracked: return values return {} @api.multi def get_old_display_value(self): result = [] for record in self: if record.field_type in [ 'integer', 'float', 'char', 'text', 'datetime', 'monetary' ]: result.append( getattr(record, 'old_value_%s' % record.field_type)) elif record.field_type == 'date': if record.old_value_datetime: old_date = datetime.strptime( record.old_value_datetime, tools.DEFAULT_SERVER_DATETIME_FORMAT).date() result.append( old_date.strftime(tools.DEFAULT_SERVER_DATE_FORMAT)) else: result.append(record.old_value_datetime) elif record.field_type == 'boolean': result.append(bool(record.old_value_integer)) elif record.field_type in ['many2one', 'selection']: result.append(record.old_value_char) else: result.append(record.old_value_char) return result @api.multi def get_new_display_value(self): result = [] for record in self: if record.field_type in [ 'integer', 'float', 'char', 'text', 'datetime', 'monetary' ]: result.append( getattr(record, 'new_value_%s' % record.field_type)) elif record.field_type == 'date': if record.new_value_datetime: new_date = datetime.strptime( record.new_value_datetime, tools.DEFAULT_SERVER_DATETIME_FORMAT).date() result.append( new_date.strftime(tools.DEFAULT_SERVER_DATE_FORMAT)) else: result.append(record.new_value_datetime) elif record.field_type == 'boolean': result.append(bool(record.new_value_integer)) elif record.field_type in ['many2one', 'selection']: result.append(record.new_value_char) else: result.append(record.new_value_char) return result
class DocumentPage(models.Model): """This class is use to manage Document.""" _name = "document.page" _inherit = ['mail.thread'] _description = "Document Page" _order = 'name' name = fields.Char('Title', required=True) type = fields.Selection([('content', 'Content'), ('category', 'Category')], 'Type', help="Page type", default="content") parent_id = fields.Many2one('document.page', 'Category', domain=[('type', '=', 'category')]) child_ids = fields.One2many('document.page', 'parent_id', 'Children') content = fields.Text("Content") display_content = fields.Text(string='Displayed Content', compute='_get_display_content') history_ids = fields.One2many('document.page.history', 'page_id', 'History') menu_id = fields.Many2one('ir.ui.menu', "Menu", readonly=True) create_date = fields.Datetime("Created on", select=True, readonly=True) create_uid = fields.Many2one('res.users', 'Author', select=True, readonly=True) write_date = fields.Datetime("Modification Date", select=True, readonly=True) write_uid = fields.Many2one('res.users', "Last Contributor", select=True, readonly=True) def _get_page_index(self, page, link=True): """Return the index of a document.""" index = [] for subpage in page.child_ids: index += ["<li>" + self._get_page_index(subpage) + "</li>"] r = '' if link: r = '<a href="#id=%s">%s</a>' % (page.id, page.name) if index: r += "<ul>" + "".join(index) + "</ul>" return r def _get_display_content(self): """Return the content of a document.""" for page in self: if page.type == "category": display_content = self._get_page_index(page, link=False) else: display_content = page.content page.display_content = display_content @api.onchange("parent_id") def do_set_content(self): """We Set it the right content to the new parent.""" if self.parent_id and not self.content: if self.parent_id.type == "category": self.content = self.parent_id.content def create_history(self, page_id, content): """Create the first history of a newly created document.""" history = self.env['document.page.history'] return history.create({"content": content, "page_id": page_id}) @api.multi def write(self, vals): """Write the content and set the history.""" result = super(DocumentPage, self).write(vals) content = vals.get('content') if content: for page in self: self.create_history(page.id, content) return result @api.model @api.returns('self', lambda value: value.id) def create(self, vals): """Create the first history of a document.""" page_id = super(DocumentPage, self).create(vals) content = vals.get('content') if content: self.create_history(page_id.id, content) return page_id
class SaasPortalClient(models.Model): _name = 'saas_portal.client' _description = 'Client' _rec_name = 'name' _inherit = ['mail.thread', 'saas_portal.database', 'saas_base.client'] name = fields.Char(required=True) partner_id = fields.Many2one('res.partner', string='Partner', track_visibility='onchange') plan_id = fields.Many2one('saas_portal.plan', string='Plan', track_visibility='onchange', ondelete='restrict') expired = fields.Boolean('Expired', default=False, readonly=True) user_id = fields.Many2one('res.users', default=lambda self: self.env.user, string='Salesperson') notification_sent = fields.Boolean(default=False, readonly=True, help='notification about oncoming expiration has sent') support_team_id = fields.Many2one('saas_portal.support_team', 'Support Team') expiration_datetime_sent = fields.Datetime(help='updates every time send_expiration_info_to_client_db is executed') active = fields.Boolean(default=True, compute='_compute_active', store=True) block_on_expiration = fields.Boolean('Block clients on expiration', default=False) block_on_storage_exceed = fields.Boolean('Block clients on storage exceed', default=False) storage_exceed = fields.Boolean('Storage limit has been exceed', default=False) _track = { 'expired': { 'saas_portal.mt_expired': lambda self, cr, uid, obj, ctx=None: obj.expired } } @api.multi @api.depends('state') def _compute_active(self): for record in self: record.active = record.state != 'deleted' @api.model def _cron_suspend_expired_clients(self): payload = { 'params': [{'key': 'saas_client.suspended', 'value': '1', 'hidden': True}], } now = fields.Datetime.now() expired = self.search([ ('expiration_datetime', '<', now), ('expired', '=', False) ]) expired.write({'expired': True}) for record in expired: if record.trial or record.block_on_expiration: template = self.env.ref('saas_portal.email_template_has_expired_notify') record.message_post_with_template(template.id, composition_mode='comment') self.env['saas.config'].do_upgrade_database(payload, record.id) @api.model def _cron_notify_expired_clients(self): # send notification about expiration by email notification_delta = int(self.env['ir.config_parameter'].get_param('saas.expiration_notify_in_advance', '0')) if notification_delta > 0: records = self.search([('expiration_datetime', '<=', (datetime.now() + timedelta(days=notification_delta)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)), ('notification_sent', '=', False)]) records.write({'notification_sent': True}) for record in records: template = self.env.ref('saas_portal.email_template_expiration_notify') record.with_context(days=notification_delta).message_post_with_template(template.id, composition_mode='comment') def unlink(self, cr, uid, ids, context=None): user_model = self.pool.get('res.users') token_model = self.pool.get('oauth.access_token') for obj in self.browse(cr, uid, ids): to_search1 = [('application_id', '=', obj.id)] tk_ids = token_model.search(cr, uid, to_search1, context=context) if tk_ids: token_model.unlink(cr, uid, tk_ids) # TODO: it seems we don't need stuff below #to_search2 = [('database', '=', obj.name)] #user_ids = user_model.search(cr, uid, to_search2, context=context) #if user_ids: # user_model.unlink(cr, uid, user_ids) #ecore.service.db.exp_drop(obj.name) return super(SaasPortalClient, self).unlink(cr, uid, ids, context) @api.multi def rename_database(self, new_dbname): self.ensure_one() # TODO async state = { 'd': self.name, 'client_id': self.client_id, 'new_dbname': new_dbname, } url = self.server_id._request_server(path='/saas_server/rename_database', state=state, client_id=self.client_id)[0] res = requests.get(url, verify=(self.server_id.request_scheme == 'https' and self.server_id.verify_ssl)) _logger.info('delete database: %s', res.text) if res.status_code != 500: self.name = new_dbname @api.one def duplicate_database(self, dbname=None, partner_id=None, expiration=None): server = self.server_id if not server: server = self.env['saas_portal.server'].get_saas_server() server.action_sync_server() vals = {'name': dbname, 'server_id': server.id, 'plan_id': self.plan_id.id, 'partner_id': partner_id or self.partner_id.id, } if expiration: now = datetime.now() delta = timedelta(hours=expiration) vals['expiration_datetime'] = (now + delta).strftime(DEFAULT_SERVER_DATETIME_FORMAT) client = self.env['saas_portal.client'].create(vals) client_id = client.client_id scheme = server.request_scheme port = server.request_port state = { 'd': client.name, 'e': client.expiration_datetime, 'r': '%s://%s:%s/web' % (scheme, port, client.name), } state.update({'db_template': self.name, 'disable_mail_server' : True}) scope = ['userinfo', 'force_login', 'trial', 'skiptheuse'] # TODO use _request_server url = server._request(path='/saas_server/new_database', scheme=scheme, port=port, state=state, client_id=client_id, scope=scope,)[0] return url @api.multi def send_expiration_info(self): for record in self: if record.expiration_datetime_sent and record.expiration_datetime and record.expiration_datetime_sent != record.expiration_datetime: record.expiration_datetime_sent = record.expiration_datetime record.send_expiration_info_to_client_db() record.send_expiration_info_to_partner() # expiration date has been changed, flush expiration notification flag record.notification_sent = False else: record.expiration_datetime_sent = record.expiration_datetime @api.multi def send_expiration_info_to_client_db(self): for record in self: # TODO: how to do refactoring for params sending # max_users should be updated in client each time the new invoice is paid max_users = record.invoice_lines.sorted(key=lambda r: r.create_date)[0].max_users if record.expiration_datetime: payload = { 'params': [{'key': 'saas_client.expiration_datetime', 'value': record.expiration_datetime, 'hidden': True}, {'key': 'saas_client.trial', 'value': 'False', 'hidden': True}, {'key': 'saas_client.max_users', 'value': max_users, 'hidden': True}], } self.env['saas.config'].do_upgrade_database(payload, record.id) @api.multi def send_params_to_client_db(self): for record in self: payload = { 'params': [{'key': 'saas_client.max_users', 'value': record.max_users, 'hidden': True}, {'key': 'saas_client.expiration_datetime', 'value': record.expiration_datetime, 'hidden': True}, {'key': 'saas_client.total_storage_limit', 'value': record.total_storage_limit, 'hidden': True}], } self.env['saas.config'].do_upgrade_database(payload, record.id) @api.multi def send_expiration_info_to_partner(self): for record in self: if record.expiration_datetime: template = self.env.ref('saas_portal.email_template_expiration_datetime_updated') record.message_post_with_template(template.id, composition_mode='comment') @api.one def write(self, vals): if 'expiration_datetime' in vals and vals['expiration_datetime']: self.send_expiration_info_to_client_db() result = super(SaasPortalClient, self).write(vals) return result @api.multi def storage_usage_monitoring(self): payload = { 'params': [{'key': 'saas_client.suspended', 'value': '1', 'hidden': True}], } for r in self: if r.total_storage_limit < r.file_storage + r.db_storage and r.storage_exceed is False: r.write({'storage_exceed': True}) template = self.env.ref('saas_portal.email_template_storage_exceed') r.message_post_with_template(template.id, composition_mode='comment') if r.block_on_storage_exceed: self.env['saas.config'].do_upgrade_database(payload, r.id) if r.total_storage_limit >= r.file_storage + r.db_storage and r.storage_exceed is True: r.write({'storage_exceed': False})
class MailChannel(models.Model): """ Chat Session Reprensenting a conversation between users. It extends the base method for anonymous usage. """ _name = 'mail.channel' _inherit = ['mail.channel', 'rating.mixin'] anonymous_name = fields.Char('Anonymous Name') create_date = fields.Datetime('Create Date', required=True) channel_type = fields.Selection(selection_add=[('livechat', 'Livechat Conversation')]) livechat_channel_id = fields.Many2one('im_livechat.channel', 'Channel') @api.multi def _channel_message_notifications(self, message): """ When a anonymous user create a mail.channel, the operator is not notify (to avoid massive polling when clicking on livechat button). So when the anonymous person is sending its FIRST message, the channel header should be added to the notification, since the user cannot be listining to the channel. """ notifications = super(MailChannel, self)._channel_message_notifications(message) if not message.author_id: unpinned_channel_partner = self.mapped( 'channel_last_seen_partner_ids').filtered( lambda cp: not cp.is_pinned) if unpinned_channel_partner: unpinned_channel_partner.write({'is_pinned': True}) notifications = self._channel_channel_notifications( unpinned_channel_partner.mapped( 'partner_id').ids) + notifications return notifications @api.multi def channel_info(self, extra_info=False): """ Extends the channel header by adding the livechat operator and the 'anonymous' profile :rtype : list(dict) """ channel_infos = super(MailChannel, self).channel_info(extra_info) # add the operator id if self.env.context.get('im_livechat_operator_partner_id'): partner_name = self.env['res.partner'].browse( self.env.context.get( 'im_livechat_operator_partner_id')).name_get()[0] for channel_info in channel_infos: channel_info['operator_pid'] = partner_name # add the anonymous name channel_infos_dict = dict((c['id'], c) for c in channel_infos) for channel in self: if channel.anonymous_name: channel_infos_dict[ channel.id]['anonymous_name'] = channel.anonymous_name return channel_infos_dict.values() @api.model def channel_fetch_slot(self): values = super(MailChannel, self).channel_fetch_slot() pinned_channels = self.env['mail.channel.partner'].search([ ('partner_id', '=', self.env.user.partner_id.id), ('is_pinned', '=', True) ]).mapped('channel_id') values['channel_livechat'] = self.search([ ('channel_type', '=', 'livechat'), ('public', 'in', ['public']), ('id', 'in', pinned_channels.ids) ]).channel_info() return values @api.model def cron_remove_empty_session(self): hours = 1 # never remove empty session created within the last hour self.env.cr.execute( """ SELECT id as id FROM mail_channel C WHERE NOT EXISTS ( SELECT * FROM mail_message_mail_channel_rel R WHERE R.mail_channel_id = C.id ) AND C.channel_type = 'livechat' AND livechat_channel_id IS NOT NULL AND COALESCE(write_date, create_date, (now() at time zone 'UTC'))::timestamp < ((now() at time zone 'UTC') - interval %s)""", ("%s hours" % hours, )) empty_channel_ids = [item['id'] for item in self.env.cr.dictfetchall()] self.browse(empty_channel_ids).unlink()
class EventMailScheduler(models.Model): """ Event automated mailing. This model replaces all existing fields and configuration allowing to send emails on events since eCore 9. A cron exists that periodically checks for mailing to run. """ _name = 'event.mail' event_id = fields.Many2one('event.event', string='Event', required=True, ondelete='cascade') sequence = fields.Integer('Display order') interval_nbr = fields.Integer('Interval', default=1) interval_unit = fields.Selection([ ('now', 'Immediately'), ('hours', 'Hour(s)'), ('days', 'Day(s)'), ('weeks', 'Week(s)'), ('months', 'Month(s)')], string='Unit', default='hours', required=True) interval_type = fields.Selection([ ('after_sub', 'After each subscription'), ('before_event', 'Before the event'), ('after_event', 'After the event')], string='When to Run ', default="before_event", required=True) template_id = fields.Many2one( 'mail.template', string='Email to Send', domain=[('model', '=', 'event.registration')], required=True, ondelete='restrict', help='This field contains the template of the mail that will be automatically sent') scheduled_date = fields.Datetime('Scheduled Sent Mail', compute='_compute_scheduled_date', store=True) mail_registration_ids = fields.One2many('event.mail.registration', 'scheduler_id') mail_sent = fields.Boolean('Mail Sent on Event') done = fields.Boolean('Sent', compute='_compute_done', store=True) @api.one @api.depends('mail_sent', 'interval_type', 'event_id.registration_ids', 'mail_registration_ids') def _compute_done(self): if self.interval_type in ['before_event', 'after_event']: self.done = self.mail_sent else: self.done = len(self.mail_registration_ids) == len(self.event_id.registration_ids) and all(filter(lambda line: line.mail_sent, self.mail_registration_ids)) @api.one @api.depends('event_id.state', 'event_id.date_begin', 'interval_type', 'interval_unit', 'interval_nbr') def _compute_scheduled_date(self): if self.event_id.state not in ['confirm', 'done']: self.scheduled_date = False else: if self.interval_type == 'after_sub': date, sign = self.event_id.create_date, 1 elif self.interval_type == 'before_event': date, sign = self.event_id.date_begin, -1 else: date, sign = self.event_id.date_end, 1 self.scheduled_date = datetime.strptime(date, tools.DEFAULT_SERVER_DATETIME_FORMAT) + _INTERVALS[self.interval_unit](sign * self.interval_nbr) @api.one def execute(self): if self.interval_type == 'after_sub': # update registration lines lines = [] for registration in filter(lambda item: item not in [mail_reg.registration_id for mail_reg in self.mail_registration_ids], self.event_id.registration_ids): lines.append((0, 0, {'registration_id': registration.id})) if lines: self.write({'mail_registration_ids': lines}) # execute scheduler on registrations self.mail_registration_ids.filtered(lambda reg: reg.scheduled_date and reg.scheduled_date <= datetime.strftime(fields.datetime.now(), tools.DEFAULT_SERVER_DATETIME_FORMAT)).execute() else: if not self.mail_sent: self.event_id.mail_attendees(self.template_id.id) self.write({'mail_sent': True}) return True @api.model def run(self, autocommit=False): schedulers = self.search([('done', '=', False), ('scheduled_date', '<=', datetime.strftime(fields.datetime.now(), tools.DEFAULT_SERVER_DATETIME_FORMAT))]) for scheduler in schedulers: scheduler.execute() if autocommit: self.env.cr.commit() return True
class ResPartner(models.Model): _name = 'res.partner' _inherit = 'res.partner' _description = 'Partner' @api.multi def _credit_debit_get(self): tables, where_clause, where_params = self.env['account.move.line']._query_get() where_params = [tuple(self.ids)] + where_params self._cr.execute("""SELECT l.partner_id, act.type, SUM(l.amount_residual) FROM account_move_line l LEFT JOIN account_account a ON (l.account_id=a.id) LEFT JOIN account_account_type act ON (a.user_type_id=act.id) WHERE act.type IN ('receivable','payable') AND l.partner_id IN %s AND l.reconciled IS FALSE """ + where_clause + """ GROUP BY l.partner_id, act.type """, where_params) for pid, type, val in self._cr.fetchall(): partner = self.browse(pid) if type == 'receivable': partner.credit = val elif type == 'payable': partner.debit = -val @api.multi def _asset_difference_search(self, type, args): if not args: return [] having_values = tuple(map(itemgetter(2), args)) where = ' AND '.join( map(lambda x: '(SUM(bal2) %(operator)s %%s)' % { 'operator':x[1]},args)) query = self.env['account.move.line']._query_get() self._cr.execute(('SELECT pid AS partner_id, SUM(bal2) FROM ' \ '(SELECT CASE WHEN bal IS NOT NULL THEN bal ' \ 'ELSE 0.0 END AS bal2, p.id as pid FROM ' \ '(SELECT (debit-credit) AS bal, partner_id ' \ 'FROM account_move_line l ' \ 'WHERE account_id IN ' \ '(SELECT id FROM account_account '\ 'WHERE type=%s AND active) ' \ 'AND reconciled IS FALSE ' \ 'AND '+query+') AS l ' \ 'RIGHT JOIN res_partner p ' \ 'ON p.id = partner_id ) AS pl ' \ 'GROUP BY pid HAVING ' + where), (type,) + having_values) res = self._cr.fetchall() if not res: return [('id', '=', '0')] return [('id', 'in', map(itemgetter(0), res))] @api.multi def _credit_search(self, args): return self._asset_difference_search('receivable', args) @api.multi def _debit_search(self, args): return self._asset_difference_search('payable', args) @api.multi def _invoice_total(self): account_invoice_report = self.env['account.invoice.report'] if not self.ids: self.total_invoiced = 0.0 return True user_currency_id = self.env.user.company_id.currency_id.id for partner in self: all_partner_ids = self.search([('id', 'child_of', partner.id)]).ids # searching account.invoice.report via the orm is comparatively expensive # (generates queries "id in []" forcing to build the full table). # In simple cases where all invoices are in the same currency than the user's company # access directly these elements # generate where clause to include multicompany rules where_query = account_invoice_report._where_calc([ ('partner_id', 'in', all_partner_ids), ('state', 'not in', ['draft', 'cancel']), ('company_id', '=', self.env.user.company_id.id) ]) account_invoice_report._apply_ir_rules(where_query, 'read') from_clause, where_clause, where_clause_params = where_query.get_sql() query = """ SELECT SUM(price_total) as total FROM account_invoice_report account_invoice_report WHERE %s """ % where_clause # price_total is in the company currency self.env.cr.execute(query, where_clause_params) partner.total_invoiced = self.env.cr.fetchone()[0] @api.multi def _journal_item_count(self): for partner in self: partner.journal_item_count = self.env['account.move.line'].search_count([('partner_id', '=', partner.id)]) partner.contracts_count = self.env['account.analytic.account'].search_count([('partner_id', '=', partner.id)]) def get_followup_lines_domain(self, date, overdue_only=False, only_unblocked=False): domain = [('reconciled', '=', False), ('account_id.deprecated', '=', False), ('account_id.internal_type', '=', 'receivable'), '|', ('debit', '!=', 0), ('credit', '!=', 0), ('company_id', '=', self.env.user.company_id.id)] if only_unblocked: domain += [('blocked', '=', False)] if self.ids: if 'exclude_given_ids' in self._context: domain += [('partner_id', 'not in', self.ids)] else: domain += [('partner_id', 'in', self.ids)] #adding the overdue lines overdue_domain = ['|', '&', ('date_maturity', '!=', False), ('date_maturity', '<=', date), '&', ('date_maturity', '=', False), ('date', '<=', date)] if overdue_only: domain += overdue_domain return domain @api.multi def _compute_issued_total(self): """ Returns the issued total as will be displayed on partner view """ today = fields.Date.context_today(self) for partner in self: domain = partner.get_followup_lines_domain(today, overdue_only=True) issued_total = 0 for aml in self.env['account.move.line'].search(domain): issued_total += aml.amount_residual partner.issued_total = issued_total @api.one def _compute_has_unreconciled_entries(self): # Avoid useless work if has_unreconciled_entries is not relevant for this partner if not self.active or not self.is_company and self.parent_id: return self.env.cr.execute( """ SELECT 1 FROM( SELECT p.last_time_entries_checked AS last_time_entries_checked, MAX(l.write_date) AS max_date FROM account_move_line l RIGHT JOIN account_account a ON (a.id = l.account_id) RIGHT JOIN res_partner p ON (l.partner_id = p.id) WHERE p.id = %s AND EXISTS ( SELECT 1 FROM account_move_line l WHERE l.account_id = a.id AND l.partner_id = p.id AND l.amount_residual > 0 ) AND EXISTS ( SELECT 1 FROM account_move_line l WHERE l.account_id = a.id AND l.partner_id = p.id AND l.amount_residual < 0 ) GROUP BY p.last_time_entries_checked ) as s WHERE (last_time_entries_checked IS NULL OR max_date > last_time_entries_checked) """, (self.id,)) self.has_unreconciled_entries = self.env.cr.rowcount == 1 @api.multi def mark_as_reconciled(self): return self.write({'last_time_entries_checked': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}) @api.one def _get_company_currency(self): if self.company_id: self.currency_id = self.sudo().company_id.currency_id else: self.currency_id = self.env.user.company_id.currency_id credit = fields.Monetary(compute='_credit_debit_get', search=_credit_search, string='Total Receivable', help="Total amount this customer owes you.") debit = fields.Monetary(compute='_credit_debit_get', search=_debit_search, string='Total Payable', help="Total amount you have to pay to this vendor.") debit_limit = fields.Monetary('Payable Limit') total_invoiced = fields.Monetary(compute='_invoice_total', string="Total Invoiced", groups='account.group_account_invoice') currency_id = fields.Many2one('res.currency', compute='_get_company_currency', readonly=True, help='Utility field to express amount currency') contracts_count = fields.Integer(compute='_journal_item_count', string="Contracts", type='integer') journal_item_count = fields.Integer(compute='_journal_item_count', string="Journal Items", type="integer") issued_total = fields.Monetary(compute='_compute_issued_total', string="Journal Items") property_account_payable_id = fields.Many2one('account.account', company_dependent=True, string="Account Payable", oldname="property_account_payable", domain="[('internal_type', '=', 'payable'), ('deprecated', '=', False)]", help="This account will be used instead of the default one as the payable account for the current partner", required=True) property_account_receivable_id = fields.Many2one('account.account', company_dependent=True, string="Account Receivable", oldname="property_account_receivable", domain="[('internal_type', '=', 'receivable'), ('deprecated', '=', False)]", help="This account will be used instead of the default one as the receivable account for the current partner", required=True) property_account_position_id = fields.Many2one('account.fiscal.position', company_dependent=True, string="Fiscal Position", help="The fiscal position will determine taxes and accounts used for the partner.", oldname="property_account_position") property_payment_term_id = fields.Many2one('account.payment.term', company_dependent=True, string ='Customer Payment Term', help="This payment term will be used instead of the default one for sale orders and customer invoices", oldname="property_payment_term") property_supplier_payment_term_id = fields.Many2one('account.payment.term', company_dependent=True, string ='Vendor Payment Term', help="This payment term will be used instead of the default one for purchase orders and vendor bills", oldname="property_supplier_payment_term") ref_company_ids = fields.One2many('res.company', 'partner_id', string='Companies that refers to partner', oldname="ref_companies") has_unreconciled_entries = fields.Boolean(compute='_compute_has_unreconciled_entries', help="The partner has at least one unreconciled debit and credit since last time the invoices & payments matching was performed.") last_time_entries_checked = fields.Datetime(oldname='last_reconciliation_date', string='Latest Invoices & Payments Matching Date', readonly=True, copy=False, help='Last time the invoices & payments matching was performed for this partner. ' 'It is set either if there\'s not at least an unreconciled debit and an unreconciled credit ' 'or if you click the "Done" button.') invoice_ids = fields.One2many('account.invoice', 'partner_id', string='Invoices', readonly=True, copy=False) contract_ids = fields.One2many('account.analytic.account', 'partner_id', string='Contracts', readonly=True) bank_account_count = fields.Integer(compute='_compute_bank_count', string="Bank") @api.multi def _compute_bank_count(self): bank_data = self.env['res.partner.bank'].read_group([('partner_id', 'in', self.ids)], ['partner_id'], ['partner_id']) mapped_data = dict([(bank['partner_id'][0], bank['partner_id_count']) for bank in bank_data]) for partner in self: partner.bank_account_count = mapped_data.get(partner.id, 0) def _find_accounting_partner(self, partner): ''' Find the partner for which the accounting entries will be created ''' return partner.commercial_partner_id @api.model def _commercial_fields(self): return super(ResPartner, self)._commercial_fields() + \ ['debit_limit', 'property_account_payable_id', 'property_account_receivable_id', 'property_account_position_id', 'property_payment_term_id', 'property_supplier_payment_term_id', 'last_time_entries_checked']
class report_event_registration(models.Model): """Events Analysis""" _name = "report.event.registration" _order = 'event_date desc' _auto = False create_date = fields.Datetime('Creation Date', readonly=True) event_date = fields.Datetime('Event Date', readonly=True) event_id = fields.Many2one('event.event', 'Event', required=True) draft_state = fields.Integer(' # No of Draft Registrations') cancel_state = fields.Integer(' # No of Cancelled Registrations') confirm_state = fields.Integer(' # No of Confirmed Registrations') seats_max = fields.Integer('Max Seats') nbevent = fields.Integer('Number of Events') nbregistration = fields.Integer('Number of Registrations') event_type_id = fields.Many2one('event.type', 'Event Type') registration_state = fields.Selection([('draft', 'Draft'), ('confirm', 'Confirmed'), ('done', 'Attended'), ('cancel', 'Cancelled')], 'Registration State', readonly=True, required=True) event_state = fields.Selection([('draft', 'Draft'), ('confirm', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Event State', readonly=True, required=True) user_id = fields.Many2one('res.users', 'Event Responsible', readonly=True) name_registration = fields.Char('Participant / Contact Name', readonly=True) company_id = fields.Many2one('res.company', 'Company', readonly=True) def init(self, cr): """Initialize the sql view for the event registration """ tools.drop_view_if_exists(cr, 'report_event_registration') # TOFIX this request won't select events that have no registration cr.execute(""" CREATE VIEW report_event_registration AS ( SELECT e.id::varchar || '/' || coalesce(r.id::varchar,'') AS id, e.id AS event_id, e.user_id AS user_id, r.name AS name_registration, r.create_date AS create_date, e.company_id AS company_id, e.date_begin AS event_date, count(r.id) AS nbevent, count(r.event_id) AS nbregistration, CASE WHEN r.state IN ('draft') THEN count(r.event_id) ELSE 0 END AS draft_state, CASE WHEN r.state IN ('open','done') THEN count(r.event_id) ELSE 0 END AS confirm_state, CASE WHEN r.state IN ('cancel') THEN count(r.event_id) ELSE 0 END AS cancel_state, e.event_type_id AS event_type_id, e.seats_max AS seats_max, e.state AS event_state, r.state AS registration_state FROM event_event e LEFT JOIN event_registration r ON (e.id=r.event_id) GROUP BY event_id, r.id, registration_state, event_type_id, e.id, e.date_begin, e.user_id, event_state, e.company_id, e.seats_max, name_registration ) """)
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 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 SaasServerClient(models.Model): _name = 'saas_server.client' _inherit = ['mail.thread', 'saas_base.client'] name = fields.Char('Database name', readonly=True, required=True) client_id = fields.Char('Database UUID', readonly=True, select=True) expiration_datetime = fields.Datetime(readonly=True) state = fields.Selection([('template', 'Template'), ('draft', 'New'), ('open', 'In Progress'), ('cancelled', 'Cancelled'), ('pending', 'Pending'), ('deleted', 'Deleted')], 'State', default='draft', track_visibility='onchange') _sql_constraints = [ ('client_id_uniq', 'unique (client_id)', 'client_id should be unique!'), ] @api.one def create_database(self, template_db=None, demo=False, lang='en_US'): new_db = self.name if template_db: ecore.service.db._drop_conn(self.env.cr, template_db) ecore.service.db.exp_duplicate_database(template_db, new_db) else: password = ''.join( random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(32)) ecore.service.db.exp_create_database(new_db, demo, lang, user_password=password) self.state = 'open' @api.one def registry(self, new=False, **kwargs): m = ecore.modules.registry.RegistryManager if new: return m.new(self.name, **kwargs) else: return m.get(self.name, **kwargs) @api.one def install_addons(self, addons, is_template_db): addons = set(addons) addons.add('mail_delete_sent_by_footer') # debug if is_template_db: addons.add('auth_oauth') addons.add('saas_client') else: addons.add('saas_client') if not addons: return with self.registry()[0].cursor() as cr: env = api.Environment(cr, SUPERUSER_ID, self._context) self._install_addons(env, addons) @api.one def disable_mail_servers(self): ''' disables mailserver on db to stop it from sending and receiving mails ''' # let's disable incoming mail servers incoming_mail_servers = self.env['fetchmail.server'].search([]) if len(incoming_mail_servers): incoming_mail_servers.write({'active': False}) # let's disable outgoing mailservers too outgoing_mail_servers = self.env['ir.mail_server'].search([]) if len(outgoing_mail_servers): outgoing_mail_servers.write({'active': False}) @api.one def _install_addons(self, client_env, addons): for addon in client_env['ir.module.module'].search([('name', 'in', list(addons))]): addon.button_install() @api.one def update_registry(self): self.registry(new=True, update_module=True) @api.one def prepare_database(self, **kwargs): with self.registry()[0].cursor() as cr: env = api.Environment(cr, SUPERUSER_ID, self._context) self._prepare_database(env, **kwargs) @api.model def _config_parameters_to_copy(self): return ['saas_client.ab_location', 'saas_client.ab_register'] @api.one def _prepare_database(self, client_env, owner_user=None, is_template_db=False, addons=[], access_token=None, tz=None): client_id = self.client_id # update saas_server.client state if is_template_db: self.state = 'template' # set tz if tz: client_env['res.users'].search([]).write({'tz': tz}) client_env['ir.values'].set_default('res.partner', 'tz', tz) # update database.uuid client_env['ir.config_parameter'].set_param('database.uuid', client_id) # copy configs for key in self._config_parameters_to_copy(): value = self.env['ir.config_parameter'].get_param(key, default='') client_env['ir.config_parameter'].set_param(key, value) # copy auth provider from saas_server saas_oauth_provider = self.env.ref('saas_server.saas_oauth_provider') oauth_provider = None if is_template_db and not client_env.ref( 'saas_server.saas_oauth_provider', raise_if_not_found=False): oauth_provider_data = {'enabled': False, 'client_id': client_id} for attr in [ 'name', 'auth_endpoint', 'scope', 'validation_endpoint', 'data_endpoint', 'css_class', 'body', 'enabled' ]: oauth_provider_data[attr] = getattr(saas_oauth_provider, attr) oauth_provider = client_env['auth.oauth.provider'].create( oauth_provider_data) client_env['ir.model.data'].create({ 'name': 'saas_oauth_provider', 'module': 'saas_server', 'noupdate': True, 'model': 'auth.oauth.provider', 'res_id': oauth_provider.id, }) if not oauth_provider: oauth_provider = client_env.ref('saas_server.saas_oauth_provider') if not is_template_db: oauth_provider.client_id = client_id # prepare users OWNER_TEMPLATE_LOGIN = '******' user = None if is_template_db: client_env['res.users'].create({ 'login': OWNER_TEMPLATE_LOGIN, 'name': 'NAME', 'email': '*****@*****.**', }) client_env['res.users'].browse(SUPERUSER_ID).write({ 'oauth_provider_id': oauth_provider.id, 'oauth_uid': SUPERUSER_ID, 'oauth_access_token': access_token }) else: domain = [('login', '=', OWNER_TEMPLATE_LOGIN)] res = client_env['res.users'].search(domain) if res: user = res[0] client_env['ir.config_parameter'].set_param( 'res.users.owner', user.id, groups=['saas_client.group_saas_support']) res = client_env['res.users'].search([('oauth_uid', '=', owner_user['user_id'])]) if res: # user already exists (e.g. administrator) user = res[0] if not user: user = client_env['res.users'].browse(SUPERUSER_ID) user.write({ 'login': owner_user['login'], 'name': owner_user['name'], 'email': owner_user['email'], 'oauth_provider_id': oauth_provider.id, 'oauth_uid': owner_user['user_id'], 'oauth_access_token': access_token }) @api.model def update_all(self): self.sudo().search([]).update() @api.one def update(self): try: registry = self.registry()[0] except psycopg2.OperationalError: if self.state != 'draft': self.state = 'deleted' return with registry.cursor() as client_cr: client_env = api.Environment(client_cr, SUPERUSER_ID, self._context) data = self._get_data(client_env, self.client_id)[0] self.write(data) @api.one def _get_data(self, client_env, check_client_id): client_id = client_env['ir.config_parameter'].get_param( 'database.uuid') if check_client_id != client_id: return {'state': 'deleted'} users = client_env['res.users'].search([('share', '=', False)]) param_obj = client_env['ir.config_parameter'] max_users = param_obj.get_param('saas_client.max_users', '_') suspended = param_obj.get_param('saas_client.suspended', '0') total_storage_limit = param_obj.get_param( 'saas_client.total_storage_limit', '0') users_len = len(users) data_dir = ecore.tools.config['data_dir'] file_storage = get_size('%s/filestore/%s' % (data_dir, self.name)) file_storage = int(file_storage / (1024 * 1024)) client_env.cr.execute("select pg_database_size('%s')" % self.name) db_storage = client_env.cr.fetchone()[0] db_storage = int(db_storage / (1024 * 1024)) data = { 'client_id': client_id, 'users_len': users_len, 'max_users': max_users, 'file_storage': file_storage, 'db_storage': db_storage, 'total_storage_limit': total_storage_limit, } if suspended == '0' and self.state == 'pending': data.update({'state': 'open'}) if suspended == '1' and self.state == 'open': data.update({'state': 'pending'}) return data @api.one def upgrade_database(self, **kwargs): with self.registry()[0].cursor() as cr: env = api.Environment(cr, SUPERUSER_ID, self._context) return self._upgrade_database(env, **kwargs)[0] @api.one def _upgrade_database(self, client_env, data): # "data" comes from saas_portal/models/wizard.py::upgrade_database post = data module = client_env['ir.module.module'] print '_upgrade_database', data res = {} # 0. Update module list update_list = post.get('update_addons_list', False) if update_list: module.update_list() # 1. Update addons update_addons = post.get('update_addons', []) if update_addons: module.search([('name', 'in', update_addons) ]).button_immediate_upgrade() # 2. Install addons install_addons = post.get('install_addons', []) if install_addons: module.search([('name', 'in', install_addons) ]).button_immediate_install() # 3. Uninstall addons uninstall_addons = post.get('uninstall_addons', []) if uninstall_addons: module.search([('name', 'in', uninstall_addons) ]).button_immediate_uninstall() # 4. Run fixes fixes = post.get('fixes', []) for model, method in fixes: getattr(request.registry[model], method)() # 5. update parameters params = post.get('params', []) for obj in params: if obj['key'] == 'saas_client.expiration_datetime': self.expiration_datetime = obj['value'] if obj['key'] == 'saas_client.trial' and obj['value'] == 'False': self.trial = False groups = [] if obj.get('hidden'): groups = ['saas_client.group_saas_support'] client_env['ir.config_parameter'].set_param(obj['key'], obj['value'] or ' ', groups=groups) # 6. Access rights access_owner_add = post.get('access_owner_add', []) owner_id = client_env['ir.config_parameter'].get_param( 'res.users.owner', 0) owner_id = int(owner_id) if not owner_id: res['owner_id'] = "Owner's user is not found" if access_owner_add and owner_id: res['access_owner_add'] = [] for g_ref in access_owner_add: g = client_env.ref(g_ref, raise_if_not_found=False) if not g: res['access_owner_add'].append('group not found: %s' % g_ref) continue g.write({'users': [(4, owner_id, 0)]}) access_remove = post.get('access_remove', []) if access_remove: res['access_remove'] = [] for g_ref in access_remove: g = client_env.ref(g_ref, raise_if_not_found=False) if not g: res['access_remove'].append('group not found: %s' % g_ref) continue users = [] for u in g.users: if u.id != SUPERUSER_ID: users.append((3, u.id, 0)) g.write({'users': users}) return res @api.model def delete_expired_databases(self): now = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) res = self.search([('state', 'not in', ['deleted']), ('expiration_datetime', '<=', now), ('trial', '=', True)]) _logger.info('delete_expired_databases %s', res) res.delete_database() @api.one def delete_database(self): ecore.service.db.exp_drop(self.name) self.write({'state': 'deleted'}) @api.one def rename_database(self, new_dbname): ecore.service.db.exp_rename(self.name, new_dbname) self.name = new_dbname
class Vote(models.Model): _name = 'forum.post.vote' _description = 'Vote' post_id = fields.Many2one('forum.post', string='Post', ondelete='cascade', required=True) user_id = fields.Many2one('res.users', string='User', required=True, default=lambda self: self._uid) vote = fields.Selection([('1', '1'), ('-1', '-1'), ('0', '0')], string='Vote', required=True, default='1') create_date = fields.Datetime('Create Date', select=True, readonly=True) forum_id = fields.Many2one('forum.forum', string='Forum', related="post_id.forum_id", store=True) recipient_id = fields.Many2one('res.users', string='To', related="post_id.create_uid", store=True) def _get_karma_value(self, old_vote, new_vote, up_karma, down_karma): _karma_upd = { '-1': { '-1': 0, '0': -1 * down_karma, '1': -1 * down_karma + up_karma }, '0': { '-1': 1 * down_karma, '0': 0, '1': up_karma }, '1': { '-1': -1 * up_karma + down_karma, '0': -1 * up_karma, '1': 0 } } return _karma_upd[old_vote][new_vote] @api.model def create(self, vals): vote = super(Vote, self).create(vals) # own post check if vote.user_id.id == vote.post_id.create_uid.id: raise UserError(_('Not allowed to vote for its own post')) # karma check if vote.vote == '1' and not vote.post_id.can_upvote: raise KarmaError('Not enough karma to upvote.') elif vote.vote == '-1' and not vote.post_id.can_downvote: raise KarmaError('Not enough karma to downvote.') if vote.post_id.parent_id: karma_value = self._get_karma_value( '0', vote.vote, vote.forum_id.karma_gen_answer_upvote, vote.forum_id.karma_gen_answer_downvote) else: karma_value = self._get_karma_value( '0', vote.vote, vote.forum_id.karma_gen_question_upvote, vote.forum_id.karma_gen_question_downvote) vote.recipient_id.sudo().add_karma(karma_value) return vote @api.multi def write(self, values): if 'vote' in values: for vote in self: # own post check if vote.user_id.id == vote.post_id.create_uid.id: raise UserError(_('Not allowed to vote for its own post')) # karma check if (values['vote'] == '1' or vote.vote == '-1' and values['vote'] == '0') and not vote.post_id.can_upvote: raise KarmaError('Not enough karma to upvote.') elif (values['vote'] == '-1' or vote.vote == '1' and values['vote'] == '0') and not vote.post_id.can_downvote: raise KarmaError('Not enough karma to downvote.') # karma update if vote.post_id.parent_id: karma_value = self._get_karma_value( vote.vote, values['vote'], vote.forum_id.karma_gen_answer_upvote, vote.forum_id.karma_gen_answer_downvote) else: karma_value = self._get_karma_value( vote.vote, values['vote'], vote.forum_id.karma_gen_question_upvote, vote.forum_id.karma_gen_question_downvote) vote.recipient_id.sudo().add_karma(karma_value) res = super(Vote, self).write(values) return res
class hr_recruitment_report(models.Model): _name = "hr.recruitment.report" _description = "Recruitments Statistics" _auto = False _rec_name = 'date_create' _order = 'date_create desc' user_id = fields.Many2one('res.users', 'User', readonly=True) company_id = fields.Many2one('res.company', 'Company', readonly=True) date_create = fields.Datetime('Create Date', readonly=True) date_last_stage_update = fields.Datetime('Last Stage Update', readonly=True) date_closed = fields.Date('Closed', readonly=True) job_id = fields.Many2one('hr.job', 'Applied Job', readonly=True) stage_id = fields.Many2one('hr.recruitment.stage', 'Stage') type_id = fields.Many2one('hr.recruitment.degree', 'Degree') department_id = fields.Many2one('hr.department', 'Department', readonly=True) priority = fields.Selection(hr_recruitment.AVAILABLE_PRIORITIES, 'Appreciation') salary_prop = fields.Float("Salary Proposed", digits=0) salary_prop_avg = fields.Float("Avg. Proposed Salary", group_operator="avg", digits=0) salary_exp = fields.Float("Salary Expected", digits=0) salary_exp_avg = fields.Float("Avg. Expected Salary", group_operator="avg", digits=0) partner_id = fields.Many2one('res.partner', 'Partner', readonly=True) delay_close = fields.Float( 'Avg. Delay to Close', digits=(16, 2), readonly=True, group_operator="avg", help="Number of Days to close the project issue") last_stage_id = fields.Many2one('hr.recruitment.stage', 'Last Stage') medium_id = fields.Many2one( 'utm.medium', 'Medium', readonly=True, help="This is the method of delivery. Ex: Postcard, Email, or Banner Ad" ) source_id = fields.Many2one( 'utm.source', 'Source', readonly=True, help= "This is the source of the link Ex: Search Engine, another domain, or name of email list" ) def init(self, cr): tools.drop_view_if_exists(cr, 'hr_recruitment_report') cr.execute(""" create or replace view hr_recruitment_report as ( select min(s.id) as id, s.create_date as date_create, date(s.date_closed) as date_closed, s.date_last_stage_update as date_last_stage_update, s.partner_id, s.company_id, s.user_id, s.job_id, s.type_id, s.department_id, s.priority, s.stage_id, s.last_stage_id, s.medium_id, s.source_id, sum(salary_proposed) as salary_prop, (sum(salary_proposed)/count(*)) as salary_prop_avg, sum(salary_expected) as salary_exp, (sum(salary_expected)/count(*)) as salary_exp_avg, extract('epoch' from (s.write_date-s.create_date))/(3600*24) as delay_close, count(*) as nbr from hr_applicant s group by s.date_open, s.create_date, s.write_date, s.date_closed, s.date_last_stage_update, s.partner_id, s.company_id, s.user_id, s.stage_id, s.last_stage_id, s.type_id, s.priority, s.job_id, s.department_id, s.medium_id, s.source_id ) """)
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 AccountAccount(models.Model): _name = "account.account" _description = "Account" _order = "code" #@api.multi #def _compute_has_unreconciled_entries(self): # print "ici dedans" # account_ids = self.ids # for account in self: # # Avoid useless work if has_unreconciled_entries is not relevant for this account # if account.deprecated or not account.reconcile: # account.has_unreconciled_entries = False # account_ids = account_ids - account # if account_ids: # res = dict.fromkeys([x.id for x in account_ids], False) # self.env.cr.execute( # """ SELECT s.account_id FROM( # SELECT # a.id as account_id, a.last_time_entries_checked AS last_time_entries_checked, # MAX(l.write_date) AS max_date # FROM # account_move_line l # RIGHT JOIN account_account a ON (a.id = l.account_id) # WHERE # a.id in %s # AND EXISTS ( # SELECT 1 # FROM account_move_line l # WHERE l.account_id = a.id # AND l.amount_residual > 0 # ) # AND EXISTS ( # SELECT 1 # FROM account_move_line l # WHERE l.account_id = a.id # AND l.amount_residual < 0 # ) # GROUP BY a.id, a.last_time_entries_checked # ) as s # WHERE (last_time_entries_checked IS NULL OR max_date > last_time_entries_checked) # """ % (account_ids,)) # res.update(self.env.cr.dictfetchall()) # for account in self.browse(res.keys()): # if res[account.id]: # account.has_unreconciled_entries = True @api.multi @api.constrains('internal_type', 'reconcile') def _check_reconcile(self): for account in self: if account.internal_type in ( 'receivable', 'payable') and account.reconcile == False: raise ValueError( _('You cannot have a receivable/payable account that is not reconciliable. (account code: %s)' ) % account.code) name = fields.Char(required=True, index=True) currency_id = fields.Many2one( 'res.currency', string='Account Currency', help="Forces all moves for this account to have this account currency." ) code = fields.Char(size=64, required=True, index=True) deprecated = fields.Boolean(index=True, default=False) user_type_id = fields.Many2one( 'account.account.type', string='Type', required=True, oldname="user_type", help= "Account Type is used for information purpose, to generate country-specific legal reports, and set the rules to close a fiscal year and generate opening entries." ) internal_type = fields.Selection(related='user_type_id.type', store=True) #has_unreconciled_entries = fields.Boolean(compute='_compute_has_unreconciled_entries', # help="The account has at least one unreconciled debit and credit since last time the invoices & payments matching was performed.") last_time_entries_checked = fields.Datetime(string='Latest Invoices & Payments Matching Date', readonly=True, copy=False, help='Last time the invoices & payments matching was performed on this account. It is set either if there\'s not at least '\ 'an unreconciled debit and an unreconciled credit Or if you click the "Done" button.') reconcile = fields.Boolean( string='Allow Reconciliation', default=False, help= "Check this box if this account allows invoices & payments matching of journal items." ) tax_ids = fields.Many2many('account.tax', 'account_account_tax_default_rel', 'account_id', 'tax_id', string='Default Taxes') note = fields.Text('Internal Notes') company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env['res.company']. _company_default_get('account.account')) tag_ids = fields.Many2many( 'account.account.tag', 'account_account_account_tag', string='Tags', help="Optional tags you may want to assign for custom reporting") _sql_constraints = [ ('code_company_uniq', 'unique (code,company_id)', 'The code of the account must be unique per company !') ] @api.model def name_search(self, name, args=None, operator='ilike', limit=100): args = args or [] domain = [] if name: domain = [ '|', ('code', '=ilike', name + '%'), ('name', operator, name) ] if operator in expression.NEGATIVE_TERM_OPERATORS: domain = ['&'] + domain accounts = self.search(domain + args, limit=limit) return accounts.name_get() @api.onchange('internal_type') def onchange_internal_type(self): if self.internal_type in ('receivable', 'payable'): self.reconcile = True @api.multi @api.depends('name', 'code') def name_get(self): result = [] for account in self: name = account.code + ' ' + account.name result.append((account.id, name)) return result @api.one def copy(self, default=None): default = dict(default or {}) default.update(code=_("%s (copy)") % (self.code or '')) return super(AccountAccount, self).copy(default) @api.multi def write(self, vals): # Dont allow changing the company_id when account_move_line already exist if vals.get('company_id', False): move_lines = self.env['account.move.line'].search( [('account_id', 'in', self.ids)], limit=1) for account in self: if (account.company_id.id <> vals['company_id']) and move_lines: raise UserError( _('You cannot change the owner company of an account that already contains journal items.' )) return super(AccountAccount, self).write(vals) @api.multi def unlink(self): if self.env['account.move.line'].search( [('account_id', 'in', self.ids)], limit=1): raise UserError( _('You cannot do that on an account that contains journal items.' )) #Checking whether the account is set as a property to any Partner or not values = [ 'account.account,%s' % (account_id, ) for account_id in self.ids ] partner_prop_acc = self.env['ir.property'].search( [('value_reference', 'in', values)], limit=1) if partner_prop_acc: raise UserError( _('You cannot remove/deactivate an account which is set on a customer or vendor.' )) return super(AccountAccount, self).unlink() @api.multi def mark_as_reconciled(self): return self.write({ 'last_time_entries_checked': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) })