diff --git a/addons/hr_contract/__manifest__.py b/addons/hr_contract/__manifest__.py index d229c773b53..f05b650c937 100644 --- a/addons/hr_contract/__manifest__.py +++ b/addons/hr_contract/__manifest__.py @@ -23,7 +23,7 @@ You can assign several contracts per employee. 'security/ir.model.access.csv', 'data/hr_contract_data.xml', 'views/hr_contract_views.xml', - 'views/hr_contract_templates.xml', + 'views/assets.xml', ], 'demo': ['data/hr_contract_demo.xml'], 'installable': True, diff --git a/addons/hr_contract/data/hr_contract_demo.xml b/addons/hr_contract/data/hr_contract_demo.xml index c7092205f57..591137f2f88 100644 --- a/addons/hr_contract/data/hr_contract_demo.xml +++ b/addons/hr_contract/data/hr_contract_demo.xml @@ -11,6 +11,7 @@ eval="obj().env.ref('hr.employee_admin').department_id.id"/> draft + normal @@ -23,6 +24,7 @@ eval="obj().env.ref('hr.employee_al').department_id.id"/> open + normal @@ -35,6 +37,7 @@ eval="obj().env.ref('hr.employee_mit').department_id.id"/> open + normal @@ -48,6 +51,7 @@ eval="obj().env.ref('hr.employee_stw').department_id.id"/> open + normal @@ -60,6 +64,7 @@ eval="obj().env.ref('hr.employee_qdp').department_id.id"/> draft + normal @@ -72,6 +77,7 @@ eval="obj().env.ref('hr.employee_han').department_id.id"/> open + normal @@ -85,6 +91,7 @@ eval="obj().env.ref('hr.employee_niv').department_id.id"/> draft + normal @@ -98,6 +105,7 @@ eval="obj().env.ref('hr.employee_jth').department_id.id"/> draft + normal @@ -110,6 +118,7 @@ eval="obj().env.ref('hr.employee_chs').department_id.id"/> cancel + normal @@ -122,6 +131,7 @@ eval="obj().env.ref('hr.employee_jve').department_id.id"/> cancel + normal @@ -134,7 +144,8 @@ - pending + open + blocked @@ -148,6 +159,7 @@ eval="obj().env.ref('hr.employee_fpi').department_id.id"/> close + normal @@ -161,6 +173,7 @@ eval="obj().env.ref('hr.employee_vad').department_id.id"/> close + normal \ No newline at end of file diff --git a/addons/hr_contract/models/hr_contract.py b/addons/hr_contract/models/hr_contract.py index 544ec534715..343479f3bdb 100644 --- a/addons/hr_contract/models/hr_contract.py +++ b/addons/hr_contract/models/hr_contract.py @@ -6,8 +6,8 @@ from dateutil.relativedelta import relativedelta from odoo import api, fields, models, _ from odoo.exceptions import ValidationError -from odoo.osv import expression +from odoo.osv import expression class Contract(models.Model): _name = 'hr.contract' @@ -33,14 +33,23 @@ class Contract(models.Model): notes = fields.Text('Notes') state = fields.Selection([ ('draft', 'New'), - ('incoming', 'Incoming'), ('open', 'Running'), - ('pending', 'To Renew'), ('close', 'Expired'), ('cancel', 'Cancelled') ], string='Status', group_expand='_expand_states', tracking=True, help='Status of the contract', default='draft') 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) 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) @@ -57,6 +66,7 @@ class Contract(models.Model): def _expand_states(self, states, domain, order): return [key for key, val in type(self).state.selection] + @api.onchange('employee_id') def _onchange_employee_id(self): if self.employee_id: @@ -64,14 +74,18 @@ class Contract(models.Model): self.department_id = self.employee_id.department_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): - """ Two contracts in state [incoming | pending | open] cannot overlap """ - for contract in self.filtered(lambda c: c.state not in ['draft', 'cancel', 'close']): + """ Two contracts in state [incoming | open | close] cannot overlap """ + for contract in self.filtered(lambda c: c.state not in ['draft', 'cancel'] or c.state == 'draft' and c.kanban_state == 'done'): domain = [ ('id', '!=', contract.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: @@ -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=1))), ]).write({ - 'state': 'pending' + 'kanban_state': 'blocked' }) self.search([ - ('state', 'in', ('open', 'pending')), + ('state', '=', 'open'), '|', ('date_end', '<=', 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' }) - 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' }) 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 - def create(self, vals_list): - contracts = super().create(vals_list) - open_contracts = contracts.filtered(lambda c: c.state in ['open', 'pending']) - # sync contract -> employee - for contract in open_contracts: - contract.employee_id.contract_id = contract + def create(self, vals): + contracts = super(Contract, self).create(vals) + if vals.get('state') == 'open': + contracts._assign_open_contract() + open_contracts = contracts.filtered(lambda c: c.state == 'open' or c.state == 'draft' and c.kanban_state == 'done') # sync contract calendar -> calendar employee for contract in open_contracts.filtered(lambda c: c.resource_calendar_id): contract.employee_id.resource_calendar_id = contract.resource_calendar_id 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 def _track_subtype(self, init_values): 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') elif 'state' in init_values and self.state == 'close': return self.env.ref('hr_contract.mt_contract_close') diff --git a/addons/hr_contract/models/hr_employee.py b/addons/hr_contract/models/hr_employee.py index 4d596f046e0..a1f36d333b7 100644 --- a/addons/hr_contract/models/hr_employee.py +++ b/addons/hr_contract/models/hr_employee.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models +from odoo.osv import expression class Employee(models.Model): @@ -14,6 +15,12 @@ class Employee(models.Model): groups="hr.group_hr_user", help='Current contract of the employee') calendar_mismatch = fields.Boolean(related='contract_id.calendar_mismatch') 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): # read_group as sudo, since contract count is displayed on form view @@ -22,20 +29,27 @@ class Employee(models.Model): for employee in self: 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 """ - return self.env['hr.contract'].search([ - '&', '&', '&', - ('employee_id', 'in', self.ids), - ('state', 'in', states), - ('date_start', '<=', date_to), - '|', ('date_end', '=', False), ('date_end', '>=', date_from) - ]) + state_domain = [('state', 'in', states)] + if kanban_state: + state_domain = expression.AND([state_domain, [('kanban_state', 'in', kanban_state)]]) + + return self.env['hr.contract'].search( + 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 - 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 """ diff --git a/addons/hr_contract/static/src/scss/state_selection.scss b/addons/hr_contract/static/src/scss/state_selection.scss new file mode 100644 index 00000000000..329cfd28feb --- /dev/null +++ b/addons/hr_contract/static/src/scss/state_selection.scss @@ -0,0 +1,9 @@ +.o_hr_contract_state { + .o_status { + vertical-align: unset !important; + } +} + +.o_hr_contract_job_id { + min-height: 1.5rem; +} \ No newline at end of file diff --git a/addons/hr_contract/tests/test_auto_status.py b/addons/hr_contract/tests/test_auto_status.py index c342d05dd0f..fcff8621bb9 100644 --- a/addons/hr_contract/tests/test_auto_status.py +++ b/addons/hr_contract/tests/test_auto_status.py @@ -12,6 +12,9 @@ class TestHrContracts(TestContractBase): 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') + def test_employee_contractwarning(self): + self.assertEquals(self.employee.contract_warning, True) + def apply_cron(self): 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.apply_cron() 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.contract.write(self.test_contract) 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({ 'date_start': datetime.now() + relativedelta(days=-50), 'date_end': datetime.now() + relativedelta(days=-1), - 'state': 'pending', + 'state': 'open', + 'kanban_state': 'blocked', }) self.contract.write(self.test_contract) self.apply_cron() @@ -40,19 +47,21 @@ class TestHrContracts(TestContractBase): self.test_contract.update(dict(date_end=False)) self.contract = self.contracts.create(self.test_contract) 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.test_contract.update({ 'date_start': datetime.now() + relativedelta(days=-50), - 'state': 'pending', + 'state': 'open', + 'kanban_state': 'blocked', }) self.contract.write(self.test_contract) self.apply_cron() self.assertEquals(self.contract.state, 'close') 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.apply_cron() self.assertEquals(self.contract.state, 'open') diff --git a/addons/hr_contract/tests/test_contract.py b/addons/hr_contract/tests/test_contract.py index ca05600929c..3d339c4c101 100644 --- a/addons/hr_contract/tests/test_contract.py +++ b/addons/hr_contract/tests/test_contract.py @@ -12,11 +12,12 @@ class TestHrContracts(TestContractBase): super(TestHrContracts, self).setUp() 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({ 'name': 'Contract', 'employee_id': self.employee.id, 'state': state, + 'kanban_state': kanban_state, 'wage': 1, 'date_start': start, 'date_end': end, @@ -25,65 +26,65 @@ class TestHrContracts(TestContractBase): def test_incoming_overlapping_contract(self): start = datetime.strptime('2015-11-01', '%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 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() 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): start = datetime.strptime('2015-11-01', '%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 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() 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 start = datetime.strptime('2015-11-15', '%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): start = datetime.strptime('2015-11-01', '%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 start = datetime.strptime('2015-11-15', '%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): # 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): start = datetime.strptime('2015-11-15', '%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): start = datetime.strptime('2015-11-01', '%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): # 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): - 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) 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' self.assertEqual(self.employee.contract_id, contract) diff --git a/addons/hr_contract/views/assets.xml b/addons/hr_contract/views/assets.xml new file mode 100644 index 00000000000..f482256b4ae --- /dev/null +++ b/addons/hr_contract/views/assets.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/addons/hr_contract/views/hr_contract_views.xml b/addons/hr_contract/views/hr_contract_views.xml index a1c9c3cd843..ecf835d094c 100644 --- a/addons/hr_contract/views/hr_contract_views.xml +++ b/addons/hr_contract/views/hr_contract_views.xml @@ -53,6 +53,37 @@ + + hr.employee.view.kanban + hr.employee + + + + + + + + + + + + + + + hr.employee.view.search + hr.employee + + + + + + + + + + hr.user.preferences.view.form.contract.inherit res.users @@ -83,7 +114,7 @@ - + @@ -114,8 +145,16 @@
-
@@ -143,7 +182,7 @@