| @ -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> | ||||