Ejemplo n.º 1
0
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
Ejemplo n.º 2
0
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
Ejemplo n.º 3
0
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
Ejemplo n.º 4
0
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)
Ejemplo n.º 5
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)
Ejemplo n.º 6
0
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)
Ejemplo n.º 7
0
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.')]
Ejemplo n.º 8
0
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
Ejemplo n.º 9
0
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)
Ejemplo n.º 10
0
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
Ejemplo n.º 11
0
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()
Ejemplo n.º 12
0
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)
Ejemplo n.º 13
0
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
Ejemplo n.º 14
0
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'
    )
Ejemplo n.º 15
0
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)
Ejemplo n.º 16
0
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)
Ejemplo n.º 17
0
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)
Ejemplo n.º 18
0
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)
Ejemplo n.º 19
0
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()
Ejemplo n.º 20
0
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()
Ejemplo n.º 21
0
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')
Ejemplo n.º 22
0
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'))
Ejemplo n.º 23
0
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)
Ejemplo n.º 24
0
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,
    )
Ejemplo n.º 25
0
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
            )""")
Ejemplo n.º 26
0
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)
Ejemplo n.º 27
0
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)
Ejemplo n.º 28
0
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
Ejemplo n.º 29
0
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]
Ejemplo n.º 30
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 {}