Files
gzth/yuthon_catering/models/yuthon_catering.py
T
2026-07-09 10:06:16 +08:00

565 lines
23 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 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}日'