basic debugging step 1
This commit is contained in:
@ -9,6 +9,6 @@ services:
|
||||
- ..:/app
|
||||
environment:
|
||||
- PYTHONPATH=/app
|
||||
command: /bin/bash
|
||||
command: python ./testdata/sample.py
|
||||
tty: true
|
||||
|
||||
|
||||
@ -2,10 +2,10 @@ FROM python:3.9-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 必要なシステムパッケージのインストール
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
# GPGキーの更新とパッケージのインストール
|
||||
RUN apt-get update --allow-insecure-repositories && \
|
||||
apt-get install -y --allow-unauthenticated python3-dev libpq-dev && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Pythonパッケージのインストール
|
||||
COPY requirements.txt .
|
||||
@ -16,12 +16,11 @@ COPY . .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 開発用パッケージのインストール
|
||||
RUN pip install --no-cache-dir \
|
||||
RUN pip install --no-cache-dir --upgrade pip \
|
||||
pytest \
|
||||
pytest-cov \
|
||||
flake8
|
||||
|
||||
|
||||
# パッケージのインストール
|
||||
RUN pip install -e .
|
||||
|
||||
|
||||
@ -2,4 +2,4 @@ openpyxl>=3.0.0
|
||||
pandas>=1.0.0
|
||||
pillow>=8.0.0
|
||||
configparser>=5.0.0
|
||||
|
||||
psycopg2-binary==2.9.9
|
||||
|
||||
166
SumasenLibs/excel_lib/sumaexcel/config_handler.py
Normal file
166
SumasenLibs/excel_lib/sumaexcel/config_handler.py
Normal file
@ -0,0 +1,166 @@
|
||||
# config_handler.py
|
||||
#
|
||||
import configparser
|
||||
import os
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import configparser
|
||||
import os
|
||||
import re
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
class ConfigHandler:
|
||||
"""変数置換機能付きの設定ファイル管理クラス"""
|
||||
|
||||
def __init__(self, ini_file_path: str, variables: Dict[str, str] = None):
|
||||
"""
|
||||
Args:
|
||||
ini_file_path (str): INIファイルのパス
|
||||
variables (Dict[str, str], optional): 置換用の変数辞書
|
||||
"""
|
||||
self.ini_file_path = ini_file_path
|
||||
self.variables = variables or {}
|
||||
self.config = configparser.ConfigParser()
|
||||
self.load_config()
|
||||
|
||||
def _substitute_variables(self, text: str) -> str:
|
||||
"""
|
||||
テキスト内の変数を置換する
|
||||
|
||||
Args:
|
||||
text (str): 置換対象のテキスト
|
||||
|
||||
Returns:
|
||||
str: 置換後のテキスト
|
||||
"""
|
||||
# ${var}形式の変数を置換
|
||||
pattern1 = r'\${([^}]+)}'
|
||||
# [var]形式の変数を置換
|
||||
pattern2 = r'\[([^\]]+)\]'
|
||||
|
||||
def replace_var(match):
|
||||
var_name = match.group(1)
|
||||
return self.variables.get(var_name, match.group(0))
|
||||
|
||||
# 両方のパターンで置換を実行
|
||||
text = re.sub(pattern1, replace_var, text)
|
||||
text = re.sub(pattern2, replace_var, text)
|
||||
|
||||
return text
|
||||
|
||||
def load_config(self) -> None:
|
||||
"""設定ファイルを読み込み、変数を置換する"""
|
||||
if not os.path.exists(self.ini_file_path):
|
||||
raise FileNotFoundError(f"設定ファイルが見つかりません: {self.ini_file_path}")
|
||||
|
||||
# まず生のテキストとして読み込む
|
||||
with open(self.ini_file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# 変数を置換
|
||||
substituted_content = self._substitute_variables(content)
|
||||
|
||||
# 置換済みの内容を StringIO 経由で configparser に読み込ませる
|
||||
from io import StringIO
|
||||
self.config.read_file(StringIO(substituted_content))
|
||||
|
||||
def get_value(self, section: str, key: str, default: Any = None) -> Optional[str]:
|
||||
"""
|
||||
指定されたセクションのキーの値を取得する
|
||||
|
||||
Args:
|
||||
section (str): セクション名
|
||||
key (str): キー名
|
||||
default (Any): デフォルト値(オプション)
|
||||
|
||||
Returns:
|
||||
Optional[str]: 設定値。存在しない場合はデフォルト値
|
||||
"""
|
||||
try:
|
||||
return self.config[section][key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def get_section(self, section: str) -> Dict[str, str]:
|
||||
"""
|
||||
指定されたセクションの全ての設定を取得する
|
||||
|
||||
Args:
|
||||
section (str): セクション名
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: セクションの設定をディクショナリで返す
|
||||
"""
|
||||
try:
|
||||
return dict(self.config[section])
|
||||
except KeyError:
|
||||
return {}
|
||||
|
||||
def get_all_sections(self) -> Dict[str, Dict[str, str]]:
|
||||
"""
|
||||
全てのセクションの設定を取得する
|
||||
|
||||
Returns:
|
||||
Dict[str, Dict[str, str]]: 全セクションの設定をネストされたディクショナリで返す
|
||||
"""
|
||||
return {section: dict(self.config[section]) for section in self.config.sections()}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 使用例
|
||||
if __name__ == "__main__":
|
||||
# サンプルのINIファイル作成
|
||||
sample_ini = """
|
||||
[Database]
|
||||
host = localhost
|
||||
port = 5432
|
||||
database = mydb
|
||||
user = admin
|
||||
password = secret
|
||||
|
||||
[Application]
|
||||
debug = true
|
||||
log_level = INFO
|
||||
max_connections = 100
|
||||
|
||||
[Paths]
|
||||
data_dir = /var/data
|
||||
log_file = /var/log/app.log
|
||||
"""
|
||||
|
||||
# サンプルINIファイルを作成
|
||||
with open('config.ini', 'w', encoding='utf-8') as f:
|
||||
f.write(sample_ini)
|
||||
|
||||
# 設定を読み込んで使用
|
||||
config = ConfigHandler('config.ini')
|
||||
|
||||
# 特定の値を取得
|
||||
db_host = config.get_value('Database', 'host')
|
||||
db_port = config.get_value('Database', 'port')
|
||||
print(f"Database connection: {db_host}:{db_port}")
|
||||
|
||||
# セクション全体を取得
|
||||
db_config = config.get_section('Database')
|
||||
print("Database configuration:", db_config)
|
||||
|
||||
# 全ての設定を取得
|
||||
all_config = config.get_all_sections()
|
||||
print("All configurations:", all_config)
|
||||
|
||||
|
||||
# サンプル:
|
||||
# # 設定ファイルから値を取得
|
||||
# config = ConfigHandler('config.ini')
|
||||
#
|
||||
# # データベース設定を取得
|
||||
# db_host = config.get_value('Database', 'host')
|
||||
# db_port = config.get_value('Database', 'port')
|
||||
# db_name = config.get_value('Database', 'database')
|
||||
#
|
||||
# # アプリケーション設定を取得
|
||||
# debug_mode = config.get_value('Application', 'debug')
|
||||
# log_level = config.get_value('Application', 'log_level')
|
||||
#
|
||||
@ -10,183 +10,360 @@ import shutil
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, Any, Union, Tuple
|
||||
from pathlib import Path
|
||||
# psycopg2のインポートを追加
|
||||
import psycopg2
|
||||
import psycopg2.extras
|
||||
|
||||
from .config_handler import ConfigHandler # ini file のロード
|
||||
|
||||
from .styles import StyleManager
|
||||
from .merge import MergeManager
|
||||
from .image import ImageManager
|
||||
from .conditional import ConditionalFormatManager
|
||||
from .page import PageManager, PaperSizes
|
||||
import logging
|
||||
|
||||
class SumasenExcel:
|
||||
"""Enhanced Excel handling class with extended functionality"""
|
||||
|
||||
def __init__(self, debug: bool = False):
|
||||
self.debug = debug
|
||||
def __init__(self, document: str, variables: Dict[str, Any],
|
||||
docbase: str = "./docbase") -> Dict[str, str]:
|
||||
|
||||
"""Initialize Excel document with basic settings
|
||||
|
||||
Args:
|
||||
document: Document name
|
||||
variables: Variables for document
|
||||
docbase: Base directory for documents (default: "./docbase")
|
||||
|
||||
Returns:
|
||||
Dict with status (true/false) and optional error message
|
||||
|
||||
"""
|
||||
self.debug = True
|
||||
|
||||
self.workbook = None
|
||||
self.template_filepath = None
|
||||
self.output_filepath = None
|
||||
self.current_sheet = None
|
||||
|
||||
self.dbname = None
|
||||
self.user = None
|
||||
self.password = None
|
||||
self.host = None
|
||||
self.port = None
|
||||
|
||||
self._style_manager = None
|
||||
self._merge_manager = None
|
||||
self._image_manager = None
|
||||
self._conditional_manager = None
|
||||
self._page_manager = None
|
||||
|
||||
def init(self, username: str, project_id: str, document: str,
|
||||
lang: str = "jp", docbase: str = "./docbase") -> Dict[str, str]:
|
||||
"""Initialize Excel document with basic settings
|
||||
|
||||
Args:
|
||||
username: User name for file operations
|
||||
project_id: Project identifier
|
||||
document: Document name
|
||||
lang: Language code (default: "jp")
|
||||
docbase: Base directory for documents (default: "./docbase")
|
||||
|
||||
Returns:
|
||||
Dict with status ("ACK"/"NCK") and optional error message
|
||||
"""
|
||||
try:
|
||||
self.username = username
|
||||
self.project_id = project_id
|
||||
self.language = lang
|
||||
|
||||
# Setup directory structure
|
||||
# document base を設定
|
||||
self.docpath = docbase
|
||||
self.docpath2 = f"{docbase}/{project_id}"
|
||||
if not os.path.exists(docbase):
|
||||
logging.error(f"Document base directory not found: {docbase}")
|
||||
|
||||
# Create directories if they don't exist
|
||||
for path in [docbase, self.docpath2]:
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path, mode=0o755)
|
||||
# ini fileをロード
|
||||
self.ini_file_path = f"{docbase}/{document}.ini"
|
||||
self.conf = ConfigHandler(self.ini_file_path, variables)
|
||||
if not os.path.exists(self.ini_file_path):
|
||||
logging.error(f"INI file not found: {self.ini_file_path}")
|
||||
|
||||
# Load template
|
||||
inifile = f"{document}.ini"
|
||||
self.inifilepath = f"{self.docpath2}/{inifile}"
|
||||
# basic section をロード
|
||||
basic = self.conf.get_section("basic")
|
||||
if not basic:
|
||||
logging.error(f"Basic section not found in INI file: {self.ini_file_path}")
|
||||
|
||||
if not os.path.exists(self.inifilepath):
|
||||
return {"status": "NCK", "message": f"INI file not found: {self.inifilepath}"}
|
||||
self.basic = basic
|
||||
|
||||
# Load template workbook
|
||||
template_file = self._get_ini_param("basic", f"templatefile_{lang}")
|
||||
self.template_filepath = f"{self.docpath2}/{template_file}"
|
||||
# basicセクションから必要なパラメータを取得
|
||||
template_file = basic.get("template_file")
|
||||
if not template_file:
|
||||
logging.error("template_file not found in basic section")
|
||||
|
||||
doc_file = basic.get("doc_file")
|
||||
if not doc_file:
|
||||
logging.error("doc_file not found in basic section")
|
||||
|
||||
self.maxcol = basic.get("maxcol")
|
||||
if not self.maxcol:
|
||||
self.maxcol = 100 # デフォルト値を設定
|
||||
logging.warning("maxcol not found in basic section. Defaulting to 100")
|
||||
else:
|
||||
self.maxcol = int(self.maxcol)
|
||||
|
||||
sections = basic.get("sections")
|
||||
if not sections:
|
||||
logging.error("sections not found in basic section")
|
||||
|
||||
|
||||
# セクションをリストに変換
|
||||
self.section_list = [s.strip() for s in sections.split(",")]
|
||||
if not self.section_list:
|
||||
logging.error("sections not found in basic section")
|
||||
|
||||
# 出力ファイルパスを設定
|
||||
self.output_filepath = f"{self.docpath}/{doc_file}"
|
||||
# 新規のExcelワークブックを作成
|
||||
self.workbook = openpyxl.Workbook()
|
||||
# デフォルトで作成されるシートを削除
|
||||
self.workbook.remove(self.workbook.active)
|
||||
|
||||
# テンプレートワークブックをロード
|
||||
self.template_filepath = f"{self.docpath}/{template_file}"
|
||||
if not os.path.exists(self.template_filepath):
|
||||
# Copy from default if not exists
|
||||
default_template = f"{self.docpath}/{template_file}"
|
||||
shutil.copy2(default_template, self.template_filepath)
|
||||
logging.error(f"Template file not found: {self.template_filepath}")
|
||||
|
||||
self.workbook = openpyxl.load_workbook(self.template_filepath)
|
||||
|
||||
return {"status": "ACK"}
|
||||
self.template_workbook = openpyxl.load_workbook(self.template_filepath)
|
||||
self.template_sheet = self.template_workbook.active
|
||||
|
||||
except Exception as e:
|
||||
return {"status": "NCK", "message": str(e)}
|
||||
logging.error(f"Error initializing Excel document: {str(e)}")
|
||||
|
||||
def _get_ini_param(self, section: str, param: str) -> Optional[str]:
|
||||
"""Get parameter from INI file
|
||||
|
||||
Args:
|
||||
section: INI file section
|
||||
param: Parameter name
|
||||
|
||||
Returns:
|
||||
Parameter value or None if not found
|
||||
"""
|
||||
def make_report(self,variables: Dict[str, Any]):
|
||||
"""レポートを生成する"""
|
||||
logging.info("make_report step-1")
|
||||
try:
|
||||
# Use configparser to handle INI files
|
||||
import configparser
|
||||
config = configparser.ConfigParser()
|
||||
config.read(self.inifilepath)
|
||||
return config[section][param]
|
||||
except:
|
||||
return None
|
||||
ret_status = True
|
||||
# セクションごとに処理を実行(ワークシート単位)
|
||||
for section in self.section_list:
|
||||
ret = self.proceed_section(section,variables)
|
||||
if ret["status"] == False:
|
||||
message = ret.get("message", "No message provided")
|
||||
return {"status": False, "message": f"Fail generating section: {section}...{message}"}
|
||||
|
||||
def make_report(self, db, data_rec: Dict[str, Any],
|
||||
out_filename: Optional[str] = None,
|
||||
screen_index: int = 0) -> None:
|
||||
"""Generate Excel report from template
|
||||
# 生成したワークブックを保存
|
||||
self.workbook.save(self.output_filepath)
|
||||
return {"status": True, "message": f"Report generated successfully : {self.output_filepath}", "filepath":self.output_filepath}
|
||||
|
||||
except Exception as e:
|
||||
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}")
|
||||
|
||||
try:
|
||||
# セクションの設定を取得
|
||||
section_config = self.conf.get_section(section)
|
||||
# セクションが存在しない場合はスキップ
|
||||
if not section_config:
|
||||
return {"status": False, "message": f"Error no section found: {section}"}
|
||||
|
||||
# テンプレートシートをコピー
|
||||
template_sheet_name = section_config.get("template_sheet")
|
||||
if not template_sheet_name or template_sheet_name not in self.template_workbook.sheetnames:
|
||||
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
|
||||
|
||||
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')
|
||||
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"}
|
||||
|
||||
# PostgreSQLに接続
|
||||
self.conn = psycopg2.connect(
|
||||
self.dbname,
|
||||
self.user,
|
||||
self.password,
|
||||
self.host,
|
||||
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)
|
||||
|
||||
# シートの幅を設定
|
||||
fit_to_width = section_config.get("fit_to_width")
|
||||
if fit_to_width:
|
||||
new_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
|
||||
|
||||
# グループ定義を取得
|
||||
groups = section_config.get("groups")
|
||||
if not groups:
|
||||
return {"status": False, "message": f"Error no group definitions found: {section}"}
|
||||
|
||||
# グループをリストに変換
|
||||
group_list = [g.strip() for g in groups.split(",")]
|
||||
if not group_list:
|
||||
return {"status": False, "message": f"Error invalid group definitions found: {section}"}
|
||||
|
||||
# 各グループの設定を取得
|
||||
for group in group_list:
|
||||
ret = self.proceed_group(group,variables)
|
||||
if ret["status"] == False:
|
||||
return ret
|
||||
|
||||
return {"status": True, "message": f"Success generating section: {section}"}
|
||||
|
||||
except Exception as e:
|
||||
return {"status": False, "message": f"Exception in proceed_section : Error generating report: {str(e)}"}
|
||||
|
||||
def proceed_group(self,group:str,variables: Dict[str, Any]):
|
||||
logging.info(f"make_report.proceed_group step-1:section={group}")
|
||||
|
||||
try:
|
||||
group_config = self.conf.get_section(group)
|
||||
if not group_config:
|
||||
return {"status": False, "message": f"Error no group section found: {group}"}
|
||||
|
||||
# グループの処理パラメータを取得
|
||||
group_range = group_config.get("group_range")
|
||||
table = group_config.get("table")
|
||||
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}"}
|
||||
|
||||
sort = group_config.get("sort")
|
||||
if not sort:
|
||||
ret = self.proceed_one_record(table,where,group_range,variables)
|
||||
if ret.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:
|
||||
return {"status": True, "message": f"Success generating group: {group}"}
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error in proceed_group: {str(e)}")
|
||||
return {"status": False, "message": f"Exception in proceed_group : Error generating report: {str(e)}"}
|
||||
|
||||
def proceed_one_record(self,table:str,where:str,group_range:str,variables: Dict[str, Any]):
|
||||
"""1レコードのデータを取得してシートの値を置き換える
|
||||
|
||||
Args:
|
||||
db: Database connection
|
||||
data_rec: Data records to populate report
|
||||
out_filename: Optional output filename
|
||||
screen_index: Screen index for multi-screen reports
|
||||
table: テーブル名
|
||||
where: WHERE句
|
||||
group_range: 処理対象範囲
|
||||
variables: DB接続情報を含む変数辞書
|
||||
"""
|
||||
# Get output filename
|
||||
if out_filename:
|
||||
outfile = f"{out_filename}_{self._get_ini_param('basic', 'doc_file')}"
|
||||
else:
|
||||
outfile = self._get_ini_param('basic', 'doc_file')
|
||||
logging.info(f"make_report.proceed_one_record step-1:table={table},where={where},group_range={group_range}")
|
||||
|
||||
self.output_filepath = f"{self.docpath2}/{outfile}"
|
||||
try:
|
||||
# まずself.template_sheetの指定範囲のセルをself.current_sheetにコピーする。
|
||||
self.copy_template_to_current(self.range,self.range)
|
||||
|
||||
# Process sections
|
||||
sections = self._get_ini_param('basic', 'sections')
|
||||
if not sections:
|
||||
return
|
||||
cursor = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
|
||||
|
||||
for section in sections.split(','):
|
||||
self._process_section(section, db, data_rec, screen_index)
|
||||
# SQLクエリを実行
|
||||
query = f"SELECT * FROM {table} WHERE {where} LIMIT 1"
|
||||
cursor.execute(query)
|
||||
record = cursor.fetchone()
|
||||
|
||||
# Save workbook
|
||||
self.workbook.save(self.output_filepath)
|
||||
if record:
|
||||
# group_rangeの範囲内のセルを走査
|
||||
for row in self.current_worksheet:
|
||||
for cell in row:
|
||||
if cell.value and isinstance(cell.value, str):
|
||||
# [field_name]形式の文字列を検索
|
||||
import re
|
||||
matches = re.findall(r'\[(.*?)\]', cell.value)
|
||||
|
||||
def _process_section(self, section: str, db, data_rec: Dict[str, Any],
|
||||
screen_index: int) -> None:
|
||||
"""Process individual section of report
|
||||
# マッチした場合、フィールド値で置換
|
||||
if matches:
|
||||
new_value = cell.value
|
||||
for field_name in matches:
|
||||
if field_name in record:
|
||||
new_value = new_value.replace(
|
||||
f'[{field_name}]',
|
||||
str(record[field_name])
|
||||
)
|
||||
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]):
|
||||
"""複数レコードを取得してシートの値を置き換える
|
||||
|
||||
Args:
|
||||
section: Section name
|
||||
db: Database connection
|
||||
data_rec: Data records
|
||||
screen_index: Screen index
|
||||
table: テーブル名
|
||||
where: WHERE句
|
||||
sort: ORDER BY句
|
||||
group_range: 処理対象範囲
|
||||
variables: DB接続情報を含む変数辞書
|
||||
"""
|
||||
# Get template sheet
|
||||
sheet_orig = self._get_ini_param(section, 'sheet')
|
||||
sheet_name = self._get_ini_param(section, f"sheetname_{self.language}")
|
||||
logging.info(f"make_report.proceed_all_record step-1:table={table},where={where},group_range={group_range}")
|
||||
|
||||
if not sheet_orig or not sheet_name:
|
||||
return
|
||||
try:
|
||||
# グループ範囲の行数を取得
|
||||
start_row, end_row = map(int, group_range.split(':')[0].split(','))
|
||||
template_rows = end_row - start_row + 1
|
||||
|
||||
# Copy template sheet
|
||||
template_sheet = self.workbook[sheet_orig]
|
||||
new_sheet = self.workbook.copy_worksheet(template_sheet)
|
||||
new_sheet.title = sheet_name
|
||||
cursor = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
|
||||
|
||||
# Process groups
|
||||
groups = self._get_ini_param(section, 'groups')
|
||||
if groups:
|
||||
for group in groups.split(','):
|
||||
self._process_group(new_sheet, section, group, db, data_rec, screen_index)
|
||||
# SQLクエリを実行
|
||||
query = f"SELECT * FROM {table} WHERE {where} ORDER BY {sort}"
|
||||
cursor.execute(query)
|
||||
records = cursor.fetchall()
|
||||
|
||||
def _process_group(self, sheet, section: str, group: str,
|
||||
db, data_rec: Dict[str, Any], screen_index: int) -> None:
|
||||
"""Process group within section
|
||||
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}"
|
||||
)
|
||||
|
||||
Args:
|
||||
sheet: Worksheet to process
|
||||
section: Section name
|
||||
group: Group name
|
||||
db: Database connection
|
||||
data_rec: Data records
|
||||
screen_index: Screen index
|
||||
"""
|
||||
pass # Implementation details will follow
|
||||
# コピーした範囲内のセルを走査して値を置換
|
||||
for row in range(current_row, current_row + template_rows):
|
||||
for cell in self.current_worksheet[row]:
|
||||
if cell.value and isinstance(cell.value, str):
|
||||
# [field_name]形式の文字列を検索
|
||||
import re
|
||||
matches = re.findall(r'\[(.*?)\]', cell.value)
|
||||
|
||||
# マッチした場合、フィールド値で置換
|
||||
if matches:
|
||||
new_value = cell.value
|
||||
for field_name in matches:
|
||||
if field_name in record:
|
||||
new_value = new_value.replace(
|
||||
f'[{field_name}]',
|
||||
str(record[field_name])
|
||||
)
|
||||
cell.value = new_value
|
||||
|
||||
current_row += template_rows
|
||||
|
||||
cursor.close()
|
||||
self.conn.close()
|
||||
return {"status": True, "message": "Success processing all records"}
|
||||
|
||||
except Exception as 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)}"}
|
||||
|
||||
def copy_template_to_current(self,range,target_range):
|
||||
# テンプレートシートから現在のシートにデータをコピー
|
||||
source_range = self.template_sheet.range(range) # コピーする範囲を指定
|
||||
target_range = self.current_sheet.range(target_range) # 貼り付け先の範囲を指定
|
||||
|
||||
# 値、数式、フォーマットをコピー
|
||||
source_range.copy(target_range)
|
||||
|
||||
|
||||
|
||||
|
||||
def init_sheet(self, sheet_name: str) -> None:
|
||||
"""Initialize worksheet and managers"""
|
||||
self.current_sheet = self.workbook[sheet_name]
|
||||
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)
|
||||
|
||||
# Style operations
|
||||
def apply_style(
|
||||
self,
|
||||
|
||||
BIN
SumasenLibs/excel_lib/testdata/certificate_template.xlsx
vendored
Normal file
BIN
SumasenLibs/excel_lib/testdata/certificate_template.xlsx
vendored
Normal file
Binary file not shown.
62
SumasenLibs/excel_lib/testdata/sample.py
vendored
62
SumasenLibs/excel_lib/testdata/sample.py
vendored
@ -1,55 +1,19 @@
|
||||
from sumaexcel import SumasenExcel
|
||||
import logging
|
||||
|
||||
# 初期化
|
||||
excel = SumasenExcel()
|
||||
excel.init("username", "project_id", "document")
|
||||
variables = {"zekken_number":"5033","event_code":"FC岐阜"}
|
||||
excel = SumasenExcel(document="test", variables=variables, docbase="./testdata")
|
||||
|
||||
logging.info("Excelファイル作成 step-1")
|
||||
|
||||
# シート初期化
|
||||
excel.init_sheet("Sheet1")
|
||||
|
||||
# スタイル適用
|
||||
excel.apply_style(
|
||||
"A1:D10",
|
||||
font={"name": "Arial", "size": 12, "bold": True},
|
||||
fill={"start_color": "FFFF00"},
|
||||
alignment={"horizontal": "center"}
|
||||
)
|
||||
|
||||
# セルのマージ
|
||||
excel.merge_range(1, 1, 1, 4)
|
||||
|
||||
# 画像追加
|
||||
excel.add_image(
|
||||
"logo.png",
|
||||
position=(1, 1),
|
||||
size=(100, 100)
|
||||
)
|
||||
|
||||
# 条件付き書式
|
||||
excel.add_conditional_format(
|
||||
"B2:B10",
|
||||
format_type="color_scale",
|
||||
min_color="00FF0000",
|
||||
max_color="0000FF00"
|
||||
)
|
||||
|
||||
# ページ設定
|
||||
excel.setup_page(
|
||||
orientation="landscape",
|
||||
paper_size=PaperSizes.A4,
|
||||
margins={
|
||||
"left": 1.0,
|
||||
"right": 1.0,
|
||||
"top": 1.0,
|
||||
"bottom": 1.0
|
||||
},
|
||||
header_footer={
|
||||
"odd_header": "&L&BPage &P of &N&C&BConfidential",
|
||||
"odd_footer": "&RDraft"
|
||||
}
|
||||
)
|
||||
|
||||
# レポート生成
|
||||
excel.make_report(db, data_rec)
|
||||
|
||||
ret = excel.make_report(variables=variables)
|
||||
logging.info(f"Excelファイル作成 step-2 : ret={ret}")
|
||||
if ret["status"]==True:
|
||||
filepath=ret["filepath"]
|
||||
logging.info(f"Excelファイル作成 : ret.filepath={filepath}")
|
||||
else:
|
||||
message = ret.get("message", "No message provided")
|
||||
logging.error(f"Excelファイル作成失敗 : ret.message={message}")
|
||||
|
||||
|
||||
17
SumasenLibs/excel_lib/testdata/test.ini
vendored
17
SumasenLibs/excel_lib/testdata/test.ini
vendored
@ -1,25 +1,24 @@
|
||||
[basic]
|
||||
templatefile_jp="certificate_template.xlsx"
|
||||
doc_file="certificate_[zekken_number].xlsx"
|
||||
template_file=certificate_template.xlsx
|
||||
doc_file=certificate_[zekken_number].xlsx
|
||||
sections=section1,section2
|
||||
developer=Sumasen
|
||||
maxcol=8
|
||||
|
||||
[section1]
|
||||
sheet="certificate"
|
||||
sheetname_jp="岐阜ロゲ通過証明書"
|
||||
groups="group1,group2"
|
||||
template_sheet=certificate
|
||||
groups=group1,group2
|
||||
fit_to_width=1
|
||||
orientation=portrait
|
||||
|
||||
[section1.group1]
|
||||
table_name=rog_entry
|
||||
where="zekken_number='[zekken_number]' and event_code='[event_code]'"
|
||||
group_range="0,0,8,11"
|
||||
where=zekken_number='[zekken_number]' and event_code='[event_code]'
|
||||
group_range=0,0,8,11
|
||||
|
||||
|
||||
[section1.group2]
|
||||
table_name=gps_checkins
|
||||
where=""zekken_number='[zekken_number]' and event_code='[event_code]'
|
||||
where=zekken_number='[zekken_number]' and event_code='[event_code]'
|
||||
sort=order
|
||||
group_range=0,12,8,12
|
||||
|
||||
|
||||
Reference in New Issue
Block a user