diff --git a/SumasenLibs/excel_lib/requirements.txt b/SumasenLibs/excel_lib/requirements.txt index 8938e66..60c70be 100644 --- a/SumasenLibs/excel_lib/requirements.txt +++ b/SumasenLibs/excel_lib/requirements.txt @@ -3,3 +3,4 @@ pandas>=1.0.0 pillow>=8.0.0 configparser>=5.0.0 psycopg2-binary==2.9.9 +requests diff --git a/SumasenLibs/excel_lib/sumaexcel/.sumaexcel.py.swp b/SumasenLibs/excel_lib/sumaexcel/.sumaexcel.py.swp deleted file mode 100644 index d9fd27f..0000000 Binary files a/SumasenLibs/excel_lib/sumaexcel/.sumaexcel.py.swp and /dev/null differ diff --git a/SumasenLibs/excel_lib/sumaexcel/sumaexcel.py b/SumasenLibs/excel_lib/sumaexcel/sumaexcel.py index c977537..3db4b40 100644 --- a/SumasenLibs/excel_lib/sumaexcel/sumaexcel.py +++ b/SumasenLibs/excel_lib/sumaexcel/sumaexcel.py @@ -24,6 +24,14 @@ from .config_handler import ConfigHandler # ini file のロード #from .conditional import ConditionalFormatManager from .page import PageManager, PaperSizes +from datetime import datetime +import pytz +from urllib.parse import urlparse +import requests +from io import BytesIO +from PIL import Image +import re + import logging logging.basicConfig( @@ -165,9 +173,18 @@ class SumasenExcel: return {"status": False, "message": f"Error no template sheet found: {template_sheet_name}"} # シートの名前を設定 - new_sheet = self.workbook.create_sheet(title=section_config.get("sheet_name", section)) - self.worksheet = new_sheet + new_sheet_name = section_config.get("sheet_name", section) + # Create new sheet with template name if it doesn't exist + if new_sheet_name not in self.workbook.sheetnames: + self.current_sheet = self.workbook.create_sheet(new_sheet_name) + else: + self.current_sheet = self.workbook[new_sheet_name] + + # Remove default sheet if it exists + if 'Sheet' in self.workbook.sheetnames: + del self.workbook['Sheet'] + self.dbname=variables.get('db') self.user=variables.get('username') self.password=variables.get('password') @@ -195,12 +212,17 @@ class SumasenExcel: # シートの幅を設定 fit_to_width = section_config.get("fit_to_width") if fit_to_width: - new_sheet.sheet_view.zoomScaleNormal = float(fit_to_width) + self.current_sheet.sheet_view.zoomScaleNormal = float(fit_to_width) # シートの向きを設定 orientation = section_config.get("orientation") - new_sheet.sheet_view.orientation = orientation if orientation else "portrait" - self.current_worksheet = new_sheet + self.current_sheet.sheet_view.orientation = orientation if orientation else "portrait" + + max_col = self.basic.get("maxcol",20) + if not max_col: + return {"status": False, "message": f"Error no maxcol found: basic"} + #self.set_column_width(1,int(max_col)) + self.set_column_width_from_config(self.current_sheet) # グループ定義を取得 groups = section_config.get("groups") @@ -253,6 +275,78 @@ class SumasenExcel: logging.error(f"Error in proceed_group: {str(e)}") return {"status": False, "message": f"Exception in proceed_group : Error generating report: {str(e)}"} + def format_cell_value(self,field_value, cell): + """セルの値を適切な形式に変換する + + Args: + field_value: DBから取得した値 + cell: 対象のExcelセル + + Returns: + 変換後の値 + """ + # Noneの場合は空文字を返す + if field_value is None: + return "" + + # 真偽値の場合 + if isinstance(field_value, bool): + return "OK" if field_value else "-" + + # 日時型の場合 + if isinstance(field_value, datetime): + jst = pytz.timezone('Asia/Tokyo') + # UTC -> JST変換 + jst_time = field_value.astimezone(jst) + return jst_time.strftime('%H:%M:%S') + + # 文字列の場合、URLかどうかをチェック + if isinstance(field_value, str): + # URL形式かどうかチェック + try: + result = urlparse(field_value) + # URLで、かつ画像ファイルの拡張子を持つ場合 + if all([result.scheme, result.netloc]) and \ + re.search(r'\.(jpg|jpeg|png|gif|bmp)$', result.path, re.I): + try: + # 画像をダウンロード + response = requests.get(field_value) + img = Image.open(BytesIO(response.content)) + + # セルの大きさを取得(ピクセル単位) + cell_width = cell.column_dimensions.width * 6 # 概算のピクセル変換 + cell_height = cell.row_dimensions.height + + # アスペクト比を保持しながらリサイズ + img_aspect = img.width / img.height + cell_aspect = cell_width / cell_height + + if img_aspect > cell_aspect: + width = int(cell_width) + height = int(width / img_aspect) + else: + height = int(cell_height) + width = int(height * img_aspect) + + # 画像をリサイズしてセルに追加 + img = img.resize((width, height)) + + # 画像をセルに追加(実装方法はExcelライブラリに依存) + if hasattr(cell, 'add_image'): # 使用するライブラリによって適切なメソッドを使用 + cell.add_image(img) + return "" # 画像を追加したので、テキスト値は空にする + except Exception as e: + logging.warning(f"画像の処理に失敗: {str(e)}") + return field_value # エラーの場合はURLをそのまま返す + except Exception as e: + logging.warning(f"形式変換の処理に失敗: {str(e)}") + return field_value # エラーの場合はURLをそのまま返す + + # その他の場合は文字列に変換 + return str(field_value) + + + def proceed_one_record(self,table:str,where:str,group_range:str,variables: Dict[str, Any]): """1レコードのデータを取得してシートの値を置き換える @@ -268,6 +362,7 @@ class SumasenExcel: # まずself.template_sheetの指定範囲のセルをself.current_sheetにコピーする。 self.copy_template_to_current(group_range,group_range) + print(f"step-1") cursor = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor) @@ -280,7 +375,7 @@ class SumasenExcel: print(f"record={record}") if record: # group_rangeの範囲内のセルを走査 - for row in self.current_worksheet: + for row in self.current_sheet: for cell in row: if cell.value and isinstance(cell.value, str): # [field_name]形式の文字列を検索 @@ -292,12 +387,14 @@ class SumasenExcel: new_value = cell.value for field_name in matches: if field_name in record: + # 新しい形式変換関数を使用 + formatted_value = self.format_cell_value(record[field_name], cell) new_value = new_value.replace( - f'[{field_name}]', - str(record[field_name]) - ) + f'[{field_name}]', + formatted_value + ) cell.value = new_value - + cursor.close() return {"status": True, "message": f"Success generating group: "} @@ -378,7 +475,7 @@ class SumasenExcel: # コピーした範囲内のセルを走査して値を置換 for row in range(current_row, current_row + template_rows): - for cell in self.current_worksheet[row]: + for cell in self.current_sheet[row]: if cell.value and isinstance(cell.value, str): # [field_name]形式の文字列を検索 import re @@ -389,9 +486,11 @@ class SumasenExcel: new_value = cell.value for field_name in matches: if field_name in record: + # 新しい形式変換関数を使用 + formatted_value = self.format_cell_value(record[field_name], cell) new_value = new_value.replace( - f'[{field_name}]', - str(record[field_name]) + f'[{field_name}]', + formatted_value ) cell.value = new_value @@ -405,40 +504,224 @@ class SumasenExcel: logging.error(f"Error in proceed_all_record: {str(e)}") return {"status": False, "message": f"Exception in proceed_all_record:Error processing records: {str(e)}"} + + def set_column_width_from_config(self, worksheet): + """ + INIファイルの設定に基づいて列幅を設定する + + Parameters: + worksheet: 設定対象のワークシート + + Returns: + dict: 処理結果を示す辞書 + """ + try: + section_config = self.conf.get_section('basic') + + # basic セクションから column_width を読み取る + if not section_config.get('column_width'): + raise ValueError("column_width setting not found in [basic] section") + + # カンマ区切りの文字列を数値のリストに変換 + width_str = section_config.get('column_width') + widths = [float(w.strip()) for w in width_str.split(',')] + + # 各列に幅を設定 + for col_index, width in enumerate(widths, start=1): + col_letter = get_column_letter(col_index) + worksheet.column_dimensions[col_letter].width = width + logging.info(f"Set column {col_letter} width to {width}") + + return { + "status": True, + "message": "Column widths set successfully from config", + "details": { + "num_columns": len(widths), + "widths": widths + } + } + + except ValueError as ve: + logging.error(f"Invalid column width value in config: {str(ve)}") + return { + "status": False, + "message": f"Invalid column width configuration: {str(ve)}" + } + except Exception as e: + logging.error(f"Error setting column widths: {str(e)}") + return { + "status": False, + "message": f"Error setting column widths: {str(e)}" + } + + def verify_column_widths(self, worksheet, expected_widths=None): + """ + 列幅が正しく設定されているか検証する + + Parameters: + worksheet: 検証対象のワークシート + expected_widths: 期待される幅のリスト(指定がない場合はINIファイルから読み取り) + + Returns: + list: 検証結果のリスト + """ + if expected_widths is None: + width_str = self.config.get('basic', 'column_width') + expected_widths = [float(w.strip()) for w in width_str.split(',')] + + verification_results = [] + for col_index, expected_width in enumerate(expected_widths, start=1): + col_letter = get_column_letter(col_index) + actual_width = worksheet.column_dimensions[col_letter].width + + matches = abs(float(actual_width) - float(expected_width)) < 0.01 + verification_results.append({ + 'column': col_letter, + 'expected_width': expected_width, + 'actual_width': actual_width, + 'matches': matches + }) + + if not matches: + logging.warning( + f"Column {col_letter} width mismatch: " + f"expected={expected_width}, actual={actual_width}" + ) + + return verification_results + + def copy_template_range(self, orig_min_row, orig_min_col, orig_max_row, orig_max_col, target_min_row): + """ + テンプレートの範囲をコピーし、マージセルの罫線を正しく設定する + """ + try: + # マージセルの情報を保存 + merged_ranges = [] + + # マージセルをコピー + for merged_range in self.template_sheet.merged_cells: + min_col, min_row, max_col, max_row = range_boundaries(str(merged_range)) + + # コピー範囲と重なるマージセルをチェック + if (min_col >= orig_min_col and max_col <= orig_max_col and + min_row >= orig_min_row and max_row <= orig_max_row): + # ターゲットのマージ範囲を計算 + target_merge_min_row = target_min_row + (min_row - orig_min_row) + target_merge_max_row = target_min_row + (max_row - orig_min_row) + target_merge_range = f"{get_column_letter(min_col)}{target_merge_min_row}:" \ + f"{get_column_letter(max_col)}{target_merge_max_row}" + + # マージ情報を保存 + merged_ranges.append({ + 'range': target_merge_range, + 'min_col': min_col, + 'max_col': max_col, + 'min_row': target_merge_min_row, + 'max_row': target_merge_max_row, + 'source_cell': self.template_sheet.cell(row=min_row, column=min_col) + }) + + # セルをマージ + self.current_sheet.merge_cells(target_merge_range) + + # セルの内容とスタイルをコピー + row_offset = target_min_row - orig_min_row + for row in range(orig_min_row, orig_max_row + 1): + for col in range(orig_min_col, orig_max_col + 1): + source_cell = self.template_sheet.cell(row=row, column=col) + target_cell = self.current_sheet.cell(row=row+row_offset, column=col) + + # 値とスタイルをコピー + if source_cell.value is not None: + target_cell.value = source_cell.value + + if source_cell.has_style: + target_cell.font = copy(source_cell.font) + target_cell.fill = copy(source_cell.fill) + target_cell.number_format = source_cell.number_format + target_cell.protection = copy(source_cell.protection) + target_cell.alignment = copy(source_cell.alignment) + + # マージセルの罫線を設定 + for merge_info in merged_ranges: + source_cell = merge_info['source_cell'] + source_border = source_cell.border + + # マージ範囲内の各セルに罫線を設定 + for row in range(merge_info['min_row'], merge_info['max_row'] + 1): + for col in range(merge_info['min_col'], merge_info['max_col'] + 1): + target_cell = self.current_sheet.cell(row=row, column=col) + + # 新しい罫線スタイルを作成 + new_border = copy(source_border) + + # 範囲の端のセルの場合のみ、対応する辺の罫線を設定 + if col == merge_info['min_col']: # 左端 + new_border.left = copy(source_border.left) + if col == merge_info['max_col']: # 右端 + new_border.right = copy(source_border.right) + if row == merge_info['min_row']: # 上端 + new_border.top = copy(source_border.top) + if row == merge_info['max_row']: # 下端 + new_border.bottom = copy(source_border.bottom) + + target_cell.border = new_border + + return {"status": True, "message": "Range copied successfully with merged cell borders"} + + except Exception as e: + logging.error(f"Error copying template range: {str(e)}") + return {"status": False, "message": f"Error copying template range: {str(e)}"} + + def verify_merged_cell_borders(self, worksheet, merge_range): + """ + マージセルの罫線が正しく設定されているか検証する + """ + try: + min_col, min_row, max_col, max_row = range_boundaries(merge_range) + + border_status = { + 'top': [], + 'bottom': [], + 'left': [], + 'right': [] + } + + # 各辺の罫線をチェック + for row in range(min_row, max_row + 1): + for col in range(min_col, max_col + 1): + cell = worksheet.cell(row=row, column=col) + + if col == min_col: # 左辺 + border_status['left'].append(cell.border.left) + if col == max_col: # 右辺 + border_status['right'].append(cell.border.right) + if row == min_row: # 上辺 + border_status['top'].append(cell.border.top) + if row == max_row: # 下辺 + border_status['bottom'].append(cell.border.bottom) + + return border_status + + except Exception as e: + logging.error(f"Error verifying merged cell borders: {str(e)}") + return None + + + def copy_template_to_current(self, orig_range, target_range): try: - print(f"orig_rage={orig_range},target_range={target_range}") + #print(f"copy_template_to_current : orig_range={orig_range},target_range={target_range}") # 範囲をパースする orig_min_col, orig_min_row, orig_max_col, orig_max_row = range_boundaries(orig_range) target_min_col, target_min_row, target_max_col, target_max_row = range_boundaries(target_range) - print(f"min_col, min_row, max_col, max_row = {orig_min_col}, {orig_min_row}, {orig_max_col}, {orig_max_row}") - - print(f"min_col, min_row, max_col, max_row = {target_min_col}, {target_min_row}, {target_max_col}, {target_max_row}") + #print(f"min_col, min_row, max_col, max_row = {orig_min_col}, {orig_min_row}, {orig_max_col}, {orig_max_row}") # Get template sheet name from ini file section_config = self.conf.get_section(self.section_list[0]) # 現在の処理中のセクション template_sheet_name = section_config.get("template_sheet") - - if not template_sheet_name: - raise ValueError("Template sheet name not found in configuration") - - # Create new sheet with template name if it doesn't exist - if template_sheet_name not in self.workbook.sheetnames: - self.current_sheet = self.workbook.create_sheet(template_sheet_name) - else: - self.current_sheet = self.workbook[template_sheet_name] - - # Remove default sheet if it exists - if 'Sheet' in self.workbook.sheetnames: - del self.workbook['Sheet'] - - # Copy column widths - for col in range(orig_min_col, orig_max_col + 1): - col_letter = get_column_letter(col) - if col_letter in self.template_sheet.column_dimensions: - self.current_sheet.column_dimensions[col_letter].width = \ - self.template_sheet.column_dimensions[col_letter].width + new_sheet_name = section_config.get("sheet_name",template_sheet_name) # Copy row heights for row in range(orig_min_row, orig_max_row + 1): @@ -449,19 +732,24 @@ class SumasenExcel: if target_row not in self.current_sheet.row_dimensions: self.current_sheet.row_dimensions[target_row] = openpyxl.worksheet.dimensions.RowDimension(target_row) self.current_sheet.row_dimensions[target_row].height = source_height + #print(f"Row(target_row).height = {source_height}") # Copy merged cells - for merged_range in self.template_sheet.merged_cells: - min_col, min_row, max_col, max_row = range_boundaries(str(merged_range)) - # Check if merge range intersects with our copy range - if (min_col >= orig_min_col and max_col <= orig_max_col and - min_row >= orig_min_row and max_row <= orig_max_row): - # Calculate target merge range - target_merge_min_row = target_min_row + (min_row - orig_min_row) - target_merge_max_row = target_min_row + (max_row - orig_min_row) - target_merge_range = f"{get_column_letter(min_col)}{target_merge_min_row}:" \ - f"{get_column_letter(max_col)}{target_merge_max_row}" - self.current_sheet.merge_cells(target_merge_range) + self.copy_template_range(orig_min_row, orig_min_col, orig_max_row, orig_max_col, target_min_row) + + #for merged_range in self.template_sheet.merged_cells: + # min_col, min_row, max_col, max_row = range_boundaries(str(merged_range)) + # #print(f"Merge cell: min_col, min_row, max_col, max_row = {min_col}, {min_row}, {max_col}, {max_row}") + # # Check if merge range intersects with our copy range + # if (min_col >= orig_min_col and max_col <= orig_max_col and + # min_row >= orig_min_row and max_row <= orig_max_row): + # # Calculate target merge range + # target_merge_min_row = target_min_row + (min_row - orig_min_row) + # target_merge_max_row = target_min_row + (max_row - orig_min_row) + # target_merge_range = f"{get_column_letter(min_col)}{target_merge_min_row}:" \ + # f"{get_column_letter(max_col)}{target_merge_max_row}" + # self.current_sheet.merge_cells(target_merge_range) + # #print(f"Merge : {target_merge_range}") # Copy cell contents and styles row_offset = target_min_row - orig_min_row @@ -470,17 +758,18 @@ class SumasenExcel: source_cell = self.template_sheet.cell(row=row, column=col) target_cell = self.current_sheet.cell(row=row+row_offset, column=col) - # Copy value - target_cell.value = source_cell.value - - # Copy styles - if source_cell.has_style: - target_cell.font = copy(source_cell.font) - target_cell.border = copy(source_cell.border) - target_cell.fill = copy(source_cell.fill) - target_cell.number_format = source_cell.number_format - target_cell.protection = copy(source_cell.protection) - target_cell.alignment = copy(source_cell.alignment) + if source_cell.value: + # Copy value + target_cell.value = source_cell.value + #print(f"({col},{row}) : {target_cell.value}") + # Copy styles + if source_cell.has_style: + target_cell.font = copy(source_cell.font) + target_cell.border = copy(source_cell.border) + target_cell.fill = copy(source_cell.fill) + target_cell.number_format = source_cell.number_format + target_cell.protection = copy(source_cell.protection) + target_cell.alignment = copy(source_cell.alignment) # Copy page setup target_page_setup = self.current_sheet.page_setup diff --git a/SumasenLibs/excel_lib/testdata/certificate_5033.xlsx b/SumasenLibs/excel_lib/testdata/certificate_5033.xlsx index 492d82a..1a14c2b 100644 Binary files a/SumasenLibs/excel_lib/testdata/certificate_5033.xlsx and b/SumasenLibs/excel_lib/testdata/certificate_5033.xlsx differ diff --git a/SumasenLibs/excel_lib/testdata/certificate_template.xlsx b/SumasenLibs/excel_lib/testdata/certificate_template.xlsx index a5f7d3f..5c96695 100644 Binary files a/SumasenLibs/excel_lib/testdata/certificate_template.xlsx and b/SumasenLibs/excel_lib/testdata/certificate_template.xlsx differ diff --git a/SumasenLibs/excel_lib/testdata/test.ini b/SumasenLibs/excel_lib/testdata/test.ini index 9392f46..e59f05a 100644 --- a/SumasenLibs/excel_lib/testdata/test.ini +++ b/SumasenLibs/excel_lib/testdata/test.ini @@ -2,10 +2,12 @@ template_file=certificate_template.xlsx doc_file=certificate_[zekken_number].xlsx sections=section1 -maxcol=8 +maxcol=10 +column_width=3,5,16,16,16,16,16,8,8,12,3 [section1] template_sheet=certificate +sheet_name=certificate groups=group1,group2 fit_to_width=1 orientation=portrait @@ -13,12 +15,12 @@ orientation=portrait [section1.group1] table_name=mv_entry_details where=zekken_number='[zekken_number]' and event_name='[event_code]' -group_range=A1:H11 +group_range=A1:J12 [section1.group2] -table_name=gps_checkins +table_name=v_checkins_locations where=zekken_number='[zekken_number]' and event_code='[event_code]' sort=path_order -group_range=A12:H12 +group_range=A13:J13