From f17655dd773096f2d37ece392dfe980ae4795819 Mon Sep 17 00:00:00 2001 From: Kevin Baptiste Date: Thu, 16 May 2019 13:30:01 +0000 Subject: [PATCH] [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) --- addons/hr_contract/__manifest__.py | 2 +- addons/hr_contract/data/hr_contract_demo.xml | 15 +++- addons/hr_contract/models/hr_contract.py | 77 ++++++++++++------- addons/hr_contract/models/hr_employee.py | 32 +++++--- .../static/src/scss/state_selection.scss | 9 +++ addons/hr_contract/tests/test_auto_status.py | 19 +++-- addons/hr_contract/tests/test_contract.py | 29 +++---- addons/hr_contract/views/assets.xml | 7 ++ .../hr_contract/views/hr_contract_views.xml | 58 ++++++++++++-- 9 files changed, 183 insertions(+), 65 deletions(-) create mode 100644 addons/hr_contract/static/src/scss/state_selection.scss create mode 100644 addons/hr_contract/views/assets.xml 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 @@