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 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 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('gecoerp.sql_db'): presence.write(values) # avoid TransactionRollbackError self.env.cr.commit() # TODO : check if still necessary
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 GECOERP Google Internal ID' )
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('GECOERP 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 CompanyDependent(models.Model): _name = 'test_new_api.company' foo = fields.Char(company_dependent=True) date = fields.Date(company_dependent=True) moment = fields.Datetime(company_dependent=True) tag_id = fields.Many2one('test_new_api.multi.tag', company_dependent=True)
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 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 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 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 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 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 ImLivechatReportOperator(models.Model): """ Livechat Support Report on the Operator """ _name = "im_livechat.report.operator" _description = "Livechat Support Report" _order = 'livechat_channel_id, partner_id' _auto = False partner_id = fields.Many2one('res.partner', 'Operator', readonly=True) livechat_channel_id = fields.Many2one('im_livechat.channel', 'Channel', readonly=True) nbr_channel = fields.Integer('# of Sessions', readonly=True, group_operator="sum", help="Number of conversation") channel_id = fields.Many2one('mail.channel', 'Conversation', readonly=True) start_date = fields.Datetime('Start Date of session', readonly=True, help="Start date of the conversation") time_to_answer = fields.Float( 'Time to answer', digits=(16, 2), readonly=True, group_operator="avg", help="Average time to give the first answer to the visitor") duration = fields.Float('Average duration', digits=(16, 2), readonly=True, group_operator="avg", help="Duration of the conversation (in seconds)") @api.model_cr def init(self): # Note : start_date_hour must be remove when the read_group will allow grouping on the hour of a datetime. Don't forget to change the view ! tools.drop_view_if_exists(self.env.cr, 'im_livechat_report_operator') self.env.cr.execute(""" CREATE OR REPLACE VIEW im_livechat_report_operator AS ( SELECT row_number() OVER () AS id, P.id as partner_id, L.id as livechat_channel_id, count(C.id) as nbr_channel, C.id as channel_id, C.create_date as start_date, EXTRACT('epoch' FROM (max((SELECT (max(M.create_date)) FROM mail_message M JOIN mail_message_mail_channel_rel R ON (R.mail_message_id = M.id) WHERE R.mail_channel_id = C.id))-C.create_date)) as duration, EXTRACT('epoch' from ((SELECT min(M.create_date) FROM mail_message M, mail_message_mail_channel_rel R WHERE M.author_id=P.id AND R.mail_channel_id = C.id AND R.mail_message_id = M.id)-(SELECT min(M.create_date) FROM mail_message M, mail_message_mail_channel_rel R WHERE M.author_id IS NULL AND R.mail_channel_id = C.id AND R.mail_message_id = M.id))) as time_to_answer FROM im_livechat_channel_im_user O JOIN res_users U ON (O.user_id = U.id) JOIN res_partner P ON (U.partner_id = P.id) LEFT JOIN im_livechat_channel L ON (L.id = O.channel_id) LEFT JOIN mail_channel C ON (C.livechat_channel_id = L.id) GROUP BY P.id, L.id, C.id, C.create_date ) """)
class Project(models.Model): _inherit = "project.project" # This method should be called once a day by the scheduler @api.model def _send_rating_all(self): projects = self.search([('rating_status', '=', 'periodic'), ('rating_request_deadline', '<=', fields.Datetime.now())]) projects.mapped('task_ids')._send_task_rating_mail() projects._compute_rating_request_deadline() @api.depends('percentage_satisfaction_task') def _compute_percentage_satisfaction_project(self): domain = [('create_date', '>=', fields.Datetime.to_string(fields.datetime.now() - timedelta(days=30)))] for project in self: activity = project.tasks.rating_get_grades(domain) project.percentage_satisfaction_project = activity['great'] * 100 / sum(activity.values()) if sum(activity.values()) else -1 @api.one @api.depends('tasks.rating_ids.rating') def _compute_percentage_satisfaction_task(self): activity = self.tasks.rating_get_grades() self.percentage_satisfaction_task = activity['great'] * 100 / sum(activity.values()) if sum(activity.values()) else -1 percentage_satisfaction_task = fields.Integer( compute='_compute_percentage_satisfaction_task', string="Happy % on Task", store=True, default=-1) percentage_satisfaction_project = fields.Integer( compute="_compute_percentage_satisfaction_project", string="Happy % on Project", store=True, default=-1) rating_request_deadline = fields.Datetime(compute='_compute_rating_request_deadline', store=True) rating_status = fields.Selection([('stage', 'Rating when changing stage'), ('periodic', 'Periodical Rating'), ('no','No rating')], 'Customer(s) Ratings', help="How to get the customer's feedbacks?\n" "- Rating when changing stage: Email will be sent when a task/issue is pulled in another stage\n" "- Periodical Rating: Email will be sent periodically\n\n" "Don't forget to set up the mail templates on the stages for which you want to get the customer's feedbacks.", default="no", required=True) rating_status_period = fields.Selection([ ('daily', 'Daily'), ('weekly', 'Weekly'), ('bimonthly', 'Twice a Month'), ('monthly', 'Once a Month'), ('quarterly', 'Quarterly'), ('yearly', 'Yearly') ], 'Rating Frequency') @api.depends('rating_status', 'rating_status_period') def _compute_rating_request_deadline(self): periods = {'daily': 1, 'weekly': 7, 'bimonthly': 15, 'monthly': 30, 'quarterly': 90, 'yearly': 365} for project in self: project.rating_request_deadline = fields.datetime.now() + timedelta(days=periods.get(project.rating_status_period, 0)) @api.multi def action_view_all_rating(self): """ return the action to see all the rating of the project, and activate default filters """ action = self.env['ir.actions.act_window'].for_xml_id('rating_project', 'rating_rating_action_view_project_rating') action['name'] = _('Ratings of %s') % (self.name,) action_context = safe_eval(action['context']) if action['context'] else {} action_context.update(self._context) action_context['search_default_rating_tasks'] = 1 return dict(action, context=action_context)
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 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 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 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 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 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 Meeting(models.Model): _inherit = "calendar.event" oe_update_date = fields.Datetime('GECOERP 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 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 MrpWorkorder(models.Model): _name = 'mrp.workorder' _description = 'Work Order' _inherit = ['mail.thread'] name = fields.Char( 'Work Order', required=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) workcenter_id = fields.Many2one( 'mrp.workcenter', 'Work Center', required=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) working_state = fields.Selection( 'Workcenter Status', related='workcenter_id.working_state', help='Technical: used in views only') production_id = fields.Many2one( 'mrp.production', 'Manufacturing Order', index=True, ondelete='cascade', required=True, track_visibility='onchange', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) product_id = fields.Many2one( 'product.product', 'Product', related='production_id.product_id', readonly=True, help='Technical: used in views only.', store=True) product_uom_id = fields.Many2one( 'product.uom', 'Unit of Measure', related='production_id.product_uom_id', readonly=True, help='Technical: used in views only.') production_availability = fields.Selection( 'Stock Availability', readonly=True, related='production_id.availability', store=True, help='Technical: used in views and domains only.') production_state = fields.Selection( 'Production State', readonly=True, related='production_id.state', help='Technical: used in views only.') product_tracking = fields.Selection( 'Product Tracking', related='production_id.product_id.tracking', help='Technical: used in views only.') qty_production = fields.Float('Original Production Quantity', readonly=True, related='production_id.product_qty') qty_remaining = fields.Float('Quantity To Be Produced', compute='_compute_qty_remaining', digits=dp.get_precision('Product Unit of Measure')) qty_produced = fields.Float( 'Quantity', default=0.0, readonly=True, digits=dp.get_precision('Product Unit of Measure'), help="The number of products already handled by this work order") qty_producing = fields.Float( 'Currently Produced Quantity', default=1.0, digits=dp.get_precision('Product Unit of Measure'), states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) is_produced = fields.Boolean(string="Has Been Produced", compute='_compute_is_produced') state = fields.Selection([ ('pending', 'Pending'), ('ready', 'Ready'), ('progress', 'In Progress'), ('done', 'Finished'), ('cancel', 'Cancelled')], string='Status', default='pending') date_planned_start = fields.Datetime( 'Scheduled Date Start', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) date_planned_finished = fields.Datetime( 'Scheduled Date Finished', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) date_start = fields.Datetime( 'Effective Start Date', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) date_finished = fields.Datetime( 'Effective End Date', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) duration_expected = fields.Float( 'Expected Duration', digits=(16, 2), states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="Expected duration (in minutes)") duration = fields.Float( 'Real Duration', compute='_compute_duration', readonly=True, store=True) duration_unit = fields.Float( 'Duration Per Unit', compute='_compute_duration', readonly=True, store=True) duration_percent = fields.Integer( 'Duration Deviation (%)', compute='_compute_duration', group_operator="avg", readonly=True, store=True) operation_id = fields.Many2one( 'mrp.routing.workcenter', 'Operation') # Should be used differently as BoM can change in the meantime worksheet = fields.Binary( 'Worksheet', related='operation_id.worksheet', readonly=True) move_raw_ids = fields.One2many( 'stock.move', 'workorder_id', 'Moves') move_line_ids = fields.One2many( 'stock.move.line', 'workorder_id', 'Moves to Track', domain=[('done_wo', '=', True)], help="Inventory moves for which you must scan a lot number at this work order") active_move_line_ids = fields.One2many( 'stock.move.line', 'workorder_id', domain=[('done_wo', '=', False)]) final_lot_id = fields.Many2one( 'stock.production.lot', 'Lot/Serial Number', domain="[('product_id', '=', product_id)]", states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) tracking = fields.Selection(related='production_id.product_id.tracking') time_ids = fields.One2many( 'mrp.workcenter.productivity', 'workorder_id') is_user_working = fields.Boolean( 'Is the Current User Working', compute='_compute_is_user_working', help="Technical field indicating whether the current user is working. ") production_messages = fields.Html('Workorder Message', compute='_compute_production_messages') next_work_order_id = fields.Many2one('mrp.workorder', "Next Work Order") scrap_ids = fields.One2many('stock.scrap', 'workorder_id') scrap_count = fields.Integer(compute='_compute_scrap_move_count', string='Scrap Move') production_date = fields.Datetime('Production Date', related='production_id.date_planned_start', store=True) color = fields.Integer('Color', compute='_compute_color') capacity = fields.Float( 'Capacity', default=1.0, help="Number of pieces that can be produced in parallel.") @api.multi def name_get(self): return [(wo.id, "%s - %s - %s" % (wo.production_id.name, wo.product_id.name, wo.name)) for wo in self] @api.one @api.depends('production_id.product_qty', 'qty_produced') def _compute_is_produced(self): rounding = self.production_id.product_uom_id.rounding self.is_produced = float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) >= 0 @api.one @api.depends('time_ids.duration', 'qty_produced') def _compute_duration(self): self.duration = sum(self.time_ids.mapped('duration')) self.duration_unit = round(self.duration / max(self.qty_produced, 1), 2) # rounding 2 because it is a time if self.duration_expected: self.duration_percent = 100 * (self.duration_expected - self.duration) / self.duration_expected else: self.duration_percent = 0 def _compute_is_user_working(self): """ Checks whether the current user is working """ for order in self: if order.time_ids.filtered(lambda x: (x.user_id.id == self.env.user.id) and (not x.date_end) and (x.loss_type in ('productive', 'performance'))): order.is_user_working = True else: order.is_user_working = False @api.depends('production_id', 'workcenter_id', 'production_id.bom_id') def _compute_production_messages(self): ProductionMessage = self.env['mrp.message'] for workorder in self: domain = [ ('valid_until', '>=', fields.Date.today()), '|', ('workcenter_id', '=', False), ('workcenter_id', '=', workorder.workcenter_id.id), '|', '|', '|', ('product_id', '=', workorder.product_id.id), '&', ('product_id', '=', False), ('product_tmpl_id', '=', workorder.product_id.product_tmpl_id.id), ('bom_id', '=', workorder.production_id.bom_id.id), ('routing_id', '=', workorder.operation_id.routing_id.id)] messages = ProductionMessage.search(domain).mapped('message') workorder.production_messages = "<br/>".join(messages) or False @api.multi def _compute_scrap_move_count(self): data = self.env['stock.scrap'].read_group([('workorder_id', 'in', self.ids)], ['workorder_id'], ['workorder_id']) count_data = dict((item['workorder_id'][0], item['workorder_id_count']) for item in data) for workorder in self: workorder.scrap_count = count_data.get(workorder.id, 0) @api.multi @api.depends('date_planned_finished', 'production_id.date_planned_finished') def _compute_color(self): late_orders = self.filtered(lambda x: x.production_id.date_planned_finished and x.date_planned_finished > x.production_id.date_planned_finished) for order in late_orders: order.color = 4 for order in (self - late_orders): order.color = 2 @api.onchange('qty_producing') def _onchange_qty_producing(self): """ Update stock.move.lot records, according to the new qty currently produced. """ moves = self.move_raw_ids.filtered(lambda move: move.state not in ('done', 'cancel') and move.product_id.tracking != 'none' and move.product_id.id != self.production_id.product_id.id) for move in moves: move_lots = self.active_move_line_ids.filtered(lambda move_lot: move_lot.move_id == move) if not move_lots: continue rounding = move.product_uom.rounding new_qty = float_round(move.unit_factor * self.qty_producing, precision_rounding=rounding) if move.product_id.tracking == 'lot': move_lots[0].product_qty = new_qty move_lots[0].qty_done = new_qty elif move.product_id.tracking == 'serial': # Create extra pseudo record qty_todo = float_round(new_qty - sum(move_lots.mapped('qty_done')), precision_rounding=rounding) if float_compare(qty_todo, 0.0, precision_rounding=rounding) > 0: while float_compare(qty_todo, 0.0, precision_rounding=rounding) > 0: self.active_move_line_ids += self.env['stock.move.line'].new({ 'move_id': move.id, 'product_id': move.product_id.id, 'lot_id': False, 'product_uom_qty': 0.0, 'product_uom_id': move.product_uom.id, 'qty_done': min(1.0, qty_todo), 'workorder_id': self.id, 'done_wo': False, 'location_id': move.location_id.id, 'location_dest_id': move.location_dest_id.id, 'date': move.date, }) qty_todo -= 1 elif float_compare(qty_todo, 0.0, precision_rounding=rounding) < 0: qty_todo = abs(qty_todo) for move_lot in move_lots: if float_compare(qty_todo, 0, precision_rounding=rounding) <= 0: break if not move_lot.lot_id and float_compare(qty_todo, move_lot.qty_done, precision_rounding=rounding) >= 0: qty_todo = float_round(qty_todo - move_lot.qty_done, precision_rounding=rounding) self.active_move_line_ids -= move_lot # Difference operator else: #move_lot.product_qty = move_lot.product_qty - qty_todo if float_compare(move_lot.qty_done - qty_todo, 0, precision_rounding=rounding) == 1: move_lot.qty_done = move_lot.qty_done - qty_todo else: move_lot.qty_done = 0 qty_todo = 0 @api.multi def write(self, values): if ('date_planned_start' in values or 'date_planned_finished' in values) and any(workorder.state == 'done' for workorder in self): raise UserError(_('You can not change the finished work order.')) return super(MrpWorkorder, self).write(values) def _generate_lot_ids(self): """ Generate stock move lines """ self.ensure_one() MoveLine = self.env['stock.move.line'] tracked_moves = self.move_raw_ids.filtered( lambda move: move.state not in ('done', 'cancel') and move.product_id.tracking != 'none' and move.product_id != self.production_id.product_id and move.bom_line_id) for move in tracked_moves: qty = move.unit_factor * self.qty_producing if move.product_id.tracking == 'serial': while float_compare(qty, 0.0, precision_rounding=move.product_uom.rounding) > 0: MoveLine.create({ 'move_id': move.id, 'product_uom_qty': 0, 'product_uom_id': move.product_uom.id, 'qty_done': min(1, qty), 'production_id': self.production_id.id, 'workorder_id': self.id, 'product_id': move.product_id.id, 'done_wo': False, 'location_id': move.location_id.id, 'location_dest_id': move.location_dest_id.id, }) qty -= 1 else: MoveLine.create({ 'move_id': move.id, 'product_uom_qty': 0, 'product_uom_id': move.product_uom.id, 'qty_done': qty, 'product_id': move.product_id.id, 'production_id': self.production_id.id, 'workorder_id': self.id, 'done_wo': False, 'location_id': move.location_id.id, 'location_dest_id': move.location_dest_id.id, }) def _assign_default_final_lot_id(self): self.final_lot_id = self.env['stock.production.lot'].search([('use_next_on_work_order_id', '=', self.id)], order='create_date, id', limit=1) def _get_byproduct_move_line(self, by_product_move, quantity): return { 'move_id': by_product_move.id, 'product_id': by_product_move.product_id.id, 'product_uom_qty': quantity, 'product_uom_id': by_product_move.product_uom.id, 'qty_done': quantity, 'workorder_id': self.id, 'location_id': by_product_move.location_id.id, 'location_dest_id': by_product_move.location_dest_id.id, } @api.multi def record_production(self): self.ensure_one() if self.qty_producing <= 0: raise UserError(_('Please set the quantity you are currently producing. It should be different from zero.')) if (self.production_id.product_id.tracking != 'none') and not self.final_lot_id and self.move_raw_ids: raise UserError(_('You should provide a lot/serial number for the final product')) # Update quantities done on each raw material line # For each untracked component without any 'temporary' move lines, # (the new workorder tablet view allows registering consumed quantities for untracked components) # we assume that only the theoretical quantity was used for move in self.move_raw_ids: if move.has_tracking == 'none' and (move.state not in ('done', 'cancel')) and move.bom_line_id\ and move.unit_factor and not move.move_line_ids.filtered(lambda ml: not ml.done_wo): rounding = move.product_uom.rounding if self.product_id.tracking != 'none': qty_to_add = float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding) move._generate_consumed_move_line(qty_to_add, self.final_lot_id) elif len(move._get_move_lines()) < 2: move.quantity_done += float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding) else: move._set_quantity_done(move.quantity_done + float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding)) # Transfer quantities from temporary to final move lots or make them final for move_line in self.active_move_line_ids: # Check if move_line already exists if move_line.qty_done <= 0: # rounding... move_line.sudo().unlink() continue if move_line.product_id.tracking != 'none' and not move_line.lot_id: raise UserError(_('You should provide a lot/serial number for a component')) # Search other move_line where it could be added: lots = self.move_line_ids.filtered(lambda x: (x.lot_id.id == move_line.lot_id.id) and (not x.lot_produced_id) and (not x.done_move) and (x.product_id == move_line.product_id)) if lots: lots[0].qty_done += move_line.qty_done lots[0].lot_produced_id = self.final_lot_id.id move_line.sudo().unlink() else: move_line.lot_produced_id = self.final_lot_id.id move_line.done_wo = True # One a piece is produced, you can launch the next work order if self.next_work_order_id.state == 'pending': self.next_work_order_id.state = 'ready' self.move_line_ids.filtered( lambda move_line: not move_line.done_move and not move_line.lot_produced_id and move_line.qty_done > 0 ).write({ 'lot_produced_id': self.final_lot_id.id, 'lot_produced_qty': self.qty_producing }) # If last work order, then post lots used # TODO: should be same as checking if for every workorder something has been done? if not self.next_work_order_id: production_move = self.production_id.move_finished_ids.filtered( lambda x: (x.product_id.id == self.production_id.product_id.id) and (x.state not in ('done', 'cancel'))) if production_move.product_id.tracking != 'none': move_line = production_move.move_line_ids.filtered(lambda x: x.lot_id.id == self.final_lot_id.id) if move_line: move_line.product_uom_qty += self.qty_producing move_line.qty_done += self.qty_producing else: move_line.create({'move_id': production_move.id, 'product_id': production_move.product_id.id, 'lot_id': self.final_lot_id.id, 'product_uom_qty': self.qty_producing, 'product_uom_id': production_move.product_uom.id, 'qty_done': self.qty_producing, 'workorder_id': self.id, 'location_id': production_move.location_id.id, 'location_dest_id': production_move.location_dest_id.id, }) else: production_move.quantity_done += self.qty_producing if not self.next_work_order_id: for by_product_move in self.production_id.move_finished_ids.filtered(lambda x: (x.product_id.id != self.production_id.product_id.id) and (x.state not in ('done', 'cancel'))): if by_product_move.has_tracking != 'serial': values = self._get_byproduct_move_line(by_product_move, self.qty_producing * by_product_move.unit_factor) self.env['stock.move.line'].create(values) elif by_product_move.has_tracking == 'serial': qty_todo = by_product_move.product_uom._compute_quantity(self.qty_producing * by_product_move.unit_factor, by_product_move.product_id.uom_id) for i in range(0, int(float_round(qty_todo, precision_digits=0))): values = self._get_byproduct_move_line(by_product_move, 1) self.env['stock.move.line'].create(values) # Update workorder quantity produced self.qty_produced += self.qty_producing if self.final_lot_id: self.final_lot_id.use_next_on_work_order_id = self.next_work_order_id self.final_lot_id = False # Set a qty producing rounding = self.production_id.product_uom_id.rounding if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) >= 0: self.qty_producing = 0 elif self.production_id.product_id.tracking == 'serial': self._assign_default_final_lot_id() self.qty_producing = 1.0 self._generate_lot_ids() else: self.qty_producing = float_round(self.production_id.product_qty - self.qty_produced, precision_rounding=rounding) self._generate_lot_ids() if self.next_work_order_id and self.production_id.product_id.tracking != 'none': self.next_work_order_id._assign_default_final_lot_id() if float_compare(self.qty_produced, self.production_id.product_qty, precision_rounding=rounding) >= 0: self.button_finish() return True @api.multi def button_start(self): self.ensure_one() # As button_start is automatically called in the new view if self.state in ('done', 'cancel'): return True # Need a loss in case of the real time exceeding the expected timeline = self.env['mrp.workcenter.productivity'] if self.duration < self.duration_expected: loss_id = self.env['mrp.workcenter.productivity.loss'].search([('loss_type','=','productive')], limit=1) if not len(loss_id): raise UserError(_("You need to define at least one productivity loss in the category 'Productivity'. Create one from the Manufacturing app, menu: Configuration / Productivity Losses.")) else: loss_id = self.env['mrp.workcenter.productivity.loss'].search([('loss_type','=','performance')], limit=1) if not len(loss_id): raise UserError(_("You need to define at least one productivity loss in the category 'Performance'. Create one from the Manufacturing app, menu: Configuration / Productivity Losses.")) for workorder in self: if workorder.production_id.state != 'progress': workorder.production_id.write({ 'state': 'progress', 'date_start': datetime.now(), }) timeline.create({ 'workorder_id': workorder.id, 'workcenter_id': workorder.workcenter_id.id, 'description': _('Time Tracking: ')+self.env.user.name, 'loss_id': loss_id[0].id, 'date_start': datetime.now(), 'user_id': self.env.user.id }) return self.write({'state': 'progress', 'date_start': datetime.now(), }) @api.multi def button_finish(self): self.ensure_one() self.end_all() return self.write({'state': 'done', 'date_finished': fields.Datetime.now()}) @api.multi def end_previous(self, doall=False): """ @param: doall: This will close all open time lines on the open work orders when doall = True, otherwise only the one of the current user """ # TDE CLEANME timeline_obj = self.env['mrp.workcenter.productivity'] domain = [('workorder_id', 'in', self.ids), ('date_end', '=', False)] if not doall: domain.append(('user_id', '=', self.env.user.id)) not_productive_timelines = timeline_obj.browse() for timeline in timeline_obj.search(domain, limit=None if doall else 1): wo = timeline.workorder_id if wo.duration_expected <= wo.duration: if timeline.loss_type == 'productive': not_productive_timelines += timeline timeline.write({'date_end': fields.Datetime.now()}) else: maxdate = fields.Datetime.from_string(timeline.date_start) + relativedelta(minutes=wo.duration_expected - wo.duration) enddate = datetime.now() if maxdate > enddate: timeline.write({'date_end': enddate}) else: timeline.write({'date_end': maxdate}) not_productive_timelines += timeline.copy({'date_start': maxdate, 'date_end': enddate}) if not_productive_timelines: loss_id = self.env['mrp.workcenter.productivity.loss'].search([('loss_type', '=', 'performance')], limit=1) if not len(loss_id): raise UserError(_("You need to define at least one unactive productivity loss in the category 'Performance'. Create one from the Manufacturing app, menu: Configuration / Productivity Losses.")) not_productive_timelines.write({'loss_id': loss_id.id}) return True @api.multi def end_all(self): return self.end_previous(doall=True) @api.multi def button_pending(self): self.end_previous() return True @api.multi def button_unblock(self): for order in self: order.workcenter_id.unblock() return True @api.multi def action_cancel(self): return self.write({'state': 'cancel'}) @api.multi def button_done(self): if any([x.state in ('done', 'cancel') for x in self]): raise UserError(_('A Manufacturing Order is already done or cancelled!')) self.end_all() return self.write({'state': 'done', 'date_finished': datetime.now()}) @api.multi def button_scrap(self): self.ensure_one() return { 'name': _('Scrap'), 'view_type': 'form', 'view_mode': 'form', 'res_model': 'stock.scrap', 'view_id': self.env.ref('stock.stock_scrap_form_view2').id, 'type': 'ir.actions.act_window', 'context': {'default_workorder_id': self.id, 'default_production_id': self.production_id.id, 'product_ids': (self.production_id.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) | self.production_id.move_finished_ids.filtered(lambda x: x.state == 'done')).mapped('product_id').ids}, # 'context': {'product_ids': self.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')).mapped('product_id').ids + [self.production_id.product_id.id]}, 'target': 'new', } @api.multi def action_see_move_scrap(self): self.ensure_one() action = self.env.ref('stock.action_stock_scrap').read()[0] action['domain'] = [('workorder_id', '=', self.id)] return action @api.multi @api.depends('qty_production', 'qty_produced') def _compute_qty_remaining(self): for wo in self: wo.qty_remaining = float_round(wo.qty_production - wo.qty_produced, precision_rounding=wo.production_id.product_uom_id.rounding)
class ReportProjectTaskUser(models.Model): _name = "report.project.task.user" _description = "Tasks by user and project" _order = 'name desc, project_id' _auto = False name = fields.Char(string='Task Title', readonly=True) user_id = fields.Many2one('res.users', string='Assigned To', readonly=True) date_start = fields.Datetime(string='Assignation Date', readonly=True) date_end = fields.Datetime(string='Ending Date', readonly=True) date_deadline = fields.Date(string='Deadline', readonly=True) date_last_stage_update = fields.Datetime(string='Last Stage Update', readonly=True) project_id = fields.Many2one('project.project', string='Project', readonly=True) working_days_close = fields.Float(string='# Working Days to Close', digits=(16,2), readonly=True, group_operator="avg", help="Number of Working Days to close the task") working_days_open = fields.Float(string='# Working Days to Assign', digits=(16,2), readonly=True, group_operator="avg", help="Number of Working Days to Open the task") delay_endings_days = fields.Float(string='# Days to Deadline', digits=(16,2), readonly=True) nbr = fields.Integer('# of Tasks', readonly=True) # TDE FIXME master: rename into nbr_tasks priority = fields.Selection([ ('0', 'Low'), ('1', 'Normal'), ('2', 'High') ], size=1, readonly=True, string="Priority") state = fields.Selection([ ('normal', 'In Progress'), ('blocked', 'Blocked'), ('done', 'Ready for next stage') ], string='Kanban State', readonly=True) company_id = fields.Many2one('res.company', string='Company', readonly=True) partner_id = fields.Many2one('res.partner', string='Contact', readonly=True) stage_id = fields.Many2one('project.task.type', string='Stage', readonly=True) def _select(self): select_str = """ SELECT (select 1 ) AS nbr, t.id as id, t.date_start as date_start, t.date_end as date_end, t.date_last_stage_update as date_last_stage_update, t.date_deadline as date_deadline, t.user_id, t.project_id, t.priority, t.name as name, t.company_id, t.partner_id, t.stage_id as stage_id, t.kanban_state as state, t.working_days_close as working_days_close, t.working_days_open as working_days_open, (extract('epoch' from (t.date_deadline-(now() at time zone 'UTC'))))/(3600*24) as delay_endings_days """ return select_str def _group_by(self): group_by_str = """ GROUP BY t.id, create_date, write_date, date_start, date_end, date_deadline, date_last_stage_update, t.user_id, t.project_id, t.priority, name, t.company_id, t.partner_id, stage_id """ return group_by_str def init(self): tools.drop_view_if_exists(self._cr, self._table) self._cr.execute(""" CREATE view %s as %s FROM project_task t WHERE t.active = 'true' %s """ % (self._table, self._select(), self._group_by()))
class ResPartner(models.Model): _inherit = 'res.partner' signup_token = fields.Char(copy=False, groups="base.group_erp_manager") signup_type = fields.Char(string='Signup Token Type', copy=False, groups="base.group_erp_manager") signup_expiration = fields.Datetime(copy=False, groups="base.group_erp_manager") signup_valid = fields.Boolean(compute='_compute_signup_valid', string='Signup Token is Valid') signup_url = fields.Char(compute='_compute_signup_url', string='Signup URL') @api.multi @api.depends('signup_token', 'signup_expiration') def _compute_signup_valid(self): dt = now() for partner in self.sudo(): partner.signup_valid = bool(partner.signup_token) and \ (not partner.signup_expiration or dt <= partner.signup_expiration) @api.multi def _compute_signup_url(self): """ proxy for function field towards actual implementation """ result = self.sudo()._get_signup_url_for_action() for partner in self: if any( u.has_group('base.group_user') for u in partner.user_ids if u != self.env.user): self.env['res.users'].check_access_rights('write') partner.signup_url = result.get(partner.id, False) @api.multi def _get_signup_url_for_action(self, action=None, view_type=None, menu_id=None, res_id=None, model=None): """ generate a signup url for the given partner ids and action, possibly overriding the url state components (menu_id, id, view_type) """ res = dict.fromkeys(self.ids, False) base_url = self.env['ir.config_parameter'].sudo().get_param( 'web.base.url') for partner in self: # when required, make sure the partner has a valid signup token if self.env.context.get('signup_valid') and not partner.user_ids: partner.sudo().signup_prepare() route = 'login' # the parameters to encode for the query query = dict(db=self.env.cr.dbname) signup_type = self.env.context.get( 'signup_force_type_in_url', partner.sudo().signup_type or '') if signup_type: route = 'reset_password' if signup_type == 'reset' else signup_type if partner.sudo().signup_token and signup_type: query['token'] = partner.sudo().signup_token elif partner.user_ids: query['login'] = partner.user_ids[0].login else: continue # no signup token, no user, thus no signup url! fragment = dict() base = '/web#' if action == '/mail/view': base = '/mail/view?' elif action: fragment['action'] = action if view_type: fragment['view_type'] = view_type if menu_id: fragment['menu_id'] = menu_id if model: fragment['model'] = model if res_id: fragment['res_id'] = res_id if fragment: query['redirect'] = base + werkzeug.urls.url_encode(fragment) res[partner.id] = werkzeug.urls.url_join( base_url, "/web/%s?%s" % (route, werkzeug.urls.url_encode(query))) return res @api.multi def action_signup_prepare(self): return self.signup_prepare() def signup_get_auth_param(self): """ Get a signup token related to the partner if signup is enabled. If the partner already has a user, get the login parameter. """ res = defaultdict(dict) allow_signup = self.env['ir.config_parameter'].sudo().get_param( 'auth_signup.allow_uninvited', 'False').lower() == 'true' for partner in self: if allow_signup and not partner.user_ids: partner = partner.sudo() partner.signup_prepare() res[partner.id]['auth_signup_token'] = partner.signup_token elif partner.user_ids: res[partner.id]['auth_login'] = partner.user_ids[0].login return res @api.multi def signup_cancel(self): return self.write({ 'signup_token': False, 'signup_type': False, 'signup_expiration': False }) @api.multi def signup_prepare(self, signup_type="signup", expiration=False): """ generate a new token for the partners with the given validity, if necessary :param expiration: the expiration datetime of the token (string, optional) """ for partner in self: if expiration or not partner.signup_valid: token = random_token() while self._signup_retrieve_partner(token): token = random_token() partner.write({ 'signup_token': token, 'signup_type': signup_type, 'signup_expiration': expiration }) return True @api.model def _signup_retrieve_partner(self, token, check_validity=False, raise_exception=False): """ find the partner corresponding to a token, and possibly check its validity :param token: the token to resolve :param check_validity: if True, also check validity :param raise_exception: if True, raise exception instead of returning False :return: partner (browse record) or False (if raise_exception is False) """ partner = self.search([('signup_token', '=', token)], limit=1) if not partner: if raise_exception: raise exceptions.UserError( _("Signup token '%s' is not valid") % token) return False if check_validity and not partner.signup_valid: if raise_exception: raise exceptions.UserError( _("Signup token '%s' is no longer valid") % token) return False return partner @api.model def signup_retrieve_info(self, token): """ retrieve the user info about the token :return: a dictionary with the user information: - 'db': the name of the database - 'token': the token, if token is valid - 'name': the name of the partner, if token is valid - 'login': the user login, if the user already exists - 'email': the partner email, if the user does not exist """ partner = self._signup_retrieve_partner(token, raise_exception=True) res = {'db': self.env.cr.dbname} if partner.signup_valid: res['token'] = token res['name'] = partner.name if partner.user_ids: res['login'] = partner.user_ids[0].login else: res['email'] = res['login'] = partner.email or '' return res
class Users(models.Model): """ User class. A res.users record models an OpenERP user and is different from an employee. res.users class now inherits from res.partner. The partner model is used to store the data related to the partner: lang, name, address, avatar, ... The user model is now dedicated to technical data. """ _name = "res.users" _description = 'Users' _inherits = {'res.partner': 'partner_id'} _order = 'name, login' __uid_cache = defaultdict(dict) # {dbname: {uid: password}} # User can write on a few of his own fields (but not his groups for example) SELF_WRITEABLE_FIELDS = ['signature', 'action_id', 'company_id', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz'] # User can read a few of his own fields SELF_READABLE_FIELDS = ['signature', 'company_id', 'login', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz', 'tz_offset', 'groups_id', 'partner_id', '__last_update', 'action_id'] def _default_groups(self): default_user = self.env.ref('base.default_user', raise_if_not_found=False) return (default_user or self.env['res.users']).sudo().groups_id def _companies_count(self): return self.env['res.company'].sudo().search_count([]) partner_id = fields.Many2one('res.partner', required=True, ondelete='restrict', auto_join=True, string='Related Partner', help='Partner-related data of the user') login = fields.Char(required=True, help="Used to log into the system") password = fields.Char(default='', invisible=True, copy=False, help="Keep empty if you don't want the user to be able to connect on the system.") new_password = fields.Char(string='Set Password', compute='_compute_password', inverse='_inverse_password', help="Specify a value only when creating a user or if you're "\ "changing the user's password, otherwise leave empty. After "\ "a change of password, the user has to login again.") signature = fields.Html() active = fields.Boolean(default=True) action_id = fields.Many2one('ir.actions.actions', string='Home Action', help="If specified, this action will be opened at log on for this user, in addition to the standard menu.") groups_id = fields.Many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', string='Groups', default=_default_groups) log_ids = fields.One2many('res.users.log', 'create_uid', string='User log entries') login_date = fields.Datetime(related='log_ids.create_date', string='Latest connection') share = fields.Boolean(compute='_compute_share', compute_sudo=True, string='Share User', store=True, help="External user with limited access, created only for the purpose of sharing data.") companies_count = fields.Integer(compute='_compute_companies_count', string="Number of Companies", default=_companies_count) tz_offset = fields.Char(compute='_compute_tz_offset', string='Timezone offset', invisible=True) @api.model def _get_company(self): return self.env.user.company_id # Special behavior for this field: res.company.search() will only return the companies # available to the current user (should be the user's companies?), when the user_preference # context is set. company_id = fields.Many2one('res.company', string='Company', required=True, default=_get_company, help='The company this user is currently working for.', context={'user_preference': True}) company_ids = fields.Many2many('res.company', 'res_company_users_rel', 'user_id', 'cid', string='Companies', default=_get_company) # overridden inherited fields to bypass access rights, in case you have # access to the user but not its corresponding partner name = fields.Char(related='partner_id.name', inherited=True) email = fields.Char(related='partner_id.email', inherited=True) _sql_constraints = [ ('login_key', 'UNIQUE (login)', 'You can not have two users with the same login !') ] def _compute_password(self): for user in self: user.password = '' def _inverse_password(self): for user in self: if not user.new_password: # Do not update the password if no value is provided, ignore silently. # For example web client submits False values for all empty fields. continue if user == self.env.user: # To change their own password, users must use the client-specific change password wizard, # so that the new password is immediately used for further RPC requests, otherwise the user # will face unexpected 'Access Denied' exceptions. raise UserError(_('Please use the change password wizard (in User Preferences or User menu) to change your own password.')) else: user.password = user.new_password @api.depends('groups_id') def _compute_share(self): for user in self: user.share = not user.has_group('base.group_user') @api.multi def _compute_companies_count(self): companies_count = self._companies_count() for user in self: user.companies_count = companies_count @api.depends('tz') def _compute_tz_offset(self): for user in self: user.tz_offset = datetime.datetime.now(pytz.timezone(user.tz or 'GMT')).strftime('%z') @api.onchange('login') def on_change_login(self): if self.login and tools.single_email_re.match(self.login): self.email = self.login @api.onchange('parent_id') def onchange_parent_id(self): return self.mapped('partner_id').onchange_parent_id() def _read_from_database(self, field_names, inherited_field_names=[]): super(Users, self)._read_from_database(field_names, inherited_field_names) canwrite = self.check_access_rights('write', raise_exception=False) if not canwrite and set(USER_PRIVATE_FIELDS).intersection(field_names): for record in self: for f in USER_PRIVATE_FIELDS: try: record._cache[f] record._cache[f] = '********' except Exception: # skip SpecialValue (e.g. for missing record or access right) pass @api.multi @api.constrains('company_id', 'company_ids') def _check_company(self): if any(user.company_ids and user.company_id not in user.company_ids for user in self): raise ValidationError(_('The chosen company is not in the allowed companies for this user')) @api.multi @api.constrains('action_id') def _check_action_id(self): action_open_website = self.env.ref('base.action_open_website', raise_if_not_found=False) if action_open_website and any(user.action_id.id == action_open_website.id for user in self): raise ValidationError(_('The "App Switcher" action cannot be selected as home action.')) @api.multi def read(self, fields=None, load='_classic_read'): if fields and self == self.env.user: for key in fields: if not (key in self.SELF_READABLE_FIELDS or key.startswith('context_')): break else: # safe fields only, so we read as super-user to bypass access rights self = self.sudo() return super(Users, self).read(fields=fields, load=load) @api.model def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): groupby_fields = set([groupby] if isinstance(groupby, pycompat.string_types) else groupby) if groupby_fields.intersection(USER_PRIVATE_FIELDS): raise AccessError(_("Invalid 'group by' parameter")) return super(Users, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) @api.model def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): if self._uid != SUPERUSER_ID and args: domain_fields = {term[0] for term in args if isinstance(term, (tuple, list))} if domain_fields.intersection(USER_PRIVATE_FIELDS): raise AccessError(_('Invalid search criterion')) return super(Users, self)._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid) @api.model def create(self, vals): user = super(Users, self).create(vals) user.partner_id.active = user.active if user.partner_id.company_id: user.partner_id.write({'company_id': user.company_id.id}) return user @api.multi def write(self, values): if values.get('active') == False: for user in self: if user.id == SUPERUSER_ID: raise UserError(_("You cannot deactivate the admin user.")) elif user.id == self._uid: raise UserError(_("You cannot deactivate the user you're currently logged in as.")) if self == self.env.user: for key in list(values): if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')): break else: if 'company_id' in values: if values['company_id'] not in self.env.user.company_ids.ids: del values['company_id'] # safe fields only, so we write as super-user to bypass access rights self = self.sudo() res = super(Users, self).write(values) if 'company_id' in values: for user in self: # if partner is global we keep it that way if user.partner_id.company_id and user.partner_id.company_id.id != values['company_id']: user.partner_id.write({'company_id': user.company_id.id}) # clear default ir values when company changes self.env['ir.default'].clear_caches() # clear caches linked to the users if 'groups_id' in values: self.env['ir.model.access'].call_cache_clearing_methods() self.env['ir.rule'].clear_caches() self.has_group.clear_cache(self) if any(key.startswith('context_') or key in ('lang', 'tz') for key in values): self.context_get.clear_cache(self) if any(key in values for key in ['active'] + USER_PRIVATE_FIELDS): db = self._cr.dbname for id in self.ids: self.__uid_cache[db].pop(id, None) if any(key in values for key in self._get_session_token_fields()): self._invalidate_session_cache() return res @api.multi def unlink(self): if SUPERUSER_ID in self.ids: raise UserError(_('You can not remove the admin user as it is used internally for resources created by GECOERP (updates, module installation, ...)')) db = self._cr.dbname for id in self.ids: self.__uid_cache[db].pop(id, None) self._invalidate_session_cache() return super(Users, self).unlink() @api.model def name_search(self, name='', args=None, operator='ilike', limit=100): if args is None: args = [] users = self.browse() if name and operator in ['=', 'ilike']: users = self.search([('login', '=', name)] + args, limit=limit) if not users: users = self.search([('name', operator, name)] + args, limit=limit) return users.name_get() @api.multi def copy(self, default=None): self.ensure_one() default = dict(default or {}) if ('name' not in default) and ('partner_id' not in default): default['name'] = _("%s (copy)") % self.name if 'login' not in default: default['login'] = _("%s (copy)") % self.login return super(Users, self).copy(default) @api.model @tools.ormcache('self._uid') def context_get(self): user = self.env.user result = {} for k in self._fields: if k.startswith('context_'): context_key = k[8:] elif k in ['lang', 'tz']: context_key = k else: context_key = False if context_key: res = getattr(user, k) or False if isinstance(res, models.BaseModel): res = res.id result[context_key] = res or False return result @api.model @api.returns('ir.actions.act_window', lambda record: record.id) def action_get(self): return self.sudo().env.ref('base.action_res_users_my') def check_super(self, passwd): return check_super(passwd) @api.model def check_credentials(self, password): """ Override this method to plug additional authentication methods""" user = self.sudo().search([('id', '=', self._uid), ('password', '=', password)]) if not user: raise AccessDenied() @api.model def _update_last_login(self): # only create new records to avoid any side-effect on concurrent transactions # extra records will be deleted by the periodical garbage collection self.env['res.users.log'].create({}) # populated by defaults @classmethod def _login(cls, db, login, password): if not password: return False user_id = False try: with cls.pool.cursor() as cr: self = api.Environment(cr, SUPERUSER_ID, {})[cls._name] user = self.search([('login', '=', login)]) if user: user_id = user.id user.sudo(user_id).check_credentials(password) user.sudo(user_id)._update_last_login() except AccessDenied: user_id = False status = "successful" if user_id else "failed" ip = request.httprequest.environ['REMOTE_ADDR'] if request else 'n/a' _logger.info("Login %s for db:%s login:%s from %s", status, db, login, ip) return user_id @classmethod def authenticate(cls, db, login, password, user_agent_env): """Verifies and returns the user ID corresponding to the given ``login`` and ``password`` combination, or False if there was no matching user. :param str db: the database on which user is trying to authenticate :param str login: username :param str password: user password :param dict user_agent_env: environment dictionary describing any relevant environment attributes """ uid = cls._login(db, login, password) if uid == SUPERUSER_ID: # Successfully logged in as admin! # Attempt to guess the web base url... if user_agent_env and user_agent_env.get('base_location'): try: with cls.pool.cursor() as cr: base = user_agent_env['base_location'] ICP = api.Environment(cr, uid, {})['ir.config_parameter'] if not ICP.get_param('web.base.url.freeze'): ICP.set_param('web.base.url', base) except Exception: _logger.exception("Failed to update web.base.url configuration parameter") return uid @classmethod def check(cls, db, uid, passwd): """Verifies that the given (uid, password) is authorized for the database ``db`` and raise an exception if it is not.""" if not passwd: # empty passwords disallowed for obvious security reasons raise AccessDenied() db = cls.pool.db_name if cls.__uid_cache[db].get(uid) == passwd: return cr = cls.pool.cursor() try: self = api.Environment(cr, uid, {})[cls._name] self.check_credentials(passwd) cls.__uid_cache[db][uid] = passwd finally: cr.close() def _get_session_token_fields(self): return {'id', 'login', 'password', 'active'} @tools.ormcache('sid') def _compute_session_token(self, sid): """ Compute a session token given a session id and a user id """ # retrieve the fields used to generate the session token session_fields = ', '.join(sorted(self._get_session_token_fields())) self.env.cr.execute("""SELECT %s, (SELECT value FROM ir_config_parameter WHERE key='database.secret') FROM res_users WHERE id=%%s""" % (session_fields), (self.id,)) if self.env.cr.rowcount != 1: self._invalidate_session_cache() return False data_fields = self.env.cr.fetchone() # generate hmac key key = (u'%s' % (data_fields,)).encode('utf-8') # hmac the session id data = sid.encode('utf-8') h = hmac.new(key, data, sha256) # keep in the cache the token return h.hexdigest() @api.multi def _invalidate_session_cache(self): """ Clear the sessions cache """ self._compute_session_token.clear_cache(self) @api.model def change_password(self, old_passwd, new_passwd): """Change current user password. Old password must be provided explicitly to prevent hijacking an existing user session, or for cases where the cleartext password is not used to authenticate requests. :return: True :raise: gecoerp.exceptions.AccessDenied when old password is wrong :raise: gecoerp.exceptions.UserError when new password is not set or empty """ self.check(self._cr.dbname, self._uid, old_passwd) if new_passwd: # use self.env.user here, because it has uid=SUPERUSER_ID return self.env.user.write({'password': new_passwd}) raise UserError(_("Setting empty passwords is not allowed for security reasons!")) @api.multi def preference_save(self): return { 'type': 'ir.actions.client', 'tag': 'reload_context', } @api.multi def preference_change_password(self): return { 'type': 'ir.actions.client', 'tag': 'change_password', 'target': 'new', } @api.model def has_group(self, group_ext_id): # use singleton's id if called on a non-empty recordset, otherwise # context uid uid = self.id or self._uid return self.sudo(user=uid)._has_group(group_ext_id) @api.model @tools.ormcache('self._uid', 'group_ext_id') def _has_group(self, group_ext_id): """Checks whether user belongs to given group. :param str group_ext_id: external ID (XML ID) of the group. Must be provided in fully-qualified form (``module.ext_id``), as there is no implicit module to use.. :return: True if the current user is a member of the group with the given external ID (XML ID), else False. """ assert group_ext_id and '.' in group_ext_id, "External ID must be fully qualified" module, ext_id = group_ext_id.split('.') self._cr.execute("""SELECT 1 FROM res_groups_users_rel WHERE uid=%s AND gid IN (SELECT res_id FROM ir_model_data WHERE module=%s AND name=%s)""", (self._uid, module, ext_id)) return bool(self._cr.fetchone()) # for a few places explicitly clearing the has_group cache has_group.clear_cache = _has_group.clear_cache @api.multi def _is_public(self): self.ensure_one() return self.has_group('base.group_public') @api.multi def _is_system(self): self.ensure_one() return self.has_group('base.group_system') @api.multi def _is_admin(self): self.ensure_one() return self._is_superuser() or self.has_group('base.group_erp_manager') @api.multi def _is_superuser(self): self.ensure_one() return self.id == SUPERUSER_ID @api.model def get_company_currency_id(self): return self.env.user.company_id.currency_id.id
class MailChannel(models.Model): """ Chat Session Reprensenting a conversation between users. It extends the base method for anonymous usage. """ _name = 'mail.channel' _inherit = ['mail.channel', 'rating.mixin'] anonymous_name = fields.Char('Anonymous Name') create_date = fields.Datetime('Create Date', required=True) channel_type = fields.Selection(selection_add=[('livechat', 'Livechat Conversation')]) livechat_channel_id = fields.Many2one('im_livechat.channel', 'Channel') @api.multi def _channel_message_notifications(self, message): """ When a anonymous user create a mail.channel, the operator is not notify (to avoid massive polling when clicking on livechat button). So when the anonymous person is sending its FIRST message, the channel header should be added to the notification, since the user cannot be listining to the channel. """ notifications = super(MailChannel, self)._channel_message_notifications(message) message_values_dict = notifications[0][1] if len( notifications) else dict(message.message_format()[0]) for channel in self: # add uuid for private livechat channels to allow anonymous to listen if channel.channel_type == 'livechat': notifications.append([channel.uuid, message_values_dict]) if not message.author_id: unpinned_channel_partner = self.mapped( 'channel_last_seen_partner_ids').filtered( lambda cp: not cp.is_pinned) if unpinned_channel_partner: unpinned_channel_partner.write({'is_pinned': True}) notifications = self._channel_channel_notifications( unpinned_channel_partner.mapped( 'partner_id').ids) + notifications return notifications @api.multi def channel_info(self, extra_info=False): """ Extends the channel header by adding the livechat operator and the 'anonymous' profile :rtype : list(dict) """ channel_infos = super(MailChannel, self).channel_info(extra_info) # add the operator id if self.env.context.get('im_livechat_operator_partner_id'): partner_name = self.env['res.partner'].browse( self.env.context.get( 'im_livechat_operator_partner_id')).name_get()[0] for channel_info in channel_infos: channel_info['operator_pid'] = partner_name channel_infos_dict = dict((c['id'], c) for c in channel_infos) for channel in self: # add the anonymous name if channel.anonymous_name: channel_infos_dict[ channel.id]['anonymous_name'] = channel.anonymous_name # add the last message date if channel.channel_type == 'livechat': last_msg = self.env['mail.message'].search( [("channel_ids", "in", [channel.id])], limit=1) if last_msg: channel_infos_dict[ channel.id]['last_message_date'] = last_msg.date return list(channel_infos_dict.values()) @api.model def channel_fetch_slot(self): values = super(MailChannel, self).channel_fetch_slot() pinned_channels = self.env['mail.channel.partner'].search([ ('partner_id', '=', self.env.user.partner_id.id), ('is_pinned', '=', True) ]).mapped('channel_id') values['channel_livechat'] = self.search([ ('channel_type', '=', 'livechat'), ('id', 'in', pinned_channels.ids) ]).channel_info() return values @api.model def remove_empty_livechat_sessions(self): hours = 1 # never remove empty session created within the last hour self.env.cr.execute( """ SELECT id as id FROM mail_channel C WHERE NOT EXISTS ( SELECT * FROM mail_message_mail_channel_rel R WHERE R.mail_channel_id = C.id ) AND C.channel_type = 'livechat' AND livechat_channel_id IS NOT NULL AND COALESCE(write_date, create_date, (now() at time zone 'UTC'))::timestamp < ((now() at time zone 'UTC') - interval %s)""", ("%s hours" % hours, )) empty_channel_ids = [item['id'] for item in self.env.cr.dictfetchall()] self.browse(empty_channel_ids).unlink() @api.model def get_empty_list_help(self, help): if help: return '<p>%s</p>' % (help) return super(MailChannel, self).get_empty_list_help(help) def _define_command_history(self): return { 'channel_types': ['livechat'], 'help': _('See 15 last visited pages') } def _execute_command_history(self, **kwargs): notification = [] notification_values = { '_type': 'history_command', } notification.append([self.uuid, dict(notification_values)]) return self.env['bus.bus'].sendmany(notification) def _send_history_message(self, pid, page_history): message_body = _('No history found') if page_history: html_links = [ '<li><a href="%s" target="_blank">%s</a></li>' % (page, page) for page in page_history ] message_body = '<span class="o_mail_notification"><ul>%s</ul></span>' % ( ''.join(html_links)) self.env['bus.bus'].sendone( (self._cr.dbname, 'res.partner', pid), { 'body': message_body, 'channel_ids': self.ids, 'info': 'transient_message', }) # Rating Mixin def rating_get_parent_model_name(self, values): return 'im_livechat.channel' def rating_get_parent_id(self): return self.livechat_channel_id.id
class Track(models.Model): _name = "event.track" _description = 'Event Track' _order = 'priority, date' _inherit = [ 'mail.thread', 'mail.activity.mixin', 'website.seo.metadata', 'website.published.mixin' ] @api.model def _get_default_stage_id(self): return self.env['event.track.stage'].search([], limit=1).id name = fields.Char('Title', required=True, translate=True) active = fields.Boolean(default=True) user_id = fields.Many2one('res.users', 'Responsible', track_visibility='onchange', default=lambda self: self.env.user) partner_id = fields.Many2one('res.partner', 'Speaker') partner_name = fields.Char('Speaker Name') partner_email = fields.Char('Speaker Email') partner_phone = fields.Char('Speaker Phone') partner_biography = fields.Html('Speaker Biography') tag_ids = fields.Many2many('event.track.tag', string='Tags') stage_id = fields.Many2one('event.track.stage', string='Stage', index=True, copy=False, default=_get_default_stage_id, group_expand='_read_group_stage_ids', required=True, track_visibility='onchange') kanban_state = fields.Selection( [('normal', 'Grey'), ('done', 'Green'), ('blocked', 'Red')], string='Kanban State', copy=False, default='normal', required=True, track_visibility='onchange', help= "A track's kanban state indicates special situations affecting it:\n" " * Grey is the default situation\n" " * Red indicates something is preventing the progress of this track\n" " * Green indicates the track is ready to be pulled to the next stage") description = fields.Html('Track Description', translate=html_translate, sanitize_attributes=False) date = fields.Datetime('Track Date') duration = fields.Float('Duration', default=1.5) location_id = fields.Many2one('event.track.location', 'Room') event_id = fields.Many2one('event.event', 'Event', required=True) color = fields.Integer('Color Index') priority = fields.Selection([('0', 'Low'), ('1', 'Medium'), ('2', 'High'), ('3', 'Highest')], 'Priority', required=True, default='1') image = fields.Binary('Image', related='partner_id.image_medium', store=True, attachment=True) @api.multi @api.depends('name') def _compute_website_url(self): super(Track, self)._compute_website_url() for track in self: if not isinstance(track.id, models.NewId): track.website_url = '/event/%s/track/%s' % (slug( track.event_id), slug(track)) @api.onchange('partner_id') def _onchange_partner_id(self): if self.partner_id: self.partner_name = self.partner_id.name self.partner_email = self.partner_id.email self.partner_phone = self.partner_id.phone self.partner_biography = self.partner_id.website_description @api.model def create(self, vals): track = super(Track, self).create(vals) track.event_id.message_post_with_view( 'website_event_track.event_track_template_new', values={'track': track}, subject=track.name, subtype_id=self.env.ref('website_event_track.mt_event_track').id, ) return track @api.multi def write(self, vals): if 'stage_id' in vals and 'kanban_state' not in vals: vals['kanban_state'] = 'normal' res = super(Track, self).write(vals) if vals.get('partner_id'): self.message_subscribe([vals['partner_id']]) return res @api.model def _read_group_stage_ids(self, stages, domain, order): """ Always display all stages """ return stages.search([], order=order) @api.multi def _track_template(self, tracking): res = super(Track, self)._track_template(tracking) track = self[0] changes, tracking_value_ids = tracking[track.id] if 'stage_id' in changes and track.stage_id.mail_template_id: res['stage_id'] = (track.stage_id.mail_template_id, { 'composition_mode': 'mass_mail' }) return res @api.multi def _track_subtype(self, init_values): self.ensure_one() if 'kanban_state' in init_values and self.kanban_state == 'blocked': return 'website_event_track.mt_track_blocked' elif 'kanban_state' in init_values and self.kanban_state == 'done': return 'website_event_track.mt_track_ready' return super(Track, self)._track_subtype(init_values) @api.multi def message_get_suggested_recipients(self): recipients = super(Track, self).message_get_suggested_recipients() for track in self: if track.partner_email != track.partner_id.email: track._message_add_suggested_recipient( recipients, email=track.partner_email, reason=_('Speaker Email')) return recipients def _message_post_after_hook(self, message): if self.partner_email and not self.partner_id: # we consider that posting a message with a specified recipient (not a follower, a specific one) # on a document without customer means that it was created through the chatter using # suggested recipients. This heuristic allows to avoid ugly hacks in JS. new_partner = message.partner_ids.filtered( lambda partner: partner.email == self.partner_email) if new_partner: self.search([ ('partner_id', '=', False), ('partner_email', '=', new_partner.email), ('stage_id.is_cancel', '=', False), ]).write({'partner_id': new_partner.id}) return super(Track, self)._message_post_after_hook(message) @api.multi def open_track_speakers_list(self): return { 'name': _('Speakers'), 'domain': [('id', 'in', self.mapped('partner_id').ids)], 'view_type': 'form', 'view_mode': 'kanban,form', 'res_model': 'res.partner', 'view_id': False, 'type': 'ir.actions.act_window', }