diff --git a/excel_report_designer/models/report_excel.py b/excel_report_designer/models/report_excel.py
index 5148f33d9..7be98878c 100644
--- a/excel_report_designer/models/report_excel.py
+++ b/excel_report_designer/models/report_excel.py
@@ -1351,3 +1351,82 @@ class ReportExcel(models.Model):
output.seek(0)
response.stream.write(output.read())
output.close()
+
+ def get_xlsx_meal_active_report(self, data, response):
+ """误餐归档明细报表的自定义 Excel 样式"""
+ output = io.BytesIO()
+ workbook = xlsxwriter.Workbook(output, {'in_memory': True})
+ sheet = workbook.add_worksheet('误餐归档明细')
+ 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
+ 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):
+ 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):
+ 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':
+ sheet.write(row, col, value or 0, data_format)
+ elif field.type == '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()
diff --git a/yuthon_hr_attendance/__manifest__.py b/yuthon_hr_attendance/__manifest__.py
index 3356b692b..285b78118 100644
--- a/yuthon_hr_attendance/__manifest__.py
+++ b/yuthon_hr_attendance/__manifest__.py
@@ -113,6 +113,8 @@
'yuthon_hr_attendance/static/src/xml/button_overtime_export.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',
+ 'yuthon_hr_attendance/static/src/xml/button_meal_active_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/static/src/js/button_meal_active_export.js b/yuthon_hr_attendance/static/src/js/button_meal_active_export.js
new file mode 100644
index 000000000..3bd17d5fd
--- /dev/null
+++ b/yuthon_hr_attendance/static/src/js/button_meal_active_export.js
@@ -0,0 +1,35 @@
+/** @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 CustomMealActiveExportController extends ListController {
+ setup() {
+ super.setup();
+ }
+
+ async exportMealActiveExcel() {
+ const selectedIds = await this.getSelectedResIds();
+
+ return this.actionService.doAction({
+ type: 'ir.actions.act_window',
+ res_model: 'export.date.wizard',
+ name: '打印薪资',
+ view_mode: 'form',
+ view_type: 'form',
+ views: [[false, 'form']],
+ target: 'new',
+ context: {
+ default_export_type: 'meal_active',
+ default_model_name: 'vacation.meal.active',
+ default_active_ids: JSON.stringify(selectedIds),
+ },
+ });
+ }
+}
+
+registry.category("views").add("custom_meal_active_export_button", {
+ ...listView,
+ Controller: CustomMealActiveExportController,
+ buttonTemplate: "meal_active_export_module.ListView.Buttons",
+});
diff --git a/yuthon_hr_attendance/static/src/js/button_overtime_active_export.js b/yuthon_hr_attendance/static/src/js/button_overtime_active_export.js
new file mode 100644
index 000000000..75a18cb25
--- /dev/null
+++ b/yuthon_hr_attendance/static/src/js/button_overtime_active_export.js
@@ -0,0 +1,37 @@
+/** @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 CustomOvertimeActiveExportController extends ListController {
+ setup() {
+ super.setup();
+ }
+
+ async exportOvertimeActiveExcel() {
+ // 获取选中的记录 ID
+ const selectedIds = await this.getSelectedResIds();
+
+ // 直接打开向导,不经过 Python 方法
+ return this.actionService.doAction({
+ type: 'ir.actions.act_window',
+ res_model: 'export.date.wizard',
+ name: '打印薪资',
+ view_mode: 'form',
+ view_type: 'form',
+ views: [[false, 'form']],
+ target: 'new',
+ context: {
+ default_export_type: 'overtime_active',
+ default_model_name: 'overtime.active.line',
+ default_active_ids: JSON.stringify(selectedIds),
+ },
+ });
+ }
+}
+
+registry.category("views").add("custom_overtime_active_export_button", {
+ ...listView,
+ Controller: CustomOvertimeActiveExportController,
+ buttonTemplate: "overtime_active_export_module.ListView.Buttons",
+});
diff --git a/yuthon_hr_attendance/static/src/js/button_overtime_export.js b/yuthon_hr_attendance/static/src/js/button_overtime_export.js
new file mode 100644
index 000000000..0c40c036f
--- /dev/null
+++ b/yuthon_hr_attendance/static/src/js/button_overtime_export.js
@@ -0,0 +1,56 @@
+/** @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 CustomOvertimeExportController extends ListController {
+ setup() {
+ super.setup();
+ }
+
+ async exportOvertimeExcel() {
+ // 获取当前选中的记录 ID(如果有选中)
+ const selectedIds = await this.getSelectedResIds();
+
+ return this.actionService.doAction({
+ type: 'ir.actions.act_window',
+ res_model: 'export.date.wizard',
+ name: '打印薪资',
+ view_mode: 'form',
+ view_type: 'form',
+ views: [[false, 'form']],
+ target: 'new',
+ context: {
+ default_export_type: 'overtime',
+ default_model_name: 'vacation.over.line',
+ default_active_ids: JSON.stringify(selectedIds),
+ },
+ });
+ }
+
+ async exportOvertimeDetailExcel() {
+ // 导出加班详情(包含审批类型、开始结束时间、事由、餐费等)
+ const selectedIds = await this.getSelectedResIds();
+
+ return this.actionService.doAction({
+ type: 'ir.actions.act_window',
+ res_model: 'export.date.wizard',
+ name: '打印薪资',
+ view_mode: 'form',
+ view_type: 'form',
+ views: [[false, 'form']],
+ target: 'new',
+ context: {
+ default_export_type: 'overtime_detail',
+ default_model_name: 'vacation.over.line',
+ default_active_ids: JSON.stringify(selectedIds),
+ },
+ });
+ }
+}
+
+registry.category("views").add("custom_overtime_export_button", {
+ ...listView,
+ Controller: CustomOvertimeExportController,
+ buttonTemplate: "overtime_export_module.ListView.Buttons",
+});
diff --git a/yuthon_hr_attendance/static/src/xml/button_meal_active_export.xml b/yuthon_hr_attendance/static/src/xml/button_meal_active_export.xml
new file mode 100644
index 000000000..a8107c4c4
--- /dev/null
+++ b/yuthon_hr_attendance/static/src/xml/button_meal_active_export.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/yuthon_hr_attendance/static/src/xml/button_overtime_active_export.xml b/yuthon_hr_attendance/static/src/xml/button_overtime_active_export.xml
new file mode 100644
index 000000000..a68a01240
--- /dev/null
+++ b/yuthon_hr_attendance/static/src/xml/button_overtime_active_export.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/yuthon_hr_attendance/static/src/xml/button_overtime_export.xml b/yuthon_hr_attendance/static/src/xml/button_overtime_export.xml
new file mode 100644
index 000000000..b9c7b03bf
--- /dev/null
+++ b/yuthon_hr_attendance/static/src/xml/button_overtime_export.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
diff --git a/yuthon_hr_attendance/views/vacation_meal_active_views.xml b/yuthon_hr_attendance/views/vacation_meal_active_views.xml
index 2d35b2b08..ee8e5db9a 100644
--- a/yuthon_hr_attendance/views/vacation_meal_active_views.xml
+++ b/yuthon_hr_attendance/views/vacation_meal_active_views.xml
@@ -4,7 +4,7 @@
vacation.meal.active.tree
vacation.meal.active
-
+
diff --git a/yuthon_hr_attendance/wizard/export_date_wizard.py b/yuthon_hr_attendance/wizard/export_date_wizard.py
index db7a48acb..e1e9cdbe2 100644
--- a/yuthon_hr_attendance/wizard/export_date_wizard.py
+++ b/yuthon_hr_attendance/wizard/export_date_wizard.py
@@ -43,6 +43,8 @@ class ExportDateWizard(models.TransientModel):
return self._export_overtime_active(start_date, end_date)
elif self.export_type == 'meal':
return self._export_meal(start_date, end_date)
+ elif self.export_type == 'meal_active':
+ return self._export_meal_active(start_date, end_date)
else:
raise UserError('未知的导出类型')
@@ -282,3 +284,42 @@ class ExportDateWizard(models.TransientModel):
},
'report_type': 'xlsx',
}
+
+ def _export_meal_active(self, start_date, end_date):
+ """导出误餐归档明细"""
+ active_ids = json.loads(self.active_ids) if self.active_ids else []
+
+ domain = [('start_time', '>=', start_date), ('start_time', '<', end_date)]
+ if active_ids:
+ domain = [('id', 'in', active_ids)] + domain
+
+ filtered_records = self.env['vacation.meal.active'].search(domain)
+
+ if not filtered_records:
+ raise UserError(f'在 {self.year} 年 {self.month} 月没有找到符合条件的误餐归档记录')
+
+ filtered_ids = filtered_records.ids
+
+ fields_name = ['employee_id', 'department_id', 'workflow_id', 'start_time',
+ 'end_time', 'duration', 'reasons', 'meal']
+ field_label = ['发起人', '部门', '审批类型', '加班开始时间', '加班结束时间',
+ '加班时长', '事由', '加班餐费']
+
+ data = {
+ 'report_name': '误餐归档明细报表',
+ 'model_name': 'vacation.meal.active',
+ 'fields_name': fields_name,
+ 'field_label': field_label,
+ 'active_ids': filtered_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',
+ }