[IMP] hr_contract: Replace some contract states by kanban status

Purpose
=======

Having some usability check to be sure to make the pay
for everyone, and don't loose contracts.

We would like to remove incoming and to renew columns in
hr.contract and replace them by a kanban state.  In employee, we would
like to add warning on the kanban to warn the hr responsible there
is no contract or a contract is about to expire

Specification
=============

hr.contract kanban:

- add a kanabn state
- it becomes red automatically when the contract is in running and  expire in 7 days
- add aquick search in filter to filter on expered soon
- the kanban state can be changed manaually (in case of there is several rh)
- sort by contract end date set first (more close of today first)

Employee kanban

- add an red icon if there is no contract in Runnig status OR there is a
kanban state red in their contract (whatever the status).
- add a quick seaach on employee with warning

- State pending + green = incoming
- State confirmed + red = about to expire / to renew

closes odoo/odoo#33489

Signed-off-by: Yannick Tivisse (yti) <yti@odoo.com>
This commit is contained in:
Kevin Baptiste
2019-06-28 06:59:32 +00:00
committed by Yannick Tivisse
parent 5548029fb7
commit f17655dd77
9 changed files with 183 additions and 65 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ You can assign several contracts per employee.
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'data/hr_contract_data.xml', 'data/hr_contract_data.xml',
'views/hr_contract_views.xml', 'views/hr_contract_views.xml',
'views/hr_contract_templates.xml', 'views/assets.xml',
], ],
'demo': ['data/hr_contract_demo.xml'], 'demo': ['data/hr_contract_demo.xml'],
'installable': True, 'installable': True,
+14 -1
View File
@@ -11,6 +11,7 @@
eval="obj().env.ref('hr.employee_admin').department_id.id"/> eval="obj().env.ref('hr.employee_admin').department_id.id"/>
<field eval="7540.0" name="wage"/> <field eval="7540.0" name="wage"/>
<field name="state">draft</field> <field name="state">draft</field>
<field name="kanban_state">normal</field>
</record> </record>
<record id="hr_contract_al" model="hr.contract"> <record id="hr_contract_al" model="hr.contract">
@@ -23,6 +24,7 @@
eval="obj().env.ref('hr.employee_al').department_id.id"/> eval="obj().env.ref('hr.employee_al').department_id.id"/>
<field eval="4000.0" name="wage"/> <field eval="4000.0" name="wage"/>
<field name="state">open</field> <field name="state">open</field>
<field name="kanban_state">normal</field>
</record> </record>
<record id="hr_contract_mit" model="hr.contract"> <record id="hr_contract_mit" model="hr.contract">
@@ -35,6 +37,7 @@
eval="obj().env.ref('hr.employee_mit').department_id.id"/> eval="obj().env.ref('hr.employee_mit').department_id.id"/>
<field eval="4500.0" name="wage"/> <field eval="4500.0" name="wage"/>
<field name="state">open</field> <field name="state">open</field>
<field name="kanban_state">normal</field>
</record> </record>
<record id="hr_contract_stw" model="hr.contract"> <record id="hr_contract_stw" model="hr.contract">
@@ -48,6 +51,7 @@
eval="obj().env.ref('hr.employee_stw').department_id.id"/> eval="obj().env.ref('hr.employee_stw').department_id.id"/>
<field eval="4500.0" name="wage"/> <field eval="4500.0" name="wage"/>
<field name="state">open</field> <field name="state">open</field>
<field name="kanban_state">normal</field>
</record> </record>
<record id="hr_contract_qdp" model="hr.contract"> <record id="hr_contract_qdp" model="hr.contract">
@@ -60,6 +64,7 @@
eval="obj().env.ref('hr.employee_qdp').department_id.id"/> eval="obj().env.ref('hr.employee_qdp').department_id.id"/>
<field eval="3750.0" name="wage"/> <field eval="3750.0" name="wage"/>
<field name="state">draft</field> <field name="state">draft</field>
<field name="kanban_state">normal</field>
</record> </record>
<record id="hr_contract_han" model="hr.contract"> <record id="hr_contract_han" model="hr.contract">
@@ -72,6 +77,7 @@
eval="obj().env.ref('hr.employee_han').department_id.id"/> eval="obj().env.ref('hr.employee_han').department_id.id"/>
<field eval="4600.0" name="wage"/> <field eval="4600.0" name="wage"/>
<field name="state">open</field> <field name="state">open</field>
<field name="kanban_state">normal</field>
</record> </record>
<record id="hr_contract_niv" model="hr.contract"> <record id="hr_contract_niv" model="hr.contract">
@@ -85,6 +91,7 @@
eval="obj().env.ref('hr.employee_niv').department_id.id"/> eval="obj().env.ref('hr.employee_niv').department_id.id"/>
<field eval="4000.0" name="wage"/> <field eval="4000.0" name="wage"/>
<field name="state">draft</field> <field name="state">draft</field>
<field name="kanban_state">normal</field>
</record> </record>
<record id="hr_contract_jth" model="hr.contract"> <record id="hr_contract_jth" model="hr.contract">
@@ -98,6 +105,7 @@
eval="obj().env.ref('hr.employee_jth').department_id.id"/> eval="obj().env.ref('hr.employee_jth').department_id.id"/>
<field eval="4200.0" name="wage"/> <field eval="4200.0" name="wage"/>
<field name="state">draft</field> <field name="state">draft</field>
<field name="kanban_state">normal</field>
</record> </record>
<record id="hr_contract_chs" model="hr.contract"> <record id="hr_contract_chs" model="hr.contract">
@@ -110,6 +118,7 @@
eval="obj().env.ref('hr.employee_chs').department_id.id"/> eval="obj().env.ref('hr.employee_chs').department_id.id"/>
<field eval="3750.0" name="wage"/> <field eval="3750.0" name="wage"/>
<field name="state">cancel</field> <field name="state">cancel</field>
<field name="kanban_state">normal</field>
</record> </record>
<record id="hr_contract_jve" model="hr.contract"> <record id="hr_contract_jve" model="hr.contract">
@@ -122,6 +131,7 @@
eval="obj().env.ref('hr.employee_jve').department_id.id"/> eval="obj().env.ref('hr.employee_jve').department_id.id"/>
<field eval="3950.0" name="wage"/> <field eval="3950.0" name="wage"/>
<field name="state">cancel</field> <field name="state">cancel</field>
<field name="kanban_state">normal</field>
</record> </record>
<record id="hr_contract_fme" model="hr.contract"> <record id="hr_contract_fme" model="hr.contract">
@@ -134,7 +144,8 @@
<field name="department_id" model="hr.department" <field name="department_id" model="hr.department"
eval="obj().env.ref('hr.employee_fme').department_id.id"/> eval="obj().env.ref('hr.employee_fme').department_id.id"/>
<field eval="3650.0" name="wage"/> <field eval="3650.0" name="wage"/>
<field name="state">pending</field> <field name="state">open</field>
<field name="kanban_state">blocked</field>
</record> </record>
<record id="hr_contract_fpi" model="hr.contract"> <record id="hr_contract_fpi" model="hr.contract">
@@ -148,6 +159,7 @@
eval="obj().env.ref('hr.employee_fpi').department_id.id"/> eval="obj().env.ref('hr.employee_fpi').department_id.id"/>
<field eval="3750.0" name="wage"/> <field eval="3750.0" name="wage"/>
<field name="state">close</field> <field name="state">close</field>
<field name="kanban_state">normal</field>
</record> </record>
<record id="hr_contract_vad" model="hr.contract"> <record id="hr_contract_vad" model="hr.contract">
@@ -161,6 +173,7 @@
eval="obj().env.ref('hr.employee_vad').department_id.id"/> eval="obj().env.ref('hr.employee_vad').department_id.id"/>
<field eval="3750.0" name="wage"/> <field eval="3750.0" name="wage"/>
<field name="state">close</field> <field name="state">close</field>
<field name="kanban_state">normal</field>
</record> </record>
</odoo> </odoo>
+49 -28
View File
@@ -6,8 +6,8 @@ from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _ from odoo import api, fields, models, _
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
from odoo.osv import expression
from odoo.osv import expression
class Contract(models.Model): class Contract(models.Model):
_name = 'hr.contract' _name = 'hr.contract'
@@ -33,14 +33,23 @@ class Contract(models.Model):
notes = fields.Text('Notes') notes = fields.Text('Notes')
state = fields.Selection([ state = fields.Selection([
('draft', 'New'), ('draft', 'New'),
('incoming', 'Incoming'),
('open', 'Running'), ('open', 'Running'),
('pending', 'To Renew'),
('close', 'Expired'), ('close', 'Expired'),
('cancel', 'Cancelled') ('cancel', 'Cancelled')
], string='Status', group_expand='_expand_states', ], string='Status', group_expand='_expand_states',
tracking=True, help='Status of the contract', default='draft') tracking=True, help='Status of the contract', default='draft')
company_id = fields.Many2one('res.company', default=lambda self: self.env.company) company_id = fields.Many2one('res.company', default=lambda self: self.env.company)
"""
kanban_state:
* draft + green = "Incoming" state (will be set as Open once the contract has started)
* open + red = "Pending" state (will be set as Closed once the contract has ended)
* red = Shows a warning on the employees kanban view
"""
kanban_state = fields.Selection([
('normal', 'Grey'),
('done', 'Green'),
('blocked', 'Red')
], string='Kanban State', default='normal', tracking=True)
currency_id = fields.Many2one(string="Currency", related='company_id.currency_id', readonly=True) currency_id = fields.Many2one(string="Currency", related='company_id.currency_id', readonly=True)
permit_no = fields.Char('Work Permit No', related="employee_id.permit_no", readonly=False) permit_no = fields.Char('Work Permit No', related="employee_id.permit_no", readonly=False)
visa_no = fields.Char('Visa No', related="employee_id.visa_no", readonly=False) visa_no = fields.Char('Visa No', related="employee_id.visa_no", readonly=False)
@@ -57,6 +66,7 @@ class Contract(models.Model):
def _expand_states(self, states, domain, order): def _expand_states(self, states, domain, order):
return [key for key, val in type(self).state.selection] return [key for key, val in type(self).state.selection]
@api.onchange('employee_id') @api.onchange('employee_id')
def _onchange_employee_id(self): def _onchange_employee_id(self):
if self.employee_id: if self.employee_id:
@@ -64,14 +74,18 @@ class Contract(models.Model):
self.department_id = self.employee_id.department_id self.department_id = self.employee_id.department_id
self.resource_calendar_id = self.employee_id.resource_calendar_id self.resource_calendar_id = self.employee_id.resource_calendar_id
@api.constrains('employee_id', 'state', 'date_start', 'date_end') @api.constrains('employee_id', 'state', 'kanban_state', 'date_start', 'date_end')
def _check_current_contract(self): def _check_current_contract(self):
""" Two contracts in state [incoming | pending | open] cannot overlap """ """ Two contracts in state [incoming | open | close] cannot overlap """
for contract in self.filtered(lambda c: c.state not in ['draft', 'cancel', 'close']): for contract in self.filtered(lambda c: c.state not in ['draft', 'cancel'] or c.state == 'draft' and c.kanban_state == 'done'):
domain = [ domain = [
('id', '!=', contract.id), ('id', '!=', contract.id),
('employee_id', '=', contract.employee_id.id), ('employee_id', '=', contract.employee_id.id),
('state', 'in', ['incoming', 'pending', 'open']), '|',
('state', 'in', ['open', 'close']),
'&',
('state', '=', 'draft'),
('kanban_state', '=', 'done') # replaces incoming
] ]
if not contract.date_end: if not contract.date_end:
@@ -102,11 +116,11 @@ class Contract(models.Model):
('visa_expire', '<=', fields.Date.to_string(date.today() + relativedelta(days=60))), ('visa_expire', '<=', fields.Date.to_string(date.today() + relativedelta(days=60))),
('visa_expire', '>=', fields.Date.to_string(date.today() + relativedelta(days=1))), ('visa_expire', '>=', fields.Date.to_string(date.today() + relativedelta(days=1))),
]).write({ ]).write({
'state': 'pending' 'kanban_state': 'blocked'
}) })
self.search([ self.search([
('state', 'in', ('open', 'pending')), ('state', '=', 'open'),
'|', '|',
('date_end', '<=', fields.Date.to_string(date.today() + relativedelta(days=1))), ('date_end', '<=', fields.Date.to_string(date.today() + relativedelta(days=1))),
('visa_expire', '<=', fields.Date.to_string(date.today() + relativedelta(days=1))), ('visa_expire', '<=', fields.Date.to_string(date.today() + relativedelta(days=1))),
@@ -114,38 +128,45 @@ class Contract(models.Model):
'state': 'close' 'state': 'close'
}) })
self.search([('state', '=', 'incoming'), ('date_start', '<=', fields.Date.to_string(date.today())),]).write({ self.search([('state', '=', 'draft'), ('kanban_state', '=', 'done'), ('date_start', '<=', fields.Date.to_string(date.today())),]).write({
'state': 'open' 'state': 'open'
}) })
return True return True
def _assign_open_contract(self):
for contract in self:
contract.employee_id.sudo().write({'contract_id': contract.id})
@api.multi
def write(self, vals):
res = super(Contract, self).write(vals)
if vals.get('state') == 'open':
self._assign_open_contract()
calendar = vals.get('resource_calendar_id')
if calendar and (self.state == 'open' or (self.state == 'draft' and self.kanban_state == 'done')):
self.mapped('employee_id').write({'resource_calendar_id': calendar})
if 'state' in vals and 'kanban_state' not in vals:
self.write({'kanban_state': 'normal'})
return res
@api.model @api.model
def create(self, vals_list): def create(self, vals):
contracts = super().create(vals_list) contracts = super(Contract, self).create(vals)
open_contracts = contracts.filtered(lambda c: c.state in ['open', 'pending']) if vals.get('state') == 'open':
# sync contract -> employee contracts._assign_open_contract()
for contract in open_contracts: open_contracts = contracts.filtered(lambda c: c.state == 'open' or c.state == 'draft' and c.kanban_state == 'done')
contract.employee_id.contract_id = contract
# sync contract calendar -> calendar employee # sync contract calendar -> calendar employee
for contract in open_contracts.filtered(lambda c: c.resource_calendar_id): for contract in open_contracts.filtered(lambda c: c.resource_calendar_id):
contract.employee_id.resource_calendar_id = contract.resource_calendar_id contract.employee_id.resource_calendar_id = contract.resource_calendar_id
return contracts return contracts
@api.multi
def write(self, vals):
res = super(Contract, self).write(vals)
if vals.get('state') == 'open':
for contract in self:
contract.employee_id.sudo().write({'contract_id': contract.id})
calendar = vals.get('resource_calendar_id')
if calendar and self.state in ['open', 'pending']:
self.mapped('employee_id').write({'resource_calendar_id': calendar})
return res
@api.multi @api.multi
def _track_subtype(self, init_values): def _track_subtype(self, init_values):
self.ensure_one() self.ensure_one()
if 'state' in init_values and self.state == 'pending': if 'state' in init_values and self.state == 'open' and 'kanban_state' in init_values and self.kanban_state == 'blocked':
return self.env.ref('hr_contract.mt_contract_pending') return self.env.ref('hr_contract.mt_contract_pending')
elif 'state' in init_values and self.state == 'close': elif 'state' in init_values and self.state == 'close':
return self.env.ref('hr_contract.mt_contract_close') return self.env.ref('hr_contract.mt_contract_close')
+23 -9
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models from odoo import api, fields, models
from odoo.osv import expression
class Employee(models.Model): class Employee(models.Model):
@@ -14,6 +15,12 @@ class Employee(models.Model):
groups="hr.group_hr_user", help='Current contract of the employee') groups="hr.group_hr_user", help='Current contract of the employee')
calendar_mismatch = fields.Boolean(related='contract_id.calendar_mismatch') calendar_mismatch = fields.Boolean(related='contract_id.calendar_mismatch')
contracts_count = fields.Integer(compute='_compute_contracts_count', string='Contract Count') contracts_count = fields.Integer(compute='_compute_contracts_count', string='Contract Count')
contract_warning = fields.Boolean(string='Contract Warning', store=True, compute='_compute_contract_warning', groups="hr.group_hr_user")
@api.depends('contract_id', 'contract_id.state', 'contract_id.kanban_state')
def _compute_contract_warning(self):
for employee in self:
employee.contract_warning = not employee.contract_id or employee.contract_id.kanban_state == 'blocked' or employee.contract_id.state != 'open'
def _compute_contracts_count(self): def _compute_contracts_count(self):
# read_group as sudo, since contract count is displayed on form view # read_group as sudo, since contract count is displayed on form view
@@ -22,20 +29,27 @@ class Employee(models.Model):
for employee in self: for employee in self:
employee.contracts_count = result.get(employee.id, 0) employee.contracts_count = result.get(employee.id, 0)
def _get_contracts(self, date_from, date_to, states=['open', 'pending']): def _get_contracts(self, date_from, date_to, states=['open'], kanban_state=False):
""" """
Returns the contracts of the employee between date_from and date_to Returns the contracts of the employee between date_from and date_to
""" """
return self.env['hr.contract'].search([ state_domain = [('state', 'in', states)]
'&', '&', '&', if kanban_state:
('employee_id', 'in', self.ids), state_domain = expression.AND([state_domain, [('kanban_state', 'in', kanban_state)]])
('state', 'in', states),
('date_start', '<=', date_to), return self.env['hr.contract'].search(
'|', ('date_end', '=', False), ('date_end', '>=', date_from) expression.AND([[('employee_id', 'in', self.ids)],
]) state_domain,
[('date_start', '<=', date_to),
'|',
('date_end', '=', False),
('date_end', '>=', date_from)]]))
def _get_incoming_contracts(self, date_from, date_to):
return self._get_contracts(date_from, date_to, states=['draft'], kanban_state=['done'])
@api.model @api.model
def _get_all_contracts(self, date_from, date_to, states=['open', 'pending']): def _get_all_contracts(self, date_from, date_to, states=['open']):
""" """
Returns the contracts of all employees between date_from and date_to Returns the contracts of all employees between date_from and date_to
""" """
@@ -0,0 +1,9 @@
.o_hr_contract_state {
.o_status {
vertical-align: unset !important;
}
}
.o_hr_contract_job_id {
min-height: 1.5rem;
}
+14 -5
View File
@@ -12,6 +12,9 @@ class TestHrContracts(TestContractBase):
self.contracts = self.env['hr.contract'].with_context(tracking_disable=True) self.contracts = self.env['hr.contract'].with_context(tracking_disable=True)
self.test_contract = dict(name='Test', wage=1, employee_id=self.employee.id, state='open') self.test_contract = dict(name='Test', wage=1, employee_id=self.employee.id, state='open')
def test_employee_contractwarning(self):
self.assertEquals(self.employee.contract_warning, True)
def apply_cron(self): def apply_cron(self):
self.env.ref('hr_contract.ir_cron_data_contract_update_state').method_direct_trigger() self.env.ref('hr_contract.ir_cron_data_contract_update_state').method_direct_trigger()
@@ -20,16 +23,20 @@ class TestHrContracts(TestContractBase):
self.contract = self.contracts.create(self.test_contract) self.contract = self.contracts.create(self.test_contract)
self.apply_cron() self.apply_cron()
self.assertEquals(self.contract.state, 'open') self.assertEquals(self.contract.state, 'open')
self.assertEquals(self.contract.kanban_state, 'normal')
self.assertEquals(self.employee.contract_warning, False)
self.test_contract.update(dict(date_end=datetime.now() + relativedelta(days=5))) self.test_contract.update(dict(date_end=datetime.now() + relativedelta(days=5)))
self.contract.write(self.test_contract) self.contract.write(self.test_contract)
self.apply_cron() self.apply_cron()
self.assertEquals(self.contract.state, 'pending') self.assertEquals(self.contract.state, 'open')
self.assertEquals(self.contract.kanban_state, 'blocked')
self.test_contract.update({ self.test_contract.update({
'date_start': datetime.now() + relativedelta(days=-50), 'date_start': datetime.now() + relativedelta(days=-50),
'date_end': datetime.now() + relativedelta(days=-1), 'date_end': datetime.now() + relativedelta(days=-1),
'state': 'pending', 'state': 'open',
'kanban_state': 'blocked',
}) })
self.contract.write(self.test_contract) self.contract.write(self.test_contract)
self.apply_cron() self.apply_cron()
@@ -40,19 +47,21 @@ class TestHrContracts(TestContractBase):
self.test_contract.update(dict(date_end=False)) self.test_contract.update(dict(date_end=False))
self.contract = self.contracts.create(self.test_contract) self.contract = self.contracts.create(self.test_contract)
self.apply_cron() self.apply_cron()
self.assertEquals(self.contract.state, 'pending') self.assertEquals(self.contract.state, 'open')
self.assertEquals(self.contract.kanban_state, 'blocked')
self.employee.visa_expire = date.today() + relativedelta(days=-5) self.employee.visa_expire = date.today() + relativedelta(days=-5)
self.test_contract.update({ self.test_contract.update({
'date_start': datetime.now() + relativedelta(days=-50), 'date_start': datetime.now() + relativedelta(days=-50),
'state': 'pending', 'state': 'open',
'kanban_state': 'blocked',
}) })
self.contract.write(self.test_contract) self.contract.write(self.test_contract)
self.apply_cron() self.apply_cron()
self.assertEquals(self.contract.state, 'close') self.assertEquals(self.contract.state, 'close')
def test_contract_start_date(self): def test_contract_start_date(self):
self.test_contract.update(dict(date_start=datetime.now(), state='incoming')) self.test_contract.update(dict(date_start=datetime.now(), state='draft', kanban_state='done'))
self.contract = self.contracts.create(self.test_contract) self.contract = self.contracts.create(self.test_contract)
self.apply_cron() self.apply_cron()
self.assertEquals(self.contract.state, 'open') self.assertEquals(self.contract.state, 'open')
+15 -14
View File
@@ -12,11 +12,12 @@ class TestHrContracts(TestContractBase):
super(TestHrContracts, self).setUp() super(TestHrContracts, self).setUp()
self.contracts = self.env['hr.contract'].with_context(tracking_disable=True) self.contracts = self.env['hr.contract'].with_context(tracking_disable=True)
def create_contract(self, state, start, end=None): def create_contract(self, state, kanban_state, start, end=None):
return self.env['hr.contract'].create({ return self.env['hr.contract'].create({
'name': 'Contract', 'name': 'Contract',
'employee_id': self.employee.id, 'employee_id': self.employee.id,
'state': state, 'state': state,
'kanban_state': kanban_state,
'wage': 1, 'wage': 1,
'date_start': start, 'date_start': start,
'date_end': end, 'date_end': end,
@@ -25,65 +26,65 @@ class TestHrContracts(TestContractBase):
def test_incoming_overlapping_contract(self): def test_incoming_overlapping_contract(self):
start = datetime.strptime('2015-11-01', '%Y-%m-%d').date() start = datetime.strptime('2015-11-01', '%Y-%m-%d').date()
end = datetime.strptime('2015-11-30', '%Y-%m-%d').date() end = datetime.strptime('2015-11-30', '%Y-%m-%d').date()
self.create_contract('open', start, end) self.create_contract('open', 'normal', start, end)
# Incoming contract # Incoming contract
with self.assertRaises(ValidationError, msg="It should not create two contract in state open or incoming"): with self.assertRaises(ValidationError, msg="It should not create two contract in state open or incoming"):
start = datetime.strptime('2015-11-15', '%Y-%m-%d').date() start = datetime.strptime('2015-11-15', '%Y-%m-%d').date()
end = datetime.strptime('2015-12-30', '%Y-%m-%d').date() end = datetime.strptime('2015-12-30', '%Y-%m-%d').date()
self.create_contract('incoming', start, end) self.create_contract('draft', 'done', start, end)
def test_pending_overlapping_contract(self): def test_pending_overlapping_contract(self):
start = datetime.strptime('2015-11-01', '%Y-%m-%d').date() start = datetime.strptime('2015-11-01', '%Y-%m-%d').date()
end = datetime.strptime('2015-11-30', '%Y-%m-%d').date() end = datetime.strptime('2015-11-30', '%Y-%m-%d').date()
self.create_contract('open', start, end) self.create_contract('open', 'normal', start, end)
# Pending contract # Pending contract
with self.assertRaises(ValidationError, msg="It should not create two contract in state open or pending"): with self.assertRaises(ValidationError, msg="It should not create two contract in state open or pending"):
start = datetime.strptime('2015-11-15', '%Y-%m-%d').date() start = datetime.strptime('2015-11-15', '%Y-%m-%d').date()
end = datetime.strptime('2015-12-30', '%Y-%m-%d').date() end = datetime.strptime('2015-12-30', '%Y-%m-%d').date()
self.create_contract('pending', start, end) self.create_contract('open', 'blocked', start, end)
# Draft contract -> should not raise # Draft contract -> should not raise
start = datetime.strptime('2015-11-15', '%Y-%m-%d').date() start = datetime.strptime('2015-11-15', '%Y-%m-%d').date()
end = datetime.strptime('2015-12-30', '%Y-%m-%d').date() end = datetime.strptime('2015-12-30', '%Y-%m-%d').date()
self.create_contract('draft', start, end) self.create_contract('draft', 'normal', start, end)
def test_draft_overlapping_contract(self): def test_draft_overlapping_contract(self):
start = datetime.strptime('2015-11-01', '%Y-%m-%d').date() start = datetime.strptime('2015-11-01', '%Y-%m-%d').date()
end = datetime.strptime('2015-11-30', '%Y-%m-%d').date() end = datetime.strptime('2015-11-30', '%Y-%m-%d').date()
self.create_contract('open', start, end) self.create_contract('open', 'normal', start, end)
# Draft contract -> should not raise even if overlapping # Draft contract -> should not raise even if overlapping
start = datetime.strptime('2015-11-15', '%Y-%m-%d').date() start = datetime.strptime('2015-11-15', '%Y-%m-%d').date()
end = datetime.strptime('2015-12-30', '%Y-%m-%d').date() end = datetime.strptime('2015-12-30', '%Y-%m-%d').date()
self.create_contract('draft', start, end) self.create_contract('draft', 'normal', start, end)
def test_overlapping_contract_no_end(self): def test_overlapping_contract_no_end(self):
# No end date # No end date
self.create_contract('open', datetime.strptime('2015-11-01', '%Y-%m-%d').date()) self.create_contract('open', 'normal', datetime.strptime('2015-11-01', '%Y-%m-%d').date())
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
start = datetime.strptime('2015-11-15', '%Y-%m-%d').date() start = datetime.strptime('2015-11-15', '%Y-%m-%d').date()
end = datetime.strptime('2015-12-30', '%Y-%m-%d').date() end = datetime.strptime('2015-12-30', '%Y-%m-%d').date()
self.create_contract('incoming', start, end) self.create_contract('draft', 'done', start, end)
def test_overlapping_contract_no_end2(self): def test_overlapping_contract_no_end2(self):
start = datetime.strptime('2015-11-01', '%Y-%m-%d').date() start = datetime.strptime('2015-11-01', '%Y-%m-%d').date()
end = datetime.strptime('2015-12-30', '%Y-%m-%d').date() end = datetime.strptime('2015-12-30', '%Y-%m-%d').date()
self.create_contract('open', start, end) self.create_contract('open', 'normal', start, end)
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
# No end # No end
self.create_contract('incoming', datetime.strptime('2015-01-01', '%Y-%m-%d').date()) self.create_contract('draft', 'done', datetime.strptime('2015-01-01', '%Y-%m-%d').date())
def test_set_employee_contract_create(self): def test_set_employee_contract_create(self):
contract = self.create_contract('open', date(2018, 1, 1), date(2018, 1, 2)) contract = self.create_contract('open', 'normal', date(2018, 1, 1), date(2018, 1, 2))
self.assertEqual(self.employee.contract_id, contract) self.assertEqual(self.employee.contract_id, contract)
def test_set_employee_contract_write(self): def test_set_employee_contract_write(self):
contract = self.create_contract('draft', date(2018, 1, 1), date(2018, 1, 2)) contract = self.create_contract('draft', 'normal', date(2018, 1, 1), date(2018, 1, 2))
contract.state = 'open' contract.state = 'open'
self.assertEqual(self.employee.contract_id, contract) self.assertEqual(self.employee.contract_id, contract)
+7
View File
@@ -0,0 +1,7 @@
<odoo>
<template id="assets_backend" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" type="text/css" href="/hr_contract/static/src/scss/state_selection.scss"/>
</xpath>
</template>
</odoo>
+51 -7
View File
@@ -53,6 +53,37 @@
</field> </field>
</record> </record>
<record id="hr_employee_view_kanban" model="ir.ui.view">
<field name="name">hr.employee.view.kanban</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.hr_kanban_view_employees"/>
<field name="arch" type="xml">
<data>
<xpath expr="//div[hasclass('o_kanban_record_headings')]" position="after">
<field name="contract_warning" invisible="1"/>
<t t-if="record.contract_warning.raw_value">
<i class="fa fa-exclamation-triangle text-danger" role="img"
title="There is something wrong with the contract. Either there is no running contract for this employee or employee's contract is about to expire."
aria-label="There is something wrong with the contract. Either there is no running contract for this employee or employee's contract is about to expire."></i>
</t>
</xpath>
</data>
</field>
</record>
<record id="hr_employee_view_search" model="ir.ui.view">
<field name="name">hr.employee.view.search</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_filter"/>
<field name="arch" type="xml">
<data>
<xpath expr="." position="inside">
<filter string="Contract Warning" name="with_contract_warning" domain="[('contract_warning', '=', True)]"/>
</xpath>
</data>
</field>
</record>
<record id="hr_user_view_form" model="ir.ui.view"> <record id="hr_user_view_form" model="ir.ui.view">
<field name="name">hr.user.preferences.view.form.contract.inherit</field> <field name="name">hr.user.preferences.view.form.contract.inherit</field>
<field name="model">res.users</field> <field name="model">res.users</field>
@@ -83,7 +114,7 @@
<field name="department_id" operator="child_of"/> <field name="department_id" operator="child_of"/>
<field name="state"/> <field name="state"/>
<filter string="Running" name="running" domain="[('state', '=', 'open')]"/> <filter string="Running" name="running" domain="[('state', '=', 'open')]"/>
<filter string="To Renew" name="to_renew" domain="[('state', '=', 'pending')]"/> <filter string="To Renew / Expires Soon" name="to_renew" domain="['&amp;', ('state', '=', 'open'), ('kanban_state', '=', 'blocked')]"/>
<separator /> <separator />
<filter string="Current Employee" name="current_employee" domain="[('employee_id.active','=',True)]"/> <filter string="Current Employee" name="current_employee" domain="[('employee_id.active','=',True)]"/>
<separator/> <separator/>
@@ -114,8 +145,16 @@
</button> </button>
</div> </div>
<div class="oe_title" name="title"> <div class="oe_title" name="title">
<label for="name" class="oe_edit_only"/> <div class="oe_edit_only">
<div class="float-left">
<field name="kanban_state" widget="state_selection" class="mr-2"/>
</div>
<label for="name"/>
</div>
<h1> <h1>
<div class="float-left oe_read_only">
<field name="kanban_state" widget="state_selection" class="mr-2"/>
</div>
<field name="name" placeholder="Contract Reference"/> <field name="name" placeholder="Contract Reference"/>
</h1> </h1>
</div> </div>
@@ -143,7 +182,7 @@
<label for="resource_calendar_id"/> <label for="resource_calendar_id"/>
<div> <div>
<field name="resource_calendar_id" required="1" nolabel="1"/> <field name="resource_calendar_id" required="1" nolabel="1"/>
<span attrs="{'invisible': ['|', ('calendar_mismatch', '=', False), ('state', 'not in', ['open', 'pending'])]}" <span attrs="{'invisible': ['|', ('calendar_mismatch', '=', False), ('state', '!=', 'open')]}"
class="fa fa-exclamation-triangle text-danger o_calendar_warning pl-3"> class="fa fa-exclamation-triangle text-danger o_calendar_warning pl-3">
</span> </span>
<span class="o_calendar_warning_tooltip text-danger"> <span class="o_calendar_warning_tooltip text-danger">
@@ -196,6 +235,7 @@
<field name="date_start"/> <field name="date_start"/>
<field name="date_end"/> <field name="date_end"/>
<field name="state"/> <field name="state"/>
<field name="kanban_state" widget="state_selection" readonly="1"/>
<field name="wage" invisible="1"/> <field name="wage" invisible="1"/>
<field name="message_needaction" invisible="1"/> <field name="message_needaction" invisible="1"/>
<field name="company_id" groups="base.group_multi_company"/> <field name="company_id" groups="base.group_multi_company"/>
@@ -207,9 +247,10 @@
<field name="name">hr.contract.kanban</field> <field name="name">hr.contract.kanban</field>
<field name="model">hr.contract</field> <field name="model">hr.contract</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<kanban class="o_kanban_small_column"> <kanban class="o_kanban_small_column" default_order="date_end">
<field name="employee_id"/> <field name="employee_id"/>
<field name="activity_state"/> <field name="activity_state"/>
<field name="state"/>
<progressbar field="activity_state" colors='{"planned": "success", "today": "warning", "overdue": "danger"}'/> <progressbar field="activity_state" colors='{"planned": "success", "today": "warning", "overdue": "danger"}'/>
<templates> <templates>
<t t-name="kanban-box"> <t t-name="kanban-box">
@@ -224,10 +265,13 @@
</div> </div>
</div> </div>
<div class="oe_kanban_content"> <div class="oe_kanban_content">
<div> <div class="o_hr_contract_state">
<strong class="o_kanban_record_title"><field name="name"/></strong> <field name="kanban_state" widget="state_selection" />
<strong class="o_kanban_record_title">
<field name="name"/>
</strong>
</div> </div>
<div class="text-muted o_kanban_record_subtitle"> <div class="text-muted o_kanban_record_subtitle o_hr_contract_job_id">
<field name="job_id"/> <field name="job_id"/>
</div> </div>
<div class="oe_kanban_bottom_right"> <div class="oe_kanban_bottom_right">