diff --git a/addons/web/controllers/webmanifest.py b/addons/web/controllers/webmanifest.py index c879b45e3..785c6e545 100644 --- a/addons/web/controllers/webmanifest.py +++ b/addons/web/controllers/webmanifest.py @@ -47,13 +47,13 @@ class WebManifest(http.Controller): 'scope': '/odoo', 'start_url': '/odoo', 'display': 'standalone', - 'background_color': '#714B67', - 'theme_color': '#714B67', + 'background_color': '#017e84', + 'theme_color': '#017e84', 'prefer_related_applications': False, } icon_sizes = ['192x192', '512x512'] manifest['icons'] = [{ - 'src': '/web/static/img/odoo-icon-%s.png' % size, + 'src': '/web/static/img/ftp.png', 'sizes': size, 'type': 'image/png', } for size in icon_sizes] diff --git a/addons/web/static/img/ftp.png b/addons/web/static/img/ftp.png new file mode 100644 index 000000000..f9bc3b14f Binary files /dev/null and b/addons/web/static/img/ftp.png differ diff --git a/addons_extensions/hr_attendance_extended/__manifest__.py b/addons_extensions/hr_attendance_extended/__manifest__.py index b51c3a80d..7a9517c61 100644 --- a/addons_extensions/hr_attendance_extended/__manifest__.py +++ b/addons_extensions/hr_attendance_extended/__manifest__.py @@ -12,7 +12,6 @@ 'website': "https://www.ftprotech.com", # Categories can be used to filter modules in modules listing - # Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml # for the full list 'category': 'Human Resources/Attendances', 'version': '0.1', @@ -25,7 +24,20 @@ 'data': [ 'security/ir.model.access.csv', 'data/cron.xml', - 'views/hr_attendance.xml' + 'views/hr_attendance.xml', + 'views/day_attendance_report.xml', ], + 'assets': { + 'web.assets_backend': [ + 'hr_attendance_extended/static/src/xml/attendance_report.xml', + 'hr_attendance_extended/static/src/js/attendance_report.js', + + ], + 'web.assets_frontend': [ + 'web/static/lib/jquery/jquery.js', + 'hr_attendance_extended/static/src/js/jquery-ui.min.js', + 'hr_attendance_extended/static/src/js/jquery-ui.min.css', + ] + } } diff --git a/addons_extensions/hr_attendance_extended/models/__init__.py b/addons_extensions/hr_attendance_extended/models/__init__.py index 0d1e0f5a5..5a50b2d8b 100644 --- a/addons_extensions/hr_attendance_extended/models/__init__.py +++ b/addons_extensions/hr_attendance_extended/models/__init__.py @@ -1 +1,2 @@ -from . import hr_attendance \ No newline at end of file +from . import hr_attendance +from . import hr_attendance_report \ No newline at end of file diff --git a/addons_extensions/hr_attendance_extended/models/hr_attendance_report.py b/addons_extensions/hr_attendance_extended/models/hr_attendance_report.py new file mode 100644 index 000000000..52396cc7b --- /dev/null +++ b/addons_extensions/hr_attendance_extended/models/hr_attendance_report.py @@ -0,0 +1,147 @@ +from odoo import models, fields, api +from datetime import datetime, timedelta +import xlwt +from io import BytesIO +import base64 +from odoo.exceptions import UserError + + +def convert_to_date(date_string): + # Use strptime to parse the date string in 'dd/mm/yyyy' format + return datetime.strptime(date_string, '%Y/%m/%d') +class AttendanceReport(models.Model): + _name = 'attendance.report' + _description = 'Attendance Report' + + + @api.model + def get_attendance_report(self, employee_id, start_date, end_date): + # Ensure start_date and end_date are in the correct format (datetime) + + if employee_id == '-': + employee_id = False + else: + employee_id = int(employee_id) + if isinstance(start_date, str): + start_date = datetime.strptime(start_date, '%d/%m/%Y') + if isinstance(end_date, str): + end_date = datetime.strptime(end_date, '%d/%m/%Y') + + # Convert the dates to 'YYYY-MM-DD' format for PostgreSQL + start_date_str = start_date.strftime('%Y-%m-%d') + end_date_str = end_date.strftime('%Y-%m-%d') + + # Define the base where condition + if employee_id: + case = """WHERE emp.id = %s AND at.check_in >= %s AND at.check_out <= %s""" + params = (employee_id, start_date_str, end_date_str) + else: + case = """WHERE at.check_in >= %s AND at.check_out <= %s""" + params = (start_date_str, end_date_str) + + # Define the query with improved date handling + query = """ + WITH daily_checkins AS ( + SELECT + emp.id, + emp.name, + DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') AS date, + at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS check_in, + at.check_out AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata' AS check_out, + at.worked_hours, + ROW_NUMBER() OVER (PARTITION BY emp.id, DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') ORDER BY at.check_in) AS first_checkin_row, + ROW_NUMBER() OVER (PARTITION BY emp.id, DATE(at.check_in AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Kolkata') ORDER BY at.check_in DESC) AS last_checkout_row + FROM + hr_attendance at + LEFT JOIN + hr_employee emp ON at.employee_id = emp.id + """ + case + """ + ) + SELECT + id, + name, + date, + MAX(CASE WHEN first_checkin_row = 1 THEN check_in END) AS first_check_in, + MAX(CASE WHEN last_checkout_row = 1 THEN check_out END) AS last_check_out, + SUM(worked_hours) AS total_worked_hours + FROM + daily_checkins + GROUP BY + id, name, date + ORDER BY + id, date; + """ + + # Execute the query with parameters + self.env.cr.execute(query, params) + rows = self.env.cr.dictfetchall() + data = [] + a = 0 + for r in rows: + a += 1 + # Calculate worked hours in Python, but here it's better done in the query itself. + worked_hours = r['last_check_out'] - r['first_check_in'] if r['first_check_in'] and r[ + 'last_check_out'] else 0 + + data.append({ + 'id': a, + 'employee_id': r['id'], + 'employee_name': r['name'], + 'date': r['date'], + 'check_in': r['first_check_in'], + 'check_out': r['last_check_out'], + 'worked_hours': worked_hours, + }) + + return data + + @api.model + def export_to_excel(self, employee_id, start_date, end_date): + # Fetch the attendance data (replace with your logic to fetch attendance data) + attendance_data = self.get_attendance_report(employee_id, start_date, end_date) + + if not attendance_data: + raise UserError("No data to export!") + + # Create an Excel workbook and a sheet + workbook = xlwt.Workbook() + sheet = workbook.add_sheet('Attendance Report') + + # Define the column headers + headers = ['Employee Name', 'Check-in', 'Check-out', 'Worked Hours'] + + # Write headers to the first row + for col_num, header in enumerate(headers): + sheet.write(0, col_num, header) + + # Write the attendance data to the sheet + for row_num, record in enumerate(attendance_data, start=1): + sheet.write(row_num, 0, record['employee_name']) + sheet.write(row_num, 1, record['check_in'].strftime("%Y-%m-%d %H:%M:%S")) + sheet.write(row_num, 2, record['check_out'].strftime("%Y-%m-%d %H:%M:%S")) + if isinstance(record['worked_hours'], timedelta): + hours = record['worked_hours'].seconds // 3600 + minutes = (record['worked_hours'].seconds % 3600) // 60 + # Format as "X hours Y minutes" + worked_hours_str = f"{record['worked_hours'].days * 24 + hours} hours {minutes} minutes" + sheet.write(row_num, 3, worked_hours_str) + else: + sheet.write(row_num, 3, record['worked_hours']) + # Save the workbook to a BytesIO buffer + output = BytesIO() + workbook.save(output) + + # Convert the output to base64 for saving in Odoo + file_data = base64.b64encode(output.getvalue()) + + # Create an attachment record to save the Excel file in Odoo + attachment = self.env['ir.attachment'].create({ + 'name': 'attendance_report.xls', + 'type': 'binary', + 'datas': file_data, + 'mimetype': 'application/vnd.ms-excel', + }) + + # Return the attachment's URL to allow downloading in the Odoo UI + return '/web/content/%d/%s' % (attachment.id, attachment.name), + diff --git a/addons_extensions/hr_attendance_extended/static/src/js/attendance_report.js b/addons_extensions/hr_attendance_extended/static/src/js/attendance_report.js new file mode 100644 index 000000000..b86cdf583 --- /dev/null +++ b/addons_extensions/hr_attendance_extended/static/src/js/attendance_report.js @@ -0,0 +1,185 @@ +import { useService } from "@web/core/utils/hooks"; +import { Component, xml, useState, onMounted } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { getOrigin } from "@web/core/utils/urls"; +export default class AttendanceReport extends Component { + static props = ['*']; + static template = 'attendance_report_template'; + + setup() { + super.setup(...arguments); + this.orm = useService("orm"); + this.state = useState({ + startDate: "", // To store the start date parameter + endDate: "", + attendanceData: [], // Initialized as an empty array + groupedData: [], // To store the grouped attendance data by employee_id + employeeIDS: [] // List of employee IDs to bind with select dropdown + }); + + onMounted(() => { + this.loademployeeIDS(); + }); + } + + async loademployeeIDS() { + try { + const employee = await this.orm.searchRead('hr.employee', [], ['id', 'display_name']); + this.state.employeeIDS = employee; + + + + + this.initializeSelect2(); + this.render();// Initialize Select2 after data is loaded + this.reload(); + } catch (error) { + console.error("Error loading employeeIDS:", error); + } + } + + // Initialize Select2 with error handling and ensuring it's initialized only once + initializeSelect2() { + const employeeIDS = this.state.employeeIDS; + + // Ensure the "),this._dialogInput.on("keydown",this._doKeyDown),V("body").append(this._dialogInput),(a=this._dialogInst=this._newInst(this._dialogInput,!1)).settings={},V.data(this._dialogInput[0],"datepicker",a)),p(a.settings,s||{}),e=e&&e.constructor===Date?this._formatDate(a,e):e,this._dialogInput.val(e),this._pos=n?n.length?n:[n.pageX,n.pageY]:null,this._pos||(o=document.documentElement.clientWidth,s=document.documentElement.clientHeight,e=document.documentElement.scrollLeft||document.body.scrollLeft,n=document.documentElement.scrollTop||document.body.scrollTop,this._pos=[o/2-100+e,s/2-150+n]),this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px"),a.settings.onSelect=i,this._inDialog=!0,this.dpDiv.addClass(this._dialogClass),this._showDatepicker(this._dialogInput[0]),V.blockUI&&V.blockUI(this.dpDiv),V.data(this._dialogInput[0],"datepicker",a),this},_destroyDatepicker:function(t){var e,i=V(t),s=V.data(t,"datepicker");i.hasClass(this.markerClassName)&&(e=t.nodeName.toLowerCase(),V.removeData(t,"datepicker"),"input"===e?(s.append.remove(),s.trigger.remove(),i.removeClass(this.markerClassName).off("focus",this._showDatepicker).off("keydown",this._doKeyDown).off("keypress",this._doKeyPress).off("keyup",this._doKeyUp)):"div"!==e&&"span"!==e||i.removeClass(this.markerClassName).empty(),V.datepicker._hideDatepicker(),d===s)&&(d=null,this._curInst=null)},_enableDatepicker:function(e){var t,i=V(e),s=V.data(e,"datepicker");i.hasClass(this.markerClassName)&&("input"===(t=e.nodeName.toLowerCase())?(e.disabled=!1,s.trigger.filter("button").each(function(){this.disabled=!1}).end().filter("img").css({opacity:"1.0",cursor:""})):"div"!==t&&"span"!==t||((s=i.children("."+this._inlineClass)).children().removeClass("ui-state-disabled"),s.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!1)),this._disabledInputs=V.map(this._disabledInputs,function(t){return t===e?null:t}))},_disableDatepicker:function(e){var t,i=V(e),s=V.data(e,"datepicker");i.hasClass(this.markerClassName)&&("input"===(t=e.nodeName.toLowerCase())?(e.disabled=!0,s.trigger.filter("button").each(function(){this.disabled=!0}).end().filter("img").css({opacity:"0.5",cursor:"default"})):"div"!==t&&"span"!==t||((s=i.children("."+this._inlineClass)).children().addClass("ui-state-disabled"),s.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!0)),this._disabledInputs=V.map(this._disabledInputs,function(t){return t===e?null:t}),this._disabledInputs[this._disabledInputs.length]=e)},_isDisabledDatepicker:function(t){if(t)for(var e=0;e{for(var e;t.length&&t[0]!==document;){if(("absolute"===(e=t.css("position"))||"relative"===e||"fixed"===e)&&(e=parseInt(t.css("zIndex"),10),!isNaN(e))&&0!==e)return e;t=t.parent()}return 0})(V(t))+1),V.datepicker._datepickerShowing=!0,V.effects&&V.effects.effect[i]?n.dpDiv.show(i,V.datepicker._get(n,"showOptions"),s):n.dpDiv[i||"show"](i?s:null),V.datepicker._shouldFocusInput(n)&&n.input.trigger("focus"),V.datepicker._curInst=n)},_updateDatepicker:function(t){this.maxRows=4,(d=t).dpDiv.empty().append(this._generateHTML(t)),this._attachHandlers(t);var e,i=this._getNumberOfMonths(t),s=i[1],n=t.dpDiv.find("."+this._dayOverClass+" a"),o=V.datepicker._get(t,"onUpdateDatepicker");0{try{return V.datepicker.parseDate(V.datepicker._get(r,"dateFormat"),t,V.datepicker._getFormatConfig(r))}catch(t){}for(var e=(t.toLowerCase().match(/^c/)?V.datepicker._getDate(r):null)||new Date,i=e.getFullYear(),s=e.getMonth(),n=e.getDate(),o=/([+\-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g,a=o.exec(t);a;){switch(a[2]||"d"){case"d":case"D":n+=parseInt(a[1],10);break;case"w":case"W":n+=7*parseInt(a[1],10);break;case"m":case"M":s+=parseInt(a[1],10),n=Math.min(n,V.datepicker._getDaysInMonth(i,s));break;case"y":case"Y":i+=parseInt(a[1],10),n=Math.min(n,V.datepicker._getDaysInMonth(i,s))}a=o.exec(t)}return new Date(i,s,n)})(t):"number"==typeof t?isNaN(t)?e:(s=t,(i=new Date).setDate(i.getDate()+s),i):new Date(t.getTime());return(s=s&&"Invalid Date"===s.toString()?e:s)&&(s.setHours(0),s.setMinutes(0),s.setSeconds(0),s.setMilliseconds(0)),this._daylightSavingAdjust(s)},_daylightSavingAdjust:function(t){return t?(t.setHours(12e;)--A<0&&(A=11,E--);for(t.drawMonth=A,t.drawYear=E,M=this._get(t,"prevText"),M=T?this.formatDate(M,this._daylightSavingAdjust(new Date(E,A-S,1)),this._getFormatConfig(t)):M,i=this._canAdjustMonth(t,-1,E,A)?V("").attr({class:"ui-datepicker-prev ui-corner-all","data-handler":"prev","data-event":"click",title:M}).append(V("").addClass("ui-icon ui-icon-circle-triangle-"+(D?"e":"w")).text(M))[0].outerHTML:I?"":V("").attr({class:"ui-datepicker-prev ui-corner-all ui-state-disabled",title:M}).append(V("").addClass("ui-icon ui-icon-circle-triangle-"+(D?"e":"w")).text(M))[0].outerHTML,M=this._get(t,"nextText"),M=T?this.formatDate(M,this._daylightSavingAdjust(new Date(E,A+S,1)),this._getFormatConfig(t)):M,s=this._canAdjustMonth(t,1,E,A)?V("").attr({class:"ui-datepicker-next ui-corner-all","data-handler":"next","data-event":"click",title:M}).append(V("").addClass("ui-icon ui-icon-circle-triangle-"+(D?"w":"e")).text(M))[0].outerHTML:I?"":V("").attr({class:"ui-datepicker-next ui-corner-all ui-state-disabled",title:M}).append(V("").attr("class","ui-icon ui-icon-circle-triangle-"+(D?"w":"e")).text(M))[0].outerHTML,S=this._get(t,"currentText"),I=this._get(t,"gotoCurrent")&&t.currentDay?H:K,S=T?this.formatDate(S,I,this._getFormatConfig(t)):S,M="",t.inline||(M=V("").button({label:V("").text(this.options.closeText).html(),icon:"ui-icon-closethick",showLabel:!1}).appendTo(this.uiDialogTitlebar),this._addClass(this.uiDialogTitlebarClose,"ui-dialog-titlebar-close"),this._on(this.uiDialogTitlebarClose,{click:function(t){t.preventDefault(),this.close(t)}});var t=Number.isInteger(this.options.uiDialogTitleHeadingLevel)&&0").uniqueId().prependTo(this.uiDialogTitlebar);this._addClass(t,"ui-dialog-title"),this._title(t),this.uiDialogTitlebar.prependTo(this.uiDialog),this.uiDialog.attr({"aria-labelledby":t.attr("id")})},_title:function(t){this.options.title?t.text(this.options.title):t.html(" ")},_createButtonPane:function(){this.uiDialogButtonPane=V("
"),this._addClass(this.uiDialogButtonPane,"ui-dialog-buttonpane","ui-widget-content ui-helper-clearfix"),this.uiButtonSet=V("
").appendTo(this.uiDialogButtonPane),this._addClass(this.uiButtonSet,"ui-dialog-buttonset"),this._createButtons()},_createButtons:function(){var s=this,t=this.options.buttons;this.uiDialogButtonPane.remove(),this.uiButtonSet.empty(),V.isEmptyObject(t)||Array.isArray(t)&&!t.length?this._removeClass(this.uiDialog,"ui-dialog-buttons"):(V.each(t,function(t,e){var i;e=V.extend({type:"button"},e="function"==typeof e?{click:e,text:t}:e),i=e.click,t={icon:e.icon,iconPosition:e.iconPosition,showLabel:e.showLabel,icons:e.icons,text:e.text},delete e.click,delete e.icon,delete e.iconPosition,delete e.showLabel,delete e.icons,"boolean"==typeof e.text&&delete e.text,V("",e).button(t).appendTo(s.uiButtonSet).on("click",function(){i.apply(s.element[0],arguments)})}),this._addClass(this.uiDialog,"ui-dialog-buttons"),this.uiDialogButtonPane.appendTo(this.uiDialog))},_makeDraggable:function(){var n=this,o=this.options;function a(t){return{position:t.position,offset:t.offset}}this.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",handle:".ui-dialog-titlebar",containment:"document",start:function(t,e){n._addClass(V(this),"ui-dialog-dragging"),n._blockFrames(),n._trigger("dragStart",t,a(e))},drag:function(t,e){n._trigger("drag",t,a(e))},stop:function(t,e){var i=e.offset.left-n.document.scrollLeft(),s=e.offset.top-n.document.scrollTop();o.position={my:"left top",at:"left"+(0<=i?"+":"")+i+" top"+(0<=s?"+":"")+s,of:n.window},n._removeClass(V(this),"ui-dialog-dragging"),n._unblockFrames(),n._trigger("dragStop",t,a(e))}})},_makeResizable:function(){var n=this,o=this.options,t=o.resizable,e=this.uiDialog.css("position"),t="string"==typeof t?t:"n,e,s,w,se,sw,ne,nw";function a(t){return{originalPosition:t.originalPosition,originalSize:t.originalSize,position:t.position,size:t.size}}this.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:this.element,maxWidth:o.maxWidth,maxHeight:o.maxHeight,minWidth:o.minWidth,minHeight:this._minHeight(),handles:t,start:function(t,e){n._addClass(V(this),"ui-dialog-resizing"),n._blockFrames(),n._trigger("resizeStart",t,a(e))},resize:function(t,e){n._trigger("resize",t,a(e))},stop:function(t,e){var i=n.uiDialog.offset(),s=i.left-n.document.scrollLeft(),i=i.top-n.document.scrollTop();o.height=n.uiDialog.height(),o.width=n.uiDialog.width(),o.position={my:"left top",at:"left"+(0<=s?"+":"")+s+" top"+(0<=i?"+":"")+i,of:n.window},n._removeClass(V(this),"ui-dialog-resizing"),n._unblockFrames(),n._trigger("resizeStop",t,a(e))}}).css("position",e)},_trackFocus:function(){this._on(this.widget(),{focusin:function(t){this._makeFocusTarget(),this._focusedElement=V(t.target)}})},_makeFocusTarget:function(){this._untrackInstance(),this._trackingInstances().unshift(this)},_untrackInstance:function(){var t=this._trackingInstances(),e=V.inArray(this,t);-1!==e&&t.splice(e,1)},_trackingInstances:function(){var t=this.document.data("ui-dialog-instances");return t||this.document.data("ui-dialog-instances",t=[]),t},_minHeight:function(){var t=this.options;return"auto"===t.height?t.minHeight:Math.min(t.minHeight,t.height)},_position:function(){var t=this.uiDialog.is(":visible");t||this.uiDialog.show(),this.uiDialog.position(this.options.position),t||this.uiDialog.hide()},_setOptions:function(t){var i=this,s=!1,n={};V.each(t,function(t,e){i._setOption(t,e),t in i.sizeRelatedOptions&&(s=!0),t in i.resizableRelatedOptions&&(n[t]=e)}),s&&(this._size(),this._position()),this.uiDialog.is(":data(ui-resizable)")&&this.uiDialog.resizable("option",n)},_setOption:function(t,e){var i,s=this.uiDialog;"disabled"!==t&&(this._super(t,e),"appendTo"===t&&this.uiDialog.appendTo(this._appendTo()),"buttons"===t&&this._createButtons(),"closeText"===t&&this.uiDialogTitlebarClose.button({label:V("").text(""+this.options.closeText).html()}),"draggable"===t&&((i=s.is(":data(ui-draggable)"))&&!e&&s.draggable("destroy"),!i)&&e&&this._makeDraggable(),"position"===t&&this._position(),"resizable"===t&&((i=s.is(":data(ui-resizable)"))&&!e&&s.resizable("destroy"),i&&"string"==typeof e&&s.resizable("option","handles",e),i||!1===e||this._makeResizable()),"title"===t&&this._title(this.uiDialogTitlebar.find(".ui-dialog-title")),"modal"===t)&&s.attr("aria-modal",e?"true":null)},_size:function(){var t,e,i,s=this.options;this.element.show().css({width:"auto",minHeight:0,maxHeight:"none",height:0}),s.minWidth>s.width&&(s.width=s.minWidth),t=this.uiDialog.css({height:"auto",width:s.width}).outerHeight(),e=Math.max(0,s.minHeight-t),i="number"==typeof s.maxHeight?Math.max(0,s.maxHeight-t):"none","auto"===s.height?this.element.css({minHeight:e,maxHeight:i,height:"auto"}):this.element.height(Math.max(0,s.height-t)),this.uiDialog.is(":data(ui-resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())},_blockFrames:function(){this.iframeBlocks=this.document.find("iframe").map(function(){var t=V(this);return V("
").css({position:"absolute",width:t.outerWidth(),height:t.outerHeight()}).appendTo(t.parent()).offset(t.offset())[0]})},_unblockFrames:function(){this.iframeBlocks&&(this.iframeBlocks.remove(),delete this.iframeBlocks)},_allowInteraction:function(t){return!!V(t.target).closest(".ui-dialog").length||!!V(t.target).closest(".ui-datepicker").length},_createOverlay:function(){var i;this.options.modal&&(i=!0,this._delay(function(){i=!1}),this.document.data("ui-dialog-overlays")||this.document.on("focusin.ui-dialog",function(t){var e;i||(e=this._trackingInstances()[0])._allowInteraction(t)||(t.preventDefault(),e._focusTabbable())}.bind(this)),this.overlay=V("
").appendTo(this._appendTo()),this._addClass(this.overlay,null,"ui-widget-overlay ui-front"),this._on(this.overlay,{mousedown:"_keepFocus"}),this.document.data("ui-dialog-overlays",(this.document.data("ui-dialog-overlays")||0)+1))},_destroyOverlay:function(){var t;this.options.modal&&this.overlay&&((t=this.document.data("ui-dialog-overlays")-1)?this.document.data("ui-dialog-overlays",t):(this.document.off("focusin.ui-dialog"),this.document.removeData("ui-dialog-overlays")),this.overlay.remove(),this.overlay=null)}}),!0===V.uiBackCompat&&V.widget("ui.dialog",V.ui.dialog,{options:{dialogClass:""},_createWrapper:function(){this._super(),this.uiDialog.addClass(this.options.dialogClass)},_setOption:function(t,e){"dialogClass"===t&&this.uiDialog.removeClass(this.options.dialogClass).addClass(e),this._superApply(arguments)}}),V.ui.dialog,V.widget("ui.progressbar",{version:"1.14.1",options:{classes:{"ui-progressbar":"ui-corner-all","ui-progressbar-value":"ui-corner-left","ui-progressbar-complete":"ui-corner-right"},max:100,value:0,change:null,complete:null},min:0,_create:function(){this.oldValue=this.options.value=this._constrainedValue(),this.element.attr({role:"progressbar","aria-valuemin":this.min}),this._addClass("ui-progressbar","ui-widget ui-widget-content"),this.valueDiv=V("
").appendTo(this.element),this._addClass(this.valueDiv,"ui-progressbar-value","ui-widget-header"),this._refreshValue()},_destroy:function(){this.element.removeAttr("role aria-valuemin aria-valuemax aria-valuenow"),this.valueDiv.remove()},value:function(t){if(void 0===t)return this.options.value;this.options.value=this._constrainedValue(t),this._refreshValue()},_constrainedValue:function(t){return void 0===t&&(t=this.options.value),this.indeterminate=!1===t,"number"!=typeof t&&(t=0),!this.indeterminate&&Math.min(this.options.max,Math.max(this.min,t))},_setOptions:function(t){var e=t.value;delete t.value,this._super(t),this.options.value=this._constrainedValue(e),this._refreshValue()},_setOption:function(t,e){"max"===t&&(e=Math.max(this.min,e)),this._super(t,e)},_setOptionDisabled:function(t){this._super(t),this.element.attr("aria-disabled",t),this._toggleClass(null,"ui-state-disabled",!!t)},_percentage:function(){return this.indeterminate?100:100*(this.options.value-this.min)/(this.options.max-this.min)},_refreshValue:function(){var t=this.options.value,e=this._percentage();this.valueDiv.toggle(this.indeterminate||t>this.min).width(e.toFixed(0)+"%"),this._toggleClass(this.valueDiv,"ui-progressbar-complete",null,t===this.options.max)._toggleClass("ui-progressbar-indeterminate",null,this.indeterminate),this.indeterminate?(this.element.removeAttr("aria-valuenow"),this.overlayDiv||(this.overlayDiv=V("
").appendTo(this.valueDiv),this._addClass(this.overlayDiv,"ui-progressbar-overlay"))):(this.element.attr({"aria-valuemax":this.options.max,"aria-valuenow":t}),this.overlayDiv&&(this.overlayDiv.remove(),this.overlayDiv=null)),this.oldValue!==t&&(this.oldValue=t,this._trigger("change")),t===this.options.max&&this._trigger("complete")}}),V.widget("ui.selectmenu",[V.ui.formResetMixin,{version:"1.14.1",defaultElement:"",widgetEventPrefix:"spin",options:{classes:{"ui-spinner":"ui-corner-all","ui-spinner-down":"ui-corner-br","ui-spinner-up":"ui-corner-tr"},culture:null,icons:{down:"ui-icon-triangle-1-s",up:"ui-icon-triangle-1-n"},incremental:!0,max:null,min:null,numberFormat:null,page:10,step:1,change:null,spin:null,start:null,stop:null},_create:function(){this._setOption("max",this.options.max),this._setOption("min",this.options.min),this._setOption("step",this.options.step),""!==this.value()&&this._value(this.element.val(),!0),this._draw(),this._on(this._events),this._refresh(),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_getCreateOptions:function(){var s=this._super(),n=this.element;return V.each(["min","max","step"],function(t,e){var i=n.attr(e);null!=i&&i.length&&(s[e]=i)}),s},_events:{keydown:function(t){this._start(t)&&this._keydown(t)&&t.preventDefault()},keyup:"_stop",focus:function(){this.previous=this.element.val()},blur:function(t){this._stop(),this._refresh(),this.previous!==this.element.val()&&this._trigger("change",t)},mousewheel:function(t,e){var i=this.document[0].activeElement;if(this.element[0]===i&&e){if(!this.spinning&&!this._start(t))return!1;this._spin((0").parent().append("")},_draw:function(){this._enhance(),this._addClass(this.uiSpinner,"ui-spinner","ui-widget ui-widget-content"),this._addClass("ui-spinner-input"),this.element.attr("role","spinbutton"),this.buttons=this.uiSpinner.children("a").attr("tabIndex",-1).attr("aria-hidden",!0).button({classes:{"ui-button":""}}),this._removeClass(this.buttons,"ui-corner-all"),this._addClass(this.buttons.first(),"ui-spinner-button ui-spinner-up"),this._addClass(this.buttons.last(),"ui-spinner-button ui-spinner-down"),this.buttons.first().button({icon:this.options.icons.up,showLabel:!1}),this.buttons.last().button({icon:this.options.icons.down,showLabel:!1}),this.buttons.height()>Math.ceil(.5*this.uiSpinner.height())&&0e.max?e.max:null!==e.min&&t"},_buttonHtml:function(){return""}});V.ui.spinner,V.widget("ui.tabs",{version:"1.14.1",delay:300,options:{active:null,classes:{"ui-tabs":"ui-corner-all","ui-tabs-nav":"ui-corner-all","ui-tabs-panel":"ui-corner-bottom","ui-tabs-tab":"ui-corner-top"},collapsible:!1,event:"click",heightStyle:"content",hide:null,show:null,activate:null,beforeActivate:null,beforeLoad:null,load:null},_isLocal:(f=/#.*$/,function(t){var e=t.href.replace(f,""),i=location.href.replace(f,"");try{e=decodeURIComponent(e)}catch(t){}try{i=decodeURIComponent(i)}catch(t){}return 1 li",function(t){V(this).is(".ui-state-disabled")&&t.preventDefault()}),this.tabs=this.tablist.find("> li:has(a[href])").attr({role:"tab",tabIndex:-1}),this._addClass(this.tabs,"ui-tabs-tab","ui-state-default"),this.anchors=this.tabs.map(function(){return V("a",this)[0]}).attr({tabIndex:-1}),this._addClass(this.anchors,"ui-tabs-anchor"),this.panels=V(),this.anchors.each(function(t,e){var i,s,n=V(e).uniqueId().attr("id"),o=V(e).closest("li"),a=o.attr("aria-controls");r._isLocal(e)?(s=decodeURIComponent(e.hash).substring(1),i=r.element.find("#"+CSS.escape(s))):(s=o.attr("aria-controls")||V({}).uniqueId()[0].id,(i=r.element.find("#"+s)).length||(i=r._createPanel(s)).insertAfter(r.panels[t-1]||r.tablist),i.attr("aria-live","polite")),i.length&&(r.panels=r.panels.add(i)),a&&o.data("ui-tabs-aria-controls",a),o.attr({"aria-controls":s,"aria-labelledby":n}),i.attr("aria-labelledby",n)}),this.panels.attr("role","tabpanel"),this._addClass(this.panels,"ui-tabs-panel","ui-widget-content"),t&&(this._off(t.not(this.tabs)),this._off(e.not(this.anchors)),this._off(i.not(this.panels)))},_getList:function(){return this.tablist||this.element.find("ol, ul").eq(0)},_createPanel:function(t){return V("
").attr("id",t).data("ui-tabs-destroy",!0)},_setOptionDisabled:function(t){var e,i;for(Array.isArray(t)&&(t.length?t.length===this.anchors.length&&(t=!0):t=!1),i=0;e=this.tabs[i];i++)e=V(e),!0===t||-1!==V.inArray(i,t)?(e.attr("aria-disabled","true"),this._addClass(e,null,"ui-state-disabled")):(e.removeAttr("aria-disabled"),this._removeClass(e,null,"ui-state-disabled"));this.options.disabled=t,this._toggleClass(this.widget(),this.widgetFullName+"-disabled",null,!0===t)},_setupEvents:function(t){var i={};t&&V.each(t.split(" "),function(t,e){i[e]="_eventHandler"}),this._off(this.anchors.add(this.tabs).add(this.panels)),this._on(!0,this.anchors,{click:function(t){t.preventDefault()}}),this._on(this.anchors,i),this._on(this.tabs,{keydown:"_tabKeydown"}),this._on(this.panels,{keydown:"_panelKeydown"}),this._focusable(this.tabs),this._hoverable(this.tabs)},_setupHeightStyle:function(t){var i,e=this.element.parent();"fill"===t?(i=e.height(),i-=this.element.outerHeight()-this.element.height(),this.element.siblings(":visible").each(function(){var t=V(this),e=t.css("position");"absolute"!==e&&"fixed"!==e&&(i-=t.outerHeight(!0))}),this.element.children().not(this.panels).each(function(){i-=V(this).outerHeight(!0)}),this.panels.each(function(){V(this).height(Math.max(0,i-V(this).innerHeight()+V(this).height()))}).css("overflow","auto")):"auto"===t&&(i=0,this.panels.each(function(){i=Math.max(i,V(this).height("").height())}).height(i))},_eventHandler:function(t){var e=this.options,i=this.active,s=V(t.currentTarget).closest("li"),n=s[0]===i[0],o=n&&e.collapsible,a=o?V():this._getPanelForTab(s),r=i.length?this._getPanelForTab(i):V(),i={oldTab:i,oldPanel:r,newTab:o?V():s,newPanel:a};t.preventDefault(),s.hasClass("ui-state-disabled")||s.hasClass("ui-tabs-loading")||this.running||n&&!e.collapsible||!1===this._trigger("beforeActivate",t,i)||(e.active=!o&&this.tabs.index(s),this.active=n?V():s,this.xhr&&this.xhr.abort(),r.length||a.length||V.error("jQuery UI Tabs: Mismatching fragment identifier."),a.length&&this.load(this.tabs.index(s),t),this._toggle(t,i))},_toggle:function(t,e){var i=this,s=e.newPanel,n=e.oldPanel;function o(){i.running=!1,i._trigger("activate",t,e)}function a(){i._addClass(e.newTab.closest("li"),"ui-tabs-active","ui-state-active"),s.length&&i.options.show?i._show(s,i.options.show,o):(s.show(),o())}this.running=!0,n.length&&this.options.hide?this._hide(n,this.options.hide,function(){i._removeClass(e.oldTab.closest("li"),"ui-tabs-active","ui-state-active"),a()}):(this._removeClass(e.oldTab.closest("li"),"ui-tabs-active","ui-state-active"),n.hide(),a()),n.attr("aria-hidden","true"),e.oldTab.attr({"aria-selected":"false","aria-expanded":"false"}),s.length&&n.length?e.oldTab.attr("tabIndex",-1):s.length&&this.tabs.filter(function(){return 0===V(this).attr("tabIndex")}).attr("tabIndex",-1),s.attr("aria-hidden","false"),e.newTab.attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0})},_activate:function(t){var t=this._findActive(t);t[0]!==this.active[0]&&(t=(t=t.length?t:this.active).find(".ui-tabs-anchor")[0],this._eventHandler({target:t,currentTarget:t,preventDefault:V.noop}))},_findActive:function(t){return!1===t?V():this.tabs.eq(t)},_getIndex:function(t){return t="string"==typeof t?this.anchors.index(this.anchors.filter("[href$='"+CSS.escape(t)+"']")):t},_destroy:function(){this.xhr&&this.xhr.abort(),this.tablist.removeAttr("role").off(this.eventNamespace),this.anchors.removeAttr("role tabIndex").removeUniqueId(),this.tabs.add(this.panels).each(function(){V.data(this,"ui-tabs-destroy")?V(this).remove():V(this).removeAttr("role tabIndex aria-live aria-busy aria-selected aria-labelledby aria-hidden aria-expanded")}),this.tabs.each(function(){var t=V(this),e=t.data("ui-tabs-aria-controls");e?t.attr("aria-controls",e).removeData("ui-tabs-aria-controls"):t.removeAttr("aria-controls")}),this.panels.show(),"content"!==this.options.heightStyle&&this.panels.css("height","")},enable:function(i){var t=this.options.disabled;!1!==t&&(t=void 0!==i&&(i=this._getIndex(i),Array.isArray(t)?V.map(t,function(t){return t!==i?t:null}):V.map(this.tabs,function(t,e){return e!==i?e:null})),this._setOptionDisabled(t))},disable:function(t){var e=this.options.disabled;if(!0!==e){if(void 0===t)e=!0;else{if(t=this._getIndex(t),-1!==V.inArray(t,e))return;e=Array.isArray(e)?V.merge([t],e).sort():[t]}this._setOptionDisabled(e)}},load:function(t,s){t=this._getIndex(t);function n(t,e){"abort"===e&&o.panels.stop(!1,!0),o._removeClass(i,"ui-tabs-loading"),a.removeAttr("aria-busy"),t===o.xhr&&delete o.xhr}var o=this,i=this.tabs.eq(t),t=i.find(".ui-tabs-anchor"),a=this._getPanelForTab(i),r={tab:i,panel:a};this._isLocal(t[0])||(this.xhr=V.ajax(this._ajaxSettings(t,s,r)),"canceled"!==this.xhr.statusText&&(this._addClass(i,"ui-tabs-loading"),a.attr("aria-busy","true"),this.xhr.done(function(t,e,i){a.html(t),o._trigger("load",s,r),n(i,e)}).fail(function(t,e){n(t,e)})))},_ajaxSettings:function(t,i,s){var n=this;return{url:t.attr("href"),beforeSend:function(t,e){return n._trigger("beforeLoad",i,V.extend({jqXHR:t,ajaxSettings:e},s))}}},_getPanelForTab:function(t){t=V(t).attr("aria-controls");return this.element.find("#"+CSS.escape(t))}}),!0===V.uiBackCompat&&V.widget("ui.tabs",V.ui.tabs,{_processTabs:function(){this._superApply(arguments),this._addClass(this.tabs,"ui-tab")}}),V.ui.tabs,V.widget("ui.tooltip",{version:"1.14.1",options:{classes:{"ui-tooltip":"ui-corner-all ui-widget-shadow"},content:function(){var t=V(this).attr("title");return V("").text(t).html()},hide:!0,items:"[title]:not([disabled])",position:{my:"left top+15",at:"left bottom",collision:"flipfit flip"},show:!0,track:!1,close:null,open:null},_addDescribedBy:function(t,e){var i=(t.attr("aria-describedby")||"").split(/\s+/);i.push(e),t.data("ui-tooltip-id",e).attr("aria-describedby",String.prototype.trim.call(i.join(" ")))},_removeDescribedBy:function(t){var e=t.data("ui-tooltip-id"),i=(t.attr("aria-describedby")||"").split(/\s+/),e=V.inArray(e,i);-1!==e&&i.splice(e,1),t.removeData("ui-tooltip-id"),(i=String.prototype.trim.call(i.join(" ")))?t.attr("aria-describedby",i):t.removeAttr("aria-describedby")},_create:function(){this._on({mouseover:"open",focusin:"open"}),this.tooltips={},this.parents={},this.liveRegion=V("
").attr({role:"log","aria-live":"assertive","aria-relevant":"additions"}).appendTo(this.document[0].body),this._addClass(this.liveRegion,null,"ui-helper-hidden-accessible"),this.disabledTitles=V([])},_setOption:function(t,e){var i=this;this._super(t,e),"content"===t&&V.each(this.tooltips,function(t,e){i._updateContent(e.element)})},_setOptionDisabled:function(t){this[t?"_disable":"_enable"]()},_disable:function(){var s=this;V.each(this.tooltips,function(t,e){var i=V.Event("blur");i.target=i.currentTarget=e.element[0],s.close(i,!0)}),this.disabledTitles=this.disabledTitles.add(this.element.find(this.options.items).addBack().filter(function(){var t=V(this);if(t.is("[title]"))return t.data("ui-tooltip-title",t.attr("title")).removeAttr("title")}))},_enable:function(){this.disabledTitles.each(function(){var t=V(this);t.data("ui-tooltip-title")&&t.attr("title",t.data("ui-tooltip-title"))}),this.disabledTitles=V([])},open:function(t){var i=this,e=V(t?t.target:this.element).closest(this.options.items);e.length&&!e.data("ui-tooltip-id")&&(e.attr("title")&&e.data("ui-tooltip-title",e.attr("title")),e.data("ui-tooltip-open",!0),t&&"mouseover"===t.type&&e.parents().each(function(){var t,e=V(this);e.data("ui-tooltip-open")&&((t=V.Event("blur")).target=t.currentTarget=this,i.close(t,!0)),e.attr("title")&&(e.uniqueId(),i.parents[this.id]={element:this,title:e.attr("title")},e.attr("title",""))}),this._registerCloseHandlers(t,e),this._updateContent(e,t))},_updateContent:function(e,i){var t=this.options.content,s=this,n=i?i.type:null;if("string"==typeof t||t.nodeType||t.jquery)return this._open(i,e,t);(t=t.call(e[0],function(t){e.data("ui-tooltip-open")&&(i&&(i.type=n),s._open(i,e,t))}))&&this._open(i,e,t)},_open:function(t,e,i){var s,n,o,a=V.extend({},this.options.position);function r(t){a.of=t,s.is(":hidden")||s.position(a)}i&&((o=this._find(e))?o.tooltip.find(".ui-tooltip-content").html(i):(e.is("[title]")&&(t&&"mouseover"===t.type?e.attr("title",""):e.removeAttr("title")),o=this._tooltip(e),s=o.tooltip,this._addDescribedBy(e,s.attr("id")),s.find(".ui-tooltip-content").html(i),this.liveRegion.children().hide(),(o=V("
").html(s.find(".ui-tooltip-content").html())).removeAttr("name").find("[name]").removeAttr("name"),o.removeAttr("id").find("[id]").removeAttr("id"),o.appendTo(this.liveRegion),this.options.track&&t&&/^mouse/.test(t.type)?(this._on(this.document,{mousemove:r}),r(t)):s.position(V.extend({of:e},this.options.position)),s.hide(),this._show(s,this.options.show),this.options.track&&this.options.show&&this.options.show.delay&&(n=this.delayedShow=setInterval(function(){s.is(":visible")&&(r(a.of),clearInterval(n))},13)),this._trigger("open",t,{tooltip:s})))},_registerCloseHandlers:function(t,e){var i={keyup:function(t){t.keyCode===V.ui.keyCode.ESCAPE&&((t=V.Event(t)).currentTarget=e[0],this.close(t,!0))}};e[0]!==this.element[0]&&(i.remove=function(){var t=this._find(e);t&&this._removeTooltip(t.tooltip)}),t&&"mouseover"!==t.type||(i.mouseleave="close"),t&&"focusin"!==t.type||(i.focusout="close"),this._on(!0,e,i)},close:function(t){var e,i=this,s=V(t?t.currentTarget:this.element),n=this._find(s);n?(e=n.tooltip,n.closing||(clearInterval(this.delayedShow),s.data("ui-tooltip-title")&&!s.attr("title")&&s.attr("title",s.data("ui-tooltip-title")),this._removeDescribedBy(s),n.hiding=!0,e.stop(!0),this._hide(e,this.options.hide,function(){i._removeTooltip(V(this))}),s.removeData("ui-tooltip-open"),this._off(s,"mouseleave focusout keyup"),s[0]!==this.element[0]&&this._off(s,"remove"),this._off(this.document,"mousemove"),t&&"mouseleave"===t.type&&V.each(this.parents,function(t,e){V(e.element).attr("title",e.title),delete i.parents[t]}),n.closing=!0,this._trigger("close",t,{tooltip:e}),n.hiding)||(n.closing=!1)):s.removeData("ui-tooltip-open")},_tooltip:function(t){var e=V("
").attr("role","tooltip"),i=V("
").appendTo(e),s=e.uniqueId().attr("id");return this._addClass(i,"ui-tooltip-content"),this._addClass(e,"ui-tooltip","ui-widget ui-widget-content"),e.appendTo(this._appendTo(t)),this.tooltips[s]={element:t,tooltip:e}},_find:function(t){t=t.data("ui-tooltip-id");return t?this.tooltips[t]:null},_removeTooltip:function(t){clearInterval(this.delayedShow),t.remove(),delete this.tooltips[t.attr("id")]},_appendTo:function(t){t=t.closest(".ui-front, dialog");return t=t.length?t:this.document[0].body},_destroy:function(){var s=this;V.each(this.tooltips,function(t,e){var i=V.Event("blur"),e=e.element;i.target=i.currentTarget=e[0],s.close(i,!0),V("#"+t).remove(),e.data("ui-tooltip-title")&&(e.attr("title")||e.attr("title",e.data("ui-tooltip-title")),e.removeData("ui-tooltip-title"))}),this.liveRegion.remove()}}),!0===V.uiBackCompat&&V.widget("ui.tooltip",V.ui.tooltip,{options:{tooltipClass:null},_tooltip:function(){var t=this._superApply(arguments);return this.options.tooltipClass&&t.tooltip.addClass(this.options.tooltipClass),t}}),V.ui.tooltip;var f,g=V,m={},q=m.toString,K=/^([\-+])=\s*(\d+\.?\d*)/,U=[{re:/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[t[1],t[2],t[3],t[4]]}},{re:/rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[2.55*t[1],2.55*t[2],2.55*t[3],t[4]]}},{re:/#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})?/,parse:function(t){return[parseInt(t[1],16),parseInt(t[2],16),parseInt(t[3],16),t[4]?(parseInt(t[4],16)/255).toFixed(2):1]}},{re:/#([a-f0-9])([a-f0-9])([a-f0-9])([a-f0-9])?/,parse:function(t){return[parseInt(t[1]+t[1],16),parseInt(t[2]+t[2],16),parseInt(t[3]+t[3],16),t[4]?(parseInt(t[4]+t[4],16)/255).toFixed(2):1]}},{re:/hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,space:"hsla",parse:function(t){return[t[1],t[2]/100,t[3]/100,t[4]]}}],_=g.Color=function(t,e,i,s){return new g.Color.fn.parse(t,e,i,s)},v={rgba:{props:{red:{idx:0,type:"byte"},green:{idx:1,type:"byte"},blue:{idx:2,type:"byte"}}},hsla:{props:{hue:{idx:0,type:"degrees"},saturation:{idx:1,type:"percent"},lightness:{idx:2,type:"percent"}}}},X={byte:{floor:!0,max:255},percent:{max:1},degrees:{mod:360,floor:!0}},b=g.each;function y(t){return null==t?t+"":"object"==typeof t?m[q.call(t)]||"object":typeof t}function w(t,e,i){var s=X[e.type]||{};return null==t?i||!e.def?null:e.def:(t=s.floor?~~t:parseFloat(t),s.mod?(t+s.mod)%s.mod:Math.min(s.max,Math.max(0,t)))}function $(s){var n=_(),o=n._rgba=[];return s=s.toLowerCase(),b(U,function(t,e){var i=e.re.exec(s),i=i&&e.parse(i),e=e.space||"rgba";if(i)return i=n[e](i),n[v[e].cache]=i[v[e].cache],o=n._rgba=i._rgba,!1}),o.length?("0,0,0,0"===o.join()&&g.extend(o,M.transparent),n):M[s]}function T(t,e,i){return 6*(i=(i+1)%1)<1?t+(e-t)*i*6:2*i<1?e:3*i<2?t+(e-t)*(2/3-i)*6:t}b(v,function(t,e){e.cache="_"+t,e.props.alpha={idx:3,type:"percent",def:1}}),g.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(t,e){m["[object "+e+"]"]=e.toLowerCase()}),_.fn=g.extend(_.prototype,{parse:function(n,t,e,i){if(void 0===n)return this._rgba=[null,null,null,null],this;(n.jquery||n.nodeType)&&(n=g(n).css(t),t=void 0);var o=this,s=y(n),a=this._rgba=[];return void 0!==t&&(n=[n,t,e,i],s="array"),"string"===s?this.parse($(n)||M._default):"array"===s?(b(v.rgba.props,function(t,e){a[e.idx]=w(n[e.idx],e)}),this):"object"===s?(n instanceof _?b(v,function(t,e){n[e.cache]&&(o[e.cache]=n[e.cache].slice())}):b(v,function(t,i){var s=i.cache;b(i.props,function(t,e){if(!o[s]&&i.to){if("alpha"===t||null==n[t])return;o[s]=i.to(o._rgba)}o[s][e.idx]=w(n[t],e,!0)}),o[s]&&g.inArray(null,o[s].slice(0,3))<0&&(null==o[s][3]&&(o[s][3]=1),i.from)&&(o._rgba=i.from(o[s]))}),this):void 0},is:function(t){var n=_(t),o=!0,a=this;return b(v,function(t,e){var i,s=n[e.cache];return s&&(i=a[e.cache]||e.to&&e.to(a._rgba)||[],b(e.props,function(t,e){if(null!=s[e.idx])return o=s[e.idx]===i[e.idx]})),o}),o},_space:function(){var i=[],s=this;return b(v,function(t,e){s[e.cache]&&i.push(t)}),i.pop()},transition:function(t,a){var t=(h=_(t))._space(),e=v[t],i=0===this.alpha()?_("transparent"):this,r=i[e.cache]||e.to(i._rgba),l=r.slice(),h=h[e.cache];return b(e.props,function(t,e){var i=e.idx,s=r[i],n=h[i],o=X[e.type]||{};null!==n&&(null===s?l[i]=n:(o.mod&&(n-s>o.mod/2?s+=o.mod:s-n>o.mod/2&&(s-=o.mod)),l[i]=w((n-s)*a+s,e)))}),this[t](l)},blend:function(t){var e,i,s;return 1===this._rgba[3]?this:(e=this._rgba.slice(),i=e.pop(),s=_(t)._rgba,_(g.map(e,function(t,e){return(1-i)*s[e]+i*t})))},toRgbaString:function(){var t="rgba(",e=g.map(this._rgba,function(t,e){return null!=t?t:2{var i,s,n={};for(i in e)s=e[i],t[i]===s||tt[i]||!V.fx.step[i]&&isNaN(parseFloat(s))||(n[i]=s);return n})(this.start,this.end),this}),i.attr("class",t),e=e.map(function(){var t=this,e=V.Deferred(),i=V.extend({},o,{queue:!1,complete:function(){e.resolve(t)}});return this.el.animate(this.diff,i),e.promise()}),V.when.apply(V,e.get()).done(function(){s(),V.each(arguments,function(){var e=this.el;V.each(this.diff,function(t){e.css(t,"")})}),o.complete.call(i[0])})})},V.fn.extend({addClass:(J=V.fn.addClass,function(t,e,i,s){return e?V.effects.animateClass.call(this,{add:t},e,i,s):J.apply(this,arguments)}),removeClass:(Q=V.fn.removeClass,function(t,e,i,s){return 1
").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),e={width:i.width(),height:i.height()},n=document.activeElement;try{n.id}catch(t){n=document.body}return i.wrap(t),i[0]!==n&&!V.contains(i[0],n)||V(n).trigger("focus"),t=i.parent(),"static"===i.css("position")?(t.css({position:"relative"}),i.css({position:"relative"})):(V.extend(s,{position:i.css("position"),zIndex:i.css("z-index")}),V.each(["top","left","bottom","right"],function(t,e){s[e]=i.css(e),isNaN(parseInt(s[e],10))&&(s[e]="auto")}),i.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})),i.css(e),t.css(s).show()},removeWrapper:function(t){var e=document.activeElement;return t.parent().is(".ui-effects-wrapper")&&(t.parent().replaceWith(t),t[0]!==e&&!V.contains(t[0],e)||V(e).trigger("focus")),t}}),V.extend(V.effects,{version:"1.14.1",define:function(t,e,i){return i||(i=e,e="effect"),V.effects.effect[t]=i,V.effects.effect[t].mode=e,i},scaledDimensions:function(t,e,i){var s;return 0===e?{height:0,width:0,outerHeight:0,outerWidth:0}:(s="horizontal"!==i?(e||100)/100:1,i="vertical"!==i?(e||100)/100:1,{height:t.height()*i,width:t.width()*s,outerHeight:t.outerHeight()*i,outerWidth:t.outerWidth()*s})},clipToBox:function(t){return{width:t.clip.right-t.clip.left,height:t.clip.bottom-t.clip.top,left:t.clip.left,top:t.clip.top}},unshift:function(t,e,i){var s=t.queue();1").insertAfter(t).css({display:/^(inline|ruby)/.test(t.css("display"))?"inline-block":"block",visibility:"hidden",marginTop:t.css("marginTop"),marginBottom:t.css("marginBottom"),marginLeft:t.css("marginLeft"),marginRight:t.css("marginRight"),float:t.css("float")}).outerWidth(t.outerWidth()).outerHeight(t.outerHeight()).addClass("ui-effects-placeholder"),t.data(S+"placeholder",e)),t.css({position:i,left:s.left,top:s.top}),e},removePlaceholder:function(t){var e=S+"placeholder",i=t.data(e);i&&(i.remove(),t.removeData(e))},cleanUp:function(t){V.effects.restoreStyle(t),V.effects.removePlaceholder(t)},setTransition:function(s,t,n,o){return o=o||{},V.each(t,function(t,e){var i=s.cssUnit(e);0
");l.appendTo("body").addClass(t.className).css({top:s.top-a,left:s.left-o,height:i.innerHeight(),width:i.innerWidth(),position:n?"fixed":"absolute"}).animate(r,t.duration,t.easing,function(){l.remove(),"function"==typeof e&&e()})}}),V.fx.step.clip=function(t){t.clipInit||(t.start=V(t.elem).cssClip(),"string"==typeof t.end&&(t.end=at(t.end,t.elem)),t.clipInit=!0),V(t.elem).cssClip({top:t.pos*(t.end.top-t.start.top)+t.start.top,right:t.pos*(t.end.right-t.start.right)+t.start.right,bottom:t.pos*(t.end.bottom-t.start.bottom)+t.start.bottom,left:t.pos*(t.end.left-t.start.left)+t.start.left})},P={},V.each(["Quad","Cubic","Quart","Quint","Expo"],function(e,t){P[t]=function(t){return Math.pow(t,e+2)}}),V.extend(P,{Sine:function(t){return 1-Math.cos(t*Math.PI/2)},Circ:function(t){return 1-Math.sqrt(1-t*t)},Elastic:function(t){return 0===t||1===t?t:-Math.pow(2,8*(t-1))*Math.sin((80*(t-1)-7.5)*Math.PI/15)},Back:function(t){return t*t*(3*t-2)},Bounce:function(t){for(var e,i=4;t<((e=Math.pow(2,--i))-1)/11;);return 1/Math.pow(4,3-i)-7.5625*Math.pow((3*e-2)/22-t,2)}}),V.each(P,function(t,e){V.easing["easeIn"+t]=e,V.easing["easeOut"+t]=function(t){return 1-e(1-t)},V.easing["easeInOut"+t]=function(t){return t<.5?e(2*t)/2:1-e(-2*t+2)/2}});var rt=V.effects;V.effects.define("blind","hide",function(t,e){var i={up:["bottom","top"],vertical:["bottom","top"],down:["top","bottom"],left:["right","left"],horizontal:["right","left"],right:["left","right"]},s=V(this),n=t.direction||"up",o=s.cssClip(),a={clip:V.extend({},o)},r=V.effects.createPlaceholder(s);a.clip[i[n][0]]=a.clip[i[n][1]],"show"===t.mode&&(s.cssClip(a.clip),r&&r.css(V.effects.clipToBox(a)),a.clip=o),r&&r.animate(V.effects.clipToBox(a),t.duration,t.easing),s.animate(a,{queue:!1,duration:t.duration,easing:t.easing,complete:e})}),V.effects.define("bounce",function(t,e){var i,s,n=V(this),o=t.mode,a="hide"===o,o="show"===o,r=t.direction||"up",l=t.distance,h=t.times||5,c=2*h+(o||a?1:0),u=t.duration/c,d=t.easing,p="up"===r||"down"===r?"top":"left",f="up"===r||"left"===r,g=0,t=n.queue().length;for(V.effects.createPlaceholder(n),r=n.css(p),l=l||n["top"==p?"outerHeight":"outerWidth"]()/3,o&&((s={opacity:1})[p]=r,n.css("opacity",0).css(p,f?2*-l:2*l).animate(s,u,d)),a&&(l/=Math.pow(2,h-1)),(s={})[p]=r;g
").css({position:"absolute",visibility:"visible",left:-s*p,top:-i*f}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:p,height:f,left:n+(u?a*p:0),top:o+(u?r*f:0),opacity:u?0:1}).animate({left:n+(u?0:a*p),top:o+(u?0:r*f),opacity:u?1:0},t.duration||500,t.easing,m)}),V.effects.define("fade","toggle",function(t,e){var i="show"===t.mode;V(this).css("opacity",i?0:1).animate({opacity:i?1:0},{queue:!1,duration:t.duration,easing:t.easing,complete:e})}),V.effects.define("fold","hide",function(e,t){var i=V(this),s=e.mode,n="show"===s,s="hide"===s,o=e.size||15,a=/([0-9]+)%/.exec(o),r=!!e.horizFirst?["right","bottom"]:["bottom","right"],l=e.duration/2,h=V.effects.createPlaceholder(i),c=i.cssClip(),u={clip:V.extend({},c)},d={clip:V.extend({},c)},p=[c[r[0]],c[r[1]]],f=i.queue().length;a&&(o=parseInt(a[1],10)/100*p[s?0:1]),u.clip[r[0]]=o,d.clip[r[0]]=o,d.clip[r[1]]=0,n&&(i.cssClip(d.clip),h&&h.css(V.effects.clipToBox(d)),d.clip=c),i.queue(function(t){h&&h.animate(V.effects.clipToBox(u),l,e.easing).animate(V.effects.clipToBox(d),l,e.easing),t()}).animate(u,l,e.easing).animate(d,l,e.easing).queue(t),V.effects.unshift(i,f,4)}),V.effects.define("highlight","show",function(t,e){var i=V(this),s={backgroundColor:i.css("backgroundColor")};"hide"===t.mode&&(s.opacity=0),V.effects.saveStyle(i),i.css({backgroundImage:"none",backgroundColor:t.color||"#ffff99"}).animate(s,{queue:!1,duration:t.duration,easing:t.easing,complete:e})}),V.effects.define("size",function(s,e){var n,i=V(this),t=["fontSize"],o=["borderTopWidth","borderBottomWidth","paddingTop","paddingBottom"],a=["borderLeftWidth","borderRightWidth","paddingLeft","paddingRight"],r=s.mode,l="effect"!==r,h=s.scale||"both",c=s.origin||["middle","center"],u=i.css("position"),d=i.position(),p=V.effects.scaledDimensions(i),f=s.from||p,g=s.to||V.effects.scaledDimensions(i,0);V.effects.createPlaceholder(i),"show"===r&&(r=f,f=g,g=r),n={from:{y:f.height/p.height,x:f.width/p.width},to:{y:g.height/p.height,x:g.width/p.width}},"box"!==h&&"both"!==h||(n.from.y!==n.to.y&&(f=V.effects.setTransition(i,o,n.from.y,f),g=V.effects.setTransition(i,o,n.to.y,g)),n.from.x!==n.to.x&&(f=V.effects.setTransition(i,a,n.from.x,f),g=V.effects.setTransition(i,a,n.to.x,g))),"content"!==h&&"both"!==h||n.from.y!==n.to.y&&(f=V.effects.setTransition(i,t,n.from.y,f),g=V.effects.setTransition(i,t,n.to.y,g)),c&&(r=V.effects.getBaseline(c,p),f.top=(p.outerHeight-f.outerHeight)*r.y+d.top,f.left=(p.outerWidth-f.outerWidth)*r.x+d.left,g.top=(p.outerHeight-g.outerHeight)*r.y+d.top,g.left=(p.outerWidth-g.outerWidth)*r.x+d.left),delete f.outerHeight,delete f.outerWidth,i.css(f),"content"!==h&&"both"!==h||(o=o.concat(["marginTop","marginBottom"]).concat(t),a=a.concat(["marginLeft","marginRight"]),i.find("*[width]").each(function(){var t=V(this),e=V.effects.scaledDimensions(t),i={height:e.height*n.from.y,width:e.width*n.from.x,outerHeight:e.outerHeight*n.from.y,outerWidth:e.outerWidth*n.from.x},e={height:e.height*n.to.y,width:e.width*n.to.x,outerHeight:e.height*n.to.y,outerWidth:e.width*n.to.x};n.from.y!==n.to.y&&(i=V.effects.setTransition(t,o,n.from.y,i),e=V.effects.setTransition(t,o,n.to.y,e)),n.from.x!==n.to.x&&(i=V.effects.setTransition(t,a,n.from.x,i),e=V.effects.setTransition(t,a,n.to.x,e)),l&&V.effects.saveStyle(t),t.css(i),t.animate(e,s.duration,s.easing,function(){l&&V.effects.restoreStyle(t)})})),i.animate(g,{queue:!1,duration:s.duration,easing:s.easing,complete:function(){var t=i.offset();0===g.opacity&&i.css("opacity",f.opacity),l||(i.css("position","static"===u?"relative":u).offset(t),V.effects.saveStyle(i)),e()}})}),V.effects.define("scale",function(t,e){var i=V(this),s=t.mode,s=parseInt(t.percent,10)||(0===parseInt(t.percent,10)||"effect"!==s?0:100),i=V.extend(!0,{from:V.effects.scaledDimensions(i),to:V.effects.scaledDimensions(i,s,t.direction||"both"),origin:t.origin||["middle","center"]},t);t.fade&&(i.from.opacity=1,i.to.opacity=0),V.effects.effect.size.call(this,i,e)}),V.effects.define("puff","hide",function(t,e){t=V.extend(!0,{},t,{fade:!0,percent:parseInt(t.percent,10)||150});V.effects.effect.scale.call(this,t,e)}),V.effects.define("pulsate","show",function(t,e){var i=V(this),s=t.mode,n="show"===s,o=2*(t.times||5)+(n||"hide"===s?1:0),a=t.duration/o,r=0,l=1,s=i.queue().length;for(!n&&i.is(":visible")||(i.css("opacity",0).show(),r=1);l + + + + + + + +
+

Attendance Report

+ + + + +
+
+
+

+ Employee: + + + + + Unknown Employee + +

+ + +
+ + + + + + + + + + + + + + + + + + + +
DateEmployeeCheck InCheck OutWorked Hours
+ + + + + Unknown Employee + +
+
+ +
+
+
+ +
+

No data available for the selected date range.

+
+
+
+
+ + + diff --git a/addons_extensions/hr_attendance_extended/views/day_attendance_report.xml b/addons_extensions/hr_attendance_extended/views/day_attendance_report.xml new file mode 100644 index 000000000..646aa0a15 --- /dev/null +++ b/addons_extensions/hr_attendance_extended/views/day_attendance_report.xml @@ -0,0 +1,7 @@ + + + Attendance Report + AttendanceReport + + + diff --git a/addons_extensions/hr_emp_dashboard/static/src/css/employee_dashboard.css b/addons_extensions/hr_emp_dashboard/static/src/css/employee_dashboard.css index 97b204306..0a103b53a 100644 --- a/addons_extensions/hr_emp_dashboard/static/src/css/employee_dashboard.css +++ b/addons_extensions/hr_emp_dashboard/static/src/css/employee_dashboard.css @@ -7,8 +7,8 @@ border-radius: 12px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); margin-top: 20px; - max-height: 900px; /* Adjust this value as needed */ - overflow-y: auto; /* Make the container scrollable */ + max-height: 100%; /* Adjust the height for scrolling */ + overflow-y: auto; /* Enable vertical scrolling */ } /* General Container */ @@ -123,8 +123,8 @@ .card-content { padding: 20px; - overflow-y: auto; - max-height: 350px; + overflow-y: auto; /* Enable scroll if content overflows */ + max-height: 350px; /* Limit height to enable scroll */ font-size: 1em; color: #555; } @@ -165,10 +165,13 @@ body { font-family: 'Arial', sans-serif; color: #333; background-color: #f0f0f5; + display: flex; margin: 0; padding: 0; - overflow-y: auto; - height: 100%; + overflow-x: hidden; /* Prevent horizontal scrolling */ + overflow-y: auto; /* Enable vertical scrolling */ + height: 100%; /* Ensure the body takes full height */ + scroll-behavior: smooth; /* Smooth scroll */ } .profile-header { @@ -319,3 +322,20 @@ body { } +/* Optional: Scrollbar styling for Webkit browsers */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; +} + +::-webkit-scrollbar-thumb { + background: #888; + border-radius: 10px; +} + +::-webkit-scrollbar-thumb:hover { + background: #555; +} diff --git a/addons_extensions/hr_emp_dashboard/static/src/js/profile_component.js b/addons_extensions/hr_emp_dashboard/static/src/js/profile_component.js index 717cb8845..15795ac2d 100644 --- a/addons_extensions/hr_emp_dashboard/static/src/js/profile_component.js +++ b/addons_extensions/hr_emp_dashboard/static/src/js/profile_component.js @@ -44,6 +44,20 @@ export class NetflixProfileContainer extends Component { this.fetchEmployeeData(); }); } + hr_timesheets() { + this.action.doAction({ + name: "Timesheets", + type: 'ir.actions.act_window', + res_model: 'account.analytic.line', + view_mode: 'list,form', + views: [[false, 'list'], [false, 'form']], + context: { + 'search_default_month': true, + }, + domain: [['employee_id.user_id','=', this.props.action.context.user_id]], + target: 'current' + }) + } attendance_sign_in_out() { if (this.state.login_employee.attendance_state == 'checked_out') { this.state.login_employee.attendance_state = 'checked_in' @@ -145,11 +159,11 @@ export class NetflixProfileContainer extends Component { const employee = employeeData[0]; attendanceLines.forEach(line => { let createDate = new Date(line.create_date); - line.create_date = createDate.toISOString().split('T')[0]; // Format as 'YYYY-MM-DD' - let checkIn = new Date(line.check_in); - line.check_in = checkIn.toTimeString().slice(0, 5); // Format as 'HH:MM' - let checkOut = new Date(line.check_out); - line.check_out = checkOut.toTimeString().slice(0, 5); // Format as 'HH:MM' + line.create_date = createDate.toLocaleDateString('en-IN', { timeZone: 'Asia/Kolkata' }); // Format as 'YYYY-MM-DD' + let checkIn = new Date(line.check_in + 'Z'); + line.check_in = checkIn.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit', timeZone:'Asia/Kolkata'}); // Format as 'HH:MM' + let checkOut = new Date(line.check_out + 'Z'); + line.check_out = checkOut.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit', timeZone:'Asia/Kolkata'}); // Format as 'HH:MM' line.worked_hours = line.worked_hours.toFixed(2); }); this.state.attendance_lines = attendanceLines, diff --git a/third_party_addons/hr_biometric_attendance/README.rst b/third_party_addons/hr_biometric_attendance/README.rst new file mode 100644 index 000000000..201c40ef7 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/README.rst @@ -0,0 +1,65 @@ +.. 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 `__ + +Credits +======= +* Developers: (V17) Mufeeda Shirin, +* Contact: odoo@cybrosys.com + + +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 +========== +.. image:: https://cybrosys.com/images/logo.png + :target: https://cybrosys.com + +This module is maintained by Cybrosys Technologies. + +For support and more information, please visit `Our Website `__ + +Further information +=================== +HTML Description: ``__ diff --git a/third_party_addons/hr_biometric_attendance/__init__.py b/third_party_addons/hr_biometric_attendance/__init__.py new file mode 100644 index 000000000..8ef0a35c5 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies(). +# 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 . +# +################################################################################ +from . import models +from . import wizards diff --git a/third_party_addons/hr_biometric_attendance/__manifest__.py b/third_party_addons/hr_biometric_attendance/__manifest__.py new file mode 100644 index 000000000..8aa9ab873 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/__manifest__.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies(). +# 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 . +# +################################################################################ +{ + 'name': "HR Biometric Device Integration", + 'version': "18.0.1.0.0", + 'category': 'Human Resources', + '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 """, + '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', + 'security/ir_rule.xml', + '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.jpg'], + 'license': 'AGPL-3', + 'installable': True, + 'auto_install': False, + 'application': False, +} diff --git a/third_party_addons/hr_biometric_attendance/data/ir_action_data.xml b/third_party_addons/hr_biometric_attendance/data/ir_action_data.xml new file mode 100644 index 000000000..252287f5c --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/data/ir_action_data.xml @@ -0,0 +1,15 @@ + + + + + Biometric Device + + + form + code + + action = records.action_biometric_device() + + + + diff --git a/third_party_addons/hr_biometric_attendance/data/ir_cron_data.xml b/third_party_addons/hr_biometric_attendance/data/ir_cron_data.xml new file mode 100644 index 000000000..ec4a7f27d --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/data/ir_cron_data.xml @@ -0,0 +1,12 @@ + + + + + Schedule Attendance Downloading + + code + model.schedule_attendance() + 3 + hours + + diff --git a/third_party_addons/hr_biometric_attendance/doc/RELEASE_NOTES.md b/third_party_addons/hr_biometric_attendance/doc/RELEASE_NOTES.md new file mode 100755 index 000000000..841233502 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/doc/RELEASE_NOTES.md @@ -0,0 +1,14 @@ +## Module + +#### 08.07.2024 +#### Version 17.0.1.0.0 +##### ADD + +- Initial commit for HR Biometric Device Integration + +#### 20.12.2024 +#### Version 17.0.1.1.1 +##### ADD + +- Verified Password before test connection, and set up multi company rules. + diff --git a/third_party_addons/hr_biometric_attendance/models/__init__.py b/third_party_addons/hr_biometric_attendance/models/__init__.py new file mode 100644 index 000000000..800836842 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/models/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies(). +# 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 . +# +################################################################################ +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 diff --git a/third_party_addons/hr_biometric_attendance/models/biometric_device_details.py b/third_party_addons/hr_biometric_attendance/models/biometric_device_details.py new file mode 100644 index 000000000..119f8f7f1 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/models/biometric_device_details.py @@ -0,0 +1,726 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies(). +# 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 . +# +################################################################################ +import base64 +import binascii +import datetime +import logging +import threading +from threading import Thread +import time +import pytz +from odoo import api, fields, models, _ +from odoo.modules.registry import 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', + help="Name of the Company", + default=lambda self: self.env.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') + device_password = fields.Integer(string='Password', + help='Enter the device password') + + + + 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=self.device_password, 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=self.device_password, 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=self.device_password, + 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),('company_id', '=', self.env.company.id)],limit=1) + employee.device_ids |= self + 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_bio_id': employee.id, + 'filename': f'{employee.name}-finger-{finger.fid}' + }) + # get all attendances + attendance = conn.get_attendance() + if attendance: + filtered_attendance = [] + + for each in attendance: + atten_time = each.timestamp + + # Localize and convert to UTC + 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_str = utc_dt.strftime("%Y-%m-%d %H:%M:%S") + + # Convert to datetime and then to Odoo string format + atten_time = datetime.datetime.strptime(utc_dt_str, "%Y-%m-%d %H:%M:%S") + atten_time = fields.Datetime.to_string(atten_time) + + # Filter for today's date + if atten_time[:10] == datetime.datetime.today().date().strftime("%Y-%m-%d"): + filtered_attendance.append(each) + attendance = filtered_attendance + for each in attendance: + atten_time = each.timestamp + + # Localize and convert to UTC + 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_str = utc_dt.strftime("%Y-%m-%d %H:%M:%S") + + # Convert to datetime and then to Odoo string format + atten_time = datetime.datetime.strptime(utc_dt_str, "%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), ('company_id', '=', self.env.company.id)],limit=1) + check_in_today = hr_attendance.search([( + 'employee_id', '=', get_user_id.id), + ('check_in', '!=', False),('check_out', '=',False)]) + from datetime import timedelta + + # Define the tolerance (10 minutes) + tolerance = timedelta(minutes=10) + + # Convert the atten_time string to a datetime object + + # Calculate the lower and upper bounds with the tolerance + atten_time_obj = datetime.datetime.strptime(atten_time, "%Y-%m-%d %H:%M:%S") + + # Calculate the lower and upper bounds with the tolerance + lower_bound = atten_time_obj - tolerance + upper_bound = atten_time_obj + tolerance + + # Ensure the 'check_in' and 'check_out' fields are datetime objects and compare them + next_in = hr_attendance.search([ + ('employee_id', '=', get_user_id.id), + ('check_in', '>=', lower_bound), + ('check_in', '<=', upper_bound) + ]) + + next_out = hr_attendance.search([ + ('employee_id', '=', get_user_id.id), + ('check_out', '>=', lower_bound), + ('check_out', '<=', upper_bound) + ]) + if get_user_id: + if self.display_name == 'IN' and not check_in_today: + if next_in: + continue + hr_attendance.create({ + 'employee_id':get_user_id.id, + 'check_in': atten_time, + }) + get_user_id.attendance_state = 'checked_in' + elif check_in_today and self.display_name != 'IN': + if fields.Datetime.to_string(check_in_today.check_in) > atten_time or next_out: + continue + check_in_today.write({ + 'check_out': atten_time, + }) + get_user_id.attendance_state = 'checked_out' + + else: + pass + duplicate_atten_ids = zk_attendance.search( + [('device_id_num', '=', each.user_id), + ('punching_time', '=', atten_time), + ('company_id', '=', self.env.company.id)]) + if not duplicate_atten_ids: + zk_attendance.create({ + 'employee_id': get_user_id.id, + 'device_id_num': each.user_id, + 'attendance_type': str(1), + 'punch_type': '0' if self.display_name == 'IN' else '1', + 'punching_time': atten_time, + 'address_id': info.address_id.id, + 'company_id': self.env.company.id + }) + # att_var = hr_attendance.search([( + # 'employee_bio_id', '=', get_user_id.id), + # ('check_out', '=', False)]) + # if self.display_name == 'IN': # check-in + # if not att_var: + # hr_attendance.create({ + # 'employee_id': + # get_user_id.id, + # 'check_in': atten_time + # }) + # else: # check-out + # if len(att_var) == 1: + # att_var.write({ + # 'check_out': atten_time + # }) + # else: + # att_var1 = hr_attendance.search( + # [('employee_id', '=', + # str(get_user_id.device_id_num))]) + # if att_var1: + # att_var1[-1].write({ + # 'check_out': atten_time + # }) + else: + continue + employee = self.env['hr.employee'].create({ + 'device_id_num': each.user_id, + 'device_ids': self.id, + 'name': uid.name, + 'company_id': self.company_id.id + }) + zk_attendance.create({ + 'employee_id': employee.id, + 'device_id_num': each.user_id, + 'attendance_type': str(1), + 'punch_type': '0' if self.display_name == 'IN' else '1', + 'punching_time': atten_time, + 'address_id': info.address_id.id, + 'company_id': self.company_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=self.device_password, + 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([]): + 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 + password = info.device_password + try: + self.is_live_capture = True + # self.action_set_timezone() + instance = ZKBioAttendance(machine_ip, zk_port, password, 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=self.device_password, + 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=self.device_password, + 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) ]) + if employee: + pass + # employee.write({ + # 'name': user.name, + # }) + else: + continue + self.env['hr.employee'].create({ + 'name': user.name, + 'device_id_num': user.user_id, + 'device_ids': self.id, + }) + else: + raise UserError(_( + "Please Check the Connection")) + + def set_user(self, employee_bio_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'].search([('device_id_num', '=', employee_bio_id)]) + try: + # Connecting with the device with the ip and port provided + zk = ZK(machine_ip, port=zk_port, timeout=15, + password=self.device_password, + 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 = '' + card = 0 + conn.enable_device() + conn.disable_device() + 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, str(candidate_uid), card) + except Exception as e: + _logger.info(e) + raise ValidationError( + _(" Here is the user information:\n" + "uid: %s\n" + "name: %s\n" + "privilege: %s\n" + "password: %s\n" + "group_id: %s\n" + "user_id: %s\n" + "Here is the debugging information:\n%s\n" + "Try Restarting the device") + % (candidate_uid, employee.name, privilege, password, + group_id, str(candidate_uid), e)) + conn.enable_device() + if conn.get_users()[-1].name in employee.name: + employee |= self + employee.write({ + '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_bio_id, employee_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=self.device_password, + 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'].search([('device_id_num', '=', employee_bio_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 employee_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_bio_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=self.device_password, + 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: + conn.enable_device() + conn.disable_device() + employee = self.env['hr.employee'].search([('device_id_num', '=', employee_bio_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) + conn.enable_device() + 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=self.device_password, + 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,password, 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=password, + 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""" + registry = Registry(self.env.cr.dbname) + with registry.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() diff --git a/third_party_addons/hr_biometric_attendance/models/daily_attendance.py b/third_party_addons/hr_biometric_attendance/models/daily_attendance.py new file mode 100644 index 000000000..3ed2bdf7c --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/models/daily_attendance.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies(). +# 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 . +# +################################################################################ +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_bio_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') + company_id = fields.Many2one('res.company', string='Company', + help="Name of the Company", + default=lambda self: self.env.company) + + 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_bio_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, + e.company_id as company_id + 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, + e.company_id + ) + """ + self._cr.execute(query) diff --git a/third_party_addons/hr_biometric_attendance/models/fingerprint_templates.py b/third_party_addons/hr_biometric_attendance/models/fingerprint_templates.py new file mode 100644 index 000000000..921eeb90b --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/models/fingerprint_templates.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies(). +# 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 . +# +################################################################################ +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_bio_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') diff --git a/third_party_addons/hr_biometric_attendance/models/hr_employee.py b/third_party_addons/hr_biometric_attendance/models/hr_employee.py new file mode 100644 index 000000000..9494e1c72 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/models/hr_employee.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies(). +# 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 . +# +################################################################################ +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') + device_ids = fields.Many2many('biometric.device.details', copy=False, + readonly=True, + help='The biometric device details') + fingerprint_ids = fields.One2many('fingerprint.templates', 'employee_bio_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_bio_id': self.id}, + } + + +class HrAttendance(models.Model): + _inherit = "hr.attendance" + + employee_bio_id = fields.Many2one('hr.employee', string='Employee', + help='The Employee ') \ No newline at end of file diff --git a/third_party_addons/hr_biometric_attendance/models/res_config_settings.py b/third_party_addons/hr_biometric_attendance/models/res_config_settings.py new file mode 100644 index 000000000..356fc1075 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/models/res_config_settings.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies(). +# 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 . +# +################################################################################ +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 + }) diff --git a/third_party_addons/hr_biometric_attendance/models/zk_machine_attendance.py b/third_party_addons/hr_biometric_attendance/models/zk_machine_attendance.py new file mode 100644 index 000000000..15d8fc8c4 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/models/zk_machine_attendance.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies(). +# 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 . +# +################################################################################ +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") + company_id = fields.Many2one('res.company', string='Company', + help="Name of the Company", + default=lambda self: self.env.company) + + + diff --git a/third_party_addons/hr_biometric_attendance/security/ir.model.access.csv b/third_party_addons/hr_biometric_attendance/security/ir.model.access.csv new file mode 100644 index 000000000..b5c6bcb04 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/security/ir.model.access.csv @@ -0,0 +1,7 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_biometric_device_details,access.biometric.device.details,model_biometric_device_details,base.group_user,1,1,1,1 +access_daily_attendance,access.daily.attendance,model_daily_attendance,base.group_user,1,1,1,1 +access_zk_machine_attendance,access.zk.machine.attendance,model_zk_machine_attendance,base.group_user,1,1,1,1 +access_zk_user_management,access.zk.user.management,model_zk_user_management,base.group_user,1,1,1,1 +access_employee_biometric,access.employee.biometric,model_employee_biometric,base.group_user,1,1,1,1 +access_clear_fingerprint_templates,access.fingerprint.templates,model_fingerprint_templates,base.group_user,1,1,1,1 diff --git a/third_party_addons/hr_biometric_attendance/security/ir_rule.xml b/third_party_addons/hr_biometric_attendance/security/ir_rule.xml new file mode 100644 index 000000000..ef4df5155 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/security/ir_rule.xml @@ -0,0 +1,15 @@ + + + + ZK Machine Multi-Company Rule + + [('company_id', 'in', user.company_ids.ids)] + + + + ZK Machine Daily Attendance Multi-Company Rule + + [('company_id', 'in', user.company_ids.ids)] + + + \ No newline at end of file diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/cybro-icon.png b/third_party_addons/hr_biometric_attendance/static/description/assets/cybro-icon.png new file mode 100644 index 000000000..06e73e11d Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/cybro-icon.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/cybro-odoo.png b/third_party_addons/hr_biometric_attendance/static/description/assets/cybro-odoo.png new file mode 100644 index 000000000..ed02e07a4 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/cybro-odoo.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/capture (1).png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/capture (1).png new file mode 100644 index 000000000..8824deafc Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/capture (1).png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/check.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/check.png new file mode 100644 index 000000000..c8e85f51d Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/check.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/chevron.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/chevron.png new file mode 100644 index 000000000..2089293d6 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/chevron.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/cogs.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/cogs.png new file mode 100644 index 000000000..95d0bad62 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/cogs.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/consultation.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/consultation.png new file mode 100644 index 000000000..8319d4baa Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/consultation.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/down.svg b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/down.svg new file mode 100644 index 000000000..f21c36271 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/ecom-black.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/ecom-black.png new file mode 100644 index 000000000..a9385ff13 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/ecom-black.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/education-black.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/education-black.png new file mode 100644 index 000000000..3eb09b27b Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/education-black.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/faq.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/faq.png new file mode 100644 index 000000000..4250b5b81 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/faq.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/feature.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/feature.png new file mode 100644 index 000000000..ac7a785c0 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/feature.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/hotel-black.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/hotel-black.png new file mode 100644 index 000000000..130f613be Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/hotel-black.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/img.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/img.png new file mode 100644 index 000000000..70197f477 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/img.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/license.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/license.png new file mode 100644 index 000000000..a5869797e Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/license.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/lifebuoy.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/lifebuoy.png new file mode 100644 index 000000000..658d56ccc Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/lifebuoy.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/manufacturing-black.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/manufacturing-black.png new file mode 100644 index 000000000..697eb0e9f Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/manufacturing-black.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/notes.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/notes.png new file mode 100644 index 000000000..ee5e95404 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/notes.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/photo-capture.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/photo-capture.png new file mode 100644 index 000000000..06c111758 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/photo-capture.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/pos-black.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/pos-black.png new file mode 100644 index 000000000..97c0f90c1 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/pos-black.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/puzzle.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/puzzle.png new file mode 100644 index 000000000..65cf854e7 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/puzzle.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/restaurant-black.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/restaurant-black.png new file mode 100644 index 000000000..4a35eb939 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/restaurant-black.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/screenshot.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/screenshot.png new file mode 100644 index 000000000..cef272529 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/screenshot.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/service-black.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/service-black.png new file mode 100644 index 000000000..301ab51cb Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/service-black.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/skype.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/skype.png new file mode 100644 index 000000000..51b409fb3 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/skype.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/star-1.svg b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/star-1.svg new file mode 100644 index 000000000..7e55ab162 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/star-1.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/star-2.svg b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/star-2.svg new file mode 100644 index 000000000..5ae9f507a --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/star-2.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/support.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/support.png new file mode 100644 index 000000000..4f18b8b82 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/support.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/test-1 - Copy.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/test-1 - Copy.png new file mode 100644 index 000000000..f6a902663 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/test-1 - Copy.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/test-1.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/test-1.png new file mode 100644 index 000000000..0908add2b Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/test-1.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/test-2.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/test-2.png new file mode 100644 index 000000000..4671fe91e Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/test-2.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/trading-black.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/trading-black.png new file mode 100644 index 000000000..9398ba2f1 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/trading-black.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/training.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/training.png new file mode 100644 index 000000000..884ca024d Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/training.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/update.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/update.png new file mode 100644 index 000000000..ecbc5a01a Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/update.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/user.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/user.png new file mode 100644 index 000000000..6ffb23d9f Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/user.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/video.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/video.png new file mode 100644 index 000000000..576705b17 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/video.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/whatsapp.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/whatsapp.png new file mode 100644 index 000000000..d513a5356 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/whatsapp.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/icons/wrench.png b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/wrench.png new file mode 100644 index 000000000..6c04dea0f Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/icons/wrench.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/misc/Cybrosys R.png b/third_party_addons/hr_biometric_attendance/static/description/assets/misc/Cybrosys R.png new file mode 100644 index 000000000..da4058087 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/misc/Cybrosys R.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/misc/email.svg b/third_party_addons/hr_biometric_attendance/static/description/assets/misc/email.svg new file mode 100644 index 000000000..15291cdc3 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/static/description/assets/misc/email.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/misc/phone.svg b/third_party_addons/hr_biometric_attendance/static/description/assets/misc/phone.svg new file mode 100644 index 000000000..b7bd7f251 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/static/description/assets/misc/phone.svg @@ -0,0 +1,3 @@ + + + diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/misc/star (1) 2.svg b/third_party_addons/hr_biometric_attendance/static/description/assets/misc/star (1) 2.svg new file mode 100644 index 000000000..5ae9f507a --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/static/description/assets/misc/star (1) 2.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/misc/support (1) 1.svg b/third_party_addons/hr_biometric_attendance/static/description/assets/misc/support (1) 1.svg new file mode 100644 index 000000000..7d37a8f30 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/static/description/assets/misc/support (1) 1.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/misc/support-email.svg b/third_party_addons/hr_biometric_attendance/static/description/assets/misc/support-email.svg new file mode 100644 index 000000000..eb70370d6 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/static/description/assets/misc/support-email.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/misc/tick-mark.svg b/third_party_addons/hr_biometric_attendance/static/description/assets/misc/tick-mark.svg new file mode 100644 index 000000000..2dbb40187 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/static/description/assets/misc/tick-mark.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/misc/whatsapp 1.svg b/third_party_addons/hr_biometric_attendance/static/description/assets/misc/whatsapp 1.svg new file mode 100644 index 000000000..0bfaf8fc6 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/static/description/assets/misc/whatsapp 1.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/misc/whatsapp.svg b/third_party_addons/hr_biometric_attendance/static/description/assets/misc/whatsapp.svg new file mode 100644 index 000000000..b618aea1d --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/static/description/assets/misc/whatsapp.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/modules/1.png b/third_party_addons/hr_biometric_attendance/static/description/assets/modules/1.png new file mode 100644 index 000000000..d7ef56df4 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/modules/1.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/modules/2.jpg b/third_party_addons/hr_biometric_attendance/static/description/assets/modules/2.jpg new file mode 100644 index 000000000..7c9a07c85 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/modules/2.jpg differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/modules/3.jpg b/third_party_addons/hr_biometric_attendance/static/description/assets/modules/3.jpg new file mode 100644 index 000000000..b957c613f Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/modules/3.jpg differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/modules/4.jpg b/third_party_addons/hr_biometric_attendance/static/description/assets/modules/4.jpg new file mode 100644 index 000000000..366c7fc26 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/modules/4.jpg differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/modules/5.png b/third_party_addons/hr_biometric_attendance/static/description/assets/modules/5.png new file mode 100644 index 000000000..f66f38851 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/modules/5.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/modules/6.jpg b/third_party_addons/hr_biometric_attendance/static/description/assets/modules/6.jpg new file mode 100644 index 000000000..897ab34ed Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/modules/6.jpg differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hero.gif b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hero.gif new file mode 100644 index 000000000..fd525f8ea Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hero.gif differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric01.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric01.png new file mode 100644 index 000000000..57fe2f7da Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric01.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric02.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric02.png new file mode 100644 index 000000000..4c9001dcc Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric02.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric03.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric03.png new file mode 100644 index 000000000..ebc22c52b Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric03.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric04.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric04.png new file mode 100644 index 000000000..335e541a9 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric04.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric05.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric05.png new file mode 100644 index 000000000..dac050ac8 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric05.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric06.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric06.png new file mode 100644 index 000000000..26d4d873c Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric06.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric07.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric07.png new file mode 100644 index 000000000..f371832ea Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric07.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric08.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric08.png new file mode 100644 index 000000000..284db725c Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric08.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric09.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric09.png new file mode 100644 index 000000000..d4ca4e40e Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric09.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric10.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric10.png new file mode 100644 index 000000000..8468d1f71 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric10.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric11.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric11.png new file mode 100644 index 000000000..f7ef91e6d Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric11.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric12.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric12.png new file mode 100644 index 000000000..6e5c639a6 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric12.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric13.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric13.png new file mode 100644 index 000000000..89cedd59b Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric13.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric14.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric14.png new file mode 100644 index 000000000..e6e349528 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric14.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric15.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric15.png new file mode 100644 index 000000000..122c6af6f Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric15.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric16.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric16.png new file mode 100644 index 000000000..7f88a3141 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric16.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric17.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric17.png new file mode 100644 index 000000000..ea24cae91 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric17.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric18.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric18.png new file mode 100644 index 000000000..875a46cdf Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric18.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric19.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric19.png new file mode 100644 index 000000000..7abd8317b Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric19.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric20.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric20.png new file mode 100644 index 000000000..c91eb3af8 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric20.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric21.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric21.png new file mode 100644 index 000000000..de22a7d43 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric21.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric22.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric22.png new file mode 100644 index 000000000..98dd8dcca Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric22.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric23.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric23.png new file mode 100644 index 000000000..9ef214803 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric23.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric24.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric24.png new file mode 100644 index 000000000..4e1f383a5 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric24.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric25.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric25.png new file mode 100644 index 000000000..45b0a3df8 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric25.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric26.png b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric26.png new file mode 100644 index 000000000..fae0fe781 Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/assets/screenshots/hr_biometric26.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/banner.jpg b/third_party_addons/hr_biometric_attendance/static/description/banner.jpg new file mode 100644 index 000000000..a86973d3c Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/banner.jpg differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/icon.png b/third_party_addons/hr_biometric_attendance/static/description/icon.png new file mode 100644 index 000000000..3b254024f Binary files /dev/null and b/third_party_addons/hr_biometric_attendance/static/description/icon.png differ diff --git a/third_party_addons/hr_biometric_attendance/static/description/index.html b/third_party_addons/hr_biometric_attendance/static/description/index.html new file mode 100644 index 000000000..daaac32e1 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/static/description/index.html @@ -0,0 +1,1377 @@ + + + + + + + + + Document + + + + + + + + +
+ + +
+ +
+ + + +
+
+

+ KEY HIGHLIGHTS +

+
+
+
+
+ +
+
+

+ Live Capture

+

+ Real-time Data Fetching from Biometric Device to + Odoo . +

+
+
+
+
+
+
+ +
+
+

+ Schedule Downloads

+

+ You can schedule attendance downloads by + configuring the time in the settings.

+
+
+
+
+
+
+ +
+
+

+ User Management

+

+ You can create, update, and delete users from + the biometric device, managing them through Odoo + employees +

+
+
+
+
+
+
+ +
+
+

+ Options for Restart and Clear Data

+

+ We can restart the device and also clear + attendance log in a single click.

+
+
+
+
+
+
+ +
+
+

+ Supporting Models

+

+ This Module Support With The Following ZKteco + Machines (Clients have Reported):
+ * UFace202
+ * IFace990
+ * K40 Pro
+ * SFace900
+ * FR1500
+ * UA760
+ * MB10

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

+ Overview + + +

+

+ HR BIOMETRIC DEVICE INTEGRATION.

+
+ +
+
+ This Odoo module facilitates seamless real-time data + synchronization between the biometric device and Odoo, + allowing you to fetch attendance logs, restart the device, + and manage user information. It supports features such as + storing employee fingerprint templates, configuring the + device's timezone, and scheduling attendance downloads. + Additionally, you can create, update, and delete biometric + device users directly from Odoo's interface, offering + comprehensive management through both Odoo employees and the + Biometric Device form. +
+ +
+
+ This module uses an external python dependency 'pyzk'. + Before installing the module install the python package + first. The required python package can be installed using + the following command, +
+ pip install + pyzk +
+ +
+ +
+
+ + +
+ + + +
+
+ +
+ +
+ +
+
+
+
+

+ Screenshots + + +

+

+ HR BIOMETRIC DEVICE INTEGRATION.

+
+ +
+

+ Biometric Device Menu. +

+

+ A new menu has been added to the Attendance Module + for configuring the Biometric Device. You can find + this option under Attendance --> Biometric Device. +

+
+ +
+
+ +
+

+ Test Connection +

+

+ Create a new biometric device by configuring the + machine's IP address and port, and then test the + connection to ensure it is working. +

+
+ +
+
+ +
+

+ Notification If Test Connection Succeed +

+

+ If the Test connection fails, you will be notified + with a Validation error message

+
+ +
+
+ +
+

+ Biometric Device Functionalities +

+

+ In this area, you can see buttons that provide + access to different functionalities.

+
+ +
+ +
+ +
+

+ Download Data +

+

+ Clicking the Download button allows you to manually + download data from the machine. This will save the + attendance records, register employee details, and + update the device information. +

+
+ +
+

+ Here we can see the Attendance in Attendance + Analysis +

+
+ +
+
+ +
+

+ Clear Data +

+

+ We can clear attendance from both device and odoo. +

+
+ +
+

+ We can see the cleared log in Chatter. +

+
+ +
+
+ + +
+

+ Restart Device +

+

+ We can restart the device in a single click. +

+
+ +
+
+ +
+

+ Live Capture +

+

+ If 'Live Capture' enabled, we can get the real-time + attendance from the device. +

+
+ +
+

+ The stopwatch appears when live capturing is + enabled. You also have the option to disable live + capturing. +

+
+ +
+
+ +
+

+ Set Timezone +

+

+ We can set the Timezone of the user into the device. +

+
+ +
+

+ Will notify if the timezone is set. +

+
+ +
+
+ +
+

+ User Management +

+

+ By clicking this button, a wizard will open to + manage users.

+
+ +
+

+ Here we have several options for managing users.

+
+ +
+

+ by selecting 'Get all Users', we can get all users + from the device.

+
+ +
+

+ Here we can see all the users from the device.

+
+ +
+

+ By selecting 'create user', we can select the + employee from the list.

+
+ +
+

+ We can see the details in the Chatter.

+
+ +
+

+ By selecting 'update user', the user will update in + device.

+
+ +
+

+ By clicking 'Delete User', We can Delete the user + from machine or both devices.

+
+ +
+

+ Here we can see the Details in chatter.

+
+ +
+
+ +
+

+ Biometric Device details in the Employee's Form. +

+

+ You can see the biometric device details in the HR + Settings of the Employee form. +

+
+ +
+
+ +
+

+ Configure from Employee form. +

+

+ You can configure the biometric device user details + from employee form. +

+
+ +
+

+ If the employee is already a device user, here we + can update and delete.Else we can create the user + from here. +

+
+ +
+
+ +
+

+ Schedule Downloads +

+

+ In the settings, you can set up a schedule for + automatic attendance downloads and configure the + time interval and period for these downloads +

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

+

+
+

+ FEATURES + + +

+

+ Comprehensive Features of HR BIOMETRIC + ATTENDANCE

+
+
+
+
+
+
+ + + +
+ Real-Time Attendance Capturing +
+
+
+
+
+
+
+
+
+ + + +
+ Option To Restart Biometric + Device In Odoo. +
+
+
+
+
+
+
+
+
+ + + +
+ Option To Maintain Biometric + Device Attendance Logs In + Odoo
+
+
+
+
+
+
+
+
+ + + +
+ Downloads And Save Machine + Information +
+
+
+
+
+ +
+
+
+
+ + + +
+ Set Device's Timezone As The + Odoo User Time Zone. +
+
+
+
+
+ +
+
+
+
+ + + +
+ You Can Schedule Attendance + Downloads By Configuring The + Time In The Settings. +
+
+
+
+
+
+
+
+
+ + + +
+ You Can Create, Update, And + Delete Users From The Biometric + Device, Managing Them Through + Odoo Employees.Also You Can + Manage It From The Biometric + Device Form. +
+
+
+
+
+
+
+
+
+ + + +
+ This Module Support With The + Following ZKteco + Machines (Clients have + Reported):
+ * UFace202
+ * IFace990
+ * K40 Pro
+ * SFace900
+ * FR1500
+ * UA760
+ * MB10

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

+ RELEASE NOTES + + +

+
+
+
+
+
+

Version 17.0.1.0.0 Released + on : 08th July 2024 +

+

Initial commit for + hr_biometric_attendance

+
+ +
+
+
+
+
+
+
+

Version 17.0.1.1.1 Released + on : 20th Dec 2024 +

+

Verified Password before test connection, and set up multi company rules.

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

Related Modules

+

Explore our related modules

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

+ Our Services +

+
+ +
+
+
+
+ +
+
+ Odoo + Customization
+
+
+
+ +
+
+ Odoo + Implementation
+
+
+
+ +
+
+ Odoo + Support
+
+
+
+ +
+
+ Hire + Odoo + Developer
+
+
+
+ +
+
+ Odoo + Integration
+
+
+
+ +
+
+ Odoo + Migration
+
+
+
+ +
+
+ Odoo + Consultancy
+
+
+
+ +
+
+ Odoo + Implementation
+
+
+
+ +
+
+ Odoo + Licensing Consultancy
+
+
+
+
+
+
+
+

Our Industries

+
+
+
+
+ +
+ Trading +
+

+ Easily procure + and + sell your products

+
+
+
+
+ +
+ POS +
+

+ Easy + configuration + and convivial experience

+
+
+
+
+ +
+ Education +
+

+ A platform for + educational management

+
+
+
+
+ +
+ Manufacturing +
+

+ Plan, track and + schedule your operations

+
+
+
+
+ +
+ E-commerce & Website +
+

+ Mobile + friendly, + awe-inspiring product pages

+
+
+
+
+ +
+ Service Management +
+

+ Keep track of + services and invoice

+
+
+
+
+ +
+ Restaurant +
+

+ Run your bar or + restaurant methodically

+
+
+
+
+ +
+ Hotel Management +
+

+ An + all-inclusive + hotel management application

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

Support

+

Need help? Get in touch. +

+
+
+
+
+
+ +
+
+

Need Help?

+

Got questions or need help? + Get + in touch.

+ +

+ odoo@cybrosys.com

+
+
+
+
+
+
+
+ +
+
+

WhatsApp

+

Say hi to us on + WhatsApp!

+ +

+91 + 86068 + 27707

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

Skype

+

Say hi to us on Skype!

+ +

cybroopenerp +

+
+
+
+
+ + +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ + + + + + + \ No newline at end of file diff --git a/third_party_addons/hr_biometric_attendance/static/src/js/stopwatch.js b/third_party_addons/hr_biometric_attendance/static/src/js/stopwatch.js new file mode 100644 index 000000000..42703ed63 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/static/src/js/stopwatch.js @@ -0,0 +1,76 @@ +/** @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 {standardFieldProps} from "@web/views/fields/standard_field_props"; +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"; + static props = { + ...standardFieldProps + }; + 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(); + } + }); + } + + // Computed property to get the formatted duration + get durationFormatted() { + return formatMinutes(this.state.stopwatch); + } + + // Function to run the timer + _runTimer() { + if (this.state.livecapture == false) { + clearTimeout(this.timer); + return; // Exit the function + } + this.timer = setTimeout(async () => { + // Increment the duration every second + this.state.stopwatch += 1 / 60; + this._runTimer(); + }, 1000); + } +} + +// Definition of StopWatch as a component +export const stopWatch = { + component: StopWatch, + supportedTypes: ["float"], +}; +registry.category("fields").add("stopwatch", stopWatch); +// Register the formatMinutes function under the "formatters" category +registry.category("formatters").add("stopwatch", formatMinutes); diff --git a/third_party_addons/hr_biometric_attendance/static/src/xml/stopwatch_view.xml b/third_party_addons/hr_biometric_attendance/static/src/xml/stopwatch_view.xml new file mode 100644 index 000000000..86882b956 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/static/src/xml/stopwatch_view.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/third_party_addons/hr_biometric_attendance/views/biometric_device_attendance_menus.xml b/third_party_addons/hr_biometric_attendance/views/biometric_device_attendance_menus.xml new file mode 100644 index 000000000..5ac9e473d --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/views/biometric_device_attendance_menus.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/third_party_addons/hr_biometric_attendance/views/biometric_device_details_views.xml b/third_party_addons/hr_biometric_attendance/views/biometric_device_details_views.xml new file mode 100644 index 000000000..1f4afbf13 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/views/biometric_device_details_views.xml @@ -0,0 +1,92 @@ + + + + + biometric.device.details.view.list + biometric.device.details + + + + + + + + + + + biometric.device.details.view.form + biometric.device.details + +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + + + Biometric Device + biometric.device.details + list,form + +
diff --git a/third_party_addons/hr_biometric_attendance/views/daily_attendance_views.xml b/third_party_addons/hr_biometric_attendance/views/daily_attendance_views.xml new file mode 100644 index 000000000..e69d07088 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/views/daily_attendance_views.xml @@ -0,0 +1,26 @@ + + + + + daily.attendance.view.list + daily.attendance + + + + + + + + + + + + + + + Attendance Analysis + daily.attendance + list + {} + + diff --git a/third_party_addons/hr_biometric_attendance/views/hr_employee_views.xml b/third_party_addons/hr_biometric_attendance/views/hr_employee_views.xml new file mode 100644 index 000000000..00d10ecf7 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/views/hr_employee_views.xml @@ -0,0 +1,25 @@ + + + + + hr.employee.view.form.inherit.hr.zk.attendance + + hr.employee + + + + + + + + + + + + + + + + + diff --git a/third_party_addons/hr_biometric_attendance/views/res_config_settings_views.xml b/third_party_addons/hr_biometric_attendance/views/res_config_settings_views.xml new file mode 100755 index 000000000..9d471ab07 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/views/res_config_settings_views.xml @@ -0,0 +1,32 @@ + + + + + res.config.settings.view.form.inherit.hr.biometric.attendance + res.config.settings + + + + + + +
+
+
+
+
+
+
+
+
+
diff --git a/third_party_addons/hr_biometric_attendance/wizards/__init__.py b/third_party_addons/hr_biometric_attendance/wizards/__init__.py new file mode 100644 index 000000000..b276b7965 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/wizards/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies(). +# 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 . +# +################################################################################ +from . import employee_biometric +from . import user_management diff --git a/third_party_addons/hr_biometric_attendance/wizards/employee_biometric.py b/third_party_addons/hr_biometric_attendance/wizards/employee_biometric.py new file mode 100644 index 000000000..04c965b49 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/wizards/employee_biometric.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies(). +# 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 . +# +################################################################################ +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_bio_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_bio_id') + def _compute_is_biometric_user(self): + """Compute if it is already a biometric user or not""" + for record in self: + if record.employee_bio_id.device_id_num: + 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_bio_id.device_id.update_user( + employee_bio_id=self.employee_bio_id.id) + else: + self.employee_bio_id.device_id.delete_user( + employee_bio_id=self.employee_bio_id.id, + employee_user_selection=None) + else: + self.employee_bio_id.device_id = self.biometric_device_id.id + self.biometric_device_id.set_user(employee_bio_id=self.employee_bio_id.id) diff --git a/third_party_addons/hr_biometric_attendance/wizards/employee_biometric_views.xml b/third_party_addons/hr_biometric_attendance/wizards/employee_biometric_views.xml new file mode 100644 index 000000000..dafeedc57 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/wizards/employee_biometric_views.xml @@ -0,0 +1,34 @@ + + + + + Biometric Management + employee.biometric + form + new + + + + employee.biometric.view.form + employee.biometric + +
+ + + + + + + + + +
+
+
+
+
+
\ No newline at end of file diff --git a/third_party_addons/hr_biometric_attendance/wizards/user_management.py b/third_party_addons/hr_biometric_attendance/wizards/user_management.py new file mode 100644 index 000000000..bca8987f2 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/wizards/user_management.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies(). +# 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 . +# +################################################################################ +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_bio_ids = fields.Many2many('hr.employee', string='Employees', compute='_compute_employee_bio_ids') + employee_bio_id = fields.Many2one( + 'hr.employee', string='Employee', help='Select the Employee', domain="[('id', 'in', employee_bio_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_bio_ids(self): + """Compute Employees By the Selected Option""" + for record in self: + if record.manage_users == 'create_user': + record.employee_bio_ids = self.env['hr.employee'].search( + [('device_ids', '!=', int(self.env.context.get('active_id')))]).ids + elif record.manage_users in ['delete_user', 'update_user']: + record.employee_bio_ids = self.env['hr.employee'].search( + [('device_ids', '=', int(self.env.context.get('active_id')))]).ids + else: + record.employee_bio_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': 'list,form', + 'domain': [('device_ids', '=', 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_bio_id=self.employee_bio_id.id) + elif self.manage_users == 'update_user': + self.env['biometric.device.details'].browse(int(self.env.context.get('active_id'))).update_user( + employee_bio_id=self.employee_bio_id.id) + else: + self.env['biometric.device.details'].browse(int(self.env.context.get('active_id'))).delete_user( + employee_bio_id=self.employee_bio_id.id, employee_user_selection=self.delete_user_selection) diff --git a/third_party_addons/hr_biometric_attendance/wizards/user_management_views.xml b/third_party_addons/hr_biometric_attendance/wizards/user_management_views.xml new file mode 100644 index 000000000..46381e679 --- /dev/null +++ b/third_party_addons/hr_biometric_attendance/wizards/user_management_views.xml @@ -0,0 +1,36 @@ + + + + + User Management + zk.user.management + form + new + + + + zk.user.management.view.form + zk.user.management + +
+ + + + + + + + + + +
+
+
+
\ No newline at end of file