# -*- coding: utf-8 -*- ################################################################################ # # Cybrosys Technologies Pvt. Ltd. # # Copyright (C) 2024-TODAY Cybrosys Technologies (). # Author: Cybrosys Techno Solutions () # # 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 . # ################################################################################ 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) ]