[REF] hr_recruitment stage management cleaning
Using stages and states is not easy to manage. Indeed those two fields mess with the same basic concept. This task remove the state on hr.applicant model, leaving only stages. Main features: - removed base_stage inheritance on hr.applicant model - removed state field on hr.applicant model - removed state field on hr.recruitment.stage model - removed date field on hr.applicant model, not used anywhere; other date fields are considered as sufficient - added date_last_stage_update on hr.applicant model, holding the date of the last stage change, allowing reporting and analysis on time spend in stages - date_open is set when assigning an user to the applicant; date_open now means assignation date This allows to remove nearly all buttons on the form view. As the statusbar is clickable, the only remaining action is to create an employee from the applicant, which is a link button in the form view. Subtypes have been updated : - new applicant: stage_id.sequence=1 - other: stage change Misc: - [IMP] views accordingly; - [IMP] reports accordingly, added date_last_stage_update in reports; - [IMP] subtypes: New, Stage Changed and Hired are kept; - [REM] removed some tests because they are not applicable anymore; - [DOC] doc directory + changelog + stage explanation; - [REM] removed hired.employee wizard bzr revid: tde@openerp.com-20130709115904-o9ac29fesu3lreol
This commit is contained in:
commit
9e2c08219d
|
@ -47,7 +47,6 @@ You can define the different phases of interviews and easily rate the applicant
|
|||
'fetchmail',
|
||||
],
|
||||
'data': [
|
||||
'wizard/hr_recruitment_employee_hired.xml',
|
||||
'wizard/hr_recruitment_create_partner_job_view.xml',
|
||||
'hr_recruitment_view.xml',
|
||||
'hr_recruitment_menu.xml',
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<field name="res_model">hr.applicant</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="domain">[('state','in',('draft','open'))]</field>
|
||||
<field name="domain">[('stage_id.fold', '!=', True)]</field>
|
||||
<field name="view_id" ref="view_applicants_status_tree"/>
|
||||
</record>
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
.. _changelog:
|
||||
|
||||
Changelog
|
||||
=========
|
||||
|
||||
`trunk (saas-2)`
|
||||
----------------
|
||||
|
||||
- Stage/state update
|
||||
|
||||
- ``hr.applicant``: removed inheritance from ``base_stage`` class and removed
|
||||
``state`` field. Added ``date_last_stage_update`` field holding last stage_id
|
||||
modification. Removed ``date`` field not used anywhere. Updated reports.
|
||||
- ``hr.recruitment.stage``: removed ``state`` field.
|
||||
|
||||
- Removed ``hired.employee`` wizard.
|
|
@ -0,0 +1,22 @@
|
|||
=====================
|
||||
HR Recruitment DevDoc
|
||||
=====================
|
||||
|
||||
Hr Recruitment module documentation
|
||||
===================================
|
||||
|
||||
Documentation topics
|
||||
''''''''''''''''''''
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
stage_status.rst
|
||||
|
||||
Changelog
|
||||
'''''''''
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
changelog.rst
|
|
@ -0,0 +1,54 @@
|
|||
.. _stage_status:
|
||||
|
||||
Stage and Status
|
||||
================
|
||||
|
||||
.. versionchanged:: 8.0 saas-2 state/stage cleaning
|
||||
|
||||
Stage
|
||||
+++++
|
||||
|
||||
This revision removed the concept of state on hr.applicant objects. The ``state``
|
||||
field has been totally removed and replaced by stages, using ``stage_id``. The
|
||||
following models are impacted:
|
||||
|
||||
- ``hr.applicant`` now use only stages. However a convention still exists about
|
||||
'New' stage. An applicant is consdered as ``new`` when it has the following
|
||||
properties:
|
||||
|
||||
- ``stage_id and stage_id.sequence = 1``
|
||||
|
||||
- ``hr.recruitment.stage`` do not have any ``state`` field anymore.
|
||||
- ``hr.recruitment.report`` do not have any ``state`` field anymore.
|
||||
|
||||
By default a newly created applicant be in a new stage. It means that it will
|
||||
fetch the stage having ``sequence = 1``. Stage mangement is done using the
|
||||
kanban view or the clikable statusbar. It is not done using buttons anymore.
|
||||
Employee creation is still feasible directly from a link button in the form view.
|
||||
|
||||
Stage analysis
|
||||
++++++++++++++
|
||||
|
||||
Stage analysis can be performed using the newly introduced ``date_last_stage_update``
|
||||
datetime field. This field is updated everytime ``stage_id`` is updated.
|
||||
|
||||
``hr.recruitment.report`` model also uses the ``date_last_stage_update`` field.
|
||||
This allows to group and analyse the time spend in the various stages.
|
||||
|
||||
Open / Assignation date
|
||||
+++++++++++++++++++++++
|
||||
|
||||
The ``date_open`` field meaning has been updated. It is now set when the ``user_id``
|
||||
(responsible) is set. It is therefore the assignation date.
|
||||
|
||||
Subtypes
|
||||
++++++++
|
||||
|
||||
The following subtypes are triggered on ``hr.applicant``:
|
||||
|
||||
- ``mt_applicant_new``: new applicants. Condition: ``obj.stage_id and obj.stage_id.sequence == 1``
|
||||
- ``mt_applicant_stage_changed``: stage changed. Condition: ``obj.stage_id and obj.stage_id.sequence != 1``
|
||||
|
||||
The following subtype is trigerred on ``hr.job``:
|
||||
|
||||
- ``mt_job_new_applicant``: new applicant in the job.
|
|
@ -19,23 +19,13 @@
|
|||
#
|
||||
##############################################################################
|
||||
|
||||
import time
|
||||
from openerp import tools
|
||||
|
||||
from openerp.addons.base_status.base_stage import base_stage
|
||||
from datetime import datetime
|
||||
from openerp.osv import fields, osv
|
||||
from openerp.tools.translate import _
|
||||
from openerp.tools import html2plaintext
|
||||
|
||||
AVAILABLE_STATES = [
|
||||
('draft', 'New'),
|
||||
('cancel', 'Refused'),
|
||||
('open', 'In Progress'),
|
||||
('pending', 'Pending'),
|
||||
('done', 'Hired')
|
||||
]
|
||||
|
||||
AVAILABLE_PRIORITIES = [
|
||||
('', ''),
|
||||
('5', 'Not Good'),
|
||||
|
@ -62,13 +52,11 @@ class hr_recruitment_stage(osv.osv):
|
|||
'name': fields.char('Name', size=64, required=True, translate=True),
|
||||
'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of stages."),
|
||||
'department_id':fields.many2one('hr.department', 'Specific to a Department', help="Stages of the recruitment process may be different per department. If this stage is common to all departments, keep this field empty."),
|
||||
'state': fields.selection(AVAILABLE_STATES, 'Status', required=True, help="The related status for the stage. The status of your document will automatically change according to the selected stage. Example, a stage is related to the status 'Close', when your document reach this stage, it will be automatically closed."),
|
||||
'fold': fields.boolean('Hide in views if empty', help="This stage is not visible, for example in status bar or kanban view, when there are no records in that stage to display."),
|
||||
'requirements': fields.text('Requirements'),
|
||||
}
|
||||
_defaults = {
|
||||
'sequence': 1,
|
||||
'state': 'draft',
|
||||
'fold': False,
|
||||
}
|
||||
|
||||
|
@ -87,18 +75,15 @@ class hr_recruitment_degree(osv.osv):
|
|||
('name_uniq', 'unique (name)', 'The name of the Degree of Recruitment must be unique!')
|
||||
]
|
||||
|
||||
class hr_applicant(base_stage, osv.Model):
|
||||
class hr_applicant(osv.Model):
|
||||
_name = "hr.applicant"
|
||||
_description = "Applicant"
|
||||
_order = "id desc"
|
||||
_inherit = ['mail.thread', 'ir.needaction_mixin']
|
||||
_track = {
|
||||
'state': {
|
||||
'hr_recruitment.mt_applicant_hired': lambda self, cr, uid, obj, ctx=None: obj.state == 'done',
|
||||
'hr_recruitment.mt_applicant_refused': lambda self, cr, uid, obj, ctx=None: obj.state == 'cancel',
|
||||
},
|
||||
'stage_id': {
|
||||
'hr_recruitment.mt_stage_changed': lambda self, cr, uid, obj, ctx=None: obj.state not in ['done', 'cancel'],
|
||||
'hr_recruitment.mt_applicant_new': lambda self, cr, uid, obj, ctx=None: obj.stage_id and obj.stage_id.sequence == 1,
|
||||
'hr_recruitment.mt_applicant_stage_changed': lambda self, cr, uid, obj, ctx=None: obj.stage_id and obj.stage_id.sequence != 1,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -109,7 +94,7 @@ class hr_applicant(base_stage, osv.Model):
|
|||
def _get_default_stage_id(self, cr, uid, context=None):
|
||||
""" Gives default stage_id """
|
||||
department_id = self._get_default_department_id(cr, uid, context=context)
|
||||
return self.stage_find(cr, uid, [], department_id, [('state', '=', 'draft')], context=context)
|
||||
return self.stage_find(cr, uid, [], department_id, [('sequence', '=', '1')], context=context)
|
||||
|
||||
def _resolve_department_id_from_context(self, cr, uid, context=None):
|
||||
""" Returns ID of department based on the value of 'default_department_id'
|
||||
|
@ -197,20 +182,12 @@ class hr_applicant(base_stage, osv.Model):
|
|||
'write_date': fields.datetime('Update Date', readonly=True),
|
||||
'stage_id': fields.many2one ('hr.recruitment.stage', 'Stage', track_visibility='onchange',
|
||||
domain="['|', ('department_id', '=', department_id), ('department_id', '=', False)]"),
|
||||
'state': fields.related('stage_id', 'state', type="selection", store=True,
|
||||
selection=AVAILABLE_STATES, string="Status", readonly=True,
|
||||
help='The status is set to \'Draft\', when a case is created.\
|
||||
If the case is in progress the status is set to \'Open\'.\
|
||||
When the case is over, the status is set to \'Done\'.\
|
||||
If the case needs to be reviewed then the status is \
|
||||
set to \'Pending\'.'),
|
||||
'categ_ids': fields.many2many('hr.applicant_category', string='Tags'),
|
||||
'company_id': fields.many2one('res.company', 'Company'),
|
||||
'user_id': fields.many2one('res.users', 'Responsible', track_visibility='onchange'),
|
||||
# Applicant Columns
|
||||
'date_closed': fields.datetime('Closed', readonly=True, select=True),
|
||||
'date_open': fields.datetime('Opened', readonly=True, select=True),
|
||||
'date': fields.datetime('Date'),
|
||||
'date_open': fields.datetime('Assigned', readonly=True, select=True),
|
||||
'date_last_stage_update': fields.datetime('Last Stage Update', select=True),
|
||||
'date_action': fields.date('Next Action Date'),
|
||||
'title_action': fields.char('Next Action', size=64),
|
||||
'priority': fields.selection(AVAILABLE_PRIORITIES, 'Appreciation'),
|
||||
|
@ -234,35 +211,35 @@ class hr_applicant(base_stage, osv.Model):
|
|||
'day_close': fields.function(_compute_day, string='Days to Close', \
|
||||
multi='day_close', type="float", store=True),
|
||||
'color': fields.integer('Color Index'),
|
||||
'emp_id': fields.many2one('hr.employee', 'employee'),
|
||||
'emp_id': fields.many2one('hr.employee', string='Employee',
|
||||
help='Employee linked to the applicant.'),
|
||||
'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True),
|
||||
}
|
||||
|
||||
_defaults = {
|
||||
'active': lambda *a: 1,
|
||||
'user_id': lambda s, cr, uid, c: uid,
|
||||
'email_from': lambda s, cr, uid, c: s._get_default_email(cr, uid, c),
|
||||
'stage_id': lambda s, cr, uid, c: s._get_default_stage_id(cr, uid, c),
|
||||
'department_id': lambda s, cr, uid, c: s._get_default_department_id(cr, uid, c),
|
||||
'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'hr.applicant', context=c),
|
||||
'color': 0,
|
||||
'date_last_stage_update': fields.datetime.now(),
|
||||
}
|
||||
|
||||
_group_by_full = {
|
||||
'stage_id': _read_group_stage_ids
|
||||
}
|
||||
|
||||
def onchange_job(self, cr, uid, ids, job, context=None):
|
||||
if job:
|
||||
job_record = self.pool.get('hr.job').browse(cr, uid, job, context=context)
|
||||
def onchange_job(self, cr, uid, ids, job_id=False, context=None):
|
||||
if job_id:
|
||||
job_record = self.pool.get('hr.job').browse(cr, uid, job_id, context=context)
|
||||
if job_record and job_record.department_id:
|
||||
return {'value': {'department_id': job_record.department_id.id}}
|
||||
return {}
|
||||
|
||||
def onchange_department_id(self, cr, uid, ids, department_id=False, context=None):
|
||||
obj_recru_stage = self.pool.get('hr.recruitment.stage')
|
||||
stage_ids = obj_recru_stage.search(cr, uid, ['|',('department_id','=',department_id),('department_id','=',False)], context=context)
|
||||
stage_id = stage_ids and stage_ids[0] or False
|
||||
def onchange_department_id(self, cr, uid, ids, department_id=False, stage_id=False, context=None):
|
||||
if not stage_id:
|
||||
stage_id = self.stage_find(cr, uid, [], department_id, [('sequence', '=', '1')], context=context)
|
||||
return {'value': {'stage_id': stage_id}}
|
||||
|
||||
def onchange_partner_id(self, cr, uid, ids, partner_id, context=None):
|
||||
|
@ -412,21 +389,20 @@ class hr_applicant(base_stage, osv.Model):
|
|||
self.pool.get('hr.job').message_post(cr, uid, [applicant.job_id.id], body=_('Applicant <b>created</b>'), subtype="hr_recruitment.mt_job_new_applicant", context=context)
|
||||
return obj_id
|
||||
|
||||
def case_open(self, cr, uid, ids, context=None):
|
||||
"""
|
||||
open Request of the applicant for the hr_recruitment
|
||||
"""
|
||||
res = super(hr_applicant, self).case_open(cr, uid, ids, context)
|
||||
date = self.read(cr, uid, ids, ['date_open'])[0]
|
||||
if not date['date_open']:
|
||||
self.write(cr, uid, ids, {'date_open': time.strftime('%Y-%m-%d %H:%M:%S'),})
|
||||
return res
|
||||
def write(self, cr, uid, ids, vals, context=None):
|
||||
if isinstance(ids, (int, long)):
|
||||
ids = [ids]
|
||||
# stage change: update date_last_stage_update
|
||||
if 'stage_id' in vals:
|
||||
vals['date_last_stage_update'] = fields.datetime.now()
|
||||
# user_id change: update date_start
|
||||
if vals.get('user_id'):
|
||||
vals['date_start'] = fields.datetime.now()
|
||||
|
||||
def case_close(self, cr, uid, ids, context=None):
|
||||
res = super(hr_applicant, self).case_close(cr, uid, ids, context)
|
||||
return res
|
||||
return super(hr_applicant, self).write(cr, uid, ids, vals, context=context)
|
||||
|
||||
def case_close_with_emp(self, cr, uid, ids, context=None):
|
||||
def create_employee_from_applicant(self, cr, uid, ids, context=None):
|
||||
""" Create an hr.employee from the hr.applicants """
|
||||
if context is None:
|
||||
context = {}
|
||||
hr_employee = self.pool.get('hr.employee')
|
||||
|
@ -436,17 +412,16 @@ class hr_applicant(base_stage, osv.Model):
|
|||
for applicant in self.browse(cr, uid, ids, context=context):
|
||||
address_id = contact_name = False
|
||||
if applicant.partner_id:
|
||||
address_id = self.pool.get('res.partner').address_get(cr,uid,[applicant.partner_id.id],['contact'])['contact']
|
||||
contact_name = self.pool.get('res.partner').name_get(cr,uid,[applicant.partner_id.id])[0][1]
|
||||
address_id = self.pool.get('res.partner').address_get(cr, uid, [applicant.partner_id.id], ['contact'])['contact']
|
||||
contact_name = self.pool.get('res.partner').name_get(cr, uid, [applicant.partner_id.id])[0][1]
|
||||
if applicant.job_id and (applicant.partner_name or contact_name):
|
||||
applicant.job_id.write({'no_of_recruitment': applicant.job_id.no_of_recruitment - 1})
|
||||
emp_id = hr_employee.create(cr,uid,{'name': applicant.partner_name or contact_name,
|
||||
emp_id = hr_employee.create(cr, uid, {'name': applicant.partner_name or contact_name,
|
||||
'job_id': applicant.job_id.id,
|
||||
'address_home_id': address_id,
|
||||
'department_id': applicant.department_id.id
|
||||
})
|
||||
self.write(cr, uid, [applicant.id], {'emp_id': emp_id}, context=context)
|
||||
self.case_close(cr, uid, [applicant.id], context)
|
||||
else:
|
||||
raise osv.except_osv(_('Warning!'), _('You must define an Applied Job and a Contact Name for this applicant.'))
|
||||
|
||||
|
@ -457,26 +432,6 @@ class hr_applicant(base_stage, osv.Model):
|
|||
dict_act_window['view_mode'] = 'form,tree'
|
||||
return dict_act_window
|
||||
|
||||
def case_cancel(self, cr, uid, ids, context=None):
|
||||
"""Overrides cancel for crm_case for setting probability
|
||||
"""
|
||||
res = super(hr_applicant, self).case_cancel(cr, uid, ids, context)
|
||||
self.write(cr, uid, ids, {'probability': 0.0})
|
||||
return res
|
||||
|
||||
def case_pending(self, cr, uid, ids, context=None):
|
||||
"""Marks case as pending"""
|
||||
res = super(hr_applicant, self).case_pending(cr, uid, ids, context)
|
||||
self.write(cr, uid, ids, {'probability': 0.0})
|
||||
return res
|
||||
|
||||
def case_reset(self, cr, uid, ids, context=None):
|
||||
"""Resets case as draft
|
||||
"""
|
||||
res = super(hr_applicant, self).case_reset(cr, uid, ids, context)
|
||||
self.write(cr, uid, ids, {'date_open': False, 'date_closed': False})
|
||||
return res
|
||||
|
||||
def set_priority(self, cr, uid, ids, priority, *args):
|
||||
"""Set applicant priority
|
||||
"""
|
||||
|
|
|
@ -51,34 +51,29 @@
|
|||
<field name="name">Doctoral Degree</field>
|
||||
<field name="sequence">4</field>
|
||||
</record>
|
||||
|
||||
<record model="hr.recruitment.stage" id="stage_job1">
|
||||
<field name="name">Initial Qualification</field>
|
||||
<field name="state">draft</field>
|
||||
<field name="sequence">1</field>
|
||||
</record>
|
||||
<record model="hr.recruitment.stage" id="stage_job2">
|
||||
<field name="name">First Interview</field>
|
||||
<field name="state">open</field>
|
||||
<field name="sequence">2</field>
|
||||
</record>
|
||||
<record model="hr.recruitment.stage" id="stage_job3">
|
||||
<field name="name">Second Interview</field>
|
||||
<field name="state">open</field>
|
||||
<field name="sequence">3</field>
|
||||
</record>
|
||||
<record model="hr.recruitment.stage" id="stage_job4">
|
||||
<field name="name">Contract Proposed</field>
|
||||
<field name="state">pending</field>
|
||||
<field name="sequence">4</field>
|
||||
</record>
|
||||
<record model="hr.recruitment.stage" id="stage_job5">
|
||||
<field name="name">Contract Signed</field>
|
||||
<field name="state">done</field>
|
||||
<field name="sequence">5</field>
|
||||
</record>
|
||||
<record model="hr.recruitment.stage" id="stage_job6">
|
||||
<field name="name">Refused</field>
|
||||
<field name="state">cancel</field>
|
||||
<field name="sequence">6</field>
|
||||
<field name="fold" eval="True"/>
|
||||
</record>
|
||||
|
@ -467,24 +462,24 @@
|
|||
<field name="res_model">hr.job</field>
|
||||
</record>
|
||||
<!-- Applicant-related subtypes for messaging / Chatter -->
|
||||
<record id="mt_stage_changed" model="mail.message.subtype">
|
||||
<record id="mt_applicant_new" model="mail.message.subtype">
|
||||
<field name="name">New Applicant</field>
|
||||
<field name="res_model">hr.applicant</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="description">Applicant created</field>
|
||||
</record>
|
||||
<record id="mt_applicant_stage_changed" model="mail.message.subtype">
|
||||
<field name="name">Stage Changed</field>
|
||||
<field name="res_model">hr.applicant</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="description">Stage changed</field>
|
||||
</record>
|
||||
<record id="mt_applicant_hired" model="mail.message.subtype">
|
||||
<record id="mt_applicant_employee" model="mail.message.subtype">
|
||||
<field name="name">Applicant Hired</field>
|
||||
<field name="res_model">hr.applicant</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="description">Applicant hired</field>
|
||||
</record>
|
||||
<record id="mt_applicant_refused" model="mail.message.subtype">
|
||||
<field name="name">Applicant Refused</field>
|
||||
<field name="res_model">hr.applicant</field>
|
||||
<field name="default" eval="False"/>
|
||||
<field name="description">Applicant refused</field>
|
||||
</record>
|
||||
<!-- Applicant Categories(Tag) -->
|
||||
<record id="tag_applicant_reserve" model="hr.applicant_category">
|
||||
<field name="name">Reserve</field>
|
||||
|
|
|
@ -39,9 +39,10 @@
|
|||
<field name="name">Applicants</field>
|
||||
<field name="model">hr.applicant</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Applicants" fonts="bold:message_unread==True" colors="grey:state in ('cancel','done');blue:state=='pending'">
|
||||
<tree string="Applicants" fonts="bold:message_unread==True">
|
||||
<field name="message_unread" invisible="1"/>
|
||||
<field name="create_date"/>
|
||||
<field name="date_last_stage_update" invisible="1"/>
|
||||
<field name="name" string="Subject"/>
|
||||
<field name="partner_name"/>
|
||||
<field name="email_from"/>
|
||||
|
@ -58,7 +59,6 @@
|
|||
<field name="availability" invisible="1"/>
|
||||
<field name="department_id" invisible="context.get('invisible_department', True)"/>
|
||||
<field name="user_id"/>
|
||||
<field name="state" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
@ -69,13 +69,7 @@
|
|||
<field name="arch" type="xml">
|
||||
<form string="Jobs - Recruitment Form" version="7.0">
|
||||
<header>
|
||||
<button name="case_close_with_emp" string="Hire & Create Employee" type="object"
|
||||
class="oe_highlight"
|
||||
attrs="{'invisible':['|', ('emp_id','!=',False), ('state','=','cancel')]}"/>
|
||||
<button name="case_cancel" string="Refuse" type="object"
|
||||
states="draft,open,pending" class="oe_highlight"/>
|
||||
<field name="stage_id" widget="statusbar" clickable="True"/>
|
||||
<field name="emp_id" invisible="1"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_right oe_button_box">
|
||||
|
@ -93,9 +87,12 @@
|
|||
<label for="name" class="oe_edit_only"/>
|
||||
<h1><field name="name"/></h1>
|
||||
<label for="partner_name" class="oe_edit_only"/>
|
||||
<h2>
|
||||
<h2 style="display: inline-block;">
|
||||
<field name="partner_name" class="oe_inline"/>
|
||||
</h2>
|
||||
<button string="Create Employee" name="create_employee_from_applicant" type="object"
|
||||
class="oe_link oe_inline" style="margin-left: 8px;"
|
||||
attrs="{'invisible': [('emp_id', '!=', False)]}"/>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
|
@ -115,7 +112,6 @@
|
|||
<field name="title_action" class="oe_inline" placeholder="e.g. Call for interview"/>
|
||||
</div>
|
||||
<field name="priority"/>
|
||||
<field name="state" invisible="1"/>
|
||||
<field name="source_id"/>
|
||||
<field name="reference"/>
|
||||
</group>
|
||||
|
@ -123,7 +119,7 @@
|
|||
<field name="survey" invisible="1"/>
|
||||
<field name="response" invisible="1"/>
|
||||
<field name="job_id" on_change="onchange_job(job_id)"/>
|
||||
<field name="department_id" on_change="onchange_department_id(department_id)"/>
|
||||
<field name="department_id" on_change="onchange_department_id(department_id, stage_id)"/>
|
||||
<label for="availability"/>
|
||||
<div>
|
||||
<field name="availability" class="oe_inline"/> <label string="Day(s)" class="oe_inline"/>
|
||||
|
@ -142,6 +138,7 @@
|
|||
<span class="oe_inline" attrs="{'invisible':[('salary_proposed_extra','=',False)]}"> + </span>
|
||||
<field name="salary_proposed_extra" class="oe_inline" placeholder="Extra advantages..."/>
|
||||
</div>
|
||||
<field name="emp_id" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
|
@ -165,7 +162,7 @@
|
|||
<graph string="Cases By Stage and Estimates" type="bar" orientation="vertical">
|
||||
<field name="job_id"/>
|
||||
<field name="salary_expected" operator="+"/>
|
||||
<field name="state" group="True"/>
|
||||
<field name="stage_id" group="True"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
@ -174,23 +171,22 @@
|
|||
<field name="name">Jobs - Recruitment Search</field>
|
||||
<field name="model">hr.applicant</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Jobs">
|
||||
<search string="Search Applicants">
|
||||
<field name="partner_name" filter_domain="['|','|',('name','ilike',self),('partner_name','ilike',self),('email_from','ilike',self)]" string="Subject / Applicant"/>
|
||||
<filter string="Unassigned" domain="[('user_id', '=', False)]"/>
|
||||
<filter string="My" domain="[('user_id', '=', uid)]"/>
|
||||
<separator/>
|
||||
<filter string="Unread Messages" name="message_unread" domain="[('message_unread','=',True)]"/>
|
||||
<separator/>
|
||||
<filter string="New" domain="[('state','=','draft')]" help="All Initial Jobs"/>
|
||||
<filter string="In Progress" domain="[('state','=','open')]" help="Open Jobs"/>
|
||||
<filter string="Pending" domain="[('state','=','pending')]" help="Pending Jobs"/>
|
||||
<separator/>
|
||||
<filter string="Unassigned Recruitments" domain="[('user_id','=',False)]" help="Unassigned Recruitments"/>
|
||||
<separator/>
|
||||
<filter string="Next Actions" context="{'invisible_next_action':False, 'invisible_next_date':False}"
|
||||
domain="[('date_action','<>',False)]" help="Filter and view on next actions and date"/>
|
||||
<field name="job_id"/>
|
||||
<field name="department_id"/>
|
||||
<field name="user_id"/>
|
||||
<field name="stage_id" domain="[]"/>
|
||||
<separator/>
|
||||
<field name="categ_ids"/>
|
||||
<separator/>
|
||||
<group expand="0" string="Group By...">
|
||||
<filter string="Responsible" domain="[]" context="{'group_by':'user_id'}"/>
|
||||
<filter string="Department" domain="[]" context="{'group_by':'department_id'}"/>
|
||||
|
@ -201,6 +197,7 @@
|
|||
<filter string="Stage" domain="[]" context="{'group_by':'stage_id'}"/>
|
||||
<filter string="Source" domain="[]" context="{'group_by':'source_id'}"/>
|
||||
<filter string="Creation Date" domain="[]" context="{'group_by':'create_date'}"/>
|
||||
<filter string="Last Stage Update" context="{'group_by':'date_last_stage_update'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
|
@ -343,7 +340,6 @@
|
|||
<field name="sequence" invisible="1"/>
|
||||
<field name="name"/>
|
||||
<field name="department_id"/>
|
||||
<field name="state"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
@ -361,7 +357,6 @@
|
|||
<field name="department_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="state"/>
|
||||
<field name="sequence"/>
|
||||
<field name="fold"/>
|
||||
</group>
|
||||
|
|
|
@ -18,30 +18,24 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from openerp import tools
|
||||
from openerp.osv import fields,osv
|
||||
from openerp.osv import fields, osv
|
||||
from .. import hr_recruitment
|
||||
from openerp.addons.decimal_precision import decimal_precision as dp
|
||||
|
||||
|
||||
AVAILABLE_STATES = [
|
||||
('draft','New'),
|
||||
('open','Open'),
|
||||
('cancel', 'Refused'),
|
||||
('done', 'Hired'),
|
||||
('pending','Pending')
|
||||
]
|
||||
|
||||
class hr_recruitment_report(osv.osv):
|
||||
class hr_recruitment_report(osv.Model):
|
||||
_name = "hr.recruitment.report"
|
||||
_description = "Recruitments Statistics"
|
||||
_auto = False
|
||||
_rec_name = 'date'
|
||||
_rec_name = 'date_create'
|
||||
_order = 'date_create desc'
|
||||
|
||||
_columns = {
|
||||
'user_id': fields.many2one('res.users', 'User', readonly=True),
|
||||
'nbr': fields.integer('# of Applications', readonly=True),
|
||||
'state': fields.selection(AVAILABLE_STATES, 'Status', size=16, readonly=True),
|
||||
# TDE TODO: use MONTHS
|
||||
'month':fields.selection([('01', 'January'), ('02', 'February'), \
|
||||
('03', 'March'), ('04', 'April'),\
|
||||
('05', 'May'), ('06', 'June'), \
|
||||
|
@ -51,7 +45,8 @@ class hr_recruitment_report(osv.osv):
|
|||
'company_id': fields.many2one('res.company', 'Company', readonly=True),
|
||||
'day': fields.char('Day', size=128, readonly=True),
|
||||
'year': fields.char('Year', size=4, readonly=True),
|
||||
'date': fields.date('Date', readonly=True),
|
||||
'date_create': fields.date('Create Date', readonly=True),
|
||||
'date_last_stage_update': fields.datetime('Last Stage Update', readonly=True),
|
||||
'date_closed': fields.date('Closed', readonly=True),
|
||||
'job_id': fields.many2one('hr.job', 'Applied Job',readonly=True),
|
||||
'stage_id': fields.many2one ('hr.recruitment.stage', 'Stage'),
|
||||
|
@ -67,19 +62,19 @@ class hr_recruitment_report(osv.osv):
|
|||
'delay_close': fields.float('Avg. Delay to Close', digits=(16,2), readonly=True, group_operator="avg",
|
||||
help="Number of Days to close the project issue"),
|
||||
}
|
||||
_order = 'date desc'
|
||||
|
||||
def init(self, cr):
|
||||
tools.drop_view_if_exists(cr, 'hr_recruitment_report')
|
||||
cr.execute("""
|
||||
create or replace view hr_recruitment_report as (
|
||||
select
|
||||
min(s.id) as id,
|
||||
date_trunc('day',s.create_date) as date,
|
||||
date_trunc('day',s.create_date) as date_create,
|
||||
date_trunc('day',s.date_closed) as date_closed,
|
||||
date_trunc('day',s.date_last_stage_update) as date_last_stage_update,
|
||||
to_char(s.create_date, 'YYYY') as year,
|
||||
to_char(s.create_date, 'MM') as month,
|
||||
to_char(s.create_date, 'YYYY-MM-DD') as day,
|
||||
s.state,
|
||||
s.partner_id,
|
||||
s.company_id,
|
||||
s.user_id,
|
||||
|
@ -93,7 +88,7 @@ class hr_recruitment_report(osv.osv):
|
|||
(sum(salary_proposed)/count(*)) as salary_prop_avg,
|
||||
sum(salary_expected) as salary_exp,
|
||||
(sum(salary_expected)/count(*)) as salary_exp_avg,
|
||||
extract('epoch' from (s.date_closed-s.create_date))/(3600*24) as delay_close,
|
||||
extract('epoch' from (s.date_closed-s.create_date))/(3600*24) as delay_close,
|
||||
count(*) as nbr
|
||||
from hr_applicant s
|
||||
group by
|
||||
|
@ -105,7 +100,7 @@ class hr_recruitment_report(osv.osv):
|
|||
s.date_open,
|
||||
s.create_date,
|
||||
s.date_closed,
|
||||
s.state,
|
||||
s.date_last_stage_update,
|
||||
s.partner_id,
|
||||
s.company_id,
|
||||
s.user_id,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<field name="model">hr.recruitment.report</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Recruitment Analysis" create="false">
|
||||
<field name="date" invisible="1"/>
|
||||
<field name="date_create" invisible="1"/>
|
||||
<field name="user_id" invisible="1"/>
|
||||
<field name="job_id"/>
|
||||
<field name="stage_id" invisible="1" />
|
||||
|
@ -14,10 +14,10 @@
|
|||
<field name="type_id" invisible="1"/>
|
||||
<field name="partner_id" invisible="1"/>
|
||||
<field name="company_id" groups="base.group_multi_company" invisible="1"/>
|
||||
<field name="state" invisible="1"/>
|
||||
<field name="year" invisible="1"/>
|
||||
<field name="day" invisible="1"/>
|
||||
<field name="month" invisible="1"/>
|
||||
<field name="date_last_stage_update" invisible="1"/>
|
||||
<field name="nbr" sum="# of Applications"/>
|
||||
<field name="available" sum="Available"/>
|
||||
<field name="salary_exp" sum="Expected Salary"/>
|
||||
|
@ -46,34 +46,33 @@
|
|||
<field name="model">hr.recruitment.report</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Recruitment Analysis">
|
||||
<filter string="New" icon="terp-document-new" domain="[('state','=','draft')]" help = "Draft recruitment"/>
|
||||
<filter string="In progress" icon="terp-camera_test" domain="[('state', '=' ,'open')]" help = "In progress recruitment"/>
|
||||
<filter string="Pending" icon="terp-gtk-media-pause" domain="[('state','=','pending')]" help = "Pending recruitment"/>
|
||||
<filter string="Hired" icon="terp-camera_test" domain="[('state','=','done')]" help = "Hired employees"/>
|
||||
<separator/>
|
||||
<filter icon="terp-personal" string="My Recruitment" help="My Recruitment" domain="[('user_id','=',uid)]"/>
|
||||
<field name="job_id"/>
|
||||
<field name="department_id"/>
|
||||
<field name="user_id" string="Responsible"/>
|
||||
<field name="user_id"/>
|
||||
<filter string="New" domain="[('sequence', '=', 1)]"/>
|
||||
<separator/>
|
||||
<filter string="Unassigned" domain="[('user_id', '=', False)]"/>
|
||||
<filter string="My" domain="[('user_id', '=', uid)]"/>
|
||||
<separator/>
|
||||
<group expand="0" string="Extended Filters...">
|
||||
<!--field name="job_id"/-->
|
||||
<field name="priority"/>
|
||||
<field name="stage_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<field name="date" string="Creation Date"/>
|
||||
<field name="date_create"/>
|
||||
<field name="date_closed"/>
|
||||
</group>
|
||||
<group expand="1" string="Group By ...">
|
||||
<filter string="Responsible" name='User' icon="terp-personal" domain="[]" context="{'group_by':'user_id'}"/>
|
||||
<filter string="Company" icon="terp-go-home" domain="[]" context="{'group_by':'company_id'}" groups="base.group_multi_company"/>
|
||||
<filter string="Partner" icon="terp-partner" domain="[]" context="{'group_by':'partner_id'}" />
|
||||
<filter string="Jobs" name="job" icon="terp-gtk-select-all" domain="[]" context="{'group_by':'job_id'}"/>
|
||||
<filter string="Department" name="department" icon="terp-personal+" domain="[]" context="{'group_by':'department_id'}"/>
|
||||
<filter string="Degree" name="degree" icon="terp-gtk-select-all" domain="[]" context="{'group_by':'type_id'}"/>
|
||||
<filter string="Stage" icon="terp-stage" domain="[]" context="{'group_by':'stage_id'}" />
|
||||
<filter string="Day" name = "day" icon="terp-go-today" domain="[]" context="{'group_by':'day'}" help="Creation Date"/>
|
||||
<filter string="Month" icon="terp-go-month" domain="[]" context="{'group_by':'month'}" help="Creation Date"/>
|
||||
<filter string="Year" icon="terp-go-year" domain="[]" context="{'group_by':'year'}" help="Creation Date"/>
|
||||
<filter string="Responsible" name='User' context="{'group_by':'user_id'}"/>
|
||||
<filter string="Company" context="{'group_by':'company_id'}" groups="base.group_multi_company"/>
|
||||
<filter string="Partner" context="{'group_by':'partner_id'}" />
|
||||
<filter string="Jobs" name="job" context="{'group_by':'job_id'}"/>
|
||||
<filter string="Department" name="department" context="{'group_by':'department_id'}"/>
|
||||
<filter string="Degree" name="degree" context="{'group_by':'type_id'}"/>
|
||||
<filter string="Stage" context="{'group_by':'stage_id'}" />
|
||||
<filter string="Last Stage Update" context="{'group_by':'date_last_stage_update'}" />
|
||||
<filter string="Day" name="day" context="{'group_by':'day'}" help="Creation Date"/>
|
||||
<filter string="Month" context="{'group_by':'month'}" help="Creation Date"/>
|
||||
<filter string="Year" context="{'group_by':'year'}" help="Creation Date"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
|
|
|
@ -17,33 +17,8 @@
|
|||
resume_ids = self.pool.get('ir.attachment').search(cr, uid, [('datas_fname','=','resume.pdf'),('res_model','=',self._name),('res_id','=',applicant.id)])
|
||||
assert applicant.name == "Application for the post of Jr.application Programmer.", "Applicant name does not match."
|
||||
assert applicant.stage_id.id == ref('hr_recruitment.stage_job1'), "Stage should be 'Initial qualification' and is '%s'." % (applicant.stage_id.name)
|
||||
assert applicant.state == "draft", "Applicant state should be 'draft'."
|
||||
assert applicant.stage_id.sequence == 1, "Applicant stage sequence should be 1."
|
||||
assert len(resume_ids), "Resume is not attached."
|
||||
-
|
||||
I refuse the applicant (hr_case_programmer)
|
||||
-
|
||||
!python {model: hr.applicant}: |
|
||||
self.case_cancel(cr, uid, [ref("hr_case_programmer")])
|
||||
-
|
||||
I check the details of the refused applicant.
|
||||
-
|
||||
!python {model: hr.applicant}: |
|
||||
applicant = self.browse(cr, uid, ref("hr_case_programmer"), context=context)
|
||||
assert applicant.stage_id.id == ref('hr_recruitment.stage_job6'), "Stage should be 'Refused' and is %s." % (applicant.stage_id.name)
|
||||
assert applicant.state == 'cancel', "Applicant is not in 'cancel' state."
|
||||
-
|
||||
I reset and re-open the previously refused applicant.
|
||||
-
|
||||
!python {model: hr.applicant}: |
|
||||
self.case_reset(cr, uid, [ref("hr_case_programmer")])
|
||||
self.case_open(cr, uid, [ref("hr_case_programmer")])
|
||||
-
|
||||
I check the details of the re-opened applicant.
|
||||
-
|
||||
!python {model: hr.applicant}: |
|
||||
applicant = self.browse(cr, uid, ref("hr_case_programmer"), context=context)
|
||||
assert applicant.stage_id.id == ref('hr_recruitment.stage_job2'), "Stage should be 'First interview' and is '%s'." % (applicant.stage_id.name)
|
||||
assert applicant.state == "open", "Applicant state should be 'open'."
|
||||
-
|
||||
I assign the Job position to the applicant
|
||||
-
|
||||
|
@ -54,21 +29,6 @@
|
|||
-
|
||||
!python {model: hr.applicant}: |
|
||||
self.action_makeMeeting(cr, uid, [ref('hr_case_programmer')])
|
||||
-
|
||||
I check Initial Qualification of applicant.
|
||||
-
|
||||
!python {model: hr.applicant}: |
|
||||
self.stage_next(cr, uid, [ref('hr_case_programmer')])
|
||||
-
|
||||
I schedule First Interview of applicant.
|
||||
-
|
||||
!python {model: hr.applicant}: |
|
||||
self.stage_next(cr, uid, [ref('hr_case_programmer')])
|
||||
-
|
||||
On a successful First Interview of applicant, I schedule Second Interview.
|
||||
-
|
||||
!python {model: hr.applicant}: |
|
||||
self.stage_next(cr, uid, [ref('hr_case_programmer')])
|
||||
-
|
||||
Applicant fillup the answer of the interview quetion.
|
||||
-
|
||||
|
@ -83,40 +43,8 @@
|
|||
I print Applicant fill up the interview quetion
|
||||
-
|
||||
!python {model: hr.applicant}: |
|
||||
self.action_print_survey(cr, uid, [ref('hr_case_programmer')])
|
||||
-
|
||||
On a successful Second Interview of applicant Contract is Proposed to applicant.
|
||||
-
|
||||
!python {model: hr.applicant}: |
|
||||
self.stage_next(cr, uid, [ref('hr_case_programmer')])
|
||||
self.stage_previous(cr, uid, [ref('hr_case_programmer')])
|
||||
-
|
||||
I Hired Applicant.
|
||||
-
|
||||
!python {model: hr.applicant}: |
|
||||
self.case_close_with_emp(cr, uid, [ref('hr_case_programmer')])
|
||||
-
|
||||
I check that applicant is "Hired".
|
||||
-
|
||||
!assert {model: hr.applicant, id: hr_case_programmer, string: Applicant state is done}:
|
||||
- state == 'done'
|
||||
-
|
||||
I do not give employment to the hired the applicant.
|
||||
-
|
||||
!python {model: hired.employee}: |
|
||||
context.update({'active_model': 'hr.applicant', 'active_ids': [ref("hr_recruitment.hr_case_programmer")], 'active_id': ref("hr_recruitment.hr_case_programmer")})
|
||||
emp_id = self.create(cr, uid, {}, context=context)
|
||||
self.case_close(cr, uid, [emp_id], context=context)
|
||||
-
|
||||
Now I give employment to hired applicant .
|
||||
-
|
||||
!python {model: hr.applicant}: |
|
||||
hired_emp_obj = self.pool.get('hired.employee')
|
||||
self.case_reset(cr, uid, [ref("hr_case_programmer")])
|
||||
self.case_open(cr, uid, [ref("hr_case_programmer")])
|
||||
context.update({'active_model': 'hr.applicant', 'active_ids': [ref("hr_recruitment.hr_case_programmer")], 'active_id': ref("hr_recruitment.hr_case_programmer")})
|
||||
emp_hr_id = hired_emp_obj.create(cr, uid, {}, context=context)
|
||||
hired_emp_obj.case_close_with_emp(cr, uid, [emp_hr_id], context=context)
|
||||
self.action_print_survey(cr, uid, [ref('hr_case_programmer')])
|
||||
-
|
||||
Now hired employee want to be a partner of company.
|
||||
-
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
|
||||
# Copyright (C) 2004-Today OpenERP (<http://www.openerp.com>).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
|
@ -19,9 +19,7 @@
|
|||
#
|
||||
##############################################################################
|
||||
|
||||
|
||||
import hr_recruitment_create_partner_job
|
||||
import hr_recruitment_employee_hired
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Management Solution
|
||||
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
|
||||
# $Id$
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
from openerp.osv import fields, osv
|
||||
from openerp.tools.translate import _
|
||||
|
||||
class hired_employee(osv.osv_memory):
|
||||
_name = 'hired.employee'
|
||||
_description = 'Create Employee'
|
||||
|
||||
def case_close(self, cr, uid, ids, context=None):
|
||||
"""
|
||||
@param self: The object pointer
|
||||
@param cr: the current row, from the database cursor,
|
||||
@param uid: the current user’s ID for security checks,
|
||||
@param ids: List of case's Ids
|
||||
@param *args: Give Tuple Value
|
||||
"""
|
||||
if context is None:
|
||||
context = {}
|
||||
self.pool.get('hr.applicant').case_close(cr, uid,context.get('active_ids',[]))
|
||||
return {}
|
||||
|
||||
def case_close_with_emp(self, cr, uid, ids, context=None):
|
||||
"""
|
||||
@param self: The object pointer
|
||||
@param cr: the current row, from the database cursor,
|
||||
@param uid: the current user’s ID for security checks,
|
||||
@param ids: List of case's Ids
|
||||
"""
|
||||
if context is None:
|
||||
context = {}
|
||||
return self.pool.get('hr.applicant').case_close_with_emp(cr, uid,context.get('active_ids', []))
|
||||
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -1,30 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="view_hr_recruitment_hired_employee" model="ir.ui.view">
|
||||
<field name="name">hr.recruitment.hired2employee.form</field>
|
||||
<field name="model">hired.employee</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Create New Employee" version="7.0">
|
||||
<header>
|
||||
<button name="case_close_with_emp" string="Yes" type="object" class="oe_highlight" />
|
||||
<button name="case_close" string="No" type="object"/>
|
||||
or
|
||||
<button string="Cancel" class="oe_link" special="cancel" />
|
||||
</header>
|
||||
<label string="Would you like to create an employee ?"/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_hr_recruitment_hired_employee" model="ir.actions.act_window">
|
||||
<field name="name">Create Employee</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">hired.employee</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
Loading…
Reference in New Issue