565 lines
23 KiB
Python
565 lines
23 KiB
Python
|
||
from odoo import api, fields, models
|
||
from datetime import datetime, timedelta
|
||
|
||
|
||
class YuthonCatering(models.Model):
|
||
_name = 'yuthon.catering'
|
||
|
||
date = fields.Date(string='日期', required=True)
|
||
employee_number = fields.Char(string='序号', related='employee_id.number', store=True)
|
||
week_day = fields.Char(string='星期', compute='_compute_week_day', store=True)
|
||
dept_id = fields.Many2one(string='部门', comodel_name='hr.department', related='employee_id.department_id', store=True)
|
||
employee_id = fields.Many2one(string='员工', comodel_name='hr.employee', required=True)
|
||
entry_date = fields.Date(related='employee_id.entry_date', string="入职时间", store=True)
|
||
breakfast = fields.Boolean(string='早餐', default=False, group_operator='count_true')
|
||
lunch = fields.Boolean(string='午餐', default=False, group_operator='count_true')
|
||
breakfast_count = fields.Integer(string='早餐人数', compute='_compute_meal_count', store=True)
|
||
lunch_count = fields.Integer(string='午餐人数', compute='_compute_meal_count', store=True)
|
||
menu_day_id = fields.Many2one('yuthon.menu.setting', string='菜单')
|
||
amount = fields.Float(string='结算费用', compute='_compute_amount', store=True)
|
||
|
||
@api.depends('breakfast', 'lunch')
|
||
def _compute_meal_count(self):
|
||
for rec in self:
|
||
rec.breakfast_count = 1 if rec.breakfast else 0
|
||
rec.lunch_count = 1 if rec.lunch else 0
|
||
|
||
@api.depends('date')
|
||
def _compute_week_day(self):
|
||
week_map = {'0': '周一', '1': '周二', '2': '周三', '3': '周四', '4': '周五', '5': '周六', '6': '周日'}
|
||
for record in self:
|
||
if record.date:
|
||
record.week_day = week_map[str(record.date.weekday())]
|
||
else:
|
||
record.week_day = ''
|
||
|
||
@api.depends('breakfast', 'lunch')
|
||
def _compute_amount(self):
|
||
for record in self:
|
||
record.amount = (1.0 if record.breakfast else 0.0) + (2.0 if record.lunch else 0.0)
|
||
note = fields.Text(string='备注')
|
||
state = fields.Selection([
|
||
('unconfirmed', '未确认'),
|
||
('modifying', '修改中'),
|
||
('confirmed', '已确认'),
|
||
], default='unconfirmed', string='状态')
|
||
|
||
def _recompute_related_meal_expenses(self):
|
||
"""当 yuthon.catering 数据变化时,主动触发关联 yuthon.meal.expenses 重算"""
|
||
for record in self:
|
||
if not record.employee_id or not record.date:
|
||
continue
|
||
year = str(record.date.year)
|
||
month = str(record.date.month)
|
||
meal_records = self.env['yuthon.meal.expenses'].search([
|
||
('employee_id', '=', record.employee_id.id),
|
||
('select_year', '=', year),
|
||
('select_time', '=', month),
|
||
])
|
||
if meal_records:
|
||
meal_records._compute_catering_counts()
|
||
|
||
def write(self, vals):
|
||
# 记录写入前的员工和日期(防止改了 employee_id 或 date 后找不到旧记录)
|
||
old_data = [(r.employee_id, r.date) for r in self]
|
||
result = super().write(vals)
|
||
# 触发旧记录关联的 meal.expenses 重算
|
||
for emp, date in old_data:
|
||
if emp and date:
|
||
year = str(date.year)
|
||
month = str(date.month)
|
||
meal_records = self.env['yuthon.meal.expenses'].search([
|
||
('employee_id', '=', emp.id),
|
||
('select_year', '=', year),
|
||
('select_time', '=', month),
|
||
])
|
||
if meal_records:
|
||
meal_records._compute_catering_counts()
|
||
# 如果改了 employee_id 或 date,也触发新记录
|
||
if 'employee_id' in vals or 'date' in vals:
|
||
self._recompute_related_meal_expenses()
|
||
return result
|
||
|
||
@api.model_create_multi
|
||
def create(self, vals_list):
|
||
records = super().create(vals_list)
|
||
records._recompute_related_meal_expenses()
|
||
return records
|
||
|
||
def unlink(self):
|
||
# 删除前先收集关联信息
|
||
related_info = [(r.employee_id, r.date) for r in self if r.employee_id and r.date]
|
||
result = super().unlink()
|
||
for emp, date in related_info:
|
||
year = str(date.year)
|
||
month = str(date.month)
|
||
meal_records = self.env['yuthon.meal.expenses'].search([
|
||
('employee_id', '=', emp.id),
|
||
('select_year', '=', year),
|
||
('select_time', '=', month),
|
||
])
|
||
if meal_records:
|
||
meal_records._compute_catering_counts()
|
||
return result
|
||
|
||
def task_catering22(self):
|
||
cat_ids = self.env["yuthon.catering"].browse(self._context.get('active_ids', self._context.get('active_id')))
|
||
for cat in cat_ids:
|
||
cat.write({
|
||
'breakfast': True,
|
||
'lunch': True,
|
||
})
|
||
|
||
def _get_active_conf_employees(self):
|
||
"""获取当前有效配置(无失效日期)的员工列表"""
|
||
conf = self.env['yuthon.catering.conf'].search([('expiry_date', '=', False)], limit=1)
|
||
return conf.employee_ids if conf else self.env['hr.employee']
|
||
|
||
def task_catering(self):
|
||
today = fields.Date.today()
|
||
|
||
# 幂等检查:今天已经发过就跳过,防止 cron 重试/并发导致重复发送
|
||
config_param = self.env['ir.config_parameter'].sudo()
|
||
last_sent = config_param.get_param('yuthon_catering.last_task_catering_date')
|
||
if last_sent == str(today):
|
||
return
|
||
|
||
tomorrow = today + timedelta(days=1)
|
||
employee_ids = self._get_active_conf_employees()
|
||
if not self._process_shift_days(today):
|
||
return
|
||
tomorrow_menu = self.env['yuthon.menu.setting'].search([
|
||
('menu_date', '=', tomorrow),
|
||
('breakfast_ids', '!=', False),
|
||
('lunch_ids', '!=', False)
|
||
], limit=1)
|
||
if not tomorrow_menu:
|
||
return
|
||
for emp in employee_ids:
|
||
if not emp.user_id:
|
||
continue # 防止 user_id 为空导致 URL 异常
|
||
url = f"https://phone.thtzjt.com/phone/Restaurant/{emp.user_id.id}"
|
||
self.sudo().env["wecom.apps"].sync_send_message_textcard(
|
||
category='can',
|
||
user_id=emp.wecom_userid,
|
||
title="餐饮通知",
|
||
description="请在 18:00 前登记下一工作日用餐,18点后未填的系统将自动视为就餐,如有疑问,联系办公室。",
|
||
url=url,
|
||
btntxt="详细信息"
|
||
)
|
||
|
||
# 全部发完后才标记今天已发送
|
||
config_param.set_param('yuthon_catering.last_task_catering_date', str(today))
|
||
def task_conf(self):
|
||
tomorrow = fields.Date.today() + timedelta(days=1)
|
||
company_main = self.env["res.company"].search([("name", "=", "集团本部")], limit=1)
|
||
catering_conf = self.env['yuthon.catering.conf'].search([('company_id', '=', company_main.id), ('expiry_date', '=', False)], limit=1)
|
||
employee_ids = catering_conf.employee_ids if catering_conf else self.env['hr.employee']
|
||
menu_setting = self.env["yuthon.menu.setting"].search([("menu_date", "=", tomorrow)], limit=1)
|
||
if not menu_setting:
|
||
return
|
||
for employee in employee_ids:
|
||
catering_record = self.env["yuthon.catering"].search([
|
||
("employee_id", "=", employee.id),
|
||
("date", "=", tomorrow)
|
||
], limit=1)
|
||
if not catering_record:
|
||
self.env["yuthon.catering"].create({
|
||
"date": tomorrow,
|
||
"employee_id": employee.id,
|
||
"breakfast": False,
|
||
"lunch": False,
|
||
"menu_day_id": False,
|
||
"note": "",
|
||
})
|
||
|
||
def _process_shift_days(self, current_data):
|
||
"""
|
||
判断是否是工作日 调休日 双休
|
||
"""
|
||
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),
|
||
('resource_id', '=', False),
|
||
])
|
||
holidays_list = []
|
||
shift_days_list = []
|
||
# 提取节假日和调休日
|
||
for leave in calendar_leaves:
|
||
if leave.holidays:
|
||
holidays_list.extend(leave.holidays.split(','))
|
||
if leave.alternate_rest_day:
|
||
holidays_list.extend(leave.alternate_rest_day.split(','))
|
||
if leave.shift_replacement_day:
|
||
shift_days_list.extend(leave.shift_replacement_day.split(','))
|
||
|
||
holidays_list = [self.remove_leading_zero(date) for date in holidays_list]
|
||
shift_days_list = [self.remove_leading_zero(date) for date in shift_days_list]
|
||
result = f"{current_data.month}月{current_data.day}日"
|
||
if result in shift_days_list:
|
||
return True
|
||
elif (result in holidays_list) or (current_data.weekday() in [5, 6]):
|
||
return False
|
||
else:
|
||
return True
|
||
|
||
|
||
def catering_task(self):
|
||
employee_ids = self._get_active_conf_employees()
|
||
|
||
# 获取今天的日期
|
||
today = fields.Date.today()
|
||
# 创建未来7天的日期列表
|
||
date_list = [today + timedelta(days=i) for i in range(7)]
|
||
|
||
# 初始化待创建记录列表
|
||
create_vals = []
|
||
# 遍历每个员工
|
||
for employee in employee_ids:
|
||
# 遍历每个日期
|
||
for target_date in date_list:
|
||
# 检查目标日期是否为工作日,如果不是则跳过
|
||
if not self._process_shift_days(target_date):
|
||
continue
|
||
# 检查是否已存在该员工在该日期的记录
|
||
existing = self.search([
|
||
('employee_id', '=', employee.id),
|
||
('date', '=', target_date)
|
||
], limit=1)
|
||
|
||
# 查找当天的菜单设置
|
||
menu_record = self.env['yuthon.menu.setting'].search([
|
||
('menu_date', '=', target_date)
|
||
], limit=1)
|
||
|
||
# 如果不存在记录,则准备创建新记录的数据
|
||
if not existing:
|
||
create_vals.append({
|
||
'menu_record': menu_record.id if menu_record else False,
|
||
'date': target_date,
|
||
'employee_id': employee.id,
|
||
})
|
||
|
||
# 如果有待创建的记录,则批量创建
|
||
if create_vals:
|
||
self.create(create_vals)
|
||
|
||
# 定时任务:每天晙21点自动补全当天缺失的餐饮数据,再自动确认
|
||
def cron_fill_today_missing(self):
|
||
today = fields.Date.today() + timedelta(days=1)
|
||
# 检查当天是否有菜单记录,没有则跳过
|
||
menu_today = self.env['yuthon.menu.setting'].search([('menu_date', '=', today)], limit=1)
|
||
if not menu_today:
|
||
return
|
||
employee_ids = self._get_active_conf_employees()
|
||
if not employee_ids:
|
||
return
|
||
create_vals = []
|
||
for employee in employee_ids:
|
||
existing = self.search([
|
||
('employee_id', '=', employee.id),
|
||
('date', '=', today),
|
||
], limit=1)
|
||
if not existing:
|
||
create_vals.append({
|
||
'date': today,
|
||
'employee_id': employee.id,
|
||
'state': 'unconfirmed',
|
||
})
|
||
if create_vals:
|
||
self.create(create_vals)
|
||
# 补全后自动确认当天所有未确认记录
|
||
self.cron_auto_confirm_today()
|
||
|
||
def cron_auto_confirm_today(self):
|
||
today = fields.Date.today()
|
||
unconfirmed_records = self.search([
|
||
('date', '>', today),
|
||
('state', '=', 'unconfirmed'),
|
||
])
|
||
if unconfirmed_records:
|
||
for record in unconfirmed_records:
|
||
if self.is_expatriates(record.date, record.employee_id) or self.is_leave(record.date, record.employee_id):
|
||
breakfast = False
|
||
lunch = False
|
||
else:
|
||
breakfast = True
|
||
lunch = True
|
||
record.write({'state': 'confirmed',
|
||
'breakfast': breakfast, 'lunch': lunch})
|
||
|
||
def is_expatriates(self, current_data, employee):
|
||
expat_approval = self.env['expatriation.approval'].search([
|
||
('is_effect', '=', False),
|
||
('work_end', '=', True),
|
||
('expatriates_ids', 'in', [employee.id])
|
||
], limit=1)
|
||
# 查找有效的返回审批单
|
||
back_approval = self.env['back.approval'].search([
|
||
('is_void', '=', False),
|
||
('work_end', '=', True),
|
||
('expatriates_id', '=', employee.id)
|
||
], limit=1)
|
||
|
||
# 3. 外派中:当前时间 >= 外派开始日期 → 属于外派
|
||
if expat_approval and expat_approval.start_date:
|
||
if current_data >= expat_approval.start_date:
|
||
return True
|
||
|
||
if back_approval and back_approval.end_date:
|
||
if current_data >= back_approval.end_date:
|
||
return False
|
||
|
||
def is_leave(self, current_data, employee):
|
||
leaves = self.env['hr.leave'].search([
|
||
('employee_id', '=', employee.id),
|
||
('state', '=', 'validate'),
|
||
])
|
||
for leave in leaves:
|
||
if leave.date_from.date() <= current_data <= leave.date_to.date():
|
||
return True
|
||
return False
|
||
|
||
def is_maternity_leave(self, employee):
|
||
"""判断员工当前是否在产假期间"""
|
||
today = fields.Date.today()
|
||
leaves = self.env['hr.leave'].search([
|
||
('employee_id', '=', employee.id),
|
||
('state', '=', 'validate'),
|
||
('holiday_status_id.name', '=', '产假'),
|
||
])
|
||
for leave in leaves:
|
||
if leave.date_from.date() <= today <= leave.date_to.date():
|
||
return True
|
||
return False
|
||
|
||
# 定义定时任务方法,每天九点半发送消息提醒
|
||
def cron_send_message(self):
|
||
"""
|
||
定时任务:每天上午九点半发送消息提醒
|
||
"""
|
||
# 获取当前时间(本地时间)
|
||
current_utc_time = fields.Datetime.now()
|
||
local_time = current_utc_time + timedelta(hours=8)
|
||
h = local_time.hour
|
||
m = local_time.minute
|
||
# 检查明天是否有菜单记录,没有则跳过
|
||
tomorrow = fields.Date.today() + timedelta(days=1)
|
||
menu_tomorrow = self.env['yuthon.menu.setting'].search([('menu_date', '=', tomorrow)], limit=1)
|
||
if not menu_tomorrow:
|
||
return
|
||
# 检查是否是上午9:30左右
|
||
if not (h == 9 and 25 <= m <= 35): # 9:25-9:35之间
|
||
return
|
||
# 判断今天是否为工作日
|
||
today = fields.Date.today()
|
||
if not self._process_shift_days(today) :
|
||
# 如果不是工作日,则不发送提醒
|
||
return
|
||
# 发送通用提醒消息
|
||
employees = self._get_active_conf_employees()
|
||
|
||
for employee in employees:
|
||
# 产假期间不发消息
|
||
if self.is_maternity_leave(employee):
|
||
continue
|
||
if employee.user_id: # 确保员工有关联的用户
|
||
# 构造跳转URL,格式为 f'https://phone.thtzjt.com/phone/Restaurant/(users_id)
|
||
users_id = employee.user_id.id
|
||
target_url = f'https://phone.thtzjt.com/phone/Restaurant/{users_id}'
|
||
|
||
# 发送消息,使用指定的格式
|
||
if hasattr(self.env["wecom.apps"], 'sync_send_message_textcard'):
|
||
description = f"您有1条待办未处理,请及时登OA系统处理,谢谢配合"
|
||
title = "餐饮登记提醒"
|
||
|
||
self.env["wecom.apps"].sync_send_message_textcard(
|
||
category='oa',
|
||
user_id=employee.wecom_userid if hasattr(employee, 'wecom_userid') else employee.user_id.login,
|
||
title=title,
|
||
description=description,
|
||
url=target_url,
|
||
btntxt="查看详情"
|
||
)
|
||
|
||
# 一次性,生成3.1-3.3所有数据,状态为已确认
|
||
def test_1(self):
|
||
employee_ids = self._get_active_conf_employees()
|
||
for emp in employee_ids:
|
||
self.env['yuthon.catering'].create({
|
||
'date': '2026-03-01',
|
||
'state': 'confirmed',
|
||
'employee_id': emp.id,
|
||
'breakfast': True,
|
||
'lunch': True,
|
||
})
|
||
self.env['yuthon.catering'].create({
|
||
'date': '2026-03-02',
|
||
'state': 'confirmed',
|
||
'employee_id': emp.id,
|
||
'breakfast': True,
|
||
'lunch': True,
|
||
})
|
||
self.env['yuthon.catering'].create({
|
||
'date': '2026-03-03',
|
||
'state': 'confirmed',
|
||
'employee_id': emp.id,
|
||
'breakfast': True,
|
||
'lunch': True,
|
||
})
|
||
|
||
def batch_confirm_catering(self):
|
||
records = self.env['yuthon.catering'].browse(self._context.get('active_ids', []))
|
||
records.write({
|
||
'state': 'confirmed',
|
||
'breakfast': True,
|
||
'lunch': True,
|
||
})
|
||
|
||
def fill_march4_confirmed_catering(self):
|
||
"""一次性:补3月4日缺失数据,已确认且早餐午餐全勾"""
|
||
from datetime import date as date_cls
|
||
target_date = date_cls(2026, 4, 21)
|
||
employee_ids = self._get_active_conf_employees()
|
||
if not employee_ids:
|
||
return
|
||
create_vals = []
|
||
for employee in employee_ids:
|
||
existing = self.search([
|
||
('employee_id', '=', employee.id),
|
||
('date', '=', target_date),
|
||
], limit=1)
|
||
if not existing:
|
||
create_vals.append({
|
||
'date': target_date,
|
||
'employee_id': employee.id,
|
||
'state': 'confirmed',
|
||
'breakfast': True,
|
||
'lunch': True,
|
||
})
|
||
if create_vals:
|
||
self.create(create_vals)
|
||
|
||
def fill_march12_missing_catering(self):
|
||
"""表3月12日缺失员工的餐饮收集任务数据"""
|
||
from datetime import date as date_cls
|
||
target_date = date_cls(2026, 3, 12)
|
||
employee_ids = self._get_active_conf_employees()
|
||
if not employee_ids:
|
||
return
|
||
create_vals = []
|
||
for employee in employee_ids:
|
||
existing = self.search([
|
||
('employee_id', '=', employee.id),
|
||
('date', '=', target_date),
|
||
], limit=1)
|
||
if not existing:
|
||
create_vals.append({
|
||
'date': target_date,
|
||
'employee_id': employee.id,
|
||
'state': 'unconfirmed',
|
||
})
|
||
if create_vals:
|
||
self.create(create_vals)
|
||
|
||
def batch_generate_catering(self):
|
||
"""批量生成2025-12、2026-01、2026-02的餐饮收集任务数据,排除节假日"""
|
||
from datetime import date as date_cls
|
||
conf = self.env['yuthon.catering.conf'].search([('expiry_date', '=', False)], limit=1)
|
||
if not conf or not conf.employee_ids:
|
||
return
|
||
employee_ids = conf.employee_ids
|
||
target_months = [
|
||
(2025, 12),
|
||
(2026, 1),
|
||
(2026, 2),
|
||
]
|
||
all_dates = []
|
||
for y, m in target_months:
|
||
d = date_cls(y, m, 1)
|
||
while d.month == m:
|
||
all_dates.append(d)
|
||
d += timedelta(days=1)
|
||
|
||
work_dates = [d for d in all_dates if self._process_shift_days(d)]
|
||
|
||
create_vals = []
|
||
for target_date in work_dates:
|
||
for employee in employee_ids:
|
||
existing = self.search([
|
||
('employee_id', '=', employee.id),
|
||
('date', '=', target_date),
|
||
], limit=1)
|
||
if not existing:
|
||
create_vals.append({
|
||
'date': target_date,
|
||
'employee_id': employee.id,
|
||
'state': 'unconfirmed',
|
||
})
|
||
|
||
if create_vals:
|
||
self.create(create_vals)
|
||
|
||
# 定义批量提醒方法
|
||
def batch_remind_catering(self):
|
||
"""
|
||
批量提醒功能:手动触发提醒选定的记录
|
||
"""
|
||
# 从环境上下文中获取选中的记录ID
|
||
active_ids = self.env.context.get('active_ids')
|
||
# 浏览选中的记录,如果没有则使用当前记录
|
||
selected_records = self.browse(active_ids) if active_ids else self
|
||
# 按员工分组存储待通知的记录
|
||
employees_to_notify = {}
|
||
# 遍历选中的记录
|
||
for record in selected_records:
|
||
# 只处理员工存在且状态为未确认的记录
|
||
if record.employee_id and record.state == 'unconfirmed':
|
||
# 如果员工ID不在字典中,则初始化
|
||
if record.employee_id.id not in employees_to_notify:
|
||
employees_to_notify[record.employee_id.id] = {
|
||
'employee': record.employee_id,
|
||
'records': []
|
||
}
|
||
# 将记录添加到对应员工的列表中
|
||
employees_to_notify[record.employee_id.id]['records'].append(record)
|
||
# 发送提醒给每个员工
|
||
notified_count = 0
|
||
# 遍历员工字典
|
||
for emp_id, data in employees_to_notify.items():
|
||
employee = data['employee']
|
||
records = data['records']
|
||
# 构造跳转URL
|
||
users_id = employee.user_id.id if employee.user_id else ''
|
||
target_url = f'https://phone.thtzjt.com/phone/Restaurant/{users_id}'
|
||
# 发送消息,使用指定的格式
|
||
if hasattr(self.env["wecom.apps"], 'sync_send_message_textcard'):
|
||
description = f"您有{len(data['records'])}条待办未处理,请及时登OA系统处理,谢谢配合"
|
||
title = "餐饮登记提醒"
|
||
self.env["wecom.apps"].sync_send_message_textcard(
|
||
category='can', # 消息类别为OA
|
||
user_id=employee.wecom_userid if hasattr(employee, 'wecom_userid') else employee.user_id.login if employee.user_id else '',
|
||
title=title, # 消息标题
|
||
description=description, # 消息内容
|
||
url=target_url, # 点击后跳转的URL
|
||
btntxt="查看详情" # 按钮文字
|
||
)
|
||
|
||
def action_confirm(self):
|
||
self.ensure_one()
|
||
self.write({
|
||
'state': 'confirmed'
|
||
})
|
||
|
||
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}日'
|