| @ -0,0 +1,64 @@ | |||
| .. image:: https://img.shields.io/badge/license-AGPL--3-blue.svg | |||
|     :target: https://www.gnu.org/licenses/agpl-3.0-standalone.html | |||
|     :alt: License: AGPL-3 | |||
| 
 | |||
| HR Biometric Device Integration | |||
| =============================== | |||
| This Cybrosys's module integrates Odoo attendance with biometric device attendance. | |||
| 
 | |||
| Configuration | |||
| ============ | |||
| * This integration is  applicable for the the ZK Devices. | |||
| * zklib you can install zklib library using "sudo pip install zklib" | |||
| 
 | |||
| Compatible Devices | |||
| ---------------- | |||
| This module support with the following machines | |||
| 
 | |||
| * uFace202 (ZKteco) | |||
| * iFace990 (ZKteco) | |||
| 
 | |||
| Clients have reported that the module works well with the following machine | |||
| 
 | |||
| * K40 Pro (ZKteco) | |||
| * SFace900 (ZKteco) | |||
| * FR1500 (ZKteco) | |||
| * UA760 (ZKteco) | |||
| * MB10 (ZKteco | |||
| 
 | |||
| License | |||
| ------- | |||
| General Public License, Version 3 (AGPL-3). | |||
| (https://www.gnu.org/licenses/agpl-3.0-standalone.html) | |||
| 
 | |||
| Company | |||
| ------- | |||
| * `Cybrosys Techno Solutions <https://cybrosys.com/>`__ | |||
| 
 | |||
| Credits | |||
| ------- | |||
| * Developers: (V17) Mufeeda Shirin, | |||
|               (V16) Nihala KP | |||
| 
 | |||
| 
 | |||
| Contacts | |||
| -------- | |||
| * Mail Contact : odoo@cybrosys.com | |||
| * Website : https://cybrosys.com | |||
| 
 | |||
| Bug Tracker | |||
| ----------- | |||
| Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. | |||
| 
 | |||
| Maintainer | |||
| -------- | |||
| This module is maintained by Cybrosys Technologies. | |||
| 
 | |||
| For support and more information, please visit https://www.cybrosys.com | |||
| 
 | |||
| .. image:: https://cybrosys.com/images/logo.png | |||
|    :target: https://cybrosys.com" | |||
| 
 | |||
| Further Information | |||
| ----------- | |||
| HTML Description: `<static/description/index.html>`__ | |||
| @ -0,0 +1,23 @@ | |||
| # -*- coding: utf-8 -*- | |||
| ################################################################################ | |||
| # | |||
| #    Cybrosys Technologies Pvt. Ltd. | |||
| # | |||
| #    Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). | |||
| #    Author: Cybrosys Techno Solutions (odoo@cybrosys.com) | |||
| # | |||
| #    You can modify it under the terms of the GNU AFFERO | |||
| #    GENERAL PUBLIC LICENSE (AGPL v3), Version 3. | |||
| # | |||
| #    This program is distributed in the hope that it will be useful, | |||
| #    but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | |||
| #    GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. | |||
| # | |||
| #    You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE | |||
| #    (AGPL v3) along with this program. | |||
| #    If not, see <http://www.gnu.org/licenses/>. | |||
| # | |||
| ################################################################################ | |||
| from . import models | |||
| from . import wizards | |||
| @ -0,0 +1,59 @@ | |||
| # -*- coding: utf-8 -*- | |||
| ################################################################################ | |||
| # | |||
| #    Cybrosys Technologies Pvt. Ltd. | |||
| # | |||
| #    Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). | |||
| #    Author: Cybrosys Techno Solutions (odoo@cybrosys.com) | |||
| # | |||
| #    You can modify it under the terms of the GNU AFFERO | |||
| #    GENERAL PUBLIC LICENSE (AGPL v3), Version 3. | |||
| # | |||
| #    This program is distributed in the hope that it will be useful, | |||
| #    but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | |||
| #    GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. | |||
| # | |||
| #    You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE | |||
| #    (AGPL v3) along with this program. | |||
| #    If not, see <http://www.gnu.org/licenses/>. | |||
| # | |||
| ################################################################################ | |||
| { | |||
|     'name': "HR Biometric Device Integration", | |||
|     'version': "16.0.1.0.0", | |||
|     'summary': "Integrating Zk Biometric Devices With HR Attendance", | |||
|     'description': '''This module integrates Odoo with ZK biometric devices,  | |||
|     incorporating features such as live capturing and user management''', | |||
|     'category': 'Human Resources', | |||
|     'author': 'Cybrosys Techno Solutions', | |||
|     'company': 'Cybrosys Techno Solutions', | |||
|     'maintainer': 'Cybrosys Techno Solutions', | |||
|     'website': "https://www.cybrosys.com", | |||
|     'depends': ['base_setup', 'hr_attendance', 'web'], | |||
|     'data': [ | |||
|         'security/ir.model.access.csv', | |||
|         'data/ir_cron_data.xml', | |||
|         'data/ir_action_data.xml', | |||
|         'wizards/user_management_views.xml', | |||
|         'wizards/employee_biometric_views.xml', | |||
|         'views/biometric_device_details_views.xml', | |||
|         'views/hr_employee_views.xml', | |||
|         'views/daily_attendance_views.xml', | |||
|         'views/res_config_settings_views.xml', | |||
|         'views/biometric_device_attendance_menus.xml', | |||
|     ], | |||
|     'assets': { | |||
|         'web.assets_backend': [ | |||
|             'hr_biometric_attendance/static/src/xml/stopwatch_view.xml', | |||
|             'hr_biometric_attendance/static/src/js/stopwatch.js', | |||
|         ] | |||
|     }, | |||
|     'external_dependencies': { | |||
|         'python': ['pyzk'], }, | |||
|     'images': ['static/description/banner.png'], | |||
|     'license': 'AGPL-3', | |||
|     'installable': True, | |||
|     'auto_install': False, | |||
|     'application': False, | |||
| } | |||
| @ -0,0 +1,14 @@ | |||
| <?xml version="1.0" encoding="UTF-8" ?> | |||
| <odoo> | |||
|     <!--    Server actions for Biometric Device in hr.employee--> | |||
|     <record id="biometric_device_user" model="ir.actions.server"> | |||
|         <field name="name">Biometric Device</field> | |||
|         <field name="model_id" ref="model_hr_employee"/> | |||
|         <field name="binding_model_id" ref="model_hr_employee"/> | |||
|         <field name="binding_view_types">form</field> | |||
|         <field name="state">code</field> | |||
|         <field name="code"> | |||
|             action = records.action_biometric_device() | |||
|         </field> | |||
|     </record> | |||
| </odoo> | |||
| @ -0,0 +1,14 @@ | |||
| <?xml version="1.0" encoding="UTF-8" ?> | |||
| <odoo> | |||
|     <!--    Schedule Job for Attendance downloading--> | |||
|     <record id="ir_cron_schedule_attendance_action" model="ir.cron"> | |||
|         <field name="name">Schedule Attendance Downloading</field> | |||
|         <field name="model_id" ref="model_biometric_device_details"/> | |||
|         <field name="state">code</field> | |||
|         <field name="code">model.schedule_attendance()</field> | |||
|         <field name="interval_number">1</field> | |||
|         <field name="interval_type">days</field> | |||
|         <field name="numbercall">-1</field> | |||
|         <field name="doall" eval="False"/> | |||
|     </record> | |||
| </odoo> | |||
| @ -0,0 +1,6 @@ | |||
| ## Module <hr_biometric_attendance> | |||
| 
 | |||
| #### 25.07.2024 | |||
| #### Version 16.0.1.0.0 | |||
| #### ADD | |||
| - Initial commit for HR Biometric Device Integration | |||
| @ -0,0 +1,27 @@ | |||
| # -*- coding: utf-8 -*- | |||
| ################################################################################ | |||
| # | |||
| #    Cybrosys Technologies Pvt. Ltd. | |||
| # | |||
| #    Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). | |||
| #    Author: Cybrosys Techno Solutions (odoo@cybrosys.com) | |||
| # | |||
| #    You can modify it under the terms of the GNU AFFERO | |||
| #    GENERAL PUBLIC LICENSE (AGPL v3), Version 3. | |||
| # | |||
| #    This program is distributed in the hope that it will be useful, | |||
| #    but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | |||
| #    GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. | |||
| # | |||
| #    You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE | |||
| #    (AGPL v3) along with this program. | |||
| #    If not, see <http://www.gnu.org/licenses/>. | |||
| # | |||
| ################################################################################ | |||
| from . import biometric_device_details | |||
| from . import zk_machine_attendance | |||
| from . import daily_attendance | |||
| from . import fingerprint_templates | |||
| from . import hr_employee | |||
| from . import res_config_settings | |||
| @ -0,0 +1,635 @@ | |||
| # -*- coding: utf-8 -*- | |||
| ################################################################################ | |||
| # | |||
| #    Cybrosys Technologies Pvt. Ltd. | |||
| # | |||
| #    Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). | |||
| #    Author: Cybrosys Techno Solutions (odoo@cybrosys.com) | |||
| # | |||
| #    You can modify it under the terms of the GNU AFFERO | |||
| #    GENERAL PUBLIC LICENSE (AGPL v3), Version 3. | |||
| # | |||
| #    This program is distributed in the hope that it will be useful, | |||
| #    but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | |||
| #    GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. | |||
| # | |||
| #    You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE | |||
| #    (AGPL v3) along with this program. | |||
| #    If not, see <http://www.gnu.org/licenses/>. | |||
| # | |||
| ################################################################################ | |||
| import base64 | |||
| import binascii | |||
| import datetime | |||
| import logging | |||
| import threading | |||
| from threading import Thread | |||
| import time | |||
| import pytz | |||
| from zk.exception import ZKErrorResponse | |||
| from odoo import api, fields, models, registry, _ | |||
| from odoo.exceptions import UserError, ValidationError | |||
| live_capture_thread = None | |||
| _logger = logging.getLogger(__name__) | |||
| try: | |||
|     from zk import const, ZK | |||
|     from zk.finger import Finger | |||
| except ImportError: | |||
|     _logger.error("Please Install pyzk library.") | |||
| 
 | |||
| 
 | |||
| class BiometricDeviceDetails(models.Model): | |||
|     """Model for configuring and connect the biometric device with odoo""" | |||
|     _name = 'biometric.device.details' | |||
|     _description = 'Biometric Device Details' | |||
|     _inherit = ['mail.thread', 'mail.activity.mixin'] | |||
| 
 | |||
|     name = fields.Char(string='Name', required=True, help='Record Name') | |||
|     device_ip = fields.Char(string='Device IP', required=True, | |||
|                             help='The IP address of the Device') | |||
|     port_number = fields.Integer(string='Port Number', required=True, | |||
|                                  help="The Port Number of the Device") | |||
|     address_id = fields.Many2one('res.partner', string='Working Address', | |||
|                                  help='Working address of the partner') | |||
|     is_live_capture = fields.Boolean('Live Capturing', | |||
|                                      help="if enabled, gets the live capture " | |||
|                                           "from the device", | |||
|                                      readonly=True) | |||
|     company_id = fields.Many2one('res.company', string='Company', | |||
|                                  default=lambda | |||
|                                      self: self.env.user.company_id.id, | |||
|                                  help='Current Company') | |||
|     stopwatch_time = fields.Float('Stopwatch timer', | |||
|                                   help='Time from Live capture enabled') | |||
|     device_name = fields.Char(String='Device Name', readonly=True, | |||
|                               help='Device Name') | |||
|     device_firmware = fields.Char(String='Device Firmware Version', | |||
|                                   readonly=True, help='Device Firmware') | |||
|     device_serial_no = fields.Char(String='Device Serial No', readonly=True, | |||
|                                    help='Device serial No') | |||
|     device_platform = fields.Char(String='Device Platform', readonly=True, | |||
|                                   help='Device platform') | |||
|     device_mac = fields.Char(String='Device Mac ID', readonly=True, | |||
|                              help='Device Mac') | |||
|     live_capture_start_time = fields.Datetime('Live Capture Time', | |||
|                                               help='The Time When Live ' | |||
|                                                    'Capture Enabled') | |||
| 
 | |||
|     def device_connect(self, zk): | |||
|         """Function for connecting the device with Odoo""" | |||
|         try: | |||
|             conn = zk.connect() | |||
|             return conn | |||
|         except Exception: | |||
|             return False | |||
| 
 | |||
|     def action_test_connection(self): | |||
|         """Checking the connection status""" | |||
|         zk = ZK(self.device_ip, port=self.port_number, timeout=30, | |||
|                 password=False, ommit_ping=False) | |||
|         try: | |||
|             if zk.connect(): | |||
|                 zk.test_voice(index=0) | |||
|                 return { | |||
|                     'type': 'ir.actions.client', | |||
|                     'tag': 'display_notification', | |||
|                     'params': { | |||
|                         'message': 'Successfully Connected', | |||
|                         'type': 'success', | |||
|                         'sticky': False | |||
|                     } | |||
|                 } | |||
| 
 | |||
|         except Exception as error: | |||
|             raise ValidationError(f'{error}') | |||
| 
 | |||
|     def action_clear_attendance(self): | |||
|         """Methode to clear record from the zk.machine.attendance model and | |||
|         from the device""" | |||
|         for info in self: | |||
|             try: | |||
|                 machine_ip = info.device_ip | |||
|                 zk_port = info.port_number | |||
|                 try: | |||
|                     # Connecting with the device | |||
|                     zk = ZK(machine_ip, port=zk_port, timeout=30, | |||
|                             password=0, force_udp=False, ommit_ping=False) | |||
|                 except NameError: | |||
|                     raise UserError(_( | |||
|                         "Please install it with 'pip3 install pyzk'.")) | |||
|                 conn = self.device_connect(zk) | |||
|                 if conn: | |||
|                     conn.enable_device() | |||
|                     clear_data = zk.get_attendance() | |||
|                     if clear_data: | |||
|                         # Clearing data in the device | |||
|                         conn.clear_attendance() | |||
|                         # Clearing data from attendance log | |||
|                         self._cr.execute( | |||
|                             """delete from zk_machine_attendance""") | |||
|                         current_time = fields.datetime.now().strftime( | |||
|                             '%Y-%m-%d %H:%M:%S') | |||
|                         message = (f'Attendances Are cleared from the Device on' | |||
|                                    f' {current_time} By {self.env.user.name}') | |||
|                         self.message_post(body=message) | |||
|                         conn.disconnect() | |||
|                     else: | |||
|                         raise UserError( | |||
|                             _('Unable to clear Attendance log.Are you sure ' | |||
|                               'attendance log is not empty.')) | |||
|                 else: | |||
|                     raise UserError( | |||
|                         _('Unable to connect to Attendance Device. Please use ' | |||
|                           'Test Connection button to verify.')) | |||
|             except Exception as error: | |||
|                 raise ValidationError(f'{error}') | |||
| 
 | |||
|     def action_download_attendance(self): | |||
|         """Function to download attendance records from the device""" | |||
|         _logger.info("++++++++++++Cron Executed++++++++++++++++++++++") | |||
|         zk_attendance = self.env['zk.machine.attendance'] | |||
|         hr_attendance = self.env['hr.attendance'] | |||
|         for info in self: | |||
|             machine_ip = info.device_ip | |||
|             zk_port = info.port_number | |||
|             try: | |||
|                 # Connecting with the device with the ip and port provided | |||
|                 zk = ZK(machine_ip, port=zk_port, timeout=15, | |||
|                         password=0, | |||
|                         force_udp=False, ommit_ping=False) | |||
|             except NameError: | |||
|                 raise UserError( | |||
|                     _("Pyzk module not Found. Please install it" | |||
|                       "with 'pip3 install pyzk'.")) | |||
|             conn = self.device_connect(zk) | |||
|             self.get_device_information() | |||
|             if conn: | |||
|                 conn.disable_device() | |||
|                 self.get_all_users() | |||
|                 self.action_set_timezone() | |||
|                 user = conn.get_users() | |||
|                 # get All Fingerprints | |||
|                 fingers = conn.get_templates() | |||
|                 for use in user: | |||
|                     for finger in fingers: | |||
|                         if finger.uid == use.uid: | |||
|                             templates = conn.get_user_template(uid=use.uid, | |||
|                                                                temp_id=finger.fid, | |||
|                                                                user_id=use.user_id) | |||
|                             hex_data = templates.template.hex() | |||
|                             # Convert hex data to binary | |||
|                             binary_data = binascii.unhexlify(hex_data) | |||
|                             base64_data = base64.b64encode(binary_data).decode( | |||
|                                 'utf-8') | |||
|                             employee = self.env['hr.employee'].search( | |||
|                                 [('device_id_num', '=', use.user_id)]) | |||
|                             employee.write({ | |||
|                                 'device_id': self.id, | |||
|                             }) | |||
|                             if str(finger.fid) in employee.fingerprint_ids.mapped( | |||
|                                     'finger_id'): | |||
|                                 employee.fingerprint_ids.search( | |||
|                                     [('finger_id', '=', finger.fid)]).update({ | |||
|                                     'finger_template': base64_data, | |||
|                                 }) | |||
|                             else: | |||
|                                 employee.fingerprint_ids.create({ | |||
|                                     'finger_template': base64_data, | |||
|                                     'finger_id': finger.fid, | |||
|                                     'employee_id': employee.id, | |||
|                                     'filename': f'{employee.name}-finger-{finger.fid}' | |||
|                                 }) | |||
|                 # get all attendances | |||
|                 attendance = conn.get_attendance() | |||
|                 if attendance: | |||
|                     for each in attendance: | |||
|                         atten_time = each.timestamp | |||
|                         local_tz = pytz.timezone( | |||
|                             self.env.user.partner_id.tz or 'GMT') | |||
|                         local_dt = local_tz.localize(atten_time, is_dst=None) | |||
|                         utc_dt = local_dt.astimezone(pytz.utc) | |||
|                         utc_dt = utc_dt.strftime("%Y-%m-%d %H:%M:%S") | |||
|                         atten_time = datetime.datetime.strptime( | |||
|                             utc_dt, "%Y-%m-%d %H:%M:%S") | |||
|                         atten_time = fields.Datetime.to_string(atten_time) | |||
|                         for uid in user: | |||
|                             if uid.user_id == each.user_id: | |||
|                                 get_user_id = self.env['hr.employee'].search( | |||
|                                     [('device_id_num', '=', each.user_id)]) | |||
|                                 if get_user_id: | |||
|                                     duplicate_atten_ids = zk_attendance.search( | |||
|                                         [('device_id_num', '=', each.user_id), | |||
|                                          ('punching_time', '=', atten_time)]) | |||
|                                     if not duplicate_atten_ids: | |||
|                                         zk_attendance.create({ | |||
|                                             'employee_id': get_user_id.id, | |||
|                                             'device_id_num': each.user_id, | |||
|                                             'attendance_type': str(each.status), | |||
|                                             'punch_type': str(each.punch), | |||
|                                             'punching_time': atten_time, | |||
|                                             'address_id': info.address_id.id | |||
|                                         }) | |||
|                                         att_var = hr_attendance.search([( | |||
|                                             'employee_id', '=', get_user_id.id), | |||
|                                             ('check_out', '=', False)]) | |||
|                                         if each.punch == 0:  # check-in | |||
|                                             if not att_var: | |||
|                                                 hr_attendance.create({ | |||
|                                                     'employee_id': | |||
|                                                         get_user_id.id, | |||
|                                                     'check_in': atten_time | |||
|                                                 }) | |||
|                                         if each.punch == 1:  # check-out | |||
|                                             if len(att_var) == 1: | |||
|                                                 att_var.write({ | |||
|                                                     'check_out': atten_time | |||
|                                                 }) | |||
|                                             else: | |||
|                                                 att_var1 = hr_attendance.search( | |||
|                                                     [('employee_id', '=', | |||
|                                                       get_user_id.id)]) | |||
|                                                 if att_var1: | |||
|                                                     att_var1[-1].write({ | |||
|                                                         'check_out': atten_time | |||
|                                                     }) | |||
|                                 else: | |||
|                                     employee = self.env['hr.employee'].create({ | |||
|                                         'device_id_num': each.user_id, | |||
|                                         'device_id': self.id, | |||
|                                         'name': uid.name | |||
|                                     }) | |||
|                                     zk_attendance.create({ | |||
|                                         'employee_id': employee.id, | |||
|                                         'device_id_num': each.user_id, | |||
|                                         'attendance_type': str(each.status), | |||
|                                         'punch_type': str(each.punch), | |||
|                                         'punching_time': atten_time, | |||
|                                         'address_id': info.address_id.id | |||
|                                     }) | |||
|                                     hr_attendance.create({ | |||
|                                         'employee_id': employee.id, | |||
|                                         'check_in': atten_time | |||
|                                     }) | |||
|                     if not self.is_live_capture: | |||
|                         current_time = fields.datetime.now().strftime( | |||
|                             '%Y-%m-%d %H:%M:%S') | |||
|                         message = (f'Downloaded data from the device on ' | |||
|                                    f'{current_time} by {self.env.user.name}') | |||
|                         self.message_post(body=message) | |||
|                     conn.disconnect() | |||
|                     return True | |||
|                 else: | |||
|                     zk.test_voice(index=4) | |||
|                     raise UserError(_('Unable to get the attendance log, please' | |||
|                                       'try again later.')) | |||
|             else: | |||
|                 raise UserError(_('Unable to connect, please check the' | |||
|                                   'parameters and network connections.')) | |||
| 
 | |||
|     def action_restart_device(self): | |||
|         """For restarting the device""" | |||
|         zk = ZK(self.device_ip, port=self.port_number, timeout=15, | |||
|                 password=0, | |||
|                 force_udp=False, ommit_ping=False) | |||
|         if self.device_connect(zk): | |||
|             if self.is_live_capture: | |||
|                 self.action_stop_live_capture() | |||
|             self.device_connect(zk).restart() | |||
|             return { | |||
|                 'type': 'ir.actions.client', | |||
|                 'tag': 'display_notification', | |||
|                 'params': { | |||
|                     'message': 'Successfully Device Restarted', | |||
|                     'type': 'success', | |||
|                     'sticky': False | |||
|                 } | |||
|             } | |||
|         else: | |||
|             raise UserError(_( | |||
|                 "Please Check the Connection")) | |||
| 
 | |||
|     def schedule_attendance(self): | |||
|         """Schedule action for attendance downloading""" | |||
|         for record in self.search([]): | |||
|             if record.is_live_capture: | |||
|                 record.action_stop_live_capture() | |||
|                 record.action_download_attendance() | |||
|                 record.action_live_capture() | |||
|             else: | |||
|                 record.action_download_attendance() | |||
| 
 | |||
|     def action_live_capture(self): | |||
|         """ Enable Live capture With Thread""" | |||
|         for info in self: | |||
|             machine_ip = info.device_ip | |||
|             zk_port = info.port_number | |||
|             try: | |||
|                 self.is_live_capture = True | |||
|                 self.action_set_timezone() | |||
|                 instance = ZKBioAttendance(machine_ip, zk_port, info) | |||
|                 global live_capture_thread | |||
|                 live_capture_thread = instance | |||
|                 live_capture_thread.start() | |||
|                 self.live_capture_start_time = fields.datetime.now() | |||
|                 return { | |||
|                     'type': 'ir.actions.client', | |||
|                     'tag': 'reload', | |||
|                 } | |||
|             except NameError: | |||
|                 raise UserError(_( | |||
|                     "Please install it with 'pip3 install pyzk'.")) | |||
| 
 | |||
|     def action_stop_live_capture(self): | |||
|         """Function to stop Live capture""" | |||
|         try: | |||
|             self.is_live_capture = False | |||
|             if live_capture_thread: | |||
|                 live_capture_thread.stop() | |||
|             return { | |||
|                 'type': 'ir.actions.client', | |||
|                 'tag': 'reload', | |||
|             } | |||
|         except NameError: | |||
|             raise UserError(_( | |||
|                 "Please install it with 'pip3 install pyzk'.")) | |||
| 
 | |||
|     def action_set_timezone(self): | |||
|         """Function to set user's timezone to device""" | |||
|         for info in self: | |||
|             machine_ip = info.device_ip | |||
|             zk_port = info.port_number | |||
|             try: | |||
|                 # Connecting with the device with the ip and port provided | |||
|                 zk = ZK(machine_ip, port=zk_port, timeout=15, | |||
|                         password=0, | |||
|                         force_udp=False, ommit_ping=False) | |||
|             except NameError: | |||
|                 raise UserError( | |||
|                     _("Pyzk module not Found. Please install it" | |||
|                       "with 'pip3 install pyzk'.")) | |||
|             conn = self.device_connect(zk) | |||
|             if conn: | |||
|                 user_tz = self.env.context.get( | |||
|                     'tz') or self.env.user.tz or 'UTC' | |||
|                 user_timezone_time = pytz.utc.localize(fields.Datetime.now()) | |||
|                 user_timezone_time = user_timezone_time.astimezone( | |||
|                     pytz.timezone(user_tz)) | |||
|                 conn.set_time(user_timezone_time) | |||
|                 return { | |||
|                     'type': 'ir.actions.client', | |||
|                     'tag': 'display_notification', | |||
|                     'params': { | |||
|                         'message': 'Successfully Set the Time', | |||
|                         'type': 'success', | |||
|                         'sticky': False | |||
|                     } | |||
|                 } | |||
|             else: | |||
|                 raise UserError(_( | |||
|                     "Please Check the Connection")) | |||
| 
 | |||
|     def get_all_users(self): | |||
|         """Function to get all user's details""" | |||
|         for info in self: | |||
|             machine_ip = info.device_ip | |||
|             zk_port = info.port_number | |||
|             try: | |||
|                 # Connecting with the device with the ip and port provided | |||
|                 zk = ZK(machine_ip, port=zk_port, timeout=15, | |||
|                         password=0, | |||
|                         force_udp=False, ommit_ping=False) | |||
|             except NameError: | |||
|                 raise UserError( | |||
|                     _("Pyzk module not Found. Please install it" | |||
|                       "with 'pip3 install pyzk'.")) | |||
|             conn = self.device_connect(zk) | |||
|             if conn: | |||
|                 users = conn.get_users() | |||
|                 for user in users: | |||
|                     employee = self.env['hr.employee'].search( | |||
|                         [('device_id_num', '=', user.user_id), | |||
|                          ('device_id', '=', self.id)]) | |||
|                     if employee: | |||
|                         employee.write({ | |||
|                             'name': user.name, | |||
|                         }) | |||
|                     else: | |||
|                         self.env['hr.employee'].create({ | |||
|                             'name': user.name, | |||
|                             'device_id_num': user.user_id, | |||
|                             'device_id': self.id, | |||
|                         }) | |||
|             else: | |||
|                 raise UserError(_( | |||
|                     "Please Check the Connection")) | |||
| 
 | |||
|     def set_user(self, employee_id): | |||
|         """Function to create or update users""" | |||
|         for info in self: | |||
|             machine_ip = info.device_ip | |||
|             zk_port = info.port_number | |||
|             employee = self.env['hr.employee'].browse(int(employee_id)) | |||
|             try: | |||
|                 # Connecting with the device with the ip and port provided | |||
|                 zk = ZK(machine_ip, port=zk_port, timeout=15, | |||
|                         password=0, | |||
|                         force_udp=False, ommit_ping=False) | |||
|             except NameError: | |||
|                 raise UserError( | |||
|                     _("Pyzk module not Found. Please install it" | |||
|                       "with 'pip3 install pyzk'.")) | |||
|             conn = self.device_connect(zk) | |||
|             if conn: | |||
|                 last_user = conn.get_users()[-1] | |||
|                 privilege = 0 | |||
|                 password = '' | |||
|                 group_id = '' | |||
|                 user_id = '' | |||
|                 card = 0 | |||
|                 try: | |||
|                     uids = [user.uid for user in conn.get_users()] | |||
|                     candidate_uid = last_user.uid + 1 | |||
|                     while candidate_uid in uids: | |||
|                         candidate_uid += 1 | |||
|                     conn.set_user(candidate_uid, employee.name, privilege, | |||
|                                   password, group_id, user_id, card) | |||
|                 except ZKErrorResponse as e: | |||
|                     _logger.error("Failed to set user on the device: %s", | |||
|                                   str(e)) | |||
|                 if conn.get_users()[-1].name == employee.name: | |||
|                     employee.write({ | |||
|                         'device_id': self.id, | |||
|                         'device_id_num': conn.get_users()[-1].user_id | |||
|                     }) | |||
|                     current_time = fields.datetime.now().strftime( | |||
|                         '%Y-%m-%d %H:%M:%S') | |||
|                     message = (f'New User {employee.name} Created on ' | |||
|                                f'{current_time} by {self.env.user.name}') | |||
|                     self.message_post(body=message) | |||
|             else: | |||
|                 raise UserError(_( | |||
|                     "Please Check the Connection")) | |||
| 
 | |||
|     def delete_user(self, employee_id, delete_user_selection): | |||
|         """Function to Delete a user""" | |||
|         for info in self: | |||
|             machine_ip = info.device_ip | |||
|             zk_port = info.port_number | |||
|             try: | |||
|                 # Connecting with the device with the ip and port provided | |||
|                 zk = ZK(machine_ip, port=zk_port, timeout=15, | |||
|                         password=0, | |||
|                         force_udp=False, ommit_ping=False) | |||
|             except NameError: | |||
|                 raise UserError( | |||
|                     _("Pyzk module not Found. Please install it" | |||
|                       "with 'pip3 install pyzk'.")) | |||
|             conn = self.device_connect(zk) | |||
|             if conn: | |||
|                 employee = self.env['hr.employee'].browse(int(employee_id)) | |||
|                 employee_name = employee.name | |||
|                 conn.delete_user(uid=None, user_id=employee.device_id_num) | |||
|                 employee.write({ | |||
|                     'device_id_num': False, | |||
|                     'device_id': False | |||
|                 }) | |||
|                 employee.fingerprint_ids.unlink() | |||
|                 if delete_user_selection == 'both_device': | |||
|                     employee.unlink() | |||
|                 current_time = fields.datetime.now().strftime( | |||
|                     '%Y-%m-%d %H:%M:%S') | |||
|                 message = (f'Deleted User {employee_name} on ' | |||
|                            f'{current_time} by {self.env.user.name}') | |||
|                 self.message_post(body=message) | |||
|             else: | |||
|                 raise UserError(_( | |||
|                     "Please Check the Connection")) | |||
| 
 | |||
|     def update_user(self, employee_id): | |||
|         """Function to Update a user""" | |||
|         for info in self: | |||
|             machine_ip = info.device_ip | |||
|             zk_port = info.port_number | |||
|             try: | |||
|                 # Connecting with the device with the ip and port provided | |||
|                 zk = ZK(machine_ip, port=zk_port, timeout=15, | |||
|                         password=0, | |||
|                         force_udp=False, ommit_ping=False) | |||
|             except NameError: | |||
|                 raise UserError( | |||
|                     _("Pyzk module not Found. Please install it" | |||
|                       "with 'pip3 install pyzk'.")) | |||
|             conn = self.device_connect(zk) | |||
|             if conn: | |||
|                 employee = self.env['hr.employee'].browse(int(employee_id)) | |||
|                 for line in conn.get_users(): | |||
|                     if line.user_id == employee.device_id_num: | |||
|                         privilege = 0 | |||
|                         password = '' | |||
|                         group_id = '' | |||
|                         user_id = employee.device_id_num | |||
|                         card = 0 | |||
|                         conn.set_user(line.uid, employee.name, privilege, | |||
|                                       password, group_id, user_id, card) | |||
|                         return { | |||
|                             'type': 'ir.actions.client', | |||
|                             'tag': 'display_notification', | |||
|                             'params': { | |||
|                                 'message': 'Successfully Updated User', | |||
|                                 'type': 'success', | |||
|                                 'sticky': False | |||
|                             } | |||
|                         } | |||
|                 else: | |||
|                     raise UserError(_( | |||
|                         "Please Check the Connection")) | |||
| 
 | |||
|     def get_device_information(self): | |||
|         """Gets device Information""" | |||
|         for info in self: | |||
|             machine_ip = info.device_ip | |||
|             zk_port = info.port_number | |||
|             try: | |||
|                 # Connecting with the device with the ip and port provided | |||
|                 zk = ZK(machine_ip, port=zk_port, timeout=15, | |||
|                         password=0, | |||
|                         force_udp=False, ommit_ping=False) | |||
|             except NameError: | |||
|                 raise UserError( | |||
|                     _("Pyzk module not Found. Please install it" | |||
|                       "with 'pip3 install pyzk'.")) | |||
|             conn = self.device_connect(zk) | |||
|             if conn: | |||
|                 self.device_name = conn.get_device_name() | |||
|                 self.device_firmware = conn.get_firmware_version() | |||
|                 self.device_serial_no = conn.get_serialnumber() | |||
|                 self.device_platform = conn.get_platform() | |||
|                 self.device_mac = conn.get_mac() | |||
|             else: | |||
|                 raise UserError(_( | |||
|                     "Please Check the Connection")) | |||
| 
 | |||
| 
 | |||
| class ZKBioAttendance(Thread): | |||
|     """ | |||
|     Represents a thread for capturing live attendance data from a ZKTeco | |||
|     biometric device. | |||
| 
 | |||
|     Attributes: - machine_ip: The IP address of the ZKTeco biometric device. | |||
|     - port_no: The port number for communication with the ZKTeco biometric | |||
|     device. - conn: The connection object to the ZKTeco biometric device. | |||
| 
 | |||
|     Methods: - run(): Overrides the run method of the Thread class to capture | |||
|     live attendance data. | |||
|     """ | |||
| 
 | |||
|     def __init__(self, machine_ip, port_no, record): | |||
|         """Function to Initialize the thread""" | |||
|         Thread.__init__(self) | |||
|         self.machine_ip = machine_ip | |||
|         self.port_no = port_no | |||
|         self.record = record | |||
|         self.env = record.env | |||
|         self.stop_event = threading.Event() | |||
| 
 | |||
|         zk_device = ZK( | |||
|             machine_ip, | |||
|             port=port_no, | |||
|             timeout=5, | |||
|             password=0, | |||
|             force_udp=False, | |||
|             ommit_ping=False, | |||
|         ) | |||
|         conn = zk_device.connect() | |||
|         if conn: | |||
|             self.conn = conn | |||
|         else: | |||
|             raise UserError(_( | |||
|                 "Please Check the Connection")) | |||
| 
 | |||
|     def run(self): | |||
|         """Function to run the Thread""" | |||
|         while not self.stop_event.is_set(): | |||
|             try: | |||
|                 if not self.conn.end_live_capture: | |||
|                     for attendance in self.conn.live_capture(2000): | |||
|                         self._data_live_capture() | |||
|                 time.sleep(10) | |||
|             except Exception as e: | |||
|                 self.env.cr.rollback()  # Rollback the current transaction | |||
|                 time.sleep(1) | |||
| 
 | |||
|     def stop(self): | |||
|         """Stops the live capture and stops the thread""" | |||
|         if self.conn: | |||
|             self.conn.end_live_capture = True | |||
|         self.stop_event.set() | |||
| 
 | |||
|     def _data_live_capture(self): | |||
|         """Updated the Live Capture real time""" | |||
|         with registry(self.env.cr.dbname).cursor() as new_cr: | |||
|             new_env = api.Environment(new_cr, self.env.uid, self.env.context) | |||
|             if self.conn.get_attendance(): | |||
|                 self.record.with_env(new_env).action_download_attendance() | |||
|             new_cr.commit() | |||
| @ -0,0 +1,73 @@ | |||
| # -*- coding: utf-8 -*- | |||
| ################################################################################ | |||
| # | |||
| #    Cybrosys Technologies Pvt. Ltd. | |||
| # | |||
| #    Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). | |||
| #    Author: Cybrosys Techno Solutions (odoo@cybrosys.com) | |||
| # | |||
| #    You can modify it under the terms of the GNU AFFERO | |||
| #    GENERAL PUBLIC LICENSE (AGPL v3), Version 3. | |||
| # | |||
| #    This program is distributed in the hope that it will be useful, | |||
| #    but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | |||
| #    GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. | |||
| # | |||
| #    You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE | |||
| #    (AGPL v3) along with this program. | |||
| #    If not, see <http://www.gnu.org/licenses/>. | |||
| # | |||
| ################################################################################ | |||
| from odoo import fields, models, tools | |||
| 
 | |||
| 
 | |||
| class DailyAttendance(models.Model): | |||
|     """Model to hold data from the biometric device""" | |||
|     _name = 'daily.attendance' | |||
|     _description = 'Daily Attendance Report' | |||
|     _auto = False | |||
|     _order = 'punching_day desc' | |||
| 
 | |||
|     employee_id = fields.Many2one('hr.employee', string='Employee', | |||
|                                   help='Employee Name') | |||
|     punching_day = fields.Datetime(string='Date', help='Date of punching') | |||
|     address_id = fields.Many2one('res.partner', string='Working Address', | |||
|                                  help='Working address of the employee') | |||
|     attendance_type = fields.Selection([('1', 'Finger'), ('15', 'Face'), | |||
|                                         ('2', 'Type_2'), ('3', 'Password'), | |||
|                                         ('4', 'Card')], string='Category', | |||
|                                        help='Attendance detecting methods') | |||
|     punch_type = fields.Selection([('0', 'Check In'), ('1', 'Check Out'), | |||
|                                    ('2', 'Break Out'), ('3', 'Break In'), | |||
|                                    ('4', 'Overtime In'), ('5', 'Overtime Out')], | |||
|                                   string='Punching Type', | |||
|                                   help='The Punching Type of attendance') | |||
|     punching_time = fields.Datetime(string='Punching Time', | |||
|                                     help='Punching time in the device') | |||
| 
 | |||
|     def init(self): | |||
|         """Retrieve the data's for attendance report""" | |||
|         tools.drop_view_if_exists(self._cr, 'daily_attendance') | |||
|         query = """ | |||
|                 create or replace view daily_attendance as ( | |||
|                     select | |||
|                         min(z.id) as id, | |||
|                         z.employee_id as employee_id, | |||
|                         z.write_date as punching_day, | |||
|                         z.address_id as address_id, | |||
|                         z.attendance_type as attendance_type, | |||
|                         z.punching_time as punching_time, | |||
|                         z.punch_type as punch_type | |||
|                     from zk_machine_attendance z | |||
|                         join hr_employee e on (z.employee_id=e.id) | |||
|                     GROUP BY | |||
|                         z.employee_id, | |||
|                         z.write_date, | |||
|                         z.address_id, | |||
|                         z.attendance_type, | |||
|                         z.punch_type, | |||
|                         z.punching_time | |||
|                 ) | |||
|             """ | |||
|         self._cr.execute(query) | |||
| @ -0,0 +1,37 @@ | |||
| # -*- coding: utf-8 -*- | |||
| ################################################################################ | |||
| # | |||
| #    Cybrosys Technologies Pvt. Ltd. | |||
| # | |||
| #    Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). | |||
| #    Author: Cybrosys Techno Solutions (odoo@cybrosys.com) | |||
| # | |||
| #    You can modify it under the terms of the GNU AFFERO | |||
| #    GENERAL PUBLIC LICENSE (AGPL v3), Version 3. | |||
| # | |||
| #    This program is distributed in the hope that it will be useful, | |||
| #    but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | |||
| #    GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. | |||
| # | |||
| #    You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE | |||
| #    (AGPL v3) along with this program. | |||
| #    If not, see <http://www.gnu.org/licenses/>. | |||
| # | |||
| ################################################################################ | |||
| from odoo import fields, models | |||
| 
 | |||
| 
 | |||
| class FingerprintTemplates(models.Model): | |||
|     """Inherit the model to add field""" | |||
|     _name = 'fingerprint.templates' | |||
|     _description = 'Finger Print Templates for Employee' | |||
| 
 | |||
|     employee_id = fields.Many2one('hr.employee', string='Employee', | |||
|                                   help='The Employee ') | |||
|     finger_id = fields.Char(string='Finger Id', | |||
|                             help='The Number that refers the Finger') | |||
|     filename = fields.Char(string='Finger File Name', | |||
|                            help='File Name of the Uploaded Finger Print') | |||
|     finger_template = fields.Binary(string='Finger Template', | |||
|                                     help='The Uploaded Finger Print file') | |||
| @ -0,0 +1,48 @@ | |||
| # -*- coding: utf-8 -*- | |||
| ################################################################################ | |||
| # | |||
| #    Cybrosys Technologies Pvt. Ltd. | |||
| # | |||
| #    Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). | |||
| #    Author: Cybrosys Techno Solutions (odoo@cybrosys.com) | |||
| # | |||
| #    You can modify it under the terms of the GNU AFFERO | |||
| #    GENERAL PUBLIC LICENSE (AGPL v3), Version 3. | |||
| # | |||
| #    This program is distributed in the hope that it will be useful, | |||
| #    but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | |||
| #    GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. | |||
| # | |||
| #    You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE | |||
| #    (AGPL v3) along with this program. | |||
| #    If not, see <http://www.gnu.org/licenses/>. | |||
| # | |||
| ################################################################################ | |||
| from odoo import fields, models, _ | |||
| 
 | |||
| 
 | |||
| class HrEmployee(models.Model): | |||
|     """Inherit the model to add field""" | |||
|     _inherit = 'hr.employee' | |||
| 
 | |||
|     device_id_num = fields.Char(string='Biometric Device ID', | |||
|                                 help="Give the biometric device id", copy=False) | |||
|     device_id = fields.Many2one('biometric.device.details', copy=False, | |||
|                                 readonly=True, | |||
|                                 help='The biometric device details') | |||
|     fingerprint_ids = fields.One2many('fingerprint.templates', 'employee_id', | |||
|                                       help='Store finger print templates of ' | |||
|                                            'an employee') | |||
| 
 | |||
|     def action_biometric_device(self): | |||
|         """Server Action for Biometric Device which open a wizard with | |||
|         several options""" | |||
|         return { | |||
|             'type': 'ir.actions.act_window', | |||
|             'target': 'new', | |||
|             'name': _('Biometric Management'), | |||
|             'view_mode': 'form', | |||
|             'res_model': 'employee.biometric', | |||
|             'context': {'default_employee_id': self.id}, | |||
|         } | |||
| @ -0,0 +1,68 @@ | |||
| # -*- coding: utf-8 -*- | |||
| ################################################################################ | |||
| # | |||
| #    Cybrosys Technologies Pvt. Ltd. | |||
| # | |||
| #    Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). | |||
| #    Author: Cybrosys Techno Solutions (odoo@cybrosys.com) | |||
| # | |||
| #    You can modify it under the terms of the GNU AFFERO | |||
| #    GENERAL PUBLIC LICENSE (AGPL v3), Version 3. | |||
| # | |||
| #    This program is distributed in the hope that it will be useful, | |||
| #    but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | |||
| #    GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. | |||
| # | |||
| #    You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE | |||
| #    (AGPL v3) along with this program. | |||
| #    If not, see <http://www.gnu.org/licenses/>. | |||
| # | |||
| ################################################################################ | |||
| from datetime import timedelta | |||
| from odoo import fields, models | |||
| 
 | |||
| 
 | |||
| class ResConfigSettings(models.TransientModel): | |||
|     """ Inherited res.config.settings to add new fields """ | |||
|     _inherit = 'res.config.settings' | |||
| 
 | |||
|     schedule_attendance_downloads = fields.Boolean(string="Schedule Downloads", | |||
|                                                    config_parameter='hr_biometric_attendance.schedule_downloads', | |||
|                                                    default=False, | |||
|                                                    help='If Enabled we can ' | |||
|                                                         'schedule attendance ' | |||
|                                                         'downloading from ' | |||
|                                                         'device') | |||
|     schedule_time_interval = fields.Integer(string="Schedule Time Interval", | |||
|                                             config_parameter='hr_biometric_attendance.schedule_time_interval', | |||
|                                             default=1, | |||
|                                             help='We can set Time interval ' | |||
|                                                  'for the Scheduling') | |||
|     schedule_time_period = fields.Selection( | |||
|         selection=[('hours', 'Hours'), ('days', 'Days')], | |||
|         string="Schedule Time Period", | |||
|         config_parameter='hr_biometric_attendance.schedule_time_period', | |||
|         default='days', help='We can set Time Period for the Scheduling') | |||
| 
 | |||
|     def set_values(self): | |||
|         """ Super the function to set  the values from settings to the | |||
|         cron.job""" | |||
|         super().set_values() | |||
|         if self.schedule_attendance_downloads: | |||
|             self.env['ir.cron'].search( | |||
|                 [('name', '=', 'Schedule Attendance Downloading')]).write({ | |||
|                     'active': True, | |||
|                     'interval_type': self.schedule_time_period, | |||
|                     'interval_number': self.schedule_time_interval, | |||
|                     'nextcall': fields.datetime.now() + timedelta( | |||
|                         hours=self.schedule_time_interval) if | |||
|                     self.schedule_time_period == 'hours' else | |||
|                     fields.datetime.now() + timedelta( | |||
|                         days=self.schedule_time_interval), | |||
|                 }) | |||
|         else: | |||
|             self.env['ir.cron'].search( | |||
|                 [('name', '=', 'Schedule Attendance Downloading')]).write({ | |||
|                     'active': False | |||
|                 }) | |||
| @ -0,0 +1,52 @@ | |||
| # -*- coding: utf-8 -*- | |||
| ################################################################################ | |||
| # | |||
| #    Cybrosys Technologies Pvt. Ltd. | |||
| # | |||
| #    Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). | |||
| #    Author: Cybrosys Techno Solutions (odoo@cybrosys.com) | |||
| # | |||
| #    You can modify it under the terms of the GNU AFFERO | |||
| #    GENERAL PUBLIC LICENSE (AGPL v3), Version 3. | |||
| # | |||
| #    This program is distributed in the hope that it will be useful, | |||
| #    but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | |||
| #    GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. | |||
| # | |||
| #    You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE | |||
| #    (AGPL v3) along with this program. | |||
| #    If not, see <http://www.gnu.org/licenses/>. | |||
| # | |||
| ################################################################################ | |||
| from odoo import api, fields, models | |||
| 
 | |||
| 
 | |||
| class ZkMachineAttendance(models.Model): | |||
|     """Model to hold data from the biometric device""" | |||
|     _name = 'zk.machine.attendance' | |||
|     _description = 'Attendance' | |||
|     _inherit = 'hr.attendance' | |||
| 
 | |||
|     @api.constrains('check_in', 'check_out', 'employee_id') | |||
|     def _check_validity(self): | |||
|         """Overriding the __check_validity function for employee attendance.""" | |||
|         pass | |||
| 
 | |||
|     device_id_num = fields.Char(string='Biometric Device ID', | |||
|                                 help="The ID of the Biometric Device") | |||
|     punch_type = fields.Selection([('0', 'Check In'), ('1', 'Check Out'), | |||
|                                    ('2', 'Break Out'), ('3', 'Break In'), | |||
|                                    ('4', 'Overtime In'), ('5', 'Overtime Out'), | |||
|                                    ('255', 'Duplicate')], | |||
|                                   string='Punching Type', | |||
|                                   help='Punching type of the attendance') | |||
|     attendance_type = fields.Selection([('1', 'Finger'), ('15', 'Face'), | |||
|                                         ('2', 'Type_2'), ('3', 'Password'), | |||
|                                         ('4', 'Card'), ('255', 'Duplicate')], | |||
|                                        string='Category', | |||
|                                        help="Attendance detecting methods") | |||
|     punching_time = fields.Datetime(string='Punching Time', | |||
|                                     help="Punching time in the device") | |||
|     address_id = fields.Many2one('res.partner', string='Working Address', | |||
|                                  help="Working address of the employee") | |||
| 
 | 
| After Width: | Height: | Size: 28 KiB | 
| After Width: | Height: | Size: 3.6 KiB | 
| After Width: | Height: | Size: 310 B | 
| After Width: | Height: | Size: 1.3 KiB | 
| After Width: | Height: | Size: 1.4 KiB | 
| After Width: | Height: | Size: 576 B | 
| After Width: | Height: | Size: 733 B | 
| After Width: | Height: | Size: 4.3 KiB | 
| After Width: | Height: | Size: 4.0 KiB | 
| After Width: | Height: | Size: 911 B | 
| After Width: | Height: | Size: 1.1 KiB | 
| After Width: | Height: | Size: 1.2 KiB | 
| After Width: | Height: | Size: 673 B | 
| After Width: | Height: | Size: 2.0 KiB | 
| After Width: | Height: | Size: 878 B | 
| After Width: | Height: | Size: 653 B | 
| After Width: | Height: | Size: 905 B | 
| After Width: | Height: | Size: 4.3 KiB | 
| After Width: | Height: | Size: 839 B | 
| After Width: | Height: | Size: 5.9 KiB | 
| After Width: | Height: | Size: 34 KiB | 
| After Width: | Height: | Size: 3.8 KiB | 
| After Width: | Height: | Size: 1.9 KiB | 
| After Width: | Height: | Size: 427 B | 
| After Width: | Height: | Size: 627 B | 
| After Width: | Height: | Size: 1.2 KiB | 
| After Width: | Height: | Size: 988 B | 
| After Width: | Height: | Size: 5.0 KiB | 
| After Width: | Height: | Size: 1.2 KiB | 
| After Width: | Height: | Size: 1.5 KiB | 
| After Width: | Height: | Size: 1.1 KiB | 
| After Width: | Height: | Size: 1.9 KiB | 
| After Width: | Height: | Size: 1.1 KiB | 
| After Width: | Height: | Size: 2.1 KiB | 
| After Width: | Height: | Size: 4.4 KiB | 
| After Width: | Height: | Size: 589 B | 
| After Width: | Height: | Size: 3.4 KiB | 
| After Width: | Height: | Size: 1.7 KiB | 
| After Width: | Height: | Size: 2.3 KiB | 
| After Width: | Height: | Size: 967 B | 
| After Width: | Height: | Size: 1.6 KiB | 
| After Width: | Height: | Size: 3.8 KiB | 
| After Width: | Height: | Size: 5.0 KiB | 
| After Width: | Height: | Size: 83 KiB | 
| After Width: | Height: | Size: 95 KiB | 
| After Width: | Height: | Size: 95 KiB | 
| After Width: | Height: | Size: 84 KiB | 
| After Width: | Height: | Size: 80 KiB | 
| After Width: | Height: | Size: 75 KiB | 
| After Width: | Height: | Size: 758 KiB | 
| After Width: | Height: | Size: 168 KiB | 
| After Width: | Height: | Size: 163 KiB | 
| After Width: | Height: | Size: 169 KiB | 
| After Width: | Height: | Size: 118 KiB | 
| After Width: | Height: | Size: 127 KiB | 
| After Width: | Height: | Size: 160 KiB | 
| After Width: | Height: | Size: 124 KiB | 
| After Width: | Height: | Size: 125 KiB | 
| After Width: | Height: | Size: 145 KiB | 
| After Width: | Height: | Size: 150 KiB | 
| After Width: | Height: | Size: 115 KiB | 
| After Width: | Height: | Size: 99 KiB | 
| After Width: | Height: | Size: 144 KiB | 
| After Width: | Height: | Size: 117 KiB | 
| After Width: | Height: | Size: 146 KiB | 
| After Width: | Height: | Size: 164 KiB | 
| After Width: | Height: | Size: 162 KiB | 
| After Width: | Height: | Size: 162 KiB | 
| After Width: | Height: | Size: 163 KiB | 
| After Width: | Height: | Size: 670 KiB | 
| After Width: | Height: | Size: 72 KiB | 
| After Width: | Height: | Size: 12 KiB | 
| @ -0,0 +1,62 @@ | |||
| /** @odoo-module **/ | |||
| import { registry } from '@web/core/registry'; | |||
| import { parseFloatTime } from '@web/views/fields/parsers'; | |||
| import { useInputField } from '@web/views/fields/input_field_hook'; | |||
| import { Component, useState, onMounted } from '@odoo/owl'; | |||
| 
 | |||
| // Function to format minutes into HH:MM:SS format
 | |||
| function formatMinutes(value) { | |||
|     if (value === false) { | |||
|         return ""; | |||
|     } | |||
|     const isNegative = value < 0; | |||
|     if (isNegative) { | |||
|         value = Math.abs(value); | |||
|     } | |||
|     let hours = Math.floor(value / 60); | |||
|     let minutes = Math.floor(value % 60); | |||
|     let seconds = Math.floor((value % 1) * 60); | |||
|     seconds = `${seconds}`.padStart(2, "0"); | |||
|     minutes = `${minutes}`.padStart(2, "0"); | |||
|     return `${isNegative ? "-" : ""}${hours}:${minutes}:${seconds}`; | |||
| } | |||
| export class StopWatch extends Component { | |||
|     static template = "StopwatchTemplate"; | |||
|     setup() { | |||
|         this.state = useState({ | |||
|             stopwatch: 0, | |||
|             livecapture: this.props.record.data.is_live_capture | |||
|         }); | |||
| 
 | |||
|         useInputField({ | |||
|             getValue: () => this.durationFormatted, | |||
|             refName: "numpadDecimal", | |||
|             parse: (v) => parseFloatTime(v), | |||
|         }); | |||
| 
 | |||
|         onMounted(async () => { | |||
|             if (this.state.livecapture) { | |||
|                 const datetimeObj = new Date(this.props.record.data.live_capture_start_time); | |||
|                 const now = new Date(); | |||
|                 const timeDiff = now - datetimeObj; | |||
|                 this.state.stopwatch = timeDiff / 1000 / 60; | |||
|                 this._runTimer(); | |||
|             } | |||
|         }); | |||
|     } | |||
|     get durationFormatted() { | |||
|         return formatMinutes(this.state.stopwatch); | |||
|     } | |||
|     _runTimer() { | |||
|         if (!this.state.livecapture) { | |||
|             clearTimeout(this.timer); | |||
|             return; | |||
|         } | |||
|         this.timer = setTimeout(async () => { | |||
|             this.state.stopwatch += 1 / 60; | |||
|             this._runTimer(); | |||
|         }, 1000); | |||
|     } | |||
| } | |||
| registry.category("fields").add("stopwatch", StopWatch); | |||
| registry.category("formatters").add("stopwatch", formatMinutes); | |||
| @ -0,0 +1,10 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <!-- Template of the Stopwatch widget --> | |||
| <templates id="template" xml:space="preserve"> | |||
|     <t t-name="StopwatchTemplate" owl="1"> | |||
|         <span t-if="props.readonly" t-esc="durationFormatted" class="disable"/> | |||
|         <input t-else="" t-att-id="props.id" t-ref="numpadDecimal" t-model="state.stopwatch" | |||
|                t-att-placeholder="props.placeholder" inputmode="numeric" | |||
|                class="o_input timer" readonly="1"  /> | |||
|     </t> | |||
| </templates> | |||
| @ -0,0 +1,16 @@ | |||
| <?xml version="1.0" encoding="UTF-8" ?> | |||
| <odoo> | |||
|     <!--    Defined all menus here --> | |||
|     <menuitem id="biometric_device_details_menu" | |||
|               name="Biometric Device" | |||
|               parent="hr_attendance.menu_hr_attendance_root" | |||
|               sequence="21"/> | |||
|     <menuitem id="biometric_device_details_sub_menu" | |||
|               action="biometric_device_details_action" | |||
|               parent="biometric_device_details_menu" | |||
|               sequence="21"/> | |||
|     <menuitem id="daily_attendance_menu" | |||
|               action="daily_attendance_action" | |||
|               parent="biometric_device_details_menu" | |||
|               groups="hr_attendance.group_hr_attendance_user"/> | |||
| </odoo> | |||
| @ -0,0 +1,88 @@ | |||
| <?xml version="1.0" encoding="UTF-8" ?> | |||
| <odoo> | |||
|     <!--Biometric device configuration tree view--> | |||
|     <record id="biometric_device_details_view_tree" model="ir.ui.view"> | |||
|         <field name="name">biometric.device.details.view.tree</field> | |||
|         <field name="model">biometric.device.details</field> | |||
|         <field name="arch" type="xml"> | |||
|             <tree> | |||
|                 <field name="name"/> | |||
|                 <field name="device_ip"/> | |||
|                 <field name="port_number"/> | |||
|             </tree> | |||
|         </field> | |||
|     </record> | |||
|     <!--Biometric device configuration tree view--> | |||
|     <record id="biometric_device_details_view_form" model="ir.ui.view"> | |||
|         <field name="name">biometric.device.details.view.form</field> | |||
|         <field name="model">biometric.device.details</field> | |||
|         <field name="arch" type="xml"> | |||
|             <form> | |||
|                 <header> | |||
|                     <button name="action_download_attendance" | |||
|                             string="Download Data" | |||
|                             type="object" class="oe_highlight" attrs="{'invisible':[('is_live_capture','=',True)]}"/> | |||
|                     <button name="action_clear_attendance" string="Clear Data" | |||
|                             type="object" class="oe_highlight" | |||
|                             attrs="{'invisible':[('is_live_capture','=',True)]}"/> | |||
|                     <button name="action_restart_device" string="Restart" | |||
|                             type="object" class="oe_highlight" | |||
|                             confirm="Are you sure you want Restart the Biometric Device?"/> | |||
|                     <button name="action_live_capture" string="Live Capture" | |||
|                             type="object" class="oe_highlight" attrs="{'invisible':[('is_live_capture','=',True)]}"/> | |||
|                     <button name="action_stop_live_capture" string=" Stop Live Capture" | |||
|                             type="object" class="btn btn-secondary" attrs="{'invisible':[('is_live_capture','=',False)]}"/> | |||
|                     <button name="action_set_timezone" string=" Set Time" | |||
|                             type="object" class="oe_highlight"/> | |||
|                     <button name="%(hr_biometric_attendance.action_view_zk_user_management)d" string="User Management" | |||
|                             type="action" class="oe_highlight"/> | |||
|                 </header> | |||
|                 <sheet> | |||
|                     <div> | |||
|                         <label for="is_live_capture" name="is_live_capture" string="Live Capture"/> | |||
|                         <field name="is_live_capture" widget="boolean_toggle" string="Live Capture"/> | |||
|                         <field name="stopwatch_time" widget="stopwatch" readonly="True" | |||
|                                style="font-size: 15px; font-weight: 600;font-family: monospace;" | |||
|                                attrs="{'invisible':[('is_live_capture','=',False)]}"/> | |||
|                         <field name="live_capture_start_time" invisible="1"/> | |||
| 
 | |||
|                     </div> | |||
|                     <group> | |||
|                         <field name="name"/> | |||
|                         <field name="device_ip"/> | |||
|                         <field name="port_number"/> | |||
|                         <field name="address_id"/> | |||
| 
 | |||
|                     </group> | |||
|                     <button name="action_test_connection" | |||
|                             type="object" class="btn btn-secondary"> | |||
|                         <i class="fa fa-fw o_button_icon fa-television"/> | |||
|                         Test Connection | |||
|                     </button> | |||
|                     <notebook> | |||
|                         <page string="Device Information"> | |||
|                             <group> | |||
|                                 <field name="device_name"/> | |||
|                                 <field name="device_firmware"/> | |||
|                                 <field name="device_serial_no"/> | |||
|                                 <field name="device_platform"/> | |||
|                                 <field name="device_mac"/> | |||
|                             </group> | |||
|                         </page> | |||
|                     </notebook> | |||
|                 </sheet> | |||
|                 <div class="oe_chatter"> | |||
|                     <field name="message_follower_ids" groups="base.group_user"/> | |||
|                     <field name="activity_ids"/> | |||
|                     <field name="message_ids"/> | |||
|                 </div> | |||
|             </form> | |||
|         </field> | |||
|     </record> | |||
|     <!--	Action for the biometric device--> | |||
|     <record id="biometric_device_details_action" model="ir.actions.act_window"> | |||
|         <field name="name">Biometric Device</field> | |||
|         <field name="res_model">biometric.device.details</field> | |||
|         <field name="view_mode">tree,form</field> | |||
|     </record> | |||
| </odoo> | |||
| @ -0,0 +1,25 @@ | |||
| <?xml version="1.0" encoding="UTF-8" ?> | |||
| <odoo> | |||
|     <!--    Daily attendance tree view--> | |||
|     <record id="daily_attendance_view_tree" model="ir.ui.view"> | |||
|         <field name="name">daily.attendance.view.tree</field> | |||
|         <field name="model">daily.attendance</field> | |||
|         <field name="arch" type="xml"> | |||
|             <tree string="Attendance" create="false" delete="false"> | |||
|                 <field name="punching_day"/> | |||
|                 <field name="employee_id"/> | |||
|                 <field name="punch_type"/> | |||
|                 <field name="attendance_type"/> | |||
|                 <field name="punching_time"/> | |||
|                 <field name="address_id"/> | |||
|             </tree> | |||
|         </field> | |||
|     </record> | |||
|     <!--    Attendance analysis action--> | |||
|     <record id="daily_attendance_action" model="ir.actions.act_window"> | |||
|         <field name="name">Attendance Analysis</field> | |||
|         <field name="res_model">daily.attendance</field> | |||
|         <field name="view_mode">tree</field> | |||
|         <field name="context">{}</field> | |||
|     </record> | |||
| </odoo> | |||
| @ -0,0 +1,25 @@ | |||
| <?xml version="1.0" encoding="UTF-8" ?> | |||
| <odoo> | |||
|     <!--    Inherited hr employee for adding new field--> | |||
|     <record id="view_employee_form" model="ir.ui.view"> | |||
|         <field name="name">hr.employee.view.form.inherit.hr.zk.attendance | |||
|         </field> | |||
|         <field name="model">hr.employee</field> | |||
|         <field name="inherit_id" ref="hr.view_employee_form"/> | |||
|         <field name="arch" type="xml"> | |||
|             <xpath expr="//page[@name='hr_settings']//field[@name='user_id']" | |||
|                    position="after"> | |||
|                 <field name="device_id_num" readonly="1"/> | |||
|                 <field name="device_id"/> | |||
|                 <field name="fingerprint_ids" attrs="{'invisible':[('device_id','=',False)]}"> | |||
|                     <tree editable="bottom" create="false"> | |||
|                         <field name="employee_id" column_invisible="1"/> | |||
|                         <field name="filename" column_invisible="1"/> | |||
|                         <field name="finger_id" create="false" edit="false"/> | |||
|                         <field name="finger_template" create="false" widget="binary" filename="filename"/> | |||
|                     </tree> | |||
|                 </field> | |||
|             </xpath> | |||
|         </field> | |||
|     </record> | |||
| </odoo> | |||
| @ -0,0 +1,52 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | |||
| <odoo> | |||
|     <!--    Adding fields in res.config.settings--> | |||
|     <record id="res_config_settings_view_form" model="ir.ui.view"> | |||
|         <field name="name"> | |||
|             res.config.settings.view.form.inherit.hr.biometric.attendance | |||
|         </field> | |||
|         <field name="model">res.config.settings</field> | |||
|         <field name="inherit_id" | |||
|                ref="hr_attendance.res_config_settings_view_form"/> | |||
|         <field name="arch" type="xml"> | |||
|             <xpath expr="//div[@name='overtime_settings']" position="after"> | |||
|                 <h2>Biometric Device</h2> | |||
|                 <div class="row mt16 o_settings_container" name="schedule_downloads"> | |||
|                     <div class="col-12 col-lg-6 o_setting_box"> | |||
|                         <div class="o_setting_left_pane" | |||
|                              title="Activate the count of employees' extra hours."> | |||
|                             <field name="schedule_attendance_downloads"/> | |||
|                         </div> | |||
|                         <div class="o_setting_right_pane"> | |||
|                             <label for="hr_attendance_overtime" string="Schedule Downloads" | |||
|                                    class="o_form_label">Schedule Downloads | |||
|                             </label> | |||
|                             <div class="text-muted"> | |||
|                                 If Schedule Download Enabled, You will get all | |||
|                                 the attendance and details from the Biometric | |||
|                                 device into your Odoo | |||
|                             </div> | |||
|                             <div class="mt16" attrs="{'invisible': [('schedule_attendance_downloads', '=', False)], | |||
|                                                           'required': [('schedule_attendance_downloads', '=', True)]}"> | |||
|                                 <div class="mt16 row" | |||
|                                      title="Count of extra hours is considered from this date. Potential extra hours prior to this date are not considered."> | |||
|                                     <label for="schedule_time_interval" | |||
|                                            string="Time Interval" | |||
|                                            class="o_light_label col-lg-4"/> | |||
|                                     <field name="schedule_time_interval" | |||
|                                            class="col-lg-2 w-30 " | |||
|                                            style="width:90px;" | |||
|                                            attrs="{'required':[('schedule_attendance_downloads','=',True)]}"/> | |||
|                                     <field name="schedule_time_period" | |||
|                                            class="col-lg-2 w-30" | |||
|                                            style="width:190px;" | |||
|                                            attrs="{'required':[('schedule_attendance_downloads','=',True)]}"/> | |||
|                                 </div> | |||
|                             </div> | |||
|                         </div> | |||
|                     </div> | |||
|                 </div> | |||
|             </xpath> | |||
|         </field> | |||
|     </record> | |||
| </odoo> | |||
| @ -0,0 +1,23 @@ | |||
| # -*- coding: utf-8 -*- | |||
| ################################################################################ | |||
| # | |||
| #    Cybrosys Technologies Pvt. Ltd. | |||
| # | |||
| #    Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). | |||
| #    Author: Cybrosys Techno Solutions (odoo@cybrosys.com) | |||
| # | |||
| #    You can modify it under the terms of the GNU AFFERO | |||
| #    GENERAL PUBLIC LICENSE (AGPL v3), Version 3. | |||
| # | |||
| #    This program is distributed in the hope that it will be useful, | |||
| #    but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | |||
| #    GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. | |||
| # | |||
| #    You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE | |||
| #    (AGPL v3) along with this program. | |||
| #    If not, see <http://www.gnu.org/licenses/>. | |||
| # | |||
| ################################################################################ | |||
| from . import employee_biometric | |||
| from . import user_management | |||
| @ -0,0 +1,66 @@ | |||
| # -*- coding: utf-8 -*- | |||
| ################################################################################ | |||
| # | |||
| #    Cybrosys Technologies Pvt. Ltd. | |||
| # | |||
| #    Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). | |||
| #    Author: Cybrosys Techno Solutions (odoo@cybrosys.com) | |||
| # | |||
| #    You can modify it under the terms of the GNU AFFERO | |||
| #    GENERAL PUBLIC LICENSE (AGPL v3), Version 3. | |||
| # | |||
| #    This program is distributed in the hope that it will be useful, | |||
| #    but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | |||
| #    GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. | |||
| # | |||
| #    You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE | |||
| #    (AGPL v3) along with this program. | |||
| #    If not, see <http://www.gnu.org/licenses/>. | |||
| # | |||
| ################################################################################ | |||
| from odoo import api, fields, models | |||
| 
 | |||
| 
 | |||
| class EmployeeBiometric(models.TransientModel): | |||
|     """Transient model for Biometric device options in Employee""" | |||
|     _name = 'employee.biometric' | |||
|     _description = 'Employee Biometric Wizard' | |||
| 
 | |||
|     handle_create = fields.Selection( | |||
|         [('create_user', 'Create User')], | |||
|         'Handle Data', help='User Management', ) | |||
|     handle_update_delete = fields.Selection( | |||
|         [('update_user', 'Update User'), ('delete_user', 'Delete User')], | |||
|         'Handle Data', help='User Management', ) | |||
|     employee_id = fields.Many2one( | |||
|         'hr.employee', string='Employee', help='Select the Employee') | |||
|     is_biometric_user = fields.Boolean('Is Already User?', | |||
|                                        help='Checking if already a user?', | |||
|                                        compute='_compute_is_biometric_user') | |||
|     biometric_device_id = fields.Many2one('biometric.device.details', | |||
|                                           string='Biometric Device', | |||
|                                           help='Choose Biometric Device') | |||
| 
 | |||
|     @api.depends('employee_id') | |||
|     def _compute_is_biometric_user(self): | |||
|         """Compute if it is already a biometric user or not""" | |||
|         for record in self: | |||
|             if record.employee_id.device_id: | |||
|                 record.is_biometric_user = True | |||
|             else: | |||
|                 record.is_biometric_user = False | |||
| 
 | |||
|     def action_confirm_biometric_management(self): | |||
|         """Go to the desired functions in biometric.device.details""" | |||
|         if self.is_biometric_user: | |||
|             if self.handle_update_delete == 'update_user': | |||
|                 self.employee_id.device_id.update_user( | |||
|                     employee_id=self.employee_id.id) | |||
|             else: | |||
|                 self.employee_id.device_id.delete_user( | |||
|                     employee_id=self.employee_id.id, | |||
|                     delete_user_selection='device_only') | |||
|         else: | |||
|             self.employee_id.device_id = self.biometric_device_id.id | |||
|             self.biometric_device_id.set_user(employee_id=self.employee_id.id) | |||
| @ -0,0 +1,40 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <odoo> | |||
|     <!--    Action for Employee Biometric Wizard--> | |||
|     <record id="action_view_employee_biometric" model="ir.actions.act_window"> | |||
|         <field name="name">Biometric Management</field> | |||
|         <field name="res_model">employee.biometric</field> | |||
|         <field name="view_mode">form</field> | |||
|         <field name="target">new</field> | |||
|     </record> | |||
|     <!-- Define a form view for the Employee Biometric Wizard model --> | |||
|     <record id="employee_biometric_view_form" model="ir.ui.view"> | |||
|         <field name="name">employee.biometric.view.form</field> | |||
|         <field name="model">employee.biometric</field> | |||
|         <field name="arch" type="xml"> | |||
|             <form> | |||
|                 <group> | |||
|                     <group> | |||
|                         <field name="handle_create" | |||
|                                attrs="{'invisible':[('is_biometric_user','=',True)],'required':[('is_biometric_user','=',False)]}" | |||
|                         /> | |||
|                         <field name="biometric_device_id" | |||
|                                attrs="{'invisible':[('handle_create', '!=', 'create_user')]}"/> | |||
|                         <field name="handle_update_delete" | |||
|                                attrs="{'invisible':[('is_biometric_user','=', False)],'required':[('is_biometric_user','=',True)]}" | |||
|                         /> | |||
|                         <field name="employee_id" invisible="1"/> | |||
|                         <field name="is_biometric_user" invisible="1"/> | |||
|                     </group> | |||
|                 </group> | |||
|                 <footer> | |||
|                     <button class="btn btn-primary" string="Confirm" | |||
|                             name="action_confirm_biometric_management" | |||
|                             type="object"/> | |||
|                     <button class="btn btn-secondary" string="DISCARD" | |||
|                             special="cancel"/> | |||
|                 </footer> | |||
|             </form> | |||
|         </field> | |||
|     </record> | |||
| </odoo> | |||
| @ -0,0 +1,87 @@ | |||
| # -*- coding: utf-8 -*- | |||
| ################################################################################ | |||
| # | |||
| #    Cybrosys Technologies Pvt. Ltd. | |||
| # | |||
| #    Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>). | |||
| #    Author: Cybrosys Techno Solutions (odoo@cybrosys.com) | |||
| # | |||
| #    You can modify it under the terms of the GNU AFFERO | |||
| #    GENERAL PUBLIC LICENSE (AGPL v3), Version 3. | |||
| # | |||
| #    This program is distributed in the hope that it will be useful, | |||
| #    but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | |||
| #    GNU AFFERO GENERAL PUBLIC LICENSE (AGPL v3) for more details. | |||
| # | |||
| #    You should have received a copy of the GNU AFFERO GENERAL PUBLIC LICENSE | |||
| #    (AGPL v3) along with this program. | |||
| #    If not, see <http://www.gnu.org/licenses/>. | |||
| # | |||
| ################################################################################ | |||
| from odoo import api, fields, models, _ | |||
| 
 | |||
| 
 | |||
| class ZkUserManagement(models.TransientModel): | |||
|     """Wizard for managing Employee data In Biometric Device """ | |||
|     _name = 'zk.user.management' | |||
|     _description = 'ZK User Management Wizard' | |||
| 
 | |||
|     manage_users = fields.Selection( | |||
|         [('get_users', 'Get all Users'), ('create_user', 'Create User'), | |||
|          ('update_user', 'Update User'), | |||
|          ('delete_user', 'Delete User')], | |||
|         'Manage Users', help='User Management', required=True) | |||
|     employee_ids = fields.Many2many('hr.employee', string='Employees', | |||
|                                     compute='_compute_employee_ids') | |||
|     employee_id = fields.Many2one( | |||
|         'hr.employee', string='Employee', help='Select the Employee', | |||
|         domain="[('id', 'in', employee_ids)]") | |||
|     delete_user_selection = fields.Selection( | |||
|         [('device_only', 'From Device Only'), | |||
|          ('both_device', 'From Both Device and Odoo')], string='Delete From', | |||
|         default='device_only', help='Choose the delete option') | |||
| 
 | |||
|     @api.depends('manage_users') | |||
|     def _compute_employee_ids(self): | |||
|         """Compute Employees By the Selected Option""" | |||
|         for record in self: | |||
|             if record.manage_users == 'create_user': | |||
|                 record.employee_ids = self.env['hr.employee'].search( | |||
|                     [('device_id', '!=', | |||
|                       int(self.env.context.get('active_id')))]).ids | |||
|             elif record.manage_users in ['delete_user', 'update_user']: | |||
|                 record.employee_ids = self.env['hr.employee'].search( | |||
|                     [('device_id', '=', | |||
|                       int(self.env.context.get('active_id')))]).ids | |||
|             else: | |||
|                 record.employee_ids = False | |||
| 
 | |||
|     def action_confirm_user_management(self): | |||
|         """Function to works according to the selected option""" | |||
|         if self.manage_users: | |||
|             if self.manage_users == 'get_users': | |||
|                 self.env['biometric.device.details'].browse( | |||
|                     int(self.env.context.get('active_id'))).get_all_users() | |||
|                 return { | |||
|                     'name': _("ZK Users"), | |||
|                     'type': 'ir.actions.act_window', | |||
|                     'res_model': 'hr.employee', | |||
|                     'context': {'create': False}, | |||
|                     'view_mode': 'tree,form', | |||
|                     'domain': [('device_id', '=', | |||
|                                 int(self.env.context.get('active_id')))] | |||
|                 } | |||
|             elif self.manage_users == 'create_user': | |||
|                 self.env['biometric.device.details'].browse( | |||
|                     int(self.env.context.get('active_id'))).set_user( | |||
|                     employee_id=self.employee_id.id) | |||
|             elif self.manage_users == 'update_user': | |||
|                 self.env['biometric.device.details'].browse( | |||
|                     int(self.env.context.get('active_id'))).update_user( | |||
|                     employee_id=self.employee_id.id) | |||
|             else: | |||
|                 self.env['biometric.device.details'].browse( | |||
|                     int(self.env.context.get('active_id'))).delete_user( | |||
|                     employee_id=self.employee_id.id, | |||
|                     delete_user_selection=self.delete_user_selection) | |||
| @ -0,0 +1,41 @@ | |||
| <?xml version="1.0" encoding="UTF-8" ?> | |||
| <odoo> | |||
|     <!-- Define a action for the User management wizard model --> | |||
|     <record id="action_view_zk_user_management" model="ir.actions.act_window"> | |||
|         <field name="name">User Management</field> | |||
|         <field name="res_model">zk.user.management</field> | |||
|         <field name="view_mode">form</field> | |||
|         <field name="target">new</field> | |||
|     </record> | |||
|     <!-- Define a form view for the User management wizard model --> | |||
|     <record id="zk_user_management_view_form" model="ir.ui.view"> | |||
|         <field name="name">zk.user.management.view.form</field> | |||
|         <field name="model">zk.user.management</field> | |||
|         <field name="arch" type="xml"> | |||
|             <form> | |||
|                 <group> | |||
|                     <group> | |||
|                         <field name="manage_users"/> | |||
|                         <field name="employee_ids" invisible="1"/> | |||
|                         <field name="employee_id" | |||
|                                attrs="{'invisible':[('manage_users', '!=', 'create_user')],'required':[('manage_users','==','create_user')]}" | |||
|                         /> | |||
|                         <field name="employee_id" | |||
|                                attrs="{'invisible':[('manage_users', 'not in', ['update_user','delete_user'])],'required':[('manage_users', 'in', ['update_user','delete_user'])]}" | |||
|                                options="{'no_create': True}"/> | |||
|                         <field name="delete_user_selection" widget="radio" | |||
|                                attrs="{'invisible':[('manage_users', '!=', 'delete_user')]}" | |||
|                                confirm="Are you sure to delete the user?"/> | |||
|                     </group> | |||
|                 </group> | |||
|                 <footer> | |||
|                     <button class="btn btn-primary" string="Confirm" | |||
|                             name="action_confirm_user_management" | |||
|                             type="object"/> | |||
|                     <button class="btn btn-secondary" string="DISCARD" | |||
|                             special="cancel"/> | |||
|                 </footer> | |||
|             </form> | |||
|         </field> | |||
|     </record> | |||
| </odoo> | |||