[IMP] hr_recruitment: stages are now job-related, not department-related
anymore. Also added default stages.
This commit is contained in:
committed by
Thibault Delavallée
parent
817fc0a8d7
commit
1701e7764f
@@ -36,6 +36,7 @@ AVAILABLE_PRIORITIES = [
|
||||
('3', 'Excellent')
|
||||
]
|
||||
|
||||
|
||||
class hr_recruitment_source(osv.osv):
|
||||
""" Sources of HR Recruitment """
|
||||
_name = "hr.recruitment.source"
|
||||
@@ -95,10 +96,14 @@ class hr_recruitment_stage(osv.osv):
|
||||
_name = "hr.recruitment.stage"
|
||||
_description = "Stage of Recruitment"
|
||||
_order = 'sequence'
|
||||
|
||||
_columns = {
|
||||
'name': fields.char('Name', required=True, translate=True),
|
||||
'name': fields.char('Stage Name', 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."),
|
||||
'case_default': fields.boolean(
|
||||
'Default for New Recruitment',
|
||||
help="If you check this field, this stage will be proposed by default on each new Recruitment. It will not assign this stage to existing Recruitments."),
|
||||
'job_ids': fields.many2many('hr.job', 'job_stage_rel', 'stage_id', 'job_id', 'Job Stages'),
|
||||
'requirements': fields.text('Requirements'),
|
||||
'template_id': fields.many2one('mail.template', 'Use template', help="If set, a message is posted on the applicant using the template when the applicant is set to the stage."),
|
||||
'legend_priority': fields.text(
|
||||
@@ -108,10 +113,13 @@ class hr_recruitment_stage(osv.osv):
|
||||
help='This stage is folded in the kanban view when'
|
||||
'there are no records in that stage to display.'),
|
||||
}
|
||||
|
||||
_defaults = {
|
||||
'sequence': 1,
|
||||
'job_ids': lambda self, cr, uid, context=None: [context['default_job_id']] if context and context.get('default_job_id') else None,
|
||||
}
|
||||
|
||||
|
||||
class hr_recruitment_degree(osv.osv):
|
||||
""" Degree of HR Recruitment """
|
||||
_name = "hr.recruitment.degree"
|
||||
@@ -127,6 +135,7 @@ class hr_recruitment_degree(osv.osv):
|
||||
('name_uniq', 'unique (name)', 'The name of the Degree of Recruitment must be unique!')
|
||||
]
|
||||
|
||||
|
||||
class hr_applicant(osv.Model):
|
||||
_name = "hr.applicant"
|
||||
_description = "Applicant"
|
||||
@@ -134,30 +143,9 @@ class hr_applicant(osv.Model):
|
||||
_inherit = ['mail.thread', 'ir.needaction_mixin', 'utm.mixin']
|
||||
_mail_mass_mailing = _('Applicants')
|
||||
|
||||
def _get_default_department_id(self, cr, uid, context=None):
|
||||
""" Gives default department by checking if present in the context """
|
||||
return (self._resolve_department_id_from_context(cr, uid, context=context) or False)
|
||||
|
||||
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, [('fold', '=', False)], 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'
|
||||
context key, or None if it cannot be resolved to a single
|
||||
department.
|
||||
"""
|
||||
if context is None:
|
||||
context = {}
|
||||
if type(context.get('default_department_id')) in (int, long):
|
||||
return context.get('default_department_id')
|
||||
if isinstance(context.get('default_department_id'), basestring):
|
||||
department_name = context['default_department_id']
|
||||
department_ids = self.pool.get('hr.department').name_search(cr, uid, name=department_name, context=context)
|
||||
if len(department_ids) == 1:
|
||||
return int(department_ids[0][0])
|
||||
return None
|
||||
job_id = context and context.get('default_job_id')
|
||||
return self.stage_find(cr, uid, [], job_id, [('fold', '=', False)], context=context)
|
||||
|
||||
def _get_default_company_id(self, cr, uid, department_id=None, context=None):
|
||||
company_id = False
|
||||
@@ -175,15 +163,14 @@ class hr_applicant(osv.Model):
|
||||
# lame hack to allow reverting search, should just work in the trivial case
|
||||
if read_group_order == 'stage_id desc':
|
||||
order = "%s desc" % order
|
||||
# retrieve section_id from the context and write the domain
|
||||
# - ('id', 'in', 'ids'): add columns that should be present
|
||||
# - OR ('department_id', '=', False), ('fold', '=', False): add default columns that are not folded
|
||||
# - OR ('department_id', 'in', department_id), ('fold', '=', False) if department_id: add department columns that are not folded
|
||||
department_id = self._resolve_department_id_from_context(cr, uid, context=context)
|
||||
# retrieve job_id from the context and write the domain: ids + contextual columns (job or default)
|
||||
job_id = context and context.get('default_job_id')
|
||||
search_domain = []
|
||||
if department_id:
|
||||
search_domain += ['|', ('department_id', '=', department_id)]
|
||||
search_domain += ['|', ('id', 'in', ids), ('department_id', '=', False)]
|
||||
if job_id:
|
||||
search_domain += ['|', ('job_ids', '=', job_id)]
|
||||
else:
|
||||
search_domain += ['|', ('case_default', '=', True)]
|
||||
search_domain += [('id', 'in', ids)]
|
||||
stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context)
|
||||
result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context)
|
||||
# restore order of the search
|
||||
@@ -233,8 +220,9 @@ class hr_applicant(osv.Model):
|
||||
'partner_id': fields.many2one('res.partner', 'Contact'),
|
||||
'create_date': fields.datetime('Creation Date', readonly=True, select=True),
|
||||
'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)]"),
|
||||
'stage_id': fields.many2one(
|
||||
'hr.recruitment.stage', 'Stage', track_visibility='onchange',
|
||||
domain="['|', ('case_default', '=', True), ('job_ids', '=', job_id)]", copy=False, select=1),
|
||||
'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'),
|
||||
@@ -276,9 +264,8 @@ class hr_applicant(osv.Model):
|
||||
_defaults = {
|
||||
'active': lambda *a: 1,
|
||||
'user_id': lambda s, cr, uid, c: uid,
|
||||
'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._get_default_company_id(cr, uid, s._get_default_department_id(cr, uid, c), c),
|
||||
'stage_id': _get_default_stage_id,
|
||||
'company_id': lambda s, cr, uid, c: s._get_default_company_id(cr, uid, c and c.get('default_department_id'), c),
|
||||
'color': 0,
|
||||
'priority': '0',
|
||||
'date_last_stage_update': fields.datetime.now,
|
||||
@@ -296,23 +283,16 @@ class hr_applicant(osv.Model):
|
||||
action['res_id'] = applicant and applicant.emp_id.id or False
|
||||
return action
|
||||
|
||||
def onchange_job(self, cr, uid, ids, job_id=False, context=None):
|
||||
def onchange_job(self, cr, uid, ids, job_id=False, stage_id=False, context=None):
|
||||
department_id = False
|
||||
user_id = False
|
||||
if job_id:
|
||||
job_record = self.pool.get('hr.job').browse(cr, uid, job_id, context=context)
|
||||
department_id = job_record and job_record.department_id and job_record.department_id.id or False
|
||||
user_id = job_record and job_record.user_id and job_record.user_id.id or False
|
||||
return {'value': {'department_id': department_id, 'user_id': user_id}}
|
||||
|
||||
def onchange_department_id(self, cr, uid, ids, department_id=False, stage_id=False, context=None):
|
||||
values = {}
|
||||
if not stage_id:
|
||||
values['stage_id'] = self.stage_find(cr, uid, [], department_id, [('fold', '=', False)], context=context)
|
||||
if department_id:
|
||||
department = self.pool['hr.department'].browse(cr, uid, department_id, context=context)
|
||||
values['company_id'] = department.company_id.id
|
||||
return {'value': values}
|
||||
if not stage_id and job_id:
|
||||
stage_id = self.stage_find(cr, uid, [], job_id, [('fold', '=', False)], context=context)
|
||||
return {'value': {'department_id': department_id, 'user_id': user_id, 'stage_id': stage_id}}
|
||||
|
||||
def onchange_partner_id(self, cr, uid, ids, partner_id, context=None):
|
||||
data = {'partner_phone': False,
|
||||
@@ -333,30 +313,26 @@ class hr_applicant(osv.Model):
|
||||
return {'value': {'date_closed': fields.datetime.now()}}
|
||||
return {'value': {'date_closed': False}}
|
||||
|
||||
def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None):
|
||||
def stage_find(self, cr, uid, applicants, job_id, domain=[], order='sequence', context=None):
|
||||
""" Override of the base.stage method
|
||||
Parameter of the stage search taken from the lead:
|
||||
- department_id: if set, stages must belong to this section or
|
||||
- job_id: if set, stages must belong to this section or
|
||||
be a default case
|
||||
"""
|
||||
if isinstance(cases, (int, long)):
|
||||
cases = self.browse(cr, uid, cases, context=context)
|
||||
# collect all section_ids
|
||||
department_ids = []
|
||||
if section_id:
|
||||
department_ids.append(section_id)
|
||||
for case in cases:
|
||||
if case.department_id:
|
||||
department_ids.append(case.department_id.id)
|
||||
# OR all section_ids and OR with case_default
|
||||
search_domain = []
|
||||
if department_ids:
|
||||
search_domain += ['|', ('department_id', 'in', department_ids)]
|
||||
search_domain.append(('department_id', '=', False))
|
||||
if isinstance(applicants, (int, long)):
|
||||
applicants = self.browse(cr, uid, applicants, context=context)
|
||||
job_ids = set([applicant.job_id.id for applicant in applicants if applicant.job_id])
|
||||
if job_id:
|
||||
job_ids.add(job_id)
|
||||
# OR all job_id and OR with applicant_default
|
||||
if job_ids:
|
||||
search_domain = ['|', ('case_default', '=', True), ('job_ids', 'in', list(job_ids))]
|
||||
else:
|
||||
search_domain = [('case_default', '=', True)]
|
||||
# AND with the domain in parameter
|
||||
search_domain += list(domain)
|
||||
# perform search, return the first found
|
||||
stage_ids = self.pool.get('hr.recruitment.stage').search(cr, uid, search_domain, order=order, context=context)
|
||||
stage_ids = self.pool.get('hr.recruitment.stage').search(cr, uid, search_domain, order=order, limit=1, context=context)
|
||||
if stage_ids:
|
||||
return stage_ids[0]
|
||||
return False
|
||||
@@ -605,15 +581,12 @@ class hr_job(osv.osv):
|
||||
_name = "hr.job"
|
||||
_inherits = {'mail.alias': 'alias_id'}
|
||||
|
||||
|
||||
|
||||
def _track_subtype(self, cr, uid, ids, init_values, context=None):
|
||||
record = self.browse(cr, uid, ids[0], context=context)
|
||||
if 'state' in init_values and record.state == 'open':
|
||||
return 'hr_recruitment.mt_job_new'
|
||||
return super(hr_job, self)._track_subtype(cr, uid, ids, init_values, context=context)
|
||||
|
||||
|
||||
def _get_attached_docs(self, cr, uid, ids, field_name, arg, context=None):
|
||||
res = {}
|
||||
attachment_obj = self.pool.get('ir.attachment')
|
||||
@@ -646,6 +619,7 @@ class hr_job(osv.osv):
|
||||
'application_ids': fields.one2many('hr.applicant', 'job_id', 'Applications'),
|
||||
'application_count': fields.function(_count_all, type='integer', string='Applications', multi=True),
|
||||
'manager_id': fields.related('department_id', 'manager_id', type='many2one', string='Department Manager', relation='hr.employee', readonly=True, store=True),
|
||||
'stage_ids': fields.many2many('hr.recruitment.stage', 'job_stage_rel', 'job_id', 'stage_id', 'Job Stages'),
|
||||
'document_ids': fields.function(_get_attached_docs, type='one2many', relation='ir.attachment', string='Applications'),
|
||||
'documents_count': fields.function(_count_all, type='integer', string='Documents', multi=True),
|
||||
'user_id': fields.many2one('res.users', 'Recruitment Responsible', track_visibility='onchange'),
|
||||
@@ -656,8 +630,12 @@ class hr_job(osv.osv):
|
||||
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
|
||||
return user.company_id.partner_id.id
|
||||
|
||||
def _get_type_common(self, cr, uid, context=None):
|
||||
return self.pool['hr.recruitment.stage'].search(cr, uid, [('case_default', '=', True)], context=context)
|
||||
|
||||
_defaults = {
|
||||
'address_id': _address_get
|
||||
'address_id': _address_get,
|
||||
'stage_ids': _get_type_common,
|
||||
}
|
||||
|
||||
def _auto_init(self, cr, context=None):
|
||||
|
||||
@@ -93,30 +93,36 @@
|
||||
<record model="hr.recruitment.stage" id="stage_job1">
|
||||
<field name="name">Initial Qualification</field>
|
||||
<field name="sequence">1</field>
|
||||
<field name="case_default" eval="True"/>
|
||||
</record>
|
||||
<record model="hr.recruitment.stage" id="stage_job2">
|
||||
<field name="name">First Interview</field>
|
||||
<field name="template_id" ref="applicant_interest"/>
|
||||
<field name="sequence">2</field>
|
||||
<field name="case_default" eval="True"/>
|
||||
</record>
|
||||
<record model="hr.recruitment.stage" id="stage_job3">
|
||||
<field name="name">Second Interview</field>
|
||||
<field name="sequence">3</field>
|
||||
<field name="case_default" eval="True"/>
|
||||
</record>
|
||||
<record model="hr.recruitment.stage" id="stage_job4">
|
||||
<field name="name">Contract Proposed</field>
|
||||
<field name="name">Contract Proposal</field>
|
||||
<field name="sequence">4</field>
|
||||
<field name="case_default" eval="True"/>
|
||||
</record>
|
||||
<record model="hr.recruitment.stage" id="stage_job5">
|
||||
<field name="name">Contract Signed</field>
|
||||
<field name="sequence">5</field>
|
||||
<field name="fold" eval="True"/>
|
||||
<field name="case_default" eval="True"/>
|
||||
</record>
|
||||
<record model="hr.recruitment.stage" id="stage_job6">
|
||||
<field name="name">Refused</field>
|
||||
<field name="sequence">6</field>
|
||||
<field name="template_id" ref="applicant_refuse"/>
|
||||
<field name="fold" eval="True"/>
|
||||
<field name="case_default" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="mail_alias_jobs" model="mail.alias">
|
||||
|
||||
@@ -127,34 +127,41 @@
|
||||
<field name="state">recruit</field>
|
||||
<field name="no_of_recruitment">4</field>
|
||||
<field name="survey_id" ref="recruitment_form"/>
|
||||
<field name="stage_ids" eval="[(6, 0, [ref('stage_job1'), ref('stage_job2'), ref('stage_job3'), ref('stage_job4'), ref('stage_job5'), ref('stage_job6')])]"/>
|
||||
</record>
|
||||
<record id="hr.job_ceo" model="hr.job">
|
||||
<field name="survey_id" ref="recruitment_form"/>
|
||||
<field name="stage_ids" eval="[(6, 0, [ref('stage_job1'), ref('stage_job2'), ref('stage_job3'), ref('stage_job4'), ref('stage_job5'), ref('stage_job6')])]"/>
|
||||
</record>
|
||||
<record id="hr.job_cto" model="hr.job">
|
||||
<field name="survey_id" ref="recruitment_form"/>
|
||||
<field name="stage_ids" eval="[(6, 0, [ref('stage_job1'), ref('stage_job2'), ref('stage_job3'), ref('stage_job4'), ref('stage_job5'), ref('stage_job6')])]"/>
|
||||
</record>
|
||||
<record id="hr.job_consultant" model="hr.job">
|
||||
<field name="state">recruit</field>
|
||||
<field name="no_of_recruitment">1</field>
|
||||
<field name="survey_id" ref="recruitment_form"/>
|
||||
<field name="stage_ids" eval="[(6, 0, [ref('stage_job1'), ref('stage_job2'), ref('stage_job3'), ref('stage_job4'), ref('stage_job5'), ref('stage_job6')])]"/>
|
||||
</record>
|
||||
<record id="hr.job_hrm" model="hr.job">
|
||||
<field name="no_of_recruitment">1</field>
|
||||
<field name="state">recruit</field>
|
||||
<field name="survey_id" ref="recruitment_form"/>
|
||||
<field name="stage_ids" eval="[(6, 0, [ref('stage_job1'), ref('stage_job2'), ref('stage_job3'), ref('stage_job4'), ref('stage_job5'), ref('stage_job6')])]"/>
|
||||
</record>
|
||||
<record id="hr.job_marketing" model="hr.job">
|
||||
<field name="state">recruit</field>
|
||||
<field name="no_of_recruitment">3</field>
|
||||
<field name="survey_id" ref="recruitment_form"/>
|
||||
<field name="stage_ids" eval="[(6, 0, [ref('stage_job1'), ref('stage_job2'), ref('stage_job3'), ref('stage_job4'), ref('stage_job5'), ref('stage_job6')])]"/>
|
||||
</record>
|
||||
<record id="hr.job_trainee" model="hr.job">
|
||||
<field name="state">recruit</field>
|
||||
<field name="no_of_recruitment">6</field>
|
||||
<field name="survey_id" ref="recruitment_form"/>
|
||||
<field name="stage_ids" eval="[(6, 0, [ref('stage_job1'), ref('stage_job2'), ref('stage_job3'), ref('stage_job4'), ref('stage_job5'), ref('stage_job6')])]"/>
|
||||
</record>
|
||||
-->
|
||||
|
||||
<record id="message_application_demo" model="mail.message">
|
||||
<field name="model">hr.applicant</field>
|
||||
<field name="res_id" ref="hr_case_advertisement"/>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Jobs - Recruitment Form">
|
||||
<header>
|
||||
<field name="stage_id" widget="statusbar" clickable="True"/>
|
||||
<field name="stage_id" widget="statusbar" clickable="True" options="{'fold_field': 'fold'}"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_right oe_button_box">
|
||||
@@ -135,8 +135,8 @@
|
||||
<group string="Job">
|
||||
<field name="survey" invisible="1"/>
|
||||
<field name="response_id" invisible="1"/>
|
||||
<field name="job_id" on_change="onchange_job(job_id)"/>
|
||||
<field name="department_id" on_change="onchange_department_id(department_id, stage_id)" groups="hr.group_multi_departments"/>
|
||||
<field name="job_id" on_change="onchange_job(job_id,stage_id)"/>
|
||||
<field name="department_id"/>
|
||||
<field name="company_id" />
|
||||
<label for="availability"/>
|
||||
<div>
|
||||
@@ -391,6 +391,13 @@
|
||||
<field name="address_id" context="{'show_address': 1}" domain= "[('is_company', '=', True )]"/>
|
||||
</div>
|
||||
</group>
|
||||
<group name="employee_data" position="after">
|
||||
<notebook>
|
||||
<page string="Recruitment Stage" name="recruitment_stage">
|
||||
<field name="stage_ids" widget="many2many"></field>
|
||||
</page>
|
||||
</notebook>
|
||||
</group>
|
||||
<xpath expr="//field[@name='department_id']" position="after">
|
||||
<label for="alias_name" string="Specific Email Address" attrs="{'invisible': [('alias_domain', '=', False)]}" help ="Define a specific contact address for this job position. If you keep it empty, the default email address will be used which is in human resources settings"/>
|
||||
<div name="alias_def" attrs="{'invisible': [('alias_domain', '=', False)]}">
|
||||
@@ -494,7 +501,9 @@
|
||||
<div class="col-md-6 oe_applications">
|
||||
<div class="oe_applications oe_job_font">
|
||||
<a name="%(action_hr_job_applications)d" type="action">
|
||||
<span><t t-esc="record.application_ids.raw_value.length"/> Applications</span>
|
||||
<span t-if="record.application_ids.raw_value.length == 0"><t t-esc="record.application_ids.raw_value.length"/> Application</span>
|
||||
<span t-if="record.application_ids.raw_value.length == 1"><t t-esc="record.application_ids.raw_value.length"/> Application</span>
|
||||
<span t-if="record.application_ids.raw_value.length gt 1"><t t-esc="record.application_ids.raw_value.length"/> Applications</span>
|
||||
</a>
|
||||
<br/>
|
||||
<a t-if="record.document_ids.raw_value.length gt 0" name="action_get_attachment_tree_view" type="object">
|
||||
@@ -563,9 +572,8 @@
|
||||
<field name="model">hr.recruitment.stage</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Stages">
|
||||
<field name="sequence" invisible="1"/>
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="department_id" groups="hr.group_multi_departments"/>
|
||||
<field name="fold"/>
|
||||
</tree>
|
||||
</field>
|
||||
@@ -580,12 +588,12 @@
|
||||
<group string="Stage Definition">
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="department_id" groups="hr.group_multi_departments"/>
|
||||
<field name="sequence"/>
|
||||
<field name="template_id" domain= "[('model_id.model', '=', 'hr.applicant')]"/>
|
||||
<field name="case_default"/>
|
||||
<field name="fold"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="fold"/>
|
||||
<field name="template_id" domain= "[('model_id.model', '=', 'hr.applicant')]"/>
|
||||
<field name="legend_priority"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
@@ -111,7 +111,7 @@ class website_hr_recruitment(http.Controller):
|
||||
}
|
||||
for f in ['email_from', 'partner_name', 'description']:
|
||||
value[f] = post.get(f)
|
||||
for f in ['department_id', 'job_id']:
|
||||
for f in ['department_id', 'job_id', 'stage_id']:
|
||||
value[f] = int(post.get(f) or 0)
|
||||
# Retro-compatibility for saas-3. "phone" field should be replace by "partner_phone" in the template in trunk.
|
||||
value['partner_phone'] = post.pop('phone', False)
|
||||
|
||||
@@ -155,6 +155,7 @@
|
||||
<form class="form-horizontal mt32" action="/jobs/thankyou" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" t-att-value="job and job.department_id.id or False" name="department_id"/>
|
||||
<input type="hidden" t-att-value="job and job.id or False" name="job_id"/>
|
||||
<input type="hidden" t-att-value="job and job.stage_ids and job.stage_ids[0].id or False" name="stage_id"/>
|
||||
<div t-attf-class="form-group #{error.get('partner_name') and 'has-error' or ''}">
|
||||
<label class="col-md-3 col-sm-4 control-label" for="partner_name">Your Name</label>
|
||||
<div class="col-md-7 col-sm-8">
|
||||
|
||||
Reference in New Issue
Block a user