From 68f3203d0d90d5d3cb097e6e30ff16770e7ac3ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Wed, 28 May 2025 16:26:02 +0800 Subject: [PATCH] 1 --- attendance_iot/__init__.py | 3 + attendance_iot/__manifest__.py | 25 +++++ attendance_iot/controllers/__init__.py | 6 ++ attendance_iot/controllers/http.py | 19 ++++ attendance_iot/controllers/kaoqin_test.py | 95 +++++++++++++++++++ attendance_iot/models/__init__.py | 4 + attendance_iot/models/attendance_iot.py | 13 +++ attendance_iot/models/attendance_iot_datas.py | 11 +++ .../models/attendance_iot_manager.py | 90 ++++++++++++++++++ attendance_iot/models/hr_employee.py | 9 ++ attendance_iot/security/ir.mode.access.csv | 5 + attendance_iot/views/attendance_iot_datas.xml | 20 ++++ .../views/attendance_iot_manager_views.xml | 46 +++++++++ attendance_iot/views/attendance_iot_views.xml | 21 ++++ .../views/inherit_hr_employee_views.xml | 22 +++++ attendance_iot/wizard/__init__.py | 1 + .../wizard/attendance_iot_manager_wizard.py | 33 +++++++ .../attendance_iot_manager_wizard_views.xml | 28 ++++++ 18 files changed, 451 insertions(+) create mode 100644 attendance_iot/__init__.py create mode 100644 attendance_iot/__manifest__.py create mode 100644 attendance_iot/controllers/__init__.py create mode 100644 attendance_iot/controllers/http.py create mode 100644 attendance_iot/controllers/kaoqin_test.py create mode 100644 attendance_iot/models/__init__.py create mode 100644 attendance_iot/models/attendance_iot.py create mode 100644 attendance_iot/models/attendance_iot_datas.py create mode 100644 attendance_iot/models/attendance_iot_manager.py create mode 100644 attendance_iot/models/hr_employee.py create mode 100644 attendance_iot/security/ir.mode.access.csv create mode 100644 attendance_iot/views/attendance_iot_datas.xml create mode 100644 attendance_iot/views/attendance_iot_manager_views.xml create mode 100644 attendance_iot/views/attendance_iot_views.xml create mode 100644 attendance_iot/views/inherit_hr_employee_views.xml create mode 100644 attendance_iot/wizard/__init__.py create mode 100644 attendance_iot/wizard/attendance_iot_manager_wizard.py create mode 100644 attendance_iot/wizard/attendance_iot_manager_wizard_views.xml diff --git a/attendance_iot/__init__.py b/attendance_iot/__init__.py new file mode 100644 index 000000000..e4f4917ae --- /dev/null +++ b/attendance_iot/__init__.py @@ -0,0 +1,3 @@ +from . import controllers +from . import models +from . import wizard diff --git a/attendance_iot/__manifest__.py b/attendance_iot/__manifest__.py new file mode 100644 index 000000000..7652b0588 --- /dev/null +++ b/attendance_iot/__manifest__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# email:pengyuthon@163.com +{ + 'name': 'Yuthon Iot Attendance', + 'summary': '', + 'description': ''' + ''', + 'author': 'pengyuthon', + 'version': '17.0.1.2.1', + 'category': 'Human Resources/Attendances', + 'sequence': 240, + 'website': 'https://www.pengyuthon.com', + 'depends': ['hr_attendance', 'base', 'hr'], + 'data': [ + 'security/ir.model.access.csv', + 'views/attendance_iot_datas.xml', + 'views/attendance_iot_manager_views.xml', + 'views/attendance_iot_views.xml', + 'views/inherit_hr_employee_views.xml', + 'wizard/attendance_iot_manager_wizard_views.xml', + ], + 'installable': True, + 'application': True, + 'license': 'LGPL-3', +} diff --git a/attendance_iot/controllers/__init__.py b/attendance_iot/controllers/__init__.py new file mode 100644 index 000000000..f9e3f1f6b --- /dev/null +++ b/attendance_iot/controllers/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- + +from . import kaoqin_test +from . import http + + diff --git a/attendance_iot/controllers/http.py b/attendance_iot/controllers/http.py new file mode 100644 index 000000000..b13daa378 --- /dev/null +++ b/attendance_iot/controllers/http.py @@ -0,0 +1,19 @@ +from odoo.http import JsonRPCDispatcher + + +class CustomJsonRPCDispatcher(JsonRPCDispatcher): + """ + 继承odoo底层源码http.py + 因为odoo本身会给对应的接口请求进行自动包装,通过判断做设备区分 + """ + def _response(self, result=None, error=None): + if 'api/v1' in self.request.httprequest.headers.environ.get('PATH_INFO'): + return self.request.make_json_response(result) + response = {'jsonrpc': '2.0', 'id': self.request_id} + if error is not None: + response['error'] = error + if result is not None: + response['result'] = result + return self.request.make_json_response(response) + + diff --git a/attendance_iot/controllers/kaoqin_test.py b/attendance_iot/controllers/kaoqin_test.py new file mode 100644 index 000000000..b772649d0 --- /dev/null +++ b/attendance_iot/controllers/kaoqin_test.py @@ -0,0 +1,95 @@ +from odoo.http import Controller, request, route +from datetime import datetime +from odoo import models, fields, api, http, SUPERUSER_ID, _ + + +class BusController(Controller): + def get_equipment_company(self, sn): + sn_address = { + 'RLM-00111018': '地址1', + } + return sn_address[sn] + + @route('/api/v1/record/face', methods=['POST'], type='json', auth='none', csrf=False) + def get_model_definitions(self): + """ + 用户通过考勤机识别,写入考勤数据 + :return: + """ + data = request.httprequest.json['logs'][0] + user_name = data.get('user_name') + employee_id = request.env(user=SUPERUSER_ID)["hr.employee"].search([('name', '=', user_name)]) + request.env(user=SUPERUSER_ID)["hr.attendance"].create({ + 'employee_id': employee_id.id, + 'check_in': datetime.strptime(data.get('recog_time'), '%Y-%m-%d %H:%M:%S') + }) + print("---------------------------------------------测试成功---------------------------------------") + return {"Result": 0, "Msg": "添加成功"} + + @route('/api/v1/user/inf_photo', methods=['POST'], type='json', auth='public') + def get_model_definitions(self): + name = request.jsonrequest.get('sn') # 姓名 + phone = request.jsonrequest.get('content') # 电话 + print(name, phone) + return { + "Result": 0, + "Msg": "更新成功" + } + + # 员工识别接口 + @route('/api/v1/stranger', methods=['POST'], type='json', auth='none') + def get_model_definitions(self): + data = request.httprequest.json.get('logs')[0] + user_name = data['user_name'] + request.env["hr.employee"].create({ + 'name': user_name + 'stranger', + }) + print("---------------------------------------------测试成功---------------------------------------") + return { + "Result": 0, + "Msg": "更新成功" + } + + @route('/api/v1/addFace', methods=['POST'], type='json', auth='none', csrf=False) + def get_model_kaoqin(self): + """ + 获取用户录入考勤机数据,和系统内用户绑定关联 + :return: + """ + data = request.httprequest.json + user_name = data.get('name') + # validity_period = datetime.strptime(data.get('validity_period'), '%Y-%m-%d').date() + employee_id = request.env(user=SUPERUSER_ID)["hr.employee"].search([('name', '=', user_name)]) + attendance_iot = request.env(user=SUPERUSER_ID)["attendance.iot.manager"] + if not attendance_iot.search([('employee_id', '=', employee_id.id), ('attendance_number', '=', data.get('sn'))]): + employee_id.write({ + 'image_1920': data.get('photo'), + }) + attendance_iot.create({ + 'data_selection': "api", + 'attendance_number': data.get('sn'), + 'attendance_address': self.get_equipment_company(data.get('sn')), + 'employee_id': employee_id.id, + }) + print("---------------------------------------------测试成功---------------------------------------") + return {"Result": 0, "Msg": "添加成功"} + + @route('/api/v1/record/face', methods=['POST'], type='json', auth='none', csrf=False) + def get_model_identify(self): + """ + 获取上传识别成功记录 + :return: + """ + data = request.httprequest.json + user_name = data.get('logs').get('user_id') + employee_id = request.env(user=SUPERUSER_ID)["hr.employee"].search([('user_id', '=', user_name.id)]) + attendance_iot = request.env(user=SUPERUSER_ID)["attendance.iot.datas"] + if not attendance_iot.search( + [('employee_id', '=', employee_id.id), ('attendance_number', '=', data.get('sn'))]): + attendance_iot.create({ + 'attendance_number': data.get('sn'), + 'attendance_date': data.get('logs').get('recog_time'), + 'employee_id': employee_id.id, + }) + print("---------------------------------------------测试成功---------------------------------------") + return {"Result": 0, "Msg": "添加成功"} diff --git a/attendance_iot/models/__init__.py b/attendance_iot/models/__init__.py new file mode 100644 index 000000000..6ecccf0e1 --- /dev/null +++ b/attendance_iot/models/__init__.py @@ -0,0 +1,4 @@ +from . import hr_employee +from . import attendance_iot +from . import attendance_iot_datas +from . import attendance_iot_manager diff --git a/attendance_iot/models/attendance_iot.py b/attendance_iot/models/attendance_iot.py new file mode 100644 index 000000000..b28f7c069 --- /dev/null +++ b/attendance_iot/models/attendance_iot.py @@ -0,0 +1,13 @@ +from odoo import api, fields, models, tools +from odoo.exceptions import ValidationError + + +class AttendanceIot(models.Model): + _name = 'attendance.iot' + _description = '考勤机设备' + _rec_name = 'name' + + name = fields.Char(string="设备号") + attendance_address = fields.Char(string="设备地址") + broker = fields.Char(string='服务器地址/ip') + port = fields.Char(string='端口号') diff --git a/attendance_iot/models/attendance_iot_datas.py b/attendance_iot/models/attendance_iot_datas.py new file mode 100644 index 000000000..3f71d699b --- /dev/null +++ b/attendance_iot/models/attendance_iot_datas.py @@ -0,0 +1,11 @@ +from odoo import api, fields, models, tools +from odoo.exceptions import ValidationError + + +class AttendanceIotDatas(models.Model): + _name = 'attendance.iot.datas' + _description = '上传识别记录' + + employee_id = fields.Many2one('hr.employee', string='人员') + attendance_date = fields.Datetime(string='打卡时间') + attendance_iot_id = fields.Many2one('attendance.iot', string="设备号") diff --git a/attendance_iot/models/attendance_iot_manager.py b/attendance_iot/models/attendance_iot_manager.py new file mode 100644 index 000000000..b3fe1b2b2 --- /dev/null +++ b/attendance_iot/models/attendance_iot_manager.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +from odoo import api, fields, models, tools, _ +from datetime import datetime, time, timedelta +from odoo import api, fields, models, tools +from odoo.exceptions import UserError +import paho.mqtt.client as mqtt +import logging +_logger = logging.getLogger(__name__) + + +class AttendanceIotManager(models.Model): + _name = 'attendance.iot.manager' + _description = "考勤机管理" + + attendance_iot_id = fields.Many2one('attendance.iot', string="设备号") + attendance_address = fields.Char(string="设备地址") + employee_id = fields.Many2one('hr.employee', string="员工") + id_valid = fields.Date(string="数据有效期") + data_selection = fields.Selection([('mqtt', 'mqtt'), ('api', 'api')], default="mqtt", string="数据类型") + + def action_attendance_iot_manager(self): + return { + "type": "ir.actions.act_window", + "name": "批量导入考勤机", + "res_model": "attendance.iot.manager.wizard", + "view_mode": "form", + "target": "new", + } + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals['data_selection'] == 'mqtt': + self.mqtt_request(self, vals['attendance_number'], 'addUser') + return super().create(vals_list) + + def mqtt_request(self, attendance_number, data): + reuqired_params = self.get_required_params(data) + if any(reuqired_param not in data.keys() for reuqired_param in reuqired_params): + raise UserError(f'缺少必填参数,请确认: {",".join(reuqired_params)}') + mqtt_config = self.get_mqtt_config() + self.start_mqtt(mqtt_config) + self.publish_message(mqtt_config, f'iot/device/{attendance_number}', data) + + def get_required_params(self, cmd): + params_dict = { + 'addUser': ['cmd', 'user_id', 'name', 'id_valid'], + 'editUser': ['cmd', 'user_id', 'edit_mode'], + 'delUser': ['cmd'], + 'delMultiUser': ['cmd', 'user_ids'], + 'setPasswordVerify': ['cmd', 'value'], + 'setOpenDoorPassword': ['cmd', 'value'], + } + if cmd not in params_dict.keys(): + raise UserError(f'指令不存在,请确认,目前支持: {",".join(params_dict.keys())}') + else: + return params_dict[cmd] + + @api.model + def get_mqtt_config(self): + mqtt_config = self.search([], limit=1) + return mqtt_config + + @api.model + def start_mqtt(self, mqtt_config): + def on_connect(client, userdata, flags, rc): + if rc == 0: + _logger.info("Connected to MQTT Broker") + else: + _logger.info(f"Failed to connect, return code {rc}", ) + + def on_message(client, userdata, message): + _logger.info(f"Received message: {str(message.payload.decode('utf-8'))}", ) + client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "OdooMQTT") + client.on_connect = on_connect + client.on_message = on_message + client.connect(mqtt_config.broker, mqtt_config.port, 60) + client.loop_start() + + @api.model + def publish_message(self, mqtt_config, topic, message): + client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, "OdooMQTT") + client.connect(mqtt_config.broker, mqtt_config.port, 60) + res = client.publish(topic, message) + if res.get('code') == 0: + _logger.info("操作成功") + else: + _logger.info(f"操作失败,返回错误码: {res.get('code')}") + client.disconnect() + diff --git a/attendance_iot/models/hr_employee.py b/attendance_iot/models/hr_employee.py new file mode 100644 index 000000000..57ff9a2cf --- /dev/null +++ b/attendance_iot/models/hr_employee.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, exceptions, _ + + +class HrEmployee(models.Model): + _inherit = "hr.employee" + _description = "考勤机和用户信息的绑定" + + attendance_iot_ids = fields.One2many('attendance.iot.manager', 'employee_id', string="考勤机信息") diff --git a/attendance_iot/security/ir.mode.access.csv b/attendance_iot/security/ir.mode.access.csv new file mode 100644 index 000000000..091bbb8e9 --- /dev/null +++ b/attendance_iot/security/ir.mode.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_attendance_iot_manager,attendance_iot_manager,model_attendance_iot_manager,base.group_user,1,1,1,1 +access_attendance_iot_manager_wizard,attendance_iot_manager_wizard,model_attendance_iot_manager_wizard,base.group_user,1,1,1,1 +access_attendance_iot_datas,attendance_iot_datas,model_attendance_iot_datas,base.group_user,1,1,1,1 +access_attendance_iot,attendance_iot,model_attendance_iot,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/attendance_iot/views/attendance_iot_datas.xml b/attendance_iot/views/attendance_iot_datas.xml new file mode 100644 index 000000000..646f6da7d --- /dev/null +++ b/attendance_iot/views/attendance_iot_datas.xml @@ -0,0 +1,20 @@ + + + + attendance.iot.datas.tree + attendance.iot.datas + + + + + + + + + + + 上传记录 + attendance.iot.datas + tree + + \ No newline at end of file diff --git a/attendance_iot/views/attendance_iot_manager_views.xml b/attendance_iot/views/attendance_iot_manager_views.xml new file mode 100644 index 000000000..45004e72c --- /dev/null +++ b/attendance_iot/views/attendance_iot_manager_views.xml @@ -0,0 +1,46 @@ + + + + attendance.iot.manager.tree + attendance.iot.manager + + + + + + + + + + + + + attendance.iot.manager.search + attendance.iot.manager + + + + + + + + + + + 考勤机设备管理 + attendance.iot.manager + tree,form + + + { "search_default_attendance_iot_id":1, } + + + + + 批量录入考勤机 + + + code + action = model.action_attendance_iot_manager() + + \ No newline at end of file diff --git a/attendance_iot/views/attendance_iot_views.xml b/attendance_iot/views/attendance_iot_views.xml new file mode 100644 index 000000000..73f536e3f --- /dev/null +++ b/attendance_iot/views/attendance_iot_views.xml @@ -0,0 +1,21 @@ + + + + 考勤机设备管理 + attendance.iot + + + + + + + + + + + + 考勤机设备管理 + attendance.iot + tree,form + + \ No newline at end of file diff --git a/attendance_iot/views/inherit_hr_employee_views.xml b/attendance_iot/views/inherit_hr_employee_views.xml new file mode 100644 index 000000000..197b23551 --- /dev/null +++ b/attendance_iot/views/inherit_hr_employee_views.xml @@ -0,0 +1,22 @@ + + + + hr.employee.inherit.attendance.form + hr.employee + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/attendance_iot/wizard/__init__.py b/attendance_iot/wizard/__init__.py new file mode 100644 index 000000000..030334691 --- /dev/null +++ b/attendance_iot/wizard/__init__.py @@ -0,0 +1 @@ +from . import attendance_iot_manager_wizard diff --git a/attendance_iot/wizard/attendance_iot_manager_wizard.py b/attendance_iot/wizard/attendance_iot_manager_wizard.py new file mode 100644 index 000000000..85c6b8bcf --- /dev/null +++ b/attendance_iot/wizard/attendance_iot_manager_wizard.py @@ -0,0 +1,33 @@ +from odoo import api, fields, models, tools +from odoo.exceptions import ValidationError + + +class AttendanceIotManagerWizard(models.TransientModel): + _name = 'attendance.iot.manager.wizard' + _description = '批量导入考勤用户数据弹窗' + + attendance_iot_id = fields.Many2one('attendance.iot', string="设备号") + department_ids = fields.Many2many('hr.department', string='部门范围') + employee_ids = fields.Many2many('hr.employee', string='人员范围') + users_role_ids = fields.Many2many('soong.user.role', string='角色范围', domain=[('user_type', '!=', 'lv_role')]) + iot_users_ids = fields.Many2many('res.users', string="人员", compute="compute_iot_users_ids", store=True) + + @api.depends('department_ids', 'employee_ids', 'users_role_ids') + def compute_iot_users_ids(self): + users_role_list = department_users_list = [] + # 部门人员 + for depart in self.department_ids: + department_users_data = self.env['hr.employee'].search([('department_id', '=', depart.id)]) + department_users_list = [de.user_id.id for de in department_users_data] + # 人员 + employee_users_list = self.employee_ids.user_id.ids + # 角色人员 + for user_id in self.users_role_ids: + department_users_data = self.env['hr.employee'].search([('user_role_id', '=', user_id.id)]) + users_role_list = [ro.user_id.id for ro in department_users_data] + publish_user_list = department_users_list + employee_users_list + users_role_list + self.iot_users_ids = [(6, 0, publish_user_list)] + + def confirm(self): + pass + diff --git a/attendance_iot/wizard/attendance_iot_manager_wizard_views.xml b/attendance_iot/wizard/attendance_iot_manager_wizard_views.xml new file mode 100644 index 000000000..496796972 --- /dev/null +++ b/attendance_iot/wizard/attendance_iot_manager_wizard_views.xml @@ -0,0 +1,28 @@ + + + + 批量导入考勤用户数据弹窗 + attendance.iot.manager.wizard + +
+ + + + + + + + + + + + + + +
+
+
+
\ No newline at end of file