diff --git a/excel_report_designer/controllers/excel_report_designer.py b/excel_report_designer/controllers/excel_report_designer.py
index c212cea1e..5ec5c17ef 100644
--- a/excel_report_designer/controllers/excel_report_designer.py
+++ b/excel_report_designer/controllers/excel_report_designer.py
@@ -48,6 +48,12 @@ class XLSXReportController(http.Controller):
)
if report_name == '薪资':
report_obj.get_xlsx_salary_report(options, response)
+ elif report_name == '请假情况报表':
+ report_obj.get_xlsx_leave_report(options, response)
+ elif report_name == '加班明细报表':
+ report_obj.get_xlsx_overtime_report(options, response)
+ elif report_name == '加班详情报表':
+ report_obj.get_xlsx_overtime_detail_report(options, response)
else:
report_obj.get_xlsx_report(options, response)
response.set_cookie('fileToken', token)
diff --git a/excel_report_designer/models/report_excel.py b/excel_report_designer/models/report_excel.py
index a8b4bc434..141289fa1 100644
--- a/excel_report_designer/models/report_excel.py
+++ b/excel_report_designer/models/report_excel.py
@@ -781,3 +781,376 @@ class ReportExcel(models.Model):
('is_salary_release', '=', False),
('payslip_run_id.name', '=', target_name)
]
+
+ def get_xlsx_leave_report(self, data, response):
+ """请假情况报表的自定义 Excel 样式"""
+ output = io.BytesIO()
+ workbook = xlsxwriter.Workbook(output, {'in_memory': True})
+ sheet = workbook.add_worksheet('请假情况')
+
+ # 表头格式:加粗、居中、带边框、背景色
+ header_format = workbook.add_format({
+ 'font_size': 11,
+ 'bold': True,
+ 'border': 1,
+ 'align': 'center',
+ 'valign': 'vcenter',
+ 'bg_color': '#D3D3D3'
+ })
+ # 数据格式:有边框、居中
+ data_format = workbook.add_format({
+ 'font_size': 10,
+ 'border': 1,
+ 'align': 'center',
+ 'valign': 'vcenter'
+ })
+ # 日期时间格式
+ datetime_format = workbook.add_format({
+ 'font_size': 10,
+ 'border': 1,
+ 'align': 'center',
+ 'valign': 'vcenter',
+ 'num_format': 'yyyy-mm-dd hh:mm'
+ })
+ # 日期格式
+ date_format = workbook.add_format({
+ 'font_size': 10,
+ 'border': 1,
+ 'align': 'center',
+ 'valign': 'vcenter',
+ 'num_format': 'yyyy-mm-dd'
+ })
+
+ # 写表头(第一行)
+ col = 0
+ for label in data['field_label']:
+ sheet.write(0, col, label, header_format)
+ # 设置列宽
+ if col == 0: # 发起人
+ sheet.set_column(col, col, 12)
+ elif col == 1: # 部门
+ sheet.set_column(col, col, 15)
+ elif col == 3: # 标题
+ sheet.set_column(col, col, 30)
+ elif col == 7: # 申请原因
+ sheet.set_column(col, col, 40)
+ else:
+ sheet.set_column(col, col, 15)
+ col += 1
+
+ # 取记录:优先用 active_ids
+ records = []
+ active_ids = data.get('active_ids')
+ if active_ids:
+ records = self.env[data['model_name']].browse(active_ids)
+ else:
+ records = self.env[data['model_name']].search([])
+
+ # 写数据行
+ row = 1
+ for rec in records:
+ col = 0
+ for field_name in data['fields_name']:
+ field = rec._fields.get(field_name)
+ value = rec[field_name]
+
+ # 根据字段类型处理值
+ if isinstance(value, datetime.datetime):
+ # datetime 类型:需要从 UTC 转换到用户时区
+ if value:
+ from pytz import UTC, timezone
+ tz = self.env.user.tz or 'Asia/Shanghai'
+ user_tz = timezone(tz)
+ value_utc = UTC.localize(value) if value.tzinfo is None else value
+ value_local = value_utc.astimezone(user_tz)
+ # 移除时区信息后写入 Excel
+ sheet.write_datetime(row, col, value_local.replace(tzinfo=None), datetime_format)
+ else:
+ sheet.write(row, col, '', data_format)
+ elif isinstance(value, datetime.date):
+ # date 类型
+ sheet.write_datetime(row, col, value, date_format)
+ elif field:
+ if field.type == 'many2one':
+ value = value.display_name if value else ''
+ sheet.write(row, col, value, data_format)
+ elif field.type == 'many2many':
+ value = ', '.join(value.mapped('display_name')) if value else ''
+ sheet.write(row, col, value, data_format)
+ elif field.type == 'selection':
+ # 获取选择字段的选项
+ selection_dict = dict(field.selection)
+ value = selection_dict.get(value, value or '')
+ sheet.write(row, col, value, data_format)
+ elif field.type == 'float':
+ # 浮点数保留2位小数
+ sheet.write(row, col, value or 0, data_format)
+ else:
+ sheet.write(row, col, value or '', data_format)
+ else:
+ sheet.write(row, col, value or '', data_format)
+ col += 1
+ row += 1
+
+ # 给整表加筛选(类似 Excel 的筛选下拉箭头)
+ if row > 1: # 至少有一行数据
+ sheet.autofilter(0, 0, row - 1, len(data['field_label']) - 1)
+
+ # 冻结首行(表头)
+ sheet.freeze_panes(1, 0)
+
+ workbook.close()
+ output.seek(0)
+ response.stream.write(output.read())
+ output.close()
+
+ def get_xlsx_overtime_detail_report(self, data, response):
+ """加班详情报表的自定义 Excel 样式"""
+ output = io.BytesIO()
+ workbook = xlsxwriter.Workbook(output, {'in_memory': True})
+ sheet = workbook.add_worksheet('加班详情')
+
+ # 获取当前用户的时区(用于 datetime 转换)
+ tz = self.env.user.tz or 'Asia/Shanghai'
+ from pytz import timezone
+ user_tz = timezone(tz)
+
+ # 表头格式:加粗、居中、带边框、背景色
+ header_format = workbook.add_format({
+ 'font_size': 11,
+ 'bold': True,
+ 'border': 1,
+ 'align': 'center',
+ 'valign': 'vcenter',
+ 'bg_color': '#D3D3D3'
+ })
+ # 数据格式:有边框、居中
+ data_format = workbook.add_format({
+ 'font_size': 10,
+ 'border': 1,
+ 'align': 'center',
+ 'valign': 'vcenter'
+ })
+ # 日期时间格式
+ datetime_format = workbook.add_format({
+ 'font_size': 10,
+ 'border': 1,
+ 'align': 'center',
+ 'valign': 'vcenter',
+ 'num_format': 'yyyy-mm-dd hh:mm'
+ })
+ # 日期格式
+ date_format = workbook.add_format({
+ 'font_size': 10,
+ 'border': 1,
+ 'align': 'center',
+ 'valign': 'vcenter',
+ 'num_format': 'yyyy-mm-dd'
+ })
+
+ # 写表头(第一行)
+ col = 0
+ for label in data['field_label']:
+ sheet.write(0, col, label, header_format)
+ # 设置列宽
+ if col == 0: # 员工
+ sheet.set_column(col, col, 15)
+ elif col == 1: # 部门
+ sheet.set_column(col, col, 18)
+ elif col == 2: # 审批类型
+ sheet.set_column(col, col, 18)
+ elif col in [3, 4]: # 开始/结束时间
+ sheet.set_column(col, col, 20)
+ elif col == 6: # 事由
+ sheet.set_column(col, col, 40)
+ else:
+ sheet.set_column(col, col, 15)
+ col += 1
+
+ # 取记录:优先用 active_ids
+ records = []
+ active_ids = data.get('active_ids')
+ if active_ids:
+ records = self.env[data['model_name']].browse(active_ids)
+ else:
+ records = self.env[data['model_name']].search([])
+
+ # 写数据行
+ row = 1
+ for rec in records:
+ col = 0
+ for field_name in data['fields_name']:
+ field = rec._fields.get(field_name)
+ value = rec[field_name]
+
+ # 根据字段类型处理值
+ if isinstance(value, datetime.datetime):
+ # datetime 类型:需要从 UTC 转换到用户时区
+ if value:
+ from pytz import UTC
+ value_utc = UTC.localize(value) if value.tzinfo is None else value
+ value_local = value_utc.astimezone(user_tz)
+ sheet.write_datetime(row, col, value_local.replace(tzinfo=None), datetime_format)
+ else:
+ sheet.write(row, col, '', data_format)
+ elif isinstance(value, datetime.date):
+ # date 类型
+ sheet.write_datetime(row, col, value, date_format)
+ elif field:
+ if field.type == 'many2one':
+ value = value.display_name if value else ''
+ sheet.write(row, col, value, data_format)
+ elif field.type == 'many2many':
+ value = ', '.join(value.mapped('display_name')) if value else ''
+ sheet.write(row, col, value, data_format)
+ elif field.type == 'selection':
+ # 获取选择字段的选项
+ selection_dict = dict(field.selection)
+ value = selection_dict.get(value, value or '')
+ sheet.write(row, col, value, data_format)
+ elif field.type == 'float':
+ # 浮点数保留2位小数
+ sheet.write(row, col, value or 0, data_format)
+ elif field.type == 'text':
+ # Text 字段(如事由)
+ sheet.write(row, col, value or '', data_format)
+ else:
+ sheet.write(row, col, value or '', data_format)
+ else:
+ sheet.write(row, col, value or '', data_format)
+ col += 1
+ row += 1
+
+ # 给整表加筛选
+ if row > 1:
+ sheet.autofilter(0, 0, row - 1, len(data['field_label']) - 1)
+
+ # 冻结首行
+ sheet.freeze_panes(1, 0)
+
+ workbook.close()
+ output.seek(0)
+ response.stream.write(output.read())
+ output.close()
+ def get_xlsx_overtime_report(self, data, response):
+ """加班明细报表的自定义 Excel 样式"""
+ output = io.BytesIO()
+ workbook = xlsxwriter.Workbook(output, {'in_memory': True})
+ sheet = workbook.add_worksheet('加班明细')
+
+ # 获取当前用户的时区(用于 datetime 转换)
+ tz = self.env.user.tz or 'Asia/Shanghai'
+ from pytz import timezone
+ user_tz = timezone(tz)
+
+ # 表头格式:加粗、居中、带边框、背景色
+ header_format = workbook.add_format({
+ 'font_size': 11,
+ 'bold': True,
+ 'border': 1,
+ 'align': 'center',
+ 'valign': 'vcenter',
+ 'bg_color': '#D3D3D3'
+ })
+ # 数据格式:有边框、居中
+ data_format = workbook.add_format({
+ 'font_size': 10,
+ 'border': 1,
+ 'align': 'center',
+ 'valign': 'vcenter'
+ })
+ # 日期时间格式
+ datetime_format = workbook.add_format({
+ 'font_size': 10,
+ 'border': 1,
+ 'align': 'center',
+ 'valign': 'vcenter',
+ 'num_format': 'yyyy-mm-dd hh:mm'
+ })
+ # 日期格式
+ date_format = workbook.add_format({
+ 'font_size': 10,
+ 'border': 1,
+ 'align': 'center',
+ 'valign': 'vcenter',
+ 'num_format': 'yyyy-mm-dd'
+ })
+
+ # 写表头(第一行)
+ col = 0
+ for label in data['field_label']:
+ sheet.write(0, col, label, header_format)
+ # 设置列宽
+ if col == 0: # 员工
+ sheet.set_column(col, col, 15)
+ elif col == 1: # 部门
+ sheet.set_column(col, col, 18)
+ elif col == 2: # 加班时间
+ sheet.set_column(col, col, 20)
+ else:
+ sheet.set_column(col, col, 15)
+ col += 1
+
+ # 取记录:优先用 active_ids
+ records = []
+ active_ids = data.get('active_ids')
+ if active_ids:
+ records = self.env[data['model_name']].browse(active_ids)
+ else:
+ records = self.env[data['model_name']].search([])
+
+ # 写数据行
+ row = 1
+ for rec in records:
+ col = 0
+ for field_name in data['fields_name']:
+ field = rec._fields.get(field_name)
+ value = rec[field_name]
+
+ # 根据字段类型处理值
+ if isinstance(value, datetime.datetime):
+ # datetime 类型:需要从 UTC 转换到用户时区
+ if value:
+ from pytz import UTC
+ value_utc = UTC.localize(value) if value.tzinfo is None else value
+ value_local = value_utc.astimezone(user_tz)
+ # 移除时区信息后写入 Excel
+ sheet.write_datetime(row, col, value_local.replace(tzinfo=None), datetime_format)
+ else:
+ sheet.write(row, col, '', data_format)
+ elif isinstance(value, datetime.date):
+ # date 类型
+ sheet.write_datetime(row, col, value, date_format)
+ elif field:
+ if field.type == 'many2one':
+ value = value.display_name if value else ''
+ sheet.write(row, col, value, data_format)
+ elif field.type == 'many2many':
+ value = ', '.join(value.mapped('display_name')) if value else ''
+ sheet.write(row, col, value, data_format)
+ elif field.type == 'selection':
+ # 获取选择字段的选项
+ selection_dict = dict(field.selection)
+ value = selection_dict.get(value, value or '')
+ sheet.write(row, col, value, data_format)
+ elif field.type == 'float':
+ # 浮点数保留2位小数
+ sheet.write(row, col, value or 0, data_format)
+ else:
+ sheet.write(row, col, value or '', data_format)
+ else:
+ sheet.write(row, col, value or '', data_format)
+ col += 1
+ row += 1
+
+ # 给整表加筛选(类似 Excel 的筛选下拉箭头)
+ if row > 1: # 至少有一行数据
+ sheet.autofilter(0, 0, row - 1, len(data['field_label']) - 1)
+
+ # 冻结首行(表头)
+ sheet.freeze_panes(1, 0)
+
+ workbook.close()
+ output.seek(0)
+ response.stream.write(output.read())
+ output.close()
diff --git a/yuthon_hr_attendance/__manifest__.py b/yuthon_hr_attendance/__manifest__.py
index 2162b408c..a8510a86b 100644
--- a/yuthon_hr_attendance/__manifest__.py
+++ b/yuthon_hr_attendance/__manifest__.py
@@ -106,6 +106,8 @@
'yuthon_hr_attendance/static/src/xml/planning_gantt_renderer.xml',
'yuthon_hr_attendance/static/src/js/button_statement_confirm.js',
'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_put.js',
'yuthon_hr_attendance/static/src/xml/button_vacation_put.xml',
'yuthon_hr_attendance/static/src/planning_gantt/planning_gantt_controller.xml',
diff --git a/yuthon_hr_attendance/models/yuthon_holiday_statement.py b/yuthon_hr_attendance/models/yuthon_holiday_statement.py
index 6711670e1..a3b58d662 100644
--- a/yuthon_hr_attendance/models/yuthon_holiday_statement.py
+++ b/yuthon_hr_attendance/models/yuthon_holiday_statement.py
@@ -2,6 +2,8 @@ from odoo import api, fields, models, tools, _
from datetime import datetime, time, timedelta
from odoo.exceptions import UserError
from datetime import date, timedelta
+import json
+from odoo.tools import date_utils
class YuthonOvertimeReport(models.Model):
_name = 'yuthon.holiday.statement'
@@ -70,4 +72,55 @@ class InheritHrLeave(models.Model):
])
total_days = sum(leave.length_leave for leave in leave_records)
leave_type_name = record.holiday_status_id.name
- record.leave_stats = f"本年内已请{leave_type_name}{total_days:.1f}天"
\ No newline at end of file
+ record.leave_stats = f"本年内已请{leave_type_name}{total_days:.1f}天"
+
+ def action_export_leave_excel(self):
+ """导出请假情况 Excel 报表"""
+ # 获取当前选中的记录 ID(来自前端 JS 传的参数)
+ active_ids = self.ids if self else []
+ if not active_ids:
+ # 如果没传 ID,就导出当前筛选条件下的全部记录
+ domain = self.env.context.get('search_default_domain') or []
+ active_ids = self.search(domain).ids
+
+ fields_name = [
+ 'employee_id',
+ 'department_id',
+ 'holiday_status_id',
+ 'caption',
+ 'date_from',
+ 'date_to',
+ 'length_leave',
+ 'application_reason',
+ 'mobile_phone',
+ ]
+ field_label = [
+ '发起人',
+ '部门',
+ '休假类型',
+ '标题',
+ '开始时间',
+ '结束时间',
+ '请假时长',
+ '申请原因',
+ '手机号',
+ ]
+
+ data = {
+ 'report_name': '请假情况报表',
+ 'model_name': 'hr.leave',
+ 'fields_name': fields_name,
+ 'field_label': field_label,
+ 'active_ids': active_ids,
+ }
+
+ return {
+ 'type': 'ir.actions.report',
+ 'data': {
+ 'model': 'report.excel',
+ 'options': json.dumps(data, default=date_utils.json_default),
+ 'output_format': 'xlsx',
+ 'report_name': '请假情况报表',
+ },
+ 'report_type': 'xlsx',
+ }
\ No newline at end of file
diff --git a/yuthon_hr_attendance/models/yuthon_vacation_reports.py b/yuthon_hr_attendance/models/yuthon_vacation_reports.py
index 2c8e95902..f34c45dba 100644
--- a/yuthon_hr_attendance/models/yuthon_vacation_reports.py
+++ b/yuthon_hr_attendance/models/yuthon_vacation_reports.py
@@ -5,6 +5,8 @@ from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
import calendar
from datetime import date
+import json
+from odoo.tools import date_utils
from dateutil.relativedelta import relativedelta
from odoo.exceptions import UserError, ValidationError
@@ -1823,6 +1825,99 @@ class VacationOverline(models.Model):
# self._sync_to_employee(reverse=True)
# return super(VacationOverline, self).unlink()
+ def action_export_overtime_excel(self):
+ """导出加班明细 Excel 报表"""
+ # 获取当前选中的记录 ID(来自前端 JS 传的参数)
+ active_ids = self.ids if self else []
+ if not active_ids:
+ # 如果没传 ID,就导出当前筛选条件下的全部记录
+ domain = self.env.context.get('search_default_domain') or []
+ active_ids = self.search(domain).ids
+
+ fields_name = [
+ 'employee_id',
+ 'department_id',
+ 'start_time',
+ 'carry_forward_date',
+ 'duration',
+ 'carry_result',
+ ]
+ field_label = [
+ '员工',
+ '部门',
+ '加班时间',
+ '结转日期',
+ '结转时长',
+ '结转结果',
+ ]
+
+ data = {
+ 'report_name': '加班明细报表',
+ 'model_name': 'vacation.over.line',
+ 'fields_name': fields_name,
+ 'field_label': field_label,
+ 'active_ids': active_ids,
+ }
+
+ return {
+ 'type': 'ir.actions.report',
+ 'data': {
+ 'model': 'report.excel',
+ 'options': json.dumps(data, default=date_utils.json_default),
+ 'output_format': 'xlsx',
+ 'report_name': '加班明细报表',
+ },
+ 'report_type': 'xlsx',
+ }
+
+ def action_export_overtime_detail_excel(self):
+ """导出加班详情 Excel 报表(包含审批类型、开始结束时间、事由、餐费等)"""
+ # 获取当前选中的记录 ID
+ active_ids = self.ids if self else []
+ if not active_ids:
+ domain = self.env.context.get('search_default_domain') or []
+ active_ids = self.search(domain).ids
+
+ fields_name = [
+ 'employee_id',
+ 'department_id',
+ 'workflow_id',
+ 'start_time',
+ 'end_time',
+ 'duration',
+ 'reason_application',
+ 'meal',
+ ]
+ field_label = [
+ '员工',
+ '部门',
+ '审批类型',
+ '加班开始时间',
+ '加班结束时间',
+ '加班时长',
+ '事由',
+ '加班餐费',
+ ]
+
+ data = {
+ 'report_name': '加班详情报表',
+ 'model_name': 'vacation.over.line',
+ 'fields_name': fields_name,
+ 'field_label': field_label,
+ 'active_ids': active_ids,
+ }
+
+ return {
+ 'type': 'ir.actions.report',
+ 'data': {
+ 'model': 'report.excel',
+ 'options': json.dumps(data, default=date_utils.json_default),
+ 'output_format': 'xlsx',
+ 'report_name': '加班详情报表',
+ },
+ 'report_type': 'xlsx',
+ }
+
class VacationMealLine(models.Model):
_name = 'vacation.meal.line'
_description = '误餐明细'
diff --git a/yuthon_hr_attendance/static/src/js/button_statement_confirm.js b/yuthon_hr_attendance/static/src/js/button_statement_confirm.js
index 4c08e982e..09e8149a8 100644
--- a/yuthon_hr_attendance/static/src/js/button_statement_confirm.js
+++ b/yuthon_hr_attendance/static/src/js/button_statement_confirm.js
@@ -12,6 +12,21 @@ export class CustomAttendanceStatementConfirmController extends ListController {
async getAction(lock_type) {
await this.orm.call('hr.leave', 'confirm', []);
}
+
+ async exportLeaveExcel() {
+ // 获取当前选中的记录 ID(如果有选中)
+ const selectedIds = await this.getSelectedResIds();
+ // 如果没选中,传空数组(后端会按当前 domain 导出全部)
+ const action = await this.orm.call(
+ 'hr.leave',
+ 'action_export_leave_excel',
+ [selectedIds],
+ {
+ context: this.props.context,
+ }
+ );
+ return this.actionService.doAction(action);
+ }
}
registry.category("views").add("custom_statement_confirm_button", {
diff --git a/yuthon_hr_attendance/static/src/xml/button_statement_confirm.xml b/yuthon_hr_attendance/static/src/xml/button_statement_confirm.xml
index 1c1ce6977..e89ac7486 100644
--- a/yuthon_hr_attendance/static/src/xml/button_statement_confirm.xml
+++ b/yuthon_hr_attendance/static/src/xml/button_statement_confirm.xml
@@ -5,6 +5,9 @@
+
\ 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 ba755105d..2367d0dbe 100644
--- a/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml
+++ b/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml
@@ -631,7 +631,7 @@
vacation.over.line.overtime.tree
vacation.over.line
-
+