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
+
+
+
+
+
+