diff --git a/yuthon_hr_attendance/__manifest__.py b/yuthon_hr_attendance/__manifest__.py
index f9c517d3b..7737082a3 100644
--- a/yuthon_hr_attendance/__manifest__.py
+++ b/yuthon_hr_attendance/__manifest__.py
@@ -27,6 +27,7 @@
'views/hr_attendance_views.xml',
'views/resource_calendar_leaves_views.xml',
'views/inherit_hr_employee.xml',
+ 'views/yuthon_meal_line_views.xml',
'views/inherit_planning_role_views.xml',
'views/inherit_planning_slot_views.xml',
'views/attendance_message_log.xml',
@@ -115,6 +116,8 @@
'yuthon_hr_attendance/static/src/xml/button_statement_confirm.xml',
'yuthon_hr_attendance/static/src/js/button_overtime_export.js',
'yuthon_hr_attendance/static/src/xml/button_overtime_export.xml',
+ 'yuthon_hr_attendance/static/src/js/button_vacation_meal_confirm.js',
+ 'yuthon_hr_attendance/static/src/xml/button_vacation_meal_confirm.xml',
'yuthon_hr_attendance/static/src/js/button_overtime_active_export.js',
'yuthon_hr_attendance/static/src/xml/button_overtime_active_export.xml',
'yuthon_hr_attendance/static/src/js/button_meal_active_export.js',
diff --git a/yuthon_hr_attendance/models/__init__.py b/yuthon_hr_attendance/models/__init__.py
index c6979f47f..82b32c066 100644
--- a/yuthon_hr_attendance/models/__init__.py
+++ b/yuthon_hr_attendance/models/__init__.py
@@ -41,6 +41,7 @@ from . import overtime_active_line
from . import vacation_meal_active
from . import duty_adjustment_schedule
from . import overtime_carryover
+from . import yuthon_meal_line
# from odoo.tools import SQL
# import odoo
# odoo.models.READ_GROUP_AGGREGATE.update({
diff --git a/yuthon_hr_attendance/models/vacation_meal_active.py b/yuthon_hr_attendance/models/vacation_meal_active.py
index f7bbb144b..6ae0920d3 100644
--- a/yuthon_hr_attendance/models/vacation_meal_active.py
+++ b/yuthon_hr_attendance/models/vacation_meal_active.py
@@ -8,12 +8,20 @@ from datetime import date
from dateutil.relativedelta import relativedelta
from odoo.exceptions import UserError, ValidationError
+from odoo.odoo.fields import first
+
class VacationMealActive(models.Model):
_name = 'vacation.meal.active'
_description = '误餐费归档'
meal_name = fields.Char(string='标题')
+ select_month = fields.Selection([('1', '一月'), ('2', '二月'), ('3', '三月'), ('4', '四月'),
+ ('5', '五月'), ('6', '六月'), ('7', '七月'), ('8', '八月'),
+ ('9', '九月'), ('10', '十月'), ('11', '十一月'),
+ ('12', '十二月')], string='月份',
+ default=lambda self: str(datetime.now().month))
+ select_year = fields.Selection(selection=[(str(i), str(i)) for i in range(2024, 2060)], string='年份',)
line_id = fields.Many2one('vacation.meal.line', string='关联误餐明细')
employee_id = fields.Many2one('hr.employee', string='员工')
workflow_id = fields.Many2one('soong.workflow', string='审批类型')
@@ -40,15 +48,28 @@ class VacationMealActive(models.Model):
def revoke(self):
self.ensure_one()
+ rec_year = self.select_year
+ rec_month = self.select_month
self.select_revoke = 'done'
- self.line_id.is_filing = True
- report_id = self.env['yuthon.vacation.reports'].search([('employee_id', '=', self.employee_id.id),
- ('vacation_date', '=', (self.start_time.date()).replace(day=1)),
- ])
+ self.line_id.is_filing = False
+ if not rec_month and not rec_year:
+ first_day = (self.start_time.date()).replace(day=1)
+ else:
+ first_day = datetime(rec_year, rec_month, 1).date()
+ report_id = self.env['yuthon.vacation.reports'].search([
+ ('employee_id', '=', self.employee_id.id),
+ ('vacation_date', '=', first_day),
+ ])
meal_id = self.env['vacation.meal.line'].search([('vacation_id', '=', report_id.id),
('update_type', '=', 'reduce')
])
if meal_id:
+ future_reps = self.env['yuthon.vacation.reports'].search([
+ ('employee_id', '=', self.employee_id),
+ ('vacation_date', '>=', first_day)
+ ], order='vacation_date asc')
+ for rep in future_reps:
+ rep.update_line()
meal_id.unlink()
def all_revoke(self):
diff --git a/yuthon_hr_attendance/models/yuthon_meal_line.py b/yuthon_hr_attendance/models/yuthon_meal_line.py
new file mode 100644
index 000000000..9e42f4ed5
--- /dev/null
+++ b/yuthon_hr_attendance/models/yuthon_meal_line.py
@@ -0,0 +1,113 @@
+from odoo import api, fields, models, tools, _
+from datetime import datetime, time, timedelta
+from odoo.exceptions import UserError
+from datetime import date, timedelta
+from dateutil.relativedelta import relativedelta
+
+
+class YuthonMealLine(models.Model):
+ _name = 'yuthon.meal.line'
+ _description = '误餐明细'
+
+ employee_id = fields.Many2one('hr.employee', string="员工")
+ meal_name = fields.Char(string='标题')
+ meal_id = fields.Many2one('vacation.meal.line', string="关联误餐额度记录")
+ entry_date = fields.Date(related='employee_id.entry_date', string="入职时间", store=True)
+ select_month = fields.Selection([('1', '一月'), ('2', '二月'), ('3', '三月'), ('4', '四月'),
+ ('5', '五月'), ('6', '六月'), ('7', '七月'), ('8', '八月'),
+ ('9', '九月'), ('10', '十月'), ('11', '十一月'),
+ ('12', '十二月')], string='月份',
+ default=lambda self: str(datetime.now().month))
+ select_year = fields.Selection(selection=[(str(i), str(i)) for i in range(2024, 2060)], string='年份',
+ default=lambda self: str(datetime.now().year))
+ workflow_record_id = fields.Many2one('soong.workflow.record', string='流程')
+ workflow_id = fields.Many2one(related="workflow_record_id.workflow_id", string="审批类型")
+ start_time = fields.Datetime(string="开始时间")
+ end_time = fields.Datetime(string="结束时间")
+ duration = fields.Float(string="时长")
+ update_lines = fields.Float(string='更新额度')
+ reasons = fields.Text(string="申请事由")
+ meal = fields.Selection([('yes', '是'), ('no', '否')])
+ is_filing = fields.Boolean(string="归档")
+
+ def confirm(self):
+ valid_leaves = self.env['yuthon.meal.line'].browse(
+ self._context.get('active_ids', self._context.get('active_id')))
+ emp_month_summary = {}
+ for rec in valid_leaves:
+ rec_year = rec.select_year
+ rec_month = rec.select_month
+ rec.meal_id.write({'is_filing': True})
+ emp_id = rec.employee_id.id
+ active_exists = self.env['vacation.meal.active'].search([
+ ('line_id', '=', rec.meal_id.id),
+ ('employee_id', '=', emp_id),
+ ('carry_forward_state', '=', 'completed')
+ ], limit=1)
+ if not active_exists:
+ meal_active_vals = {
+ 'line_id': rec.meal_id.id,
+ 'meal_name': rec.meal_name,
+ 'employee_id': emp_id,
+ 'workflow_id': rec.workflow_id.id if rec.workflow_id else False,
+ 'start_time': rec.start_time,
+ 'end_time': rec.end_time,
+ 'duration': rec.duration,
+ 'update_lines': rec.update_lines,
+ 'reasons': rec.reasons,
+ 'meal': rec.meal,
+ 'carry_forward_date': fields.Date.today(),
+ 'carry_forward_state': 'completed',
+ 'select_revoke': 'to_do'
+ }
+ self.env['vacation.meal.active'].create(meal_active_vals)
+
+ summary_key = (emp_id, rec_year, rec_month)
+ if summary_key not in emp_month_summary:
+ emp_month_summary[summary_key] = 0.0
+ emp_month_summary[summary_key] += rec.update_lines or 0.0
+
+ # 4. 第二步:遍历汇总结果,为每个【员工+年月】创建一条vacation.over.line
+ for (emp_id, rec_year, rec_month), total_amount in emp_month_summary.items():
+ if total_amount <= 1e-6:
+ continue
+ report_id = self.env['yuthon.vacation.reports'].search([
+ ('employee_id', '=', emp_id),
+ ('vacation_date', '=', fields.Date.from_string(f'{rec_year}-{rec_month}-01'))
+ ], limit=1)
+ if report_id:
+ future_reps = self.env['yuthon.vacation.reports'].search([
+ ('employee_id', '=', emp_id),
+ ('vacation_date', '>=', fields.Date.from_string(f'{rec_year}-{rec_month}-01'))
+ ], order='vacation_date asc')
+ for rep in future_reps:
+ rep.update_line()
+ over_exists = self.env['vacation.over.line'].search([
+ ('vacation_id', '=', report_id.id),
+ ('employee_id', '=', emp_id),
+ ('update_type', '=', 'reduce'),
+ ('text', '=', f'{rec_month}月份误餐结转'),
+ ], limit=1)
+ if not over_exists:
+ self.env['vacation.meal.line'].create([{
+ 'vacation_id': report_id.id,
+ 'employee_id': emp_id,
+ 'update_type': 'reduce',
+ 'update_lines': total_amount,
+ 'update_date': fields.Date.today(),
+ 'is_void': False,
+ 'text': f'{rec_month}月份误餐结转',
+ }])
+
+ def open_record(self):
+ if self.workflow_record_id:
+ return {
+ "name": "流程详情",
+ 'type': 'ir.actions.act_window',
+ 'res_model': f'{self.workflow_record_id.res_model}',
+ 'res_id': self.workflow_record_id.res_id,
+ 'view_mode': 'form',
+ }
+
+
+
diff --git a/yuthon_hr_attendance/models/yuthon_vacation_reports.py b/yuthon_hr_attendance/models/yuthon_vacation_reports.py
index 546c58f01..a002519d1 100644
--- a/yuthon_hr_attendance/models/yuthon_vacation_reports.py
+++ b/yuthon_hr_attendance/models/yuthon_vacation_reports.py
@@ -282,34 +282,6 @@ class YuthonVacationReports(models.Model):
'target': 'new',
}
- @api.model
- def meal_line(self):
- today = date.today()
- next_month = today.month if today.month < 12 else 1
- next_year = today.year if today.month < 12 else today.year + 1
- end_date = date(next_year, next_month, 1) - timedelta(days=1)
- active_records = self.env['vacation.meal.active'].search([('select_revoke', '=', 'to_do')])
- line_ids = [rec.line_id.id for rec in active_records if rec.line_id]
- return {
- 'type': 'ir.actions.act_window',
- 'name': '误餐情况',
- "views": [(False, "tree")],
- 'res_model': 'vacation.meal.line',
- 'domain': [
- ('start_time', '>=', datetime(2026, 1, 1)),
- ('meal', '=', 'yes'),
- ('vacation_id', '!=', False),
- ('workflow_id', '!=', False),
- ('id', 'not in', line_ids),
- ('update_type', '>=', 'add'),
- ('vacation_date', '<=', end_date),
- ('company_id', 'in', self.env.companies.ids),
- ('text', '!=', '上月额度继承')
- ],
- 'target': 'current',
- 'context': {'tree_view_ref': "yuthon_hr_attendance.view_vacation_meal_line_tree"}
- }
-
def update_vacation_task(self):
today = date.today()
current_month_first = today.replace(day=1)
@@ -2089,6 +2061,202 @@ class YuthonVacationReports(models.Model):
}
+ # 误餐上月
+ @api.model
+ def last_meal_month(self):
+ yuthon_meal_env = self.env['yuthon.meal.line']
+ last_record = yuthon_meal_env.search([], limit=1, order='id desc')
+
+ if last_record:
+ current_year = int(last_record.select_year)
+ current_month = int(last_record.select_month)
+ else:
+ now = datetime.now()
+ current_year = now.year
+ current_month = now.month
+
+ if current_month == 1:
+ last_year = current_year - 1
+ last_month = 12
+ else:
+ last_year = current_year
+ last_month = current_month - 1
+
+ end_date = date(last_year, last_month, 1) + relativedelta(months=1) - relativedelta(days=1)
+ domain = [
+ ('start_time', '>=', datetime(2026, 1, 1)),
+ ('meal', '=', 'yes'),
+ ('vacation_id', '!=', False),
+ ('workflow_id', '!=', False),
+ ('update_type', '>=', 'add'),
+ ('vacation_date', '<=', end_date),
+ ('company_id', 'in', self.env.companies.ids),
+ ('text', '!=', '上月额度继承')
+ ]
+
+ meal_lines = self.env['vacation.meal.line'].search(domain)
+ yuthon_meal_env.search([]).unlink()
+
+ # 批量创建
+ create_vals_list = []
+ for line in meal_lines:
+ create_vals = {
+ 'meal_name': line.meal_name,
+ 'meal_id': line.id,
+ 'employee_id': line.employee_id.id,
+ 'entry_date': line.employee_id.entry_date,
+ 'select_year': str(last_year),
+ 'select_month': str(last_month),
+ 'workflow_record_id': line.workflow_record_id.id,
+ 'start_time': line.start_time,
+ 'end_time': line.end_time,
+ 'duration': line.duration,
+ 'meal': line.meal,
+ 'is_filing':line.is_filing,
+ 'update_lines': line.update_lines,
+ 'reasons': line.reasons,
+ }
+ create_vals_list.append(create_vals)
+
+ if create_vals_list:
+ yuthon_meal_env.create(create_vals_list)
+
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': f'误餐情况',
+ "views": [(False, "tree")],
+ 'res_model': 'yuthon.meal.line',
+ 'domain': [],
+ 'target': 'current',
+ 'context': {
+ 'group_by': ['is_filing', 'employee_id'],
+ }
+ }
+
+ # 误餐本月
+ @api.model
+ def meal_line(self):
+ today = date.today()
+ end_date = date(today.year, today.month, 1) + relativedelta(months=1) - relativedelta(days=1)
+ domain = [
+ ('start_time', '>=', datetime(2026, 1, 1)),
+ ('meal', '=', 'yes'),
+ ('vacation_id', '!=', False),
+ ('workflow_id', '!=', False),
+ ('update_type', '>=', 'add'),
+ ('vacation_date', '<=', end_date),
+ ('company_id', 'in', self.env.companies.ids),
+ ('text', '!=', '上月额度继承')
+ ]
+ meal_lines = self.env['vacation.meal.line'].search(domain)
+ yuthon_meal_env = self.env['yuthon.meal.line']
+ yuthon_meal_env.search([]).unlink()
+ create_vals_list = []
+ for line in meal_lines:
+ create_vals = {
+ 'meal_id': line.id,
+ 'meal_name': line.meal_name,
+ 'employee_id': line.employee_id.id,
+ 'entry_date': line.employee_id.entry_date,
+ 'select_month': str(today.month),
+ 'select_year': str(today.year),
+ 'workflow_record_id': line.workflow_record_id.id,
+ 'start_time': line.start_time,
+ 'end_time': line.end_time,
+ 'duration': line.duration,
+ 'update_lines': line.update_lines,
+ 'is_filing': line.is_filing,
+ 'meal': line.meal,
+ 'reasons': line.reasons,
+ }
+ create_vals_list.append(create_vals)
+ if create_vals_list:
+ yuthon_meal_env.create(create_vals_list)
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': f'误餐情况',
+ "views": [(False, "tree")],
+ 'res_model': 'yuthon.meal.line',
+ 'domain': [],
+ 'target': 'current',
+ 'context': {
+ 'group_by': ['is_filing', 'employee_id'],
+ }
+ }
+
+ @api.model
+ def next_meal_month(self):
+ # 核心:从已生成数据取年月
+ yuthon_meal_env = self.env['yuthon.meal.line']
+ last_record = yuthon_meal_env.search([], limit=1, order='id desc')
+ if last_record:
+ current_year = int(last_record.select_year)
+ current_month = int(last_record.select_month)
+ else:
+ now = datetime.now()
+ current_year = now.year
+ current_month = now.month
+
+ # 计算:下月
+ if current_month == 12:
+ next_year = current_year + 1
+ next_month = 1
+ else:
+ next_year = current_year
+ next_month = current_month + 1
+
+ # 下月最后一天
+ end_date = date(next_year, next_month, 1) + relativedelta(months=1) - relativedelta(days=1)
+
+ active_records = self.env['vacation.meal.active'].search([('select_revoke', '=', 'to_do')])
+ domain = [
+ ('start_time', '>=', datetime(2026, 1, 1)),
+ ('meal', '=', 'yes'),
+ ('vacation_id', '!=', False),
+ ('workflow_id', '!=', False),
+ ('update_type', '>=', 'add'),
+ ('vacation_date', '<=', end_date),
+ ('company_id', 'in', self.env.companies.ids),
+ ('text', '!=', '上月额度继承')
+ ]
+ meal_lines = self.env['vacation.meal.line'].search(domain)
+ # 清空
+ yuthon_meal_env.search([]).unlink()
+ # 批量创建
+ create_vals_list = []
+ for line in meal_lines:
+ create_vals = {
+ 'meal_id': line.id,
+ 'meal_name': line.meal_name,
+ 'employee_id': line.employee_id.id,
+ 'entry_date': line.employee_id.entry_date,
+ 'select_year': str(next_year), # 用下一年月
+ 'select_month': str(next_month),
+ 'workflow_record_id': line.workflow_record_id.id,
+ 'start_time': line.start_time,
+ 'end_time': line.end_time,
+ 'duration': line.duration,
+ 'update_lines': line.update_lines,
+ 'is_filing': line.is_filing,
+ 'meal': line.meal,
+ 'reasons': line.reasons,
+ }
+ create_vals_list.append(create_vals)
+
+ if create_vals_list:
+ yuthon_meal_env.create(create_vals_list)
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': f'误餐情况',
+ "views": [(False, "tree")],
+ 'res_model': 'yuthon.meal.line',
+ 'domain': [],
+ 'target': 'current',
+ 'context': {
+ 'group_by': ['is_filing', 'employee_id'],
+ }
+ }
+
class VacationLeaveLine(models.Model):
_name = 'vacation.leave.line'
_description = '年假明细'
@@ -2521,6 +2689,12 @@ class VacationMealLine(models.Model):
('vacation_date', '=', fields.Date.from_string(f'{rec_year}-{rec_month}-01'))
], limit=1)
if report_id:
+ future_reps = self.env['yuthon.vacation.reports'].search([
+ ('employee_id', '=', emp_id),
+ ('vacation_date', '>=', fields.Date.from_string(f'{rec_year}-{rec_month}-01'))
+ ], order='vacation_date asc')
+ for rep in future_reps:
+ rep.update_line()
over_exists = self.env['vacation.over.line'].search([
('vacation_id', '=', report_id.id),
('employee_id', '=', emp_id),
diff --git a/yuthon_hr_attendance/security/ir.model.access.csv b/yuthon_hr_attendance/security/ir.model.access.csv
index 1673f675b..9606bbc90 100644
--- a/yuthon_hr_attendance/security/ir.model.access.csv
+++ b/yuthon_hr_attendance/security/ir.model.access.csv
@@ -98,4 +98,6 @@ access_yuthon_vacation_import_wizard,yuthon.vacation.import.wizard,model_yuthon_
access_overtime_change_wizard,overtime.change.wizard,model_overtime_change_wizard,base.group_user,1,1,1,1
access_duty_adjustment_schedule,duty.adjustment.schedule,model_duty_adjustment_schedule,base.group_user,1,1,1,1
access_overtime_carryover,overtime.carryover,model_overtime_carryover,base.group_user,1,1,1,1
-access_overtime_carryover_line,overtime.carryover.line,model_overtime_carryover_line,base.group_user,1,1,1,1
\ No newline at end of file
+access_overtime_carryover_line,overtime.carryover.line,model_overtime_carryover_line,base.group_user,1,1,1,1
+
+access_yuthon_meal_line,yuthon.meal.line,model_yuthon_meal_line,base.group_user,1,1,1,1
\ No newline at end of file
diff --git a/yuthon_hr_attendance/static/src/js/button_vacation_meal_confirm.js b/yuthon_hr_attendance/static/src/js/button_vacation_meal_confirm.js
new file mode 100644
index 000000000..940c884ee
--- /dev/null
+++ b/yuthon_hr_attendance/static/src/js/button_vacation_meal_confirm.js
@@ -0,0 +1,24 @@
+/** @odoo-module */
+import { ListController } from "@web/views/list/list_controller";
+import { registry } from '@web/core/registry';
+import { listView } from '@web/views/list/list_view';
+
+export class CustomAttendanceVacationMealConfirmController extends ListController {
+ setup() {
+ super.setup();
+ }
+ async getNextAction(lock_type) {
+ await this.orm.call('yuthon.vacation.reports', 'next_meal_month', []);
+ this.model.root.load()
+ }
+ async getLastAction(lock_type) {
+ await this.orm.call('yuthon.vacation.reports', 'last_meal_month', []);
+ this.model.root.load()
+ }
+}
+
+registry.category("views").add("custom_vacation_confirm_meal_button", {
+ ...listView,
+ Controller: CustomAttendanceVacationMealConfirmController,
+ buttonTemplate: "vacation_meal_confirm_module.ListView.Buttons",
+});
\ No newline at end of file
diff --git a/yuthon_hr_attendance/static/src/xml/button_vacation_meal_confirm.xml b/yuthon_hr_attendance/static/src/xml/button_vacation_meal_confirm.xml
new file mode 100644
index 000000000..973e79689
--- /dev/null
+++ b/yuthon_hr_attendance/static/src/xml/button_vacation_meal_confirm.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/yuthon_hr_attendance/views/yuthon_meal_line_views.xml b/yuthon_hr_attendance/views/yuthon_meal_line_views.xml
new file mode 100644
index 000000000..e43782163
--- /dev/null
+++ b/yuthon_hr_attendance/views/yuthon_meal_line_views.xml
@@ -0,0 +1,38 @@
+
+
+
+ yuthon.meal.line.tree
+ yuthon.meal.line
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ yuthon.meal.line.search
+ yuthon.meal.line
+
+
+
+
+
+
+
+
diff --git a/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml b/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml
index 5b11ff74c..a0fb05548 100644
--- a/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml
+++ b/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml
@@ -34,7 +34,6 @@