diff --git a/SumasenLibs/excel_lib/README.md b/SumasenLibs/excel_lib/README.md new file mode 100644 index 0000000..967e9c8 --- /dev/null +++ b/SumasenLibs/excel_lib/README.md @@ -0,0 +1,19 @@ +# SumasenExcel Library + +Excel操作のためのシンプルなPythonライブラリです。 + +## インストール方法 + +```bash +pip install -e . + +## 使用方法 +from sumaexcel import SumasenExcel + +excel = SumasenExcel("path/to/file.xlsx") +data = excel.read_excel() + +## ライセンス + +MIT License + diff --git a/SumasenLibs/excel_lib/docker/docker-compose.yml b/SumasenLibs/excel_lib/docker/docker-compose.yml new file mode 100644 index 0000000..6355d86 --- /dev/null +++ b/SumasenLibs/excel_lib/docker/docker-compose.yml @@ -0,0 +1,14 @@ +version: '3.8' + +services: + python: + build: + context: .. + dockerfile: docker/python/Dockerfile + volumes: + - ..:/app + environment: + - PYTHONPATH=/app + command: /bin/bash + tty: true + diff --git a/SumasenLibs/excel_lib/docker/python/Dockerfile b/SumasenLibs/excel_lib/docker/python/Dockerfile new file mode 100644 index 0000000..01d4495 --- /dev/null +++ b/SumasenLibs/excel_lib/docker/python/Dockerfile @@ -0,0 +1,27 @@ +FROM python:3.9-slim + +WORKDIR /app + +# 必要なシステムパッケージのインストール +RUN apt-get update && apt-get install -y \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Pythonパッケージのインストール +COPY requirements.txt . +COPY setup.py . +COPY README.md . +COPY . . + +RUN pip install --no-cache-dir -r requirements.txt + +# 開発用パッケージのインストール +RUN pip install --no-cache-dir \ + pytest \ + pytest-cov \ + flake8 + + +# パッケージのインストール +RUN pip install -e . + diff --git a/SumasenLibs/excel_lib/requirements.txt b/SumasenLibs/excel_lib/requirements.txt new file mode 100644 index 0000000..3522b34 --- /dev/null +++ b/SumasenLibs/excel_lib/requirements.txt @@ -0,0 +1,5 @@ +openpyxl>=3.0.0 +pandas>=1.0.0 +pillow>=8.0.0 +configparser>=5.0.0 + diff --git a/SumasenLibs/excel_lib/setup.py b/SumasenLibs/excel_lib/setup.py new file mode 100644 index 0000000..1886912 --- /dev/null +++ b/SumasenLibs/excel_lib/setup.py @@ -0,0 +1,25 @@ +# setup.py +from setuptools import setup, find_packages + +setup( + name="sumaexcel", + version="0.1.0", + packages=find_packages(), + install_requires=[ + "openpyxl>=3.0.0", + "pandas>=1.0.0" + ], + author="Akira Miyata", + author_email="akira.miyata@sumasen.net", + description="Excel handling library", + long_description=open("README.md").read(), + long_description_content_type="text/markdown", + url="https://github.com/akiramiyata/sumaexcel", + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + python_requires=">=3.6", +) + diff --git a/SumasenLibs/excel_lib/sumaexcel/__init__.py b/SumasenLibs/excel_lib/sumaexcel/__init__.py new file mode 100644 index 0000000..13d1827 --- /dev/null +++ b/SumasenLibs/excel_lib/sumaexcel/__init__.py @@ -0,0 +1,4 @@ +from .sumaexcel import SumasenExcel + +__version__ = "0.1.0" +__all__ = ["SumasenExcel"] diff --git a/SumasenLibs/excel_lib/sumaexcel/conditional.py b/SumasenLibs/excel_lib/sumaexcel/conditional.py new file mode 100644 index 0000000..212415c --- /dev/null +++ b/SumasenLibs/excel_lib/sumaexcel/conditional.py @@ -0,0 +1,102 @@ +# sumaexcel/conditional.py +from typing import Dict, Any, List, Union +from openpyxl.formatting.rule import Rule, ColorScaleRule, DataBarRule, IconSetRule +from openpyxl.styles import PatternFill, Font, Border, Side +from openpyxl.worksheet.worksheet import Worksheet + +class ConditionalFormatManager: + """Handle conditional formatting in Excel""" + + def __init__(self, worksheet: Worksheet): + self.worksheet = worksheet + + def add_color_scale( + self, + cell_range: str, + min_color: str = "00FF0000", # Red + mid_color: str = "00FFFF00", # Yellow + max_color: str = "0000FF00" # Green + ) -> None: + """Add color scale conditional formatting""" + rule = ColorScaleRule( + start_type='min', + start_color=min_color, + mid_type='percentile', + mid_value=50, + mid_color=mid_color, + end_type='max', + end_color=max_color + ) + self.worksheet.conditional_formatting.add(cell_range, rule) + + def add_data_bar( + self, + cell_range: str, + color: str = "000000FF", # Blue + show_value: bool = True + ) -> None: + """Add data bar conditional formatting""" + rule = DataBarRule( + start_type='min', + end_type='max', + color=color, + showValue=show_value + ) + self.worksheet.conditional_formatting.add(cell_range, rule) + + def add_icon_set( + self, + cell_range: str, + icon_style: str = '3Arrows', # '3Arrows', '3TrafficLights', '3Signs' + reverse_icons: bool = False + ) -> None: + """Add icon set conditional formatting""" + rule = IconSetRule( + icon_style=icon_style, + type='percent', + values=[0, 33, 67], + reverse_icons=reverse_icons + ) + self.worksheet.conditional_formatting.add(cell_range, rule) + + def add_custom_rule( + self, + cell_range: str, + rule_type: str, + formula: str, + fill_color: str = None, + font_color: str = None, + bold: bool = None, + border_style: str = None, + border_color: str = None + ) -> None: + """Add custom conditional formatting rule""" + dxf = {} + if fill_color: + dxf['fill'] = PatternFill(start_color=fill_color, end_color=fill_color) + if font_color or bold is not None: + dxf['font'] = Font(color=font_color, bold=bold) + if border_style and border_color: + side = Side(style=border_style, color=border_color) + dxf['border'] = Border(left=side, right=side, top=side, bottom=side) + + rule = Rule(type=rule_type, formula=[formula], dxf=dxf) + self.worksheet.conditional_formatting.add(cell_range, rule) + + def copy_conditional_format( + self, + source_range: str, + target_range: str + ) -> None: + """Copy conditional formatting from one range to another""" + source_rules = self.worksheet.conditional_formatting.get(source_range) + if source_rules: + for rule in source_rules: + self.worksheet.conditional_formatting.add(target_range, rule) + + def clear_conditional_format( + self, + cell_range: str + ) -> None: + """Clear conditional formatting from specified range""" + self.worksheet.conditional_formatting.delete(cell_range) diff --git a/SumasenLibs/excel_lib/sumaexcel/image.py b/SumasenLibs/excel_lib/sumaexcel/image.py new file mode 100644 index 0000000..396bf63 --- /dev/null +++ b/SumasenLibs/excel_lib/sumaexcel/image.py @@ -0,0 +1,77 @@ +# sumaexcel/image.py +from typing import Optional, Tuple, Union +from pathlib import Path +import os +from PIL import Image +from openpyxl.drawing.image import Image as XLImage +from openpyxl.worksheet.worksheet import Worksheet + +class ImageManager: + """Handle image operations in Excel""" + + def __init__(self, worksheet: Worksheet): + self.worksheet = worksheet + self.temp_dir = Path("/tmp/sumaexcel_images") + self.temp_dir.mkdir(parents=True, exist_ok=True) + + def add_image( + self, + image_path: Union[str, Path], + cell_coordinates: Tuple[int, int], + size: Optional[Tuple[int, int]] = None, + keep_aspect_ratio: bool = True, + anchor_type: str = 'absolute' + ) -> None: + """Add image to worksheet at specified position""" + # Convert path to Path object + image_path = Path(image_path) + + # Open and process image + with Image.open(image_path) as img: + # Get original size + orig_width, orig_height = img.size + + # Calculate new size if specified + if size: + target_width, target_height = size + if keep_aspect_ratio: + ratio = min(target_width/orig_width, target_height/orig_height) + target_width = int(orig_width * ratio) + target_height = int(orig_height * ratio) + + # Resize image + img = img.resize((target_width, target_height), Image.LANCZOS) + + # Save temporary resized image + temp_path = self.temp_dir / f"temp_{image_path.name}" + img.save(temp_path) + image_path = temp_path + + # Create Excel image object + excel_image = XLImage(str(image_path)) + + # Add to worksheet + self.worksheet.add_image(excel_image, anchor=f'{cell_coordinates[0]}{cell_coordinates[1]}') + + def add_image_absolute( + self, + image_path: Union[str, Path], + position: Tuple[int, int], + size: Optional[Tuple[int, int]] = None + ) -> None: + """Add image with absolute positioning""" + excel_image = XLImage(str(image_path)) + if size: + excel_image.width, excel_image.height = size + excel_image.anchor = 'absolute' + excel_image.top, excel_image.left = position + self.worksheet.add_image(excel_image) + + def cleanup(self) -> None: + """Clean up temporary files""" + for file in self.temp_dir.glob("temp_*"): + file.unlink() + + def __del__(self): + """Cleanup on object destruction""" + self.cleanup() diff --git a/SumasenLibs/excel_lib/sumaexcel/merge.py b/SumasenLibs/excel_lib/sumaexcel/merge.py new file mode 100644 index 0000000..fc15216 --- /dev/null +++ b/SumasenLibs/excel_lib/sumaexcel/merge.py @@ -0,0 +1,96 @@ +# sumaexcel/merge.py +from typing import List, Tuple, Dict +from openpyxl.worksheet.worksheet import Worksheet +from openpyxl.worksheet.merge import MergedCellRange + +class MergeManager: + """Handle merge cell operations""" + + def __init__(self, worksheet: Worksheet): + self.worksheet = worksheet + self._merged_ranges: List[MergedCellRange] = [] + self._load_merged_ranges() + + def _load_merged_ranges(self) -> None: + """Load existing merged ranges from worksheet""" + self._merged_ranges = list(self.worksheet.merged_cells.ranges) + + def merge_cells( + self, + start_row: int, + start_col: int, + end_row: int, + end_col: int + ) -> None: + """Merge cells in specified range""" + self.worksheet.merge_cells( + start_row=start_row, + start_column=start_col, + end_row=end_row, + end_column=end_col + ) + self._load_merged_ranges() + + def unmerge_cells( + self, + start_row: int, + start_col: int, + end_row: int, + end_col: int + ) -> None: + """Unmerge cells in specified range""" + self.worksheet.unmerge_cells( + start_row=start_row, + start_column=start_col, + end_row=end_row, + end_column=end_col + ) + self._load_merged_ranges() + + def copy_merged_cells( + self, + source_range: Tuple[int, int, int, int], + target_start_row: int, + target_start_col: int + ) -> None: + """Copy merged cells from source range to target position""" + src_row1, src_col1, src_row2, src_col2 = source_range + row_offset = target_start_row - src_row1 + col_offset = target_start_col - src_col1 + + for merged_range in self._merged_ranges: + if (src_row1 <= merged_range.min_row <= src_row2 and + src_col1 <= merged_range.min_col <= src_col2): + new_row1 = merged_range.min_row + row_offset + new_col1 = merged_range.min_col + col_offset + new_row2 = merged_range.max_row + row_offset + new_col2 = merged_range.max_col + col_offset + + self.merge_cells(new_row1, new_col1, new_row2, new_col2) + + def shift_merged_cells( + self, + start_row: int, + rows: int = 0, + cols: int = 0 + ) -> None: + """Shift merged cells by specified number of rows and columns""" + new_ranges = [] + for merged_range in self._merged_ranges: + if merged_range.min_row >= start_row: + new_row1 = merged_range.min_row + rows + new_col1 = merged_range.min_col + cols + new_row2 = merged_range.max_row + rows + new_col2 = merged_range.max_col + cols + + self.worksheet.unmerge_cells( + start_row=merged_range.min_row, + start_column=merged_range.min_col, + end_row=merged_range.max_row, + end_column=merged_range.max_col + ) + + new_ranges.append((new_row1, new_col1, new_row2, new_col2)) + + for new_range in new_ranges: + self.merge_cells(*new_range) diff --git a/SumasenLibs/excel_lib/sumaexcel/page.py b/SumasenLibs/excel_lib/sumaexcel/page.py new file mode 100644 index 0000000..361a60b --- /dev/null +++ b/SumasenLibs/excel_lib/sumaexcel/page.py @@ -0,0 +1,148 @@ +# sumaexcel/page.py +from typing import Optional, Dict, Any, Union +from openpyxl.worksheet.worksheet import Worksheet +from openpyxl.worksheet.page import PageMargins, PrintPageSetup + +# sumaexcel/page.py (continued) + +class PageManager: + """Handle page setup and header/footer settings""" + + def __init__(self, worksheet: Worksheet): + self.worksheet = worksheet + + def set_page_setup( + self, + orientation: str = 'portrait', + paper_size: int = 9, # A4 + fit_to_height: Optional[int] = None, + fit_to_width: Optional[int] = None, + scale: Optional[int] = None + ) -> None: + """Configure page setup + + Args: + orientation: 'portrait' or 'landscape' + paper_size: paper size (e.g., 9 for A4) + fit_to_height: number of pages tall + fit_to_width: number of pages wide + scale: zoom scale (1-400) + """ + setup = PrintPageSetup( + orientation=orientation, + paperSize=paper_size, + scale=scale, + fitToHeight=fit_to_height, + fitToWidth=fit_to_width + ) + self.worksheet.page_setup = setup + + def set_margins( + self, + left: float = 0.7, + right: float = 0.7, + top: float = 0.75, + bottom: float = 0.75, + header: float = 0.3, + footer: float = 0.3 + ) -> None: + """Set page margins in inches""" + margins = PageMargins( + left=left, + right=right, + top=top, + bottom=bottom, + header=header, + footer=footer + ) + self.worksheet.page_margins = margins + + def set_header_footer( + self, + odd_header: Optional[str] = None, + odd_footer: Optional[str] = None, + even_header: Optional[str] = None, + even_footer: Optional[str] = None, + first_header: Optional[str] = None, + first_footer: Optional[str] = None, + different_first: bool = False, + different_odd_even: bool = False + ) -> None: + """Set headers and footers + + Format codes: + - &P: Page number + - &N: Total pages + - &D: Date + - &T: Time + - &[Tab]: Sheet name + - &[Path]: File path + - &[File]: File name + - &[Tab]: Worksheet name + """ + self.worksheet.oddHeader.left = odd_header or "" + self.worksheet.oddFooter.left = odd_footer or "" + + if different_odd_even: + self.worksheet.evenHeader.left = even_header or "" + self.worksheet.evenFooter.left = even_footer or "" + + if different_first: + self.worksheet.firstHeader.left = first_header or "" + self.worksheet.firstFooter.left = first_footer or "" + + self.worksheet.differentFirst = different_first + self.worksheet.differentOddEven = different_odd_even + + def set_print_area(self, range_string: str) -> None: + """Set print area + + Args: + range_string: Cell range in A1 notation (e.g., 'A1:H42') + """ + self.worksheet.print_area = range_string + + def set_print_title_rows(self, rows: str) -> None: + """Set rows to repeat at top of each page + + Args: + rows: Row range (e.g., '1:3') + """ + self.worksheet.print_title_rows = rows + + def set_print_title_columns(self, cols: str) -> None: + """Set columns to repeat at left of each page + + Args: + cols: Column range (e.g., 'A:B') + """ + self.worksheet.print_title_cols = cols + + def set_print_options( + self, + grid_lines: bool = False, + horizontal_centered: bool = False, + vertical_centered: bool = False, + headers: bool = False + ) -> None: + """Set print options""" + self.worksheet.print_gridlines = grid_lines + self.worksheet.print_options.horizontalCentered = horizontal_centered + self.worksheet.print_options.verticalCentered = vertical_centered + self.worksheet.print_options.headers = headers + +class PaperSizes: + """Standard paper size constants""" + LETTER = 1 + LETTER_SMALL = 2 + TABLOID = 3 + LEDGER = 4 + LEGAL = 5 + STATEMENT = 6 + EXECUTIVE = 7 + A3 = 8 + A4 = 9 + A4_SMALL = 10 + A5 = 11 + B4 = 12 + B5 = 13 diff --git a/SumasenLibs/excel_lib/sumaexcel/styles.py b/SumasenLibs/excel_lib/sumaexcel/styles.py new file mode 100644 index 0000000..18f49a8 --- /dev/null +++ b/SumasenLibs/excel_lib/sumaexcel/styles.py @@ -0,0 +1,115 @@ +# sumaexcel/styles.py +from typing import Dict, Any, Optional, Union +from openpyxl.styles import ( + Font, PatternFill, Alignment, Border, Side, + NamedStyle, Protection, Color +) +from openpyxl.styles.differential import DifferentialStyle +from openpyxl.formatting.rule import Rule +from openpyxl.worksheet.worksheet import Worksheet + +class StyleManager: + """Excel style management class""" + + @staticmethod + def create_font( + name: str = "Arial", + size: int = 11, + bold: bool = False, + italic: bool = False, + color: str = "000000", + underline: str = None, + strike: bool = False + ) -> Font: + """Create a Font object with specified parameters""" + return Font( + name=name, + size=size, + bold=bold, + italic=italic, + color=color, + underline=underline, + strike=strike + ) + + @staticmethod + def create_fill( + fill_type: str = "solid", + start_color: str = "FFFFFF", + end_color: str = None + ) -> PatternFill: + """Create a PatternFill object""" + return PatternFill( + fill_type=fill_type, + start_color=start_color, + end_color=end_color or start_color + ) + + @staticmethod + def create_border( + style: str = "thin", + color: str = "000000" + ) -> Border: + """Create a Border object""" + side = Side(style=style, color=color) + return Border( + left=side, + right=side, + top=side, + bottom=side + ) + + @staticmethod + def create_alignment( + horizontal: str = "general", + vertical: str = "bottom", + wrap_text: bool = False, + shrink_to_fit: bool = False, + indent: int = 0 + ) -> Alignment: + """Create an Alignment object""" + return Alignment( + horizontal=horizontal, + vertical=vertical, + wrap_text=wrap_text, + shrink_to_fit=shrink_to_fit, + indent=indent + ) + + @staticmethod + def copy_style(source_cell: Any, target_cell: Any) -> None: + """Copy all style properties from source cell to target cell""" + target_cell.font = Font( + name=source_cell.font.name, + size=source_cell.font.size, + bold=source_cell.font.bold, + italic=source_cell.font.italic, + color=source_cell.font.color, + underline=source_cell.font.underline, + strike=source_cell.font.strike + ) + + if source_cell.fill.patternType != None: + target_cell.fill = PatternFill( + fill_type=source_cell.fill.patternType, + start_color=source_cell.fill.start_color.rgb, + end_color=source_cell.fill.end_color.rgb + ) + + target_cell.border = Border( + left=source_cell.border.left, + right=source_cell.border.right, + top=source_cell.border.top, + bottom=source_cell.border.bottom + ) + + target_cell.alignment = Alignment( + horizontal=source_cell.alignment.horizontal, + vertical=source_cell.alignment.vertical, + wrap_text=source_cell.alignment.wrap_text, + shrink_to_fit=source_cell.alignment.shrink_to_fit, + indent=source_cell.alignment.indent + ) + + if source_cell.number_format: + target_cell.number_format = source_cell.number_format diff --git a/SumasenLibs/excel_lib/sumaexcel/sumaexcel.py b/SumasenLibs/excel_lib/sumaexcel/sumaexcel.py new file mode 100644 index 0000000..24581e5 --- /dev/null +++ b/SumasenLibs/excel_lib/sumaexcel/sumaexcel.py @@ -0,0 +1,276 @@ +# sumaexcel/excel.py + +import openpyxl +from openpyxl.styles import Font, PatternFill, Alignment, Border, Side +from openpyxl.utils import get_column_letter +import pandas as pd +from typing import Optional, Dict, List, Union, Any +import os +import shutil +from datetime import datetime +from typing import Optional, Dict, Any, Union, Tuple +from pathlib import Path + +from .styles import StyleManager +from .merge import MergeManager +from .image import ImageManager +from .conditional import ConditionalFormatManager +from .page import PageManager, PaperSizes + +class SumasenExcel: + """Enhanced Excel handling class with extended functionality""" + + def __init__(self, debug: bool = False): + self.debug = debug + self.workbook = None + self.template_filepath = None + self.output_filepath = None + self.current_sheet = 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 + self.docpath = docbase + self.docpath2 = f"{docbase}/{project_id}" + + # Create directories if they don't exist + for path in [docbase, self.docpath2]: + if not os.path.exists(path): + os.makedirs(path, mode=0o755) + + # Load template + inifile = f"{document}.ini" + self.inifilepath = f"{self.docpath2}/{inifile}" + + if not os.path.exists(self.inifilepath): + return {"status": "NCK", "message": f"INI file not found: {self.inifilepath}"} + + # Load template workbook + template_file = self._get_ini_param("basic", f"templatefile_{lang}") + self.template_filepath = f"{self.docpath2}/{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) + + self.workbook = openpyxl.load_workbook(self.template_filepath) + + return {"status": "ACK"} + + except Exception as e: + return {"status": "NCK", "message": 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 + """ + try: + # Use configparser to handle INI files + import configparser + config = configparser.ConfigParser() + config.read(self.inifilepath) + return config[section][param] + except: + return None + + 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 + + Args: + db: Database connection + data_rec: Data records to populate report + out_filename: Optional output filename + screen_index: Screen index for multi-screen reports + """ + # 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') + + self.output_filepath = f"{self.docpath2}/{outfile}" + + # Process sections + sections = self._get_ini_param('basic', 'sections') + if not sections: + return + + for section in sections.split(','): + self._process_section(section, db, data_rec, screen_index) + + # Save workbook + self.workbook.save(self.output_filepath) + + def _process_section(self, section: str, db, data_rec: Dict[str, Any], + screen_index: int) -> None: + """Process individual section of report + + Args: + section: Section name + db: Database connection + data_rec: Data records + screen_index: Screen index + """ + # Get template sheet + sheet_orig = self._get_ini_param(section, 'sheet') + sheet_name = self._get_ini_param(section, f"sheetname_{self.language}") + + if not sheet_orig or not sheet_name: + return + + # Copy template sheet + template_sheet = self.workbook[sheet_orig] + new_sheet = self.workbook.copy_worksheet(template_sheet) + new_sheet.title = sheet_name + + # 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) + + def _process_group(self, sheet, section: str, group: str, + 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 + def apply_style( + self, + cell_range: str, + font: Dict[str, Any] = None, + fill: Dict[str, Any] = None, + border: Dict[str, Any] = None, + alignment: Dict[str, Any] = None + ) -> None: + """Apply styles to cell range""" + for row in self.current_sheet[cell_range]: + for cell in row: + if font: + cell.font = self._style_manager.create_font(**font) + if fill: + cell.fill = self._style_manager.create_fill(**fill) + if border: + cell.border = self._style_manager.create_border(**border) + if alignment: + cell.alignment = self._style_manager.create_alignment(**alignment) + + # Merge operations + def merge_range( + self, + start_row: int, + start_col: int, + end_row: int, + end_col: int + ) -> None: + """Merge cell range""" + self._merge_manager.merge_cells(start_row, start_col, end_row, end_col) + + # Image operations + def add_image( + self, + image_path: Union[str, Path], + position: Tuple[int, int], + size: Optional[Tuple[int, int]] = None + ) -> None: + """Add image to worksheet""" + self._image_manager.add_image(image_path, position, size) + + # Conditional formatting + def add_conditional_format( + self, + cell_range: str, + format_type: str, + **kwargs + ) -> None: + """Add conditional formatting""" + if format_type == 'color_scale': + self._conditional_manager.add_color_scale(cell_range, **kwargs) + elif format_type == 'data_bar': + self._conditional_manager.add_data_bar(cell_range, **kwargs) + elif format_type == 'icon_set': + self._conditional_manager.add_icon_set(cell_range, **kwargs) + elif format_type == 'custom': + self._conditional_manager.add_custom_rule(cell_range, **kwargs) + + # Page setup + def setup_page( + self, + orientation: str = 'portrait', + paper_size: int = PaperSizes.A4, + margins: Dict[str, float] = None, + header_footer: Dict[str, Any] = None + ) -> None: + """Configure page setup""" + self._page_manager.set_page_setup( + orientation=orientation, + paper_size=paper_size + ) + + if margins: + self._page_manager.set_margins(**margins) + + if header_footer: + self._page_manager.set_header_footer(**header_footer) + + def cleanup(self) -> None: + """Cleanup temporary files""" + if self._image_manager: + self._image_manager.cleanup() + + def __del__(self): + """Destructor""" + self.cleanup() diff --git a/SumasenLibs/excel_lib/testdata/sample.py b/SumasenLibs/excel_lib/testdata/sample.py new file mode 100644 index 0000000..3e8c542 --- /dev/null +++ b/SumasenLibs/excel_lib/testdata/sample.py @@ -0,0 +1,55 @@ +from sumaexcel import SumasenExcel + +# 初期化 +excel = SumasenExcel() +excel.init("username", "project_id", "document") + +# シート初期化 +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) + + diff --git a/SumasenLibs/excel_lib/testdata/test.ini b/SumasenLibs/excel_lib/testdata/test.ini new file mode 100644 index 0000000..a75b2f2 --- /dev/null +++ b/SumasenLibs/excel_lib/testdata/test.ini @@ -0,0 +1,25 @@ +[basic] +templatefile_jp="certificate_template.xlsx" +doc_file="certificate_[zekken_number].xlsx" +sections=section1,section2 +developer=Sumasen +maxcol=8 + +[section1] +sheet="certificate" +sheetname_jp="岐阜ロゲ通過証明書" +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" + +[section1.group2] +table_name=gps_checkins +where=""zekken_number='[zekken_number]' and event_code='[event_code]' +sort=order +group_range=0,12,8,12 +