class User(models.Model): _inherit = ['res.users'] hours_last_month = fields.Float(related='employee_id.hours_last_month') hours_last_month_display = fields.Char( related='employee_id.hours_last_month_display') attendance_state = fields.Selection(related='employee_id.attendance_state') last_check_in = fields.Datetime( related='employee_id.last_attendance_id.check_in') last_check_out = fields.Datetime( related='employee_id.last_attendance_id.check_out') def __init__(self, pool, cr): """ Override of __init__ to add access rights. Access rights are disabled by default, but allowed on some specific fields defined in self.SELF_{READ/WRITE}ABLE_FIELDS. """ attendance_readable_fields = [ 'hours_last_month', 'hours_last_month_display', 'attendance_state', 'last_check_in', 'last_check_out' ] super(User, self).__init__(pool, cr) # duplicate list to avoid modifying the original reference type(self).SELF_READABLE_FIELDS = type( self).SELF_READABLE_FIELDS + attendance_readable_fields
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 self.create(values) else: # update the last_presence if necessary, and write values if presence.last_presence < last_presence: values['last_presence'] = last_presence # Hide transaction serialization errors, which can be ignored, the presence update is not essential with tools.mute_logger('coffice.sql_db'): presence.write(values) # avoid TransactionRollbackError self.env.cr.commit() # TODO : check if still necessary
class Attendee(models.Model): _inherit = 'calendar.attendee' google_internal_event_id = fields.Char('Google Calendar Event Id') oe_synchro_date = fields.Datetime('Coffice Synchro Date') _sql_constraints = [ ('google_id_uniq', 'unique(google_internal_event_id,partner_id,event_id)', 'Google ID should be unique!') ] 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 ConverterTest(models.Model): _name = 'web_editor.converter.test' _description = '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(attachment=False) date = fields.Date() datetime = fields.Datetime() 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 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 Coffice Google Internal ID' )
class StockQuantityHistory(models.TransientModel): _name = 'stock.quantity.history' _description = 'Stock Quantity History' inventory_datetime = fields.Datetime('Inventory at Date', help="Choose a date to get the inventory at that date", default=fields.Datetime.now) def open_at_date(self): 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 domain = [('type', '=', 'product')] product_id = self.env.context.get('product_id', False) product_tmpl_id = self.env.context.get('product_tmpl_id', False) if product_id: domain = expression.AND([domain, [('id', '=', product_id)]]) elif product_tmpl_id: domain = expression.AND([domain, [('product_tmpl_id', '=', product_tmpl_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', 'domain': domain, 'context': dict(self.env.context, to_date=self.inventory_datetime), } return action
class Partner(models.Model): _inherit = 'res.partner' calendar_last_notif_ack = fields.Datetime( 'Last notification marked as read from base Calendar', default=fields.Datetime.now) 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.context.get('uid', self.env.uid)).partner_id partner.write({'calendar_last_notif_ack': datetime.now()}) return
class test_model(models.Model): _name = 'test_converter.test_model' _description = 'Test Converter Model' char = fields.Char() integer = fields.Integer() float = fields.Float() numeric = fields.Float(digits=(16, 2)) many2one = fields.Many2one('test_converter.test_model.sub', group_expand='_gbf_m2o') binary = fields.Binary(attachment=False) date = fields.Date() datetime = fields.Datetime() selection_str = fields.Selection( [ ('A', u"Qu'il n'est pas arrivé à Toronto"), ('B', u"Qu'il était supposé arriver à Toronto"), ('C', u"Qu'est-ce qu'il fout ce maudit pancake, tabernacle ?"), ('D', u"La réponse D"), ], string=u"Lorsqu'un pancake prend l'avion à destination de Toronto et " u"qu'il fait une escale technique à St Claude, on dit:") html = fields.Html() text = fields.Text() # `base` module does not contains any model that implement the functionality # `group_expand`; test this feature here... @api.model def _gbf_m2o(self, subs, domain, order): sub_ids = subs._search([], order=order, access_rights_uid=SUPERUSER_ID) return subs.browse(sub_ids)
class FillTemporal(models.Model): _name = 'test_read_group.fill_temporal' _description = 'Group Test Fill Temporal' date = fields.Date() datetime = fields.Datetime() value = fields.Integer()
class CompanyDependent(models.Model): _name = 'test_new_api.company' _description = 'Test New API Company' foo = fields.Char(company_dependent=True) date = fields.Date(company_dependent=True) moment = fields.Datetime(company_dependent=True) tag_id = fields.Many2one('test_new_api.multi.tag', company_dependent=True)
class SaleCouponRule(models.Model): _name = 'sale.coupon.rule' _description = "Sales Coupon Rule" rule_date_from = fields.Datetime(string="Start Date", help="Coupon program start date") rule_date_to = fields.Datetime(string="End Date", help="Coupon program end date") rule_partners_domain = fields.Char( string="Based on Customers", help="Coupon program will work for selected customers only") rule_products_domain = fields.Char( string="Based on Products", default=[['sale_ok', '=', True]], help="On Purchase of selected product, reward will be given") rule_min_quantity = fields.Integer( string="Minimum Quantity", default=1, help="Minimum required product quantity to get the reward") rule_minimum_amount = fields.Float( default=0.0, help="Minimum required amount to get the reward") rule_minimum_amount_tax_inclusion = fields.Selection( [('tax_included', 'Tax Included'), ('tax_excluded', 'Tax Excluded')], default="tax_excluded") @api.constrains('rule_date_to', 'rule_date_from') def _check_rule_date_from(self): if any(applicability for applicability in self if applicability.rule_date_to and applicability.rule_date_from and applicability.rule_date_to < applicability.rule_date_from): raise ValidationError( _('The start date must be before the end date')) @api.constrains('rule_minimum_amount') def _check_rule_minimum_amount(self): if self.filtered( lambda applicability: applicability.rule_minimum_amount < 0): raise ValidationError( _('Minimum purchased amount should be greater than 0')) @api.constrains('rule_min_quantity') def _check_rule_min_quantity(self): if not self.rule_min_quantity > 0: raise ValidationError( _('Minimum quantity should be greater than 0'))
class ComplexModel(models.Model): _name = model('complex') _description = 'Tests: Base Import Model Complex' f = fields.Float() m = fields.Monetary() c = fields.Char() currency_id = fields.Many2one('res.currency') d = fields.Date() dt = fields.Datetime()
class StockQuant(models.Model): _inherit = 'stock.quant' removal_date = fields.Datetime(related='lot_id.removal_date', store=True, readonly=False) @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 MailingTraceReport(models.Model): _name = 'mailing.trace.report' _auto = False _description = 'Mass Mailing Statistics' # mailing name = fields.Char(string='Mass Mail', readonly=True) mailing_type = fields.Selection([('mail', 'Mail')], string='Type', default='mail', required=True) campaign = fields.Char(string='Mass Mail Campaign', readonly=True) scheduled_date = fields.Datetime(string='Scheduled Date', readonly=True) state = fields.Selection([('draft', 'Draft'), ('test', 'Tested'), ('done', 'Sent')], string='Status', readonly=True) email_from = fields.Char('From', readonly=True) # traces sent = fields.Integer(readonly=True) delivered = fields.Integer(readonly=True) opened = fields.Integer(readonly=True) replied = fields.Integer(readonly=True) clicked = fields.Integer(readonly=True) bounced = fields.Integer(readonly=True) def init(self): """Mass Mail Statistical Report: based on mailing.trace that models the various statistics collected for each mailing, and mailing.mailing model that models the various mailing performed. """ tools.drop_view_if_exists(self.env.cr, 'mailing_trace_report') self.env.cr.execute(""" CREATE OR REPLACE VIEW mailing_trace_report AS ( SELECT min(trace.id) as id, utm_source.name as name, mailing.mailing_type, utm_campaign.name as campaign, trace.scheduled as scheduled_date, mailing.state, mailing.email_from, count(trace.sent) as sent, (count(trace.sent) - count(trace.bounced)) as delivered, count(trace.opened) as opened, count(trace.replied) as replied, count(trace.clicked) as clicked, count(trace.bounced) as bounced FROM mailing_trace as trace left join mailing_mailing as mailing ON (trace.mass_mailing_id=mailing.id) left join utm_campaign as utm_campaign ON (mailing.campaign_id = utm_campaign.id) left join utm_source as utm_source ON (mailing.source_id = utm_source.id) GROUP BY trace.scheduled, utm_source.name, utm_campaign.name, mailing.mailing_type, mailing.state, mailing.email_from )""")
class ResourceCalendarLeaves(models.Model): _name = "resource.calendar.leaves" _description = "Resource Time Off Detail" _order = "date_from" 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) resource_id = fields.Many2one( "resource.resource", 'Resource', help= "If empty, this is a generic time off for the company. If a resource is set, the time off is only for this resource" ) time_type = fields.Selection( [('leave', 'Time Off'), ('other', 'Other')], default='leave', help= "Whether this should be computed as a time off or as work time (eg: formation)" ) @api.constrains('date_from', 'date_to') def check_dates(self): if self.filtered(lambda leave: leave.date_from > leave.date_to): raise ValidationError( _('The start date of the time off must be earlier end date.')) @api.onchange('resource_id') def onchange_resource(self): if self.resource_id: self.calendar_id = self.resource_id.calendar_id
class MixedModel(models.Model): _name = 'test_new_api.mixed' _description = 'Test New API Mixed' number = fields.Float(digits=(10, 2), default=3.14) number2 = fields.Float(digits='New API Precision') date = fields.Date() moment = fields.Datetime() now = fields.Datetime(compute='_compute_now') lang = fields.Selection(string='Language', selection='_get_lang') reference = fields.Reference(string='Related Document', selection='_reference_models') comment1 = fields.Html(sanitize=False) comment2 = fields.Html(sanitize_attributes=True, strip_classes=False) comment3 = fields.Html(sanitize_attributes=True, strip_classes=True) comment4 = fields.Html(sanitize_attributes=True, strip_style=True) currency_id = fields.Many2one( 'res.currency', default=lambda self: self.env.ref('base.EUR')) amount = fields.Monetary() def _compute_now(self): # this is a non-stored computed field without dependencies for message in self: message.now = fields.Datetime.now() @api.model def _get_lang(self): return self.env['res.lang'].get_installed() @api.model def _reference_models(self): models = self.env['ir.model'].sudo().search([('state', '!=', 'manual') ]) return [(model.model, model.name) for model in models if not model.model.startswith('ir.')]
class MassMailingContactListRel(models.Model): """ Intermediate model between mass mailing list and mass mailing contact Indicates if a contact is opted out for a particular list """ _name = 'mailing.contact.subscription' _description = 'Mass Mailing Subscription Information' _table = 'mailing_contact_list_rel' _rec_name = 'contact_id' contact_id = fields.Many2one('mailing.contact', string='Contact', ondelete='cascade', required=True) list_id = fields.Many2one('mailing.list', string='Mailing List', ondelete='cascade', required=True) opt_out = fields.Boolean( string='Opt Out', help= 'The contact has chosen not to receive mails anymore from this list', default=False) unsubscription_date = fields.Datetime(string='Unsubscription Date') message_bounce = fields.Integer(related='contact_id.message_bounce', store=False, readonly=False) is_blacklisted = fields.Boolean(related='contact_id.is_blacklisted', store=False, readonly=False) _sql_constraints = [ ('unique_contact_list', 'unique (contact_id, list_id)', 'A contact cannot be subscribed multiple times to the same list!') ] @api.model def create(self, vals): if 'opt_out' in vals: vals['unsubscription_date'] = vals[ 'opt_out'] and fields.Datetime.now() return super(MassMailingContactListRel, self).create(vals) def write(self, vals): if 'opt_out' in vals: vals['unsubscription_date'] = vals[ 'opt_out'] and fields.Datetime.now() return super(MassMailingContactListRel, self).write(vals)
class MailTestFull(models.Model): """ This model can be used in tests when complex chatter features are required like modeling tasks or tickets. """ _description = 'Full Chatter Model' _name = 'mail.test.full' _inherit = ['mail.thread'] name = fields.Char() email_from = fields.Char(tracking=True) count = fields.Integer(default=1) datetime = fields.Datetime(default=fields.Datetime.now) mail_template = fields.Many2one('mail.template', 'Template') customer_id = fields.Many2one('res.partner', 'Customer', tracking=2) user_id = fields.Many2one('res.users', 'Responsible', tracking=1) umbrella_id = fields.Many2one('mail.test', tracking=True) def _track_template(self, changes): res = super(MailTestFull, self)._track_template(changes) record = self[0] if 'customer_id' in changes and record.mail_template: res['customer_id'] = (record.mail_template, { 'composition_mode': 'mass_mail' }) elif 'datetime' in changes: res['datetime'] = ('test_mail.mail_test_full_tracking_view', { 'composition_mode': 'mass_mail' }) return res def _creation_subtype(self): if self.umbrella_id: return self.env.ref('test_mail.st_mail_test_full_umbrella_upd') return super(MailTestFull, self)._creation_subtype() def _track_subtype(self, init_values): self.ensure_one() if 'umbrella_id' in init_values and self.umbrella_id: return self.env.ref('test_mail.st_mail_test_full_umbrella_upd') return super(MailTestFull, self)._track_subtype(init_values)
class Meeting(models.Model): _inherit = "calendar.event" oe_update_date = fields.Datetime('Coffice 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' ] 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: if 'oe_update_date' in self._context: values['oe_update_date'] = self._context.get('oe_update_date') else: values['oe_update_date'] = fields.Datetime.now() return super(Meeting, self).write(values) @api.returns('self', lambda value: value.id) 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) def unlink(self, can_be_deleted=False): return super(Meeting, self).unlink(can_be_deleted=can_be_deleted)
class MailingMailingScheduleDate(models.TransientModel): _name = 'mailing.mailing.schedule.date' _description = 'Mass Mailing Scheduling' schedule_date = fields.Datetime(string='Scheduled for') mass_mailing_id = fields.Many2one('mailing.mailing', required=True, ondelete='cascade') @api.constrains('schedule_date') def _check_schedule_date(self): for scheduler in self: if scheduler.schedule_date < fields.Datetime.now(): raise ValidationError( _('Please select a date equal/or greater than the current date.' )) def set_schedule_date(self): self.mass_mailing_id.write({ 'schedule_date': self.schedule_date, 'state': 'in_queue' })
class EventEvent(models.Model): """Event""" _name = 'event.event' _description = 'Event' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'date_begin' name = fields.Char( string='Event', 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, tracking=True, readonly=False, states={'done': [('readonly', True)]}) company_id = fields.Many2one( 'res.company', string='Company', change_default=True, default=lambda self: self.env.company, required=False, readonly=False, states={'done': [('readonly', True)]}) organizer_id = fields.Many2one( 'res.partner', string='Organizer', tracking=True, default=lambda self: self.env.company.partner_id, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") event_type_id = fields.Many2one( 'event.type', string='Category', readonly=False, states={'done': [('readonly', True)]}) 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', 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', 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( string='Reserved Seats', store=True, readonly=True, compute='_compute_seats') seats_available = fields.Integer( string='Available Seats', store=True, readonly=True, compute='_compute_seats') seats_unconfirmed = fields.Integer( string='Unconfirmed Seat Reservations', store=True, readonly=True, compute='_compute_seats') seats_used = fields.Integer( string='Number of Participants', store=True, readonly=True, compute='_compute_seats') seats_expected = fields.Integer( string='Number of Expected Attendees', compute_sudo=True, 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, tracking=True, states={'done': [('readonly', True)]}) date_end = fields.Datetime( string='End Date', required=True, tracking=True, 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') is_one_day = fields.Boolean(compute='_compute_field_is_one_day') 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.company.partner_id, readonly=False, states={'done': [('readonly', True)]}, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", tracking=True) country_id = fields.Many2one('res.country', 'Country', related='address_id.country_id', store=True, readonly=False) twitter_hashtag = fields.Char('Twitter Hashtag') description = fields.Html( string='Description', 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.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.env['event.registration'].flush(['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.depends('date_tz', 'date_begin') def _compute_date_begin_tz(self): for event in self: if event.date_begin: event.date_begin_located = format_datetime( self.env, event.date_begin, tz=event.date_tz, dt_format='medium') else: event.date_begin_located = False @api.depends('date_tz', 'date_end') def _compute_date_end_tz(self): for event in self: if event.date_end: event.date_end_located = format_datetime( self.env, event.date_end, tz=event.date_tz, dt_format='medium') else: event.date_end_located = False @api.depends('date_begin', 'date_end', 'date_tz') def _compute_field_is_one_day(self): for event in self: # Need to localize because it could begin late and finish early in # another timezone event = event.with_context(tz=event.date_tz) begin_tz = fields.Datetime.context_timestamp(event, event.date_begin) end_tz = fields.Datetime.context_timestamp(event, event.date_end) event.is_one_day = (begin_tz.date() == end_tz.date()) @api.onchange('is_online') def _onchange_is_online(self): if self.is_online: self.address_id = 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)] + [ (0, 0, { attribute_name: line[attribute_name] for attribute_name in self.env['event.type.mail']._get_event_mail_fields_whitelist() }) 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.constrains('date_begin', 'date_end') def _check_closing_date(self): for event in self: if event.date_end < event.date_begin: raise ValidationError(_('The closing date cannot be earlier than the beginning date.')) @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 def write(self, vals): res = super(EventEvent, self).write(vals) if vals.get('organizer_id'): self.message_subscribe([vals['organizer_id']]) return res @api.returns('self', lambda value: value.id) def copy(self, default=None): self.ensure_one() default = dict(default or {}, name=_("%s (copy)") % (self.name)) return super(EventEvent, self).copy(default) def button_draft(self): self.write({'state': 'draft'}) 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' def button_done(self): self.write({'state': 'done'}) def button_confirm(self): self.write({'state': 'confirm'}) def mail_attendees(self, template_id, force_send=False, filter_func=lambda self: self.state != 'cancel'): for event in self: for attendee in event.registration_ids.filtered(filter_func): self.env['mail.template'].browse(template_id).send_mail(attendee.id, force_send=force_send) def _is_event_registrable(self): return self.date_end > fields.Datetime.now() def _get_ics_file(self): """ Returns iCalendar file for the event invitation. :returns a dict of .ics file content for each event """ result = {} if not vobject: return result for event in self: cal = vobject.iCalendar() cal_event = cal.add('vevent') cal_event.add('created').value = fields.Datetime.now().replace(tzinfo=pytz.timezone('UTC')) cal_event.add('dtstart').value = fields.Datetime.from_string(event.date_begin).replace(tzinfo=pytz.timezone('UTC')) cal_event.add('dtend').value = fields.Datetime.from_string(event.date_end).replace(tzinfo=pytz.timezone('UTC')) cal_event.add('summary').value = event.name if event.address_id: cal_event.add('location').value = event.sudo().address_id.contact_address result[event.id] = cal.serialize().encode('utf-8') 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) 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 = attendance.check_out - attendance.check_in attendance.worked_hours = delta.total_seconds() / 3600.0 else: attendance.worked_hours = False @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.returns('self', lambda value: value.id) def copy(self): raise exceptions.UserError(_('You cannot duplicate an attendance.'))
class EventRegistration(models.Model): _name = 'event.registration' _description = 'Event Registration' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'name, create_date desc' # event 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)]}) # attendee partner_id = fields.Many2one( 'res.partner', string='Contact', states={'done': [('readonly', True)]}) name = fields.Char(string='Attendee Name', index=True) email = fields.Char(string='Email') phone = fields.Char(string='Phone') mobile = fields.Char(string='Mobile') # organization 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, tracking=True) @api.constrains('event_id', 'state') def _check_seats_limit(self): for registration in self: if registration.event_id.seats_availability == 'limited' and registration.event_id.seats_max and registration.event_id.seats_available < (1 if registration.state == 'draft' else 0): raise ValidationError(_('No more seats available for this event.')) 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), 'mobile': registration.get('mobile', partner_id.mobile), '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 def do_draft(self): self.write({'state': 'draft'}) def confirm_registration(self): self.write({'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() def button_reg_close(self): """ Close Registration """ for registration in self: today = fields.Datetime.now() if registration.event_id.date_begin <= today and registration.event_id.state == 'confirm': registration.write({'state': 'done', 'date_closed': today}) elif registration.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.")) def button_reg_cancel(self): self.write({'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 self.mobile = contact.mobile or self.mobile 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_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} def _message_post_after_hook(self, message, msg_vals): 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, msg_vals) 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', custom_layout="mail.mail_notification_light", ) return { 'name': _('Compose Email'), 'type': 'ir.actions.act_window', 'view_mode': 'form', 'res_model': 'mail.compose.message', 'views': [(compose_form.id, 'form')], 'view_id': compose_form.id, 'target': 'new', 'context': ctx, } def get_date_range_str(self): self.ensure_one() today = fields.Datetime.now() event_date = 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_datetime(self.env, self.event_begin_date, tz=self.event_id.date_tz, dt_format='medium') def summary(self): self.ensure_one() return {'information': []}
class MrpWorkcenterProductivity(models.Model): _name = "mrp.workcenter.productivity" _description = "Workcenter Productivity Log" _order = "id desc" _rec_name = "loss_id" _check_company_auto = True def _get_default_company_id(self): company_id = False if self.env.context.get('default_company_id'): company_id = self.env.context['default_company_id'] if not company_id and self.env.context.get('default_workorder_id'): workorder = self.env['mrp.workorder'].browse( self.env.context['default_workorder_id']) company_id = workorder.company_id if not company_id and self.env.context.get('default_workcenter_id'): workcenter = self.env['mrp.workcenter'].browse( self.env.context['default_workcenter_id']) company_id = workcenter.company_id if not company_id: company_id = self.env.company return company_id production_id = fields.Many2one('mrp.production', string='Manufacturing Order', related='workorder_id.production_id', readonly='True') workcenter_id = fields.Many2one('mrp.workcenter', "Work Center", required=True, check_company=True) company_id = fields.Many2one( 'res.company', required=True, index=True, default=lambda self: self._get_default_company_id()) workorder_id = fields.Many2one('mrp.workorder', 'Work Order', check_company=True) 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, readonly=False) 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._get_work_days_data( d1, d2)['hours'] blocktime.duration = round(r * 60, 2) else: blocktime.duration = round(diff.total_seconds() / 60.0, 2) else: blocktime.duration = 0.0 def button_block(self): self.ensure_one() self.workcenter_id.order_ids.end_all()
class Applicant(models.Model): _name = "hr.applicant" _description = "Applicant" _order = "priority desc, id desc" _inherit = ['mail.thread.cc', 'mail.activity.mixin', 'utm.mixin'] def _default_stage_id(self): if self._context.get('default_job_id'): return self.env['hr.recruitment.stage'].search( [ '|', ('job_ids', '=', False), ('job_ids', '=', self._context['default_job_id']), ('fold', '=', False) ], order='sequence asc', limit=1).id return False def _default_company_id(self): company_id = False if self._context.get('default_department_id'): department = self.env['hr.department'].browse( self._context['default_department_id']) company_id = department.company_id.id if not company_id and self.job_id: company_id = self.env['hr.job'].browse( self._context['default_job_id']).company_id.ids if not company_id: company_id = self.env.company return company_id name = fields.Char("Subject / Application Name", required=True) active = fields.Boolean( "Active", default=True, help= "If the active field is set to false, it will allow you to hide the case without removing it." ) description = fields.Text("Description") email_from = fields.Char("Email", size=128, help="Applicant email") probability = fields.Float("Probability") partner_id = fields.Many2one('res.partner', "Contact", copy=False) create_date = fields.Datetime("Creation Date", readonly=True, index=True) stage_id = fields.Many2one( 'hr.recruitment.stage', 'Stage', ondelete='restrict', tracking=True, domain="['|', ('job_ids', '=', False), ('job_ids', '=', job_id)]", copy=False, index=True, group_expand='_read_group_stage_ids', default=_default_stage_id) last_stage_id = fields.Many2one( 'hr.recruitment.stage', "Last Stage", help= "Stage of the applicant before being in the current stage. Used for lost cases analysis." ) categ_ids = fields.Many2many('hr.applicant.category', string="Tags") company_id = fields.Many2one('res.company', "Company", default=_default_company_id) user_id = fields.Many2one('res.users', "Responsible", tracking=True, default=lambda self: self.env.uid) date_closed = fields.Datetime("Closed", readonly=True, index=True) date_open = fields.Datetime("Assigned", readonly=True, index=True) date_last_stage_update = fields.Datetime("Last Stage Update", index=True, default=fields.Datetime.now) priority = fields.Selection(AVAILABLE_PRIORITIES, "Appreciation", default='0') job_id = fields.Many2one( 'hr.job', "Applied Job", domain= "['|', ('company_id', '=', False), ('company_id', '=', company_id)]") salary_proposed_extra = fields.Char( "Proposed Salary Extra", help="Salary Proposed by the Organisation, extra advantages") salary_expected_extra = fields.Char( "Expected Salary Extra", help="Salary Expected by Applicant, extra advantages") salary_proposed = fields.Float("Proposed Salary", group_operator="avg", help="Salary Proposed by the Organisation") salary_expected = fields.Float("Expected Salary", group_operator="avg", help="Salary Expected by Applicant") availability = fields.Date( "Availability", help= "The date at which the applicant will be available to start working") partner_name = fields.Char("Applicant's Name") partner_phone = fields.Char("Phone", size=32) partner_mobile = fields.Char("Mobile", size=32) type_id = fields.Many2one('hr.recruitment.degree', "Degree") department_id = fields.Many2one( 'hr.department', "Department", domain= "['|', ('company_id', '=', False), ('company_id', '=', company_id)]") day_open = fields.Float(compute='_compute_day', string="Days to Open", compute_sudo=True) day_close = fields.Float(compute='_compute_day', string="Days to Close", compute_sudo=True) delay_close = fields.Float(compute="_compute_day", string='Delay to Close', readonly=True, group_operator="avg", help="Number of days to close", store=True) color = fields.Integer("Color Index", default=0) emp_id = fields.Many2one('hr.employee', string="Employee", help="Employee linked to the applicant.", copy=False) user_email = fields.Char(related='user_id.email', type="char", string="User Email", readonly=True) attachment_number = fields.Integer(compute='_get_attachment_number', string="Number of Attachments") employee_name = fields.Char(related='emp_id.name', string="Employee Name", readonly=False, tracking=False) attachment_ids = fields.One2many('ir.attachment', 'res_id', domain=[('res_model', '=', 'hr.applicant') ], string='Attachments') kanban_state = fields.Selection([('normal', 'Grey'), ('done', 'Green'), ('blocked', 'Red')], string='Kanban State', copy=False, default='normal', required=True) legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked', readonly=False) legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid', readonly=False) legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing', readonly=False) application_count = fields.Integer(compute='_compute_application_count', help='Applications with the same email') meeting_count = fields.Integer(compute='_compute_meeting_count', help='Meeting Count') @api.depends('date_open', 'date_closed') def _compute_day(self): for applicant in self: if applicant.date_open: date_create = applicant.create_date date_open = applicant.date_open applicant.day_open = ( date_open - date_create).total_seconds() / (24.0 * 3600) else: applicant.day_open = False if applicant.date_closed: date_create = applicant.create_date date_closed = applicant.date_closed applicant.day_close = ( date_closed - date_create).total_seconds() / (24.0 * 3600) applicant.delay_close = applicant.day_close - applicant.day_open else: applicant.day_close = False applicant.delay_close = False @api.depends('email_from') def _compute_application_count(self): application_data = self.env['hr.applicant'].read_group( [('email_from', 'in', list(set(self.mapped('email_from'))))], ['email_from'], ['email_from']) application_data_mapped = dict( (data['email_from'], data['email_from_count']) for data in application_data) applicants = self.filtered(lambda applicant: applicant.email_from) for applicant in applicants: applicant.application_count = application_data_mapped.get( applicant.email_from, 1) - 1 (self - applicants).application_count = False def _compute_meeting_count(self): for applicant in self: applicant.meeting_count = self.env['calendar.event'].search_count([ ('applicant_id', '=', applicant.id) ]) def _get_attachment_number(self): read_group_res = self.env['ir.attachment'].read_group( [('res_model', '=', 'hr.applicant'), ('res_id', 'in', self.ids)], ['res_id'], ['res_id']) attach_data = dict( (res['res_id'], res['res_id_count']) for res in read_group_res) for record in self: record.attachment_number = attach_data.get(record.id, 0) @api.model def _read_group_stage_ids(self, stages, domain, order): # retrieve job_id from the context and write the domain: ids + contextual columns (job or default) job_id = self._context.get('default_job_id') search_domain = [('job_ids', '=', False)] if job_id: search_domain = ['|', ('job_ids', '=', job_id)] + search_domain if stages: search_domain = ['|', ('id', 'in', stages.ids)] + search_domain stage_ids = stages._search(search_domain, order=order, access_rights_uid=SUPERUSER_ID) return stages.browse(stage_ids) @api.onchange('job_id') def onchange_job_id(self): vals = self._onchange_job_id_internal(self.job_id.id) self.department_id = vals['value']['department_id'] self.user_id = vals['value']['user_id'] self.stage_id = vals['value']['stage_id'] def _onchange_job_id_internal(self, job_id): department_id = False user_id = False company_id = False stage_id = self.stage_id.id or self._context.get('default_stage_id') if job_id: job = self.env['hr.job'].browse(job_id) department_id = job.department_id.id user_id = job.user_id.id company_id = job.company_id.id if not stage_id: stage_ids = self.env['hr.recruitment.stage'].search( [ '|', ('job_ids', '=', False), ('job_ids', '=', job.id), ('fold', '=', False) ], order='sequence asc', limit=1).ids stage_id = stage_ids[0] if stage_ids else False return { 'value': { 'department_id': department_id, 'company_id': company_id, 'user_id': user_id, 'stage_id': stage_id } } @api.onchange('email_from') def onchange_email_from(self): if self.partner_id and self.email_from and not self.partner_id.email: self.partner_id.email = self.email_from @api.onchange('partner_phone') def onchange_partner_phone(self): if self.partner_id and self.partner_phone and not self.partner_id.phone: self.partner_id.phone = self.partner_phone @api.onchange('partner_mobile') def onchange_partner_mobile(self): if self.partner_id and self.partner_mobile and not self.partner_id.mobile: self.partner_id.mobile = self.partner_mobile @api.onchange('partner_id') def onchange_partner_id(self): self.partner_phone = self.partner_id.phone self.partner_mobile = self.partner_id.mobile self.email_from = self.partner_id.email @api.onchange('stage_id') def onchange_stage_id(self): vals = self._onchange_stage_id_internal(self.stage_id.id) if vals['value'].get('date_closed'): self.date_closed = vals['value']['date_closed'] def _onchange_stage_id_internal(self, stage_id): if not stage_id: return {'value': {}} stage = self.env['hr.recruitment.stage'].browse(stage_id) if stage.fold: return {'value': {'date_closed': fields.datetime.now()}} return {'value': {'date_closed': False}} @api.model def create(self, vals): if vals.get('department_id' ) and not self._context.get('default_department_id'): self = self.with_context( default_department_id=vals.get('department_id')) if vals.get('job_id') or self._context.get('default_job_id'): job_id = vals.get('job_id') or self._context.get('default_job_id') for key, value in self._onchange_job_id_internal( job_id)['value'].items(): if key not in vals: vals[key] = value if vals.get('user_id'): vals['date_open'] = fields.Datetime.now() if 'stage_id' in vals: vals.update( self._onchange_stage_id_internal( vals.get('stage_id'))['value']) return super(Applicant, self).create(vals) def write(self, vals): # user_id change: update date_open if vals.get('user_id'): vals['date_open'] = fields.Datetime.now() # stage_id: track last stage before update if 'stage_id' in vals: vals['date_last_stage_update'] = fields.Datetime.now() vals.update( self._onchange_stage_id_internal( vals.get('stage_id'))['value']) if 'kanban_state' not in vals: vals['kanban_state'] = 'normal' for applicant in self: vals['last_stage_id'] = applicant.stage_id.id res = super(Applicant, self).write(vals) else: res = super(Applicant, self).write(vals) return res @api.model def get_empty_list_help(self, help): return super( Applicant, self.with_context( empty_list_help_model='hr.job', empty_list_help_id=self.env.context.get('default_job_id'), empty_list_help_document_name=_( "job applicant"))).get_empty_list_help(help) def action_makeMeeting(self): """ This opens Meeting's calendar view to schedule meeting on current applicant @return: Dictionary value for created Meeting view """ self.ensure_one() partners = self.partner_id | self.user_id.partner_id | self.department_id.manager_id.user_id.partner_id category = self.env.ref('hr_recruitment.categ_meet_interview') res = self.env['ir.actions.act_window'].for_xml_id( 'calendar', 'action_calendar_event') res['context'] = { 'default_partner_ids': partners.ids, 'default_user_id': self.env.uid, 'default_name': self.name, 'default_categ_ids': category and [category.id] or False, } return res def action_get_attachment_tree_view(self): attachment_action = self.env.ref('base.action_attachment') action = attachment_action.read()[0] action['context'] = { 'default_res_model': self._name, 'default_res_id': self.ids[0] } action['domain'] = str( ['&', ('res_model', '=', self._name), ('res_id', 'in', self.ids)]) action['search_view_id'] = (self.env.ref( 'hr_recruitment.ir_attachment_view_search_inherit_hr_recruitment'). id, ) return action def action_applications_email(self): return { 'type': 'ir.actions.act_window', 'name': _('Applications'), 'res_model': self._name, 'view_mode': 'kanban,tree,form,pivot,graph,calendar,activity', 'domain': [('email_from', 'in', self.mapped('email_from'))], } def _track_template(self, changes): res = super(Applicant, self)._track_template(changes) applicant = self[0] if 'stage_id' in changes and applicant.stage_id.template_id: res['stage_id'] = (applicant.stage_id.template_id, { 'auto_delete_message': True, 'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'), 'email_layout_xmlid': 'mail.mail_notification_light' }) return res def _creation_subtype(self): return self.env.ref('hr_recruitment.mt_applicant_new') def _track_subtype(self, init_values): record = self[0] if 'stage_id' in init_values and record.stage_id: return self.env.ref('hr_recruitment.mt_applicant_stage_changed') return super(Applicant, self)._track_subtype(init_values) def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None): """ Override to set alias of applicants to their job definition if any. """ aliases = self.mapped('job_id')._notify_get_reply_to(default=default, records=None, company=company, doc_names=None) res = {app.id: aliases.get(app.job_id.id) for app in self} leftover = self.filtered(lambda rec: not rec.job_id) if leftover: res.update( super(Applicant, leftover)._notify_get_reply_to(default=default, records=None, company=company, doc_names=doc_names)) return res def _message_get_suggested_recipients(self): recipients = super(Applicant, self)._message_get_suggested_recipients() for applicant in self: if applicant.partner_id: applicant._message_add_suggested_recipient( recipients, partner=applicant.partner_id, reason=_('Contact')) elif applicant.email_from: email_from = applicant.email_from if applicant.partner_name: email_from = '%s<%s>' % (applicant.partner_name, email_from) applicant._message_add_suggested_recipient( recipients, email=email_from, reason=_('Contact Email')) return recipients @api.model def message_new(self, msg, custom_values=None): """ Overrides mail_thread message_new that is called by the mailgateway through message_process. This override updates the document according to the email. """ # remove default author when going through the mail gateway. Indeed we # do not want to explicitly set user_id to False; however we do not # want the gateway user to be responsible if no other responsible is # found. self = self.with_context(default_user_id=False) val = msg.get('from').split('<')[0] defaults = { 'name': msg.get('subject') or _("No Subject"), 'partner_name': val, 'email_from': msg.get('from'), 'partner_id': msg.get('author_id', False), } if msg.get('priority'): defaults['priority'] = msg.get('priority') if custom_values: defaults.update(custom_values) return super(Applicant, self).message_new(msg, custom_values=defaults) def _message_post_after_hook(self, message, msg_vals): if self.email_from 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_from) if new_partner: self.search([('partner_id', '=', False), ('email_from', '=', new_partner.email), ('stage_id.fold', '=', False) ]).write({'partner_id': new_partner.id}) return super(Applicant, self)._message_post_after_hook(message, msg_vals) def create_employee_from_applicant(self): """ Create an hr.employee from the hr.applicants """ employee = False for applicant in self: contact_name = False if applicant.partner_id: address_id = applicant.partner_id.address_get(['contact' ])['contact'] contact_name = applicant.partner_id.display_name else: if not applicant.partner_name: raise UserError( _('You must define a Contact Name for this applicant.') ) new_partner_id = self.env['res.partner'].create({ 'is_company': False, 'name': applicant.partner_name, 'email': applicant.email_from, 'phone': applicant.partner_phone, 'mobile': applicant.partner_mobile }) address_id = new_partner_id.address_get(['contact'])['contact'] if applicant.partner_name or contact_name: employee = self.env['hr.employee'].create({ 'name': applicant.partner_name or contact_name, 'job_id': applicant.job_id.id or False, 'job_title': applicant.job_id.name, 'address_home_id': address_id, 'department_id': applicant.department_id.id or False, 'address_id': applicant.company_id and applicant.company_id.partner_id and applicant.company_id.partner_id.id or False, 'work_email': applicant.department_id and applicant.department_id.company_id and applicant.department_id.company_id.email or False, 'work_phone': applicant.department_id and applicant.department_id.company_id and applicant.department_id.company_id.phone or False }) applicant.write({'emp_id': employee.id}) if applicant.job_id: applicant.job_id.write({ 'no_of_hired_employee': applicant.job_id.no_of_hired_employee + 1 }) applicant.job_id.message_post( body=_('New Employee %s Hired') % applicant.partner_name if applicant.partner_name else applicant.name, subtype="hr_recruitment.mt_job_applicant_hired") applicant.message_post_with_view( 'hr_recruitment.applicant_hired_template', values={'applicant': applicant}, subtype_id=self.env.ref( "hr_recruitment.mt_applicant_hired").id) employee_action = self.env.ref('hr.open_view_employee_list') dict_act_window = employee_action.read([])[0] dict_act_window['context'] = {'form_view_initial_mode': 'edit'} dict_act_window['res_id'] = employee.id return dict_act_window def archive_applicant(self): self.write({'active': False}) def reset_applicant(self): """ Reinsert the applicant into the recruitment pipe in the first stage""" default_stage_id = self._default_stage_id() self.write({'active': True, 'stage_id': default_stage_id})
class HrEmployeeBase(models.AbstractModel): _inherit = "hr.employee.base" attendance_ids = fields.One2many( 'hr.attendance', 'employee_id', help='list of attendances for the employee') last_attendance_id = fields.Many2one('hr.attendance', compute='_compute_last_attendance_id', store=True) last_check_in = fields.Datetime(related='last_attendance_id.check_in', store=True) last_check_out = fields.Datetime(related='last_attendance_id.check_out', store=True) attendance_state = fields.Selection(string="Attendance Status", compute='_compute_attendance_state', selection=[ ('checked_out', "Checked out"), ('checked_in', "Checked in") ]) hours_last_month = fields.Float(compute='_compute_hours_last_month') hours_today = fields.Float(compute='_compute_hours_today') hours_last_month_display = fields.Char(compute='_compute_hours_last_month') def _compute_presence_state(self): """ Override to include checkin/checkout in the presence state Attendance has the second highest priority after login """ super()._compute_presence_state() employees = self.filtered( lambda employee: employee.hr_presence_state != 'present') for employee in employees: if employee.attendance_state == 'checked_out' and employee.hr_presence_state == 'to_define': employee.hr_presence_state = 'absent' for employee in employees: if employee.attendance_state == 'checked_in': employee.hr_presence_state = 'present' def _compute_hours_last_month(self): for employee in self: now = datetime.now() start = now + relativedelta(months=-1, day=1) end = now + relativedelta(days=-1, day=1) attendances = self.env['hr.attendance'].search([ ('employee_id', '=', employee.id), ('check_in', '>=', start), ('check_out', '<=', end), ]) employee.hours_last_month = sum(attendances.mapped('worked_hours')) employee.hours_last_month_display = "%g" % employee.hours_last_month def _compute_hours_today(self): now = fields.Datetime.now() now_utc = pytz.utc.localize(now) for employee in self: # start of day in the employee's timezone might be the previous day in utc tz = pytz.timezone(employee.tz) now_tz = now_utc.astimezone(tz) start_tz = now_tz + relativedelta( hour=0, minute=0) # day start in the employee's timezone start_naive = start_tz.astimezone(pytz.utc).replace(tzinfo=None) attendances = self.env['hr.attendance'].search([ ('employee_id', '=', employee.id), ('check_in', '<=', now), '|', ('check_out', '>=', start_naive), ('check_out', '=', False), ]) worked_hours = 0 for attendance in attendances: delta = (attendance.check_out or now) - max( attendance.check_in, start_naive) worked_hours += delta.total_seconds() / 3600.0 employee.hours_today = worked_hours @api.depends('attendance_ids') def _compute_last_attendance_id(self): for employee in self: employee.last_attendance_id = self.env['hr.attendance'].search( [ ('employee_id', '=', employee.id), ], limit=1) @api.depends('last_attendance_id.check_in', 'last_attendance_id.check_out', 'last_attendance_id') def _compute_attendance_state(self): for employee in self: att = employee.last_attendance_id.sudo() employee.attendance_state = att and not att.check_out and 'checked_in' or 'checked_out' @api.model def attendance_scan(self, barcode): """ Receive a barcode scanned from the Kiosk Mode and change the attendances of corresponding employee. Returns either an action or a warning. """ employee = self.sudo().search([('barcode', '=', barcode)], limit=1) if employee: return employee._attendance_action( 'hr_attendance.hr_attendance_action_kiosk_mode') return { 'warning': _('No employee corresponding to barcode %(barcode)s') % { 'barcode': barcode } } def attendance_manual(self, next_action, entered_pin=None): self.ensure_one() can_check_without_pin = not self.env.user.has_group( 'hr_attendance.group_hr_attendance_use_pin') or ( self.user_id == self.env.user and entered_pin is None) if can_check_without_pin or entered_pin is not None and entered_pin == self.sudo( ).pin: return self._attendance_action(next_action) return {'warning': _('Wrong PIN')} def _attendance_action(self, next_action): """ Changes the attendance of the employee. Returns an action to the check in/out message, next_action defines which menu the check in/out message should return to. ("My Attendances" or "Kiosk Mode") """ self.ensure_one() employee = self.sudo() action_message = self.env.ref( 'hr_attendance.hr_attendance_action_greeting_message').read()[0] action_message[ 'previous_attendance_change_date'] = employee.last_attendance_id and ( employee.last_attendance_id.check_out or employee.last_attendance_id.check_in) or False action_message['employee_name'] = employee.name action_message['barcode'] = employee.barcode action_message['next_action'] = next_action action_message['hours_today'] = employee.hours_today if employee.user_id: modified_attendance = employee.with_user( employee.user_id)._attendance_action_change() else: modified_attendance = employee._attendance_action_change() action_message['attendance'] = modified_attendance.read()[0] return {'action': action_message} def _attendance_action_change(self): """ Check In/Check Out action Check In: create a new attendance record Check Out: modify check_out field of appropriate attendance record """ self.ensure_one() action_date = fields.Datetime.now() if self.attendance_state != 'checked_in': vals = { 'employee_id': self.id, 'check_in': action_date, } return self.env['hr.attendance'].create(vals) attendance = self.env['hr.attendance'].search( [('employee_id', '=', self.id), ('check_out', '=', False)], limit=1) if attendance: attendance.check_out = action_date else: raise exceptions.UserError( _('Cannot perform check out on %(empl_name)s, could not find corresponding check in. ' 'Your attendances have probably been modified manually by human resources.' ) % { 'empl_name': self.sudo().name, }) return attendance @api.model def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): if 'pin' in groupby or 'pin' in self.env.context.get( 'group_by', '') or self.env.context.get('no_group_by'): raise exceptions.UserError(_('Such grouping is not allowed.')) return super(HrEmployeeBase, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
class ProductReplenish(models.TransientModel): _name = 'product.replenish' _description = 'Product Replenish' product_id = fields.Many2one('product.product', string='Product', required=True) product_tmpl_id = fields.Many2one('product.template', string='Product Template', required=True) product_has_variants = fields.Boolean('Has variants', default=False, required=True) product_uom_category_id = fields.Many2one( 'uom.category', related='product_id.uom_id.category_id', readonly=True, required=True) product_uom_id = fields.Many2one('uom.uom', string='Unity of measure', required=True) quantity = fields.Float('Quantity', default=1, required=True) date_planned = fields.Datetime( 'Scheduled Date', required=True, help="Date at which the replenishment should take place.") warehouse_id = fields.Many2one('stock.warehouse', string='Warehouse', required=True, domain="[('company_id', '=', company_id)]") route_ids = fields.Many2many( 'stock.location.route', string='Preferred Routes', help= "Apply specific route(s) for the replenishment instead of product's default routes.", domain= "['|', ('company_id', '=', False), ('company_id', '=', company_id)]") company_id = fields.Many2one('res.company') @api.model def default_get(self, fields): res = super(ProductReplenish, self).default_get(fields) product_tmpl_id = self.env['product.template'] if 'product_id' in fields: if self.env.context.get('default_product_id'): product_id = self.env['product.product'].browse( self.env.context['default_product_id']) product_tmpl_id = product_id.product_tmpl_id res['product_tmpl_id'] = product_id.product_tmpl_id.id res['product_id'] = product_id.id elif self.env.context.get('default_product_tmpl_id'): product_tmpl_id = self.env['product.template'].browse( self.env.context['default_product_tmpl_id']) res['product_tmpl_id'] = product_tmpl_id.id res['product_id'] = product_tmpl_id.product_variant_id.id if len(product_tmpl_id.product_variant_ids) > 1: res['product_has_variants'] = True company = product_tmpl_id.company_id or self.env.company if 'product_uom_id' in fields: res['product_uom_id'] = product_tmpl_id.uom_id.id if 'company_id' in fields: res['company_id'] = company.id if 'warehouse_id' in fields: warehouse = self.env['stock.warehouse'].search( [('company_id', '=', company.id)], limit=1) res['warehouse_id'] = warehouse.id if 'date_planned' in fields: res['date_planned'] = datetime.datetime.now() return res def launch_replenishment(self): uom_reference = self.product_id.uom_id self.quantity = self.product_uom_id._compute_quantity( self.quantity, uom_reference) try: self.env['procurement.group'].with_context( clean_context(self.env.context)).run([ self.env['procurement.group'].Procurement( self.product_id, self.quantity, uom_reference, self.warehouse_id.lot_stock_id, # Location _("Manual Replenishment"), # Name _("Manual Replenishment"), # Origin self.warehouse_id.company_id, self._prepare_run_values() # Values ) ]) except UserError as error: raise UserError(error) def _prepare_run_values(self): replenishment = self.env['procurement.group'].create({ 'partner_id': self.product_id.with_context( force_company=self.company_id.id).responsible_id.partner_id.id, }) values = { 'warehouse_id': self.warehouse_id, 'route_ids': self.route_ids, 'date_planned': self.date_planned, 'group_id': replenishment, } return values
class PosSaleReport(models.Model): _name = "report.all.channels.sales" _description = "Sales by Channel (All in One)" _auto = False name = fields.Char('Order Reference', readonly=True) partner_id = fields.Many2one('res.partner', 'Partner', readonly=True) product_id = fields.Many2one('product.product', string='Product', readonly=True) product_tmpl_id = fields.Many2one('product.template', 'Product Template', readonly=True) date_order = fields.Datetime(string='Date Order', readonly=True) user_id = fields.Many2one('res.users', 'Salesperson', readonly=True) categ_id = fields.Many2one('product.category', 'Product Category', readonly=True) company_id = fields.Many2one('res.company', 'Company', readonly=True) price_total = fields.Float('Total', readonly=True) pricelist_id = fields.Many2one('product.pricelist', 'Pricelist', readonly=True) country_id = fields.Many2one('res.country', 'Partner Country', readonly=True) price_subtotal = fields.Float(string='Price Subtotal', readonly=True) product_qty = fields.Float('Product Quantity', readonly=True) analytic_account_id = fields.Many2one('account.analytic.account', 'Analytic Account', readonly=True) team_id = fields.Many2one('crm.team', 'Sales Team', readonly=True) def _so(self): so_str = """ SELECT sol.id AS id, so.name AS name, so.partner_id AS partner_id, sol.product_id AS product_id, pro.product_tmpl_id AS product_tmpl_id, so.date_order AS date_order, so.user_id AS user_id, pt.categ_id AS categ_id, so.company_id AS company_id, sol.price_total / CASE COALESCE(so.currency_rate, 0) WHEN 0 THEN 1.0 ELSE so.currency_rate END AS price_total, so.pricelist_id AS pricelist_id, rp.country_id AS country_id, sol.price_subtotal / CASE COALESCE(so.currency_rate, 0) WHEN 0 THEN 1.0 ELSE so.currency_rate END AS price_subtotal, (sol.product_uom_qty / u.factor * u2.factor) as product_qty, so.analytic_account_id AS analytic_account_id, so.team_id AS team_id FROM sale_order_line sol JOIN sale_order so ON (sol.order_id = so.id) LEFT JOIN product_product pro ON (sol.product_id = pro.id) JOIN res_partner rp ON (so.partner_id = rp.id) LEFT JOIN product_template pt ON (pro.product_tmpl_id = pt.id) LEFT JOIN product_pricelist pp ON (so.pricelist_id = pp.id) LEFT JOIN uom_uom u on (u.id=sol.product_uom) LEFT JOIN uom_uom u2 on (u2.id=pt.uom_id) WHERE so.state in ('sale','done') """ return so_str def _from(self): return """(%s)""" % (self._so()) def get_main_request(self): request = """ CREATE or REPLACE VIEW %s AS SELECT id AS id, name, partner_id, product_id, product_tmpl_id, date_order, user_id, categ_id, company_id, price_total, pricelist_id, analytic_account_id, country_id, team_id, price_subtotal, product_qty FROM %s AS foo""" % (self._table, self._from()) return request def init(self): tools.drop_view_if_exists(self.env.cr, self._table) self.env.cr.execute(self.get_main_request())
class ResPartner(models.Model): _name = 'res.partner' _inherit = 'res.partner' @api.depends_context('force_company') def _credit_debit_get(self): tables, where_clause, where_params = self.env['account.move.line'].with_context(state='posted', company_id=self.env.company.id)._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 """ + tables + """ 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) treated = self.browse() for pid, type, val in self._cr.fetchall(): partner = self.browse(pid) if type == 'receivable': partner.credit = val partner.debit = False treated |= partner elif type == 'payable': partner.debit = -val partner.credit = False treated |= partner remaining = (self - treated) remaining.debit = False remaining.credit = False 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 JOIN account_move move ON move.id = aml.move_id RIGHT JOIN account_account acc ON aml.account_id = acc.id WHERE acc.internal_type = %s AND NOT acc.deprecated AND acc.company_id = %s AND move.state = 'posted' GROUP BY partner.id HAVING %s * COALESCE(SUM(aml.amount_residual), 0) ''' + operator + ''' %s''', (account_type, self.env.user.company_id.id, 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) def _invoice_total(self): account_invoice_report = self.env['account.invoice.report'] if not self.ids: return True user_currency_id = self.env.company.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_subtotal) 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) 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)]) def _compute_has_unreconciled_entries(self): for partner in self: # Avoid useless work if has_unreconciled_entries is not relevant for this partner if not partner.active or not partner.is_company and partner.parent_id: partner.has_unreconciled_entries = False continue 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) """, (partner.id,)) partner.has_unreconciled_entries = self.env.cr.rowcount == 1 def mark_as_reconciled(self): self.env['account.partial.reconcile'].check_access_rights('write') return self.sudo().with_context(company_id=self.env.company.id).write({'last_time_entries_checked': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}) def _get_company_currency(self): for partner in self: if partner.company_id: partner.currency_id = partner.sudo().company_id.currency_id else: partner.currency_id = self.env.company.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') 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", 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", 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 determines the taxes/accounts used for this contact.") 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") 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") ref_company_ids = fields.One2many('res.company', 'partner_id', string='Companies that refers to partner') 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( 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.move', 'partner_id', string='Invoices', readonly=True, copy=False) contract_ids = fields.One2many('account.analytic.account', 'partner_id', string='Partner 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, default="no-message") invoice_warn_msg = fields.Text('Message for Invoice') # Computed fields to order the partners as suppliers/customers according to the # amount of their generated incoming/outgoing account moves supplier_rank = fields.Integer(default=0) customer_rank = fields.Integer(default=0) def _get_name_search_order_by_fields(self): res = super()._get_name_search_order_by_fields() partner_search_mode = self.env.context.get('res_partner_search_mode') if not partner_search_mode in ('customer', 'supplier'): return res order_by_field = 'COALESCE(res_partner.%s, 0) DESC,' if partner_search_mode == 'customer': field = 'customer_rank' else: field = 'supplier_rank' order_by_field = order_by_field % field return '%s, %s' % (res, order_by_field % field) if res else order_by_field 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'] def action_view_partner_invoices(self): self.ensure_one() action = self.env.ref('account.action_move_out_invoice_type').read()[0] action['domain'] = [ ('type', 'in', ('out_invoice', 'out_refund')), ('state', '=', 'posted'), ('partner_id', 'child_of', self.id), ] action['context'] = {'default_type':'out_invoice', 'type':'out_invoice', 'journal_type': 'sale', 'search_default_unpaid': 1} return action @api.onchange('company_id') def _onchange_company_id(self): if self.company_id: company = self.company_id else: company = self.env.company return {'domain': {'property_account_position_id': [('company_id', 'in', [company.id, False])]}} def can_edit_vat(self): ''' Can't edit `vat` if there is (non draft) issued invoices. ''' can_edit_vat = super(ResPartner, self).can_edit_vat() if not can_edit_vat: return can_edit_vat has_invoice = self.env['account.move'].search([ ('type', 'in', ['out_invoice', 'out_refund']), ('partner_id', 'child_of', self.commercial_partner_id.id), ('state', '=', 'posted') ], limit=1) return can_edit_vat and not (bool(has_invoice)) @api.model_create_multi def create(self, vals_list): search_partner_mode = self.env.context.get('res_partner_search_mode') is_customer = search_partner_mode == 'customer' is_supplier = search_partner_mode == 'supplier' if search_partner_mode: for vals in vals_list: if is_customer and 'customer_rank' not in vals: vals['customer_rank'] = 1 elif is_supplier and 'supplier_rank' not in vals: vals['supplier_rank'] = 1 return super().create(vals_list) def _increase_rank(self, field): if self.ids and field in ['customer_rank', 'supplier_rank']: try: with self.env.cr.savepoint(): query = sql.SQL(""" SELECT {field} FROM res_partner WHERE ID IN %(partner_ids)s FOR UPDATE NOWAIT; UPDATE res_partner SET {field} = {field} + 1 WHERE id IN %(partner_ids)s """).format(field=sql.Identifier(field)) self.env.cr.execute(query, {'partner_ids': tuple(self.ids)}) for partner in self: self.env.cache.remove(partner, partner._fields[field]) except DatabaseError as e: if e.pgcode == '55P03': _logger.debug('Another transaction already locked partner rows. Cannot update partner ranks.') else: raise e
class ResCompany(models.Model): _inherit = 'res.company' hr_presence_last_compute_date = fields.Datetime()