添加功能

This commit is contained in:
zhou
2025-04-22 19:57:55 +08:00
parent 2c49e1aa08
commit ce3b46f3fc
12 changed files with 519 additions and 7 deletions
+1 -1
View File
@@ -16,7 +16,7 @@
<xpath expr="//field[@name='paperformat_id']" position="after">
<field name="template_docx"
invisible="report_type not in ['docx']" string="报表模板"/>
f
<field name="out_report_type"
invisible="report_type not in ['docx']" string="输出报告类型"/>
@@ -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:
@@ -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)
]
+3
View File
@@ -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',
],
},
+12 -2
View File
@@ -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',
})
})
def button_salary_print(self):
return {
"type": "ir.actions.act_window",
"name": "导入薪资",
"res_model": "salary.print.wizard",
"view_mode": "form",
"target": "new",
}
+2 -1
View File
@@ -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
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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
18 access_yuthon_payroll_retired access_yuthon_payroll_retired model_yuthon_payroll_retired base.group_user 1 1 1 1
19 access_payroll_labor_wizard access_payroll_labor_wizard model_payroll_labor_wizard base.group_user 1 1 1 1
20 access_payroll_retired_wizard access_payroll_retired_wizard model_payroll_retired_wizard base.group_user 1 1 1 1
21 access_yuthon_salary_documents access_yuthon_salary_documents model_yuthon_salary_documents base.group_user 1 1 1 1
22 access_salary_print_wizard access_salary_print_wizard model_salary_print_wizard base.group_user 1 1 1 1
@@ -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",
});
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<templates>
<t t-name="salary_validation_module.ListView.Buttons" t-inherit="web.ListView.Buttons">
<xpath expr="//div[hasclass('o_list_buttons')]" position="after">
<button type="button" class="btn btn-primary" style="margin-left: 10px;" t-on-click="openSalaryValidationWizard">
打印
</button>
</xpath>
</t>
</templates>
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!--薪资审定-->
<record id="hr_payroll_inherit" model="ir.ui.view">
<field name="name">hr.payroll.inherit</field>
<field name="model">hr.payslip</field>
@@ -79,6 +80,60 @@
</xpath>
</field>
</record>
<!--薪资发放-->
<record id="view_hr_payslip_tree3" model="ir.ui.view">
<field name="name">hr.payslip.tree</field>
<field name="model">hr.payslip</field>
<field name="arch" type="xml">
<tree string="薪资发放" default_order="employee_id" js_class="custom_salary_validation_button">
<header>
<button name="button_salary_print" type="object" string="打印" class="btn-primary"/>
</header>
<field name="employee_id" string="姓名" widget="ztree_select"/>
<field name="company_id" string="公司"/>
<field name="struct_id" string="职级"/>
<field name="bank_account_id"/>
<field name="actual_salary" decoration-danger="1"/>
<field name="base_salary" required="1"/>
<field name="rank_salary" required="1"/>
<field name="post_salary" required="1"/>
<field name="performance_bonuses" required="1"/>
<field name="commuting_allowance" required="1"/>
<field name="seniority_salary" required="1"/>
<field name="full_attendance_award"/>
<field name="overtime_pay"/>
<field name="high_temperature_allowance"/>
<field name="temporary_performance_bonus"/>
<field name="year_compute" decoration-danger="1"/>
<field name="year_jie" decoration-danger="1"/>
<field name="jing_te" decoration-danger="1"/>
<field name="year_gong" decoration-danger="1"/>
<field name="study_on" decoration-danger="1"/>
<field name="base_salary_deduction" decoration-success="1"/>
<field name="commuting_deduction" decoration-success="1"/>
<field name="performance_deduction" decoration-success="1"/>
<field name="total_before_tax" decoration-danger="1" decoration-bf="1"/>
<field name="basic_endowment_insurance"/>
<field name="basic_medical_insurance"/>
<field name="unemployment_insurance"/>
<field name="b_year"/>
<field name="union_dues"/>
<field name="tax_payable" decoration-danger="1" decoration-bf="1"/>
<field name="deduction_standard"/>
<field name="specialized_deduction"/>
<field name="child_education"/>
<field name="elderly_support"/>
<field name="housing_loan_interest"/>
<field name="housing_rent"/>
<field name="continuing_education"/>
<field name="infant_care"/>
<field name="meal_misunderstanding_fee"/>
<field name="salary_state"/>
<field name="is_salary_release" column_invisible="1"/>
<field name="is_salary_push" column_invisible="1"/>
</tree>
</field>
</record>
<record id="hr_payroll_inherit_form" model="ir.ui.view">
<field name="name">hr.payroll.inherit.form</field>
<field name="model">hr.payslip</field>
@@ -310,7 +365,7 @@
<field name="domain">[('is_salary_push', '=', True), ('is_salary_release', '=', True)]</field>
<field name="context">{'search_default_group_by_employee': 1}</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('hr_payroll.view_hr_payslip_tree')}),
(0, 0, {'view_mode': 'tree', 'view_id': ref('yuthon_salary.view_hr_payslip_tree3')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('hr_payroll.view_hr_payslip_form')})]"/>
</record>
<record id="action_view_hr_payslip_month_form1" model="ir.actions.act_window">
+2 -1
View File
@@ -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
from . import payroll_retired_wizard
from . import salary_print_wizard
+171
View File
@@ -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',
}
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_salary_print_wizard_form" model="ir.ui.view">
<field name="name">salary.print.wizard.form</field>
<field name="model">salary.print.wizard</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="current_year"/>
<field name="current_month"/>
</group>
</sheet>
<footer>
<button name="print_fixed_report" string="确认" type="object" class="oe_highlight"/>
<button string="返回" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>