diff --git a/hr_attendance_ip_check/README.rst b/hr_attendance_ip_check/README.rst new file mode 100644 index 00000000..954e5e03 --- /dev/null +++ b/hr_attendance_ip_check/README.rst @@ -0,0 +1,95 @@ +========================= +IP-based Attendance Check +========================= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fhr-lightgray.png?logo=github + :target: https://github.com/OCA/hr/tree/16.0/hr_attendance_ip_check + :alt: OCA/hr +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/hr-16-0/hr-16-0-hr_attendance_ip_check + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/hr/16.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module extends the HR Attendance module to add IP-based validation for attendance check-in/check-out operations. +It allows companies to restrict attendance registrations to specific IP addresses or ranges, ensuring that +employees can only record their attendance when they are physically present in the company network. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============ + +To configure this module, you need to: + +* Go to Settings -> Human Resources -> Attendance +* Enable IP-based attendance check +* Enter the allowed IP addresses in the whitelist field (comma-separated) + +Usage +===== + +To use this module, you need to: + +* Employees attempt to check in/out as normal through the Attendance interface +* If IP checking is enabled: + * If the employee's IP is in the whitelist, check-in/check-out proceeds normally + * If the employee's IP is not in the whitelist, a red message appears under the button + and the check-in/check-out operation is blocked + +Bug Tracker +========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ADITI + +Contributors +~~~~~~~~~~~~ + +* Kongkea Ouch + +Maintainers +~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/hr `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. \ No newline at end of file diff --git a/hr_attendance_ip_check/__init__.py b/hr_attendance_ip_check/__init__.py new file mode 100644 index 00000000..5ad9d687 --- /dev/null +++ b/hr_attendance_ip_check/__init__.py @@ -0,0 +1,2 @@ +"""IP-based Attendance Check module initialization.""" +from . import models diff --git a/hr_attendance_ip_check/__manifest__.py b/hr_attendance_ip_check/__manifest__.py new file mode 100644 index 00000000..957bb542 --- /dev/null +++ b/hr_attendance_ip_check/__manifest__.py @@ -0,0 +1,14 @@ +{ + 'name': 'HR Attendance IP Check', + 'version': '16.0.1.0.0', + 'category': 'Human Resources', + 'summary': 'Restrict attendance check-in/check-out based on IP address', + 'author': 'ADITI, ' + 'Odoo Community Association (OCA)', + 'website': 'https://github.com/OCA/hr', + 'license': 'AGPL-3', + 'depends': ['hr_attendance'], + 'data': [ + 'views/res_config_settings_views.xml', + ] +} diff --git a/hr_attendance_ip_check/models/__init__.py b/hr_attendance_ip_check/models/__init__.py new file mode 100644 index 00000000..92a4f0f6 --- /dev/null +++ b/hr_attendance_ip_check/models/__init__.py @@ -0,0 +1,3 @@ +from . import hr_employee +from . import hr_attendance +from . import res_config_settings diff --git a/hr_attendance_ip_check/models/hr_attendance.py b/hr_attendance_ip_check/models/hr_attendance.py new file mode 100644 index 00000000..ed142f50 --- /dev/null +++ b/hr_attendance_ip_check/models/hr_attendance.py @@ -0,0 +1,35 @@ +import logging +from odoo import models, api, _ + +_logger = logging.getLogger(__name__) + + +class HrAttendance(models.Model): + _inherit = 'hr.attendance' + + @api.model_create_multi + def create(self, vals_list): + """Override create to validate IP before creating attendance records.""" + if not vals_list: + return {'warning': _('No valid attendance records to create')} + + valid_vals = [] + for vals in vals_list: + employee = self.env['hr.employee'].browse(vals.get('employee_id')) + validation_result = employee._validate_ip_address('check_in') + + if isinstance(validation_result, dict): + return validation_result + valid_vals.append(vals) + + return super().create(valid_vals) + + def write(self, vals): + """Override write to validate IP before updating attendance records.""" + if 'check_out' in vals: + for attendance in self: + validation_result = attendance.employee_id._validate_ip_address('check_out') + if isinstance(validation_result, dict): + return validation_result + + return super().write(vals) diff --git a/hr_attendance_ip_check/models/hr_employee.py b/hr_attendance_ip_check/models/hr_employee.py new file mode 100644 index 00000000..aa635891 --- /dev/null +++ b/hr_attendance_ip_check/models/hr_employee.py @@ -0,0 +1,68 @@ +import logging +from odoo import models, _ +from odoo.http import request +from werkzeug.exceptions import HTTPException + +_logger = logging.getLogger(__name__) + + +class HrEmployee(models.Model): + _inherit = 'hr.employee' + + def _get_current_ip(self): + """Get the current IP address from the request.""" + try: + if request and request.httprequest: + return request.httprequest.remote_addr + except (AttributeError, HTTPException) as e: + _logger.error("Error getting IP address: %s", str(e), exc_info=True) + return None + + def _validate_ip_address(self, action_type='attendance'): + """Validate if current IP is allowed for attendance actions.""" + if not self.env['ir.config_parameter'].sudo().get_param( + 'hr_attendance.ip_check_enabled', 'False').lower() == 'true': + return True + + current_ip = self._get_current_ip() + if not current_ip: + return { + 'warning': _('Unable to determine your IP address') + } + + whitelist_ips = [ + ip.strip() + for ip in self.env['ir.config_parameter'].sudo() + .get_param('hr_attendance.whitelist_ips', '').split(',') + if ip.strip() + ] + + if not whitelist_ips: + return { + 'warning': _('No IP addresses are whitelisted') + } + + if current_ip not in whitelist_ips: + return { + 'warning': _('You are not allowed to %(action)s from current IP address (%(ip)s)') % { + 'action': action_type.replace('_', ' '), + 'ip': current_ip + } + } + + return True + + def attendance_manual(self, next_action, entered_pin=None): + """Handle manual attendance with IP validation.""" + self.ensure_one() + + # Determine action type based on current state + action_type = 'check_out' if self.attendance_state == 'checked_in' else 'check_in' + + # Validate IP first + ip_validation = self._validate_ip_address(action_type) + if isinstance(ip_validation, dict): + return ip_validation + + # Let hr_attendance handle the rest (PIN check, etc) + return super().attendance_manual(next_action, entered_pin) diff --git a/hr_attendance_ip_check/models/res_config_settings.py b/hr_attendance_ip_check/models/res_config_settings.py new file mode 100644 index 00000000..13f6f690 --- /dev/null +++ b/hr_attendance_ip_check/models/res_config_settings.py @@ -0,0 +1,14 @@ +from odoo import fields, models + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + ip_check_enabled = fields.Boolean( + string='Enable IP-based Attendance Check', + config_parameter='hr_attendance.ip_check_enabled', + help="Enable IP address validation for attendance check-in/check-out" + ) + whitelist_ips = fields.Char( + string='Whitelist IP Addresses', + config_parameter='hr_attendance.whitelist_ips', + help="Comma-separated list of allowed IP addresses") diff --git a/hr_attendance_ip_check/readme/CONFIGURATION.rst b/hr_attendance_ip_check/readme/CONFIGURATION.rst new file mode 100644 index 00000000..18ee9bdf --- /dev/null +++ b/hr_attendance_ip_check/readme/CONFIGURATION.rst @@ -0,0 +1,8 @@ +Configuration +============ + +To configure IP-based attendance checking: + +1. Go to Settings -> Human Resources -> Attendance +2. Enable IP-based attendance check +3. Enter the allowed IP addresses in the whitelist field (comma-separated) \ No newline at end of file diff --git a/hr_attendance_ip_check/readme/CONTRIBUTOR.rst b/hr_attendance_ip_check/readme/CONTRIBUTOR.rst new file mode 100644 index 00000000..af4ec473 --- /dev/null +++ b/hr_attendance_ip_check/readme/CONTRIBUTOR.rst @@ -0,0 +1,17 @@ +Contributors +=========== + +* Kongkea Ouch + +Maintainers +---------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. \ No newline at end of file diff --git a/hr_attendance_ip_check/readme/DESCRIPTION.rst b/hr_attendance_ip_check/readme/DESCRIPTION.rst new file mode 100644 index 00000000..adc343b3 --- /dev/null +++ b/hr_attendance_ip_check/readme/DESCRIPTION.rst @@ -0,0 +1,14 @@ +========================== +IP-based Attendance Check +========================== + +This module extends the HR Attendance module to add IP-based validation for attendance check-in/check-out operations. +It allows companies to restrict attendance registrations to specific IP addresses or ranges, ensuring that +employees can only record their attendance when they are physically present in the company network. + +Key features: +------------ +* IP whitelist configuration for attendance check-in/check-out +* Easy enable/disable of IP verification +* Visual feedback when attempting check-in/check-out from unauthorized IP +* Flexible configuration through settings menu \ No newline at end of file diff --git a/hr_attendance_ip_check/readme/DEVELOP.rst b/hr_attendance_ip_check/readme/DEVELOP.rst new file mode 100644 index 00000000..28e576e6 --- /dev/null +++ b/hr_attendance_ip_check/readme/DEVELOP.rst @@ -0,0 +1,16 @@ +Development +========== + +To modify this module, you will need to: + +1. Clone the OCA HR repository +2. Create a new branch +3. Install development dependencies: + * No special dependencies required beyond Odoo dependencies + +Bug Tracker +---------- +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback. \ No newline at end of file diff --git a/hr_attendance_ip_check/readme/USAGE.rst b/hr_attendance_ip_check/readme/USAGE.rst new file mode 100644 index 00000000..0dd5ebd9 --- /dev/null +++ b/hr_attendance_ip_check/readme/USAGE.rst @@ -0,0 +1,23 @@ +Usage +===== + +Regular Usage +------------ +1. Employees attempt to check in/out as normal through the Attendance interface +2. If IP checking is enabled: + * If the employee's IP is in the whitelist, check-in/check-out proceeds normally + * If the employee's IP is not in the whitelist, a warning message appears + and the check-in/check-out operation is blocked + +Administrator Usage +----------------- +1. Go to Settings -> Human Resources -> Attendance +2. Enable or disable IP-based attendance check +3. Configure the whitelist of allowed IP addresses + +Troubleshooting +------------- +* If an employee cannot check in/out: + - Verify their IP address + - Check if the IP is in the whitelist + - Verify that IP checking is enabled \ No newline at end of file diff --git a/hr_attendance_ip_check/requirements.txt b/hr_attendance_ip_check/requirements.txt new file mode 100644 index 00000000..ca2d89e9 --- /dev/null +++ b/hr_attendance_ip_check/requirements.txt @@ -0,0 +1 @@ +# No additional requirements beyond Odoo \ No newline at end of file diff --git a/hr_attendance_ip_check/tests/__init__.py b/hr_attendance_ip_check/tests/__init__.py new file mode 100644 index 00000000..1d4d5a82 --- /dev/null +++ b/hr_attendance_ip_check/tests/__init__.py @@ -0,0 +1 @@ +from . import test_hr_attendance diff --git a/hr_attendance_ip_check/tests/test_hr_attendance.py b/hr_attendance_ip_check/tests/test_hr_attendance.py new file mode 100644 index 00000000..30dd2c18 --- /dev/null +++ b/hr_attendance_ip_check/tests/test_hr_attendance.py @@ -0,0 +1,99 @@ +from odoo.tests.common import TransactionCase, tagged +from unittest.mock import patch +import logging + +_logger = logging.getLogger(__name__) + + +@tagged('post_install', '-at_install') +class TestHrAttendance(TransactionCase): + + def setUp(self): + super().setUp() + self.employee = self.env['hr.employee'].create({ + 'name': 'Test Employee', + }) + + # Define test IPs + self.ALLOWED_IP = '192.168.1.1' + self.BLOCKED_IP = '192.168.1.2' + self.WHITELIST = f'{self.ALLOWED_IP}, 10.0.0.1' + + # Configure system parameters + self._set_config_params() + + def _set_config_params(self): + """Set configuration parameters.""" + param_obj = self.env['ir.config_parameter'].sudo() + param_obj.set_param('hr_attendance.ip_check_enabled', 'True') + param_obj.set_param('hr_attendance.whitelist_ips', self.WHITELIST) + + def _check_warning_format(self, result, message_contains=None): + """Helper method to check warning format""" + self.assertTrue(isinstance(result, dict)) + self.assertTrue('warning' in result) + if message_contains: + self.assertTrue(message_contains in result['warning']) + + def test_01_blocked_ip(self): + """Test attendance with blocked IP.""" + with patch('odoo.addons.hr_attendance_ip_check.models.hr_employee.HrEmployee._get_current_ip', + return_value=self.BLOCKED_IP): + result = self.employee.attendance_manual('hr_attendance.hr_attendance_action_my_attendances') + self._check_warning_format(result, message_contains=self.BLOCKED_IP) + + def test_02_empty_whitelist(self): + """Test when whitelist is empty.""" + self.env['ir.config_parameter'].sudo().set_param('hr_attendance.whitelist_ips', '') + + with patch('odoo.addons.hr_attendance_ip_check.models.hr_employee.HrEmployee._get_current_ip', + return_value=self.ALLOWED_IP): + result = self.employee.attendance_manual('hr_attendance.hr_attendance_action_my_attendances') + self._check_warning_format(result, message_contains='No IP addresses are whitelisted') + + def test_03_allowed_ip(self): + """Test attendance with allowed IP.""" + with patch('odoo.addons.hr_attendance_ip_check.models.hr_employee.HrEmployee._get_current_ip', + return_value=self.ALLOWED_IP): + result = self.employee.attendance_manual('hr_attendance.hr_attendance_action_my_attendances') + self.assertTrue(result.get('action')) # Should proceed with normal attendance flow + + def test_04_ip_check_disabled(self): + """Test when IP checking is disabled.""" + self.env['ir.config_parameter'].sudo().set_param('hr_attendance.ip_check_enabled', 'False') + + with patch('odoo.addons.hr_attendance_ip_check.models.hr_employee.HrEmployee._get_current_ip', + return_value=self.BLOCKED_IP): + result = self.employee.attendance_manual('hr_attendance.hr_attendance_action_my_attendances') + self.assertTrue(result.get('action')) # Should proceed regardless of IP + + def test_05_failed_ip_detection(self): + """Test when IP detection fails.""" + with patch('odoo.addons.hr_attendance_ip_check.models.hr_employee.HrEmployee._get_current_ip', + return_value=None): + result = self.employee.attendance_manual('hr_attendance.hr_attendance_action_my_attendances') + self._check_warning_format(result, message_contains='Unable to determine your IP address') + + def test_06_whitelist_with_spaces(self): + """Test IP validation with whitespace in whitelist.""" + self.env['ir.config_parameter'].sudo().set_param( + 'hr_attendance.whitelist_ips', ' 192.168.1.1, 10.0.0.1 , 192.168.1.3') + + with patch('odoo.addons.hr_attendance_ip_check.models.hr_employee.HrEmployee._get_current_ip', + return_value='192.168.1.3'): + result = self.employee.attendance_manual('hr_attendance.hr_attendance_action_my_attendances') + self.assertTrue(result.get('action')) + + def test_07_check_in_out_sequence(self): + """Test check-in/out sequence with IP validation.""" + with patch('odoo.addons.hr_attendance_ip_check.models.hr_employee.HrEmployee._get_current_ip', + return_value=self.ALLOWED_IP): + # Check-in + result1 = self.employee.attendance_manual('hr_attendance.hr_attendance_action_my_attendances') + self.assertTrue(result1.get('action')) + self.assertEqual(self.employee.attendance_state, 'checked_in') + + # Check-out + result2 = self.employee.attendance_manual('hr_attendance.hr_attendance_action_my_attendances') + self.assertTrue(result2.get('action')) + self.assertEqual(self.employee.attendance_state, 'checked_out') \ No newline at end of file diff --git a/hr_attendance_ip_check/views/res_config_settings_views.xml b/hr_attendance_ip_check/views/res_config_settings_views.xml new file mode 100644 index 00000000..533ce9de --- /dev/null +++ b/hr_attendance_ip_check/views/res_config_settings_views.xml @@ -0,0 +1,35 @@ + + + + res.config.settings.view.form.inherit.ip.attendance + res.config.settings + + + +

IP Attendance Check

+
+
+
+ +
+
+
+
+
+
+
+
+