Generate Excel dev stege final

This commit is contained in:
hayano
2024-11-06 18:26:16 +00:00
parent 7f4d37d40c
commit 6e472cf634
6 changed files with 355 additions and 63 deletions

View File

@ -3,3 +3,4 @@ pandas>=1.0.0
pillow>=8.0.0 pillow>=8.0.0
configparser>=5.0.0 configparser>=5.0.0
psycopg2-binary==2.9.9 psycopg2-binary==2.9.9
requests

View File

@ -24,6 +24,14 @@ from .config_handler import ConfigHandler # ini file のロード
#from .conditional import ConditionalFormatManager #from .conditional import ConditionalFormatManager
from .page import PageManager, PaperSizes 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 import logging
logging.basicConfig( logging.basicConfig(
@ -165,9 +173,18 @@ class SumasenExcel:
return {"status": False, "message": f"Error no template sheet found: {template_sheet_name}"} 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)) new_sheet_name = section_config.get("sheet_name", section)
self.worksheet = new_sheet # 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.dbname=variables.get('db')
self.user=variables.get('username') self.user=variables.get('username')
self.password=variables.get('password') self.password=variables.get('password')
@ -195,12 +212,17 @@ class SumasenExcel:
# シートの幅を設定 # シートの幅を設定
fit_to_width = section_config.get("fit_to_width") fit_to_width = section_config.get("fit_to_width")
if 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") orientation = section_config.get("orientation")
new_sheet.sheet_view.orientation = orientation if orientation else "portrait" self.current_sheet.sheet_view.orientation = orientation if orientation else "portrait"
self.current_worksheet = new_sheet
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") groups = section_config.get("groups")
@ -253,6 +275,78 @@ class SumasenExcel:
logging.error(f"Error in proceed_group: {str(e)}") logging.error(f"Error in proceed_group: {str(e)}")
return {"status": False, "message": f"Exception in proceed_group : Error generating report: {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]): def proceed_one_record(self,table:str,where:str,group_range:str,variables: Dict[str, Any]):
"""1レコードのデータを取得してシートの値を置き換える """1レコードのデータを取得してシートの値を置き換える
@ -268,6 +362,7 @@ class SumasenExcel:
# まずself.template_sheetの指定範囲のセルをself.current_sheetにコピーする。 # まずself.template_sheetの指定範囲のセルをself.current_sheetにコピーする。
self.copy_template_to_current(group_range,group_range) self.copy_template_to_current(group_range,group_range)
print(f"step-1") print(f"step-1")
cursor = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor) cursor = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
@ -280,7 +375,7 @@ class SumasenExcel:
print(f"record={record}") print(f"record={record}")
if record: if record:
# group_rangeの範囲内のセルを走査 # group_rangeの範囲内のセルを走査
for row in self.current_worksheet: for row in self.current_sheet:
for cell in row: for cell in row:
if cell.value and isinstance(cell.value, str): if cell.value and isinstance(cell.value, str):
# [field_name]形式の文字列を検索 # [field_name]形式の文字列を検索
@ -292,12 +387,14 @@ class SumasenExcel:
new_value = cell.value new_value = cell.value
for field_name in matches: for field_name in matches:
if field_name in record: if field_name in record:
# 新しい形式変換関数を使用
formatted_value = self.format_cell_value(record[field_name], cell)
new_value = new_value.replace( new_value = new_value.replace(
f'[{field_name}]', f'[{field_name}]',
str(record[field_name]) formatted_value
) )
cell.value = new_value cell.value = new_value
cursor.close() cursor.close()
return {"status": True, "message": f"Success generating group: "} 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 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): if cell.value and isinstance(cell.value, str):
# [field_name]形式の文字列を検索 # [field_name]形式の文字列を検索
import re import re
@ -389,9 +486,11 @@ class SumasenExcel:
new_value = cell.value new_value = cell.value
for field_name in matches: for field_name in matches:
if field_name in record: if field_name in record:
# 新しい形式変換関数を使用
formatted_value = self.format_cell_value(record[field_name], cell)
new_value = new_value.replace( new_value = new_value.replace(
f'[{field_name}]', f'[{field_name}]',
str(record[field_name]) formatted_value
) )
cell.value = new_value cell.value = new_value
@ -405,40 +504,224 @@ class SumasenExcel:
logging.error(f"Error in proceed_all_record: {str(e)}") 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)}"} 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): def copy_template_to_current(self, orig_range, target_range):
try: 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) 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) 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 = {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}")
# Get template sheet name from ini file # Get template sheet name from ini file
section_config = self.conf.get_section(self.section_list[0]) # 現在の処理中のセクション section_config = self.conf.get_section(self.section_list[0]) # 現在の処理中のセクション
template_sheet_name = section_config.get("template_sheet") template_sheet_name = section_config.get("template_sheet")
new_sheet_name = section_config.get("sheet_name",template_sheet_name)
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
# Copy row heights # Copy row heights
for row in range(orig_min_row, orig_max_row + 1): 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: 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] = openpyxl.worksheet.dimensions.RowDimension(target_row)
self.current_sheet.row_dimensions[target_row].height = source_height self.current_sheet.row_dimensions[target_row].height = source_height
#print(f"Row(target_row).height = {source_height}")
# Copy merged cells # Copy merged cells
for merged_range in self.template_sheet.merged_cells: self.copy_template_range(orig_min_row, orig_min_col, orig_max_row, orig_max_col, target_min_row)
min_col, min_row, max_col, max_row = range_boundaries(str(merged_range))
# Check if merge range intersects with our copy range #for merged_range in self.template_sheet.merged_cells:
if (min_col >= orig_min_col and max_col <= orig_max_col and # min_col, min_row, max_col, max_row = range_boundaries(str(merged_range))
min_row >= orig_min_row and max_row <= orig_max_row): # #print(f"Merge cell: min_col, min_row, max_col, max_row = {min_col}, {min_row}, {max_col}, {max_row}")
# Calculate target merge range # # Check if merge range intersects with our copy range
target_merge_min_row = target_min_row + (min_row - orig_min_row) # if (min_col >= orig_min_col and max_col <= orig_max_col and
target_merge_max_row = target_min_row + (max_row - orig_min_row) # min_row >= orig_min_row and max_row <= orig_max_row):
target_merge_range = f"{get_column_letter(min_col)}{target_merge_min_row}:" \ # # Calculate target merge range
f"{get_column_letter(max_col)}{target_merge_max_row}" # target_merge_min_row = target_min_row + (min_row - orig_min_row)
self.current_sheet.merge_cells(target_merge_range) # 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 # Copy cell contents and styles
row_offset = target_min_row - orig_min_row row_offset = target_min_row - orig_min_row
@ -470,17 +758,18 @@ class SumasenExcel:
source_cell = self.template_sheet.cell(row=row, column=col) source_cell = self.template_sheet.cell(row=row, column=col)
target_cell = self.current_sheet.cell(row=row+row_offset, column=col) target_cell = self.current_sheet.cell(row=row+row_offset, column=col)
# Copy value if source_cell.value:
target_cell.value = source_cell.value # Copy value
target_cell.value = source_cell.value
# Copy styles #print(f"({col},{row}) : {target_cell.value}")
if source_cell.has_style: # Copy styles
target_cell.font = copy(source_cell.font) if source_cell.has_style:
target_cell.border = copy(source_cell.border) target_cell.font = copy(source_cell.font)
target_cell.fill = copy(source_cell.fill) target_cell.border = copy(source_cell.border)
target_cell.number_format = source_cell.number_format target_cell.fill = copy(source_cell.fill)
target_cell.protection = copy(source_cell.protection) target_cell.number_format = source_cell.number_format
target_cell.alignment = copy(source_cell.alignment) target_cell.protection = copy(source_cell.protection)
target_cell.alignment = copy(source_cell.alignment)
# Copy page setup # Copy page setup
target_page_setup = self.current_sheet.page_setup target_page_setup = self.current_sheet.page_setup

View File

@ -2,10 +2,12 @@
template_file=certificate_template.xlsx template_file=certificate_template.xlsx
doc_file=certificate_[zekken_number].xlsx doc_file=certificate_[zekken_number].xlsx
sections=section1 sections=section1
maxcol=8 maxcol=10
column_width=3,5,16,16,16,16,16,8,8,12,3
[section1] [section1]
template_sheet=certificate template_sheet=certificate
sheet_name=certificate
groups=group1,group2 groups=group1,group2
fit_to_width=1 fit_to_width=1
orientation=portrait orientation=portrait
@ -13,12 +15,12 @@ orientation=portrait
[section1.group1] [section1.group1]
table_name=mv_entry_details table_name=mv_entry_details
where=zekken_number='[zekken_number]' and event_name='[event_code]' where=zekken_number='[zekken_number]' and event_name='[event_code]'
group_range=A1:H11 group_range=A1:J12
[section1.group2] [section1.group2]
table_name=gps_checkins table_name=v_checkins_locations
where=zekken_number='[zekken_number]' and event_code='[event_code]' where=zekken_number='[zekken_number]' and event_code='[event_code]'
sort=path_order sort=path_order
group_range=A12:H12 group_range=A13:J13