Files
gzth/yuthon_salary/models/yuthon_meal_expenses.py
2026-07-06 10:30:41 +08:00

607 lines
24 KiB
Python
Raw Permalink 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
import calendar
class YuthonMealExpenses(models.Model):
_name = 'yuthon.meal.expenses'
_description = '餐费'
employee_id = fields.Many2one('hr.employee', string='姓名')
employee_number = fields.Char(string='序号', related='employee_id.number', store=True)
card = fields.Char(string='身份证')
number_breakfasts = fields.Integer(string='早餐次数', compute='_compute_catering_counts', store=True)
number_lunches = fields.Integer(string='午餐次数', compute='_compute_catering_counts', store=True)
total_payment = fields.Float(string='合计缴费', compute='_compute_total_payment', store=True)
company_id = fields.Many2one(related='employee_id.company_id', string="公司")
select_time = fields.Selection([('1', '一月'), ('2', '二月'), ('3', '三月'), ('4', '四月'),
('5', '五月'), ('6', '六月'), ('7', '七月'), ('8', '八月'),
('9', '九月'), ('10', '十月'), ('11', '十一月'),
('12', '十二月')], string='月份',
default=lambda self: self._get_default_month())
select_year = fields.Selection(selection=[(str(i), str(i)) for i in range(2024, 2040)], string='年份',
default=lambda self: str(datetime.now().year))
meal_state = fields.Selection([('to_do', '待发送'), ('done', '已发送')], string='状态', default='to_do')
toll_state = fields.Selection([
('to_do', '待缴'),
('done', '待复核确认'),
('paid', '已缴费'),
], string='缴费状态', default='to_do')
def _get_date_range(self):
year = int(self.select_year)
month = int(self.select_time)
last_day = calendar.monthrange(year, month)[1]
date_start = '%04d-%02d-01' % (year, month)
date_end = '%04d-%02d-%02d' % (year, month, last_day)
return date_start, date_end
@api.depends('employee_id', 'select_year', 'select_time')
def _compute_catering_counts(self):
for record in self:
if not record.employee_id or not record.select_year or not record.select_time:
record.number_breakfasts = 0
record.number_lunches = 0
continue
date_start, date_end = record._get_date_range()
caterings = self.env['yuthon.catering'].search([
('employee_id', '=', record.employee_id.id),
('date', '>=', date_start),
('date', '<=', date_end),
])
record.number_breakfasts = len(caterings.filtered(lambda c: c.breakfast))
record.number_lunches = len(caterings.filtered(lambda c: c.lunch))
@api.depends('number_breakfasts', 'number_lunches')
def _compute_total_payment(self):
for record in self:
record.total_payment = record.number_breakfasts * 1.0 + record.number_lunches * 2.0
@api.model
def _name_search(self, name='', domain=None, operator='ilike', limit=100, order=None):
domain = domain or []
if name:
domain = ['|', ('employee_id.name', operator, name), ('card', operator, name)] + domain
return super(YuthonMealExpenses, self)._name_search(name='', domain=domain, operator=operator, limit=limit, order=order)
def toll_confirm(self):
self.toll_state = 'done'
def action_review_confirm(self):
"""打开缴费复核确认弹窗,只对状态为待复核确认的记录生效"""
self.ensure_one()
if self.toll_state != 'done':
return
return {
'type': 'ir.actions.act_window',
'name': '复核确认',
'res_model': 'meal.expenses.review.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'default_meal_id': self.id,
},
}
def _get_default_month(self):
current_month = datetime.now().month
return str(current_month)
def _remove_leading_zero(self, date_str):
"""去掉日期字符串的前导零,如 '01月01日' -> '1月1日'"""
month, day = date_str.split('月')
day = day.replace('日', '')
return f"{int(month)}月{int(day)}日"
def _is_chinese_workday(self, check_date=None):
"""判断给定日期是否为中国工作日(考虑法定节假日和调休)"""
if check_date is None:
check_date = datetime.now()
# 获取当年行政考勤工作日的节假日数据
current_year = check_date.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(d) for d in holidays_list]
shift_days_list = [self._remove_leading_zero(d) for d in shift_days_list]
date_str = self._remove_leading_zero(check_date.strftime('%m月%d日'))
if date_str in shift_days_list:
# 调休补班日,强制算工作日
return True
if date_str in holidays_list:
# 法定节假日或调休休息日,算非工作日
return False
# 没有特殊安排的,按周末判断
return check_date.weekday() < 5
def send_meal(self):
return {
'type': 'ir.actions.act_window',
'name': '餐费提醒',
'view_mode': 'form',
'res_model': 'party.meal.wizard',
'target': 'new',
'context': {'default_employee_ids': [(6, 0, [self.employee_id.id])],
'skip_party_month_onchange': True
},
}
def action_mark_meal_sent(self):
domain = self.env.context.get('active_domain') or []
records = self.search(domain)
to_update = records.filtered(lambda r:r.meal_state == 'to_do')
if to_update:
to_update.write({'meal_state': 'done'})
return True
def cron_send_meal_reminder(self):
"""定时任务:给状态为已发送且缴费状态为待缴的记录发送缴费提醒,每人每天只发一次"""
# 节假日不发送
if not self._is_chinese_workday():
self.env['ir.logging'].sudo().create({
'name': '餐费缴费提醒',
'type': 'server',
'message': f'今天{datetime.now().strftime("%Y-%m-%d")}是节假日或周末,跳过发送餐费缴费提醒',
'path': __name__,
'func': 'cron_send_meal_reminder',
'line': 0,
})
return True
# 每月1号跳过:交给 cron_send_monthly_meal_reminder 处理,避免重复发送
if datetime.now().day == 1:
return True
# 找出所有 已发送 且 仍然 待缴 的餐费记录
meals = self.sudo().search([
('meal_state', '=', 'done'),
('toll_state', '=', 'to_do'),
])
# 按员工去重,每人只发一条
sent_employees = set()
sent_count = 0
for meal in meals:
# 如果早餐次数和午餐次数都为0,则不发送提醒
if meal.number_breakfasts == 0 and meal.number_lunches == 0:
continue
employee = meal.employee_id
if not employee or not employee.user_id or not employee.wecom_userid:
continue
if employee.id in sent_employees:
continue
sent_employees.add(employee.id)
# 和现有手机端餐费链接保持一致
url = f'https://phone.thtzjt.com/phone/Meal/{employee.user_id.id}'
# 提示文案,按照要求修改
employee_name = employee.name or ''
description = (
f'{employee_name},您尚有餐费未缴纳,请及时登录系统查看并完成缴费,感谢配合。'
)
self.sudo().env["wecom.apps"].sync_send_message_textcard(
category='can',
user_id=employee.wecom_userid,
title="缴纳餐费提醒",
description=description,
url=url,
btntxt="详细信息",
)
sent_count += 1
# 记录发送日志
self.env['ir.logging'].sudo().create({
'name': '餐费缴费提醒',
'type': 'server',
'message': f'成功发送{sent_count}条餐费缴费提醒({len(meals)}条待缴记录,去重后发送{sent_count}人)',
'path': __name__,
'func': 'cron_send_meal_reminder',
'line': 0,
})
def action_generate_missing_feb(self):
target_year = '2026'
target_month = '2'
# 取全部参餐员工
all_confs = self.env['yuthon.catering.conf'].search([])
all_employees = all_confs.mapped('employee_ids')
# 已有记录的员工
existing = self.search([
('select_year', '=', target_year),
('select_time', '=', target_month),
])
existing_emp_ids = existing.mapped('employee_id').ids
# 批量创建缺失记录
created = 0
for emp in all_employees:
if emp.id not in existing_emp_ids:
self.create({
'employee_id': emp.id,
'card': emp.identification_id or '',
'select_year': target_year,
'select_time': target_month,
})
created += 1
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '生成完成',
'message': '共生成2026年2月缺失数据 %d 条' % created,
'type': 'success',
'sticky': False,
}
}
def action_generate_missing_mar(self):
target_year = '2026'
target_month = '3'
# 取全部参餐员工
all_confs = self.env['yuthon.catering.conf'].search([])
all_employees = all_confs.mapped('employee_ids')
# 已有记录的员工
existing = self.search([
('select_year', '=', target_year),
('select_time', '=', target_month),
])
existing_emp_ids = existing.mapped('employee_id').ids
# 批量创建缺失记录
created = 0
for emp in all_employees:
if emp.id not in existing_emp_ids:
self.create({
'employee_id': emp.id,
'card': emp.identification_id or '',
'select_year': target_year,
'select_time': target_month,
})
created += 1
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '生成完成',
'message': '共生成2026年3月缺失数据 %d 条' % created,
'type': 'success',
'sticky': False,
}
}
def cron_generate_last_month_meal(self):
"""定时任务:每月1号生成上月餐费报表"""
from datetime import datetime, timedelta
# 计算上月的年份和月份
today = datetime.now()
last_month = today - timedelta(days=today.day)
target_year = str(last_month.year)
target_month = str(last_month.month)
# 取全部参餐员工
all_confs = self.env['yuthon.catering.conf'].search([])
all_employees = all_confs.mapped('employee_ids')
# 已有记录的员工
existing = self.search([
('select_year', '=', target_year),
('select_time', '=', target_month),
])
existing_emp_ids = existing.mapped('employee_id').ids
# 批量创建缺失记录
created = 0
for emp in all_employees:
if emp.id not in existing_emp_ids:
self.create({
'employee_id': emp.id,
'card': emp.identification_id or '',
'select_year': target_year,
'select_time': target_month,
})
created += 1
# 记录日志
self.env['ir.logging'].sudo().create({
'name': '餐费报表生成',
'type': 'server',
'message': f'成功生成{target_year}年{target_month}月餐费数据 {created} 条',
'path': __name__,
'func': 'cron_generate_last_month_meal',
'line': 0,
})
return True
def cron_send_monthly_meal_reminder(self):
"""定时任务:每月第一个工作日发送餐费缴费提醒"""
from datetime import datetime, timedelta
import calendar
# 检查是否是中国工作日(含法定节假日和调休判断)
today = datetime.now()
if not self._is_chinese_workday(today):
# 记录日志
self.env['ir.logging'].sudo().create({
'name': '餐费缴费提醒',
'type': 'server',
'message': f'今天{today.strftime("%Y-%m-%d")}是节假日或周末,跳过发送餐费缴费提醒',
'path': __name__,
'func': 'cron_send_monthly_meal_reminder',
'line': 0,
})
return True
# 计算上月的年份和月份
last_month = today - timedelta(days=today.day)
last_year = str(last_month.year)
last_month_num = str(last_month.month)
# 计算当前月的年份和月份
current_year = str(today.year)
current_month = str(today.month)
# 查询所有状态为待缴且待发送的餐费记录
meals = self.sudo().search([
('toll_state', '=', 'to_do'),
('meal_state', '=', 'to_do'),
('select_year', '=', last_year),
('select_time', '=', last_month_num),
])
# 按员工分组
employee_meals = {}
for meal in meals:
if meal.employee_id not in employee_meals:
employee_meals[meal.employee_id] = meal
# 发送提醒
sent_count = 0
for employee, meal in employee_meals.items():
if not employee or not employee.user_id or not employee.wecom_userid:
continue
# 计算上月餐费金额
last_month_amount = meal.total_payment
# 计算当月餐费金额(如果有)
current_month_meal = self.sudo().search([
('employee_id', '=', employee.id),
('select_year', '=', current_year),
('select_time', '=', current_month),
], limit=1)
current_month_amount = current_month_meal.total_payment if current_month_meal else 0
# 构建提醒内容
employee_name = employee.name or ''
description = (
f'【餐费缴费提醒】亲爱的同事,{last_year}年{last_month_num}月餐费明细已出,你的上月餐费金额为{last_month_amount}元,'
f'请尽快核对并缴费到指定账户:44001580909059000222广州市天河投资集团有限公司(转账时请备注"姓名+{last_month_num}月餐费")。'
f'有任何问题,随时找办公室小伙伴沟通呀!'
)
# 发送提醒
try:
self.sudo().env["wecom.apps"].sync_send_message_textcard(
category='can',
user_id=employee.wecom_userid,
title="餐费缴费提醒",
description=description,
url=f'https://phone.thtzjt.com/phone/Meal/{employee.user_id.id}',
btntxt="详细信息",
)
# 发送后将状态改为已发送
meal.write({'meal_state': 'done'})
sent_count += 1
# 记录日志,避免重复发送
self.env['ir.logging'].sudo().create({
'name': '餐费缴费提醒',
'type': 'server',
'message': f'已发送{last_year}年{last_month_num}月餐费缴费提醒给{employee_name}',
'path': __name__,
'func': 'cron_send_monthly_meal_reminder',
'line': 0,
})
except Exception as e:
self.env['ir.logging'].sudo().create({
'name': '餐费缴费提醒',
'type': 'server',
'message': f'发送{last_year}年{last_month_num}月餐费缴费提醒给{employee_name}失败:{str(e)}',
'path': __name__,
'func': 'cron_send_monthly_meal_reminder',
'line': 0,
})
# 记录总发送情况
self.env['ir.logging'].sudo().create({
'name': '餐费缴费提醒',
'type': 'server',
'message': f'成功发送{sent_count}条{last_year}年{last_month_num}月餐费缴费提醒',
'path': __name__,
'func': 'cron_send_monthly_meal_reminder',
'line': 0,
})
return True
def action_test_monthly_reminder(self):
"""手动测试月度提醒"""
# 调用定时任务方法
result = self.cron_send_monthly_meal_reminder()
# 显示成功通知
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '测试完成',
'message': '月度提醒测试已执行,请查看日志确认结果',
'type': 'success',
'sticky': False,
}
}
def action_view_detail(self):
self.ensure_one()
date_start, date_end = self._get_date_range()
catering_tree_view = self.env.ref('yuthon_catering.yuthon_catering_tree_view')
return {
'type': 'ir.actions.act_window',
'name': '%s年%s月 %s 餐饮明细' % (self.select_year, self.select_time, self.employee_id.name),
'res_model': 'yuthon.catering',
'view_mode': 'tree',
'views': [(catering_tree_view.id, 'tree')],
'target': 'new',
'domain': [('employee_id', '=', self.employee_id.id),
('date', '>=', date_start),
('date', '<=', date_end)],
}
def action_send_reminder(self):
"""一键催缴:手动发送缴费提醒"""
self.ensure_one()
# 如果早餐次数和午餐次数都为0,则不发送提醒
if self.number_breakfasts == 0 and self.number_lunches == 0:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '提醒',
'message': '该记录早餐和午餐次数都为0,无需发送提醒',
'type': 'info',
'sticky': False,
}
}
employee = self.employee_id
if not employee or not employee.user_id or not employee.wecom_userid:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '错误',
'message': '员工信息不完整,无法发送提醒',
'type': 'danger',
'sticky': False,
}
}
# 和现有手机端餐费链接保持一致
url = f'https://phone.thtzjt.com/phone/Meal/{employee.user_id.id}'
# 提示文案,按照要求修改
employee_name = employee.name or ''
description = (
f'{employee_name},您尚有餐费未缴纳,请及时登录系统查看并完成缴费,感谢配合。'
)
try:
self.sudo().env["wecom.apps"].sync_send_message_textcard(
category='can',
user_id=employee.wecom_userid,
title="缴纳餐费提醒",
description=description,
url=url,
btntxt="详细信息",
)
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '发送成功',
'message': '提醒已发送给员工',
'type': 'success',
'sticky': False,
}
}
except Exception as e:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '发送失败',
'message': f'提醒发送失败:{str(e)}',
'type': 'danger',
'sticky': False,
}
}
def action_batch_send_reminder(self):
"""批量催缴:发送提醒给所有符合条件的员工"""
# 查询所有缴费状态为待缴,状态为已发送的记录
meals = self.sudo().search([
('meal_state', '=', 'done'),
('toll_state', '=', 'to_do'),
])
success_count = 0
skip_count = 0
error_count = 0
for meal in meals:
# 如果早餐次数和午餐次数都为0,则不发送提醒
if meal.number_breakfasts == 0 and meal.number_lunches == 0:
skip_count += 1
continue
employee = meal.employee_id
if not employee or not employee.user_id or not employee.wecom_userid:
error_count += 1
continue
# 和现有手机端餐费链接保持一致
url = f'https://phone.thtzjt.com/phone/Meal/{employee.user_id.id}'
# 提示文案,按照要求修改
employee_name = employee.name or ''
description = (
f'{employee_name},您尚有餐费未缴纳,请及时登录系统查看并完成缴费,感谢配合。'
)
try:
self.sudo().env["wecom.apps"].sync_send_message_textcard(
category='can',
user_id=employee.wecom_userid,
title="缴纳餐费提醒",
description=description,
url=url,
btntxt="详细信息",
)
success_count += 1
except Exception:
error_count += 1
# 构建返回消息
message_parts = []
if success_count > 0:
message_parts.append(f'成功发送{success_count}条提醒')
if skip_count > 0:
message_parts.append(f'跳过{skip_count}条记录(早餐和午餐次数都为0)')
if error_count > 0:
message_parts.append(f'失败{error_count}条记录(员工信息不完整)')
message = ';'.join(message_parts)
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '批量催缴完成',
'message': message,
'type': 'success' if success_count > 0 else 'info',
'sticky': False,
}
}