From 82e708418ff3ae84dec2a95a19e5160f7db8c86d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Sun, 8 Feb 2026 18:46:08 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A0=E7=8F=AD=E3=80=81=E8=AF=AF=E9=A4=90?= =?UTF-8?q?=E3=80=81=E8=AF=B7=E5=81=87excel=E4=B8=8D=E5=BD=92=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/excel_report_designer.py | 6 + excel_report_designer/models/report_excel.py | 373 ++++++++++++++++++ yuthon_hr_attendance/__manifest__.py | 2 + .../models/yuthon_holiday_statement.py | 55 ++- .../models/yuthon_vacation_reports.py | 95 +++++ .../static/src/js/button_statement_confirm.js | 15 + .../src/xml/button_statement_confirm.xml | 3 + .../views/yuthon_vacation_reports_views.xml | 2 +- 8 files changed, 549 insertions(+), 2 deletions(-) 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 - +