basic debugging step 1
This commit is contained in:
@ -9,6 +9,6 @@ services:
|
|||||||
- ..:/app
|
- ..:/app
|
||||||
environment:
|
environment:
|
||||||
- PYTHONPATH=/app
|
- PYTHONPATH=/app
|
||||||
command: /bin/bash
|
command: python ./testdata/sample.py
|
||||||
tty: true
|
tty: true
|
||||||
|
|
||||||
|
|||||||
@ -2,10 +2,10 @@ FROM python:3.9-slim
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 必要なシステムパッケージのインストール
|
# GPGキーの更新とパッケージのインストール
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update --allow-insecure-repositories && \
|
||||||
git \
|
apt-get install -y --allow-unauthenticated python3-dev libpq-dev && \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Pythonパッケージのインストール
|
# Pythonパッケージのインストール
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
@ -16,12 +16,11 @@ COPY . .
|
|||||||
RUN pip install --no-cache-dir -r requirements.txt
|
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 \
|
||||||
pytest-cov \
|
pytest-cov \
|
||||||
flake8
|
flake8
|
||||||
|
|
||||||
|
|
||||||
# パッケージのインストール
|
# パッケージのインストール
|
||||||
RUN pip install -e .
|
RUN pip install -e .
|
||||||
|
|
||||||
|
|||||||
@ -2,4 +2,4 @@ openpyxl>=3.0.0
|
|||||||
pandas>=1.0.0
|
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
|
||||||
|
|||||||
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 datetime import datetime
|
||||||
from typing import Optional, Dict, Any, Union, Tuple
|
from typing import Optional, Dict, Any, Union, Tuple
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
# psycopg2のインポートを追加
|
||||||
|
import psycopg2
|
||||||
|
import psycopg2.extras
|
||||||
|
|
||||||
|
from .config_handler import ConfigHandler # ini file のロード
|
||||||
|
|
||||||
from .styles import StyleManager
|
from .styles import StyleManager
|
||||||
from .merge import MergeManager
|
from .merge import MergeManager
|
||||||
from .image import ImageManager
|
from .image import ImageManager
|
||||||
from .conditional import ConditionalFormatManager
|
from .conditional import ConditionalFormatManager
|
||||||
from .page import PageManager, PaperSizes
|
from .page import PageManager, PaperSizes
|
||||||
|
import logging
|
||||||
|
|
||||||
class SumasenExcel:
|
class SumasenExcel:
|
||||||
"""Enhanced Excel handling class with extended functionality"""
|
"""Enhanced Excel handling class with extended functionality"""
|
||||||
|
|
||||||
def __init__(self, debug: bool = False):
|
def __init__(self, document: str, variables: Dict[str, Any],
|
||||||
self.debug = debug
|
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.workbook = None
|
||||||
self.template_filepath = None
|
self.template_filepath = None
|
||||||
self.output_filepath = None
|
self.output_filepath = None
|
||||||
self.current_sheet = 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._style_manager = None
|
||||||
self._merge_manager = None
|
self._merge_manager = None
|
||||||
self._image_manager = None
|
self._image_manager = None
|
||||||
self._conditional_manager = None
|
self._conditional_manager = None
|
||||||
self._page_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:
|
try:
|
||||||
self.username = username
|
# document base を設定
|
||||||
self.project_id = project_id
|
|
||||||
self.language = lang
|
|
||||||
|
|
||||||
# Setup directory structure
|
|
||||||
self.docpath = docbase
|
self.docpath = docbase
|
||||||
self.docpath2 = f"{docbase}/{project_id}"
|
if not os.path.exists(docbase):
|
||||||
|
logging.error(f"Document base directory not found: {docbase}")
|
||||||
|
|
||||||
|
# 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}")
|
||||||
|
|
||||||
# Create directories if they don't exist
|
# basic section をロード
|
||||||
for path in [docbase, self.docpath2]:
|
basic = self.conf.get_section("basic")
|
||||||
if not os.path.exists(path):
|
if not basic:
|
||||||
os.makedirs(path, mode=0o755)
|
logging.error(f"Basic section not found in INI file: {self.ini_file_path}")
|
||||||
|
|
||||||
# Load template
|
self.basic = basic
|
||||||
inifile = f"{document}.ini"
|
|
||||||
self.inifilepath = f"{self.docpath2}/{inifile}"
|
|
||||||
|
|
||||||
if not os.path.exists(self.inifilepath):
|
# basicセクションから必要なパラメータを取得
|
||||||
return {"status": "NCK", "message": f"INI file not found: {self.inifilepath}"}
|
template_file = basic.get("template_file")
|
||||||
|
if not template_file:
|
||||||
|
logging.error("template_file not found in basic section")
|
||||||
|
|
||||||
# Load template workbook
|
doc_file = basic.get("doc_file")
|
||||||
template_file = self._get_ini_param("basic", f"templatefile_{lang}")
|
if not doc_file:
|
||||||
self.template_filepath = f"{self.docpath2}/{template_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):
|
if not os.path.exists(self.template_filepath):
|
||||||
# Copy from default if not exists
|
logging.error(f"Template file not found: {self.template_filepath}")
|
||||||
default_template = f"{self.docpath}/{template_file}"
|
|
||||||
shutil.copy2(default_template, self.template_filepath)
|
|
||||||
|
|
||||||
self.workbook = openpyxl.load_workbook(self.template_filepath)
|
self.template_workbook = openpyxl.load_workbook(self.template_filepath)
|
||||||
|
self.template_sheet = self.template_workbook.active
|
||||||
return {"status": "ACK"}
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"status": "NCK", "message": str(e)}
|
logging.error(f"Error initializing Excel document: {str(e)}")
|
||||||
|
|
||||||
|
def make_report(self,variables: Dict[str, Any]):
|
||||||
|
"""レポートを生成する"""
|
||||||
|
logging.info("make_report step-1")
|
||||||
|
try:
|
||||||
|
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 _get_ini_param(self, section: str, param: str) -> Optional[str]:
|
# 生成したワークブックを保存
|
||||||
"""Get parameter from INI file
|
self.workbook.save(self.output_filepath)
|
||||||
|
return {"status": True, "message": f"Report generated successfully : {self.output_filepath}", "filepath":self.output_filepath}
|
||||||
|
|
||||||
Args:
|
except Exception as e:
|
||||||
section: INI file section
|
return {"status": False, "message": f"Exception in make_report: Error generating report: {str(e)}"}
|
||||||
param: Parameter name
|
|
||||||
|
def proceed_section(self, section: str, variables: Dict[str, Any]):
|
||||||
|
logging.info(f"make_report.proceed_section step-1:section={section}")
|
||||||
|
|
||||||
Returns:
|
|
||||||
Parameter value or None if not found
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
# Use configparser to handle INI files
|
# セクションの設定を取得
|
||||||
import configparser
|
section_config = self.conf.get_section(section)
|
||||||
config = configparser.ConfigParser()
|
# セクションが存在しない場合はスキップ
|
||||||
config.read(self.inifilepath)
|
if not section_config:
|
||||||
return config[section][param]
|
return {"status": False, "message": f"Error no section found: {section}"}
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def make_report(self, db, data_rec: Dict[str, Any],
|
# テンプレートシートをコピー
|
||||||
out_filename: Optional[str] = None,
|
template_sheet_name = section_config.get("template_sheet")
|
||||||
screen_index: int = 0) -> None:
|
if not template_sheet_name or template_sheet_name not in self.template_workbook.sheetnames:
|
||||||
"""Generate Excel report from template
|
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:
|
Args:
|
||||||
db: Database connection
|
table: テーブル名
|
||||||
data_rec: Data records to populate report
|
where: WHERE句
|
||||||
out_filename: Optional output filename
|
group_range: 処理対象範囲
|
||||||
screen_index: Screen index for multi-screen reports
|
variables: DB接続情報を含む変数辞書
|
||||||
"""
|
"""
|
||||||
# Get output filename
|
logging.info(f"make_report.proceed_one_record step-1:table={table},where={where},group_range={group_range}")
|
||||||
if out_filename:
|
|
||||||
outfile = f"{out_filename}_{self._get_ini_param('basic', 'doc_file')}"
|
|
||||||
else:
|
|
||||||
outfile = self._get_ini_param('basic', 'doc_file')
|
|
||||||
|
|
||||||
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
|
cursor = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
|
||||||
sections = self._get_ini_param('basic', 'sections')
|
|
||||||
if not sections:
|
# SQLクエリを実行
|
||||||
return
|
query = f"SELECT * FROM {table} WHERE {where} LIMIT 1"
|
||||||
|
cursor.execute(query)
|
||||||
for section in sections.split(','):
|
record = cursor.fetchone()
|
||||||
self._process_section(section, db, data_rec, screen_index)
|
|
||||||
|
if record:
|
||||||
# Save workbook
|
# group_rangeの範囲内のセルを走査
|
||||||
self.workbook.save(self.output_filepath)
|
for row in self.current_worksheet:
|
||||||
|
for cell in row:
|
||||||
def _process_section(self, section: str, db, data_rec: Dict[str, Any],
|
if cell.value and isinstance(cell.value, str):
|
||||||
screen_index: int) -> None:
|
# [field_name]形式の文字列を検索
|
||||||
"""Process individual section of report
|
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
|
||||||
|
|
||||||
|
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:
|
Args:
|
||||||
section: Section name
|
table: テーブル名
|
||||||
db: Database connection
|
where: WHERE句
|
||||||
data_rec: Data records
|
sort: ORDER BY句
|
||||||
screen_index: Screen index
|
group_range: 処理対象範囲
|
||||||
"""
|
variables: DB接続情報を含む変数辞書
|
||||||
# Get template sheet
|
"""
|
||||||
sheet_orig = self._get_ini_param(section, 'sheet')
|
logging.info(f"make_report.proceed_all_record step-1:table={table},where={where},group_range={group_range}")
|
||||||
sheet_name = self._get_ini_param(section, f"sheetname_{self.language}")
|
|
||||||
|
|
||||||
if not sheet_orig or not sheet_name:
|
try:
|
||||||
return
|
# グループ範囲の行数を取得
|
||||||
|
start_row, end_row = map(int, group_range.split(':')[0].split(','))
|
||||||
|
template_rows = end_row - start_row + 1
|
||||||
|
|
||||||
# Copy template sheet
|
cursor = self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor)
|
||||||
template_sheet = self.workbook[sheet_orig]
|
|
||||||
new_sheet = self.workbook.copy_worksheet(template_sheet)
|
# SQLクエリを実行
|
||||||
new_sheet.title = sheet_name
|
query = f"SELECT * FROM {table} WHERE {where} ORDER BY {sort}"
|
||||||
|
cursor.execute(query)
|
||||||
|
records = cursor.fetchall()
|
||||||
|
|
||||||
|
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}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# コピーした範囲内のセルを走査して値を置換
|
||||||
|
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)}"}
|
||||||
|
|
||||||
# Process groups
|
def copy_template_to_current(self,range,target_range):
|
||||||
groups = self._get_ini_param(section, 'groups')
|
# テンプレートシートから現在のシートにデータをコピー
|
||||||
if groups:
|
source_range = self.template_sheet.range(range) # コピーする範囲を指定
|
||||||
for group in groups.split(','):
|
target_range = self.current_sheet.range(target_range) # 貼り付け先の範囲を指定
|
||||||
self._process_group(new_sheet, section, group, db, data_rec, screen_index)
|
|
||||||
|
# 値、数式、フォーマットをコピー
|
||||||
def _process_group(self, sheet, section: str, group: str,
|
source_range.copy(target_range)
|
||||||
db, data_rec: Dict[str, Any], screen_index: int) -> None:
|
|
||||||
"""Process group within section
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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
|
# Style operations
|
||||||
def apply_style(
|
def apply_style(
|
||||||
self,
|
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
|
from sumaexcel import SumasenExcel
|
||||||
|
import logging
|
||||||
|
|
||||||
# 初期化
|
# 初期化
|
||||||
excel = SumasenExcel()
|
variables = {"zekken_number":"5033","event_code":"FC岐阜"}
|
||||||
excel.init("username", "project_id", "document")
|
excel = SumasenExcel(document="test", variables=variables, docbase="./testdata")
|
||||||
|
|
||||||
|
logging.info("Excelファイル作成 step-1")
|
||||||
|
|
||||||
# シート初期化
|
# シート初期化
|
||||||
excel.init_sheet("Sheet1")
|
ret = excel.make_report(variables=variables)
|
||||||
|
logging.info(f"Excelファイル作成 step-2 : ret={ret}")
|
||||||
# スタイル適用
|
if ret["status"]==True:
|
||||||
excel.apply_style(
|
filepath=ret["filepath"]
|
||||||
"A1:D10",
|
logging.info(f"Excelファイル作成 : ret.filepath={filepath}")
|
||||||
font={"name": "Arial", "size": 12, "bold": True},
|
else:
|
||||||
fill={"start_color": "FFFF00"},
|
message = ret.get("message", "No message provided")
|
||||||
alignment={"horizontal": "center"}
|
logging.error(f"Excelファイル作成失敗 : ret.message={message}")
|
||||||
)
|
|
||||||
|
|
||||||
# セルのマージ
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
17
SumasenLibs/excel_lib/testdata/test.ini
vendored
17
SumasenLibs/excel_lib/testdata/test.ini
vendored
@ -1,25 +1,24 @@
|
|||||||
[basic]
|
[basic]
|
||||||
templatefile_jp="certificate_template.xlsx"
|
template_file=certificate_template.xlsx
|
||||||
doc_file="certificate_[zekken_number].xlsx"
|
doc_file=certificate_[zekken_number].xlsx
|
||||||
sections=section1,section2
|
sections=section1,section2
|
||||||
developer=Sumasen
|
|
||||||
maxcol=8
|
maxcol=8
|
||||||
|
|
||||||
[section1]
|
[section1]
|
||||||
sheet="certificate"
|
template_sheet=certificate
|
||||||
sheetname_jp="岐阜ロゲ通過証明書"
|
groups=group1,group2
|
||||||
groups="group1,group2"
|
|
||||||
fit_to_width=1
|
fit_to_width=1
|
||||||
orientation=portrait
|
orientation=portrait
|
||||||
|
|
||||||
[section1.group1]
|
[section1.group1]
|
||||||
table_name=rog_entry
|
table_name=rog_entry
|
||||||
where="zekken_number='[zekken_number]' and event_code='[event_code]'"
|
where=zekken_number='[zekken_number]' and event_code='[event_code]'
|
||||||
group_range="0,0,8,11"
|
group_range=0,0,8,11
|
||||||
|
|
||||||
|
|
||||||
[section1.group2]
|
[section1.group2]
|
||||||
table_name=gps_checkins
|
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
|
sort=order
|
||||||
group_range=0,12,8,12
|
group_range=0,12,8,12
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user