class Retrospective(models.Model): _name = "retrospective" _inherit = ['ir.branch.company.mixin', 'mail.thread'] name = fields.Char(string="Retrospective Name", required=True, track_visibility='onchange') retrospective_method_id = fields.Many2one( "retrospective.method", string="Retrospective Method", track_visibility='onchange') scrum_master = fields.Many2one("res.users", string="Scrum Master", track_visibility='onchange') sprint_id = fields.Many2one( "project.sprint", string="Sprint", track_visibility='onchange') retrospective_line_ids = fields.One2many( "retrospective.lines", "retrospective_id", string="Retrospective Lines", track_visibility='onchange') start_date = fields.Datetime(string="Start Date") end_date = fields.Datetime(string="End Date") @api.onchange('sprint_id') def on_sprint_id_change(self): self.scrum_master = self.sprint_id.team_id.master_id.id @api.onchange('sprint_id') def onchange_project(self): if self.sprint_id and self.sprint_id.project_id.branch_id: self.branch_id = self.sprint_id.project_id.branch_id
class ResourceCalendarLeaves(models.Model): _name = "resource.calendar.leaves" _description = "Leave Detail" name = fields.Char('Reason') company_id = fields.Many2one( 'res.company', related='calendar_id.company_id', string="Company", readonly=True, store=True) calendar_id = fields.Many2one('resource.calendar', 'Working Hours') date_from = fields.Datetime('Start Date', required=True) date_to = fields.Datetime('End Date', required=True) tz = fields.Selection( _tz_get, string='Timezone', default=lambda self: self._context.get('tz') or self.env.user.tz or 'UTC', help="Timezone used when encoding the leave. It is used to correctly " "localize leave hours when computing time intervals.") resource_id = fields.Many2one( "resource.resource", 'Resource', help="If empty, this is a generic holiday for the company. If a resource is set, the holiday/leave is only for this resource") @api.constrains('date_from', 'date_to') def check_dates(self): if self.filtered(lambda leave: leave.date_from > leave.date_to): raise ValidationError(_('Error! leave start-date must be lower then leave end-date.')) @api.onchange('resource_id') def onchange_resource(self): if self.resource_id: self.calendar_id = self.resource_id.calendar_id
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('actpy.sql_db'): presence.write(values) # avoid TransactionRollbackError self.env.cr.commit() # TODO : check if still necessary
class MassMailingList(models.Model): """Model of a contact list. """ _name = 'mail.mass_mailing.list' _order = 'name' _description = 'Mailing List' name = fields.Char(string='Mailing List', required=True) active = fields.Boolean(default=True) create_date = fields.Datetime(string='Creation Date') contact_nbr = fields.Integer(compute="_compute_contact_nbr", string='Number of Contacts') # Compute number of contacts non opt-out for a mailing list def _compute_contact_nbr(self): self.env.cr.execute(''' select list_id, count(*) from mail_mass_mailing_contact_list_rel r left join mail_mass_mailing_contact c on (r.contact_id=c.id) where c.opt_out <> true group by list_id ''') data = dict(self.env.cr.fetchall()) for mailing_list in self: mailing_list.contact_nbr = data.get(mailing_list.id, 0)
class Attendee(models.Model): _inherit = 'calendar.attendee' google_internal_event_id = fields.Char('Google Calendar Event Id') oe_synchro_date = fields.Datetime('actpy Synchro Date') _sql_constraints = [ ('google_id_uniq', 'unique(google_internal_event_id,partner_id,event_id)', 'Google ID should be unique!') ] @api.multi def write(self, values): for attendee in self: meeting_id_to_update = values.get('event_id', attendee.event_id.id) # If attendees are updated, we need to specify that next synchro need an action # Except if it come from an update_from_google if not self._context.get('curr_attendee', False) and not self._context.get( 'NewMeeting', False): self.env['calendar.event'].browse(meeting_id_to_update).write( {'oe_update_date': fields.Datetime.now()}) return super(Attendee, self).write(values)
class test_model(models.Model): _name = 'test_converter.test_model' char = fields.Char() integer = fields.Integer() float = fields.Float() numeric = fields.Float(digits=(16, 2)) many2one = fields.Many2one('test_converter.test_model.sub', group_expand='_gbf_m2o') binary = fields.Binary() date = fields.Date() datetime = fields.Datetime() selection = fields.Selection([ (1, "réponse A"), (2, "réponse B"), (3, "réponse C"), (4, "réponse <D>"), ]) selection_str = fields.Selection([ ('A', u"Qu'il n'est pas arrivé à Toronto"), ('B', u"Qu'il était supposé arriver à Toronto"), ('C', u"Qu'est-ce qu'il fout ce maudit pancake, tabernacle ?"), ('D', u"La réponse D"), ], string=u"Lorsqu'un pancake prend l'avion à destination de Toronto et " u"qu'il fait une escale technique à St Claude, on dit:") html = fields.Html() text = fields.Text() # `base` module does not contains any model that implement the functionality # `group_expand`; test this feature here... @api.model def _gbf_m2o(self, subs, domain, order): sub_ids = subs._search([], order=order, access_rights_uid=SUPERUSER_ID) return subs.browse(sub_ids)
class MixedModel(models.Model): _name = 'test_new_api.mixed' number = fields.Float(digits=(10, 2), default=3.14) date = fields.Date() now = fields.Datetime(compute='_compute_now') lang = fields.Selection(string='Language', selection='_get_lang') reference = fields.Reference(string='Related Document', selection='_reference_models') comment1 = fields.Html(sanitize=False) comment2 = fields.Html(sanitize_attributes=True, strip_classes=False) comment3 = fields.Html(sanitize_attributes=True, strip_classes=True) comment4 = fields.Html(sanitize_attributes=True, strip_style=True) currency_id = fields.Many2one( 'res.currency', default=lambda self: self.env.ref('base.EUR')) amount = fields.Monetary() @api.one def _compute_now(self): # this is a non-stored computed field without dependencies self.now = fields.Datetime.now() @api.model def _get_lang(self): return self.env['res.lang'].get_installed() @api.model def _reference_models(self): models = self.env['ir.model'].sudo().search([('state', '!=', 'manual') ]) return [(model.model, model.name) for model in models if not model.model.startswith('ir.')]
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', index=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 OauthAccessToken(models.Model): _name = 'oauth.access_token' 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') @api.multi def _get_access_token(self, user_id=None, create=False): if not user_id: user_id = self.env.user.id access_token = self.env['oauth.access_token'].sudo().search( [('user_id', '=', user_id)], order='id DESC', limit=1) if access_token: access_token = access_token[0] if access_token.is_expired(): access_token = None if not access_token and create: expires = datetime.now() + timedelta(seconds=int(self.env.ref('rest_api.oauth2_access_token_expires_in').sudo().value)) vals = { 'user_id': user_id, 'scope': 'userinfo', 'expires': expires.strftime(DEFAULT_SERVER_DATETIME_FORMAT), 'token': oauthlib_common.generate_token(), } access_token = self.env['oauth.access_token'].sudo().create(vals) # we have to commit now, because /oauth2/tokeninfo could # be called before we finish current transaction. self._cr.commit() if not access_token: return None return access_token.token @api.multi def is_valid(self, scopes=None): """ Checks if the access token is valid. :param scopes: An iterable containing the scopes to check or None """ self.ensure_one() return not self.is_expired() and self._allow_scopes(scopes) @api.multi def is_expired(self): self.ensure_one() return datetime.now() > fields.Datetime.from_string(self.expires) @api.multi def _allow_scopes(self, scopes): self.ensure_one() if not scopes: return True provided_scopes = set(self.scope.split()) resource_scopes = set(scopes) return resource_scopes.issubset(provided_scopes)
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 ConverterTest(models.Model): _name = 'web_editor.converter.test' # disable translation export for those brilliant field labels and values _translate = False char = fields.Char() integer = fields.Integer() float = fields.Float() numeric = fields.Float(digits=(16, 2)) many2one = fields.Many2one('web_editor.converter.test.sub') binary = fields.Binary() date = fields.Date() datetime = fields.Datetime() selection = fields.Selection([ (1, "réponse A"), (2, "réponse B"), (3, "réponse C"), (4, "réponse <D>"), ]) selection_str = fields.Selection( [ ('A', "Qu'il n'est pas arrivé à Toronto"), ('B', "Qu'il était supposé arriver à Toronto"), ('C', "Qu'est-ce qu'il fout ce maudit pancake, tabernacle ?"), ('D', "La réponse D"), ], string=u"Lorsqu'un pancake prend l'avion à destination de Toronto et " u"qu'il fait une escale technique à St Claude, on dit:") html = fields.Html() text = fields.Text()
class LeadTest(models.Model): _name = "base.automation.lead.test" _description = "Action Rule Test" name = fields.Char(string='Subject', required=True, index=True) user_id = fields.Many2one('res.users', string='Responsible') state = fields.Selection([('draft', 'New'), ('cancel', 'Cancelled'), ('open', 'In Progress'), ('pending', 'Pending'), ('done', 'Closed')], string="Status", readonly=True, default='draft') active = fields.Boolean(default=True) partner_id = fields.Many2one('res.partner', string='Partner') date_action_last = fields.Datetime(string='Last Action', readonly=True) customer = fields.Boolean(related='partner_id.customer', readonly=True, store=True) line_ids = fields.One2many('base.automation.line.test', 'lead_id') priority = fields.Boolean() deadline = fields.Boolean(compute='_compute_deadline', store=True) is_assigned_to_admin = fields.Boolean(string='Assigned to admin user') @api.depends('priority') def _compute_deadline(self): for record in self: if not record.priority: record.deadline = False else: record.deadline = fields.Datetime.from_string( record.create_date) + relativedelta.relativedelta(days=3)
class Partner(models.Model): _inherit = 'res.partner' calendar_last_notif_ack = fields.Datetime( 'Last notification marked as read from base Calendar') @api.multi def get_attendee_detail(self, meeting_id): """ Return a list of tuple (id, name, status) Used by base_calendar.js : Many2ManyAttendee """ datas = [] meeting = None if meeting_id: meeting = self.env['calendar.event'].browse( get_real_ids(meeting_id)) for partner in self: data = partner.name_get()[0] data = [data[0], data[1], False, partner.color] if meeting: for attendee in meeting.attendee_ids: if attendee.partner_id.id == partner.id: data[2] = attendee.state datas.append(data) return datas @api.model def _set_calendar_last_notif_ack(self): partner = self.env['res.users'].browse(self.env.uid).partner_id partner.write({'calendar_last_notif_ack': datetime.now()}) return
class User(models.Model): _inherit = 'res.users' google_calendar_rtoken = fields.Char('Refresh Token', copy=False) google_calendar_token = fields.Char('User token', copy=False) google_calendar_token_validity = fields.Datetime('Token Validity', copy=False) google_calendar_last_sync_date = fields.Datetime('Last synchro date', copy=False) google_calendar_cal_id = fields.Char( 'Calendar ID', copy=False, help= 'Last Calendar ID who has been synchronized. If it is changed, we remove all links between GoogleID and actpy Google Internal ID' )
class PosDetails(models.TransientModel): _name = 'pos.details.wizard' _description = 'Open Sales Details Report' def _default_start_date(self): """ Find the earliest start_date of the latests sessions """ # restrict to configs available to the user config_ids = self.env['pos.config'].search([]).ids # exclude configs has not been opened for 2 days self.env.cr.execute(""" SELECT max(start_at) as start, config_id FROM pos_session WHERE config_id = ANY(%s) AND start_at > (NOW() - INTERVAL '2 DAYS') GROUP BY config_id """, (config_ids,)) latest_start_dates = [res['start'] for res in self.env.cr.dictfetchall()] # earliest of the latest sessions return latest_start_dates and min(latest_start_dates) or fields.Datetime.now() start_date = fields.Datetime(required=True, default=_default_start_date) end_date = fields.Datetime(required=True, default=fields.Datetime.now) pos_config_ids = fields.Many2many('pos.config', 'pos_detail_configs', default=lambda s: s.env['pos.config'].search([])) @api.onchange('start_date') def _onchange_start_date(self): if self.start_date and self.end_date and self.end_date < self.start_date: self.end_date = self.start_date @api.onchange('end_date') def _onchange_end_date(self): if self.end_date and self.end_date < self.start_date: self.start_date = self.end_date @api.multi def generate_report(self): if (not self.env.user.company_id.logo): raise UserError(_("You have to set a logo or a layout for your company.")) elif (not self.env.user.company_id.external_report_layout): raise UserError(_("You have to set your reports's header and footer layout.")) data = {'date_start': self.start_date, 'date_stop': self.end_date, 'config_ids': self.pos_config_ids.ids} return self.env.ref('point_of_sale.sale_details_report').report_action([], data=data)
class BadgeUser(models.Model): """User having received a badge""" _name = 'gamification.badge.user' _description = 'Gamification user badge' _order = "create_date desc" _rec_name = "badge_name" user_id = fields.Many2one('res.users', string="User", required=True, ondelete="cascade", index=True) sender_id = fields.Many2one('res.users', string="Sender", help="The user who has send the badge") badge_id = fields.Many2one('gamification.badge', string='Badge', required=True, ondelete="cascade", index=True) challenge_id = fields.Many2one( 'gamification.challenge', string='Challenge originating', help="If this badge was rewarded through a challenge") comment = fields.Text('Comment') badge_name = fields.Char(related='badge_id.name', string="Badge Name") create_date = fields.Datetime('Created', readonly=True) create_uid = fields.Many2one('res.users', string='Creator', readonly=True) def _send_badge(self): """Send a notification to a user for receiving a badge Does not verify constrains on badge granting. The users are added to the owner_ids (create badge_user if needed) The stats counters are incremented :param ids: list(int) of badge users that will receive the badge """ template = self.env.ref('gamification.email_template_badge_received') for badge_user in self: self.env['mail.thread'].message_post_with_template( template.id, model=badge_user._name, res_id=badge_user.id, composition_mode='mass_mail', partner_ids=badge_user.user_id.partner_id.ids, ) return True @api.model def create(self, vals): self.env['gamification.badge'].browse( vals['badge_id']).check_granting() return super(BadgeUser, self).create(vals)
class StockQuant(models.Model): _inherit = 'stock.quant' removal_date = fields.Datetime(related='lot_id.removal_date', store=True) @api.model def _get_removal_strategy_order(self, removal_strategy): if removal_strategy == 'fefo': return 'removal_date, in_date, id' return super(StockQuant, self)._get_removal_strategy_order(removal_strategy)
class StockAgeingWizard(models.TransientModel): _name = 'stock.ageing.wizard' _description = 'Wizard that opens the stock ageing' company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.user.company_id) branch_id = fields.Many2one( 'res.branch', string="Branch", default=lambda self: self.env.user.default_branch_id) warehouse_ids = fields.Many2many("stock.warehouse", string="Warehouse") location_ids = fields.Many2many("stock.location", string='Location', domain="[('usage', '=', 'internal')]") product_category_ids = fields.Many2many("product.category", string="Product Category") product_ids = fields.Many2many('product.product', string='Product', domain="[('type', '=', 'product')]") period_length = fields.Integer(string='Period Length (days)', default=30) date = fields.Datetime(string="Date", help="Choose a date to get the inventory ageing " "report", default=fields.Datetime.now()) @api.multi def print_report(self): """ To get the Stock Ageing report and print the report @return : return stock ageing report """ datas = {'ids': self._context.get('active_ids', [])} res = self.read([ 'company_id', 'branch_id', 'warehouse_ids', 'location_ids', 'product_category_ids', 'product_ids', 'period_length', 'date' ]) for ageing_dict in res: res = res and res[0] or {} res['company_id'] = ageing_dict['company_id'] and\ ageing_dict['company_id'][0] or False res['branch_id'] = ageing_dict['branch_id'] and \ ageing_dict['branch_id'][0] or False res['warehouse_id'] = ageing_dict['warehouse_ids'] res['location_id'] = ageing_dict['location_ids'] res['product_category_id'] = ageing_dict['product_category_ids'] res['product_id'] = ageing_dict['product_ids'] res['period_length'] = ageing_dict['period_length'] or False res['date'] = ageing_dict['date'] or False datas['form'] = res return self.env.ref( 'stock_ageing_report.action_stock_ageing_report' '').report_action(self, data=datas)
class MrpWorkcenterProductivity(models.Model): _name = "mrp.workcenter.productivity" _description = "Workcenter Productivity Log" _order = "id desc" _rec_name = "loss_id" workcenter_id = fields.Many2one('mrp.workcenter', "Work Center", required=True) workorder_id = fields.Many2one('mrp.workorder', 'Work Order') user_id = fields.Many2one( 'res.users', "User", default=lambda self: self.env.uid) loss_id = fields.Many2one( 'mrp.workcenter.productivity.loss', "Loss Reason", ondelete='restrict', required=True) loss_type = fields.Selection( "Effectiveness", related='loss_id.loss_type', store=True) description = fields.Text('Description') date_start = fields.Datetime('Start Date', default=fields.Datetime.now, required=True) date_end = fields.Datetime('End Date') duration = fields.Float('Duration', compute='_compute_duration', store=True) @api.depends('date_end', 'date_start') def _compute_duration(self): for blocktime in self: if blocktime.date_end: d1 = fields.Datetime.from_string(blocktime.date_start) d2 = fields.Datetime.from_string(blocktime.date_end) diff = d2 - d1 if (blocktime.loss_type not in ('productive', 'performance')) and blocktime.workcenter_id.resource_calendar_id: r = blocktime.workcenter_id.resource_calendar_id.get_work_hours_count(d1, d2, blocktime.workcenter_id.resource_id.id) blocktime.duration = round(r * 60, 2) else: blocktime.duration = round(diff.total_seconds() / 60.0, 2) else: blocktime.duration = 0.0 @api.multi def button_block(self): self.ensure_one() self.workcenter_id.order_ids.end_all()
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 WizIndentLine(models.TransientModel): _name = 'wiz.indent.line' _description = "Wizard Indent Line" @api.depends('purchase_indent_line_id', 'purchase_indent_line_id.product_qty', 'purchase_indent_line_id.requisition_qty') @api.multi def _compute_get_rem_qty(self): for line_id in self: remaining_qty = 0.0 if line_id.purchase_indent_line_id: remaining_qty = \ line_id.purchase_indent_line_id.product_qty - \ line_id.purchase_indent_line_id.requisition_qty line_id.remaining_qty = remaining_qty purchase_indent_ids = fields.Many2many('purchase.indent', string='Purchase Indent') name = fields.Text(string='Description', required=True) sequence = fields.Integer(string='Sequence', default=10) product_qty = fields.Float(string='Quantity', digits=dp.get_precision('Discount')) expected_date = fields.Datetime(string='Expected Date', index=True) product_uom = fields.Many2one('product.uom', string='Product Unit of Measure') product_id = fields.Many2one('product.product', string='Product', domain=[('purchase_ok', '=', True)], change_default=True, required=True) requisition_qty = fields.Float(string="Requisition Quantity", digits=dp.get_precision('Discount')) wizard_indent_id = fields.Many2one('wiz.requisition.request', 'Wiz Requisition Request') partner_id = fields.Many2one('res.partner', string='Partner') price_unit = fields.Float(string='Unit Price', digits=dp.get_precision('Product Price')) taxes_id = fields.Many2many( 'account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)]) purchase_indent_line_id = fields.Many2one('purchase.indent.line', string="Indent Line Ref") remaining_qty = fields.Float(compute='_compute_get_rem_qty', string='Remaining Quantity', store=True) order_type = fields.Selection(related='wizard_indent_id.order_type', string='Order Type')
class ProductPriceHistory(models.Model): """ Keep track of the ``product.template`` standard prices as they are changed. """ _name = 'product.price.history' _rec_name = 'datetime' _order = 'datetime desc' def _get_default_company_id(self): return self._context.get('force_company', self.env.user.company_id.id) company_id = fields.Many2one('res.company', string='Company', default=_get_default_company_id, required=True) product_id = fields.Many2one('product.product', 'Product', ondelete='cascade', required=True) datetime = fields.Datetime('Date', default=fields.Datetime.now) cost = fields.Float('Cost', digits=dp.get_precision('Product Price'))
class IrLogging(models.Model): _name = 'ir.logging' _order = 'id DESC' create_date = fields.Datetime(readonly=True) create_uid = fields.Integer(string='Uid', readonly=True) # Integer not m2o is intentionnal name = fields.Char(required=True) type = fields.Selection([('client', 'Client'), ('server', 'Server')], required=True, index=True) dbname = fields.Char(string='Database Name', index=True) level = fields.Char(index=True) message = fields.Text(required=True) path = fields.Char(required=True) func = fields.Char(string='Function', required=True) line = fields.Char(required=True)
class ResUsersPassHistory(models.Model): _name = 'res.users.pass.history' _description = 'Res Users Password History' _order = 'user_id, date desc' user_id = fields.Many2one( string='User', comodel_name='res.users', ondelete='cascade', index=True, ) password_crypt = fields.Char(string='Encrypted Password', ) date = fields.Datetime( default=lambda s: fields.Datetime.now(), index=True, )
class MassMailingReport(models.Model): _name = 'mail.statistics.report' _auto = False _description = 'Mass Mailing Statistics' scheduled_date = fields.Datetime(stirng='Scheduled Date', readonly=True) name = fields.Char(string='Mass Mail', readonly=True) campaign = fields.Char(string='Mass Mail Campaign', readonly=True) sent = fields.Integer(readonly=True) delivered = fields.Integer(readonly=True) opened = fields.Integer(readonly=True) bounced = fields.Integer(readonly=True) replied = fields.Integer(readonly=True) state = fields.Selection([('draft', 'Draft'), ('test', 'Tested'), ('done', 'Sent')], string='Status', readonly=True) email_from = fields.Char('From', readonly=True) @api.model_cr def init(self): """Mass Mail Statistical Report: based on mail.mail.statistics that models the various statistics collected for each mailing, and mail.mass_mailing model that models the various mailing performed. """ tools.drop_view_if_exists(self.env.cr, 'mail_statistics_report') self.env.cr.execute(""" CREATE OR REPLACE VIEW mail_statistics_report AS ( SELECT min(ms.id) as id, ms.scheduled as scheduled_date, utm_source.name as name, utm_campaign.name as campaign, count(ms.bounced) as bounced, count(ms.sent) as sent, (count(ms.sent) - count(ms.bounced)) as delivered, count(ms.opened) as opened, count(ms.replied) as replied, mm.state, mm.email_from FROM mail_mail_statistics as ms left join mail_mass_mailing as mm ON (ms.mass_mailing_id=mm.id) left join mail_mass_mailing_campaign as mc ON (ms.mass_mailing_campaign_id=mc.id) left join utm_campaign as utm_campaign ON (mc.campaign_id = utm_campaign.id) left join utm_source as utm_source ON (mm.source_id = utm_source.id) GROUP BY ms.scheduled, utm_source.name, utm_campaign.name, mm.state, mm.email_from )""")
class IrModelFieldsAnonymizationHistory(models.Model): _name = 'ir.model.fields.anonymization.history' _order = "date desc" date = fields.Datetime(required=True, readonly=True) field_ids = fields.Many2many('ir.model.fields.anonymization', 'anonymized_field_to_history_rel', 'field_id', 'history_id', string='Fields', readonly=True) state = fields.Selection(selection=ANONYMIZATION_HISTORY_STATE, string='Status', required=True, readonly=True) direction = fields.Selection(selection=ANONYMIZATION_DIRECTION, required=True, readonly=True) msg = fields.Text('Message', readonly=True) filepath = fields.Char('File path', readonly=True)
class Meeting(models.Model): _inherit = "calendar.event" oe_update_date = fields.Datetime('actpy Update Date') @api.model def get_fields_need_update_google(self): recurrent_fields = self._get_recurrent_fields() return recurrent_fields + [ 'name', 'description', 'allday', 'start', 'date_end', 'stop', 'attendee_ids', 'alarm_ids', 'location', 'privacy', 'active', 'start_date', 'start_datetime', 'stop_date', 'stop_datetime' ] @api.multi def write(self, values): sync_fields = set(self.get_fields_need_update_google()) if ( set(values) and sync_fields ) and 'oe_update_date' not in values and 'NewMeeting' not in self._context: values['oe_update_date'] = fields.Datetime.now() return super(Meeting, self).write(values) @api.multi def copy(self, default=None): default = default or {} if default.get('write_type', False): del default['write_type'] elif default.get('recurrent_id', False): default['oe_update_date'] = fields.Datetime.now() else: default['oe_update_date'] = False return super(Meeting, self).copy(default) @api.multi def unlink(self, can_be_deleted=False): return super(Meeting, self).unlink(can_be_deleted=can_be_deleted)
class ReleasePlanning(models.Model): _name = "release.planning" _inherit = ['ir.branch.company.mixin', 'mail.thread'] name = fields.Char(string="Planning Name", track_visibility="onchange") release_date = fields.Datetime( string="Release Date", track_visibility="onchange") sprint_id = fields.Many2one( 'project.sprint', string="Sprint", track_visibility="onchange") priority = fields.Selection([ ('low', 'Low'), ('medium', 'Medium'), ('high', 'High')], string="Priority", default='low', track_visibility="onchange") velocity = fields.Integer( related="sprint_id.estimated_velocity", string="Sprint Velocity", track_visibility="onchange", store=True) task_id = fields.One2many( "project.task", "release_planning_id", string="Task") @api.onchange('sprint_id') def onchange_project(self): if self.sprint_id and self.sprint_id.project_id.branch_id: self.branch_id = self.sprint_id.project_id.branch_id
class StockQuantityHistory(models.TransientModel): _name = 'stock.quantity.history' _description = 'Stock Quantity History' compute_at_date = fields.Selection( [(0, 'Current Inventory'), (1, 'At a Specific Date')], string="Compute", help= "Choose to analyze the current inventory or from a specific date in the past." ) date = fields.Datetime( 'Inventory at Date', help="Choose a date to get the inventory at that date", default=fields.Datetime.now) def open_table(self): self.ensure_one() if self.compute_at_date: tree_view_id = self.env.ref('stock.view_stock_product_tree').id form_view_id = self.env.ref( 'stock.product_form_view_procurement_button').id # We pass `to_date` in the context so that `qty_available` will be computed across # moves until date. action = { 'type': 'ir.actions.act_window', 'views': [(tree_view_id, 'tree'), (form_view_id, 'form')], 'view_mode': 'tree,form', 'name': _('Products'), 'res_model': 'product.product', 'context': dict(self.env.context, to_date=self.date), } return action else: self.env['stock.quant']._merge_quants() return self.env.ref('stock.quantsact').read()[0]
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(domain=lambda self: [('model', '=', self._name), ('message_type', 'in', ['email', 'comment'])]) # history create_date = fields.Datetime('Asked on', index=True, readonly=True) create_uid = fields.Many2one('res.users', string='Created by', index=True, readonly=True) write_date = fields.Datetime('Update on', index=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', index=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('Total 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', index=True) 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', search='_search_can_view') 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') website_id = fields.Many2one('website', string="Website", default=lambda self: self.env.ref('website.default_website')) def _search_can_view(self, operator, value): if operator not in ('=', '!=', '<>'): raise ValueError('Invalid operator: %s' % (operator,)) if not value: operator = operator == "=" and '!=' or '=' value = True if self._uid == SUPERUSER_ID: return [(1, '=', 1)] user = self.env['res.users'].browse(self._uid) req = """ SELECT p.id FROM forum_post p LEFT JOIN res_users u ON p.create_uid = u.id LEFT JOIN forum_forum f ON p.forum_id = f.id WHERE (p.create_uid = %s and f.karma_close_own <= %s) or (p.create_uid != %s and f.karma_close_all <= %s) or ( u.karma > 0 and (p.active or p.create_uid = %s) ) """ op = operator == "=" and "inselect" or "not inselect" # don't use param named because orm will add other param (test_active, ...) return [('id', op, (req, (user.id, user.karma, user.id, user.karma, user.id)))] @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 pycompat.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 and (post_sudo.active or post_sudo.create_uid == user)) 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 ValidationError(_('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): match = re.escape(match) # replace parenthesis or special char in regex 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.sudo().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): trusted_keys = ['active', 'is_correct', 'tag_ids'] # fields where security is checked manually 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']: if any(not post.can_close for post in self): raise KarmaError('Not enough karma to close or reopen a post.') trusted_keys += ['state', 'closed_uid', 'closed_date', 'closed_reason_id'] elif vals['state'] == 'flagged': if any(not post.can_flag for post in self): raise KarmaError('Not enough karma to flag a post.') trusted_keys += ['state', 'flag_user_id'] 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 'tag_ids' in vals: tag_ids = set(tag.get('id') for tag in self.resolve_2many_commands('tag_ids', vals['tag_ids'])) if any(set(post.tag_ids) != tag_ids for post in self) and any(self.env.user.karma < post.forum_id.karma_edit_retag for post in self): raise KarmaError(_('Not enough karma to retag.')) if any(key not in trusted_keys for key in vals) 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) if 'active' in vals: answers = self.env['forum.post'].with_context(active_test=False).search([('parent_id', 'in', self.ids)]) if answers: answers.write({'active': vals['active']}) return res @api.multi def post_notification(self): for post in self: tag_partners = post.tag_ids.mapped('message_partner_ids') tag_channels = post.tag_ids.mapped('message_channel_ids') if post.state == 'active' and post.parent_id: post.parent_id.message_post_with_view( 'website_forum.forum_post_template_new_answer', subject=_('Re: %s') % post.parent_id.name, partner_ids=[(4, p.id) for p in tag_partners], channel_ids=[(4, c.id) for c in tag_channels], subtype_id=self.env['ir.model.data'].xmlid_to_res_id('website_forum.mt_answer_new')) elif post.state == 'active' and not post.parent_id: post.message_post_with_view( 'website_forum.forum_post_template_new_question', subject=post.name, partner_ids=[(4, p.id) for p in tag_partners], channel_ids=[(4, c.id) for c in tag_channels], subtype_id=self.env['ir.model.data'].xmlid_to_res_id('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 | tag_partners partners = partners.filtered(lambda partner: partner.user_ids and any(user.karma >= post.forum_id.karma_moderate for user in partner.user_ids)) post.message_post_with_view( 'website_forum.forum_post_template_validation', subject=post.name, partner_ids=partners.ids, subtype_id=self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note')) 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) karma = post.forum_id.karma_gen_answer_flagged if post.closed_reason_id == reason_spam: # If first post, increase the karma to add count_post = post.search_count([('parent_id', '=', False), ('forum_id', '=', post.forum_id.id), ('create_uid', '=', post.create_uid.id)]) if count_post == 1: karma *= 10 post.create_uid.sudo().add_karma(karma * -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) karma = post.forum_id.karma_gen_answer_flagged if reason_id == reason_spam: # If first post, increase the karma to remove count_post = post.search_count([('parent_id', '=', False), ('forum_id', '=', post.forum_id.id), ('create_uid', '=', post.create_uid.id)]) if count_post == 1: karma *= 10 post.create_uid.sudo().add_karma(karma) 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.multi 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. """ self.ensure_one() if not self.parent_id: return self.env['mail.message'] # 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, sanitize_attributes=True, strip_style=True, strip_classes=True), 'message_type': 'comment', 'subtype': 'mail.mt_comment', 'date': self.create_date, } new_message = question.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, access_uid=None): """ 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', 'target_type': 'public', 'res_id': self.id, } @api.multi def _notification_recipients(self, message, groups): groups = super(Post, self)._notification_recipients(message, groups) for group_name, group_method, group_data in groups: group_data['has_button_access'] = True return groups @api.multi @api.returns('self', lambda value: value.id) def message_post(self, message_type='notification', subtype=None, **kwargs): question_followers = self.env['res.partner'] if self.ids and message_type == 'comment': # user comments have a restriction on karma # add followers of comments on the parent post if self.parent_id: partner_ids = kwargs.get('partner_ids', []) comment_subtype = self.sudo().env.ref('mail.mt_comment') question_followers = self.env['mail.followers'].sudo().search([ ('res_model', '=', self._name), ('res_id', '=', self.parent_id.id), ('partner_id', '!=', False), ]).filtered(lambda fol: comment_subtype in fol.subtype_ids).mapped('partner_id') partner_ids += [(4, partner.id) for partner in question_followers] kwargs['partner_ids'] = partner_ids self.ensure_one() if not self.can_comment: raise KarmaError('Not enough karma to comment') if not kwargs.get('record_name') and self.parent_id: kwargs['record_name'] = self.parent_id.name return super(Post, self).message_post(message_type=message_type, subtype=subtype, **kwargs) @api.multi def message_get_message_notify_values(self, message, message_values): """ Override to avoid keeping all notified recipients of a comment. We avoid tracking needaction on post comments. Only emails should be sufficient. """ if message.message_type == 'comment': return { 'needaction_partner_ids': [], 'partner_ids': [], } return {}