diff --git a/docx_report_pro/views/views.xml b/docx_report_pro/views/views.xml index f4ef16ed6..8e6564cbd 100644 --- a/docx_report_pro/views/views.xml +++ b/docx_report_pro/views/views.xml @@ -16,7 +16,7 @@ - +f diff --git a/excel_report_designer/controllers/excel_report_designer.py b/excel_report_designer/controllers/excel_report_designer.py index 26f4f218a..c212cea1e 100644 --- a/excel_report_designer/controllers/excel_report_designer.py +++ b/excel_report_designer/controllers/excel_report_designer.py @@ -46,7 +46,10 @@ class XLSXReportController(http.Controller): content_disposition(report_name + '.xlsx')) ] ) - report_obj.get_xlsx_report(options, response) + if report_name == '薪资': + report_obj.get_xlsx_salary_report(options, response) + else: + report_obj.get_xlsx_report(options, response) response.set_cookie('fileToken', token) return response except Exception as e: diff --git a/excel_report_designer/models/report_excel.py b/excel_report_designer/models/report_excel.py index a9a4e38ac..9c9832614 100644 --- a/excel_report_designer/models/report_excel.py +++ b/excel_report_designer/models/report_excel.py @@ -30,6 +30,8 @@ import io import json import datetime from odoo.tools import date_utils +from collections import defaultdict +from odoo.exceptions import UserError class ReportExcel(models.Model): @@ -345,3 +347,208 @@ class ReportExcel(models.Model): self.fields_ids = [] if self.fields_ids: self.field_order = str(self.fields_ids._origin.ids) + + def get_xlsx_salary_report(self, data, response): + company_name = self.env.user.company_id.name + output = io.BytesIO() + workbook = xlsxwriter.Workbook(output, {'in_memory': True, 'default_sheet_index': -1}) + # 定义基本格式 + title_format = workbook.add_format({ + 'font_size': 16, + 'align': 'center', + 'valign': 'vcenter', + 'bold': True + }) + # 定义序号列的格式,使其居中 + serial_number_format = workbook.add_format({ + 'font_size': 10, + 'border': 1, + 'align': 'center', + 'valign': 'vcenter' + }) + header_format = workbook.add_format({'font_size': 11, 'bold': True, 'border': 1}) + data_format = workbook.add_format({'font_size': 10, 'border': 1}) + domain = self._get_payslip_domain(data) + records = self.env[data['model_name']].search(domain) + # # 按 salary_group_id 分组 + grouped_records = defaultdict(list) + for record in records: + group_id = record.salary_group_id.id if record.salary_group_id else None + grouped_records[group_id].append(record) + for group_id, group_records in grouped_records.items(): + if group_id: + group_name = self.env['yuthon.salary.group'].browse(group_id).name + else: + group_name = '未分组' + # 创建新的工作表 + sheet = workbook.add_worksheet(group_name[:31]) + # 写入公司和年月信息 + sheet.merge_range(0, 0, 0, len(data['field_label']) - 1, f'员工{data["current_year"]}年{data["current_month"]}工资表({company_name})', title_format) + # 写入表头 + headers = ['序号']+data['field_label'] + col_num = 0 + for label in headers: + sheet.write(1, col_num, label, header_format) + col_num += 1 + + row_num = 2 + serial_number = 1 + for record in group_records: + col_num = 0 + sheet.write(row_num, col_num, serial_number, serial_number_format) + col_num += 1 + for field_name in data['fields_name']: + field = self.env[data['model_name']]._fields.get(field_name) + value = record[field_name] + if isinstance(value, datetime.date): + value = value.strftime('%Y-%m-%d') + elif field: + if field.type == 'many2one': + if value: + value = value.display_name + else: + value = "" + elif field.type == 'many2many': + value = ', '.join([rec.display_name for rec in value]) if value else "" + elif field.type == 'one2many': + value = ', '.join([rec.display_name for rec in value]) if value else "" + elif field.type == 'selection': + # 获取选择字段的选项列表 + selection_options = dict(field.selection) + # 将值转换为对应的显示标签 + value = selection_options.get(value, value) + sheet.write(row_num, col_num, value, data_format) + col_num += 1 + row_num += 1 + serial_number += 1 # 序号递增 + # 创建劳务信息工作表 + labor_sheet = workbook.add_worksheet('劳务信息') + labor_fields = ['employee_id', 'labor_amount'] + labor_labels = ['员工姓名', '劳务金额'] + labor_headers = ['序号'] + labor_labels + # 写入劳务费大标题 + labor_title = f'员工{data["current_year"]}年{data["current_month"]}月劳务费({company_name})' + labor_sheet.merge_range(0, 0, 0, len(labor_headers) - 1, labor_title, title_format) + title_width = len(labor_title) + 2 # 适当增加一些宽度余量 + labor_sheet.set_column(0, len(labor_headers) - 1, title_width) + # 写入表头 + col_num = 0 + for label in labor_headers: + labor_sheet.write(1, col_num, label, header_format) + col_num += 1 + labor_records = self.env['yuthon.payroll.labor'].search([ + ('current_year', '=', data['current_year']), + ('current_month', '=', data['current_month']) + ]) + row_num = 2 + serial_number = 1 + for labor_record in labor_records: + col_num = 0 + labor_sheet.write(row_num, col_num, serial_number, serial_number_format) + col_num += 1 + for field_name in labor_fields: + field = self.env['yuthon.payroll.labor']._fields.get(field_name) + value = labor_record[field_name] + if isinstance(value, datetime.date): + value = value.strftime('%Y-%m-%d') + elif field: + if field.type == 'many2one': + if value: + value = value.display_name + else: + value = "" + elif field.type == 'many2many': + value = ', '.join([rec.display_name for rec in value]) if value else "" + elif field.type == 'one2many': + value = ', '.join([rec.display_name for rec in value]) if value else "" + elif field.type == 'selection': + selection_options = dict(field.selection) + value = selection_options.get(value, value) + labor_sheet.write(row_num, col_num, value, data_format) + col_num += 1 + row_num += 1 + serial_number += 1 + + # 创建退休人员费用工作表 + retired_sheet = workbook.add_worksheet('退休人员费用') + retired_fields = ['name', 'retired_amount'] + retired_labels = ['姓名', '费用'] + retired_headers = ['序号'] + retired_labels + # 根据标题长度动态计算并设置列宽 + retired_title = f'员工{data["current_year"]}年{data["current_month"]}月退休费用({company_name})' + retired_sheet.merge_range(0, 0, 0, len(labor_headers) - 1, retired_title, title_format) + title_width = len(retired_title) + 2 # 适当增加一些宽度余量 + retired_sheet.set_column(0, len(retired_headers) - 1, title_width) + # 写入表头 + col_num = 0 + for label in retired_headers: + retired_sheet.write(1, col_num, label, header_format) + col_num += 1 + retired_records = self.env['yuthon.payroll.retired'].search([ + ('current_year', '=', data['current_year']), + ('current_month', '=', data['current_month']) + ]) + row_num = 2 + serial_number = 1 + for retired_record in retired_records: + col_num = 0 + retired_sheet.write(row_num, col_num, serial_number, serial_number_format) + col_num += 1 + for field_name in retired_fields: + field = self.env['yuthon.payroll.retired']._fields.get(field_name) + value = retired_record[field_name] + if isinstance(value, datetime.date): + value = value.strftime('%Y-%m-%d') + elif field: + if field.type == 'many2one': + if value: + value = value.display_name + else: + value = "" + elif field.type == 'many2many': + value = ', '.join([rec.display_name for rec in value]) if value else "" + elif field.type == 'one2many': + value = ', '.join([rec.display_name for rec in value]) if value else "" + elif field.type == 'selection': + selection_options = dict(field.selection) + value = selection_options.get(value, value) + retired_sheet.write(row_num, col_num, value, data_format) + col_num += 1 + row_num += 1 + serial_number += 1 + + # 关闭工作簿 + workbook.close() + # 将文件指针移动到文件开头 + output.seek(0) + # 将文件内容写入响应流 + response.stream.write(output.read()) + # 关闭文件对象 + output.close() + + + def _get_payslip_domain(self, data): + current_month = data['current_month'] + current_year = data['current_year'] + # 把数字月份转换为中文月份 + month_mapping = { + '1': '一月', + '2': '二月', + '3': '三月', + '4': '四月', + '5': '五月', + '6': '六月', + '7': '七月', + '8': '八月', + '9': '九月', + '10': '十月', + '11': '十一月', + '12': '十二月' + } + chinese_month = month_mapping.get(current_month) + target_name = f"{current_year}年-{chinese_month}" + return [ + ('is_salary_push', '=', True), + ('is_salary_release', '=', True), + ('payslip_run_id.name', '=', target_name) + ] \ No newline at end of file diff --git a/yuthon_salary/__manifest__.py b/yuthon_salary/__manifest__.py index bc807f210..c6e2e1971 100644 --- a/yuthon_salary/__manifest__.py +++ b/yuthon_salary/__manifest__.py @@ -32,6 +32,7 @@ 'wizard/salary_payslip_wizard_views.xml', 'wizard/select_delete_wizard_viwes.xml', 'wizard/put_return_wizard_viwes.xml', + 'wizard/salary_print_wizard_views.xml', ], 'assets': { 'web.assets_backend': [ @@ -42,11 +43,13 @@ 'yuthon_salary/static/src/js/salary_labor_button.js', 'yuthon_salary/static/src/js/salary_return_button.js', 'yuthon_salary/static/src/js/salary_retired_button.js', + 'yuthon_salary/static/src/js/salary_validation_button.js', 'yuthon_salary/static/src/xml/salary_create_button.xml', 'yuthon_salary/static/src/xml/salary_labor_button.xml', 'yuthon_salary/static/src/xml/salary_retired_button.xml', 'yuthon_salary/static/src/xml/salary_payslip_button.xml', 'yuthon_salary/static/src/xml/salary_documents_button.xml', + 'yuthon_salary/static/src/xml/salary_validation_button.xml', 'yuthon_salary/static/src/xml/salary_return_button.xml', ], }, diff --git a/yuthon_salary/models/inherit_hr_payslip.py b/yuthon_salary/models/inherit_hr_payslip.py index f78a0b16f..0f9ff1e0f 100644 --- a/yuthon_salary/models/inherit_hr_payslip.py +++ b/yuthon_salary/models/inherit_hr_payslip.py @@ -1,6 +1,7 @@ from odoo import api, fields, models, _ from odoo.exceptions import ValidationError, UserError - +import json +from odoo.tools import date_utils class InheritHrPayslip(models.Model): _inherit = 'hr.payslip' @@ -332,4 +333,13 @@ class InheritHrPayslip(models.Model): record.write({ 'is_salary_release': False, 'salary_state': 'to_signed', - }) \ No newline at end of file + }) + + def button_salary_print(self): + return { + "type": "ir.actions.act_window", + "name": "导入薪资", + "res_model": "salary.print.wizard", + "view_mode": "form", + "target": "new", + } \ No newline at end of file diff --git a/yuthon_salary/security/ir.model.access.csv b/yuthon_salary/security/ir.model.access.csv index b9ce2dba8..0f13f6672 100644 --- a/yuthon_salary/security/ir.model.access.csv +++ b/yuthon_salary/security/ir.model.access.csv @@ -18,4 +18,5 @@ access_yuthon_payroll_labor,access_yuthon_payroll_labor,model_yuthon_payroll_lab access_yuthon_payroll_retired,access_yuthon_payroll_retired,model_yuthon_payroll_retired,base.group_user,1,1,1,1 access_payroll_labor_wizard,access_payroll_labor_wizard,model_payroll_labor_wizard,base.group_user,1,1,1,1 access_payroll_retired_wizard,access_payroll_retired_wizard,model_payroll_retired_wizard,base.group_user,1,1,1,1 -access_yuthon_salary_documents,access_yuthon_salary_documents,model_yuthon_salary_documents,base.group_user,1,1,1,1 \ No newline at end of file +access_yuthon_salary_documents,access_yuthon_salary_documents,model_yuthon_salary_documents,base.group_user,1,1,1,1 +access_salary_print_wizard, access_salary_print_wizard,model_salary_print_wizard,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/yuthon_salary/static/src/js/salary_validation_button.js b/yuthon_salary/static/src/js/salary_validation_button.js new file mode 100644 index 000000000..819ac1e91 --- /dev/null +++ b/yuthon_salary/static/src/js/salary_validation_button.js @@ -0,0 +1,29 @@ +/** @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 CustomSalaryValidationController extends ListController { + setup() { + super.setup(); + } + + openSalaryValidationWizard() { + this.actionService.doAction({ + type: 'ir.actions.act_window', + res_model: 'salary.print.wizard', + name: '薪资审定打印弹窗', + view_mode: 'form', + view_type: 'form', + views: [[false, 'form']], + target: 'new', // 以弹窗形式打开 + context: {}, + }); + } +} + +registry.category("views").add("custom_salary_validation_button", { + ...listView, + Controller: CustomSalaryValidationController, + buttonTemplate: "salary_validation_module.ListView.Buttons", +}); \ No newline at end of file diff --git a/yuthon_salary/static/src/xml/salary_validation_button.xml b/yuthon_salary/static/src/xml/salary_validation_button.xml new file mode 100644 index 000000000..cee071d0d --- /dev/null +++ b/yuthon_salary/static/src/xml/salary_validation_button.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/yuthon_salary/views/inherit_hr_payslip_views.xml b/yuthon_salary/views/inherit_hr_payslip_views.xml index ef45f497b..3f255cc4e 100644 --- a/yuthon_salary/views/inherit_hr_payslip_views.xml +++ b/yuthon_salary/views/inherit_hr_payslip_views.xml @@ -1,5 +1,6 @@ + hr.payroll.inherit hr.payslip @@ -79,6 +80,60 @@ + + + hr.payslip.tree + hr.payslip + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
hr.payroll.inherit.form hr.payslip @@ -310,7 +365,7 @@ [('is_salary_push', '=', True), ('is_salary_release', '=', True)] {'search_default_group_by_employee': 1} diff --git a/yuthon_salary/wizard/__init__.py b/yuthon_salary/wizard/__init__.py index 4ee17002f..54bf093a8 100644 --- a/yuthon_salary/wizard/__init__.py +++ b/yuthon_salary/wizard/__init__.py @@ -6,4 +6,5 @@ from . import select_delete_wizard from . import put_return_wizard from . import temporary_payslip_wizard from . import payroll_labor_wizard -from . import payroll_retired_wizard \ No newline at end of file +from . import payroll_retired_wizard +from . import salary_print_wizard \ No newline at end of file diff --git a/yuthon_salary/wizard/salary_print_wizard.py b/yuthon_salary/wizard/salary_print_wizard.py new file mode 100644 index 000000000..378bad5da --- /dev/null +++ b/yuthon_salary/wizard/salary_print_wizard.py @@ -0,0 +1,171 @@ +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError, UserError +import json +from odoo.tools import date_utils +import datetime + + +class SalaryPrintWizard(models.Model): + _name = 'salary.print.wizard' + _description = '薪资打印弹窗' + + current_year = fields.Integer(string='年份', default=lambda self: datetime.datetime.now().year) + current_month = fields.Selection([ + ('1', '1月份'), + ('2', '2月份'), + ('3', '3月份'), + ('4', '4月份'), + ('5', '5月份'), + ('6', '6月份'), + ('7', '7月份'), + ('8', '8月份'), + ('9', '9月份'), + ('10', '10月份'), + ('11', '11月份'), + ('12', '12月份') + ], string="月份", default=lambda self: str(datetime.datetime.now().month)) + + def print_fixed_report(self): + fields_list = [ + 'employee_id', + 'company_id', + 'struct_id', + 'bank_account_id', + 'actual_salary', + 'base_salary', + 'rank_salary', + 'post_salary', + 'performance_bonuses', + 'commuting_allowance', + 'seniority_salary', + 'full_attendance_award', + 'overtime_pay', + 'high_temperature_allowance', + 'temporary_performance_bonus', + 'year_compute', + 'year_jie', + 'jing_te', + 'year_gong', + 'study_on', + 'base_salary_deduction', + 'commuting_deduction', + 'performance_deduction', + 'total_before_tax', + 'basic_endowment_insurance', + 'basic_medical_insurance', + 'unemployment_insurance', + 'b_year', + 'union_dues', + 'tax_payable', + 'deduction_standard', + 'specialized_deduction', + 'child_education', + 'elderly_support', + 'housing_loan_interest', + 'housing_rent', + 'continuing_education', + 'infant_care', + 'meal_misunderstanding_fee', + 'salary_state' + ] + data = { + 'report_name': '薪资', + 'model_name': 'hr.payslip', # 指定模型 + 'current_month': self.current_month, + 'current_year': self.current_year, + 'fields_name': [ + 'employee_id', + 'company_id', + 'struct_id', + 'bank_account_id', + 'actual_salary', + 'base_salary', + 'rank_salary', + 'post_salary', + 'performance_bonuses', + 'commuting_allowance', + 'seniority_salary', + 'full_attendance_award', + 'overtime_pay', + 'high_temperature_allowance', + 'temporary_performance_bonus', + 'year_compute', + 'year_jie', + 'jing_te', + 'year_gong', + 'study_on', + 'base_salary_deduction', + 'commuting_deduction', + 'performance_deduction', + 'total_before_tax', + 'basic_endowment_insurance', + 'basic_medical_insurance', + 'unemployment_insurance', + 'b_year', + 'union_dues', + 'tax_payable', + 'deduction_standard', + 'specialized_deduction', + 'child_education', + 'elderly_support', + 'housing_loan_interest', + 'housing_rent', + 'continuing_education', + 'infant_care', + 'meal_misunderstanding_fee', + 'salary_state' + ], + 'field_label': [ + '姓名', + '公司', + '职级', + '银行账号', + '实发工资', + '基本工资', + '职级工资', + '岗位工资', + '预发绩效奖金', + '通信补贴', + '工龄工资', + '全勤奖', + '加班费', + '高温补贴', + '临时绩效奖', + '年度清算', + '年度结余', + '经营特别奖', + '年功奖', + '学习进步奖', + '扣发基本工资', + '扣发通信补贴', + '扣发绩效奖金', + '本期收入', + '基本养老保险费', + '基本医疗保险费', + '失业保险费', + '企业(职业)年金', + '工会会费', + '应补(退)税额', + '减除费用标准', + '累计专项扣除', + '累计子女教育', + '累计赡养老人', + '累计住房贷款利息', + '累计住房租金', + '累计继续教育', + '累计3岁以下婴幼儿照护', + '误餐费', + '状态' + ], + 'field_order': str([i for i in range(1, len(fields_list) + 1)]) + } + 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_salary/wizard/salary_print_wizard_views.xml b/yuthon_salary/wizard/salary_print_wizard_views.xml new file mode 100644 index 000000000..e077f1fd7 --- /dev/null +++ b/yuthon_salary/wizard/salary_print_wizard_views.xml @@ -0,0 +1,22 @@ + + + + salary.print.wizard.form + salary.print.wizard + +
+ + + + + + + +
+
+
+ +