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 + + +
+
+ + + + + + + + + + + + + +