1645 lines
71 KiB
Python
1645 lines
71 KiB
Python
# -*- coding: utf-8 -*-
|
||
################################################################################
|
||
#
|
||
# Cybrosys Technologies Pvt. Ltd.
|
||
#
|
||
# Copyright (C) 2024-TODAY Cybrosys Technologies (<https://www.cybrosys.com>).
|
||
# Author: Cybrosys Techno Solutions (<https://www.cybrosys.com>)
|
||
#
|
||
# This program is free software: you can modify
|
||
# it under the terms of the GNU Affero General Public License (AGPL) as
|
||
# published by the Free Software Foundation, either version 3 of the
|
||
# License, or (at your option) any later version.
|
||
#
|
||
# This program is distributed in the hope that it will be useful,
|
||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
# GNU Affero General Public License for more details.
|
||
#
|
||
# You should have received a copy of the GNU Affero General Public License
|
||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
#
|
||
################################################################################
|
||
from odoo import api, fields, models, _
|
||
|
||
try:
|
||
from odoo.tools.misc import xlsxwriter
|
||
except ImportError:
|
||
import xlsxwriter
|
||
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):
|
||
"""this is used to create the a binding action"""
|
||
_name = 'report.excel'
|
||
_description = "report excel"
|
||
_rec_name = 'name'
|
||
|
||
name = fields.Char(string='名称', help='Name')
|
||
model_id = fields.Many2one('ir.model', string='模型',
|
||
required=True,
|
||
ondelete="cascade", help="The binding model")
|
||
fields_ids = fields.Many2many('ir.model.fields',
|
||
string='字段',
|
||
required=True, ondelete="cascade",
|
||
help="the fields to be printed in the report")
|
||
date_field = fields.Many2one('ir.model.fields',
|
||
string='日期筛选器',
|
||
ondelete="cascade",
|
||
help="filter on the basis of date")
|
||
start_date = fields.Date(string='开始日期', help="start date")
|
||
end_date = fields.Date(string='结束日期', help="end date")
|
||
field_order = fields.Char(string="字段顺序", default='[]',
|
||
help="the field order")
|
||
action_button = fields.Boolean(default=False, string="动作",
|
||
help="visibility of action")
|
||
state = fields.Selection([
|
||
('code', '执行 Python 代码'),
|
||
('object_create', '创建新记录'),
|
||
('object_write', '更新记录'),
|
||
('multi', '执行多个作')], string='要执行的动作',
|
||
default='code', required=True, copy=True,
|
||
help="根据")
|
||
binding_model_id = fields.Many2one('ir.model',
|
||
ondelete="cascade",
|
||
string="绑定模型 ID",
|
||
help="绑定模型 ID")
|
||
binding_type = fields.Selection([('action', 'Action'),
|
||
('report', 'Report')],
|
||
required=True, default='action',
|
||
string="绑定类型",
|
||
help="绑定类型")
|
||
ir_act_server_ref = fields.Many2one('ir.actions.act_window',
|
||
readonly=True,
|
||
copy=False, string="作参考",
|
||
help="作参考")
|
||
fields_ids_domain = fields.Char(compute="_compute_fields_ids_domain",
|
||
readonly=True, store=False)
|
||
date_field_domain = fields.Char(compute="_compute_date_field_domain",
|
||
readonly=True, store=False)
|
||
|
||
@api.depends('model_id')
|
||
def _compute_fields_ids_domain(self):
|
||
"""Domain for fields_ids"""
|
||
for rec in self:
|
||
rec.fields_ids_domain = json.dumps(
|
||
[('model_id', '=', rec.model_id.id)]
|
||
)
|
||
|
||
@api.depends('model_id')
|
||
def _compute_date_field_domain(self):
|
||
"""Domain for date_field"""
|
||
for rec in self:
|
||
rec.date_field_domain = json.dumps(
|
||
[('model_id', '=', rec.model_id.id),
|
||
('ttype', 'in', ['date', 'datetime'])]
|
||
)
|
||
|
||
@api.onchange('model_id')
|
||
def onchange_model_id(self):
|
||
"""This is used to return the fields"""
|
||
if self.model_id:
|
||
self.name = self.model_id.name + ' Report'
|
||
self.fields_ids = False
|
||
self.date_field = False
|
||
|
||
def print_report(self):
|
||
"""Create a function for printing reports"""
|
||
for rec in self:
|
||
data = {
|
||
'report_name': rec.name,
|
||
'model_name': rec.model_id.model,
|
||
'fields_name': rec.fields_ids.mapped('name'),
|
||
'field_label': rec.fields_ids.mapped('field_description'),
|
||
'date_field': rec.date_field.name,
|
||
'date_name': rec.date_field.field_description,
|
||
'start_date': rec.start_date,
|
||
'end_date': rec.end_date,
|
||
'field_order': rec.field_order
|
||
}
|
||
return {
|
||
'type': 'ir.actions.report',
|
||
'data': {'model': 'report.excel',
|
||
'options': json.dumps(data, default=date_utils.json_default),
|
||
'output_format': 'xlsx',
|
||
'report_name': rec.name,
|
||
},
|
||
'report_type': 'xlsx',
|
||
}
|
||
|
||
def get_xlsx_report(self, data, response):
|
||
"""this is used to print the report of all records"""
|
||
output = io.BytesIO()
|
||
workbook = xlsxwriter.Workbook(output, {'in_memory': True})
|
||
sheet = workbook.add_worksheet()
|
||
# Formats
|
||
format1 = workbook.add_format(
|
||
{'font_size': 15, 'align': 'center', 'bold': True})
|
||
format1.set_font_color('#000080')
|
||
format2 = workbook.add_format(
|
||
{'font_size': 11, 'bold': True, 'border': 1, 'bg_color': '#928E8E'})
|
||
format4 = workbook.add_format(
|
||
{'font_size': 10, 'num_format': 'yyyy-m-d', 'align': 'center',
|
||
'bold': True})
|
||
format5 = workbook.add_format(
|
||
{'font_size': 10, 'border': 1, 'text_wrap': True})
|
||
format6 = workbook.add_format({'font_size': 10, 'bold': True})
|
||
format8 = workbook.add_format({'font_size': 10, 'border': 1})
|
||
format9 = workbook.add_format(
|
||
{'font_size': 10, 'num_format': 'yyyy-m-d'})
|
||
format10 = workbook.add_format(
|
||
{'font_size': 10, 'num_format': 'yyyy-m-d', 'border': 1})
|
||
|
||
format2.set_align('center')
|
||
format4.set_align('right')
|
||
format6.set_align('right')
|
||
format8.set_align('left')
|
||
|
||
sheet.merge_range(1, 1, 1, len(data['field_label']) + 1,
|
||
data['report_name'], format1)
|
||
sheet.write(2, 0, "Date :", format4)
|
||
sheet.write(2, 1, fields.Datetime.today(), format4)
|
||
if data['date_field']:
|
||
sheet.write(3, 0, data['date_name'], format4)
|
||
if data['start_date']:
|
||
sheet.write(3, 1, "From:", format4)
|
||
sheet.write(3, 2, data['start_date'], format9)
|
||
else:
|
||
sheet.write(3, 2, "", format9)
|
||
if data['end_date']:
|
||
sheet.write(3, 3, "To:", format4)
|
||
sheet.write(3, 4, data['end_date'], format9)
|
||
else:
|
||
sheet.write(3, 4, "", format9)
|
||
sl_no = 1
|
||
sheet.write(5, 1, "SL No", format2)
|
||
row_num = 5
|
||
col_num = 2
|
||
order = data['field_order'].strip('][').split(', ')
|
||
for field_id in order:
|
||
field_name = self.env['ir.model.fields'].browse(
|
||
int(field_id)).field_description
|
||
sheet.write(row_num, col_num, field_name, format2)
|
||
col_num += 1
|
||
row_num += 1
|
||
records = []
|
||
if data['date_field']:
|
||
if data['start_date'] and data['end_date']:
|
||
records = self.env[data['model_name']].search([
|
||
(data['date_field'], '>=', data['start_date']),
|
||
(data['date_field'], '<=', data['end_date'])])
|
||
elif data['start_date'] and not data['end_date']:
|
||
records = self.env[data['model_name']].search([
|
||
(data['date_field'], '>=', data['start_date'])])
|
||
elif not data['start_date'] and data['end_date']:
|
||
records = self.env[data['model_name']].search([
|
||
(data['date_field'], '<=', data['end_date'])])
|
||
else:
|
||
records = self.env[data['model_name']].search([])
|
||
new_table = []
|
||
for record in records:
|
||
order = data['field_order'].strip('][').split(', ')
|
||
record_dict = {}
|
||
for field_id in order:
|
||
field = self.env['ir.model.fields'].browse(int(field_id))
|
||
field_name = field.name
|
||
field_type = self.env['ir.model.fields'].browse(
|
||
int(field_id)).ttype
|
||
if field_type in ['many2many']:
|
||
one2many_values = ', '.join(
|
||
record[field_name].mapped('name'))
|
||
record_dict[field] = [one2many_values]
|
||
elif field_type in ['one2many']:
|
||
o2m_list = []
|
||
for rec in record[field_name]:
|
||
if rec:
|
||
o2m_list.append(rec)
|
||
else:
|
||
o2m_list.append('')
|
||
record_dict[field] = o2m_list
|
||
else:
|
||
record_dict[field] = [record[field_name]]
|
||
new_table.append(record_dict)
|
||
for record in new_table:
|
||
col_num = 1
|
||
sheet.write(row_num, col_num, sl_no, format5)
|
||
col_num += 1
|
||
occupied_rows = max(len(value) for value in record.values())
|
||
for field in record:
|
||
field_type = self.env['ir.model.fields'].browse(
|
||
int(field.id)).ttype
|
||
if not field_type in ['one2many', 'many2many']:
|
||
try:
|
||
if isinstance(record[field][0], datetime.date):
|
||
sheet.write(row_num, col_num, record[field][0],
|
||
format10)
|
||
elif isinstance(record[field][0], bool):
|
||
if not field:
|
||
sheet.write(row_num, col_num, " ", format5)
|
||
else:
|
||
sheet.write(row_num, col_num, "Yes", format5)
|
||
else:
|
||
sheet.write(row_num, col_num, record[field][0],
|
||
format5)
|
||
except Exception as e:
|
||
if 'display_name' in record[field][0]:
|
||
sheet.write(row_num, col_num,
|
||
record[field][0].display_name, format5)
|
||
else:
|
||
sheet.write(row_num, col_num, "", format5)
|
||
elif field_type == 'one2many':
|
||
for i in range(occupied_rows):
|
||
if len(record[field]) > i:
|
||
try:
|
||
if isinstance(record[field][i], datetime.date):
|
||
sheet.write(row_num + i, col_num,
|
||
record[field][i], format10)
|
||
elif isinstance(record[field][i], bool):
|
||
if not field:
|
||
sheet.write(row_num + i, col_num, "NO",
|
||
format5)
|
||
else:
|
||
sheet.write(row_num + i, col_num, "Yes",
|
||
format5)
|
||
else:
|
||
sheet.write(row_num + i, col_num,
|
||
record[field][i], format5)
|
||
except Exception as e:
|
||
if 'display_name' in record[field][i]:
|
||
sheet.write(row_num + i, col_num,
|
||
record[field][i].display_name, format5)
|
||
else:
|
||
sheet.write(row_num + i, col_num, "",
|
||
format5)
|
||
elif field_type == 'many2many':
|
||
if record[field]:
|
||
try:
|
||
if isinstance(record[field], datetime.date):
|
||
sheet.write(row_num, col_num, record[field][0],
|
||
format10)
|
||
elif isinstance(record[field], bool):
|
||
if not field:
|
||
sheet.write(row_num, col_num, " ", format5)
|
||
else:
|
||
sheet.write(row_num, col_num, "Yes",
|
||
format5)
|
||
else:
|
||
sheet.write(row_num, col_num, record[field][0],
|
||
format5)
|
||
except Exception as e:
|
||
if 'display_name' in record[field][0]:
|
||
sheet.write(row_num, col_num,
|
||
record[field].display_name, format5)
|
||
else:
|
||
sheet.write(row_num, col_num, "", format5)
|
||
col_num += 1
|
||
row_num += occupied_rows
|
||
sl_no += 1
|
||
workbook.close()
|
||
output.seek(0)
|
||
response.stream.write(output.read())
|
||
output.close()
|
||
|
||
def create_model_action(self):
|
||
""" Create a contextual action for each server action."""
|
||
self.action_button = True
|
||
WindowAction = self.env['ir.actions.act_window']
|
||
data = self.env['ir.model.data']
|
||
for rec in self.browse(self._ids):
|
||
binding_model_id = rec.model_id.id
|
||
model_data_id = data._load_xmlid('excel_report_designer')
|
||
res_id = data.browse(model_data_id).res_id
|
||
button_name = _('打印报告 (%s)') % rec.name
|
||
act_id = WindowAction.create({
|
||
'name': button_name,
|
||
'type': 'ir.actions.act_window',
|
||
'res_model': 'excel.report.wizards',
|
||
'binding_model_id': binding_model_id,
|
||
'context': "{'excel' : %d}" % (rec.id),
|
||
'view_mode': 'form,tree',
|
||
'view_id': res_id,
|
||
'target': 'new',
|
||
})
|
||
rec.write({
|
||
'ir_act_server_ref': act_id.id,
|
||
})
|
||
return True
|
||
|
||
def unlink_model_action(self):
|
||
""" Remove the contextual actions created for the server actions. """
|
||
self.action_button = False
|
||
self.check_access_rights('write', raise_exception=True)
|
||
self.filtered('binding_model_id').write({'binding_model_id': False})
|
||
self.ir_act_server_ref.unlink()
|
||
return {
|
||
'type': 'ir.actions.client',
|
||
'tag': 'reload',
|
||
}
|
||
|
||
@api.onchange('fields_ids')
|
||
def onchange_fields(self):
|
||
"""this is used to find the fields of new models"""
|
||
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'
|
||
})
|
||
# 定义备注格式
|
||
note_format = workbook.add_format({
|
||
'font_size': 10,
|
||
'border': 1,
|
||
'align': 'left',
|
||
'valign': 'top',
|
||
'text_wrap': True
|
||
})
|
||
# 定义审批字段格式
|
||
approval_format = workbook.add_format({
|
||
'font_size': 10,
|
||
'border': 1,
|
||
'align': 'center',
|
||
'valign': 'vcenter'
|
||
})
|
||
# 定义靠右无边框显示的格式
|
||
right_align_format = workbook.add_format({
|
||
'font_size': 10,
|
||
'align': 'right',
|
||
'valign': 'vcenter'
|
||
})
|
||
# 定义靠右有边框显示的格式
|
||
right_format = workbook.add_format({
|
||
'font_size': 10,
|
||
'align': 'right',
|
||
'valign': 'vcenter',
|
||
'border': 1,
|
||
})
|
||
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])
|
||
# 写入公司和年月信息
|
||
headers = ['序号'] + data['field_label']
|
||
columns_data = [[] for _ in range(len(headers) - 1)]
|
||
float_totals = [0] * (len(headers) - 1)
|
||
|
||
current_year = int(data['current_year'])
|
||
current_month = int(data['current_month'])
|
||
valid_columns = []
|
||
for index, field_name in enumerate(data['fields_name']):
|
||
field = self.env[data['model_name']]._fields.get(field_name)
|
||
if not field:
|
||
continue
|
||
# 检查字段是否存在于 yuthon.payroll.common 和 yuthon.payroll.temporary 模型中
|
||
common_model = self.env['yuthon.payroll.common']
|
||
temp_model = self.env['yuthon.payroll.temporary']
|
||
common_records = common_model.search([('name', '=', field.string)])
|
||
temp_records = temp_model.search([('name', '=', field.string)])
|
||
if not common_records and not temp_records:
|
||
# 若两个模型中都没找到该字段,跳过日期判断,直接认为有效
|
||
valid_columns.append(index)
|
||
continue
|
||
|
||
valid = False
|
||
if field.type == 'float':
|
||
# 分别处理 common_records
|
||
for rec in common_records:
|
||
start_date = rec.start_date
|
||
end_date = rec.end_date
|
||
if not end_date:
|
||
end_date = datetime.date(current_year + 99, current_month, 1)
|
||
if start_date and end_date:
|
||
start_year_month = (start_date.year, start_date.month)
|
||
end_year_month = (end_date.year, end_date.month)
|
||
current_year_month = (current_year, current_month)
|
||
if start_year_month <= current_year_month <= end_year_month:
|
||
valid = True
|
||
break
|
||
# 如果 common_records 未找到有效记录,再处理 temp_records
|
||
if not valid:
|
||
for rec in temp_records:
|
||
start_date = rec.start_date
|
||
end_date = rec.end_date
|
||
if not end_date:
|
||
end_date = datetime.date(current_year + 99, current_month, 1)
|
||
if start_date and end_date:
|
||
start_year_month = (start_date.year, start_date.month)
|
||
end_year_month = (end_date.year, end_date.month)
|
||
current_year_month = (current_year, current_month)
|
||
if start_year_month <= current_year_month <= end_year_month:
|
||
valid = True
|
||
break
|
||
else:
|
||
valid = True
|
||
|
||
if valid:
|
||
valid_columns.append(index)
|
||
|
||
for record in group_records:
|
||
col_num = 1
|
||
for index, field_name in enumerate(data['fields_name']):
|
||
if index in valid_columns and index < len(columns_data):
|
||
value = record[field_name]
|
||
columns_data[index].append(value)
|
||
field = self.env[data['model_name']]._fields.get(field_name)
|
||
if field and field.type == 'float':
|
||
float_totals[index] += float(value) if value else 0
|
||
col_num += 1
|
||
|
||
# 记录实际显示的列索引
|
||
visible_cols = [0]
|
||
first_float_col_index = None
|
||
for index in range(len(headers) - 1):
|
||
if index in valid_columns and not all([x == 0 for x in columns_data[index]]):
|
||
visible_cols.append(index + 1)
|
||
if first_float_col_index is None and self.env[data['model_name']]._fields[
|
||
data['fields_name'][index]].type == 'float':
|
||
first_float_col_index = len(visible_cols) - 1
|
||
actual_header_length = len(visible_cols)
|
||
|
||
# 写入大表头,根据实际显示列数确定合并范围
|
||
company_year_month_text = f'员工{data["current_year"]}年{data["current_month"]}月工资表({company_name})'
|
||
sheet.merge_range(0, 0, 0, actual_header_length - 1, company_year_month_text, title_format)
|
||
|
||
# 添加单位和日期
|
||
current_date_str = datetime.datetime.now().strftime('%Y年%m月%d日')
|
||
sheet.write(1, actual_header_length - 1, f"单位:元 日期:{current_date_str}", right_align_format)
|
||
|
||
# 写入表头
|
||
col_num = 0
|
||
for index in visible_cols:
|
||
sheet.write(2, col_num, headers[index], header_format)
|
||
col_num += 1
|
||
|
||
# 写入数据
|
||
row_num = 3
|
||
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 index in visible_cols[1:]:
|
||
field_name = data['fields_name'][index - 1]
|
||
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 # 序号递增
|
||
|
||
if first_float_col_index is not None:
|
||
# 合并从首列到首个float类型字段所在列的前一列的单元格
|
||
sheet.merge_range(row_num, 0, row_num, first_float_col_index - 1, "总计", serial_number_format)
|
||
# 调整起始列索引
|
||
start_col = first_float_col_index
|
||
for col_index, visible_col in enumerate(visible_cols[first_float_col_index:]):
|
||
field_name = data['fields_name'][visible_col - 1]
|
||
field = self.env[data['model_name']]._fields.get(field_name)
|
||
if field and field.type == 'float':
|
||
# 计算正确的列索引
|
||
target_col = start_col + col_index
|
||
sheet.write(row_num, target_col, float_totals[visible_col - 1], right_format)
|
||
else:
|
||
# 计算正确的列索引
|
||
target_col = start_col + col_index
|
||
sheet.write(row_num, target_col, "", right_format)
|
||
|
||
# 写入备注,根据实际显示列数确定合并范围
|
||
start_row = row_num + 1
|
||
end_row = start_row + 4
|
||
note_text = "备注"
|
||
sheet.merge_range(start_row, 0, end_row, actual_header_length - 1, note_text, note_format)
|
||
|
||
col_num = 0
|
||
# 记录表头实际占据的列数
|
||
header_column_count = len(visible_cols)
|
||
for index in visible_cols:
|
||
sheet.write(2, col_num, headers[index], header_format)
|
||
col_num += 1
|
||
|
||
# 添加审批字段行
|
||
approval_row = end_row + 1
|
||
approval_fields = ["领导审批:", "财务审核:", "部门审核:", "制表:"]
|
||
approval_text = " ".join(approval_fields)
|
||
|
||
# 合并单元格,使其宽度和表头一致
|
||
sheet.merge_range(approval_row, 0, approval_row, header_column_count - 1, approval_text, approval_format)
|
||
|
||
# 创建劳务信息工作表
|
||
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)
|
||
# 添加单位和日期信息
|
||
current_time = datetime.datetime.now().strftime('%Y-%m-%d')
|
||
info_text = f"单位:元 日期:{current_time}"
|
||
labor_sheet.merge_range(1, 0, 1, len(labor_headers) - 1, info_text, right_align_format)
|
||
# 写入表头
|
||
col_num = 0
|
||
for label in labor_headers:
|
||
labor_sheet.write(2, 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 = 3
|
||
serial_number = 1
|
||
column_totals = [0] * len(labor_headers)
|
||
first_float_col_index = None
|
||
|
||
# 找出首个 float 类型字段所在列的索引
|
||
for col_index, field_name in enumerate(['serial_number'] + labor_fields):
|
||
field = self.env['yuthon.payroll.labor']._fields.get(field_name) if field_name in labor_fields else None
|
||
if field and field.type == 'float':
|
||
first_float_col_index = col_index
|
||
break
|
||
|
||
for labor_record in labor_records:
|
||
col_num = 0
|
||
labor_sheet.write(row_num, col_num, serial_number, serial_number_format)
|
||
if first_float_col_index is not None and col_num >= first_float_col_index:
|
||
try:
|
||
column_totals[col_num] += float(serial_number)
|
||
except (ValueError, TypeError):
|
||
pass
|
||
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)
|
||
if first_float_col_index is not None and col_num >= first_float_col_index:
|
||
try:
|
||
column_totals[col_num] += float(value)
|
||
except (ValueError, TypeError):
|
||
pass
|
||
col_num += 1
|
||
row_num += 1
|
||
serial_number += 1
|
||
# 写入总计行
|
||
total_row = row_num
|
||
if first_float_col_index is not None:
|
||
labor_sheet.merge_range(total_row, 0, total_row, first_float_col_index - 1, '合计', serial_number_format)
|
||
for col_num in range(first_float_col_index, len(labor_headers)):
|
||
labor_sheet.write(total_row, col_num, column_totals[col_num], data_format)
|
||
|
||
end_row = total_row # 假设 end_row 是总计行
|
||
approval_row = end_row + 1
|
||
approval_fields = ["领导审批:", "财务审核:", "部门审核:", "制表:"]
|
||
approval_text = " ".join(approval_fields)
|
||
|
||
# 合并单元格,使其宽度和表头一致
|
||
labor_sheet.merge_range(approval_row, 0, approval_row, len(labor_headers) - 1, approval_text, approval_format)
|
||
|
||
# 创建退休人员费用工作表
|
||
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)
|
||
# 添加单位和日期信息
|
||
current_time = datetime.datetime.now().strftime('%Y-%m-%d')
|
||
info_text = f"单位:元 日期:{current_time}"
|
||
retired_sheet.merge_range(1, 0, 1, len(retired_headers) - 1, info_text, right_align_format)
|
||
# 写入表头
|
||
col_num = 0
|
||
for label in retired_headers:
|
||
retired_sheet.write(2, 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 = 3
|
||
serial_number = 1
|
||
column_totals = [0] * len(retired_headers)
|
||
first_float_col_index = None
|
||
|
||
for col_index, field_name in enumerate(['serial_number'] + retired_fields):
|
||
field = self.env['yuthon.payroll.retired']._fields.get(field_name) if field_name in retired_fields else None
|
||
if field and field.type == 'float':
|
||
first_float_col_index = col_index
|
||
break
|
||
|
||
for retired_record in retired_records:
|
||
col_num = 0
|
||
retired_sheet.write(row_num, col_num, serial_number, serial_number_format)
|
||
if first_float_col_index is not None and col_num >= first_float_col_index:
|
||
try:
|
||
column_totals[col_num] += float(serial_number)
|
||
except (ValueError, TypeError):
|
||
pass
|
||
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)
|
||
if first_float_col_index is not None and col_num >= first_float_col_index:
|
||
try:
|
||
column_totals[col_num] += float(value)
|
||
except (ValueError, TypeError):
|
||
pass
|
||
col_num += 1
|
||
row_num += 1
|
||
serial_number += 1
|
||
# 写入总计行
|
||
total_row = row_num
|
||
if first_float_col_index is not None:
|
||
retired_sheet.merge_range(total_row, 0, total_row, first_float_col_index - 1, '合计', serial_number_format)
|
||
for col_num in range(first_float_col_index, len(retired_headers)):
|
||
retired_sheet.write(total_row, col_num, column_totals[col_num], data_format)
|
||
|
||
end_row = total_row # 假设 end_row 是总计行
|
||
approval_row = end_row + 1
|
||
approval_fields = ["领导审批:", "财务审核:", "部门审核:", "制表:"]
|
||
approval_text = " ".join(approval_fields)
|
||
|
||
# 合并单元格,使其宽度和表头一致
|
||
retired_sheet.merge_range(approval_row, 0, approval_row, len(labor_headers) - 1, approval_text, approval_format)
|
||
|
||
# 关闭工作簿
|
||
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', '=', False),
|
||
('payslip_run_id.name', '=', target_name)
|
||
]
|
||
|
||
def get_xlsx_leave_report(self, data, response):
|
||
"""请假情况报表的自定义 Excel 样式"""
|
||
output = io.BytesIO()
|
||
workbook = xlsxwriter.Workbook(output, {'in_memory': True})
|
||
sheet = workbook.add_worksheet('请假情况')
|
||
|
||
# 表头格式:加粗、居中、带边框、背景色
|
||
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, 12)
|
||
elif col == 1: # 部门
|
||
sheet.set_column(col, col, 15)
|
||
elif col == 3: # 标题
|
||
sheet.set_column(col, col, 30)
|
||
elif col == 7: # 申请原因
|
||
sheet.set_column(col, col, 40)
|
||
else:
|
||
sheet.set_column(col, col, 15)
|
||
col += 1
|
||
|
||
# 取记录:优先用 active_ids
|
||
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):
|
||
# datetime 类型:需要从 UTC 转换到用户时区
|
||
if value:
|
||
from pytz import UTC, timezone
|
||
tz = self.env.user.tz or 'Asia/Shanghai'
|
||
user_tz = timezone(tz)
|
||
value_utc = UTC.localize(value) if value.tzinfo is None else value
|
||
value_local = value_utc.astimezone(user_tz)
|
||
# 移除时区信息后写入 Excel
|
||
sheet.write_datetime(row, col, value_local.replace(tzinfo=None), datetime_format)
|
||
else:
|
||
sheet.write(row, col, '', data_format)
|
||
elif isinstance(value, datetime.date):
|
||
# 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':
|
||
# 浮点数保留2位小数
|
||
sheet.write(row, col, value or 0, data_format)
|
||
else:
|
||
sheet.write(row, col, value or '', data_format)
|
||
else:
|
||
sheet.write(row, col, value or '', data_format)
|
||
col += 1
|
||
row += 1
|
||
|
||
# 给整表加筛选(类似 Excel 的筛选下拉箭头)
|
||
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()
|
||
|
||
def get_xlsx_overtime_detail_report(self, data, response):
|
||
"""加班详情报表的自定义 Excel 样式"""
|
||
output = io.BytesIO()
|
||
workbook = xlsxwriter.Workbook(output, {'in_memory': True})
|
||
sheet = workbook.add_worksheet('加班详情')
|
||
|
||
# 获取当前用户的时区(用于 datetime 转换)
|
||
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
|
||
|
||
# 取记录:优先用 active_ids
|
||
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):
|
||
# datetime 类型:需要从 UTC 转换到用户时区
|
||
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):
|
||
# 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':
|
||
# 浮点数保留2位小数
|
||
sheet.write(row, col, value or 0, data_format)
|
||
elif field.type == 'text':
|
||
# 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()
|
||
|
||
def get_xlsx_overtime_report(self, data, response):
|
||
"""加班明细报表的自定义 Excel 样式"""
|
||
output = io.BytesIO()
|
||
workbook = xlsxwriter.Workbook(output, {'in_memory': True})
|
||
sheet = workbook.add_worksheet('加班明细')
|
||
|
||
# 获取当前用户的时区(用于 datetime 转换)
|
||
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, 20)
|
||
else:
|
||
sheet.set_column(col, col, 15)
|
||
col += 1
|
||
|
||
# 取记录:优先用 active_ids
|
||
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):
|
||
# datetime 类型:需要从 UTC 转换到用户时区
|
||
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)
|
||
# 移除时区信息后写入 Excel
|
||
sheet.write_datetime(row, col, value_local.replace(tzinfo=None), datetime_format)
|
||
else:
|
||
sheet.write(row, col, '', data_format)
|
||
elif isinstance(value, datetime.date):
|
||
# 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':
|
||
# 浮点数保留2位小数
|
||
sheet.write(row, col, value or 0, data_format)
|
||
else:
|
||
sheet.write(row, col, value or '', data_format)
|
||
else:
|
||
sheet.write(row, col, value or '', data_format)
|
||
col += 1
|
||
row += 1
|
||
|
||
# 给整表加筛选(类似 Excel 的筛选下拉箭头)
|
||
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()
|
||
|
||
def _export_overtime_active(self, start_date, end_date):
|
||
"""导出加班归档明细"""
|
||
active_ids = json.loads(self.active_ids) if self.active_ids else []
|
||
domain = [
|
||
('carry_forward_date', '>=', start_date),
|
||
('carry_forward_date', '<', end_date),
|
||
('is_void', '=', False),
|
||
('carryover_type', '=', 'fee')
|
||
]
|
||
if active_ids:
|
||
domain = [('id', 'in', active_ids)] + domain
|
||
filtered_records = self.env['overtime.carryover'].search(domain)
|
||
|
||
if not filtered_records:
|
||
raise UserError(f'在 {self.year} 年 {self.month} 月没有找到符合条件的加班归档记录')
|
||
|
||
export_data = []
|
||
for record in filtered_records:
|
||
over_line = record.over_line_id
|
||
# 提前处理时区转换,避免在Excel函数中重复处理
|
||
tz = self.env.user.tz or 'Asia/Shanghai'
|
||
user_tz = timezone(tz)
|
||
|
||
# 处理开始时间时区转换
|
||
start_time = ''
|
||
if over_line and over_line.start_time:
|
||
start_time_utc = UTC.localize(
|
||
over_line.start_time) if over_line.start_time.tzinfo is None else over_line.start_time
|
||
start_time = start_time_utc.astimezone(user_tz).replace(tzinfo=None)
|
||
|
||
# 处理结束时间时区转换
|
||
end_time = ''
|
||
if over_line and over_line.end_time:
|
||
end_time_utc = UTC.localize(
|
||
over_line.end_time) if over_line.end_time.tzinfo is None else over_line.end_time
|
||
end_time = end_time_utc.astimezone(user_tz).replace(tzinfo=None)
|
||
|
||
row_data = {
|
||
'employee_id': over_line.employee_id.name if over_line and over_line.employee_id else '',
|
||
'department_id': over_line.department_id.name if over_line and over_line.department_id else '',
|
||
'start_time': start_time,
|
||
'end_time': end_time,
|
||
'carry_forward_date': record.carry_forward_date,
|
||
'carry_forward_quantity': record.carry_forward_quantity or 0,
|
||
'carry_forward_state': dict(record._fields.get('carry_forward_state').selection).get(
|
||
record.carry_forward_state, '') if hasattr(record, 'carry_forward_state') else ''
|
||
}
|
||
export_data.append(row_data)
|
||
|
||
fields_name = ['employee_id', 'department_id', 'start_time', 'end_time',
|
||
'carry_forward_date', 'carry_forward_quantity', 'carry_forward_state']
|
||
field_label = ['姓名', '部门', '加班开始时间', '加班结束时间', '结转日期',
|
||
'结转时长', '结转结果']
|
||
|
||
data = {
|
||
'report_name': '加班归档明细报表',
|
||
'model_name': 'overtime.carryover',
|
||
'fields_name': fields_name,
|
||
'field_label': field_label,
|
||
'export_data': export_data, # 传递预处理好的数据
|
||
}
|
||
|
||
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',
|
||
}
|
||
|
||
def get_xlsx_overtime_active_report(self, data, response):
|
||
"""加班归档明细报表的自定义 Excel 样式"""
|
||
import io
|
||
import xlsxwriter
|
||
from pytz import timezone, UTC
|
||
from datetime import datetime, date
|
||
|
||
output = io.BytesIO()
|
||
workbook = xlsxwriter.Workbook(output, {'in_memory': True})
|
||
sheet = workbook.add_worksheet('加班归档明细')
|
||
|
||
# 获取当前用户的时区
|
||
tz = self.env.user.tz or 'Asia/Shanghai'
|
||
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 in [2, 3]: # 开始/结束时间
|
||
sheet.set_column(col, col, 20)
|
||
else:
|
||
sheet.set_column(col, col, 15)
|
||
col += 1
|
||
|
||
# ========== 核心修复:使用预处理的 export_data ==========
|
||
records_data = data.get('export_data', [])
|
||
|
||
# 写数据行
|
||
row = 1
|
||
for rec_data in records_data:
|
||
col = 0
|
||
for field_name in data['fields_name']:
|
||
value = rec_data.get(field_name, '')
|
||
|
||
# 根据值类型应用对应格式
|
||
if isinstance(value, datetime):
|
||
sheet.write_datetime(row, col, value, datetime_format)
|
||
elif isinstance(value, date):
|
||
sheet.write_datetime(row, col, value, date_format)
|
||
elif isinstance(value, float):
|
||
sheet.write(row, col, value, 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()
|
||
|
||
def get_xlsx_overtime_detail_report(self, data, response):
|
||
"""加班详情报表的自定义 Excel 样式 - 对应 vacation.over.line.overtime.tree 视图"""
|
||
import io
|
||
import xlsxwriter
|
||
from datetime import datetime, date
|
||
|
||
output = io.BytesIO()
|
||
workbook = xlsxwriter.Workbook(output, {'in_memory': True})
|
||
sheet = workbook.add_worksheet('加班详情')
|
||
|
||
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_widths = [40, 12, 18, 20, 20, 8, 10, 12, 12, 10, 15]
|
||
col = 0
|
||
for label in data['field_label']:
|
||
sheet.write(0, col, label, header_format)
|
||
width = col_widths[col] if col < len(col_widths) else 15
|
||
sheet.set_column(col, col, width)
|
||
col += 1
|
||
|
||
records_data = data.get('export_data', [])
|
||
row = 1
|
||
for rec_data in records_data:
|
||
col = 0
|
||
for field_name in data['fields_name']:
|
||
value = rec_data.get(field_name, '')
|
||
if isinstance(value, datetime):
|
||
sheet.write_datetime(row, col, value, datetime_format)
|
||
elif isinstance(value, date):
|
||
sheet.write_datetime(row, col, value, date_format)
|
||
elif isinstance(value, float):
|
||
sheet.write(row, col, value, 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()
|
||
|
||
def get_xlsx_overtime_carryover_report(self, data, response):
|
||
"""加班结转明细报表的自定义 Excel 样式"""
|
||
import io
|
||
import xlsxwriter
|
||
from datetime import datetime, date
|
||
|
||
output = io.BytesIO()
|
||
workbook = xlsxwriter.Workbook(output, {'in_memory': True})
|
||
sheet = workbook.add_worksheet('加班结转明细')
|
||
|
||
# 表头格式
|
||
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 in [2, 3]: # 开始/结束时间
|
||
sheet.set_column(col, col, 20)
|
||
else:
|
||
sheet.set_column(col, col, 15)
|
||
col += 1
|
||
|
||
# 写数据行(使用预处理的 export_data)
|
||
records_data = data.get('export_data', [])
|
||
row = 1
|
||
for rec_data in records_data:
|
||
col = 0
|
||
for field_name in data['fields_name']:
|
||
value = rec_data.get(field_name, '')
|
||
if isinstance(value, datetime):
|
||
sheet.write_datetime(row, col, value, datetime_format)
|
||
elif isinstance(value, date):
|
||
sheet.write_datetime(row, col, value, date_format)
|
||
elif isinstance(value, float):
|
||
sheet.write(row, col, value, 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()
|
||
|
||
def get_xlsx_meal_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()
|
||
|
||
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']:
|
||
# 特殊处理:vacation.meal.active 没有 department_id,通过 employee_id 获取
|
||
if field_name == 'department_id' and 'department_id' not in rec._fields:
|
||
value = rec.employee_id.department_id.display_name if rec.employee_id and rec.employee_id.department_id else ''
|
||
sheet.write(row, col, value, data_format)
|
||
col += 1
|
||
continue
|
||
|
||
field = rec._fields.get(field_name)
|
||
if not field:
|
||
sheet.write(row, col, '', data_format)
|
||
col += 1
|
||
continue
|
||
|
||
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()
|