From ceb783d6bd618a68e60a556a33bd36429bc7937c Mon Sep 17 00:00:00 2001 From: hayano Date: Wed, 6 Nov 2024 07:22:24 +0000 Subject: [PATCH] Generate Excel file step 1 --- .../excel_lib/docker/docker-compose.yml | 8 +- .../excel_lib/docker/python/Dockerfile | 2 +- SumasenLibs/excel_lib/sumaexcel/sumaexcel.py | 165 +++++++++++++----- .../excel_lib/testdata/certificate_5033.xlsx | Bin 0 -> 6294 bytes SumasenLibs/excel_lib/testdata/sample.py | 11 +- SumasenLibs/excel_lib/testdata/test.ini | 12 +- 6 files changed, 146 insertions(+), 52 deletions(-) create mode 100644 SumasenLibs/excel_lib/testdata/certificate_5033.xlsx diff --git a/SumasenLibs/excel_lib/docker/docker-compose.yml b/SumasenLibs/excel_lib/docker/docker-compose.yml index e0b1267..bd37861 100644 --- a/SumasenLibs/excel_lib/docker/docker-compose.yml +++ b/SumasenLibs/excel_lib/docker/docker-compose.yml @@ -9,6 +9,12 @@ services: - ..:/app environment: - PYTHONPATH=/app - command: python ./testdata/sample.py + - POSTGRES_DB=rogdb + - POSTGRES_USER=admin + - POSTGRES_PASSWORD=admin123456 + - POSTGRES_HOST=localhost + - POSTGRES_PORT=5432 + network_mode: "host" tty: true + container_name: python_container # コンテナ名を明示的に指定 diff --git a/SumasenLibs/excel_lib/docker/python/Dockerfile b/SumasenLibs/excel_lib/docker/python/Dockerfile index f7a1e5d..2a7e958 100644 --- a/SumasenLibs/excel_lib/docker/python/Dockerfile +++ b/SumasenLibs/excel_lib/docker/python/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /app # GPGキーの更新とパッケージのインストール RUN apt-get update --allow-insecure-repositories && \ - apt-get install -y --allow-unauthenticated python3-dev libpq-dev && \ + apt-get install -y --allow-unauthenticated python3-dev libpq-dev postgresql-client && \ rm -rf /var/lib/apt/lists/* # Pythonパッケージのインストール diff --git a/SumasenLibs/excel_lib/sumaexcel/sumaexcel.py b/SumasenLibs/excel_lib/sumaexcel/sumaexcel.py index 26841e3..f019ef7 100644 --- a/SumasenLibs/excel_lib/sumaexcel/sumaexcel.py +++ b/SumasenLibs/excel_lib/sumaexcel/sumaexcel.py @@ -13,16 +13,24 @@ from pathlib import Path # psycopg2のインポートを追加 import psycopg2 import psycopg2.extras +from copy import copy +from openpyxl.utils import range_boundaries from .config_handler import ConfigHandler # ini file のロード -from .styles import StyleManager -from .merge import MergeManager -from .image import ImageManager -from .conditional import ConditionalFormatManager +#from .styles import StyleManager +#from .merge import MergeManager +#from .image import ImageManager +#from .conditional import ConditionalFormatManager from .page import PageManager, PaperSizes + import logging +logging.basicConfig( + level=logging.INFO, # INFOレベル以上のログを表示 + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) + class SumasenExcel: """Enhanced Excel handling class with extended functionality""" @@ -142,7 +150,7 @@ class SumasenExcel: return {"status": False, "message": f"Exception in make_report: Error generating report: {str(e)}"} def proceed_section(self, section: str, variables: Dict[str, Any]): - logging.info(f"make_report.proceed_section step-1:section={section}") + print(f"make_report.proceed_section step-1:section={section}") try: # セクションの設定を取得 @@ -160,28 +168,29 @@ class SumasenExcel: new_sheet = self.workbook.create_sheet(title=section_config.get("sheet_name", section)) self.worksheet = new_sheet - self.dbname=variables.get('db'), - self.user=variables.get('username'), - self.password=variables.get('password'), - self.host=variables.get('host'), - self.port=variables.get('port') + self.dbname=variables.get('db') + self.user=variables.get('username') + self.password=variables.get('password') + self.host=variables.get('host','postgres') + self.port=variables.get('port','5432') if not self.dbname or not self.user or not self.password or not self.host or not self.port: return {"status": False, "message": f"Error no database connection information"} + print(f"db={self.dbname},user={self.user},pass={self.password},host={self.host},port={self.port}") # PostgreSQLに接続 self.conn = psycopg2.connect( - self.dbname, - self.user, - self.password, - self.host, - self.port + dbname=self.dbname, + user=self.user, + password=self.password, + host=self.host, + port=self.port ) - self._style_manager = StyleManager() - self._merge_manager = MergeManager(self.current_sheet) - self._image_manager = ImageManager(self.current_sheet) - self._conditional_manager = ConditionalFormatManager(self.current_sheet) - self._page_manager = PageManager(self.current_sheet) + #self._style_manager = StyleManager() + #self._merge_manager = MergeManager(self.current_sheet) + #self._image_manager = ImageManager(self.current_sheet) + #self._conditional_manager = ConditionalFormatManager(self.current_sheet) + #self._page_manager = PageManager(self.current_sheet) # シートの幅を設定 fit_to_width = section_config.get("fit_to_width") @@ -205,7 +214,8 @@ class SumasenExcel: # 各グループの設定を取得 for group in group_list: - ret = self.proceed_group(group,variables) + section_group = f"{section}.{group}" + ret = self.proceed_group(section_group,variables) if ret["status"] == False: return ret @@ -224,7 +234,7 @@ class SumasenExcel: # グループの処理パラメータを取得 group_range = group_config.get("group_range") - table = group_config.get("table") + table = group_config.get("table_name") where = group_config.get("where") if not where or not table or not group_range: return {"status": False, "message": f"Error invalid group parameters: {group_config}"} @@ -232,11 +242,11 @@ class SumasenExcel: sort = group_config.get("sort") if not sort: ret = self.proceed_one_record(table,where,group_range,variables) - if ret.status == True: + if ret.get("status") == True: return {"status": True, "message": f"Success generating group: {group}"} else: ret = self.proceed_all_records(table,where,sort,group_range,variables) - if ret.status == True: + if ret.get("status") == True: return {"status": True, "message": f"Success generating group: {group}"} except Exception as e: @@ -252,19 +262,22 @@ class SumasenExcel: group_range: 処理対象範囲 variables: DB接続情報を含む変数辞書 """ - logging.info(f"make_report.proceed_one_record step-1:table={table},where={where},group_range={group_range}") try: + print(f"make_report.proceed_one_record step-1:table={table},where={where},group_range={group_range}") # まずself.template_sheetの指定範囲のセルをself.current_sheetにコピーする。 - self.copy_template_to_current(self.range,self.range) + self.copy_template_to_current(group_range,group_range) + print(f"step-1") cursor = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor) # SQLクエリを実行 query = f"SELECT * FROM {table} WHERE {where} LIMIT 1" cursor.execute(query) record = cursor.fetchone() - + + print(f"query={query}") + print(f"record={record}") if record: # group_rangeの範囲内のセルを走査 for row in self.current_worksheet: @@ -286,14 +299,36 @@ class SumasenExcel: cell.value = new_value cursor.close() - self.conn.close() return {"status": True, "message": f"Success generating group: "} except Exception as e: logging.error(f"Error in proceed_one_record: {str(e)}") return {"status": False, "message": f"Exception in proceed_one_record:Error generating report: {str(e)}"} - def proceed_all_record(self, table: str, where: str, sort: str, group_range: str, variables: Dict[str, Any]): + def get_column_letter(self, cell_reference): + """ + セル参照(例:'A12')から列文字を取得 + """ + if isinstance(cell_reference, str): + # アルファベット部分を抽出 + column = ''.join(c for c in cell_reference if c.isalpha()) + if column: + return column + return 'A' # デフォルト値 + + def get_row_number(self,cell_reference): + """ + セル参照(例:'A12')から行番号を取得 + """ + if isinstance(cell_reference, str): + # 数字部分のみを抽出 + digits = ''.join(c for c in cell_reference if c.isdigit()) + if digits: + return int(digits) + return int(cell_reference) + + + def proceed_all_records(self, table: str, where: str, sort: str, group_range: str, variables: Dict[str, Any]): """複数レコードを取得してシートの値を置き換える Args: @@ -303,11 +338,27 @@ class SumasenExcel: group_range: 処理対象範囲 variables: DB接続情報を含む変数辞書 """ - logging.info(f"make_report.proceed_all_record step-1:table={table},where={where},group_range={group_range}") + print(f"make_report.proceed_all_record step-1:table={table},where={where},group_range={group_range}") try: + # グループ範囲の行数を取得(セル参照対応) + if not group_range or ':' not in group_range: + raise ValueError(f"Invalid group_range format: {group_range}") + # グループ範囲の行数を取得 - start_row, end_row = map(int, group_range.split(':')[0].split(',')) + range_parts = group_range.split(':') + + logging.info(f"Processing range_parts: {range_parts}") # デバッグ用ログ + + start_row = self.get_row_number(range_parts[0].strip()) + start_col = self.get_column_letter(range_parts[0].strip()) + + end_row = self.get_row_number(range_parts[1].strip()) + end_col = self.get_column_letter(range_parts[1].strip()) + + if start_row > end_row: + raise ValueError(f"Invalid row range: start_row ({start_row}) > end_row ({end_row})") + template_rows = end_row - start_row + 1 cursor = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor) @@ -316,13 +367,13 @@ class SumasenExcel: query = f"SELECT * FROM {table} WHERE {where} ORDER BY {sort}" cursor.execute(query) records = cursor.fetchall() - + print(f"query={query}, records={len(records)}") + current_row = start_row for record in records: # テンプレート範囲をコピー - self.copy_template_to_current( - f"{start_row},{end_row}", - f"{current_row},{current_row + template_rows - 1}" + self.copy_template_to_current(f"{start_col}{start_row}:{end_col}{end_row}", + f"{start_col}{current_row}:{end_col}{current_row + template_rows - 1}" ) # コピーした範囲内のセルを走査して値を置換 @@ -354,15 +405,43 @@ 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 copy_template_to_current(self,range,target_range): - # テンプレートシートから現在のシートにデータをコピー - source_range = self.template_sheet.range(range) # コピーする範囲を指定 - target_range = self.current_sheet.range(target_range) # 貼り付け先の範囲を指定 + def copy_template_to_current(self, orig_range, target_range): + try: + print(f"orig_rage={orig_range},target_range={target_range}") + + # 範囲をパースする + min_col, min_row, max_col, max_row = range_boundaries(orig_range) + print(f"min_col, min_row, max_col, max_row = {min_col}, {min_row}, {max_col}, {max_row}") + + # 新しいシートを作成(必要な場合) + if 'Sheet' not in self.workbook.sheetnames: + self.current_sheet = self.workbook.create_sheet('Sheet') + else: + self.current_sheet = self.workbook['Sheet'] - # 値、数式、フォーマットをコピー - source_range.copy(target_range) - - + # セルの内容とスタイルをコピー + for row in range(min_row, max_row + 1): + for col in range(min_col, max_col + 1): + source_cell = self.template_sheet.cell(row=row, column=col) + target_cell = self.current_sheet.cell(row=row, column=col) + + # 値のコピー + target_cell.value = source_cell.value + + # スタイルのコピー + 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) + + return {"status": True, "message": "Successfully copied template range"} + + except Exception as e: + logging.error(f"Error in copy_template_to_current: {str(e)}") + return {"status": False, "message": f"Exception in copy_template_to_current: {str(e)}"} # Style operations def apply_style( diff --git a/SumasenLibs/excel_lib/testdata/certificate_5033.xlsx b/SumasenLibs/excel_lib/testdata/certificate_5033.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..e852aa6eafe776996139943e7518f07f52ea757e GIT binary patch literal 6294 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 J8