From 2c4af33fa8380e10d148cf3c87f826bfafff61ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Mon, 9 Feb 2026 00:05:53 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- excel_report_designer/models/report_excel.py | 79 +++++++++++++++++++ yuthon_hr_attendance/__manifest__.py | 2 + .../src/js/button_meal_active_export.js | 35 ++++++++ .../src/js/button_overtime_active_export.js | 37 +++++++++ .../static/src/js/button_overtime_export.js | 56 +++++++++++++ .../src/xml/button_meal_active_export.xml | 10 +++ .../src/xml/button_overtime_active_export.xml | 10 +++ .../static/src/xml/button_overtime_export.xml | 13 +++ .../views/vacation_meal_active_views.xml | 2 +- .../wizard/export_date_wizard.py | 41 ++++++++++ 10 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 yuthon_hr_attendance/static/src/js/button_meal_active_export.js create mode 100644 yuthon_hr_attendance/static/src/js/button_overtime_active_export.js create mode 100644 yuthon_hr_attendance/static/src/js/button_overtime_export.js create mode 100644 yuthon_hr_attendance/static/src/xml/button_meal_active_export.xml create mode 100644 yuthon_hr_attendance/static/src/xml/button_overtime_active_export.xml create mode 100644 yuthon_hr_attendance/static/src/xml/button_overtime_export.xml 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', + }