diff --git a/yuthon_hr_attendance/__manifest__.py b/yuthon_hr_attendance/__manifest__.py
index 10eeab9cb..0bd3dd7ec 100644
--- a/yuthon_hr_attendance/__manifest__.py
+++ b/yuthon_hr_attendance/__manifest__.py
@@ -30,6 +30,7 @@
'views/inherit_planning_role_views.xml',
'views/inherit_planning_slot_views.xml',
'views/attendance_message_log.xml',
+ 'views/duty_adjustment_schedule_views.xml',
'views/inherit_planning_slot_template_views.xml',
'views/yuthon_customer_approval_views.xml',
'views/yuthon_shift_planning_views.xml',
diff --git a/yuthon_hr_attendance/models/__init__.py b/yuthon_hr_attendance/models/__init__.py
index 98a88c0cb..444e9c856 100644
--- a/yuthon_hr_attendance/models/__init__.py
+++ b/yuthon_hr_attendance/models/__init__.py
@@ -39,6 +39,7 @@ from . import vacation_overtime_active
from . import yuthon_overtime_line
from . import overtime_active_line
from . import vacation_meal_active
+from . import duty_adjustment_schedule
# from odoo.tools import SQL
# import odoo
# odoo.models.READ_GROUP_AGGREGATE.update({
diff --git a/yuthon_hr_attendance/models/duty_adjustment_schedule.py b/yuthon_hr_attendance/models/duty_adjustment_schedule.py
new file mode 100644
index 000000000..a27f015fd
--- /dev/null
+++ b/yuthon_hr_attendance/models/duty_adjustment_schedule.py
@@ -0,0 +1,133 @@
+# -*- coding: utf-8 -*-
+from odoo import api, fields, models, tools, _
+from datetime import datetime, time, timedelta
+from odoo.exceptions import UserError
+from datetime import date, timedelta
+
+
+class DutyAdjustmentSchedule(models.Model):
+ _name = 'duty.adjustment.schedule'
+ _description = '值班调休表'
+
+ vacation_id = fields.Many2one('yuthon.vacation.reports', string="关联假期额度日志")
+ employee_id = fields.Many2one('hr.employee', string="员工")
+ is_adjustment = fields.Boolean(string="已调休")
+ hide_selection = fields.Selection([('legal_date', '法定日'), ('adjust_holidays', '调班日'), ('workday', '工作日')], string="", compute="_compute_is_hide")
+ is_hide = fields.Boolean(string="隐藏", compute='_compute_is_hide')
+ company_id = fields.Many2one(related='employee_id.company_id', string="公司")
+ duty_date = fields.Date(string="值班日期")
+ week_day = fields.Char(string='星期', compute='_compute_week_day', store=True)
+
+ @api.depends('duty_date')
+ def _compute_is_hide(self):
+ """
+ 最终规则(周末/调休日不隐藏):
+ - 法定日 → 隐藏(is_hide=True),原因=法定日
+ - 调班日 → 隐藏(is_hide=True),原因=调班日
+ - 平常工作日 → 隐藏(is_hide=True),原因=工作日
+ - 调休日/周末 → 不隐藏(is_hide=False),无原因
+ """
+ for record in self:
+ record.is_hide = False
+ record.hide_selection = False
+ if not record.duty_date:
+ continue
+ date_type = record._process_shift_days(record.duty_date)
+ if date_type == 'legal_date':
+ record.is_hide = True
+ record.hide_selection = 'legal_date'
+ elif date_type == 'adjust_holidays':
+ record.is_hide = True
+ record.hide_selection = 'adjust_holidays'
+ elif date_type == 'workday':
+ record.is_hide = True
+ record.hide_selection = 'workday'
+
+ @api.depends('duty_date')
+ def _compute_week_day(self):
+ week_map = {'0': '周一', '1': '周二', '2': '周三', '3': '周四', '4': '周五', '5': '周六', '6': '周日'}
+ for record in self:
+ if record.duty_date:
+ record.week_day = week_map[str(record.duty_date.weekday())]
+ else:
+ record.week_day = ''
+
+ def compensation(self):
+ for record in self:
+ if record.vacation_id.vacation_state == 'confirmed':
+ raise UserError('该假期额度已确认,无法进行调休操作')
+ record.is_adjustment = True
+ over_record = self.env['vacation.over.line'].search([('employee_id', '=', record.employee_id.id), ('update_date', '=', record.duty_date),
+ ('vacation_id', '=', record.vacation_id.id)], limit=1)
+ if not over_record:
+ self.env['vacation.over.line'].create({
+ 'employee_id': record.employee_id.id,
+ 'update_date': record.duty_date,
+ 'vacation_id': record.vacation_id.id,
+ 'valid_date': date(record.duty_date.year + 1, 6, 30),
+ 'workday': 8,
+ 'update_type': 'add',
+ 'text': '值班调休'
+ })
+ record.vacation_id.update_line()
+
+ def revoke(self):
+ for record in self:
+ record.is_adjustment = False
+ over_record = self.env['vacation.over.line'].search([('employee_id', '=', record.employee_id.id), ('update_date', '=', record.duty_date),
+ ('vacation_id', '=', record.vacation_id.id), ('text', '=', '值班调休')], limit=1)
+ if over_record:
+ over_record.unlink()
+ record.vacation_id.update_line()
+
+ def _process_shift_days(self, current_data):
+ """
+ 判断指定日期的类型:
+ - 法定日(holidays)→ 'legal_date'
+ - 调班日(shift_replacement_day)→ 'adjust_holidays'
+ - 平常工作日(1-5)→ 'workday'
+ - 调休日(alternate_rest_day)/周末 → None
+ """
+ if not current_data:
+ return None
+ current_year = current_data.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)
+ ])
+ holidays_list = [] # 法定日
+ shift_days_list = [] # 调班日(对应adjust_holidays)
+ adjust_days_list = [] # 调休日(不隐藏,无原因)
+
+ for leave in calendar_leaves:
+ if leave.holidays:
+ holidays_list.extend(leave.holidays.split(','))
+ if leave.shift_replacement_day: # 调班日
+ shift_days_list.extend(leave.shift_replacement_day.split(','))
+ if leave.alternate_rest_day: # 调休日(仅判断,不赋值原因)
+ adjust_days_list.extend(leave.alternate_rest_day.split(','))
+
+ holidays_list = [self.remove_leading_zero(d) for d in holidays_list if d]
+ shift_days_list = [self.remove_leading_zero(d) for d in shift_days_list if d]
+ current_date_str = f"{current_data.month}月{current_data.day}日"
+
+ if current_date_str in holidays_list:
+ return 'legal_date' # 法定日
+ elif current_date_str in shift_days_list:
+ return 'adjust_holidays' # 调班日
+ elif current_data.weekday() in [0, 1, 2, 3, 4]:
+ return 'workday' # 平常工作日(周一到周五)
+ else:
+ return None # 调休日/周末(不隐藏,无原因)
+
+ # 移除日期字符串中的前导零
+ 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}日'
+
diff --git a/yuthon_hr_attendance/models/planning_template_inherit.py b/yuthon_hr_attendance/models/planning_template_inherit.py
index 15628328c..34f412956 100644
--- a/yuthon_hr_attendance/models/planning_template_inherit.py
+++ b/yuthon_hr_attendance/models/planning_template_inherit.py
@@ -88,19 +88,6 @@ class PlanningSlotTemplate(models.Model):
}
return action
- # @api.depends("planning_name")
- # def _compute_display_name(self):
- # super()._compute_display_name()
- # for record in self:
- # if record.planning_name and record.role_id:
- # record.display_name = f"{record.role_id.name}-{record.planning_name}"
- # elif record.planning_name:
- # record.display_name = record.planning_name
- # elif record.role_id:
- # record.display_name = record.role_id.name
- # else:
- # record.display_name = "Unknown"
-
@api.depends('role_id','planning_name')
def _compute_display_name(self):
for shift_template in self:
diff --git a/yuthon_hr_attendance/models/yuthon_vacation_reports.py b/yuthon_hr_attendance/models/yuthon_vacation_reports.py
index 38ca0f8b0..ee1378ae4 100644
--- a/yuthon_hr_attendance/models/yuthon_vacation_reports.py
+++ b/yuthon_hr_attendance/models/yuthon_vacation_reports.py
@@ -57,6 +57,7 @@ class YuthonVacationReports(models.Model):
medical_fee_ids = fields.One2many('vacation.medical.line', 'vacation_id', string='体检费明细')
parenting_line_ids = fields.One2many('vacation.parenting.line', 'vacation_id', string='育儿假明细')
other_fee_ids = fields.One2many('vacation.other.line', 'vacation_id', string='其他明细')
+ duty_ids = fields.One2many('duty.adjustment.schedule', 'vacation_id', string='值班调休明细', compute='_compute_duty_ids')
vacation_date = fields.Date(string='日期', default=lambda self: fields.Date.today().replace(day=1))
show_date = fields.Date(string='日期', compute='_compute_display_date', store=True)
display_date = fields.Char(string='日期', compute='_compute_display_date')
@@ -68,13 +69,14 @@ class YuthonVacationReports(models.Model):
digits=(16, 3), group_operator=None)
remain_missed_meals = fields.Float(string="剩余误餐次数", compute='_compute_meal_fee', store=True, group_operator=None)
medical_fee = fields.Float(string="体检费", compute='_compute_medical_fee', store=True, group_operator=None)
+ duty = fields.Float(string="值班", compute='_compute_duty_fee', store=True, group_operator=None)
+ duty_off_leave = fields.Float(string='值班调休', compute='_compute_duty_fee', store=True, group_operator=None)
compensatory_leave = fields.Float(string="调休d", compute='_compute_other_leave', store=True, group_operator=None)
leave_of_absence = fields.Float(string="事假d", digits=(16, 3), compute='_compute_other_leave', store=True,
group_operator=None)
statutory_leave = fields.Float(string="加班时(法定)d", compute='_compute_other_leave', store=True, group_operator=None, digits=(16, 3))
cop_leave = fields.Float(string='补休d', digits=(16, 3), store=True, compute='_compute_working_overtime',
group_operator=None)
- duty_off_leave = fields.Float(string='值班调休d', compute='_compute_other_leave', store=True, group_operator=None)
marriage_leave = fields.Float(string='婚假d', compute='_compute_other_leave', store=True, group_operator=None)
maternity_leave = fields.Float(string='产假d', compute='_compute_other_leave', store=True, group_operator=None)
paternity_leave = fields.Float(string='陪产假d', compute='_compute_other_leave', store=True, group_operator=None)
@@ -117,6 +119,77 @@ class YuthonVacationReports(models.Model):
tracking=3,
default='not_sent')
+ @api.depends('employee_id', 'vacation_date')
+ def _compute_duty_ids(self):
+ duty_schedule_env = self.env['duty.adjustment.schedule']
+ planning_slot_env = self.env['planning.slot']
+
+ for record in self:
+ if not record.employee_id or not record.vacation_date:
+ record.duty_ids = False
+ continue
+
+ emp_id = record.employee_id.id
+ year = record.vacation_date.year
+ month = record.vacation_date.month
+ month_start = date(year, month, 1)
+ last_day = calendar.monthrange(year, month)[1]
+ month_end = date(year, month, last_day)
+
+ all_duty_records = duty_schedule_env.search([
+ ('vacation_id', '=', record.id),
+ ('duty_date', '>=', month_start),
+ ('duty_date', '<=', month_end)
+ ])
+ # 过滤出:非已调休的记录 → 批量删除
+ non_adjusted_duties = all_duty_records.filtered(lambda d: not d.is_adjustment)
+ if non_adjusted_duties:
+ non_adjusted_duties.unlink()
+
+ white_duty_slots = planning_slot_env.search([
+ ('employee_id', '=', emp_id),
+ ('planning_name', '=', '白值'),
+ ('start_datetime', '>=', datetime.combine(month_start, datetime.min.time())),
+ ('start_datetime', '<=', datetime.combine(month_end, datetime.max.time()))
+ ])
+ existing_duty_dates = {
+ d.duty_date for d in duty_schedule_env.search([
+ ('vacation_id', '=', record.id),
+ ('duty_date', '>=', month_start),
+ ('duty_date', '<=', month_end)
+ ])
+ }
+ create_vals = []
+ for slot in white_duty_slots:
+ if not slot.start_datetime:
+ continue
+ duty_date = slot.start_datetime.date()
+ if duty_date in existing_duty_dates:
+ continue
+ create_vals.append({
+ 'employee_id': emp_id,
+ 'vacation_id': record.id,
+ 'duty_date': duty_date,
+ 'is_adjustment': False
+ })
+ # 批量创建(效率更高)
+ if create_vals:
+ duty_schedule_env.create(create_vals)
+ record.duty_ids = duty_schedule_env.search([
+ ('vacation_id', '=', record.id),
+ ('duty_date', '>=', month_start),
+ ('duty_date', '<=', month_end)
+ ])
+
+
+ @api.depends('duty_ids.is_adjustment')
+ def _compute_duty_fee(self):
+ for record in self:
+ total_duty_count = len(record.duty_ids)
+ duty_off_count = len([line for line in record.duty_ids if line.is_adjustment])
+ record.duty_off_leave = duty_off_count
+ record.duty = max(total_duty_count - duty_off_count, 0)
+
@api.depends('vacation_date', 'employee_id')
def _compute_number_card(self):
for record in self:
@@ -181,7 +254,6 @@ class YuthonVacationReports(models.Model):
'context': {'tree_view_ref': "yuthon_hr_attendance.view_hr_leave_statement_tree"},
}
-
def import_initial_data(self):
return {
'type': 'ir.actions.act_window',
@@ -312,7 +384,7 @@ class YuthonVacationReports(models.Model):
rec.used_this_year1 += deduct_from_current1 # 累加到本年已休
remaining_overtime_deduct -= deduct_from_current1
- add_overtime_lines = rec.overtime_ids.filtered(lambda o: o.workflow_record_id and o.update_type == 'add' and o.is_void != True)
+ add_overtime_lines = rec.overtime_ids.filtered(lambda o: o.text != '上月额度继承' and o.update_type == 'add' and o.is_void != True)
# 计算补休新增总天数
workday_overtime_add = sum((line.workday or 0) for line in add_overtime_lines) / 8
holidays_overtime_add = sum((line.public_holidays or 0) for line in add_overtime_lines) / 8
@@ -1134,7 +1206,6 @@ class YuthonVacationReports(models.Model):
'调休': 'compensatory_leave',
'事假': 'leave_of_absence',
'法定假': 'statutory_leave',
- '值班调休': 'duty_off_leave',
'婚假': 'marriage_leave',
'产假': 'maternity_leave',
'陪产假': 'paternity_leave',
@@ -1158,7 +1229,7 @@ class YuthonVacationReports(models.Model):
current_value = getattr(rec, field_name) or 0
setattr(rec, field_name, current_value + line.lines)
int_fields = [
- 'compensatory_leave', 'statutory_leave', 'duty_off_leave',
+ 'compensatory_leave', 'statutory_leave',
'marriage_leave', 'maternity_leave', 'paternity_leave',
'family_planning_leave', 'home_visit_leave',
'parents_meeting_leave', 'exam_leave'
diff --git a/yuthon_hr_attendance/security/ir.model.access.csv b/yuthon_hr_attendance/security/ir.model.access.csv
index 0b478949e..b553f270a 100644
--- a/yuthon_hr_attendance/security/ir.model.access.csv
+++ b/yuthon_hr_attendance/security/ir.model.access.csv
@@ -95,4 +95,5 @@ access_overtime_active_line,overtime.active.line,model_overtime_active_line,base
access_export_date_wizard,export.date.wizard,model_export_date_wizard,base.group_user,1,1,1,1
access_vacation_meal_active,vacation.meal.active,model_vacation_meal_active,base.group_user,1,1,1,1
access_yuthon_vacation_import_wizard,yuthon.vacation.import.wizard,model_yuthon_vacation_import_wizard,base.group_user,1,1,1,1
-access_overtime_change_wizard,overtime.change.wizard,model_overtime_change_wizard,base.group_user,1,1,1,1
\ No newline at end of file
+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
\ No newline at end of file
diff --git a/yuthon_hr_attendance/views/duty_adjustment_schedule_views.xml b/yuthon_hr_attendance/views/duty_adjustment_schedule_views.xml
new file mode 100644
index 000000000..455be456a
--- /dev/null
+++ b/yuthon_hr_attendance/views/duty_adjustment_schedule_views.xml
@@ -0,0 +1,20 @@
+
+
+ duty.adjustment.schedule.tree
+ duty.adjustment.schedule
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/yuthon_hr_attendance/views/hr_attendance_views.xml b/yuthon_hr_attendance/views/hr_attendance_views.xml
index 9697df7c7..a8c62fb52 100644
--- a/yuthon_hr_attendance/views/hr_attendance_views.xml
+++ b/yuthon_hr_attendance/views/hr_attendance_views.xml
@@ -225,13 +225,4 @@
-
- 批量更新班次名称
-
-
- code
- action = model.update_abnormal()
-
-
\ No newline at end of file
diff --git a/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml b/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml
index 957738a2a..4a5228a70 100644
--- a/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml
+++ b/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml
@@ -65,6 +65,8 @@
+
+
@@ -81,7 +83,6 @@
options='{ "fg_color": "white: statutory_leave == False"}'/>
-
@@ -129,8 +130,12 @@
+
+
+
+
@@ -148,7 +153,6 @@
options='{ "fg_color": "white: statutory_leave == False"}'/>
-
@@ -371,6 +375,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+