diff --git a/yuthon_wechat/__init__.py b/yuthon_wechat/__init__.py
index 20b498afe..b276f2f3f 100644
--- a/yuthon_wechat/__init__.py
+++ b/yuthon_wechat/__init__.py
@@ -3,4 +3,4 @@
from . import models
from . import controllers
from . import wizard
-
+from . import utils
diff --git a/yuthon_wechat/__manifest__.py b/yuthon_wechat/__manifest__.py
index 48a48193c..7ce5d0996 100644
--- a/yuthon_wechat/__manifest__.py
+++ b/yuthon_wechat/__manifest__.py
@@ -18,6 +18,7 @@
'depends': ['base', 'auth_oauth', 'hr'],
'data': [
'security/ir.model.access.csv',
+ 'data/hr_attendance_time.xml',
# 'data/wecom_apps_cron_data.xml',
'data/wecom_api_data.xml',
'data/auth_oauth_provider.xml',
@@ -25,7 +26,10 @@
'views/wecom_apps_views.xml',
'views/hr_employee_views.xml',
'views/wecom_apps_config_views.xml',
- 'wizard/wecom_api_sync_wizard.xml'
+ 'wizard/wecom_api_sync_wizard.xml',
+ 'views/hr_attendance_views.xml',
+ 'views/hr_attendance_detail_views.xml',
+ 'views/wx_work_views.xml',
],
'application': True,
'installable': True,
diff --git a/yuthon_wechat/data/hr_attendance_time.xml b/yuthon_wechat/data/hr_attendance_time.xml
new file mode 100644
index 000000000..08ec7083c
--- /dev/null
+++ b/yuthon_wechat/data/hr_attendance_time.xml
@@ -0,0 +1,18 @@
+
+
+
+
+ 定时任务:定时抓取上下班打卡信息
+
+ code
+ model.get_hr_attendance_time()
+
+
+ 120
+ months
+
+ -1
+
+
+
+
\ No newline at end of file
diff --git a/yuthon_wechat/models/__init__.py b/yuthon_wechat/models/__init__.py
index 3e861e4bd..8412b8fe6 100644
--- a/yuthon_wechat/models/__init__.py
+++ b/yuthon_wechat/models/__init__.py
@@ -1,11 +1,13 @@
# -*- coding: utf-8 -*-
-
from . import wecom_api
from . import wecom_apps
from . import hr_employee
from . import res_users
from . import wecom_apps_config
+from . import hr_attendance_detail
+from . import wx_work
+from . import hr_attendance
diff --git a/yuthon_wechat/models/hr_attendance.py b/yuthon_wechat/models/hr_attendance.py
new file mode 100644
index 000000000..9734d871a
--- /dev/null
+++ b/yuthon_wechat/models/hr_attendance.py
@@ -0,0 +1,161 @@
+# -*- coding: utf-8 -*-
+import pytz
+from datetime import datetime, timedelta
+from odoo import api, fields, models, tools, _
+
+
+class HrAttendance(models.Model):
+ _inherit = "hr.attendance"
+ _order = 'current_data desc, check_in desc'
+
+ userid = fields.Char(string='userid')
+ groupname = fields.Char(string='打卡规则名称')
+ current_data = fields.Date(string='打卡日期', tracking=True)
+ sch_check_in_time = fields.Datetime(string='上班正常时间')
+ sch_check_out_time = fields.Datetime(string='下班正常时间')
+ sch_check_noon_time = fields.Datetime(string='中午正常时间')
+ check_noon = fields.Datetime(string='中午打卡', tracking=True)
+ check_in = fields.Datetime(string="上班打卡", required=False, default=False, tracking=True)
+ check_out = fields.Datetime(string="下班打卡", tracking=True)
+ attendance_type = fields.Selection([('wx_work', '企业微信')], readonly=True, string='来源')
+ # field_work_check = fields.Datetime(string="外勤打卡")
+
+ attendance_detail_ids = fields.One2many(string="打卡详情", comodel_name='hr.attendance.detail', inverse_name='hr_attendance_id')
+ replacement_approval_ids = fields.One2many(string="补卡详情", comodel_name='approval2.request', compute='_compute_replacement_approval_ids')
+ go_out_approval_ids = fields.One2many(string="外出详情", comodel_name='approval2.request', compute='_compute_go_out_approval_ids')
+ work_overtime_approval_ids = fields.One2many(string="加班详情", comodel_name='approval2.request', compute='_compute_work_overtime_approval_ids')
+ hr_leave_approval_ids = fields.One2many(string="请假详情", comodel_name='hr.leave', compute='_compute_hr_leave_approval_ids')
+
+ def action_show_details(self):
+ self.ensure_one()
+ view = self.env.ref("yuthon_wechat.view_hr_attendance_detail_tree")
+ form_view = self.env.ref("yuthon_wechat.view_hr_attendance_detail_form")
+ return {
+ "name": _("出勤详情"),
+ "type": "ir.actions.act_window",
+ "view_mode": "tree,form",
+ "res_model": "hr.attendance.detail",
+ "views": [(view.id, "tree"), (form_view.id, "form")],
+ "view_id": view.id,
+ "target": "current",
+ "res_id": self.id,
+ "domain": [('hr_attendance_id', '=', self.id)],
+ "context": {'hr_attendance_id': self.id}
+ }
+
+ @api.constrains('check_in', 'check_out', 'employee_id')
+ def _check_validity(self):
+ for attendance in self:
+ if attendance.attendance_type != 'wx_work':
+ super(HrAttendance, attendance)._check_validity()
+
+
+ def get_work_time(self, attendance_data, current_date, tz):
+ """
+ 当前时间的工作时间段
+ :param attendance_data:
+ :param current_date:
+ :param tz:
+ :return:
+ """
+ work_time_list = []
+ for work_time in attendance_data:
+ work_time_list.append(self.time_to_utc_time(self.str_to_time(current_date, work_time.hour_from), tz))
+ work_time_list.append(self.time_to_utc_time(self.str_to_time(current_date, work_time.hour_to), tz))
+ work_time_sorted = sorted(work_time_list)
+ go_from, go_to, off_from, off_to = work_time_sorted
+ return go_from, go_to, off_from, off_to, current_date
+
+ @api.depends('check_in', 'sch_check_in_time')
+ def is_come_late(self):
+ self.ensure_one()
+ if self.check_in and self.sch_check_in_time and self.check_in > self.sch_check_in_time:
+ return True
+ return False
+
+ def get_employee_work_time(self, employee, current_week, current_date, tz):
+ """
+ 获取员工的当天是否应该出勤以及出勤时间
+ :param employee: 员工
+ :param current_week: 当天周
+ :param current_date: 当前天
+ :param tz: 时区
+ :return: 出勤时间
+ """
+ # 获取员工的工作信息
+ resource_calendar = employee.resource_calendar_id
+ # 获取工作表
+ attendance_ids = resource_calendar.attendance_ids
+ # 获取公休息日表
+ global_leave = resource_calendar.global_leave_ids
+ # 有没有工作规则
+ if not attendance_ids:
+ return False
+ # 是不是工作时间
+ attendance_data = resource_calendar.attendance_ids.filtered(lambda s: s.dayofweek == str(current_week))
+ if not attendance_data:
+ return False
+ # 获取工作时间
+ work_time = self.get_work_time(attendance_data, current_date, tz)
+ if not work_time:
+ return False
+ go_from, go_to, off_from, off_to, current_date = work_time
+ # 早上和下午上班 判断当天 是不是公休日
+ if self.get_leave_day(global_leave, go_from, tz) and self.get_leave_day(global_leave, off_from, tz):
+ return False
+ return go_from, go_to, off_from, off_to, current_date
+
+ def get_leave_day(self, global_leave, current_time, tz):
+ """
+ 判断是否是公休日
+ :param global_leave:
+ :param current_time:
+ :param tz:
+ :return:
+ """
+ if not global_leave:
+ return False
+ for leave in global_leave:
+ if self.utc_time_to_tz(tz, leave.date_from) <= current_time.strftime("%Y-%m-%d %H:%M:%S") <= self.utc_time_to_tz(tz, leave.date_to):
+ return True
+ return False
+
+ @staticmethod
+ def utc_time_to_tz(tz, chin):
+ """
+ utc时间的时区转换tz时间
+ :param tz: 时区
+ :param chin: 时间
+ :return: 转换时间
+ """
+ time_timezone = pytz.timezone('utc').localize(chin, is_dst=False)
+ time_to_utc = time_timezone.astimezone(pytz.timezone(tz))
+ utc_time_str = time_to_utc.strftime("%Y-%m-%d %H:%M:%S")
+ return utc_time_str
+
+ @staticmethod
+ def time_to_utc_time(data, tz):
+ """
+ 本地时间转换成tz时间
+ :param data: 时间
+ :param tz: 时区
+ :return: 转换时间
+ """
+ time_timezone = pytz.timezone(tz).localize(data, is_dst=False)
+ time_to_utc = time_timezone.astimezone(pytz.timezone("utc"))
+ utc_time_str = time_to_utc.strftime("%Y-%m-%d %H:%M:%S")
+ utc_time = datetime.strptime(utc_time_str, "%Y-%m-%d %H:%M:%S")
+ return utc_time
+
+ @staticmethod
+ def str_to_time(current_data, data):
+ """
+ %d:%d 的 str类型的时间转换成时间
+ :param current_data: 当前日期
+ :param data: str类型时间
+ :return: 转换后的时间
+ """
+ from_time = "%d:%d" % (int(data), round((data - int(data)) * 60))
+ from_times = str(current_data) + " " + from_time
+ from_ti = datetime.strptime(from_times, "%Y-%m-%d %H:%M")
+ return from_ti
diff --git a/yuthon_wechat/models/hr_attendance_detail.py b/yuthon_wechat/models/hr_attendance_detail.py
new file mode 100644
index 000000000..bc93dd34c
--- /dev/null
+++ b/yuthon_wechat/models/hr_attendance_detail.py
@@ -0,0 +1,330 @@
+# -*- coding: utf-8 -*-
+import os
+import base64
+import datetime
+import logging
+import traceback
+from odoo import api, fields, models, tools, _
+from ...yuthon_wechat.utils.common import utils_list_page
+
+_logger = logging.getLogger(__name__)
+
+
+class HrAttendanceDetailImage(models.Model):
+ _name = "hr.attendance.detail.image"
+ _description = "打卡原始抓取数据图片"
+
+ attendance_detail_id = fields.Many2one('hr.attendance.detail', string="原始打卡记录", ondelete="cascade", readonly=True)
+ media_url = fields.Char(string='附件地址')
+ media_file = fields.Binary(string='附件文件', compute="_compute_media_file")
+
+ @api.depends('media_url')
+ def _compute_media_file(self):
+ for record in self:
+ record.media_file = None
+ wx_work_auth = self.env['auth.oauth.provider'].search([('name', '=', 'Wechat Card')], limit=1)
+ if record.media_url:
+ image_path = os.path.join(wx_work_auth.wx_work_image_path, record.media_url)
+ record.media_file = base64.b64encode(open(image_path, 'rb').read()) if image_path else False
+
+
+class HrAttendanceDetail(models.Model):
+ _name = "hr.attendance.detail"
+ _description = "打卡原始抓取数据"
+ _order = 'checkin_time desc'
+
+ name = fields.Char(related='employee_id.name')
+ employee_id = fields.Many2one('hr.employee', string="员工", readonly=True)
+ department_id = fields.Many2one('hr.department', string="部门", related="employee_id.department_id", readonly=True)
+ userid = fields.Char(string='userid')
+ groupname = fields.Char(string='打卡规则名称')
+ checkin_type = fields.Char(string='打卡类型')
+ exception_type = fields.Char(string='异常类型')
+ checkin_time = fields.Datetime(string='打卡时间')
+ location_title = fields.Char(string='打卡地点')
+ location_detail = fields.Char(string='打卡地点详情')
+ wifiname = fields.Char(string='打卡wifi名称')
+ notes = fields.Char(string='打卡备注')
+ wifimac = fields.Char(string='打卡的MAC地址')
+ mediaids = fields.Char(string='打卡的附件media_id')
+ lat = fields.Integer(string='位置打卡地点纬度')
+ lng = fields.Integer(string='位置打卡地点经度')
+ deviceid = fields.Char(string='打卡设备id')
+ sch_checkin_time = fields.Datetime(string='标准打卡时间')
+ groupid = fields.Integer(string='规则id')
+ schedule_id = fields.Char(string='班次id')
+ timeline_id = fields.Integer(string='时段id')
+
+ attendance_detail_image_ids = fields.One2many(string="打卡记录图片", comodel_name='hr.attendance.detail.image', inverse_name='attendance_detail_id')
+ hr_attendance_id = fields.Many2one('hr.attendance', string="出勤记录", ondelete="cascade", readonly=True)
+
+ def action_show_details(self):
+ self.ensure_one()
+ view = self.env.ref("yuthon_wechat.view_hr_attendance_detail_image_tree")
+ return {
+ "name": _("图片详情"),
+ "type": "ir.actions.act_window",
+ "view_mode": "form",
+ "res_model": "hr.attendance.detail.image",
+ "views": [(view.id, "tree")],
+ "view_id": view.id,
+ "target": "new",
+ "res_id": self.id,
+ "domain": [('attendance_detail_id', '=', self.id)],
+ "context": {'default_attendance_detail_id': self.id}
+ }
+
+ def _get_wx_attendance_user_list(self):
+ """
+ 获取企业微信的关联用户
+ :return: 用户列表
+ """
+ # userid 从用户列表里获取企业微信的用户
+ user_obj = self.env['res.users'].search([])
+ # 暂时先用for循环,如果后期加大频率,需改成并发
+ user_list = []
+ for user in user_obj:
+ if user.oauth_uid:
+ user_list.append(user.oauth_uid)
+ return user_list
+
+ @staticmethod
+ def _get_wx_attendance_data(userid_limit):
+ """
+ 获取企业微信的打卡信息
+ :param wx_work_checkin_api: 企业微信api
+ :param userid_limit: 用户列表
+ :return:
+ """
+ # 先按照天抓取,时间戳
+ now_time = datetime.datetime.now() + datetime.timedelta(hours=8)
+ start_time = now_time - datetime.timedelta(days=30)
+ start_time_stamp = int(start_time.timestamp())
+ end_time_stamp = int(now_time.timestamp())
+ # 整理请求信息
+ data = {
+ "opencheckindatatype": 3,
+ "starttime": start_time_stamp,
+ "endtime": end_time_stamp,
+ "need_photo": 1,
+ "useridlist": userid_limit
+ }
+ # 重写一个类处理result获取想要的结果
+
+ return data
+
+ @staticmethod
+ def _check_wx_attendance_data(response, userid_limit):
+ """
+ 检查请求返回的信息是否正确
+ :param response: 请求返回信息
+ :param userid_limit: 用户列表
+ :return: 检查结果
+ """
+ # 对返回信息进行整理
+ if response.status_code != 200:
+ _logger.error('抓取出勤数据请求失败,异常:{0},用户列表{1}'.format(str(response.json()), str(userid_limit)))
+ return []
+ result = response.json()
+ if result.get('errcode') != 0:
+ _logger.error('获取外出打卡数据失败,异常:{0},用户列表{1}'.format(str(result.get('errmsg')), str(userid_limit)))
+ return []
+ checkin_data = result.get('checkindata', [])
+ if not checkin_data:
+ _logger.error('没有找到外出打卡记录,异常:{0},用户列表{1}'.format(str(response.json()), str(userid_limit)))
+ return []
+ return checkin_data
+
+ def _check_wx_attendance_time(self, data, userid_limit):
+ """
+ 检查保存之前的数据是否准确,并且分析出想要的打卡时间
+ :param data: 原始数据
+ :param userid_limit: 用户列表
+ :return: 分析后的打卡数据
+ """
+ userid = data.get("userid", "")
+ checkin_time_stamp = data.get("checkin_time", "")
+ sch_checkin_time_stamp = data.get("sch_checkin_time", None)
+ if not userid or not checkin_time_stamp:
+ _logger.error('没有抓取到用户出勤信息,异常:{0},用户列表{1}'.format(str(data), str(userid_limit)))
+ return False
+ checkin_time = datetime.datetime.fromtimestamp(checkin_time_stamp) if checkin_time_stamp else None
+ sch_checkin_time = datetime.datetime.fromtimestamp(sch_checkin_time_stamp) if sch_checkin_time_stamp else None
+ return checkin_time, sch_checkin_time
+
+ def _check_wx_attendance_media(self, data):
+ """
+ 分析返回来的数据获取图片原图
+ :param data: 原始数据
+ :param userid_limit: 用户列表
+ :return: 获取后的图片
+ """
+ media_list = []
+ userid = data.get("userid", "")
+ mediaids_list = data.get("mediaids", [])
+ if not mediaids_list:
+ return media_list
+
+ for media_id in mediaids_list:
+ data = {
+ "media_id": media_id
+ }
+ try:
+ result, response = self.sudo().env["wecom.apps"].sync_media_get(data)
+ if result:
+ filename = response.headers['Content-Disposition'].split('; ')[2].replace('filename=', '').replace('"', '')
+ wx_work_auth = self.env['auth.oauth.provider'].search([('name', '=', 'Wechat Card')], limit=1)
+ wx_work_image = os.path.join(wx_work_auth.wx_work_image_path, str(filename))
+ with open(wx_work_image, 'wb') as f:
+ f.write(response.content)
+ media_list.append((0, 0, {
+ 'media_url': filename,
+ }))
+ else:
+ _logger.error('检查保存之前的数据分析出想要的数据失败,异常:{0},数据信息{1}'.format(str(response), str(data)))
+ except Exception as e:
+ _logger.error('请求原始图片失败,异常:{0},数据信息{1},异常详情:{2}'.format(str(e), str(data), str(traceback.format_exc())))
+
+ return media_list
+
+ def _save_wx_attendance_detail_data(self, data, checkin_time, sch_checkin_time):
+ """
+ 保存相关的企业微信的原始打卡信息
+ :param data: 请求返回后的处理信息
+ :param checkin_time: 打卡时间
+ :param sch_checkin_time: 应打卡时间
+ :param media_list: 图片列表
+ :return: 保存结果
+ """
+ userid = data.get("userid", "")
+ # 查询是否存在数据
+ if self.env['hr.attendance.detail'].sudo().search_count([("userid", "=", userid), ("checkin_time", "=", checkin_time)]) <= 0:
+ res_user_obj = self.env['res.users'].sudo().search([("oauth_uid", "=", userid)], limit=1)
+ hr_employee_obj = self.env['hr.employee'].sudo().search([("user_id", "=", res_user_obj.id)], limit=1)
+ if not res_user_obj or not hr_employee_obj:
+ _logger.error('检查保存之前的数据异常,异常:无关联的员工数据,数据信息{0}'.format(str(data)))
+ return False
+ attendance_detail_obj = self.env['hr.attendance.detail'].sudo().create({
+ 'employee_id': hr_employee_obj.id,
+ "userid": userid,
+ "groupname": data.get("groupname", ""),
+ "checkin_type": data.get("checkin_type", ""),
+ "exception_type": data.get("exception_type", ""),
+ "checkin_time": checkin_time,
+ "location_title": data.get("location_title", ""),
+ "location_detail": data.get("location_detail", ""),
+ "wifiname": data.get("wifiname", ""),
+ "notes": data.get("notes", ""),
+ "wifimac": data.get("wifimac", ""),
+ "mediaids": str(data.get("mediaids", "")),
+ "lat": data.get("lat", 0),
+ "lng": data.get("lng", 0),
+ "deviceid": data.get("deviceid", ""),
+ "sch_checkin_time": sch_checkin_time,
+ "groupid": data.get("groupid", 0),
+ "schedule_id": data.get("schedule_id", ""),
+ "timeline_id": data.get("timeline_id", 0),
+ # "attendance_detail_image_ids": media_list
+ })
+
+ # 分析返回来的数据获取图片原图
+ try:
+ media_list = self._check_wx_attendance_media(data)
+ attendance_detail_obj.attendance_detail_image_ids = media_list
+ except Exception as e:
+ _logger.error('检查保存之前的数据分析出想要的数据失败,异常:{0},数据信息{1},异常详情:{2}'.format(str(e), str(data), str(traceback.format_exc())))
+
+ userid = data.get("userid", "")
+ # 区分当天的上班,下班,中午打卡,合并一个
+ checkin_time_now = checkin_time + datetime.timedelta(hours=8) if checkin_time else None
+ sch_checkin_time_now = sch_checkin_time + datetime.timedelta(hours=8) if sch_checkin_time else None
+ current_data = (sch_checkin_time + datetime.timedelta(hours=8)).strftime("%Y-%m-%d") if sch_checkin_time_now else checkin_time_now.strftime("%Y-%m-%d")
+ # 同步整理当天出勤数据
+ attendance_obj = self.env['hr.attendance'].search([("userid", "=", userid), ("current_data", "=", current_data)], limit=1)
+ if not attendance_obj:
+ res_user_obj = self.env['res.users'].search([("oauth_uid", "=", userid)], limit=1)
+ hr_employee_obj = self.env['hr.employee'].sudo().search([("user_id", "=", res_user_obj.id)], limit=1)
+ attendance_obj = self.env['hr.attendance'].sudo().create({
+ 'employee_id': hr_employee_obj.id,
+ "userid": userid,
+ "current_data": current_data,
+ "attendance_type": "wx_work"
+ })
+ # 赋值企业微信打卡数据
+ attendance_obj.attendance_type = "wx_work"
+
+ checkin_type = data.get("checkin_type", "")
+ exception_type = data.get("exception_type", "")
+ if exception_type != "未打卡":
+ if checkin_time < attendance_obj.check_in:
+ attendance_obj.sudo().write({
+ "sch_check_in_time": sch_checkin_time,
+ "check_in": checkin_time
+ })
+ if checkin_time > attendance_obj.check_out:
+ attendance_obj.sudo().write({
+ "sch_check_out_time": sch_checkin_time,
+ "check_out": checkin_time
+ })
+ # if sch_checkin_time_now and str(sch_checkin_time_now.strftime("%H")) == "13" and checkin_type == "上班打卡" and exception_type != "未打卡":
+ # attendance_obj.sudo().write({
+ # "sch_check_noon_time": sch_checkin_time,
+ # "check_noon": checkin_time
+ # })
+
+ # elif checkin_type == "上班打卡" and exception_type != "未打卡":
+ # attendance_obj.sudo().write({
+ # "sch_check_in_time": sch_checkin_time,
+ # "check_in": checkin_time
+ # })
+ # elif checkin_type == "下班打卡" and exception_type != "未打卡":
+ # attendance_obj.sudo().write({
+ # "sch_check_out_time": sch_checkin_time,
+ # "check_out": checkin_time
+ # })
+ # elif checkin_type == "外出打卡":
+ # attendance_obj.sudo().write({
+ # "field_work_check": checkin_time,
+ # })
+ # 进行关联
+ attendance_detail_obj.hr_attendance_id = attendance_obj.id
+ return True
+
+ def get_hr_attendance_time(self):
+ """
+ 获取企业微信打卡数据的核心逻辑
+ :return: 处理结果
+ """
+ # 从用户列表里获取企业微信的用户
+ user_list = self._get_wx_attendance_user_list()
+ # 企业微信获取打卡数据,要求用户分组100,分批获取打卡记录
+ for userid_limit in utils_list_page(user_list, 100):
+ # 获取企业微信的打卡信息
+ try:
+ attendance_data = self._get_wx_attendance_data(userid_limit)
+ attendance_result, response = self.sudo().env["wecom.apps"].sync_checkin_data(attendance_data)
+ except Exception as e:
+ _logger.error('获取企业微信的打卡信息失败,异常:{0},用户列表{1},异常详情:{2}'.format(str(e), str(userid_limit), str(traceback.format_exc())))
+ continue
+ # 检查请求返回的信息是否正确
+ try:
+ checkin_data = self._check_wx_attendance_data(response, userid_limit)
+ except Exception as e:
+ _logger.error('检查请求返回的信息失败,异常:{0},用户列表{1},异常详情:{2}'.format(str(e), str(userid_limit), str(traceback.format_exc())))
+ continue
+ # 将原始数据存储
+ for data in checkin_data:
+ # 检查保存之前的数据是否准确,并且分析出想要的打卡时间
+ try:
+ checkin_time, sch_checkin_time = self._check_wx_attendance_time(data, userid_limit)
+ except Exception as e:
+ _logger.error('检查保存之前的数据分析出想要的数据失败,异常:{0},数据信息{1},异常详情:{2}'.format(str(e), str(data), str(traceback.format_exc())))
+ continue
+ # 保存相关的企业微信的原始打卡信息
+ try:
+ self._save_wx_attendance_detail_data(data, checkin_time, sch_checkin_time)
+ except Exception as e:
+ _logger.error('保存相关的企业微信的原始打卡信息失败,异常:{0},数据信息{1},异常详情:{2}'.format(str(e), str(data), str(traceback.format_exc())))
+ return True
+
+
diff --git a/yuthon_wechat/models/wx_work.py b/yuthon_wechat/models/wx_work.py
new file mode 100644
index 000000000..24dead8d8
--- /dev/null
+++ b/yuthon_wechat/models/wx_work.py
@@ -0,0 +1,12 @@
+import logging
+import json
+from datetime import datetime, timedelta
+from odoo import models, fields
+
+_logger = logging.getLogger(__name__)
+
+
+class AuthOAuthProvider(models.Model):
+ _inherit = "auth.oauth.provider"
+
+ wx_work_image_path = fields.Char(string="企业微信图片保存地址")
diff --git a/yuthon_wechat/security/ir.model.access.csv b/yuthon_wechat/security/ir.model.access.csv
index 9f6991423..bb47359ec 100644
--- a/yuthon_wechat/security/ir.model.access.csv
+++ b/yuthon_wechat/security/ir.model.access.csv
@@ -4,3 +4,9 @@ access_wecom_apps,wecom.apps.user,model_wecom_apps,,1,1,1,1
access__wecom_apps_config,wecom.app.config.user,model_wecom_apps_config,,1,1,1,1
access_wecom_api_sync_wizard_manager,wecom.api.sync.wizard,model_wecom_api_sync_wizard,,1,1,1,1
+
+access_hr_attendance_detail_user,hr.attendance.detail.user,model_hr_attendance_detail,base.group_user,1,0,0,0
+access_hr_attendance_detail_manager,hr.attendance.detail.manager,model_hr_attendance_detail,base.group_user,1,1,1,1
+
+access_hr_attendance_detail_image_user,hr.attendance.detail.image.user,model_hr_attendance_detail_image,base.group_user,1,0,0,0
+access_hr_attendance_detail_image_manager,hr.attendance.detail.image.manager,model_hr_attendance_detail_image,base.group_user,1,1,1,1
diff --git a/yuthon_wechat/utils/__init__.py b/yuthon_wechat/utils/__init__.py
new file mode 100644
index 000000000..aa9c3f5d9
--- /dev/null
+++ b/yuthon_wechat/utils/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import common
diff --git a/yuthon_wechat/utils/common.py b/yuthon_wechat/utils/common.py
new file mode 100644
index 000000000..c58026e43
--- /dev/null
+++ b/yuthon_wechat/utils/common.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+import os
+
+def utils_list_page(data, limit):
+ """
+ 列表平分
+ :param data: list
+ :param limit: 每组多少个
+ :return: 平分结果
+ """
+ for start in range(0, len(data), limit):
+ if start + limit > len(data):
+ end = len(data)
+ else:
+ end = start + limit
+ yield data[start:end]
+
+
+def os_path_join(path, filename):
+ """
+ 默认传入的是debian的路径 /xx/xx/xx/转换成多系统兼容
+ :param path:
+ :param filename:
+ :return:
+ """
+ image_path_list = path.split('/')
+ image_path = ""
+ for path in image_path_list:
+ image_path = os.path.join(str(image_path), str(path))
+ image = os.path.join(image_path, filename)
+ return image
diff --git a/yuthon_wechat/views/hr_attendance_detail_views.xml b/yuthon_wechat/views/hr_attendance_detail_views.xml
new file mode 100644
index 000000000..f1c3b70f6
--- /dev/null
+++ b/yuthon_wechat/views/hr_attendance_detail_views.xml
@@ -0,0 +1,80 @@
+
+
+
+ hr.attendance.detail.image.tree
+ hr.attendance.detail.image
+
+
+
+
+
+
+
+
+
+ hr.attendance.detail.image.form
+ hr.attendance.detail.image
+
+
+
+
+
+
+ hr.attendance.detail.tree
+ hr.attendance.detail
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ hr.attendance.detail.form
+ hr.attendance.detail
+
+
+
+
+
+
+ 出勤详情
+ hr.attendance.detail
+ tree,form
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/yuthon_wechat/views/hr_attendance_views.xml b/yuthon_wechat/views/hr_attendance_views.xml
new file mode 100644
index 000000000..194e9f6b8
--- /dev/null
+++ b/yuthon_wechat/views/hr_attendance_views.xml
@@ -0,0 +1,55 @@
+
+
+
+ hr.attendance.tree.inherited
+ hr.attendance
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ hr.attendance.form.inherited
+ hr.attendance
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/yuthon_wechat/views/wx_work_views.xml b/yuthon_wechat/views/wx_work_views.xml
new file mode 100644
index 000000000..f863a2472
--- /dev/null
+++ b/yuthon_wechat/views/wx_work_views.xml
@@ -0,0 +1,13 @@
+
+
+
+ auth.oauth.provider.form.wwb.inherit
+ auth.oauth.provider
+
+
+
+
+
+
+
+