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('noblecrm.sql_db'): presence.write(values) # avoid TransactionRollbackError self.env.cr.commit() # TODO : check if still necessary
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 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 NobleCRM Google Internal ID' )
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 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 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 Meeting(models.Model): _inherit = "calendar.event" oe_update_date = fields.Datetime('NobleCRM 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 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 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 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 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 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 pos_config(models.Model): _inherit = 'pos.config' @api.one @api.depends('cache_ids') def _get_oldest_cache_time(self): pos_cache = self.env['pos.cache'] oldest_cache = pos_cache.search([('config_id', '=', self.id)], order='write_date', limit=1) if oldest_cache: self.oldest_cache_time = oldest_cache.write_date # Use a related model to avoid the load of the cache when the pos load his config cache_ids = fields.One2many('pos.cache', 'config_id') oldest_cache_time = fields.Datetime(compute='_get_oldest_cache_time', string='Oldest cache time', readonly=True) def _get_cache_for_user(self): pos_cache = self.env['pos.cache'] cache_for_user = pos_cache.search([('id', 'in', self.cache_ids.ids), ('compute_user_id', '=', self.env.uid)]) if cache_for_user: return cache_for_user[0] else: return None @api.multi def get_products_from_cache(self, fields, domain): cache_for_user = self._get_cache_for_user() if cache_for_user: return cache_for_user.get_cache(domain, fields) else: pos_cache = self.env['pos.cache'] pos_cache.create({ 'config_id': self.id, 'product_domain': str(domain), 'product_fields': str(fields), 'compute_user_id': self.env.uid }) new_cache = self._get_cache_for_user() return new_cache.get_cache(domain, fields) @api.one def delete_cache(self): # throw away the old caches self.cache_ids.unlink()
class 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 Attendee(models.Model): _inherit = 'calendar.attendee' google_internal_event_id = fields.Char('Google Calendar Event Id') oe_synchro_date = fields.Datetime('NobleCRM 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 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 PosSession(models.Model): _name = 'pos.session' _order = 'id desc' POS_SESSION_STATE = [ ('opening_control', 'Opening Control'), # method action_pos_session_open ('opened', 'In Progress'), # method action_pos_session_closing_control ('closing_control', 'Closing Control'), # method action_pos_session_close ('closed', 'Closed & Posted'), ] def _confirm_orders(self): for session in self: company_id = session.config_id.journal_id.company_id.id orders = session.order_ids.filtered( lambda order: order.state == 'paid') journal_id = self.env['ir.config_parameter'].sudo().get_param( 'pos.closing.journal_id_%s' % company_id, default=session.config_id.journal_id.id) if not journal_id: raise UserError( _("You have to set a Sale Journal for the POS:%s") % (session.config_id.name, )) move = self.env['pos.order'].with_context( force_company=company_id)._create_account_move( session.start_at, session.name, int(journal_id), company_id) orders.with_context( force_company=company_id)._create_account_move_line( session, move) for order in session.order_ids.filtered( lambda o: o.state not in ['done', 'invoiced']): if order.state not in ('paid'): raise UserError( _("You cannot confirm all orders of this session, because they have not the 'paid' status.\n" "{reference} is in state {state}, total amount: {total}, paid: {paid}" ).format( reference=order.pos_reference or order.name, state=order.state, total=order.amount_total, paid=order.amount_paid, )) order.action_pos_order_done() orders_to_reconcile = session.order_ids._filtered_for_reconciliation( ) orders_to_reconcile.sudo()._reconcile_payments() config_id = fields.Many2one( 'pos.config', string='Point of Sale', help="The physical point of sale you will use.", required=True, index=True) name = fields.Char(string='Session ID', required=True, readonly=True, default='/') user_id = fields.Many2one( 'res.users', string='Responsible', required=True, index=True, readonly=True, states={'opening_control': [('readonly', False)]}, default=lambda self: self.env.uid) currency_id = fields.Many2one('res.currency', related='config_id.currency_id', string="Currency") start_at = fields.Datetime(string='Opening Date', readonly=True) stop_at = fields.Datetime(string='Closing Date', readonly=True, copy=False) state = fields.Selection(POS_SESSION_STATE, string='Status', required=True, readonly=True, index=True, copy=False, default='opening_control') sequence_number = fields.Integer( string='Order Sequence Number', help='A sequence number that is incremented with each order', default=1) login_number = fields.Integer( string='Login Sequence Number', help= 'A sequence number that is incremented each time a user resumes the pos session', default=0) cash_control = fields.Boolean(compute='_compute_cash_all', string='Has Cash Control') cash_journal_id = fields.Many2one('account.journal', compute='_compute_cash_all', string='Cash Journal', store=True) cash_register_id = fields.Many2one('account.bank.statement', compute='_compute_cash_all', string='Cash Register', store=True) cash_register_balance_end_real = fields.Monetary( related='cash_register_id.balance_end_real', string="Ending Balance", help="Total of closing cash control lines.", readonly=True) cash_register_balance_start = fields.Monetary( related='cash_register_id.balance_start', string="Starting Balance", help="Total of opening cash control lines.", readonly=True) cash_register_total_entry_encoding = fields.Monetary( related='cash_register_id.total_entry_encoding', string='Total Cash Transaction', readonly=True, help="Total of all paid sales orders") cash_register_balance_end = fields.Monetary( related='cash_register_id.balance_end', digits=0, string="Theoretical Closing Balance", help="Sum of opening balance and transactions.", readonly=True) cash_register_difference = fields.Monetary( related='cash_register_id.difference', string='Difference', help= "Difference between the theoretical closing balance and the real closing balance.", readonly=True) journal_ids = fields.Many2many('account.journal', related='config_id.journal_ids', readonly=True, string='Available Payment Methods') order_ids = fields.One2many('pos.order', 'session_id', string='Orders') statement_ids = fields.One2many('account.bank.statement', 'pos_session_id', string='Bank Statement', readonly=True) picking_count = fields.Integer(compute='_compute_picking_count') rescue = fields.Boolean( string='Recovery Session', help="Auto-generated session for orphan orders, ignored in constraints", readonly=True, copy=False) _sql_constraints = [('uniq_name', 'unique(name)', "The name of this POS Session must be unique !")] @api.multi def _compute_picking_count(self): for pos in self: pickings = pos.order_ids.mapped('picking_id').filtered( lambda x: x.state != 'done') pos.picking_count = len(pickings.ids) @api.multi def action_stock_picking(self): pickings = self.order_ids.mapped('picking_id').filtered( lambda x: x.state != 'done') action_picking = self.env.ref('stock.action_picking_tree_ready') action = action_picking.read()[0] action['context'] = {} action['domain'] = [('id', 'in', pickings.ids)] return action @api.depends('config_id', 'statement_ids') def _compute_cash_all(self): for session in self: session.cash_journal_id = session.cash_register_id = session.cash_control = False if session.config_id.cash_control: for statement in session.statement_ids: if statement.journal_id.type == 'cash': session.cash_control = True session.cash_journal_id = statement.journal_id.id session.cash_register_id = statement.id if not session.cash_control and session.state != 'closed': raise UserError( _("Cash control can only be applied to cash journals.") ) @api.constrains('user_id', 'state') def _check_unicity(self): # open if there is no session in 'opening_control', 'opened', 'closing_control' for one user if self.search_count([('state', 'not in', ('closed', 'closing_control')), ('user_id', '=', self.user_id.id), ('rescue', '=', False)]) > 1: raise ValidationError( _("You cannot create two active sessions with the same responsible!" )) @api.constrains('config_id') def _check_pos_config(self): if self.search_count([('state', '!=', 'closed'), ('config_id', '=', self.config_id.id), ('rescue', '=', False)]) > 1: raise ValidationError( _("Another session is already opened for this point of sale.")) @api.model def create(self, values): config_id = values.get('config_id') or self.env.context.get( 'default_config_id') if not config_id: raise UserError( _("You should assign a Point of Sale to your session.")) # journal_id is not required on the pos_config because it does not # exists at the installation. If nothing is configured at the # installation we do the minimal configuration. Impossible to do in # the .xml files as the CoA is not yet installed. pos_config = self.env['pos.config'].browse(config_id) ctx = dict(self.env.context, company_id=pos_config.company_id.id) if not pos_config.journal_id: default_journals = pos_config.with_context(ctx).default_get( ['journal_id', 'invoice_journal_id']) if (not default_journals.get('journal_id') or not default_journals.get('invoice_journal_id')): raise UserError( _("Unable to open the session. You have to assign a sales journal to your point of sale." )) pos_config.with_context(ctx).sudo().write({ 'journal_id': default_journals['journal_id'], 'invoice_journal_id': default_journals['invoice_journal_id'] }) # define some cash journal if no payment method exists if not pos_config.journal_ids: Journal = self.env['account.journal'] journals = Journal.with_context(ctx).search([ ('journal_user', '=', True), ('type', '=', 'cash') ]) if not journals: journals = Journal.with_context(ctx).search([('type', '=', 'cash')]) if not journals: journals = Journal.with_context(ctx).search([ ('journal_user', '=', True) ]) journals.sudo().write({'journal_user': True}) pos_config.sudo().write({'journal_ids': [(6, 0, journals.ids)]}) pos_name = self.env['ir.sequence'].with_context(ctx).next_by_code( 'pos.session') if values.get('name'): pos_name += ' ' + values['name'] statements = [] ABS = self.env['account.bank.statement'] uid = SUPERUSER_ID if self.env.user.has_group( 'point_of_sale.group_pos_user') else self.env.user.id for journal in pos_config.journal_ids: # set the journal_id which should be used by # account.bank.statement to set the opening balance of the # newly created bank statement ctx['journal_id'] = journal.id if pos_config.cash_control and journal.type == 'cash' else False st_values = { 'journal_id': journal.id, 'user_id': self.env.user.id, 'name': pos_name } statements.append( ABS.with_context(ctx).sudo(uid).create(st_values).id) values.update({ 'name': pos_name, 'statement_ids': [(6, 0, statements)], 'config_id': config_id }) res = super(PosSession, self.with_context(ctx).sudo(uid)).create(values) if not pos_config.cash_control: res.action_pos_session_open() return res @api.multi def unlink(self): for session in self.filtered(lambda s: s.statement_ids): session.statement_ids.unlink() return super(PosSession, self).unlink() @api.multi def login(self): self.ensure_one() self.write({ 'login_number': self.login_number + 1, }) @api.multi def action_pos_session_open(self): # second browse because we need to refetch the data from the DB for cash_register_id # we only open sessions that haven't already been opened for session in self.filtered( lambda session: session.state == 'opening_control'): values = {} if not session.start_at: values['start_at'] = fields.Datetime.now() values['state'] = 'opened' session.write(values) session.statement_ids.button_open() return True @api.multi def action_pos_session_closing_control(self): self._check_pos_session_balance() for session in self: session.write({ 'state': 'closing_control', 'stop_at': fields.Datetime.now() }) if not session.config_id.cash_control: session.action_pos_session_close() @api.multi def _check_pos_session_balance(self): for session in self: for statement in session.statement_ids: if (statement != session.cash_register_id) and ( statement.balance_end != statement.balance_end_real): statement.write( {'balance_end_real': statement.balance_end}) @api.multi def action_pos_session_validate(self): self._check_pos_session_balance() self.action_pos_session_close() @api.multi def action_pos_session_close(self): # Close CashBox for session in self: company_id = session.config_id.company_id.id ctx = dict(self.env.context, force_company=company_id, company_id=company_id) for st in session.statement_ids: if abs(st.difference) > st.journal_id.amount_authorized_diff: # The pos manager can close statements with maximums. if not self.user_has_groups( "point_of_sale.group_pos_manager"): raise UserError( _("Your ending balance is too different from the theoretical cash closing (%.2f), the maximum allowed is: %.2f. You can contact your manager to force it." ) % (st.difference, st.journal_id.amount_authorized_diff)) if (st.journal_id.type not in ['bank', 'cash']): raise UserError( _("The type of the journal for your payment method should be bank or cash " )) st.with_context(ctx).sudo().button_confirm_bank() self.with_context(ctx)._confirm_orders() self.write({'state': 'closed'}) return { 'type': 'ir.actions.client', 'name': 'Point of Sale Menu', 'tag': 'reload', 'params': { 'menu_id': self.env.ref('point_of_sale.menu_point_root').id }, } @api.multi def open_frontend_cb(self): if not self.ids: return {} for session in self.filtered(lambda s: s.user_id.id != self.env.uid): raise UserError( _("You cannot use the session of another user. This session is owned by %s. " "Please first close this one to use this point of sale.") % session.user_id.name) return { 'type': 'ir.actions.act_url', 'target': 'self', 'url': '/pos/web/', } @api.multi def open_cashbox(self): self.ensure_one() context = dict(self._context) balance_type = context.get('balance') or 'start' context['bank_statement_id'] = self.cash_register_id.id context['balance'] = balance_type context['default_pos_id'] = self.config_id.id action = { 'name': _('Cash Control'), 'view_type': 'form', 'view_mode': 'form', 'res_model': 'account.bank.statement.cashbox', 'view_id': self.env.ref('account.view_account_bnk_stmt_cashbox').id, 'type': 'ir.actions.act_window', 'context': context, 'target': 'new' } cashbox_id = None if balance_type == 'start': cashbox_id = self.cash_register_id.cashbox_start_id.id else: cashbox_id = self.cash_register_id.cashbox_end_id.id if cashbox_id: action['res_id'] = cashbox_id return action
class MaintenanceRequest(models.Model): _name = 'maintenance.request' _inherit = ['mail.thread', 'mail.activity.mixin'] _description = 'Maintenance Requests' _order = "id desc" @api.returns('self') def _default_stage(self): return self.env['maintenance.stage'].search([], limit=1) @api.multi def _track_subtype(self, init_values): self.ensure_one() if 'stage_id' in init_values and self.stage_id.sequence <= 1: return 'maintenance.mt_req_created' elif 'stage_id' in init_values and self.stage_id.sequence > 1: return 'maintenance.mt_req_status' return super(MaintenanceRequest, self)._track_subtype(init_values) def _get_default_team_id(self): return self.env.ref('maintenance.equipment_team_maintenance', raise_if_not_found=False) name = fields.Char('Subjects', required=True) description = fields.Text('Description') request_date = fields.Date( 'Request Date', track_visibility='onchange', default=fields.Date.context_today, help="Date requested for the maintenance to happen") owner_user_id = fields.Many2one('res.users', string='Created by', default=lambda s: s.env.uid) category_id = fields.Many2one('maintenance.equipment.category', related='equipment_id.category_id', string='Category', store=True, readonly=True) equipment_id = fields.Many2one('maintenance.equipment', string='Equipment', index=True) technician_user_id = fields.Many2one('res.users', string='Owner', track_visibility='onchange', oldname='user_id') stage_id = fields.Many2one('maintenance.stage', string='Stage', track_visibility='onchange', group_expand='_read_group_stage_ids', default=_default_stage) priority = fields.Selection([('0', 'Very Low'), ('1', 'Low'), ('2', 'Normal'), ('3', 'High')], string='Priority') color = fields.Integer('Color Index') close_date = fields.Date('Close Date', help="Date the maintenance was finished. ") kanban_state = fields.Selection([('normal', 'In Progress'), ('blocked', 'Blocked'), ('done', 'Ready for next stage')], string='Kanban State', required=True, default='normal', track_visibility='onchange') # active = fields.Boolean(default=True, help="Set active to false to hide the maintenance request without deleting it.") archive = fields.Boolean( default=False, help= "Set archive to true to hide the maintenance request without deleting it." ) maintenance_type = fields.Selection([('corrective', 'Corrective'), ('preventive', 'Preventive')], string='Maintenance Type', default="corrective") schedule_date = fields.Datetime( 'Scheduled Date', help= "Date the maintenance team plans the maintenance. It should not differ much from the Request Date. " ) maintenance_team_id = fields.Many2one('maintenance.team', string='Team', required=True, default=_get_default_team_id) duration = fields.Float(help="Duration in minutes and seconds.") @api.multi def archive_equipment_request(self): self.write({'archive': True}) @api.multi def reset_equipment_request(self): """ Reinsert the maintenance request into the maintenance pipe in the first stage""" first_stage_obj = self.env['maintenance.stage'].search( [], order="sequence asc", limit=1) # self.write({'active': True, 'stage_id': first_stage_obj.id}) self.write({'archive': False, 'stage_id': first_stage_obj.id}) @api.onchange('equipment_id') def onchange_equipment_id(self): if self.equipment_id: self.technician_user_id = self.equipment_id.technician_user_id if self.equipment_id.technician_user_id else self.equipment_id.category_id.technician_user_id self.category_id = self.equipment_id.category_id if self.equipment_id.maintenance_team_id: self.maintenance_team_id = self.equipment_id.maintenance_team_id.id @api.onchange('category_id') def onchange_category_id(self): if not self.technician_user_id or not self.equipment_id or ( self.technician_user_id and not self.equipment_id.technician_user_id): self.technician_user_id = self.category_id.technician_user_id @api.model def create(self, vals): # context: no_log, because subtype already handle this self = self.with_context(mail_create_nolog=True) request = super(MaintenanceRequest, self).create(vals) if request.owner_user_id or request.technician_user_id: request._add_followers() if request.equipment_id and not request.maintenance_team_id: request.maintenance_team_id = request.equipment_id.maintenance_team_id return request @api.multi def write(self, vals): # Overridden to reset the kanban_state to normal whenever # the stage (stage_id) of the Maintenance Request changes. if vals and 'kanban_state' not in vals and 'stage_id' in vals: vals['kanban_state'] = 'normal' res = super(MaintenanceRequest, self).write(vals) if vals.get('owner_user_id') or vals.get('technician_user_id'): self._add_followers() if 'stage_id' in vals: self.filtered(lambda m: m.stage_id.done).write( {'close_date': fields.Date.today()}) return res def _add_followers(self): for request in self: user_ids = (request.owner_user_id + request.technician_user_id).ids request.message_subscribe_users(user_ids=user_ids) @api.model def _read_group_stage_ids(self, stages, domain, order): """ Read group customization in order to display all the stages in the kanban view, even if they are empty """ stage_ids = stages._search([], order=order, access_rights_uid=SUPERUSER_ID) return stages.browse(stage_ids)
class Holidays(models.Model): _name = "hr.holidays" _description = "Leave" _order = "type desc, date_from desc" _inherit = ['mail.thread'] def _default_employee(self): return self.env.context.get( 'default_employee_id') or self.env['hr.employee'].search( [('user_id', '=', self.env.uid)], limit=1) name = fields.Char('Description') state = fields.Selection( [('draft', 'To Submit'), ('cancel', 'Cancelled'), ('confirm', 'To Approve'), ('refuse', 'Refused'), ('validate1', 'Second Approval'), ('validate', 'Approved')], string='Status', readonly=True, track_visibility='onchange', copy=False, default='confirm', help= "The status is set to 'To Submit', when a leave request is created." + "\nThe status is 'To Approve', when leave request is confirmed by user." + "\nThe status is 'Refused', when leave request is refused by manager." + "\nThe status is 'Approved', when leave request is approved by manager." ) payslip_status = fields.Boolean( 'Reported in last payslips', help= 'Green this button when the leave has been taken into account in the payslip.' ) report_note = fields.Text('HR Comments') user_id = fields.Many2one('res.users', string='User', related='employee_id.user_id', related_sudo=True, store=True, default=lambda self: self.env.uid, readonly=True) date_from = fields.Datetime('Start Date', readonly=True, index=True, copy=False, states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }, track_visibility='onchange') date_to = fields.Datetime('End Date', readonly=True, copy=False, states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }, track_visibility='onchange') holiday_status_id = fields.Many2one("hr.holidays.status", string="Leave Type", required=True, readonly=True, states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }) employee_id = fields.Many2one('hr.employee', string='Employee', index=True, readonly=True, states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }, default=_default_employee, track_visibility='onchange') manager_id = fields.Many2one('hr.employee', string='Manager', readonly=True) notes = fields.Text('Reasons', readonly=True, states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }) number_of_days_temp = fields.Float( 'Allocation', copy=False, readonly=True, states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }, help= 'Number of days of the leave request according to your working schedule.' ) number_of_days = fields.Float('Number of Days', compute='_compute_number_of_days', store=True, track_visibility='onchange') meeting_id = fields.Many2one('calendar.event', string='Meeting') type = fields.Selection( [('remove', 'Leave Request'), ('add', 'Allocation Request')], string='Request Type', required=True, readonly=True, index=True, track_visibility='always', default='remove', states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }, help="Choose 'Leave Request' if someone wants to take an off-day. " "\nChoose 'Allocation Request' if you want to increase the number of leaves available for someone" ) parent_id = fields.Many2one('hr.holidays', string='Parent', copy=False) linked_request_ids = fields.One2many('hr.holidays', 'parent_id', string='Linked Requests') department_id = fields.Many2one('hr.department', string='Department', readonly=True) category_id = fields.Many2one('hr.employee.category', string='Employee Tag', readonly=True, states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }, help='Category of Employee') holiday_type = fields.Selection( [('employee', 'By Employee'), ('category', 'By Employee Tag')], string='Allocation Mode', readonly=True, required=True, default='employee', states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }, help= 'By Employee: Allocation/Request for individual Employee, By Employee Tag: Allocation/Request for group of employees in category' ) first_approver_id = fields.Many2one( 'hr.employee', string='First Approval', readonly=True, copy=False, help= 'This area is automatically filled by the user who validate the leave', oldname='manager_id') second_approver_id = fields.Many2one( 'hr.employee', string='Second Approval', readonly=True, copy=False, oldname='manager_id2', help= 'This area is automaticly filled by the user who validate the leave with second level (If Leave type need second validation)' ) double_validation = fields.Boolean( 'Apply Double Validation', related='holiday_status_id.double_validation') can_reset = fields.Boolean('Can reset', compute='_compute_can_reset') @api.multi @api.depends('number_of_days_temp', 'type') def _compute_number_of_days(self): for holiday in self: if holiday.type == 'remove': holiday.number_of_days = -holiday.number_of_days_temp else: holiday.number_of_days = holiday.number_of_days_temp @api.multi def _compute_can_reset(self): """ User can reset a leave request if it is its own leave request or if he is an Hr Manager. """ user = self.env.user group_hr_manager = self.env.ref( 'hr_holidays.group_hr_holidays_manager') for holiday in self: if group_hr_manager in user.groups_id or holiday.employee_id and holiday.employee_id.user_id == user: holiday.can_reset = True @api.constrains('date_from', 'date_to') def _check_date(self): for holiday in self: domain = [ ('date_from', '<=', holiday.date_to), ('date_to', '>=', holiday.date_from), ('employee_id', '=', holiday.employee_id.id), ('id', '!=', holiday.id), ('type', '=', holiday.type), ('state', 'not in', ['cancel', 'refuse']), ] nholidays = self.search_count(domain) if nholidays: raise ValidationError( _('You can not have 2 leaves that overlaps on same day!')) @api.constrains('state', 'number_of_days_temp', 'holiday_status_id') def _check_holidays(self): for holiday in self: if holiday.holiday_type != 'employee' or holiday.type != 'remove' or not holiday.employee_id or holiday.holiday_status_id.limit: continue leave_days = holiday.holiday_status_id.get_days( holiday.employee_id.id)[holiday.holiday_status_id.id] if float_compare(leave_days['remaining_leaves'], 0, precision_digits=2) == -1 or \ float_compare(leave_days['virtual_remaining_leaves'], 0, precision_digits=2) == -1: raise ValidationError( _('The number of remaining leaves is not sufficient for this leave type.\n' 'Please verify also the leaves waiting for validation.')) _sql_constraints = [ ('type_value', "CHECK( (holiday_type='employee' AND employee_id IS NOT NULL) or (holiday_type='category' AND category_id IS NOT NULL))", "The employee or employee category of this request is missing. Please make sure that your user login is linked to an employee." ), ('date_check2', "CHECK ( (type='add') OR (date_from <= date_to))", "The start date must be anterior to the end date."), ('date_check', "CHECK ( number_of_days_temp >= 0 )", "The number of days must be greater than 0."), ] @api.onchange('holiday_type') def _onchange_type(self): if self.holiday_type == 'employee' and not self.employee_id: self.employee_id = self.env['hr.employee'].search( [('user_id', '=', self.env.uid)], limit=1) elif self.holiday_type != 'employee': self.employee_id = None @api.onchange('employee_id') def _onchange_employee_id(self): self.manager_id = self.employee_id and self.employee_id.parent_id self.department_id = self.employee_id.department_id def _get_number_of_days(self, date_from, date_to, employee_id): """ Returns a float equals to the timedelta between two dates given as string.""" from_dt = fields.Datetime.from_string(date_from) to_dt = fields.Datetime.from_string(date_to) if employee_id: employee = self.env['hr.employee'].browse(employee_id) return employee.get_work_days_count(from_dt, to_dt) time_delta = to_dt - from_dt return math.ceil(time_delta.days + float(time_delta.seconds) / 86400) @api.onchange('date_from') def _onchange_date_from(self): """ If there are no date set for date_to, automatically set one 8 hours later than the date_from. Also update the number_of_days. """ date_from = self.date_from date_to = self.date_to # No date_to set so far: automatically compute one 8 hours later if date_from and not date_to: date_to_with_delta = fields.Datetime.from_string( date_from) + timedelta(hours=HOURS_PER_DAY) self.date_to = str(date_to_with_delta) # Compute and update the number of days if (date_to and date_from) and (date_from <= date_to): self.number_of_days_temp = self._get_number_of_days( date_from, date_to, self.employee_id.id) else: self.number_of_days_temp = 0 @api.onchange('date_to') def _onchange_date_to(self): """ Update the number_of_days. """ date_from = self.date_from date_to = self.date_to # Compute and update the number of days if (date_to and date_from) and (date_from <= date_to): self.number_of_days_temp = self._get_number_of_days( date_from, date_to, self.employee_id.id) else: self.number_of_days_temp = 0 #################################################### # ORM Overrides methods #################################################### @api.multi def name_get(self): res = [] for leave in self: if leave.type == 'remove': if self.env.context.get('short_name'): res.append((leave.id, _("%s : %.2f day(s)") % (leave.name or leave.holiday_status_id.name, leave.number_of_days_temp))) else: res.append( (leave.id, _("%s on %s : %.2f day(s)") % (leave.employee_id.name or leave.category_id.name, leave.holiday_status_id.name, leave.number_of_days_temp))) else: res.append( (leave.id, _("Allocation of %s : %.2f day(s) To %s") % (leave.holiday_status_id.name, leave.number_of_days_temp, leave.employee_id.name))) return res def _check_state_access_right(self, vals): if vals.get('state') and vals['state'] not in [ 'draft', 'confirm', 'cancel' ] and not self.env['res.users'].has_group( 'hr_holidays.group_hr_holidays_user'): return False return True @api.multi def add_follower(self, employee_id): employee = self.env['hr.employee'].browse(employee_id) if employee.user_id: self.message_subscribe_users(user_ids=employee.user_id.ids) @api.model def create(self, values): """ Override to avoid automatic logging of creation """ employee_id = values.get('employee_id', False) if not self._check_state_access_right(values): raise AccessError( _('You cannot set a leave request as \'%s\'. Contact a human resource manager.' ) % values.get('state')) if not values.get('department_id'): values.update({ 'department_id': self.env['hr.employee'].browse(employee_id).department_id.id }) holiday = super( Holidays, self.with_context(mail_create_nolog=True, mail_create_nosubscribe=True)).create(values) holiday.add_follower(employee_id) if 'employee_id' in values: holiday._onchange_employee_id() return holiday @api.multi def write(self, values): employee_id = values.get('employee_id', False) if not self._check_state_access_right(values): raise AccessError( _('You cannot set a leave request as \'%s\'. Contact a human resource manager.' ) % values.get('state')) result = super(Holidays, self).write(values) self.add_follower(employee_id) if 'employee_id' in values: self._onchange_employee_id() return result @api.multi def unlink(self): for holiday in self.filtered(lambda holiday: holiday.state not in ['draft', 'cancel', 'confirm']): raise UserError( _('You cannot delete a leave which is in %s state.') % (holiday.state, )) return super(Holidays, self).unlink() #################################################### # Business methods #################################################### @api.multi def _create_resource_leave(self): """ This method will create entry in resource calendar leave object at the time of holidays validated """ for leave in self: self.env['resource.calendar.leaves'].create({ 'name': leave.name, 'date_from': leave.date_from, 'holiday_id': leave.id, 'date_to': leave.date_to, 'resource_id': leave.employee_id.resource_id.id, 'calendar_id': leave.employee_id.resource_calendar_id.id }) return True @api.multi def _remove_resource_leave(self): """ This method will create entry in resource calendar leave object at the time of holidays cancel/removed """ return self.env['resource.calendar.leaves'].search([ ('holiday_id', 'in', self.ids) ]).unlink() @api.multi def action_draft(self): for holiday in self: if not holiday.can_reset: raise UserError( _('Only an HR Manager or the concerned employee can reset to draft.' )) if holiday.state not in ['confirm', 'refuse']: raise UserError( _('Leave request state must be "Refused" or "To Approve" in order to reset to Draft.' )) holiday.write({ 'state': 'draft', 'first_approver_id': False, 'second_approver_id': False, }) linked_requests = holiday.mapped('linked_request_ids') for linked_request in linked_requests: linked_request.action_draft() linked_requests.unlink() return True @api.multi def action_confirm(self): if self.filtered(lambda holiday: holiday.state != 'draft'): raise UserError( _('Leave request must be in Draft state ("To Submit") in order to confirm it.' )) return self.write({'state': 'confirm'}) @api.multi def _check_security_action_approve(self): if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'): raise UserError( _('Only an HR Officer or Manager can approve leave requests.')) @api.multi def action_approve(self): # if double_validation: this method is the first approval approval # if not double_validation: this method calls action_validate() below self._check_security_action_approve() current_employee = self.env['hr.employee'].search( [('user_id', '=', self.env.uid)], limit=1) for holiday in self: if holiday.state != 'confirm': raise UserError( _('Leave request must be confirmed ("To Approve") in order to approve it.' )) if holiday.double_validation: return holiday.write({ 'state': 'validate1', 'first_approver_id': current_employee.id }) else: holiday.action_validate() @api.multi def _prepare_create_by_category(self, employee): self.ensure_one() values = { 'name': self.name, 'type': self.type, 'holiday_type': 'employee', 'holiday_status_id': self.holiday_status_id.id, 'date_from': self.date_from, 'date_to': self.date_to, 'notes': self.notes, 'number_of_days_temp': self.number_of_days_temp, 'parent_id': self.id, 'employee_id': employee.id } return values @api.multi def _check_security_action_validate(self): if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'): raise UserError( _('Only an HR Officer or Manager can approve leave requests.')) @api.multi def action_validate(self): self._check_security_action_validate() current_employee = self.env['hr.employee'].search( [('user_id', '=', self.env.uid)], limit=1) for holiday in self: if holiday.state not in ['confirm', 'validate1']: raise UserError( _('Leave request must be confirmed in order to approve it.' )) if holiday.state == 'validate1' and not holiday.env.user.has_group( 'hr_holidays.group_hr_holidays_manager'): raise UserError( _('Only an HR Manager can apply the second approval on leave requests.' )) holiday.write({'state': 'validate'}) if holiday.double_validation: holiday.write({'second_approver_id': current_employee.id}) else: holiday.write({'first_approver_id': current_employee.id}) if holiday.holiday_type == 'employee' and holiday.type == 'remove': holiday._validate_leave_request() elif holiday.holiday_type == 'category': leaves = self.env['hr.holidays'] for employee in holiday.category_id.employee_ids: values = holiday._prepare_create_by_category(employee) leaves += self.with_context( mail_notify_force_send=False).create(values) # TODO is it necessary to interleave the calls? leaves.action_approve() if leaves and leaves[0].double_validation: leaves.action_validate() return True def _validate_leave_request(self): """ Validate leave requests (holiday_type='employee' and holiday.type='remove') by creating a calendar event and a resource leaves. """ for holiday in self.filtered(lambda request: request.type == 'remove' and request.holiday_type == 'employee'): meeting_values = holiday._prepare_holidays_meeting_values() meeting = self.env['calendar.event'].with_context( no_mail_to_attendees=True).create(meeting_values) holiday.write({'meeting_id': meeting.id}) holiday._create_resource_leave() @api.multi def _prepare_holidays_meeting_values(self): self.ensure_one() meeting_values = { 'name': self.display_name, 'categ_ids': [(6, 0, [self.holiday_status_id.categ_id.id])] if self.holiday_status_id.categ_id else [], 'duration': self.number_of_days_temp * HOURS_PER_DAY, 'description': self.notes, 'user_id': self.user_id.id, 'start': self.date_from, 'stop': self.date_to, 'allday': False, 'state': 'open', # to block that meeting date in the calendar 'privacy': 'confidential' } # Add the partner_id (if exist) as an attendee if self.user_id and self.user_id.partner_id: meeting_values['partner_ids'] = [(4, self.user_id.partner_id.id)] return meeting_values @api.multi def action_refuse(self): self._check_security_action_refuse() current_employee = self.env['hr.employee'].search( [('user_id', '=', self.env.uid)], limit=1) for holiday in self: if holiday.state not in ['confirm', 'validate', 'validate1']: raise UserError( _('Leave request must be confirmed or validated in order to refuse it.' )) if holiday.state == 'validate1': holiday.write({ 'state': 'refuse', 'first_approver_id': current_employee.id }) else: holiday.write({ 'state': 'refuse', 'second_approver_id': current_employee.id }) # Delete the meeting if holiday.meeting_id: holiday.meeting_id.unlink() # If a category that created several holidays, cancel all related holiday.linked_request_ids.action_refuse() self._remove_resource_leave() return True @api.multi def _check_security_action_refuse(self): if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'): raise UserError( _('Only an HR Officer or Manager can refuse leave requests.')) #################################################### # Messaging methods #################################################### @api.multi def _track_subtype(self, init_values): if 'state' in init_values and self.state == 'validate': return 'hr_holidays.mt_holidays_approved' elif 'state' in init_values and self.state == 'validate1': return 'hr_holidays.mt_holidays_first_validated' elif 'state' in init_values and self.state == 'confirm': return 'hr_holidays.mt_holidays_confirmed' elif 'state' in init_values and self.state == 'refuse': return 'hr_holidays.mt_holidays_refused' return super(Holidays, self)._track_subtype(init_values) @api.multi def _notification_recipients(self, message, groups): """ Handle HR users and officers recipients that can validate or refuse holidays directly from email. """ groups = super(Holidays, self)._notification_recipients(message, groups) self.ensure_one() hr_actions = [] if self.state == 'confirm': app_action = self._notification_link_helper( 'controller', controller='/hr_holidays/validate') hr_actions += [{'url': app_action, 'title': _('Approve')}] if self.state in ['confirm', 'validate', 'validate1']: ref_action = self._notification_link_helper( 'controller', controller='/hr_holidays/refuse') hr_actions += [{'url': ref_action, 'title': _('Refuse')}] new_group = ('group_hr_holidays_user', lambda partner: bool(partner.user_ids) and any( user.has_group('hr_holidays.group_hr_holidays_user') for user in partner.user_ids), { 'actions': hr_actions, }) return [new_group] + groups @api.multi def _message_notification_recipients(self, message, recipients): result = super(Holidays, self)._message_notification_recipients( message, recipients) leave_type = self.env[message.model].browse(message.res_id).type title = _("See Leave") if leave_type == 'remove' else _( "See Allocation") for res in result: if result[res].get('button_access'): result[res]['button_access']['title'] = title return result
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 EventEvent(models.Model): """Event""" _name = 'event.event' _description = 'Event' _inherit = ['mail.thread'] _order = 'date_begin' name = fields.Char( string='Event Name', translate=True, required=True, readonly=False, states={'done': [('readonly', True)]}) active = fields.Boolean(default=True) user_id = fields.Many2one( 'res.users', string='Responsible', default=lambda self: self.env.user, track_visibility="onchange", readonly=False, states={'done': [('readonly', True)]}) company_id = fields.Many2one( 'res.company', string='Company', change_default=True, default=lambda self: self.env['res.company']._company_default_get('event.event'), required=False, readonly=False, states={'done': [('readonly', True)]}) organizer_id = fields.Many2one( 'res.partner', string='Organizer', track_visibility="onchange", default=lambda self: self.env.user.company_id.partner_id) event_type_id = fields.Many2one( 'event.type', string='Category', readonly=False, states={'done': [('readonly', True)]}, oldname='type') color = fields.Integer('Kanban Color Index') event_mail_ids = fields.One2many('event.mail', 'event_id', string='Mail Schedule', copy=True) # Seats and computation seats_max = fields.Integer( string='Maximum Attendees Number', oldname='register_max', readonly=True, states={'draft': [('readonly', False)], 'confirm': [('readonly', False)]}, help="For each event you can define a maximum registration of seats(number of attendees), above this numbers the registrations are not accepted.") seats_availability = fields.Selection( [('limited', 'Limited'), ('unlimited', 'Unlimited')], 'Maximum Attendees', required=True, default='unlimited') seats_min = fields.Integer( string='Minimum Attendees', oldname='register_min', help="For each event you can define a minimum reserved seats (number of attendees), if it does not reach the mentioned registrations the event can not be confirmed (keep 0 to ignore this rule)") seats_reserved = fields.Integer( oldname='register_current', string='Reserved Seats', store=True, readonly=True, compute='_compute_seats') seats_available = fields.Integer( oldname='register_avail', string='Available Seats', store=True, readonly=True, compute='_compute_seats') seats_unconfirmed = fields.Integer( oldname='register_prospect', string='Unconfirmed Seat Reservations', store=True, readonly=True, compute='_compute_seats') seats_used = fields.Integer( oldname='register_attended', string='Number of Participants', store=True, readonly=True, compute='_compute_seats') seats_expected = fields.Integer( string='Number of Expected Attendees', readonly=True, compute='_compute_seats') # Registration fields registration_ids = fields.One2many( 'event.registration', 'event_id', string='Attendees', readonly=False, states={'done': [('readonly', True)]}) # Date fields date_tz = fields.Selection('_tz_get', string='Timezone', required=True, default=lambda self: self.env.user.tz or 'UTC') date_begin = fields.Datetime( string='Start Date', required=True, track_visibility='onchange', states={'done': [('readonly', True)]}) date_end = fields.Datetime( string='End Date', required=True, track_visibility='onchange', states={'done': [('readonly', True)]}) date_begin_located = fields.Char(string='Start Date Located', compute='_compute_date_begin_tz') date_end_located = fields.Char(string='End Date Located', compute='_compute_date_end_tz') state = fields.Selection([ ('draft', 'Unconfirmed'), ('cancel', 'Cancelled'), ('confirm', 'Confirmed'), ('done', 'Done')], string='Status', default='draft', readonly=True, required=True, copy=False, help="If event is created, the status is 'Draft'. If event is confirmed for the particular dates the status is set to 'Confirmed'. If the event is over, the status is set to 'Done'. If event is cancelled the status is set to 'Cancelled'.") auto_confirm = fields.Boolean(string='Autoconfirm Registrations') is_online = fields.Boolean('Online Event') address_id = fields.Many2one( 'res.partner', string='Location', default=lambda self: self.env.user.company_id.partner_id, readonly=False, states={'done': [('readonly', True)]}, track_visibility="onchange") country_id = fields.Many2one('res.country', 'Country', related='address_id.country_id', store=True) twitter_hashtag = fields.Char('Twitter Hashtag') description = fields.Html( string='Description', oldname='note', translate=html_translate, sanitize_attributes=False, readonly=False, states={'done': [('readonly', True)]}) # badge fields badge_front = fields.Html(string='Badge Front') badge_back = fields.Html(string='Badge Back') badge_innerleft = fields.Html(string='Badge Inner Left') badge_innerright = fields.Html(string='Badge Inner Right') event_logo = fields.Html(string='Event Logo') @api.multi @api.depends('seats_max', 'registration_ids.state') def _compute_seats(self): """ Determine reserved, available, reserved but unconfirmed and used seats. """ # initialize fields to 0 for event in self: event.seats_unconfirmed = event.seats_reserved = event.seats_used = event.seats_available = 0 # aggregate registrations by event and by state if self.ids: state_field = { 'draft': 'seats_unconfirmed', 'open': 'seats_reserved', 'done': 'seats_used', } query = """ SELECT event_id, state, count(event_id) FROM event_registration WHERE event_id IN %s AND state IN ('draft', 'open', 'done') GROUP BY event_id, state """ self._cr.execute(query, (tuple(self.ids),)) for event_id, state, num in self._cr.fetchall(): event = self.browse(event_id) event[state_field[state]] += num # compute seats_available for event in self: if event.seats_max > 0: event.seats_available = event.seats_max - (event.seats_reserved + event.seats_used) event.seats_expected = event.seats_unconfirmed + event.seats_reserved + event.seats_used @api.model def _tz_get(self): return [(x, x) for x in pytz.all_timezones] @api.one @api.depends('date_tz', 'date_begin') def _compute_date_begin_tz(self): if self.date_begin: self.date_begin_located = format_tz(self.with_context(use_babel=True).env, self.date_begin, tz=self.date_tz) else: self.date_begin_located = False @api.one @api.depends('date_tz', 'date_end') def _compute_date_end_tz(self): if self.date_end: self.date_end_located = format_tz(self.with_context(use_babel=True).env, self.date_end, tz=self.date_tz) else: self.date_end_located = False @api.onchange('event_type_id') def _onchange_type(self): if self.event_type_id: self.seats_min = self.event_type_id.default_registration_min self.seats_max = self.event_type_id.default_registration_max if self.event_type_id.default_registration_max: self.seats_availability = 'limited' if self.event_type_id.auto_confirm: self.auto_confirm = self.event_type_id.auto_confirm if self.event_type_id.use_hashtag: self.twitter_hashtag = self.event_type_id.default_hashtag if self.event_type_id.use_timezone: self.date_tz = self.event_type_id.default_timezone self.is_online = self.event_type_id.is_online if self.event_type_id.event_type_mail_ids: self.event_mail_ids = [(5, 0, 0)] + [{ 'template_id': line.template_id, 'interval_nbr': line.interval_nbr, 'interval_unit': line.interval_unit, 'interval_type': line.interval_type} for line in self.event_type_id.event_type_mail_ids] @api.constrains('seats_min', 'seats_max', 'seats_availability') def _check_seats_min_max(self): if any(event.seats_availability == 'limited' and event.seats_min > event.seats_max for event in self): raise ValidationError(_('Maximum attendees number should be greater than minimum attendees number.')) @api.constrains('seats_max', 'seats_available') def _check_seats_limit(self): if any(event.seats_availability == 'limited' and event.seats_max and event.seats_available < 0 for event in self): raise ValidationError(_('No more available seats.')) @api.one @api.constrains('date_begin', 'date_end') def _check_closing_date(self): if self.date_end < self.date_begin: raise ValidationError(_('Closing Date cannot be set before Beginning Date.')) @api.multi @api.depends('name', 'date_begin', 'date_end') def name_get(self): result = [] for event in self: date_begin = fields.Datetime.from_string(event.date_begin) date_end = fields.Datetime.from_string(event.date_end) dates = [fields.Date.to_string(fields.Datetime.context_timestamp(event, dt)) for dt in [date_begin, date_end] if dt] dates = sorted(set(dates)) result.append((event.id, '%s (%s)' % (event.name, ' - '.join(dates)))) return result @api.model def create(self, vals): res = super(EventEvent, self).create(vals) if res.organizer_id: res.message_subscribe([res.organizer_id.id]) if res.auto_confirm: res.button_confirm() return res @api.multi def write(self, vals): res = super(EventEvent, self).write(vals) if vals.get('organizer_id'): self.message_subscribe([vals['organizer_id']]) return res @api.multi def copy(self, default=None): self.ensure_one() default = dict(default or {}, name=_("%s (copy)") % (self.name)) return super(EventEvent, self).copy(default) @api.one def button_draft(self): self.state = 'draft' @api.multi def button_cancel(self): if any('done' in event.mapped('registration_ids.state') for event in self): raise UserError(_("There are already attendees who attended this event. Please reset it to draft if you want to cancel this event.")) self.registration_ids.write({'state': 'cancel'}) self.state = 'cancel' @api.one def button_done(self): self.state = 'done' @api.one def button_confirm(self): self.state = 'confirm' @api.one def mail_attendees(self, template_id, force_send=False, filter_func=lambda self: self.state != 'cancel'): for attendee in self.registration_ids.filtered(filter_func): self.env['mail.template'].browse(template_id).send_mail(attendee.id, force_send=force_send) @api.multi def _is_event_registrable(self): return True
class EventRegistration(models.Model): _name = 'event.registration' _description = 'Attendee' _inherit = ['mail.thread'] _order = 'name, create_date desc' origin = fields.Char( string='Source Document', readonly=True, help="Reference of the document that created the registration, for example a sales order") event_id = fields.Many2one( 'event.event', string='Event', required=True, readonly=True, states={'draft': [('readonly', False)]}) partner_id = fields.Many2one( 'res.partner', string='Contact', states={'done': [('readonly', True)]}) date_open = fields.Datetime(string='Registration Date', readonly=True, default=lambda self: fields.datetime.now()) # weird crash is directly now date_closed = fields.Datetime(string='Attended Date', readonly=True) event_begin_date = fields.Datetime(string="Event Start Date", related='event_id.date_begin', readonly=True) event_end_date = fields.Datetime(string="Event End Date", related='event_id.date_end', readonly=True) company_id = fields.Many2one( 'res.company', string='Company', related='event_id.company_id', store=True, readonly=True, states={'draft': [('readonly', False)]}) state = fields.Selection([ ('draft', 'Unconfirmed'), ('cancel', 'Cancelled'), ('open', 'Confirmed'), ('done', 'Attended')], string='Status', default='draft', readonly=True, copy=False, track_visibility='onchange') email = fields.Char(string='Email') phone = fields.Char(string='Phone') name = fields.Char(string='Attendee Name', index=True) @api.one @api.constrains('event_id', 'state') def _check_seats_limit(self): if self.event_id.seats_availability == 'limited' and self.event_id.seats_max and self.event_id.seats_available < (1 if self.state == 'draft' else 0): raise ValidationError(_('No more seats available for this event.')) @api.multi def _check_auto_confirmation(self): if self._context.get('registration_force_draft'): return False if any(registration.event_id.state != 'confirm' or not registration.event_id.auto_confirm or (not registration.event_id.seats_available and registration.event_id.seats_availability == 'limited') for registration in self): return False return True @api.model def create(self, vals): registration = super(EventRegistration, self).create(vals) if registration._check_auto_confirmation(): registration.sudo().confirm_registration() return registration @api.model def _prepare_attendee_values(self, registration): """ Method preparing the values to create new attendees based on a sales order line. It takes some registration data (dict-based) that are optional values coming from an external input like a web page. This method is meant to be inherited in various addons that sell events. """ partner_id = registration.pop('partner_id', self.env.user.partner_id) event_id = registration.pop('event_id', False) data = { 'name': registration.get('name', partner_id.name), 'phone': registration.get('phone', partner_id.phone), 'email': registration.get('email', partner_id.email), 'partner_id': partner_id.id, 'event_id': event_id and event_id.id or False, } data.update({key: value for key, value in registration.items() if key in self._fields}) return data @api.one def do_draft(self): self.state = 'draft' @api.one def confirm_registration(self): self.state = 'open' # auto-trigger after_sub (on subscribe) mail schedulers, if needed onsubscribe_schedulers = self.event_id.event_mail_ids.filtered( lambda s: s.interval_type == 'after_sub') onsubscribe_schedulers.execute() @api.one def button_reg_close(self): """ Close Registration """ today = fields.Datetime.now() if self.event_id.date_begin <= today and self.event_id.state == 'confirm': self.write({'state': 'done', 'date_closed': today}) elif self.event_id.state == 'draft': raise UserError(_("You must wait the event confirmation before doing this action.")) else: raise UserError(_("You must wait the event starting day before doing this action.")) @api.one def button_reg_cancel(self): self.state = 'cancel' @api.onchange('partner_id') def _onchange_partner(self): if self.partner_id: contact_id = self.partner_id.address_get().get('contact', False) if contact_id: contact = self.env['res.partner'].browse(contact_id) self.name = contact.name or self.name self.email = contact.email or self.email self.phone = contact.phone or self.phone @api.multi def message_get_suggested_recipients(self): recipients = super(EventRegistration, self).message_get_suggested_recipients() public_users = self.env['res.users'].sudo() public_groups = self.env.ref("base.group_public", raise_if_not_found=False) if public_groups: public_users = public_groups.sudo().with_context(active_test=False).mapped("users") try: for attendee in self: is_public = attendee.sudo().with_context(active_test=False).partner_id.user_ids in public_users if public_users else False if attendee.partner_id and not is_public: attendee._message_add_suggested_recipient(recipients, partner=attendee.partner_id, reason=_('Customer')) elif attendee.email: attendee._message_add_suggested_recipient(recipients, email=attendee.email, reason=_('Customer Email')) except AccessError: # no read access rights -> ignore suggested recipients pass return recipients def _message_post_after_hook(self, message): if self.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.email) if new_partner: self.search([ ('partner_id', '=', False), ('email', '=', new_partner.email), ('state', 'not in', ['cancel']), ]).write({'partner_id': new_partner.id}) return super(EventRegistration, self)._message_post_after_hook(message) @api.multi def message_get_default_recipients(self): # Prioritize registration email over partner_id, which may be shared when a single # partner booked multiple seats return { r.id: {'partner_ids': [], 'email_to': r.email, 'email_cc': False} for r in self } @api.multi def action_send_badge_email(self): """ Open a window to compose an email, with the template - 'event_badge' message loaded by default """ self.ensure_one() template = self.env.ref('event.event_registration_mail_template_badge') compose_form = self.env.ref('mail.email_compose_message_wizard_form') ctx = dict( default_model='event.registration', default_res_id=self.id, default_use_template=bool(template), default_template_id=template.id, default_composition_mode='comment', ) return { 'name': _('Compose Email'), 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'mail.compose.message', 'views': [(compose_form.id, 'form')], 'view_id': compose_form.id, 'target': 'new', 'context': ctx, } @api.multi def get_date_range_str(self): self.ensure_one() today = fields.Datetime.from_string(fields.Datetime.now()) event_date = fields.Datetime.from_string(self.event_begin_date) diff = (event_date.date() - today.date()) if diff.days <= 0: return _('today') elif diff.days == 1: return _('tomorrow') elif (diff.days < 7): return _('in %d days') % (diff.days, ) elif (diff.days < 14): return _('next week') elif event_date.month == (today + relativedelta(months=+1)).month: return _('next month') else: return _('on ') + format_tz(self.with_context({'use_babel': True}).env, self.event_begin_date, tz=self.event_id.date_tz or 'UTC') @api.multi def summary(self): self.ensure_one() return {'information': []}
class Page(models.Model): _name = 'website.page' _inherits = {'ir.ui.view': 'view_id'} _inherit = 'website.published.mixin' _description = 'Page' url = fields.Char('Page URL') website_ids = fields.Many2many('website', string='Websites') view_id = fields.Many2one('ir.ui.view', string='View', required=True, ondelete="cascade") website_indexed = fields.Boolean('Page Indexed', default=True) date_publish = fields.Datetime('Publishing Date') # This is needed to be able to display if page is a menu in /website/pages menu_ids = fields.One2many('website.menu', 'page_id', 'Related Menus') is_homepage = fields.Boolean(compute='_compute_homepage', inverse='_set_homepage', string='Homepage') is_visible = fields.Boolean(compute='_compute_visible', string='Is Visible') @api.one def _compute_homepage(self): self.is_homepage = self == self.env['website'].get_current_website().homepage_id @api.one def _set_homepage(self): website = self.env['website'].get_current_website() if self.is_homepage: if website.homepage_id != self: website.write({'homepage_id': self.id}) else: if website.homepage_id == self: website.write({'homepage_id': None}) @api.one def _compute_visible(self): self.is_visible = self.website_published and (not self.date_publish or self.date_publish < fields.Datetime.now()) @api.model def get_page_info(self, id, website_id): domain = ['|', ('website_ids', 'in', website_id), ('website_ids', '=', False), ('id', '=', id)] item = self.search_read(domain, fields=['id', 'name', 'url', 'website_published', 'website_indexed', 'date_publish', 'menu_ids', 'is_homepage'], limit=1) return item @api.multi def get_view_identifier(self): """ Get identifier of this page view that may be used to render it """ return self.view_id.id @api.model def save_page_info(self, website_id, data): website = self.env['website'].browse(website_id) page = self.browse(int(data['id'])) #If URL has been edited, slug it original_url = page.url url = data['url'] if not url.startswith('/'): url = '/' + url if page.url != url: url = '/' + slugify(url, max_length=1024, path=True) url = self.env['website'].get_unique_path(url) #If name has changed, check for key uniqueness if page.name != data['name']: page_key = self.env['website'].get_unique_key(slugify(data['name'])) else: page_key = page.key menu = self.env['website.menu'].search([('page_id', '=', int(data['id']))]) if not data['is_menu']: #If the page is no longer in menu, we should remove its website_menu if menu: menu.unlink() else: #The page is now a menu, check if has already one if menu: menu.write({'url': url}) else: self.env['website.menu'].create({ 'name': data['name'], 'url': url, 'page_id': data['id'], 'parent_id': website.menu_id.id, 'website_id': website.id, }) page.write({ 'key': page_key, 'name': data['name'], 'url': url, 'website_published': data['website_published'], 'website_indexed': data['website_indexed'], 'date_publish': data['date_publish'] or None, 'is_homepage': data['is_homepage'], }) # Create redirect if needed if data['create_redirect']: self.env['website.redirect'].create({ 'type': data['redirect_type'], 'url_from': original_url, 'url_to': url, 'website_id': website.id, }) return url @api.multi def copy(self, default=None): view = self.env['ir.ui.view'].browse(self.view_id.id) # website.page's ir.ui.view should have a different key than the one it # is copied from. # (eg: website_version: an ir.ui.view record with the same key is # expected to be the same ir.ui.view but from another version) new_view = view.copy({'key': view.key + '.copy', 'name': '%s %s' % (view.name, _('(copy)'))}) default = { 'name': '%s %s' % (self.name, _('(copy)')), 'url': self.env['website'].get_unique_path(self.url), 'view_id': new_view.id, } return super(Page, self).copy(default=default) @api.model def clone_page(self, page_id, clone_menu=True): """ Clone a page, given its identifier :param page_id : website.page identifier """ page = self.browse(int(page_id)) new_page = page.copy() if clone_menu: menu = self.env['website.menu'].search([('page_id', '=', page_id)], limit=1) if menu: # If the page being cloned has a menu, clone it too new_menu = menu.copy() new_menu.write({'url': new_page.url, 'name': '%s %s' % (menu.name, _('(copy)')), 'page_id': new_page.id}) return new_page.url + '?enable_editor=1' @api.multi def unlink(self): """ When a website_page is deleted, the ORM does not delete its ir_ui_view. So we got to delete it ourself, but only if the ir_ui_view is not used by another website_page. """ # Handle it's ir_ui_view for page in self: # Other pages linked to the ir_ui_view of the page being deleted (will it even be possible?) pages_linked_to_iruiview = self.search( [('view_id', '=', page.view_id.id), ('id', '!=', page.id)] ) if not pages_linked_to_iruiview: # If there is no other pages linked to that ir_ui_view, we can delete the ir_ui_view page.view_id.unlink() # And then delete the website_page itself return super(Page, self).unlink() @api.model def delete_page(self, page_id): """ Delete a page, given its identifier :param page_id : website.page identifier """ # If we are deleting a page (that could possibly be a menu with a page) page = self.env['website.page'].browse(int(page_id)) if page: # Check if it is a menu with a page and also delete menu if so menu = self.env['website.menu'].search([('page_id', '=', page.id)], limit=1) if menu: menu.unlink() page.unlink() @api.multi def write(self, vals): if 'url' in vals and not vals['url'].startswith('/'): vals['url'] = '/' + vals['url'] result = super(Page, self).write(vals) return result
class HrAttendance(models.Model): _name = "hr.attendance" _description = "Attendance" _order = "check_in desc" def _default_employee(self): return self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) employee_id = fields.Many2one('hr.employee', string="Employee", default=_default_employee, required=True, ondelete='cascade', index=True) department_id = fields.Many2one('hr.department', string="Department", related="employee_id.department_id", readonly=True) check_in = fields.Datetime(string="Check In", default=fields.Datetime.now, required=True) check_out = fields.Datetime(string="Check Out") worked_hours = fields.Float(string='Worked Hours', compute='_compute_worked_hours', store=True, readonly=True) @api.multi def name_get(self): result = [] for attendance in self: if not attendance.check_out: result.append( (attendance.id, _("%(empl_name)s from %(check_in)s") % { 'empl_name': attendance.employee_id.name, 'check_in': fields.Datetime.to_string( fields.Datetime.context_timestamp( attendance, fields.Datetime.from_string( attendance.check_in))), })) else: result.append( (attendance.id, _("%(empl_name)s from %(check_in)s to %(check_out)s") % { 'empl_name': attendance.employee_id.name, 'check_in': fields.Datetime.to_string( fields.Datetime.context_timestamp( attendance, fields.Datetime.from_string( attendance.check_in))), 'check_out': fields.Datetime.to_string( fields.Datetime.context_timestamp( attendance, fields.Datetime.from_string( attendance.check_out))), })) return result @api.depends('check_in', 'check_out') def _compute_worked_hours(self): for attendance in self: if attendance.check_out: delta = datetime.strptime( attendance.check_out, DEFAULT_SERVER_DATETIME_FORMAT) - datetime.strptime( attendance.check_in, DEFAULT_SERVER_DATETIME_FORMAT) attendance.worked_hours = delta.total_seconds() / 3600.0 @api.constrains('check_in', 'check_out') def _check_validity_check_in_check_out(self): """ verifies if check_in is earlier than check_out. """ for attendance in self: if attendance.check_in and attendance.check_out: if attendance.check_out < attendance.check_in: raise exceptions.ValidationError( _('"Check Out" time cannot be earlier than "Check In" time.' )) @api.constrains('check_in', 'check_out', 'employee_id') def _check_validity(self): """ Verifies the validity of the attendance record compared to the others from the same employee. For the same employee we must have : * maximum 1 "open" attendance record (without check_out) * no overlapping time slices with previous employee records """ for attendance in self: # we take the latest attendance before our check_in time and check it doesn't overlap with ours last_attendance_before_check_in = self.env['hr.attendance'].search( [ ('employee_id', '=', attendance.employee_id.id), ('check_in', '<=', attendance.check_in), ('id', '!=', attendance.id), ], order='check_in desc', limit=1) if last_attendance_before_check_in and last_attendance_before_check_in.check_out and last_attendance_before_check_in.check_out > attendance.check_in: raise exceptions.ValidationError( _("Cannot create new attendance record for %(empl_name)s, the employee was already checked in on %(datetime)s" ) % { 'empl_name': attendance.employee_id.name, 'datetime': fields.Datetime.to_string( fields.Datetime.context_timestamp( self, fields.Datetime.from_string( attendance.check_in))), }) if not attendance.check_out: # if our attendance is "open" (no check_out), we verify there is no other "open" attendance no_check_out_attendances = self.env['hr.attendance'].search( [ ('employee_id', '=', attendance.employee_id.id), ('check_out', '=', False), ('id', '!=', attendance.id), ], order='check_in desc', limit=1) if no_check_out_attendances: raise exceptions.ValidationError( _("Cannot create new attendance record for %(empl_name)s, the employee hasn't checked out since %(datetime)s" ) % { 'empl_name': attendance.employee_id.name, 'datetime': fields.Datetime.to_string( fields.Datetime.context_timestamp( self, fields.Datetime.from_string( no_check_out_attendances.check_in))), }) else: # we verify that the latest attendance with check_in time before our check_out time # is the same as the one before our check_in time computed before, otherwise it overlaps last_attendance_before_check_out = self.env[ 'hr.attendance'].search([ ('employee_id', '=', attendance.employee_id.id), ('check_in', '<', attendance.check_out), ('id', '!=', attendance.id), ], order='check_in desc', limit=1) if last_attendance_before_check_out and last_attendance_before_check_in != last_attendance_before_check_out: raise exceptions.ValidationError( _("Cannot create new attendance record for %(empl_name)s, the employee was already checked in on %(datetime)s" ) % { 'empl_name': attendance.employee_id.name, 'datetime': fields.Datetime.to_string( fields.Datetime.context_timestamp( self, fields.Datetime.from_string( last_attendance_before_check_out. check_in))), }) @api.multi def copy(self): raise exceptions.UserError(_('You cannot duplicate an attendance.'))
class ImBus(models.Model): _name = 'bus.bus' create_date = fields.Datetime('Create date') channel = fields.Char('Channel') message = fields.Char('Message') @api.model def gc(self): timeout_ago = datetime.datetime.utcnow() - datetime.timedelta( seconds=TIMEOUT * 2) domain = [('create_date', '<', timeout_ago.strftime(DEFAULT_SERVER_DATETIME_FORMAT))] return self.sudo().search(domain).unlink() @api.model def sendmany(self, notifications): channels = set() for channel, message in notifications: channels.add(channel) values = { "channel": json_dump(channel), "message": json_dump(message) } self.sudo().create(values) if random.random() < 0.01: self.gc() if channels: # We have to wait until the notifications are commited in database. # When calling `NOTIFY imbus`, some concurrent threads will be # awakened and will fetch the notification in the bus table. If the # transaction is not commited yet, there will be nothing to fetch, # and the longpolling will return no notification. def notify(): with noblecrm.sql_db.db_connect('postgres').cursor() as cr: cr.execute("notify imbus, %s", (json_dump(list(channels)), )) self._cr.after('commit', notify) @api.model def sendone(self, channel, message): self.sendmany([[channel, message]]) @api.model def poll(self, channels, last=0, options=None, force_status=False): if options is None: options = {} # first poll return the notification in the 'buffer' if last == 0: timeout_ago = datetime.datetime.utcnow() - datetime.timedelta( seconds=TIMEOUT) domain = [('create_date', '>', timeout_ago.strftime(DEFAULT_SERVER_DATETIME_FORMAT))] else: # else returns the unread notifications domain = [('id', '>', last)] channels = [json_dump(c) for c in channels] domain.append(('channel', 'in', channels)) notifications = self.sudo().search_read(domain) # list of notification to return result = [] for notif in notifications: result.append({ 'id': notif['id'], 'channel': json.loads(notif['channel']), 'message': json.loads(notif['message']), }) if result or force_status: partner_ids = options.get('bus_presence_partner_ids') if partner_ids: partners = self.env['res.partner'].browse(partner_ids) result += [{ 'id': -1, 'channel': (self._cr.dbname, 'bus.presence'), 'message': { 'id': r.id, 'im_status': r.im_status } } for r in partners] return result
class Message(models.Model): """ Messages model: system notification (replacing res.log notifications), comments (OpenChatter discussion) and incoming emails. """ _name = 'mail.message' _description = 'Message' _order = 'id desc' _rec_name = 'record_name' _message_read_limit = 30 @api.model def _get_default_from(self): if self.env.user.email: return formataddr((self.env.user.name, self.env.user.email)) raise UserError( _("Unable to send email, please configure the sender's email address." )) @api.model def _get_default_author(self): return self.env.user.partner_id # content subject = fields.Char('Subject') date = fields.Datetime('Date', default=fields.Datetime.now) body = fields.Html('Contents', default='', sanitize_style=True, strip_classes=True) attachment_ids = fields.Many2many( 'ir.attachment', 'message_attachment_rel', 'message_id', 'attachment_id', string='Attachments', help= 'Attachments are linked to a document through model / res_id and to the message ' 'through this field.') parent_id = fields.Many2one('mail.message', 'Parent Message', index=True, ondelete='set null', help="Initial thread message.") child_ids = fields.One2many('mail.message', 'parent_id', 'Child Messages') # related document model = fields.Char('Related Document Model', index=True) res_id = fields.Integer('Related Document ID', index=True) record_name = fields.Char('Message Record Name', help="Name get of the related document.") # characteristics message_type = fields.Selection( [('email', 'Email'), ('comment', 'Comment'), ('notification', 'System notification')], 'Type', required=True, default='email', help="Message type: email for email message, notification for system " "message, comment for other messages such as user replies", oldname='type') subtype_id = fields.Many2one('mail.message.subtype', 'Subtype', ondelete='set null', index=True) mail_activity_type_id = fields.Many2one('mail.activity.type', 'Mail Activity Type', index=True, ondelete='set null') # origin email_from = fields.Char( 'From', default=_get_default_from, help= "Email address of the sender. This field is set when no matching partner is found and replaces the author_id field in the chatter." ) author_id = fields.Many2one( 'res.partner', 'Author', index=True, ondelete='set null', default=_get_default_author, help= "Author of the message. If not set, email_from may hold an email address that did not match any partner." ) author_avatar = fields.Binary("Author's avatar", related='author_id.image_small') # recipients partner_ids = fields.Many2many('res.partner', string='Recipients') needaction_partner_ids = fields.Many2many( 'res.partner', 'mail_message_res_partner_needaction_rel', string='Partners with Need Action') needaction = fields.Boolean('Need Action', compute='_get_needaction', search='_search_needaction', help='Need Action') channel_ids = fields.Many2many('mail.channel', 'mail_message_mail_channel_rel', string='Channels') # notifications notification_ids = fields.One2many('mail.notification', 'mail_message_id', 'Notifications', auto_join=True, copy=False) # user interface starred_partner_ids = fields.Many2many( 'res.partner', 'mail_message_res_partner_starred_rel', string='Favorited By') starred = fields.Boolean( 'Starred', compute='_get_starred', search='_search_starred', help='Current user has a starred notification linked to this message') # tracking tracking_value_ids = fields.One2many( 'mail.tracking.value', 'mail_message_id', string='Tracking values', groups="base.group_no_one", help= 'Tracked values are stored in a separate model. This field allow to reconstruct ' 'the tracking and to generate statistics on the model.') # mail gateway no_auto_thread = fields.Boolean( 'No threading for answers', help= 'Answers do not go in the original document discussion thread. This has an impact on the generated message-id.' ) message_id = fields.Char('Message-Id', help='Message unique identifier', index=True, readonly=1, copy=False) reply_to = fields.Char( 'Reply-To', help= 'Reply email address. Setting the reply_to bypasses the automatic thread creation.' ) mail_server_id = fields.Many2one('ir.mail_server', 'Outgoing mail server') @api.multi def _get_needaction(self): """ Need action on a mail.message = notified on my channel """ my_messages = self.env['mail.notification'].sudo().search([ ('mail_message_id', 'in', self.ids), ('res_partner_id', '=', self.env.user.partner_id.id), ('is_read', '=', False) ]).mapped('mail_message_id') for message in self: message.needaction = message in my_messages @api.model def _search_needaction(self, operator, operand): if operator == '=' and operand: return [ '&', ('notification_ids.res_partner_id', '=', self.env.user.partner_id.id), ('notification_ids.is_read', '=', False) ] return [ '&', ('notification_ids.res_partner_id', '=', self.env.user.partner_id.id), ('notification_ids.is_read', '=', True) ] @api.depends('starred_partner_ids') def _get_starred(self): """ Compute if the message is starred by the current user. """ # TDE FIXME: use SQL starred = self.sudo().filtered( lambda msg: self.env.user.partner_id in msg.starred_partner_ids) for message in self: message.starred = message in starred @api.model def _search_starred(self, operator, operand): if operator == '=' and operand: return [('starred_partner_ids', 'in', [self.env.user.partner_id.id])] return [('starred_partner_ids', 'not in', [self.env.user.partner_id.id])] #------------------------------------------------------ # Notification API #------------------------------------------------------ @api.model def mark_all_as_read(self, channel_ids=None, domain=None): """ Remove all needactions of the current partner. If channel_ids is given, restrict to messages written in one of those channels. """ partner_id = self.env.user.partner_id.id delete_mode = not self.env.user.share # delete employee notifs, keep customer ones if not domain and delete_mode: query = "DELETE FROM mail_message_res_partner_needaction_rel WHERE res_partner_id IN %s" args = [(partner_id, )] if channel_ids: query += """ AND mail_message_id in (SELECT mail_message_id FROM mail_message_mail_channel_rel WHERE mail_channel_id in %s)""" args += [tuple(channel_ids)] query += " RETURNING mail_message_id as id" self._cr.execute(query, args) self.invalidate_cache() ids = [m['id'] for m in self._cr.dictfetchall()] else: # not really efficient method: it does one db request for the # search, and one for each message in the result set to remove the # current user from the relation. msg_domain = [('needaction_partner_ids', 'in', partner_id)] if channel_ids: msg_domain += [('channel_ids', 'in', channel_ids)] unread_messages = self.search(expression.AND([msg_domain, domain])) notifications = self.env['mail.notification'].sudo().search([ ('mail_message_id', 'in', unread_messages.ids), ('res_partner_id', '=', self.env.user.partner_id.id), ('is_read', '=', False) ]) if delete_mode: notifications.unlink() else: notifications.write({'is_read': True}) ids = unread_messages.mapped('id') notification = { 'type': 'mark_as_read', 'message_ids': ids, 'channel_ids': channel_ids } self.env['bus.bus'].sendone( (self._cr.dbname, 'res.partner', self.env.user.partner_id.id), notification) return ids @api.multi def mark_as_unread(self, channel_ids=None): """ Add needactions to messages for the current partner. """ partner_id = self.env.user.partner_id.id for message in self: message.write({'needaction_partner_ids': [(4, partner_id)]}) ids = [m.id for m in self] notification = { 'type': 'mark_as_unread', 'message_ids': ids, 'channel_ids': channel_ids } self.env['bus.bus'].sendone( (self._cr.dbname, 'res.partner', self.env.user.partner_id.id), notification) @api.multi def set_message_done(self): """ Remove the needaction from messages for the current partner. """ partner_id = self.env.user.partner_id delete_mode = not self.env.user.share # delete employee notifs, keep customer ones notifications = self.env['mail.notification'].sudo().search([ ('mail_message_id', 'in', self.ids), ('res_partner_id', '=', partner_id.id), ('is_read', '=', False) ]) if not notifications: return # notifies changes in messages through the bus. To minimize the number of # notifications, we need to group the messages depending on their channel_ids groups = [] messages = notifications.mapped('mail_message_id') current_channel_ids = messages[0].channel_ids current_group = [] for record in messages: if record.channel_ids == current_channel_ids: current_group.append(record.id) else: groups.append((current_group, current_channel_ids)) current_group = [record.id] current_channel_ids = record.channel_ids groups.append((current_group, current_channel_ids)) current_group = [record.id] current_channel_ids = record.channel_ids if delete_mode: notifications.unlink() else: notifications.write({'is_read': True}) for (msg_ids, channel_ids) in groups: notification = { 'type': 'mark_as_read', 'message_ids': msg_ids, 'channel_ids': [c.id for c in channel_ids] } self.env['bus.bus'].sendone( (self._cr.dbname, 'res.partner', partner_id.id), notification) @api.model def unstar_all(self): """ Unstar messages for the current partner. """ partner_id = self.env.user.partner_id.id starred_messages = self.search([('starred_partner_ids', 'in', partner_id)]) starred_messages.write({'starred_partner_ids': [(3, partner_id)]}) ids = [m.id for m in starred_messages] notification = { 'type': 'toggle_star', 'message_ids': ids, 'starred': False } self.env['bus.bus'].sendone( (self._cr.dbname, 'res.partner', self.env.user.partner_id.id), notification) @api.multi def toggle_message_starred(self): """ Toggle messages as (un)starred. Technically, the notifications related to uid are set to (un)starred. """ # a user should always be able to star a message he can read self.check_access_rule('read') starred = not self.starred if starred: self.sudo().write( {'starred_partner_ids': [(4, self.env.user.partner_id.id)]}) else: self.sudo().write( {'starred_partner_ids': [(3, self.env.user.partner_id.id)]}) notification = { 'type': 'toggle_star', 'message_ids': [self.id], 'starred': starred } self.env['bus.bus'].sendone( (self._cr.dbname, 'res.partner', self.env.user.partner_id.id), notification) #------------------------------------------------------ # Message loading for web interface #------------------------------------------------------ @api.model def _message_read_dict_postprocess(self, messages, message_tree): """ Post-processing on values given by message_read. This method will handle partners in batch to avoid doing numerous queries. :param list messages: list of message, as get_dict result :param dict message_tree: {[msg.id]: msg browse record as super user} """ # 1. Aggregate partners (author_id and partner_ids), attachments and tracking values partners = self.env['res.partner'].sudo() attachments = self.env['ir.attachment'] message_ids = list(message_tree.keys()) for message in message_tree.values(): if message.author_id: partners |= message.author_id if message.subtype_id and message.partner_ids: # take notified people of message with a subtype partners |= message.partner_ids elif not message.subtype_id and message.partner_ids: # take specified people of message without a subtype (log) partners |= message.partner_ids if message.needaction_partner_ids: # notified partners |= message.needaction_partner_ids if message.attachment_ids: attachments |= message.attachment_ids # Read partners as SUPERUSER -> message being browsed as SUPERUSER it is already the case partners_names = partners.name_get() partner_tree = dict( (partner[0], partner) for partner in partners_names) # 2. Attachments as SUPERUSER, because could receive msg and attachments for doc uid cannot see attachments_data = attachments.sudo().read( ['id', 'datas_fname', 'name', 'mimetype']) safari = request and request.httprequest.user_agent.browser == 'safari' attachments_tree = dict((attachment['id'], { 'id': attachment['id'], 'filename': attachment['datas_fname'], 'name': attachment['name'], 'mimetype': 'application/octet-stream' if safari and 'video' in attachment['mimetype'] else attachment['mimetype'], }) for attachment in attachments_data) # 3. Tracking values tracking_values = self.env['mail.tracking.value'].sudo().search([ ('mail_message_id', 'in', message_ids) ]) message_to_tracking = dict() tracking_tree = dict.fromkeys(tracking_values.ids, False) for tracking in tracking_values: message_to_tracking.setdefault(tracking.mail_message_id.id, list()).append(tracking.id) tracking_tree[tracking.id] = { 'id': tracking.id, 'changed_field': tracking.field_desc, 'old_value': tracking.get_old_display_value()[0], 'new_value': tracking.get_new_display_value()[0], 'field_type': tracking.field_type, } # 4. Update message dictionaries for message_dict in messages: message_id = message_dict.get('id') message = message_tree[message_id] if message.author_id: author = partner_tree[message.author_id.id] else: author = (0, message.email_from) partner_ids = [] if message.subtype_id: partner_ids = [ partner_tree[partner.id] for partner in message.partner_ids if partner.id in partner_tree ] else: partner_ids = [ partner_tree[partner.id] for partner in message.partner_ids if partner.id in partner_tree ] customer_email_data = [] for notification in message.notification_ids.filtered( lambda notif: notif.res_partner_id.partner_share and notif. res_partner_id.active): customer_email_data.append( (partner_tree[notification.res_partner_id.id][0], partner_tree[notification.res_partner_id.id][1], notification.email_status)) attachment_ids = [] for attachment in message.attachment_ids: if attachment.id in attachments_tree: attachment_ids.append(attachments_tree[attachment.id]) tracking_value_ids = [] for tracking_value_id in message_to_tracking.get( message_id, list()): if tracking_value_id in tracking_tree: tracking_value_ids.append(tracking_tree[tracking_value_id]) message_dict.update({ 'author_id': author, 'partner_ids': partner_ids, 'customer_email_status': (all(d[2] == 'sent' for d in customer_email_data) and 'sent') or (any(d[2] == 'exception' for d in customer_email_data) and 'exception') or (any(d[2] == 'bounce' for d in customer_email_data) and 'bounce') or 'ready', 'customer_email_data': customer_email_data, 'attachment_ids': attachment_ids, 'tracking_value_ids': tracking_value_ids, }) return True @api.model def message_fetch(self, domain, limit=20): return self.search(domain, limit=limit).message_format() @api.multi def message_format(self): """ Get the message values in the format for web client. Since message values can be broadcasted, computed fields MUST NOT BE READ and broadcasted. :returns list(dict). Example : { 'body': HTML content of the message 'model': u'res.partner', 'record_name': u'Agrolait', 'attachment_ids': [ { 'file_type_icon': u'webimage', 'id': 45, 'name': u'sample.png', 'filename': u'sample.png' } ], 'needaction_partner_ids': [], # list of partner ids 'res_id': 7, 'tracking_value_ids': [ { 'old_value': "", 'changed_field': "Customer", 'id': 2965, 'new_value': "Axelor" } ], 'author_id': (3, u'Administrator'), 'email_from': '*****@*****.**' # email address or False 'subtype_id': (1, u'Discussions'), 'channel_ids': [], # list of channel ids 'date': '2015-06-30 08:22:33', 'partner_ids': [[7, "Sacha Du Bourg-Palette"]], # list of partner name_get 'message_type': u'comment', 'id': 59, 'subject': False 'is_note': True # only if the subtype is internal } """ message_values = self.read([ 'id', 'body', 'date', 'author_id', 'email_from', # base message fields 'message_type', 'subtype_id', 'subject', # message specific 'model', 'res_id', 'record_name', # document related 'channel_ids', 'partner_ids', # recipients 'starred_partner_ids', # list of partner ids for whom the message is starred ]) message_tree = dict((m.id, m) for m in self.sudo()) self._message_read_dict_postprocess(message_values, message_tree) # add subtype data (is_note flag, subtype_description). Do it as sudo # because portal / public may have to look for internal subtypes subtype_ids = [ msg['subtype_id'][0] for msg in message_values if msg['subtype_id'] ] subtypes = self.env['mail.message.subtype'].sudo().browse( subtype_ids).read(['internal', 'description']) subtypes_dict = dict((subtype['id'], subtype) for subtype in subtypes) # fetch notification status notif_dict = {} notifs = self.env['mail.notification'].sudo().search([ ('mail_message_id', 'in', list(mid for mid in message_tree)), ('is_read', '=', False) ]) for notif in notifs: mid = notif.mail_message_id.id if not notif_dict.get(mid): notif_dict[mid] = {'partner_id': list()} notif_dict[mid]['partner_id'].append(notif.res_partner_id.id) for message in message_values: message['needaction_partner_ids'] = notif_dict.get( message['id'], dict()).get('partner_id', []) message['is_note'] = message['subtype_id'] and subtypes_dict[ message['subtype_id'][0]]['internal'] message['subtype_description'] = message[ 'subtype_id'] and subtypes_dict[message['subtype_id'] [0]]['description'] if message['model'] and self.env[ message['model']]._original_module: message['module_icon'] = modules.module.get_module_icon( self.env[message['model']]._original_module) return message_values #------------------------------------------------------ # mail_message internals #------------------------------------------------------ @api.model_cr def init(self): self._cr.execute( """SELECT indexname FROM pg_indexes WHERE indexname = 'mail_message_model_res_id_idx'""" ) if not self._cr.fetchone(): self._cr.execute( """CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""" ) @api.model def _find_allowed_model_wise(self, doc_model, doc_dict): doc_ids = list(doc_dict) allowed_doc_ids = self.env[doc_model].with_context( active_test=False).search([('id', 'in', doc_ids)]).ids return set([ message_id for allowed_doc_id in allowed_doc_ids for message_id in doc_dict[allowed_doc_id] ]) @api.model def _find_allowed_doc_ids(self, model_ids): IrModelAccess = self.env['ir.model.access'] allowed_ids = set() for doc_model, doc_dict in model_ids.items(): if not IrModelAccess.check(doc_model, 'read', False): continue allowed_ids |= self._find_allowed_model_wise(doc_model, doc_dict) return allowed_ids @api.model def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): """ Override that adds specific access rights of mail.message, to remove ids uid could not see according to our custom rules. Please refer to check_access_rule for more details about those rules. Non employees users see only message with subtype (aka do not see internal logs). After having received ids of a classic search, keep only: - if author_id == pid, uid is the author, OR - uid belongs to a notified channel, OR - uid is in the specified recipients, OR - uid has a notification on the message, OR - uid have read access to the related document is model, res_id - otherwise: remove the id """ # Rules do not apply to administrator if self._uid == SUPERUSER_ID: return super(Message, self)._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid) # Non-employee see only messages with a subtype (aka, no internal logs) if not self.env['res.users'].has_group('base.group_user'): args = [ '&', '&', ('subtype_id', '!=', False), ('subtype_id.internal', '=', False) ] + list(args) # Perform a super with count as False, to have the ids, not a counter ids = super(Message, self)._search(args, offset=offset, limit=limit, order=order, count=False, access_rights_uid=access_rights_uid) if not ids and count: return 0 elif not ids: return ids pid = self.env.user.partner_id.id author_ids, partner_ids, channel_ids, allowed_ids = set([]), set( []), set([]), set([]) model_ids = {} # check read access rights before checking the actual rules on the given ids super(Message, self.sudo(access_rights_uid or self._uid)).check_access_rights('read') self._cr.execute( """ SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, COALESCE(partner_rel.res_partner_id, needaction_rel.res_partner_id), channel_partner.channel_id as channel_id FROM "%s" m LEFT JOIN "mail_message_res_partner_rel" partner_rel ON partner_rel.mail_message_id = m.id AND partner_rel.res_partner_id = %%(pid)s LEFT JOIN "mail_message_res_partner_needaction_rel" needaction_rel ON needaction_rel.mail_message_id = m.id AND needaction_rel.res_partner_id = %%(pid)s LEFT JOIN "mail_message_mail_channel_rel" channel_rel ON channel_rel.mail_message_id = m.id LEFT JOIN "mail_channel" channel ON channel.id = channel_rel.mail_channel_id LEFT JOIN "mail_channel_partner" channel_partner ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = %%(pid)s WHERE m.id = ANY (%%(ids)s)""" % self._table, dict(pid=pid, ids=ids)) for id, rmod, rid, author_id, partner_id, channel_id in self._cr.fetchall( ): if author_id == pid: author_ids.add(id) elif partner_id == pid: partner_ids.add(id) elif channel_id: channel_ids.add(id) elif rmod and rid: model_ids.setdefault(rmod, {}).setdefault(rid, set()).add(id) allowed_ids = self._find_allowed_doc_ids(model_ids) final_ids = author_ids | partner_ids | channel_ids | allowed_ids if count: return len(final_ids) else: # re-construct a list based on ids, because set did not keep the original order id_list = [id for id in ids if id in final_ids] return id_list @api.multi def check_access_rule(self, operation): """ Access rules of mail.message: - read: if - author_id == pid, uid is the author OR - uid is in the recipients (partner_ids) OR - uid has been notified (needaction) OR - uid is member of a listern channel (channel_ids.partner_ids) OR - uid have read access to the related document if model, res_id - otherwise: raise - create: if - no model, no res_id (private message) OR - pid in message_follower_ids if model, res_id OR - uid can read the parent OR - uid have write or create access on the related document if model, res_id, OR - otherwise: raise - write: if - author_id == pid, uid is the author, OR - uid is in the recipients (partner_ids) OR - uid has write or create access on the related document if model, res_id - otherwise: raise - unlink: if - uid has write or create access on the related document if model, res_id - otherwise: raise Specific case: non employee users see only messages with subtype (aka do not see internal logs). """ def _generate_model_record_ids(msg_val, msg_ids): """ :param model_record_ids: {'model': {'res_id': (msg_id, msg_id)}, ... } :param message_values: {'msg_id': {'model': .., 'res_id': .., 'author_id': ..}} """ model_record_ids = {} for id in msg_ids: vals = msg_val.get(id, {}) if vals.get('model') and vals.get('res_id'): model_record_ids.setdefault(vals['model'], set()).add(vals['res_id']) return model_record_ids if self._uid == SUPERUSER_ID: return # Non employees see only messages with a subtype (aka, not internal logs) if not self.env['res.users'].has_group('base.group_user'): self._cr.execute( '''SELECT DISTINCT message.id, message.subtype_id, subtype.internal FROM "%s" AS message LEFT JOIN "mail_message_subtype" as subtype ON message.subtype_id = subtype.id WHERE message.message_type = %%s AND (message.subtype_id IS NULL OR subtype.internal IS TRUE) AND message.id = ANY (%%s)''' % (self._table), ( 'comment', self.ids, )) if self._cr.fetchall(): raise AccessError( _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)' ) % (self._description, operation)) # Read mail_message.ids to have their values message_values = dict((res_id, {}) for res_id in self.ids) if operation in ['read', 'write']: self._cr.execute( """ SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, m.parent_id, COALESCE(partner_rel.res_partner_id, needaction_rel.res_partner_id), channel_partner.channel_id as channel_id FROM "%s" m LEFT JOIN "mail_message_res_partner_rel" partner_rel ON partner_rel.mail_message_id = m.id AND partner_rel.res_partner_id = %%(pid)s LEFT JOIN "mail_message_res_partner_needaction_rel" needaction_rel ON needaction_rel.mail_message_id = m.id AND needaction_rel.res_partner_id = %%(pid)s LEFT JOIN "mail_message_mail_channel_rel" channel_rel ON channel_rel.mail_message_id = m.id LEFT JOIN "mail_channel" channel ON channel.id = channel_rel.mail_channel_id LEFT JOIN "mail_channel_partner" channel_partner ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = %%(pid)s WHERE m.id = ANY (%%(ids)s)""" % self._table, dict(pid=self.env.user.partner_id.id, ids=self.ids)) for mid, rmod, rid, author_id, parent_id, partner_id, channel_id in self._cr.fetchall( ): message_values[mid] = { 'model': rmod, 'res_id': rid, 'author_id': author_id, 'parent_id': parent_id, 'notified': any((message_values[mid].get('notified'), partner_id, channel_id)) } else: self._cr.execute( """SELECT DISTINCT id, model, res_id, author_id, parent_id FROM "%s" WHERE id = ANY (%%s)""" % self._table, (self.ids, )) for mid, rmod, rid, author_id, parent_id in self._cr.fetchall(): message_values[mid] = { 'model': rmod, 'res_id': rid, 'author_id': author_id, 'parent_id': parent_id } # Author condition (READ, WRITE, CREATE (private)) author_ids = [] if operation == 'read' or operation == 'write': author_ids = [ mid for mid, message in message_values.items() if message.get('author_id') and message.get('author_id') == self.env.user.partner_id.id ] elif operation == 'create': author_ids = [ mid for mid, message in message_values.items() if not message.get('model') and not message.get('res_id') ] # Parent condition, for create (check for received notifications for the created message parent) notified_ids = [] if operation == 'create': # TDE: probably clean me parent_ids = [ message.get('parent_id') for message in message_values.values() if message.get('parent_id') ] self._cr.execute( """SELECT DISTINCT m.id, partner_rel.res_partner_id, channel_partner.partner_id FROM "%s" m LEFT JOIN "mail_message_res_partner_rel" partner_rel ON partner_rel.mail_message_id = m.id AND partner_rel.res_partner_id = (%%s) LEFT JOIN "mail_message_mail_channel_rel" channel_rel ON channel_rel.mail_message_id = m.id LEFT JOIN "mail_channel" channel ON channel.id = channel_rel.mail_channel_id LEFT JOIN "mail_channel_partner" channel_partner ON channel_partner.channel_id = channel.id AND channel_partner.partner_id = (%%s) WHERE m.id = ANY (%%s)""" % self._table, ( self.env.user.partner_id.id, self.env.user.partner_id.id, parent_ids, )) not_parent_ids = [ mid[0] for mid in self._cr.fetchall() if any([mid[1], mid[2]]) ] notified_ids += [ mid for mid, message in message_values.items() if message.get('parent_id') in not_parent_ids ] # Recipients condition, for read and write (partner_ids) and create (message_follower_ids) other_ids = set(self.ids).difference(set(author_ids), set(notified_ids)) model_record_ids = _generate_model_record_ids(message_values, other_ids) if operation in ['read', 'write']: notified_ids = [ mid for mid, message in message_values.items() if message.get('notified') ] elif operation == 'create': for doc_model, doc_ids in model_record_ids.items(): followers = self.env['mail.followers'].sudo().search([ ('res_model', '=', doc_model), ('res_id', 'in', list(doc_ids)), ('partner_id', '=', self.env.user.partner_id.id), ]) fol_mids = [follower.res_id for follower in followers] notified_ids += [ mid for mid, message in message_values.items() if message.get('model') == doc_model and message.get('res_id') in fol_mids ] # CRUD: Access rights related to the document other_ids = other_ids.difference(set(notified_ids)) model_record_ids = _generate_model_record_ids(message_values, other_ids) document_related_ids = [] for model, doc_ids in model_record_ids.items(): DocumentModel = self.env[model] mids = DocumentModel.browse(doc_ids).exists() if hasattr(DocumentModel, 'check_mail_message_access'): DocumentModel.check_mail_message_access(mids.ids, operation) # ?? mids ? else: self.env['mail.thread'].check_mail_message_access( mids.ids, operation, model_name=model) document_related_ids += [ mid for mid, message in message_values.items() if message.get('model') == model and message.get('res_id') in mids.ids ] # Calculate remaining ids: if not void, raise an error other_ids = other_ids.difference(set(document_related_ids)) if not (other_ids and self.browse(other_ids).exists()): return raise AccessError( _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)' ) % (self._description, operation)) @api.model def _get_record_name(self, values): """ Return the related document name, using name_get. It is done using SUPERUSER_ID, to be sure to have the record name correctly stored. """ model = values.get('model', self.env.context.get('default_model')) res_id = values.get('res_id', self.env.context.get('default_res_id')) if not model or not res_id or model not in self.env: return False return self.env[model].sudo().browse(res_id).name_get()[0][1] @api.model def _get_reply_to(self, values): """ Return a specific reply_to: alias of the document through message_get_reply_to or take the email_from """ model, res_id, email_from = values.get( 'model', self._context.get('default_model')), values.get( 'res_id', self._context.get('default_res_id')), values.get( 'email_from') # ctx values / defualt_get res ? if model and hasattr(self.env[model], 'message_get_reply_to'): # return self.env[model].browse(res_id).message_get_reply_to([res_id], default=email_from)[res_id] return self.env[model].message_get_reply_to( [res_id], default=email_from)[res_id] else: # return self.env['mail.thread'].message_get_reply_to(default=email_from)[None] return self.env['mail.thread'].message_get_reply_to( [None], default=email_from)[None] @api.model def _get_message_id(self, values): if values.get('no_auto_thread', False) is True: message_id = tools.generate_tracking_message_id('reply_to') elif values.get('res_id') and values.get('model'): message_id = tools.generate_tracking_message_id( '%(res_id)s-%(model)s' % values) else: message_id = tools.generate_tracking_message_id('private') return message_id @api.multi def _invalidate_documents(self): """ Invalidate the cache of the documents followed by ``self``. """ for record in self: if record.model and record.res_id: self.env[record.model].invalidate_cache(ids=[record.res_id]) @api.model def create(self, values): # coming from mail.js that does not have pid in its values if self.env.context.get('default_starred'): self = self.with_context({ 'default_starred_partner_ids': [(4, self.env.user.partner_id.id)] }) if 'email_from' not in values: # needed to compute reply_to values['email_from'] = self._get_default_from() if not values.get('message_id'): values['message_id'] = self._get_message_id(values) if 'reply_to' not in values: values['reply_to'] = self._get_reply_to(values) if 'record_name' not in values and 'default_record_name' not in self.env.context: values['record_name'] = self._get_record_name(values) if 'attachment_ids' not in values: values.setdefault('attachment_ids', []) # extract base64 images if 'body' in values: Attachments = self.env['ir.attachment'] data_to_url = {} def base64_to_boundary(match): key = match.group(2) if not data_to_url.get(key): name = 'image%s' % len(data_to_url) attachment = Attachments.create({ 'name': name, 'datas': match.group(2), 'datas_fname': name, 'res_model': 'mail.message', }) attachment.generate_access_token() values['attachment_ids'].append((4, attachment.id)) data_to_url[key] = [ '/web/image/%s?access_token=%s' % (attachment.id, attachment.access_token), name ] return '%s%s alt="%s"' % (data_to_url[key][0], match.group(3), data_to_url[key][1]) values['body'] = _image_dataurl.sub(base64_to_boundary, tools.ustr(values['body'])) # delegate creation of tracking after the create as sudo to avoid access rights issues tracking_values_cmd = values.pop('tracking_value_ids', False) message = super(Message, self).create(values) if tracking_values_cmd: message.sudo().write({'tracking_value_ids': tracking_values_cmd}) message._invalidate_documents() if not self.env.context.get('message_create_from_mail_mail'): message._notify(force_send=self.env.context.get( 'mail_notify_force_send', True), user_signature=self.env.context.get( 'mail_notify_user_signature', True)) return message @api.multi def read(self, fields=None, load='_classic_read'): """ Override to explicitely call check_access_rule, that is not called by the ORM. It instead directly fetches ir.rules and apply them. """ self.check_access_rule('read') return super(Message, self).read(fields=fields, load=load) @api.multi def write(self, vals): if 'model' in vals or 'res_id' in vals: self._invalidate_documents() res = super(Message, self).write(vals) self._invalidate_documents() return res @api.multi def unlink(self): # cascade-delete attachments that are directly attached to the message (should only happen # for mail.messages that act as parent for a standalone mail.mail record). self.check_access_rule('unlink') self.mapped('attachment_ids').filtered( lambda attach: attach.res_model == self._name and (attach.res_id in self.ids or attach.res_id == 0)).unlink() self._invalidate_documents() return super(Message, self).unlink() #------------------------------------------------------ # Messaging API #------------------------------------------------------ @api.multi def _notify(self, force_send=False, send_after_commit=True, user_signature=True): """ Compute recipients to notify based on specified recipients and document followers. Delegate notification to partners to send emails and bus notifications and to channels to broadcast messages on channels """ group_user = self.env.ref('base.group_user') # have a sudoed copy to manipulate partners (public can go here with website modules like forum / blog / ... ) self_sudo = self.sudo() self.ensure_one() partners_sudo = self_sudo.partner_ids channels_sudo = self_sudo.channel_ids # all followers of the mail.message document have to be added as partners and notified # and filter to employees only if the subtype is internal if self_sudo.subtype_id and self.model and self.res_id: followers = self_sudo.env['mail.followers'].search([ ('res_model', '=', self.model), ('res_id', '=', self.res_id), ('subtype_ids', 'in', self_sudo.subtype_id.id), ]) if self_sudo.subtype_id.internal: followers = followers.filtered(lambda fol: fol.channel_id or ( fol.partner_id.user_ids and group_user in fol.partner_id. user_ids[0].mapped('groups_id'))) channels_sudo |= followers.mapped('channel_id') partners_sudo |= followers.mapped('partner_id') # remove author from notified partners if not self._context.get('mail_notify_author', False) and self_sudo.author_id: partners_sudo = partners_sudo - self_sudo.author_id # update message, with maybe custom values message_values = {} if channels_sudo: message_values['channel_ids'] = [(6, 0, channels_sudo.ids)] if partners_sudo: message_values['needaction_partner_ids'] = [(6, 0, partners_sudo.ids)] if self.model and self.res_id and hasattr( self.env[self.model], 'message_get_message_notify_values'): message_values.update(self.env[self.model].browse( self.res_id).message_get_message_notify_values( self, message_values)) if message_values: self.write(message_values) # notify partners and channels # those methods are called as SUPERUSER because portal users posting messages # have no access to partner model. Maybe propagating a real uid could be necessary. email_channels = channels_sudo.filtered( lambda channel: channel.email_send) notif_partners = partners_sudo.filtered( lambda partner: 'inbox' in partner.mapped( 'user_ids.notification_type')) if email_channels or partners_sudo - notif_partners: partners_sudo.search([ '|', ('id', 'in', (partners_sudo - notif_partners).ids), ('channel_ids', 'in', email_channels.ids), ('email', '!=', self_sudo.author_id.email or self_sudo.email_from), ])._notify(self, force_send=force_send, send_after_commit=send_after_commit, user_signature=user_signature) notif_partners._notify_by_chat(self) channels_sudo._notify(self) # Discard cache, because child / parent allow reading and therefore # change access rights. if self.parent_id: self.parent_id.invalidate_cache() return True
class ResPartner(models.Model): _name = 'res.partner' _inherit = 'res.partner' @api.multi def _credit_debit_get(self): tables, where_clause, where_params = self.env['account.move.line']._query_get() where_params = [tuple(self.ids)] + where_params if where_clause: where_clause = 'AND ' + where_clause self._cr.execute("""SELECT account_move_line.partner_id, act.type, SUM(account_move_line.amount_residual) FROM account_move_line LEFT JOIN account_account a ON (account_move_line.account_id=a.id) LEFT JOIN account_account_type act ON (a.user_type_id=act.id) WHERE act.type IN ('receivable','payable') AND account_move_line.partner_id IN %s AND account_move_line.reconciled IS FALSE """ + where_clause + """ GROUP BY account_move_line.partner_id, act.type """, where_params) for pid, type, val in self._cr.fetchall(): partner = self.browse(pid) if type == 'receivable': partner.credit = val elif type == 'payable': partner.debit = -val @api.multi def _asset_difference_search(self, account_type, operator, operand): if operator not in ('<', '=', '>', '>=', '<='): return [] if type(operand) not in (float, int): return [] sign = 1 if account_type == 'payable': sign = -1 res = self._cr.execute(''' SELECT partner.id FROM res_partner partner LEFT JOIN account_move_line aml ON aml.partner_id = partner.id RIGHT JOIN account_account acc ON aml.account_id = acc.id WHERE acc.internal_type = %s AND NOT acc.deprecated GROUP BY partner.id HAVING %s * COALESCE(SUM(aml.amount_residual), 0) ''' + operator + ''' %s''', (account_type, sign, operand)) res = self._cr.fetchall() if not res: return [('id', '=', '0')] return [('id', 'in', [r[0] for r in res])] @api.model def _credit_search(self, operator, operand): return self._asset_difference_search('receivable', operator, operand) @api.model def _debit_search(self, operator, operand): return self._asset_difference_search('payable', operator, operand) @api.multi def _invoice_total(self): account_invoice_report = self.env['account.invoice.report'] if not self.ids: self.total_invoiced = 0.0 return True user_currency_id = self.env.user.company_id.currency_id.id all_partners_and_children = {} all_partner_ids = [] for partner in self: # price_total is in the company currency all_partners_and_children[partner] = self.with_context(active_test=False).search([('id', 'child_of', partner.id)]).ids all_partner_ids += all_partners_and_children[partner] # searching account.invoice.report via the orm is comparatively expensive # (generates queries "id in []" forcing to build the full table). # In simple cases where all invoices are in the same currency than the user's company # access directly these elements # generate where clause to include multicompany rules where_query = account_invoice_report._where_calc([ ('partner_id', 'in', all_partner_ids), ('state', 'not in', ['draft', 'cancel']), ('type', 'in', ('out_invoice', 'out_refund')) ]) account_invoice_report._apply_ir_rules(where_query, 'read') from_clause, where_clause, where_clause_params = where_query.get_sql() # price_total is in the company currency query = """ SELECT SUM(price_total) as total, partner_id FROM account_invoice_report account_invoice_report WHERE %s GROUP BY partner_id """ % where_clause self.env.cr.execute(query, where_clause_params) price_totals = self.env.cr.dictfetchall() for partner, child_ids in all_partners_and_children.items(): partner.total_invoiced = sum(price['total'] for price in price_totals if price['partner_id'] in child_ids) @api.multi def _compute_journal_item_count(self): AccountMoveLine = self.env['account.move.line'] for partner in self: partner.journal_item_count = AccountMoveLine.search_count([('partner_id', '=', partner.id)]) @api.multi def _compute_contracts_count(self): AccountAnalyticAccount = self.env['account.analytic.account'] for partner in self: partner.contracts_count = AccountAnalyticAccount.search_count([('partner_id', '=', partner.id)]) def get_followup_lines_domain(self, date, overdue_only=False, only_unblocked=False): domain = [('reconciled', '=', False), ('account_id.deprecated', '=', False), ('account_id.internal_type', '=', 'receivable'), '|', ('debit', '!=', 0), ('credit', '!=', 0), ('company_id', '=', self.env.user.company_id.id)] if only_unblocked: domain += [('blocked', '=', False)] if self.ids: if 'exclude_given_ids' in self._context: domain += [('partner_id', 'not in', self.ids)] else: domain += [('partner_id', 'in', self.ids)] #adding the overdue lines overdue_domain = ['|', '&', ('date_maturity', '!=', False), ('date_maturity', '<', date), '&', ('date_maturity', '=', False), ('date', '<', date)] if overdue_only: domain += overdue_domain return domain @api.one def _compute_has_unreconciled_entries(self): # Avoid useless work if has_unreconciled_entries is not relevant for this partner if not self.active or not self.is_company and self.parent_id: return self.env.cr.execute( """ SELECT 1 FROM( SELECT p.last_time_entries_checked AS last_time_entries_checked, MAX(l.write_date) AS max_date FROM account_move_line l RIGHT JOIN account_account a ON (a.id = l.account_id) RIGHT JOIN res_partner p ON (l.partner_id = p.id) WHERE p.id = %s AND EXISTS ( SELECT 1 FROM account_move_line l WHERE l.account_id = a.id AND l.partner_id = p.id AND l.amount_residual > 0 ) AND EXISTS ( SELECT 1 FROM account_move_line l WHERE l.account_id = a.id AND l.partner_id = p.id AND l.amount_residual < 0 ) GROUP BY p.last_time_entries_checked ) as s WHERE (last_time_entries_checked IS NULL OR max_date > last_time_entries_checked) """, (self.id,)) self.has_unreconciled_entries = self.env.cr.rowcount == 1 @api.multi def mark_as_reconciled(self): self.env['account.partial.reconcile'].check_access_rights('write') return self.sudo().with_context(company_id=self.env.user.company_id.id).write({'last_time_entries_checked': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}) @api.one def _get_company_currency(self): if self.company_id: self.currency_id = self.sudo().company_id.currency_id else: self.currency_id = self.env.user.company_id.currency_id credit = fields.Monetary(compute='_credit_debit_get', search=_credit_search, string='Total Receivable', help="Total amount this customer owes you.") debit = fields.Monetary(compute='_credit_debit_get', search=_debit_search, string='Total Payable', help="Total amount you have to pay to this vendor.") debit_limit = fields.Monetary('Payable Limit') total_invoiced = fields.Monetary(compute='_invoice_total', string="Total Invoiced", groups='account.group_account_invoice') currency_id = fields.Many2one('res.currency', compute='_get_company_currency', readonly=True, string="Currency", help='Utility field to express amount currency') contracts_count = fields.Integer(compute='_compute_contracts_count', string="Contracts", type='integer') journal_item_count = fields.Integer(compute='_compute_journal_item_count', string="Journal Items", type="integer") property_account_payable_id = fields.Many2one('account.account', company_dependent=True, string="Account Payable", oldname="property_account_payable", domain="[('internal_type', '=', 'payable'), ('deprecated', '=', False)]", help="This account will be used instead of the default one as the payable account for the current partner", required=True) property_account_receivable_id = fields.Many2one('account.account', company_dependent=True, string="Account Receivable", oldname="property_account_receivable", domain="[('internal_type', '=', 'receivable'), ('deprecated', '=', False)]", help="This account will be used instead of the default one as the receivable account for the current partner", required=True) property_account_position_id = fields.Many2one('account.fiscal.position', company_dependent=True, string="Fiscal Position", help="The fiscal position will determine taxes and accounts used for the partner.", oldname="property_account_position") property_payment_term_id = fields.Many2one('account.payment.term', company_dependent=True, string='Customer Payment Terms', help="This payment term will be used instead of the default one for sales orders and customer invoices", oldname="property_payment_term") property_supplier_payment_term_id = fields.Many2one('account.payment.term', company_dependent=True, string='Vendor Payment Terms', help="This payment term will be used instead of the default one for purchase orders and vendor bills", oldname="property_supplier_payment_term") ref_company_ids = fields.One2many('res.company', 'partner_id', string='Companies that refers to partner', oldname="ref_companies") has_unreconciled_entries = fields.Boolean(compute='_compute_has_unreconciled_entries', help="The partner has at least one unreconciled debit and credit since last time the invoices & payments matching was performed.") last_time_entries_checked = fields.Datetime(oldname='last_reconciliation_date', string='Latest Invoices & Payments Matching Date', readonly=True, copy=False, help='Last time the invoices & payments matching was performed for this partner. ' 'It is set either if there\'s not at least an unreconciled debit and an unreconciled credit ' 'or if you click the "Done" button.') invoice_ids = fields.One2many('account.invoice', 'partner_id', string='Invoices', readonly=True, copy=False) contract_ids = fields.One2many('account.analytic.account', 'partner_id', string='Contracts', readonly=True) bank_account_count = fields.Integer(compute='_compute_bank_count', string="Bank") trust = fields.Selection([('good', 'Good Debtor'), ('normal', 'Normal Debtor'), ('bad', 'Bad Debtor')], string='Degree of trust you have in this debtor', default='normal', company_dependent=True) invoice_warn = fields.Selection(WARNING_MESSAGE, 'Invoice', help=WARNING_HELP, required=True, default="no-message") invoice_warn_msg = fields.Text('Message for Invoice') @api.multi def _compute_bank_count(self): bank_data = self.env['res.partner.bank'].read_group([('partner_id', 'in', self.ids)], ['partner_id'], ['partner_id']) mapped_data = dict([(bank['partner_id'][0], bank['partner_id_count']) for bank in bank_data]) for partner in self: partner.bank_account_count = mapped_data.get(partner.id, 0) def _find_accounting_partner(self, partner): ''' Find the partner for which the accounting entries will be created ''' return partner.commercial_partner_id @api.model def _commercial_fields(self): return super(ResPartner, self)._commercial_fields() + \ ['debit_limit', 'property_account_payable_id', 'property_account_receivable_id', 'property_account_position_id', 'property_payment_term_id', 'property_supplier_payment_term_id', 'last_time_entries_checked'] @api.multi def action_view_partner_invoices(self): self.ensure_one() action = self.env.ref('account.action_invoice_refund_out_tree').read()[0] action['domain'] = literal_eval(action['domain']) action['domain'].append(('partner_id', 'child_of', self.id)) return action @api.onchange('company_id') def _onchange_company_id(self): company = self.env['res.company'] if self.company_id: company = self.company_id else: company = self.env.user.company_id return {'domain': {'property_account_position_id': [('company_id', 'in', [company.id, False])]}}