607 lines
24 KiB
Python
607 lines
24 KiB
Python
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,
|
||
}
|
||
}
|