[REF] hr.employee: from photo to image. Updated demo data.
[odoo/odoo.git] / addons / hr / hr.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import addons
23 import logging
24 from osv import fields, osv
25 import tools
26 _logger = logging.getLogger(__name__)
27
28 class hr_employee_category(osv.osv):
29
30     def name_get(self, cr, uid, ids, context=None):
31         if not ids:
32             return []
33         reads = self.read(cr, uid, ids, ['name','parent_id'], context=context)
34         res = []
35         for record in reads:
36             name = record['name']
37             if record['parent_id']:
38                 name = record['parent_id'][1]+' / '+name
39             res.append((record['id'], name))
40         return res
41
42     def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
43         res = self.name_get(cr, uid, ids, context=context)
44         return dict(res)
45
46     _name = "hr.employee.category"
47     _description = "Employee Category"
48     _columns = {
49         'name': fields.char("Category", size=64, required=True),
50         'complete_name': fields.function(_name_get_fnc, type="char", string='Name'),
51         'parent_id': fields.many2one('hr.employee.category', 'Parent Category', select=True),
52         'child_ids': fields.one2many('hr.employee.category', 'parent_id', 'Child Categories'),
53         'employee_ids': fields.many2many('hr.employee', 'employee_category_rel', 'category_id', 'emp_id', 'Employees'),
54     }
55
56     def _check_recursion(self, cr, uid, ids, context=None):
57         level = 100
58         while len(ids):
59             cr.execute('select distinct parent_id from hr_employee_category where id IN %s', (tuple(ids), ))
60             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
61             if not level:
62                 return False
63             level -= 1
64         return True
65
66     _constraints = [
67         (_check_recursion, 'Error ! You cannot create recursive Categories.', ['parent_id'])
68     ]
69
70 hr_employee_category()
71
72 class hr_job(osv.osv):
73
74     def _no_of_employee(self, cr, uid, ids, name, args, context=None):
75         res = {}
76         for job in self.browse(cr, uid, ids, context=context):
77             nb_employees = len(job.employee_ids or [])
78             res[job.id] = {
79                 'no_of_employee': nb_employees,
80                 'expected_employees': nb_employees + job.no_of_recruitment,
81             }
82         return res
83
84     def _get_job_position(self, cr, uid, ids, context=None):
85         res = []
86         for employee in self.pool.get('hr.employee').browse(cr, uid, ids, context=context):
87             if employee.job_id:
88                 res.append(employee.job_id.id)
89         return res
90
91     _name = "hr.job"
92     _description = "Job Description"
93     _columns = {
94         'name': fields.char('Job Name', size=128, required=True, select=True),
95         'expected_employees': fields.function(_no_of_employee, string='Total Employees',
96             help='Expected number of employees for this job position after new recruitment.',
97             store = {
98                 'hr.job': (lambda self,cr,uid,ids,c=None: ids, ['no_of_recruitment'], 10),
99                 'hr.employee': (_get_job_position, ['job_id'], 10),
100             },
101             multi='no_of_employee'),
102         'no_of_employee': fields.function(_no_of_employee, string="Number of Employees",
103             help='Number of employees currently occupying this job position.',
104             store = {
105                 'hr.employee': (_get_job_position, ['job_id'], 10),
106             },
107             multi='no_of_employee'),
108         'no_of_recruitment': fields.float('Expected in Recruitment', help='Number of new employees you expect to recruit.'),
109         'employee_ids': fields.one2many('hr.employee', 'job_id', 'Employees'),
110         'description': fields.text('Job Description'),
111         'requirements': fields.text('Requirements'),
112         'department_id': fields.many2one('hr.department', 'Department'),
113         'company_id': fields.many2one('res.company', 'Company'),
114         'state': fields.selection([('open', 'In Position'), ('recruit', 'In Recruitement')], 'Status', readonly=True, required=True,
115             help="By default 'In position', set it to 'In Recruitment' if recruitment process is going on for this job position."),
116     }
117     _defaults = {
118         'expected_employees': 1,
119         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'hr.job', context=c),
120         'state': 'open',
121     }
122
123     _sql_constraints = [
124         ('name_company_uniq', 'unique(name, company_id)', 'The name of the job position must be unique per company!'),
125     ]
126
127
128     def on_change_expected_employee(self, cr, uid, ids, no_of_recruitment, no_of_employee, context=None):
129         if context is None:
130             context = {}
131         return {'value': {'expected_employees': no_of_recruitment + no_of_employee}}
132
133     def job_recruitement(self, cr, uid, ids, *args):
134         for job in self.browse(cr, uid, ids):
135             no_of_recruitment = job.no_of_recruitment == 0 and 1 or job.no_of_recruitment
136             self.write(cr, uid, [job.id], {'state': 'recruit', 'no_of_recruitment': no_of_recruitment})
137         return True
138
139     def job_open(self, cr, uid, ids, *args):
140         self.write(cr, uid, ids, {'state': 'open', 'no_of_recruitment': 0})
141         return True
142
143 hr_job()
144
145 class hr_employee(osv.osv):
146     _name = "hr.employee"
147     _description = "Employee"
148     _inherits = {'resource.resource': "resource_id"}
149
150     def _get_image_resized(self, cr, uid, ids, name, args, context=None):
151         result = dict.fromkeys(ids, False)
152         for hr_employee in self.browse(cr, uid, ids, context=context):
153             result[hr_employee.id] = {'image_medium': False, 'image_small': False}
154             if hr_employee.image:
155                 result[hr_employee.id]['image_medium'] = tools.resize_image_medium(hr_employee.image)
156                 result[hr_employee.id]['image_small'] = tools.resize_image_small(hr_employee.image)
157         return result
158     
159     def _set_image_resized(self, cr, uid, id, name, value, args, context=None):
160         if not value:
161             vals = {'image': value}
162         else:
163             vals = {'image': tools.resize_image_big(value)}
164         return self.write(cr, uid, [id], vals, context=context)
165     
166     def onchange_image(self, cr, uid, ids, value, context=None):
167         if not value:
168             return {'value': {
169                     'image': value,
170                     'image_medium': value,
171                     'image_small': value,
172                     }}
173         return {'value': {
174                     'image': tools.resize_image_big(value),
175                     'image_medium': tools.resize_image_medium(value),
176                     'image_small': tools.resize_image_small(value),
177                     }}
178     
179     _columns = {
180         'country_id': fields.many2one('res.country', 'Nationality'),
181         'birthday': fields.date("Date of Birth"),
182         'ssnid': fields.char('SSN No', size=32, help='Social Security Number'),
183         'sinid': fields.char('SIN No', size=32, help="Social Insurance Number"),
184         'identification_id': fields.char('Identification No', size=32),
185         'otherid': fields.char('Other Id', size=64),
186         'gender': fields.selection([('male', 'Male'),('female', 'Female')], 'Gender'),
187         'marital': fields.selection([('single', 'Single'), ('married', 'Married'), ('widower', 'Widower'), ('divorced', 'Divorced')], 'Marital Status'),
188         'department_id':fields.many2one('hr.department', 'Department'),
189         'address_id': fields.many2one('res.partner', 'Working Address'),
190         'address_home_id': fields.many2one('res.partner', 'Home Address'),
191         'bank_account_id':fields.many2one('res.partner.bank', 'Bank Account Number', domain="[('partner_id','=',address_home_id)]", help="Employee bank salary account"),
192         'work_phone': fields.char('Work Phone', size=32, readonly=False),
193         'mobile_phone': fields.char('Work Mobile', size=32, readonly=False),
194         'work_email': fields.char('Work E-mail', size=240),
195         'work_location': fields.char('Office Location', size=32),
196         'notes': fields.text('Notes'),
197         'parent_id': fields.many2one('hr.employee', 'Manager'),
198         'category_ids': fields.many2many('hr.employee.category', 'employee_category_rel', 'emp_id', 'category_id', 'Categories'),
199         'child_ids': fields.one2many('hr.employee', 'parent_id', 'Subordinates'),
200         'resource_id': fields.many2one('resource.resource', 'Resource', ondelete='cascade', required=True),
201         'coach_id': fields.many2one('hr.employee', 'Coach'),
202         'job_id': fields.many2one('hr.job', 'Job'),
203         'image': fields.binary("Photo",
204             help="This field holds the photo used as image for the "\
205                  "user. The avatar field is used as an interface to "\
206                  "access this field. The image is base64 encoded, "\
207                  "and PIL-supported. It is stored as a 540x450 px "\
208                  "image, in case a bigger image must be used."),
209         'image_medium': fields.function(_get_image_resized, fnct_inv=_set_image_resized,
210             string="Medium-sized photo", type="binary", multi="_get_image_resized",
211             store = {
212                 'hr.employee': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
213             },
214             help="Medium-sized photo of the user. It is automatically "\
215                  "resized as a 180x180px image, with aspect ratio keps. "\
216                  "Use this field in form views or some kanban views."),
217         'image_small': fields.function(_get_image_resized, fnct_inv=_set_image_resized,
218             string="Smal-sized photo", type="binary", multi="_get_image_resized",
219             store = {
220                 'hr.employee': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10),
221             },
222             help="Small-sized photo of the user. It is automatically "\
223                  "resized as a 50x50px image, with aspect ratio keps. "\
224                  "Use this field in form views or some kanban views."),
225         'active': fields.boolean('Active'),
226         'passport_id':fields.char('Passport No', size=64),
227         'color': fields.integer('Color Index'),
228         'city': fields.related('address_id', 'city', type='char', string='City'),
229         'login': fields.related('user_id', 'login', type='char', string='Login', readonly=1),
230         'last_login': fields.related('user_id', 'date', type='datetime', string='Latest Connection', readonly=1),
231     }
232
233     def unlink(self, cr, uid, ids, context=None):
234         resource_obj = self.pool.get('resource.resource')
235         resource_ids = []
236         for employee in self.browse(cr, uid, ids, context=context):
237             resource = employee.resource_id
238             if resource:
239                 resource_ids.append(resource.id)
240         if resource_ids:
241             resource_obj.unlink(cr, uid, resource_ids, context=context)
242         return super(hr_employee, self).unlink(cr, uid, ids, context=context)
243
244     def onchange_address_id(self, cr, uid, ids, address, context=None):
245         if address:
246             address = self.pool.get('res.partner').browse(cr, uid, address, context=context)
247             return {'value': {'work_email': address.email, 'work_phone': address.phone, 'mobile_phone': address.mobile}}
248         return {'value': {}}
249
250     def onchange_company(self, cr, uid, ids, company, context=None):
251         address_id = False
252         if company:
253             company_id = self.pool.get('res.company').browse(cr, uid, company, context=context)
254             address = self.pool.get('res.partner').address_get(cr, uid, [company_id.partner_id.id], ['default'])
255             address_id = address and address['default'] or False
256         return {'value': {'address_id' : address_id}}
257
258     def onchange_department_id(self, cr, uid, ids, department_id, context=None):
259         value = {'parent_id': False}
260         if department_id:
261             department = self.pool.get('hr.department').browse(cr, uid, department_id)
262             value['parent_id'] = department.manager_id.id
263         return {'value': value}
264
265     def onchange_user(self, cr, uid, ids, user_id, context=None):
266         work_email = False
267         if user_id:
268             work_email = self.pool.get('res.users').browse(cr, uid, user_id, context=context).user_email
269         return {'value': {'work_email' : work_email}}
270
271     def _get_photo(self, cr, uid, context=None):
272         image_path = addons.get_module_resource('hr', 'images', 'photo.png')
273         return tools.resize_image_big(open(image_path, 'rb').read().encode('base64'))
274
275     _defaults = {
276         'active': 1,
277         'image_medium': _get_photo,
278         'marital': 'single',
279         'color': 0,
280     }
281
282     def _check_recursion(self, cr, uid, ids, context=None):
283         level = 100
284         while len(ids):
285             cr.execute('SELECT DISTINCT parent_id FROM hr_employee WHERE id IN %s AND parent_id!=id',(tuple(ids),))
286             ids = filter(None, map(lambda x:x[0], cr.fetchall()))
287             if not level:
288                 return False
289             level -= 1
290         return True
291
292     _constraints = [
293         (_check_recursion, 'Error ! You cannot create recursive Hierarchy of Employees.', ['parent_id']),
294     ]
295
296 hr_employee()
297
298 class hr_department(osv.osv):
299     _description = "Department"
300     _inherit = 'hr.department'
301     _columns = {
302         'manager_id': fields.many2one('hr.employee', 'Manager'),
303         'member_ids': fields.one2many('hr.employee', 'department_id', 'Members', readonly=True),
304     }
305
306 hr_department()
307
308
309 class res_users(osv.osv):
310     _name = 'res.users'
311     _inherit = 'res.users'
312
313     def create(self, cr, uid, data, context=None):
314         user_id = super(res_users, self).create(cr, uid, data, context=context)
315
316         # add shortcut unless 'noshortcut' is True in context
317         if not(context and context.get('noshortcut', False)):
318             data_obj = self.pool.get('ir.model.data')
319             try:
320                 data_id = data_obj._get_id(cr, uid, 'hr', 'ir_ui_view_sc_employee')
321                 view_id  = data_obj.browse(cr, uid, data_id, context=context).res_id
322                 self.pool.get('ir.ui.view_sc').copy(cr, uid, view_id, default = {
323                                             'user_id': user_id}, context=context)
324             except:
325                 # Tolerate a missing shortcut. See product/product.py for similar code.
326                 _logger.debug('Skipped meetings shortcut for user "%s"', data.get('name','<new'))
327
328         return user_id
329
330 res_users()
331
332
333 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: