Files
gzth/yuthon_hr_attendance/controllers/applet_attendance.py
T
2025-12-30 17:42:37 +08:00

1358 lines
62 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from dateutil.relativedelta import relativedelta
from odoo.http import Controller, request, route
from odoo import models, fields, api, http, SUPERUSER_ID, _
from datetime import datetime, date
import logging
from geopy.distance import geodesic
from datetime import timedelta
from ..until import base
import json
import requests
from odoo.tools import float_round, float_repr
import math
import json
def clean_floats(obj):
if isinstance(obj, float):
if math.isnan(obj) or math.isinf(obj):
return None
return obj
elif isinstance(obj, dict):
return {k: clean_floats(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [clean_floats(v) for v in obj]
else:
return obj
_logger = logging.getLogger(__name__)
class AppletAttendance(Controller):
"""
小程序考勤接口
"""
@http.route('/ro/base/user/login/v1', type='http', auth="public", csrf=False, cors='*')
def ro_base_user_login_v1(self, **kw):
_logger.info("----------------/ro/base/user/login/v1---------接口收到响应-------")
password = kw.get('password')
login = kw.get('login')
try:
uid = request.session.authenticate(request.env.cr.dbname, login, password)
except Exception as e:
_logger.info(f"接口登录失败: {e}")
property_tenant_information = request.env['property.tenant.information'].sudo().search(
[('phone', '=', login)])
if not property_tenant_information:
return_data = {'data': {}, 'code': 401, 'message': '用户名或密码错误'}
else:
return_data = {'data': {'name': property_tenant_information.name, 'id': property_tenant_information.id,
'phone': property_tenant_information.phone,
'access': ['物业账单']}, 'code': 200, 'message': '用户名或密码错误'}
return json.dumps(return_data)
user_id = request.env['res.users'].sudo().browse(uid)
# 创建token
token = base.set_token(user_id.id, user_id.name)
employee_id = user_id.with_company(user_id.company_id.id).employee_id
applet_security_setting_ids = request.env['applet.security'].search(
[('employee_id', '=', employee_id.id)]).applet_security_setting_ids
access_list = [access.name for access in applet_security_setting_ids] + ['日程']
# 查询一下用户的手机号phone, 承租人关联的
data = {'name': employee_id.name, 'id': user_id.id, 'access': access_list, 'phone': employee_id.mobile_phone, 'token': token}
_logger.info(f"接口人{employee_id.name}权限{access_list}__用户id: {user_id}________")
return_data = {'data': data, 'code': 200, 'message': ''}
return json.dumps(return_data)
# 验证token就在url下面加这个装饰器
# @base.authorize
@route('/yuthon/att/today', methods=['POST'], type='http', auth='none', cors='*', csrf=False)
def apple_yuthon_att_today(self, **kw):
"""
小程序获取当前用户打卡信息
:return:
{
'location': 经纬度-打卡地点,
'att_longitude': '经度'
'att_latitude': '纬度'
'class': [班次开始时间-结束时间]
'user_id': 用户id
'check_in': [上班卡,正常]
'check_out': [下班卡,早退]
'state': 是否可以打卡['yes', 'no']
'state_result': 不可打卡原因 'xxx'
}
"""
_logger.info("----------------/yuthon/att/today---------接口收到响应-------------------------------")
current_datetime = fields.Datetime.now() + timedelta(hours=8)
current_date = fields.Date.today() + timedelta(hours=8)
# 转换为字符串格式以便日志显示
current_datetime_str = fields.Datetime.to_string(current_datetime)
current_date_str = fields.Date.to_string(current_datetime.date())
# 日志输出
_logger.info(f'当前完整时间(含8小时偏移):{current_datetime_str}')
_logger.info(f'当前日期(含8小时偏移):{current_date_str}')
user_id = request.env(user=SUPERUSER_ID)["res.users"].search([('id', '=', int(kw['user_id']))])
employee_id = request.env(user=SUPERUSER_ID)["hr.employee"].search([('name', '=', user_id.name)])
attendance_id = request.env(user=SUPERUSER_ID)["hr.attendance"].search(
[('employee_id', '=', employee_id.id), ('current_data', '=', current_date)])
check_in = attendance_id.check_in + timedelta(hours=8) if attendance_id.check_in else ''
check_out = attendance_id.check_out + timedelta(hours=8) if attendance_id.check_out else ''
if attendance_id.shift_name:
shift_name = attendance_id.shift_name
else:
shift_name = '行政班次'
def get_all_upper_depts(dept):
"""递归获取部门及其所有上级部门的ID列表"""
dept_ids = [dept.id]
if dept.parent_id:
dept_ids.extend(get_all_upper_depts(dept.parent_id))
return dept_ids
all_dept_ids = get_all_upper_depts(employee_id.department_id)
slot_start = datetime.combine(current_date, datetime.min.time())
slot_end = datetime.combine(current_date + timedelta(days=1), datetime.min.time())
# 查找员工对应的班次
planning_slots = request.env(user=SUPERUSER_ID)['planning.slot'].search([
('employee_id', '=', employee_id.id),
('start_datetime', '>=', slot_start),
('start_datetime', '<', slot_end),
], limit=1)
planning_name = planning_slots.planning_name if planning_slots else None
template_domain = []
if planning_name:
template_domain.append(('planning_name', '=', planning_name))
template_domain.append(('department_id', 'in', all_dept_ids))
template = request.env(user=SUPERUSER_ID)['planning.slot.template'].search(template_domain, limit=1)
if not template:
role_templates = [
{'role_name': '厨卫(集团)', 'template': None},
{'role_name': '项目运营岗', 'template': None}
]
for item in role_templates:
item['template'] = request.env(user=SUPERUSER_ID)['planning.slot.template'].search(
[('role_id.name', '=', item['role_name'])], )
for item in role_templates:
if item['template']:
employee_ids = item['template'].role_id.resource_ids.employee_id.ids or []
employee_list = list(employee_ids)
if employee_id.id in employee_list:
template = item['template']
break
if not template:
template = request.env(user=SUPERUSER_ID)['planning.slot.template'].search(
[('role_id.name', '=', '集团行政')],
limit=1
)
sch_check_in_time, sch_check_out_time = request.env(user=SUPERUSER_ID)[
'hr.attendance.detail'].normal_clock_in_time(template, current_date)
start_time = sch_check_in_time + timedelta(hours=8)
end_time = sch_check_out_time + timedelta(hours=8)
start_time_str = start_time.strftime('%H:%M')
end_time_str = end_time.strftime('%H:%M')
is_early_card = attendance_id.is_early_card
is_late_card = attendance_id.is_late_card
check_in_status = '缺卡' if not check_in else ('迟到' if is_late_card else '')
check_out_status = '缺卡' if not check_out else ('早退' if is_early_card else '')
state_result = ''
if is_late_card:
state_result = '迟到'
if is_early_card:
state_result = '早退'
elif not check_in or not check_out:
state_result = '缺卡'
check_in_str = check_in.strftime('%H:%M:%S') if check_in else ' '
check_out_str = check_out.strftime('%H:%M:%S') if check_out else ' '
check_in_html = f'<p style="color:red">{check_in_str}</p>' if check_in_status in ['迟到', '早退',
'缺卡'] else f'<p style="color:green">{check_in_str}</p>'
check_out_html = f'<p style="color:red">{check_out_str}</p>' if check_out_status in ['迟到', '早退',
'缺卡'] else f'<p style="color:green">{check_out_str}</p>'
state = "no"
message = "超出范围或wifi未连接,不允许打卡"
address_ids = request.env(user=SUPERUSER_ID)["applet.address"].search(
[('company_id', '=', employee_id.company_id.id)])
if kw['att_latitude'] != kw['att_longitude']:
self_address = self.get_address_from_coords(kw['att_latitude'], kw['att_longitude'])
else:
self_address = {'formatted_address': 'formatted_address'}
wifi_name_list = []
for wifi_mac in address_ids:
for mac in wifi_mac.wifi_name_ids:
wifi_name_list.append(mac.mac)
for address_id in address_ids:
if '天成大厦' in self_address['formatted_address']:
message = '可以打卡'
state = 'yes'
_logger.info(f"天成大厦在{self_address['formatted_address']}")
break
card_rice = address_id.card_rice * 0.001 # 打卡范围
if self.is_within_radius_geopy(kw['att_latitude'], kw['att_longitude'], address_id.latitude,
address_id.longitude, card_rice) or kw['wifi_name'] in wifi_name_list:
message = '可以打卡'
state = 'yes'
break
_logger.info(
f"\n人员: {employee_id.name} {employee_id.company_id.name} \n当前所在地址: {self_address['formatted_address']} \n是否可以打卡: {state}\n {wifi_name_list}\n wifi:{kw['wifi_name']}_________")
if self.is_workday(current_date):
attendance_id_class = f"{shift_name} {start_time_str}-{end_time_str}"
else:
attendance_id_class = f"{shift_name}休息"
data = {
'location': self_address['formatted_address'],
'user_id': user_id.id,
'class': f"{attendance_id_class}",
'state_result': state_result,
'check_in': check_in_html,
'check_in_status': check_in_status,
'check_out': check_out_html,
'check_out_status': check_out_status,
'state': state,
},
return_data = {'data': data, 'code': '200', 'message': message, }
return json.dumps(return_data)
@route('/yuthon/att/totday_info', methods=['POST'], csrf=False, type='http', auth='none', cors='*')
def apple_totday_info(self):
'''
获取当日打卡的信息
'''
current_date = fields.Date.today() + timedelta(hours=8)
user_id = request.env(user=SUPERUSER_ID)["res.users"].search([('id', '=', int(kw['user_id']))])
employee_id = request.env(user=SUPERUSER_ID)["hr.employee"].search([('name', '=', user_id.name)])
attendance_id = request.env(user=SUPERUSER_ID)["hr.attendance"].search(
[('employee_id', '=', employee_id.id), ('current_data', '=', current_date)])
check_in = attendance_id.check_in + timedelta(hours=8) if attendance_id.check_in else ''
check_out = attendance_id.check_out + timedelta(hours=8) if attendance_id.check_out else ''
start_time_str = ''
end_time_str = ''
if attendance_id.shift_name:
shift_name = attendance_id.shift_name
else:
shift_name = '行政班次'
if attendance_id:
if not attendance_id.shift_id:
year = attendance_id.current_data.year
month = attendance_id.current_data.month
day = attendance_id.current_data.day
current_date = datetime.today()
min_hour_from = attendance_id.employee_id.resource_id.calendar_id.attendance_ids[0].hour_from
min_hour = int(min_hour_from)
min_minute = int(min_hour_from * 10 % min_hour_from)
will_datetime_start = current_date.replace(year=year, month=month, day=day, hour=min_hour,
minute=min_minute)
max_hour_to = attendance_id.employee_id.resource_id.calendar_id.attendance_ids[-1].hour_to
max_hour = int(max_hour_to)
max_minute = int(max_hour_to * 10 % max_hour_to)
will_datetime_end = current_date.replace(year=year, month=month, day=day, hour=max_hour,
minute=max_minute)
start_time_str = will_datetime_start.strftime('%H:%M')
end_time_str = will_datetime_end.strftime('%H:%M')
else:
check_in_start = attendance_id.shift_id.template_id.check_in_start
check_out_end = attendance_id.shift_id.template_id.check_out_end
start_minute = int((check_in_start - int(check_in_start)) * 60)
end_minute = int((check_out_end - int(check_out_end)) * 60)
start_time_str = f"{int(check_in_start):02d}-{start_minute:02d}"
end_time_str = f"{int(check_out_end):02d}-{end_minute:02d}"
is_early_card = attendance_id.is_early_card
is_late_card = attendance_id.is_late_card
check_in_status = '缺卡' if not check_in else ('迟到' if is_late_card else '')
check_out_status = '缺卡' if not check_out else ('早退' if is_early_card else '')
state_result = ''
if is_late_card:
state_result = '迟到'
if is_early_card:
state_result = '早退'
elif not check_in or not check_out:
state_result = '缺卡'
check_in_str = check_in.strftime('%H:%M:%S') if check_in else ' '
check_out_str = check_out.strftime('%H:%M:%S') if check_out else ' '
check_in_html = f'<p style="color:red">{check_in_str}</p>' if check_in_status in ['迟到', '早退',
'缺卡'] else f'<p style="color:green">{check_in_str}</p>'
check_out_html = f'<p style="color:red">{check_out_str}</p>' if check_out_status in ['迟到', '早退',
'缺卡'] else f'<p style="color:green">{check_out_str}</p>'
if self.is_workday(current_date):
attendance_id_class = f"{shift_name} {start_time_str}-{end_time_str}"
else:
attendance_id_class = f"{shift_name}休息"
data = {
'user_id': user_id.id,
'class': f"{attendance_id_class}",
'state_result': state_result,
'check_in': check_in_html,
'check_in_status': check_in_status,
'check_out': check_out_html,
'check_out_status': check_out_status
},
return_data = {'data': data, 'code': '200', 'message': '成功', }
return json.dumps(return_data)
def is_workday(self, date):
"""判断指定日期是否为工作日(考虑法定节假日和调休)
如果是工作日返回True
如果是休息日返回False
"""
current_year = (fields.Date.today() + timedelta(hours=8)).year
query_start = datetime(current_year, 1, 1, 0, 0, 0) - timedelta(hours=8)
query_end = datetime(current_year, 12, 31, 23, 59, 59) - timedelta(hours=8)
calendar_leaves = self.env['resource.calendar.leaves'].sudo().search([
("calendar_id.name", "=", "行政考勤工作日"),
("date_from", ">=", query_start),
("date_from", "<=", query_end)
])
holidays_set = set()
shift_days_set = set()
for leave in calendar_leaves:
if leave.holidays:
holidays_set.update(leave.holidays.split(','))
if leave.alternate_rest_day:
holidays_set.update(leave.alternate_rest_day.split(','))
if leave.shift_replacement_day:
shift_days_set.update(leave.shift_replacement_day.split(','))
date_str = date.strftime('%m月%d日')
date_str = self.remove_leading_zero(date_str)
is_weekend = date.weekday() in [5, 6]
if date_str in holidays_set:
return False
elif date_str in shift_days_set:
return True
else:
return not is_weekend
def remove_leading_zero(self, date_str):
month, day = date_str.split('月')
month = month.lstrip('0')
day = day.rstrip('日')
day = day.lstrip('0')
return f'{month}月{day}日'
def is_within_radius_geopy(self, lat1, lon1, lat2, lon2, radius_km):
"""
使用geopy库判断是否在范围内
"""
point1 = (float(lat1), float(lon1))
point2 = (float(lat2), float(lon2))
distance = geodesic(point1, point2).kilometers
return distance <= radius_km
def get_address_from_coords(self, latitude, longitude):
api_key = 'BHXBZ-RHJC3-Y7R3G-OOMZ2-N6FBE-NQFIB' # 替换为你的实际密钥
url = f'https://apis.map.qq.com/ws/geocoder/v1?location={latitude},{longitude}&key={api_key}'
response = requests.get(url)
data = response.json()
if data['status'] == 0:
address = data['result']['address']
formatted_address = data['result']['formatted_addresses']['recommend']
return {'address': address, 'formatted_address': formatted_address}
else:
print(f"Error: {data['message']}")
return None
@route('/yuthon/att/record', methods=['POST'], type='http', auth='none', csrf=False)
def apple_yuthon_att_record(self, **kw):
"""
小程序创建打卡记录
:return:
"""
_logger.info("-------触发打卡接口/yuthon/att/record---------")
res_user_id = request.env(user=SUPERUSER_ID)["res.users"].search([('id', '=', kw.get('user_id'))])
_logger.info(
f"传过来的user_id:{kw.get('user_id')},名称{res_user_id.name}, 时间{fields.Datetime.now() + timedelta(hours=8)}")
employee_id = request.env(user=SUPERUSER_ID)["hr.employee"].search([('name', '=', res_user_id.name)])
if res_user_id and employee_id:
# 初始化变量
checkin_type = None
current_datetime = fields.Datetime.now() + timedelta(hours=8) # 打卡时间
current_date = current_datetime.date()
department = employee_id.department_id
slot_template = None
groupname = ''
if department:
slot_template = request.env(user=SUPERUSER_ID)["planning.slot.template"].search(
[('department_id', '=', department.id)], limit=1)
if not slot_template:
slot_template = request.env(user=SUPERUSER_ID)["planning.slot.template"].search(
[('planning_name', '=', '集团行政')], limit=1)
if slot_template:
if slot_template.planning_name == "集团行政":
groupname = "集团行政"
elif slot_template.role_id and slot_template.planning_name:
groupname = f"{slot_template.role_id.name} - {slot_template.planning_name}"
# 时间转换辅助函数(考虑day2次日标记)
def get_schedule_datetime(float_hour, is_check_out_end=False, is_day2=False):
if float_hour is None:
return None
# 只有下班结束时间且是次日时才加一天
base_date = current_date
if is_check_out_end and is_day2:
base_date = current_date + timedelta(days=1)
hours = int(float_hour)
minutes = int((float_hour - hours) * 60)
return datetime.combine(base_date, datetime.min.time()) + timedelta(hours=hours, minutes=minutes)
# 解析班次时间
check_in_start = None
check_in_end = None
check_out_start = None
check_out_end = None
if slot_template:
if slot_template:
check_in_start = get_schedule_datetime(slot_template.check_in_start)
check_in_end = get_schedule_datetime(slot_template.check_in_end)
check_out_start = get_schedule_datetime(slot_template.check_out_start)
check_out_end = get_schedule_datetime(slot_template.check_out_end, is_check_out_end=True,
is_day2=slot_template.day2)
# 检查该员工是否已有上班打卡记录
has_checkin = False
if employee_id:
# 查询今天是否有上班打卡记录
checkin_records = request.env(user=SUPERUSER_ID)["hr.attendance.detail"].search([
('employee_id', '=', employee_id.id),
('checkin_type', '=', '上班打卡'),
('checkin_time', '>=', fields.Datetime.to_string(current_date)),
('checkin_time', '<', fields.Datetime.to_string(current_date + timedelta(days=1)))
])
has_checkin = bool(checkin_records)
sch_checkin_time = check_in_start
# 判断打卡类型并设置标准打卡时间
if check_in_start and check_in_end and check_in_start <= current_datetime <= check_in_end:
checkin_type = "上班打卡"
sch_checkin_time = check_in_start
elif has_checkin and check_out_start and check_out_end and check_out_start <= current_datetime <= check_out_end:
checkin_type = "下班打卡"
sch_checkin_time = check_out_end
# 创建考勤记录
detail_id = request.env(user=SUPERUSER_ID)["hr.attendance.detail"].create({
'employee_id': employee_id.id,
'checkin_time': fields.datetime.now(),
'source': 'mini_programs',
'groupname': groupname,
'checkin_type': checkin_type,
'sch_checkin_time': sch_checkin_time,
})
# now_time = fields.Datetime.to_datetime(fields.Date.today()) - timedelta(hours=8, days=1)
# start_time = now_time - timedelta(days=2)
# request.env["hr.attendance.detail"].normal_clock_in_time(employee_id, now_time, start_time)
return_data = {'code': '200', 'message': '打卡成功'}
_logger.info(f"打卡记录创建成功 id = {detail_id.id} checkin_time = {fields.datetime.now()}")
else:
return_data = {'code': '200', 'message': '未识别到员工'}
return json.dumps(return_data)
@route('/yuthon/app/att', methods=['POST'], type='http', auth='none', csrf=False)
def yuthon_app_att(self, **kw):
"""
:param kw:
:return:
{
"code": "200", // 状态码,200表示成功
"message": "OK", // 状态描述
"data": {
"current_month": { // 当月考勤数据
"holiday_data": { // 假期数据
"holiday_list": ["2025-01-01", "2025-01-02"], // 法定节假日,调休休息日列表(日期字符串)
"shift_days_list": ["2025-02-10", "2025-02-11"] // 调休补班日列表(日期字符串)
},
"banner_rote": { // 异常统计
"is_no_card": 2, // 缺卡次数
"is_early_card": 1, // 早退次数
"is_late_card": 3, // 迟到次数
"is_abnormal": 5 // 异常总次数
"is_no_work" : 2 // 旷工次数
},
"attendance_data": [ // 每日考勤记录列表
{
"date": "2025-06-15", // 日期
"class": ["早班", "08:30-17:30"], // 班次信息 [班次名称, 标准时间范围]
"user_id": 1001, // 用户ID
"check_in": ["08:45:20", "迟到"], // 签到信息 [时间, 状态]
"check_out": ["17:20:15", "正常"] // 签退信息 [时间, 状态]
},
{
"date": "2025-06-16",
"class": ["晚班", "14:00-22:00"],
"user_id": 1001,
"check_in": ["缺卡", "缺卡"],
"check_out": ["缺卡", "缺卡"]
}
]
},
"previous_month": { // 上月考勤数据(结构同current_month)
"holiday_data": { ... },
"banner_rote": { ... },
"attendance_data": [ ... ]
}
}
}
"""
user_id = int(kw['user_id'])
month = kw['selected_month'] # 2015-01
year = int(month[0:4])
month = int(month[-2:])
# 当月日期范围
current_month_first = datetime(year, month, 1)
if month == 12:
current_month_last = datetime(year + 1, 1, 1) - timedelta(days=1)
else:
current_month_last = datetime(year, month + 1, 1) - timedelta(days=1)
# calendar_leaves = request.env(user=SUPERUSER_ID)['resource.calendar.leaves'].search(
# [("calendar_id", "=", "行政考勤工作日")]) # 少一个search
# 本月的节假日
holiday_data = self.get_holiday_data(current_month_first.date(), current_month_last.date())
# 本月考勤记录
leave_days = 0
current_attendance = request.env(user=SUPERUSER_ID)['hr.attendance'].sudo().search([
('employee_id.user_id', '=', user_id),
('current_data', '>=', current_month_first.date()),
('current_data', '<=', current_month_last.date()),
])
# 本月请假天数
leave_ids = request.env(user=SUPERUSER_ID)['hr.leave'].sudo().search([
('employee_id.user_id', '=', user_id),
('date_from', '>=', current_month_first),
('date_to', '<=', current_month_last),
('state', 'in', ['validate'])
])
for leave in leave_ids:
leave_days += leave.number_of_days
# 本月异常次数
current_stats = {
'is_no_card': sum(1 for rec in current_attendance if rec.is_no_card),
'is_early_card': sum(1 for rec in current_attendance if rec.is_early_card),
'is_late_card': sum(1 for rec in current_attendance if rec.is_late_card),
'is_abnormal': sum(1 for rec in current_attendance if rec.is_abnormal),
'is_holiday': leave_days,
'is_no_work': sum(1 for rec in current_attendance if rec.absenteeism_card),
}
current_records = self.process_attendances(current_attendance, user_id)
return_data = {
'code': '200',
'message': 'OK',
'data': {
'current_month': {
'holiday_data': holiday_data,
'banner_rote': current_stats,
'attendance_data': current_records,
},
}
}
return json.dumps(return_data)
@route('/yuthon/vacation/get', methods=['POST'], type='http', auth='none', csrf=False)
def yuthon_vacation_get(self, **kw):
"""
:param kw:
:return:
{
"code": "200", // 状态码,200表示成功
"message": "OK", // 状态描述
"data": {
"current_month": { // 当月考勤数据
"holiday_data": { // 假期数据
"holiday_list": ["2025-01-01", "2025-01-02"], // 法定节假日,调休休息日列表(日期字符串)
"shift_days_list": ["2025-02-10", "2025-02-11"] // 调休补班日列表(日期字符串)
},
"""
id = int(kw['id'])
record = request.env['yuthon.vacation.reports'].sudo().browse(id)
result = {
'vacation_date': '',
'employee': '',
'items': []
}
if record:
items = []
fields_list = [
('应到', 'should_attendance'),
('实到', 'actual_attendance'),
('迟到', 'is_late_card'),
('早退', 'is_early_card'),
('缺卡', 'is_no_card'),
('旷工', 'is_work'),
('异常', 'is_abnormal'),
('出勤天数', 'att_mount'),
('允许迟到次数', 'late_allowed'),
('转正天数', 'right_days'),
('实习天数', 'internship_days'),
('试用天数', 'test_days'),
('年假', 'done_leave'),
('加班时', 'done_working_overtime'),
('误餐次数', 'meal_fee'),
('体检费', 'medical_fee'),
('调休', 'compensatory_leave'),
('事假', 'leave_of_absence'),
('加班时(法定)', 'statutory_leave'),
('补休', 'cop_leave'),
('值班调休', 'duty_off_leave'),
('婚假', 'marriage_leave'),
('产假', 'maternity_leave'),
('陪产假', 'paternity_leave'),
('计生假', 'family_planning_leave'),
('丧假', 'bereavement_leave'),
('探亲假', 'home_visit_leave'),
('家长会假', 'parents_meeting_leave'),
('病假(普通)', 'general_sick_leave'),
('病假(住院期)', 'hospital_sick_leave'),
('病假(出院恢复期)', 'recovery_sick_leave'),
('考试假', 'exam_leave'),
]
for label, field in fields_list:
value = getattr(record, field, None)
if value is not None:
items.append({'key': label, 'value': value})
result = {
'vacation_date': fields.Date.to_string(record.vacation_date),
'employee': record.employee_id.name,
'items': items
}
return_data = {
'code': '200',
'message': 'OK',
'data': result
}
return json.dumps(return_data)
@route('/yuthon/vacation/confirm', methods=['POST'], type='http', auth='none', csrf=False)
def yuthon_vacation_confirm(self, **kw):
"""
:param kw:
:return:
{
"code": "200", // 状态码,200表示成功
"message": "OK", // 状态描述
"data": {
"current_month": { // 当月考勤数据
"holiday_data": { // 假期数据
"holiday_list": ["2025-01-01", "2025-01-02"], // 法定节假日,调休休息日列表(日期字符串)
"shift_days_list": ["2025-02-10", "2025-02-11"] // 调休补班日列表(日期字符串)
},
"""
id = int(kw['id'])
record = request.env['yuthon.vacation.reports'].sudo().browse(id)
record.manage_reject()
# 这里需要确认提交
return_data = {
'code': '200',
'message': 'OK',
'data': id
}
return json.dumps(return_data)
def process_attendances(self, attendances, user_id):
records = []
for att in attendances:
is_weekend = att.current_data.weekday() in [5, 6]
if not att.shift_id:
year = att.current_data.year
month = att.current_data.month
day = att.current_data.day
current_date = datetime.today()
min_hour_from = att.employee_id.resource_id.calendar_id.attendance_ids[0].hour_from
min_hour = int(min_hour_from)
min_minute = int(min_hour_from * 10 % min_hour_from)
will_datetime_start = current_date.replace(year=year, month=month, day=day, hour=min_hour,
minute=min_minute)
max_hour_to = att.employee_id.resource_id.calendar_id.attendance_ids[-1].hour_to
max_hour = int(max_hour_to)
max_minute = int(max_hour_to * 10 % max_hour_to)
will_datetime_end = current_date.replace(year=year, month=month, day=day, hour=max_hour,
minute=max_minute)
standard_check_in_str = will_datetime_start.strftime('%H:%M')
standard_check_out_str = will_datetime_end.strftime('%H:%M')
else:
# 获取班次信息
check_in_start = att.shift_id.template_id.check_in_start if att.shift_id and att.shift_id.template_id else 0
check_out_end = att.shift_id.template_id.check_out_end if att.shift_id and att.shift_id.template_id else 0
start_hour = int(check_in_start)
start_minute = int((check_in_start - start_hour) * 60)
end_hour = int(check_out_end)
end_minute = int((check_out_end - end_hour) * 60)
# 格式化标准时间
standard_check_in_str = f"{start_hour:02d}:{start_minute:02d}"
standard_check_out_str = f"{end_hour:02d}:{end_minute:02d}"
# 实际打卡时间
check_in = att.check_in
check_out = att.check_out
is_early_card = att.is_early_card
is_late_card = att.is_late_card
base_check_in_status = '缺卡' if not check_in else ('迟到' if is_late_card else '正常')
base_check_out_status = '缺卡' if not check_out else ('早退' if is_early_card else '正常')
if is_weekend:
check_in_status = '正常' if check_in else ''
check_out_status = '正常' if check_out else ''
else:
check_in_status = base_check_in_status
check_out_status = base_check_out_status
if check_in:
check_in_time = (check_in + timedelta(hours=8)).strftime('%H:%M:%S')
check_in_str = f"{check_in_status} {check_in_time}" if check_in_status in ['迟到',
'早退'] else check_in_time
else:
check_in_str = '' if is_weekend else '缺卡'
if check_out:
check_out_time = (check_out + timedelta(hours=8)).strftime('%H:%M:%S')
check_out_str = f"{check_out_status} {check_out_time}" if check_out_status in ['迟到',
'早退'] else check_out_time
else:
check_out_str = '' if is_weekend else '缺卡'
if not check_in_status and not check_out_status:
color_select = 3
else:
has_exception = (check_in_status in ['迟到', '早退', '缺卡']) or (
check_out_status in ['迟到', '早退', '缺卡'])
color_select = 2 if has_exception else 1
records.append({
'date': att.current_data.strftime('%Y/%m/%d'),
'class': [f"{att.shift_name} ", f"{standard_check_in_str}-{standard_check_out_str}"],
'user_id': user_id,
'check_in': check_in_str,
'check_out': check_out_str,
'color_select': color_select,
'is_weekend': is_weekend
})
return records
def get_holiday_data(self, start_date, end_date):
"""
获取指定日期范围内的假期和调休数据
参数:
start_date: 开始日期,date类型
end_date: 结束日期,date类型
返回:
包含假期和调休信息的字典
"""
# 从start_date中提取年份
target_year = start_date.year
target_month = start_date.month
current_year = (fields.Date.today() + timedelta(hours=8)).year
query_start = datetime(current_year, 1, 1, 0, 0, 0) - timedelta(hours=8)
query_end = datetime(current_year, 12, 31, 23, 59, 59) - timedelta(hours=8)
domain = [("calendar_id", "=", "行政考勤工作日"),
("date_from", ">=", query_start),
("date_from", "<=", query_end)]
if start_date and end_date:
# 将date对象转换为datetime对象
start_datetime = datetime.combine(start_date, datetime.min.time())
end_datetime = datetime.combine(end_date, datetime.max.time())
domain.extend([("date_from", "<=", end_datetime), ("date_to", ">=", start_datetime)])
calendar_leaves = request.env(user=SUPERUSER_ID)['resource.calendar.leaves'].search(domain)
# 根据name 来进行去重
unique_leaves = {}
for leave in calendar_leaves:
if leave.name not in unique_leaves:
unique_leaves[leave.name] = leave
unique_leave_records = request.env['resource.calendar.leaves']
for leave in unique_leaves.values():
unique_leave_records += leave
holidays_list = [] # 假期
shift_days_list = [] # 调休
for leaves in unique_leave_records:
# 处理法定节假日和调休日- 直接从date_from和date_to获取
if leaves.date_from and leaves.date_to:
# 将datetime类型转换为date类型并格式化为YYYY-MM-DD
date_from = (leaves.date_from + timedelta(hours=8)).date().isoformat()
date_to = (leaves.date_to + timedelta(hours=8)).date().isoformat()
# 处理日期范围,添加从date_from到date_to的所有日期
start_date_obj = datetime.strptime(date_from, '%Y-%m-%d').date()
end_date_obj = datetime.strptime(date_to, '%Y-%m-%d').date()
delta = end_date_obj - start_date_obj
for i in range(delta.days + 1):
day = start_date_obj + timedelta(days=i)
# 只添加年份和月份匹配的日期
if day.year == target_year and day.month == target_month:
formatted_date = f"{day.year}-{day.month}-{day.day}"
holidays_list.append(formatted_date)
# 处理调班补班
if leaves.shift_replacement_day:
day_strings = leaves.shift_replacement_day.split(',')
for day_str in day_strings:
month_str, day_str = day_str.strip().replace('日', '').split('月')
month = int(month_str)
day = int(day_str)
# 只添加月份匹配的日期
if month == target_month:
formatted_date = f"{target_year}-{month}-{day}"
shift_days_list.append(formatted_date)
return {
"holiday_list": holidays_list,
"shift_days_list": shift_days_list,
}
# 水电表接口
@route('/yuthon/meter/reading', methods=['POST'], type='http', auth='none', cors='*', csrf=False)
def yuthon_meter_reading(self, **kw):
"""
水电表抄表
:return:
table_list:[
{
building:'楼盘名称1', 楼盘固定为天河商贸大厦
floor_list:[
{
floor:'楼层1',
data_list:[
{
address: "地址address",
code: "8817298788",
employee_name: "xxx,xxx",
last_dosage: 0,
last_mater_data: "上次抄表日期",
mater_data: "抄表日期str",
meter_type: "electric",
room:'101'
},
{
address: "地址address",
code: "8817298788",
employee_name: "xxx,xxx",
last_dosage: 0,
last_mater_data: "上次抄表日期",
mater_data: "抄表日期str",
meter_type: "electric",
room:'102'
},
{
address: "地址address",
code: "8817298788",
employee_name: "xxx,xxx",
last_dosage: 0,
last_mater_data: "上次抄表日期",
mater_data: "抄表日期str",
meter_type: "electric",
room:'103'
}
]
},
{
floor:'楼层2',
data_list:[
{
address: "地址address",
code: "8817298788",
employee_name: "xxx,xxx",
last_dosage: 0,
last_mater_data: "上次抄表日期",
mater_data: "抄表日期str",
meter_type: "electric",
room:'201'
},
{
address: "地址address",
code: "8817298788",
employee_name: "xxx,xxx",
last_dosage: 0,
last_mater_data: "上次抄表日期",
mater_data: "抄表日期str",
meter_type: "electric",
room:'202'
},
"""
_logger.info("----------------/yuthon/meter/reading---------接口收到请求-------------------------------")
today = datetime.now()
first_day = today.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
last_day = (today.replace(day=1) + timedelta(days=32)).replace(day=1) - timedelta(days=1)
last_day = last_day.replace(hour=23, minute=59, second=59, microsecond=999999)
first_day_str = first_day.strftime('%Y-%m-%d')
last_day_str = last_day.strftime('%Y-%m-%d')
two_months_ago = datetime.now().date() - relativedelta(months=2)
# 去掉 数据为 0 的数据.
valid_archives = request.env(user=SUPERUSER_ID)['property.meter.archives'].search([
('is_virtual_meter', '!=', True),
'|',
('end_time', '=', False),
('end_time', '<=', two_months_ago)
])
electric_records = request.env(user=SUPERUSER_ID)['meter.archives.line'].search([
('archives_state', '!=', 'stop'),
('archives_id', 'in', valid_archives.ids),
('meter_date', '>=', first_day_str),
('meter_date', '<=', last_day_str),
])
valid_water_ids = request.env(user=SUPERUSER_ID)['property.meter.water'].search([
'|',
('end_time', '=', False),
('end_time', '<=', two_months_ago)
]).ids
water_records = request.env(user=SUPERUSER_ID)['meter.water.line'].search([
('water_state', '!=', 'stop'),
('meter_date', '>=', first_day_str),
('meter_date', '<=', last_day_str),
('water_id', 'in', valid_water_ids)
])
# 接口调整, 电表,水表数据拆分.
# 先根据状态,再根据楼层
# 电表数据
electric_result_dict = {}
# 处理电表
for electric in electric_records:
property_infos = electric.yuthon_property_ids if electric.yuthon_property_ids else [None]
for property_info in property_infos:
# 处理空物业信息的情况,所有关联字段设为空
if property_info:
building = '天河商贸大厦'
floor = electric.epitope_values_id.name if electric.epitope_values_id else ''
room = property_info.units or ''
address = electric.unit_number or ''
employee_name = property_info.lessee_id.name if property_info.lessee_id else ''
else:
building = '天河商贸大厦'
floor = electric.epitope_values_id.name if electric.epitope_values_id else ''
room = ''
address = ''
employee_name = ''
state = electric.state
electric_value = {
'id': electric.id,
'current_read': electric.current_read,
'code': electric.archives_id.name or '',
'last_dosage': electric.last_dosage or 0,
'mater_data': str(electric.meter_date) if electric.meter_date else '',
'last_mater_data': str(electric.last_meter) if electric.last_meter else '',
'meter_type': 'electric',
'employee_name': employee_name,
'address': address,
'building': '天河商贸大厦',
'floor': floor,
'room': room,
'epitope_values': electric.epitope_values_id.name,
'unit_number': electric.unit_number,
'state': electric.state, # 状态 reading unreading
'last_read': electric.last_read,
}
electric_result_dict.setdefault(state, {}).setdefault(building, {}).setdefault(floor, []).append(
electric_value)
electric_result = []
for state, building_dict in electric_result_dict.items():
building_list = []
for building, floor_dict in building_dict.items():
floor_list = []
for floor, data_list in floor_dict.items():
floor_list.append({
'floor': floor,
'data_list': data_list
})
building_list.append({
'building': building,
'floor_list': floor_list
})
electric_result.append({
'state': state,
'building_list': building_list
})
water_result_dict = {}
# 处理水表
for water in water_records:
property_infos = water.yuthon_property_ids if water.yuthon_property_ids else [None]
for property_info in property_infos:
# 处理空物业信息的情况,所有关联字段设为空
if property_info:
building = '天河商贸大厦'
floor = water.epitope_values_id.name if water.epitope_values_id else ''
room = property_info.units or ''
address = water.unit_number or ''
employee_name = property_info.lessee_id.name if property_info.lessee_id else ''
else:
building = '天河商贸大厦'
floor = water.epitope_values_id.name if water.epitope_values_id else ''
room = ''
address = ''
employee_name = ''
state = water.state
water_value = {
'id': water.id,
'current_read': water.current_read,
'code': water.water_id.name,
'last_dosage': water.last_dosage or 0,
'mater_data': str(water.meter_date) if water.meter_date else '',
'last_mater_data': str(water.last_meter) if water.last_meter else '',
'meter_type': 'water',
'employee_name': employee_name,
'address': address,
'building': building,
'floor': floor,
'room': room,
'epitope_values': water.epitope_values_id.name,
'unit_number': water.unit_number,
'state': water.state, # 状态 reading unreading
'last_read': water.last_read,
}
water_result_dict.setdefault(state, {}).setdefault(building, {}).setdefault(floor, []).append(
water_value)
water_result = []
for state, building_dict in water_result_dict.items():
building_list = []
for building, floor_dict in building_dict.items():
floor_list = []
for floor, data_list in floor_dict.items():
floor_list.append({
'floor': floor,
'data_list': data_list
})
building_list.append({
'building': building,
'floor_list': floor_list
})
water_result.append({
'state': state,
'building_list': building_list
})
return json.dumps({
'electric': electric_result,
'water': water_result,
})
# 获取楼层列表
@route('/yuthon/meter/reading/floor', methods=['POST'], type='http', auth='none', cors='*', csrf=False)
def yuthon_meter_reading_floor(self, **kw):
'''获取楼层列表'''
result = request.env['yuthon.epitope.values'].sudo().search_read([], fields=['name', 'type'])
return json.dumps(result)
# 根据楼层,状态(reading, unreading) 类型, 获取 列表数据
@route('/yuthon/meter/reading/list', methods=['POST'], type='http', auth='none', cors='*', csrf=False)
def yuthon_meter_reading_list(self, **kw):
reading_state = kw.get('state')
type = kw.get('type')
floor_id = int(kw.get('floor_id'))
_logger.info(f'楼层id{floor_id}, ---{reading_state}, ---{type}')
today = datetime.now()
first_day = today.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
last_day = (today.replace(day=1) + timedelta(days=32)).replace(day=1) - timedelta(days=1)
last_day = last_day.replace(hour=23, minute=59, second=59, microsecond=999999)
first_day_str = first_day.strftime('%Y-%m-%d')
last_day_str = last_day.strftime('%Y-%m-%d')
floor_name = request.env['yuthon.epitope.values'].sudo().search([('id', '=', floor_id)]).name
_logger.info(f'楼层{floor_name}')
# domain = [('state', '=', reading_state),
# ('epitope_values_id.name', '=', floor_name),
# ('meter_date', '>=', first_day_str),
# ('meter_date', '<=', last_day_str)]
domain = [
('epitope_values_id.name', '=', floor_name),
('meter_date', '>=', first_day_str),
('meter_date', '<=', last_day_str)
]
two_months_ago = datetime.now().date() - relativedelta(months=2)
if type == 'electric':
domain.append(('archives_state', '!=', 'stop'))
model = 'meter.archives.line'
domain.extend([
('archives_id.is_virtual_meter', '!=', True),
'|',
('archives_id.end_time', '=', False),
('archives_id.end_time', '<=', two_months_ago)
])
else:
domain.append(('water_state', '!=', 'stop'))
model = 'meter.water.line'
records = request.env[model].sudo().search(domain)
# _logger.info(f'任务数据{records}')
result = []
for record in records:
property_infos = record.yuthon_property_ids if record.yuthon_property_ids else [None]
for property_info in property_infos:
# 处理空物业信息的情况,所有关联字段设为空
if property_info:
building = '天河商贸大厦'
floor = record.epitope_values_id.name if record.epitope_values_id else ''
room = property_info.units or ''
address = property_info.name or ''
employee_name = property_info.lessee_id.name if property_info.lessee_id else ''
else:
building = '天河商贸大厦'
floor = record.epitope_values_id.name if record.epitope_values_id else ''
room = ''
address = ''
employee_name = ''
state = record.state
tvalue = {
'id': record.id,
'code': record.water_id.name if type == 'water' else record.archives_id.name,
'last_dosage': record.last_dosage or 0,
'mater_data': str(record.meter_date) if record.meter_date else '',
'last_mater_data': str(record.last_meter) if record.last_meter else '',
'meter_type': state,
'employee_name': employee_name,
'address': address,
'building': building,
'floor': floor,
'room': room,
'epitope_values': record.epitope_values_id.name,
'unit_number': record.unit_number,
'state': record.state, # 状态 reading unreading
'last_read': record.last_read,
'current_read': clean_floats(record.current_read),
'current_meter': clean_floats(record.current_meter)
}
result.append(tvalue)
return json.dumps(result, allow_nan=False)
@route('/yuthon/meter/dosage', methods=['POST'], type='http', auth='none', cors='*', csrf=False)
def yuthon_meter_dosage(self, **kw):
"""
更新仪表用量
:param code: 编号
:param dosage: 用量
:param meter_type: 仪表类型
"""
id = kw.get('id')
code = kw.get('code') # 编号
current_read = kw.get('current_read') # 本次读数
last_reading_time = kw.get('last_reading_time') # 本次读数时间
models_config = [
{'model_name': 'meter.archives.line', 'type_name': '电表'},
{'model_name': 'meter.water.line', 'type_name': '水表'}
]
record = None
meter_type_name = None
for config in models_config:
current_record = request.env(user=SUPERUSER_ID)[config['model_name']].browse(int(id))
if current_record.exists():
record = current_record
meter_type_name = config['type_name']
break
if record:
record.write({'current_read': current_read, 'last_reading_time': last_reading_time, 'state': 'reading'})
message = f"成功更新{meter_type_name}用量: {code}"
else:
message = f"未找到{meter_type_name}记录: {code}"
return_data = {
'data': {},
'code': '200',
'message': message,
}
_logger.info(message)
return json.dumps(return_data)
@route('/yuthon/peoperty/accounts/receivable', methods=['POST'], type='http', auth='none', cors='*', csrf=False)
def yuthon_peoperty_accounts_receivable(self, **kw):
_logger.info("-----/yuthon/peoperty/accounts/receivable---------接口收到请求-----")
phone = kw.get('phone')
if not phone:
return json.dumps({
'data': {},
'code': '400',
'message': '请输入手机号'
})
lessee_records = request.env(user=SUPERUSER_ID)['property.tenant.information'].search([('phone', '=', phone)])
if not lessee_records:
return json.dumps({
'data': {},
'code': '400',
'message': '未找到该手机号对应的承租方'
})
lessee_id = lessee_records[0].id
# 查询最近三个月记录
today = datetime.today()
three_months_ago = today - relativedelta(months=3)
start_of_month = three_months_ago.replace(day=1).date()
_logger.info(f'月初数据:{start_of_month}')
records = request.env(user=SUPERUSER_ID)['property.accounts.receivable'].search(
[('lessee_id', '=', lessee_id)], order='due_date desc')
data = {}
for record in records:
month = record.due_date.strftime('%Y-%m')
value = data.get(month)
if not value:
value = {
'total': 0,
'property_dict': {}
}
data.update({month: value})
# 金额合计
value['total'] += record.submitted
for prop in record.property_ids:
property_id = prop.id
property_dict = value.get('property_dict').get(property_id)
if not property_dict:
property_dict = {
'property_name': prop.name,
'total': 0,
'id': property_id,
'list': []
}
value['property_dict'].update({property_id: property_dict})
property_dict['total'] += record.submitted
property_dict['list'].append({
'id': record.id,
'submitted': float_repr(float_round(record.submitted, precision_rounding=0.01), 2),
'original_amount': float_repr(
float_round(record.original_amount - record.discount_amount, precision_rounding=0.01), 2),
'management_type': record.management_type_id.name,
'start_date': fields.Date.to_string(record.due_date),
'end_date': fields.Date.to_string(record.due_date_end) if record.due_date_end else '----'
})
result = []
for k, v in data.items():
value = {
'month': k,
'total': float_repr(float_round(v.get('total'), precision_digits=2), 2),
'property_list': [
{
'id': k1,
'property_name': v1.get('property_name'),
'total': float_repr(float_round(v1.get('total'), precision_rounding=0.01), 2),
'list': v1.get('list')
} for k1, v1 in v.get('property_dict').items()
]
}
result.append(value)
return json.dumps({
'data': result,
'code': '200',
'message': '成功'
})
@route('/yuthon/wifi/message/create', methods=['POST'], type='http', auth='none', cors='*', csrf=False)
def yuthon_wifi_message_create(self, **kwargs):
# mac, name, bssid, company_id
# 获取参数
mac = kwargs.get('mac')
name = kwargs.get('name')
bssid = kwargs.get('bssid')
company_id = kwargs.get('company_id')
# 判断是否为空
if not all([mac, name, bssid, company_id]):
return json.dumps({
'code': 400,
'message': '参数不完整'
})
wifi_message = request.env['wifi.message'].sudo().search([
('mac', '=', mac),
('company_id', '=', company_id)
], limit=1)
if wifi_message:
wifi_message.sudo().write({
'name': name,
'bssid': bssid,
})
else:
request.env['wifi.message'].sudo().create({
'mac': mac,
'name': name,
'bssid': bssid,
'company_id': company_id
})
return json.dumps({
'code': 200,
'message': '成功'
})
@route('/yuthon/company/list', methods=['POST'], type='http', auth='none', cors='*', csrf=False)
def yuthon_company_list(self, **kwargs):
return json.dumps({
'code': 200,
'message': '成功',
'data': request.env['res.company'].sudo().search_read([], ['id', 'name'])
})
@route('/yuthon/applet/address/create', methods=['POST'], type='http', auth='none', cors='*', csrf=False)
def yuthon_applet_address_create(self, **kwargs):
# address_name, address_name2, company_id, longitude,latitude,card_rice
# 获取参数
address_name = kwargs.get('address_name')
address_name2 = kwargs.get('address_name2')
company_id = kwargs.get('company_id')
longitude = kwargs.get('longitude')
latitude = kwargs.get('latitude')
card_rice = kwargs.get('card_rice')
# 判断是否为空
if not all([address_name, address_name2, longitude, latitude, card_rice, company_id]):
return json.dumps({
'code': 400,
'message': '参数不完整'
})
applet_address = request.env['applet.address'].sudo().search([
('address_name', '=', address_name),
('longitude', '=', longitude),
('latitude', '=', latitude),
('company_id', '=', company_id)
], limit=1)
if applet_address:
applet_address.sudo().write({
'address_name2': address_name2,
'card_rice': card_rice,
})
else:
request.env['applet.address'].sudo().create({
'address_name': address_name,
'address_name2': address_name2,
'longitude': longitude,
'latitude': latitude,
'card_rice': card_rice,
'company_id': company_id
})
return json.dumps({
'code': 200,
'message': '成功'
})