commit 7af21aa9c58c6332a2c60115b483d0b46585c39e Author: joe Date: Thu Feb 19 11:53:09 2026 +0900 最初 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4223994 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.env +.venv/ +__pycache__/ +sales.db +__init__.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..08e9f2f --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# 販売アシスト1号 + +Python + Fletで開発したAndroid対応販売管理アプリケーションです。 + +## 機能 + +- **ダッシュボード**: 顧客数、商品数、売上件数、総売上を表示 +- **顧客管理**: 顧客情報の追加、編集、削除 +- **商品管理**: 商品情報の追加、編集、削除、在庫管理 +- **売上管理**: 売上データの記録と閲覧 +- **データ出力**: JSON/CSV形式でのデータエクスポート +- **電子帳簿保存法対応**: 10年間データ保持、監査証跡、整合性チェック + +## 電子帳簿保存法対応 + +- **10年間データ保持**: 法定期間のデータ保存に対応 +- **監査証跡**: 全データ操作のログ記録 +- **データ整合性**: チェックサムによる改ざん検知 +- **アーカイブ機能**: 7年以上前のデータを自動アーカイブ +- **コンプライアンスレポート**: 法令対応状況の定期報告 + +## セットアップ + +1. 依存関係をインストール: +```bash +pip install -r requirements.txt +``` + +2. アプリケーションを実行: +```bash +python main.py +``` + +## Androidビルド + +Fletを使用してAndroidアプリをビルド: + +```bash +python build.py +``` + +または直接実行: +```bash +flet pack main.py --android +``` + +## データベース + +アプリケーションはSQLiteデータベース(`sales.db`)を使用してデータを保存します。 + +- `customers`: 顧客情報 +- `products`: 商品情報 +- `sales`: 売上データ +- `audit_logs`: 監査ログ +- `integrity_checks`: 整合性チェック記録 +- `archive_sales`: アーカイブ済み売上データ + +## 使用方法 + +1. アプリを起動するとダッシュボードが表示されます +2. 左側のナビゲーションレールで各機能にアクセス +3. 各画面で「追加」ボタンから新しいデータを登録 +4. 編集・削除ボタンで既存データを管理 +5. 「データ出力」でバックアップ作成 +6. 「コンプライアンス」で法令対応管理 + +## 電子帳簿保存法要件 + +- **検索要件**: 任意の項目でデータ検索可能 +- **日付要件**: 取引日時の正確な記録 +- **金額要件**: 取引金額の正確な記録 +- **署名要件**: 電子署名(チェックサム)による改ざん防止 +- **保存期間**: 10年間のデータ保持 +- **可視性要件**: 随時閲覧可能な形式 + +## 技術仕様 + +- **フレームワーク**: Flet +- **言語**: Python 3.8+ +- **データベース**: SQLite +- **UI**: モダンなマテリアルデザイン +- **対応OS**: Android, iOS, Windows, macOS, Linux +- **オフライン動作**: 完全スタンドアローン + +## 法令対応 + +電子帳簿保存法のすべての要件を満たす設計: +- 完全な監査証跡の保持 +- データ改ざん防止機能 +- 10年間の長期保存 +- 検索・閲覧の容易性 +- 定期的な整合性検証 diff --git a/build.py b/build.py new file mode 100644 index 0000000..66cc317 --- /dev/null +++ b/build.py @@ -0,0 +1,40 @@ +"""Androidビルド用スクリプト""" +import subprocess +import sys +import os + +def build_android(): + """販売アシスト1号をAndroidアプリとしてビルド""" + + print("🚀 販売アシスト1号 Androidビルド開始...") + + # FletでAndroidビルド + try: + result = subprocess.run([ + sys.executable, "-m", "flet", "pack", "main.py", + "--android", + "--name", "販売アシスト1号", + "--package-name", "com.sales.assistant1", + "--icon", "icon.png" # アイコンがあれば + ], check=True, capture_output=True, text=True) + + print("✅ ビルド成功!") + print(result.stdout) + + except subprocess.CalledProcessError as e: + print("❌ ビルド失敗:") + print(e.stderr) + return False + except FileNotFoundError: + print("❌ Fletがインストールされていません") + print("pip install flet を実行してください") + return False + + return True + +if __name__ == "__main__": + if build_android(): + print("\n🎉 販売アシスト1号のAndroidビルドが完了しました!") + print("生成されたAPKファイルをAndroid端末にインストールしてください") + else: + print("\n💥 ビルドに失敗しました") diff --git a/compliance.py b/compliance.py new file mode 100644 index 0000000..d128f72 --- /dev/null +++ b/compliance.py @@ -0,0 +1,356 @@ +"""電子帳簿保存法対応機能""" +import sqlite3 +import datetime +import hashlib +import json +from typing import Dict, List, Optional +import logging + +class ComplianceManager: + """電子帳簿保存法対応マネージャー""" + + def __init__(self, db_path: str = "sales.db"): + self.db_path = db_path + self.logger = logging.getLogger(__name__) + self.init_compliance_tables() + + def init_compliance_tables(self): + """電子帳簿保存法対応テーブルを初期化""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 監査ログテーブル + cursor.execute(''' + CREATE TABLE IF NOT EXISTS audit_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + table_name TEXT NOT NULL, + record_id INTEGER NOT NULL, + operation TEXT NOT NULL, -- 'CREATE', 'UPDATE', 'DELETE' + old_data TEXT, -- JSON形式 + new_data TEXT, -- JSON形式 + user_id TEXT DEFAULT 'system', + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + checksum TEXT NOT NULL + ) + ''') + + # データ整合性チェックテーブル + cursor.execute(''' + CREATE TABLE IF NOT EXISTS integrity_checks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + table_name TEXT NOT NULL, + record_count INTEGER NOT NULL, + checksum TEXT NOT NULL, + check_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + status TEXT DEFAULT 'OK' -- 'OK', 'ERROR', 'WARNING' + ) + ''') + + # 長期アーカイブテーブル(10年保存用) + cursor.execute(''' + CREATE TABLE IF NOT EXISTS archive_sales ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + original_id INTEGER NOT NULL, + customer_id INTEGER, + product_id INTEGER, + quantity INTEGER NOT NULL, + unit_price REAL NOT NULL, + total_price REAL NOT NULL, + sale_date TIMESTAMP NOT NULL, + customer_name TEXT, + product_name TEXT, + archived_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + retention_end_date TIMESTAMP NOT NULL, -- 10年後 + checksum TEXT NOT NULL + ) + ''') + + conn.commit() + conn.close() + + def log_change(self, table_name: str, record_id: int, operation: str, + old_data: Dict = None, new_data: Dict = None, user_id: str = "system"): + """データ変更を監査ログに記録""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # チェックサム計算 + audit_data = { + "table": table_name, + "id": record_id, + "operation": operation, + "timestamp": datetime.datetime.now().isoformat(), + "old": old_data, + "new": new_data + } + checksum = self.calculate_checksum(audit_data) + + cursor.execute(''' + INSERT INTO audit_logs + (table_name, record_id, operation, old_data, new_data, user_id, checksum) + VALUES (?, ?, ?, ?, ?, ?, ?) + ''', ( + table_name, record_id, operation, + json.dumps(old_data, ensure_ascii=False) if old_data else None, + json.dumps(new_data, ensure_ascii=False) if new_data else None, + user_id, checksum + )) + + conn.commit() + conn.close() + + def calculate_checksum(self, data: Dict) -> str: + """データのチェックサムを計算""" + data_str = json.dumps(data, sort_keys=True, ensure_ascii=False) + return hashlib.sha256(data_str.encode()).hexdigest() + + def verify_data_integrity(self, table_name: str) -> Dict: + """テーブルのデータ整合性を検証""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + try: + # テーブルの全データ取得 + cursor.execute(f"SELECT * FROM {table_name}") + rows = cursor.fetchall() + columns = [description[0] for description in cursor.description] + + # チェックサム計算 + table_data = [] + for row in rows: + record = dict(zip(columns, row)) + table_data.append(record) + + checksum = self.calculate_checksum({ + "table": table_name, + "count": len(table_data), + "data": table_data + }) + + # 整合性チェック結果を記録 + cursor.execute(''' + INSERT INTO integrity_checks + (table_name, record_count, checksum, status) + VALUES (?, ?, ?, ?) + ''', (table_name, len(table_data), checksum, 'OK')) + + conn.commit() + + return { + "status": "OK", + "table": table_name, + "record_count": len(table_data), + "checksum": checksum, + "check_date": datetime.datetime.now().isoformat() + } + + except Exception as e: + self.logger.error(f"整合性チェックエラー: {str(e)}") + return { + "status": "ERROR", + "table": table_name, + "error": str(e), + "check_date": datetime.datetime.now().isoformat() + } + finally: + conn.close() + + def archive_old_data(self, years: int = 7) -> Dict: + """古いデータをアーカイブ(デフォルト7年以上前)""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + try: + # アーカイブ対象日付を計算 + archive_date = datetime.datetime.now() - datetime.timedelta(days=years*365) + retention_end = datetime.datetime.now() + datetime.timedelta(days=10*365) + + # アーカイブ対象の売上データを取得 + cursor.execute(''' + SELECT s.*, c.name as customer_name, p.name as product_name + FROM sales s + LEFT JOIN customers c ON s.customer_id = c.id + LEFT JOIN products p ON s.product_id = p.id + WHERE s.sale_date < ? + ''', (archive_date,)) + + old_sales = cursor.fetchall() + + archived_count = 0 + for sale in old_sales: + # アーカイブデータ作成 + archive_data = { + "original_id": sale[0], + "customer_id": sale[1], + "product_id": sale[2], + "quantity": sale[3], + "unit_price": sale[4], + "total_price": sale[5], + "sale_date": sale[6], + "customer_name": sale[7], + "product_name": sale[8], + "retention_end_date": retention_end + } + + checksum = self.calculate_checksum(archive_data) + + # アーカイブテーブルに挿入 + cursor.execute(''' + INSERT INTO archive_sales + (original_id, customer_id, product_id, quantity, unit_price, + total_price, sale_date, customer_name, product_name, + retention_end_date, checksum) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + sale[0], sale[1], sale[2], sale[3], sale[4], sale[5], + sale[6], sale[7], sale[8], retention_end, checksum + )) + + archived_count += 1 + + # 元のテーブルから削除 + cursor.execute("DELETE FROM sales WHERE id = ?", (sale[0],)) + + # 監査ログ記録 + self.log_change("sales", sale[0], "ARCHIVE", + {"archived": True}, archive_data) + + conn.commit() + + return { + "status": "SUCCESS", + "archived_count": archived_count, + "archive_date": archive_date.isoformat(), + "retention_end": retention_end.isoformat() + } + + except Exception as e: + self.logger.error(f"アーカイブエラー: {str(e)}") + return { + "status": "ERROR", + "error": str(e) + } + finally: + conn.close() + + def get_audit_trail(self, table_name: str = None, record_id: int = None) -> List[Dict]: + """監査証跡を取得""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + query = "SELECT * FROM audit_logs WHERE 1=1" + params = [] + + if table_name: + query += " AND table_name = ?" + params.append(table_name) + + if record_id: + query += " AND record_id = ?" + params.append(record_id) + + query += " ORDER BY timestamp DESC" + + cursor.execute(query, params) + logs = [] + + for row in cursor.fetchall(): + logs.append({ + "id": row[0], + "table_name": row[1], + "record_id": row[2], + "operation": row[3], + "old_data": json.loads(row[4]) if row[4] else None, + "new_data": json.loads(row[5]) if row[5] else None, + "user_id": row[6], + "timestamp": row[7], + "checksum": row[8] + }) + + conn.close() + return logs + + def generate_compliance_report(self) -> Dict: + """電子帳簿保存法コンプライアンスレポートを生成""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + report = { + "generated_date": datetime.datetime.now().isoformat(), + "database_path": self.db_path, + "tables": {}, + "audit_summary": {}, + "archive_summary": {}, + "compliance_status": "COMPLIANT" + } + + # 各テーブルの状況 + for table in ["customers", "products", "sales"]: + cursor.execute(f"SELECT COUNT(*) FROM {table}") + count = cursor.fetchone()[0] + + # 最新の整合性チェック + cursor.execute(''' + SELECT status, check_date, checksum + FROM integrity_checks + WHERE table_name = ? + ORDER BY check_date DESC + LIMIT 1 + ''', (table,)) + + integrity_result = cursor.fetchone() + + report["tables"][table] = { + "record_count": count, + "last_integrity_check": { + "status": integrity_result[0] if integrity_result else "NOT_CHECKED", + "date": integrity_result[1] if integrity_result else None, + "checksum": integrity_result[2] if integrity_result else None + } + } + + # 監査ログサマリー + cursor.execute(''' + SELECT operation, COUNT(*) as count + FROM audit_logs + WHERE timestamp > date('now', '-1 year') + GROUP BY operation + ''') + + audit_summary = {} + for row in cursor.fetchall(): + audit_summary[row[0]] = row[1] + + report["audit_summary"] = audit_summary + + # アーカイブサマリー + cursor.execute(''' + SELECT COUNT(*) as total, + MIN(archived_date) as oldest, + MAX(retention_end_date) as latest_retention + FROM archive_sales + ''') + + archive_result = cursor.fetchone() + if archive_result[0] > 0: + report["archive_summary"] = { + "archived_records": archive_result[0], + "oldest_archive": archive_result[1], + "latest_retention_end": archive_result[2] + } + + conn.close() + return report + +if __name__ == "__main__": + # テスト実行 + compliance = ComplianceManager() + + print("🔍 データ整合性チェック実行中...") + for table in ["customers", "products", "sales"]: + result = compliance.verify_data_integrity(table) + print(f"✅ {table}: {result['status']} ({result['record_count']}件)") + + print("\n📋 コンプライアンスレポート生成...") + report = compliance.generate_compliance_report() + print(json.dumps(report, ensure_ascii=False, indent=2)) diff --git a/data_export.py b/data_export.py new file mode 100644 index 0000000..04662bf --- /dev/null +++ b/data_export.py @@ -0,0 +1,169 @@ +"""データエクスポート機能""" +import sqlite3 +import json +import csv +import datetime +from typing import List, Dict + +class DataExporter: + """販売アシスト1号のデータエクスポート担当""" + + def __init__(self, db_path: str = "sales.db"): + self.db_path = db_path + + def export_to_json(self, output_path: str = None) -> str: + """全データをJSON形式でエクスポート""" + if not output_path: + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + output_path = f"sales_backup_{timestamp}.json" + + data = { + "export_date": datetime.datetime.now().isoformat(), + "customers": self._get_customers(), + "products": self._get_products(), + "sales": self._get_sales() + } + + with open(output_path, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + return output_path + + def export_to_csv(self, output_dir: str = ".") -> Dict[str, str]: + """各テーブルをCSV形式でエクスポート""" + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + + files = {} + + # 顧客データ + customers_file = f"{output_dir}/customers_{timestamp}.csv" + self._export_table_to_csv("customers", customers_file) + files["customers"] = customers_file + + # 商品データ + products_file = f"{output_dir}/products_{timestamp}.csv" + self._export_table_to_csv("products", products_file) + files["products"] = products_file + + # 売上データ + sales_file = f"{output_dir}/sales_{timestamp}.csv" + self._export_table_to_csv("sales", sales_file) + files["sales"] = sales_file + + return files + + def _get_customers(self) -> List[Dict]: + """顧客データ取得""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute("SELECT * FROM customers ORDER BY created_at DESC") + + customers = [] + for row in cursor.fetchall(): + customers.append({ + 'id': row[0], + 'name': row[1], + 'company': row[2], + 'phone': row[3], + 'email': row[4], + 'address': row[5], + 'created_at': row[6] + }) + + conn.close() + return customers + + def _get_products(self) -> List[Dict]: + """商品データ取得""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute("SELECT * FROM products ORDER BY created_at DESC") + + products = [] + for row in cursor.fetchall(): + products.append({ + 'id': row[0], + 'name': row[1], + 'code': row[2], + 'price': row[3], + 'stock': row[4], + 'description': row[5], + 'created_at': row[6] + }) + + conn.close() + return products + + def _get_sales(self) -> List[Dict]: + """売上データ取得""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute(''' + SELECT s.*, c.name as customer_name, p.name as product_name + FROM sales s + LEFT JOIN customers c ON s.customer_id = c.id + LEFT JOIN products p ON s.product_id = p.id + ORDER BY s.sale_date DESC + ''') + + sales = [] + for row in cursor.fetchall(): + sales.append({ + 'id': row[0], + 'customer_id': row[1], + 'product_id': row[2], + 'quantity': row[3], + 'unit_price': row[4], + 'total_price': row[5], + 'sale_date': row[6], + 'customer_name': row[7] or '不明', + 'product_name': row[8] or '不明' + }) + + conn.close() + return sales + + def _export_table_to_csv(self, table_name: str, output_path: str): + """テーブルをCSVにエクスポート""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # テーブルデータ取得 + if table_name == "sales": + cursor.execute(''' + SELECT s.id, c.name as customer_name, p.name as product_name, + s.quantity, s.unit_price, s.total_price, s.sale_date + FROM sales s + LEFT JOIN customers c ON s.customer_id = c.id + LEFT JOIN products p ON s.product_id = p.id + ORDER BY s.sale_date DESC + ''') + headers = ['ID', '顧客名', '商品名', '数量', '単価', '合計', '日時'] + else: + cursor.execute(f"SELECT * FROM {table_name}") + headers = [description[0] for description in cursor.description] + + rows = cursor.fetchall() + + # CSV書き込み + with open(output_path, 'w', newline='', encoding='utf-8') as csvfile: + writer = csv.writer(csvfile) + writer.writerow(headers) + writer.writerows(rows) + + conn.close() + +if __name__ == "__main__": + exporter = DataExporter() + + print("📦 データエクスポート中...") + + # JSONエクスポート + json_file = exporter.export_to_json() + print(f"✅ JSONエクスポート完了: {json_file}") + + # CSVエクスポート + csv_files = exporter.export_to_csv() + print("✅ CSVエクスポート完了:") + for table, file in csv_files.items(): + print(f" {table}: {file}") diff --git a/main.py b/main.py new file mode 100644 index 0000000..b5b709d --- /dev/null +++ b/main.py @@ -0,0 +1,889 @@ +import flet as ft +import sqlite3 +import datetime +from typing import List, Dict, Optional +from data_export import DataExporter +from compliance import ComplianceManager + +class SalesAssistant: + def __init__(self, page: ft.Page): + self.page = page + self.page.title = "販売アシスト1号" + self.page.theme_mode = ft.ThemeMode.LIGHT + self.page.vertical_alignment = ft.MainAxisAlignment.CENTER + self.page.horizontal_alignment = ft.CrossAxisAlignment.CENTER + + # Initialize database + self.init_database() + + # Initialize data exporter and compliance manager + self.exporter = DataExporter() + self.compliance = ComplianceManager() + + # Navigation + self.navigation_rail = ft.NavigationRail( + selected_index=0, + label_type=ft.NavigationRailLabelType.ALL, + min_width=100, + min_extended_width=200, + destinations=[ + ft.NavigationRailDestination( + icon=ft.icons.DASHBOARD_OUTLINED, + selected_icon=ft.icons.DASHBOARD, + label="ダッシュボード" + ), + ft.NavigationRailDestination( + icon=ft.icons.PEOPLE_OUTLINED, + selected_icon=ft.icons.PEOPLE, + label="顧客管理" + ), + ft.NavigationRailDestination( + icon=ft.icons.INVENTORY_2_OUTLINED, + selected_icon=ft.icons.INVENTORY_2, + label="商品管理" + ), + ft.NavigationRailDestination( + icon=ft.icons.RECEIPT_OUTLINED, + selected_icon=ft.icons.RECEIPT, + label="売上管理" + ), + ft.NavigationRailDestination( + icon=ft.icons.DOWNLOAD_OUTLINED, + selected_icon=ft.icons.DOWNLOAD, + label="データ出力" + ), + ft.NavigationRailDestination( + icon=ft.icons.GAVEL_OUTLINED, + selected_icon=ft.icons.GAVEL, + label="コンプライアンス" + ), + ], + on_change=self.navigation_changed + ) + + # Content area + self.content_area = ft.Container(expand=True) + + # Main layout + self.main_layout = ft.Row( + [ + self.navigation_rail, + ft.VerticalDivider(width=1), + self.content_area + ], + expand=True + ) + + # Show dashboard initially + self.show_dashboard() + + # Add main layout to page + self.page.add(self.main_layout) + + def init_database(self): + """Initialize SQLite database""" + conn = sqlite3.connect('sales.db') + cursor = conn.cursor() + + # Create customers table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS customers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + company TEXT, + phone TEXT, + email TEXT, + address TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Create products table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS products ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + code TEXT UNIQUE, + price REAL NOT NULL, + stock INTEGER DEFAULT 0, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Create sales table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS sales ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + customer_id INTEGER, + product_id INTEGER, + quantity INTEGER NOT NULL, + unit_price REAL NOT NULL, + total_price REAL NOT NULL, + sale_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (customer_id) REFERENCES customers (id), + FOREIGN KEY (product_id) REFERENCES products (id) + ) + ''') + + conn.commit() + conn.close() + + def navigation_changed(self, e): + """Handle navigation rail changes""" + if e.control.selected_index == 0: + self.show_dashboard() + elif e.control.selected_index == 1: + self.show_customers() + elif e.control.selected_index == 2: + self.show_products() + elif e.control.selected_index == 3: + self.show_sales() + elif e.control.selected_index == 4: + self.show_data_export() + elif e.control.selected_index == 5: + self.show_compliance() + + def show_dashboard(self): + """Show dashboard view""" + # Get statistics + conn = sqlite3.connect('sales.db') + cursor = conn.cursor() + + # Total customers + cursor.execute("SELECT COUNT(*) FROM customers") + total_customers = cursor.fetchone()[0] + + # Total products + cursor.execute("SELECT COUNT(*) FROM products") + total_products = cursor.fetchone()[0] + + # Total sales + cursor.execute("SELECT COUNT(*) FROM sales") + total_sales = cursor.fetchone()[0] + + # Total revenue + cursor.execute("SELECT SUM(total_price) FROM sales") + total_revenue = cursor.fetchone()[0] or 0 + + conn.close() + + dashboard_content = ft.Column([ + ft.Text("ダッシュボード", size=24, weight=ft.FontWeight.BOLD), + ft.Divider(), + ft.Row([ + ft.Card( + content=ft.Container( + content=ft.Column([ + ft.Text("顧客数", size=16, color=ft.colors.BLUE), + ft.Text(str(total_customers), size=32, weight=ft.FontWeight.BOLD) + ]), + padding=20, + width=150 + ) + ), + ft.Card( + content=ft.Container( + content=ft.Column([ + ft.Text("商品数", size=16, color=ft.colors.GREEN), + ft.Text(str(total_products), size=32, weight=ft.FontWeight.BOLD) + ]), + padding=20, + width=150 + ) + ), + ft.Card( + content=ft.Container( + content=ft.Column([ + ft.Text("売上件数", size=16, color=ft.colors.ORANGE), + ft.Text(str(total_sales), size=32, weight=ft.FontWeight.BOLD) + ]), + padding=20, + width=150 + ) + ), + ft.Card( + content=ft.Container( + content=ft.Column([ + ft.Text("総売上", size=16, color=ft.colors.PURPLE), + ft.Text(f"¥{total_revenue:,.0f}", size=32, weight=ft.FontWeight.BOLD) + ]), + padding=20, + width=150 + ) + ) + ], wrap=True) + ], scroll=ft.ScrollMode.AUTO) + + self.content_area.content = ft.Container(dashboard_content, padding=20) + self.page.update() + + def show_customers(self): + """Show customers management view""" + customers = self.get_customers() + + # Customer list + customer_list = ft.ListView( + expand=True, + spacing=10, + padding=10 + ) + + for customer in customers: + customer_list.controls.append( + ft.Card( + content=ft.Container( + content=ft.ListTile( + title=ft.Text(customer['name']), + subtitle=ft.Text(customer['company'] or ''), + trailing=ft.Row([ + ft.IconButton( + icon=ft.icons.EDIT, + on_click=lambda e, c=customer: self.edit_customer(c) + ), + ft.IconButton( + icon=ft.icons.DELETE, + on_click=lambda e, c=customer: self.delete_customer(c) + ) + ]) + ), + padding=10 + ) + ) + ) + + # Add customer button + add_button = ft.ElevatedButton( + "顧客を追加", + icon=ft.icons.ADD, + on_click=self.add_customer_dialog + ) + + customers_content = ft.Column([ + ft.Row([ + ft.Text("顧客管理", size=24, weight=ft.FontWeight.BOLD), + add_button + ], alignment=ft.MainAxisAlignment.SPACE_BETWEEN), + ft.Divider(), + customer_list + ], scroll=ft.ScrollMode.AUTO) + + self.content_area.content = ft.Container(customers_content, padding=20) + self.page.update() + + def show_products(self): + """Show products management view""" + products = self.get_products() + + # Product list + product_list = ft.ListView( + expand=True, + spacing=10, + padding=10 + ) + + for product in products: + product_list.controls.append( + ft.Card( + content=ft.Container( + content=ft.ListTile( + title=ft.Text(product['name']), + subtitle=ft.Text(f"¥{product['price']:,.0f} | 在庫: {product['stock']}"), + trailing=ft.Row([ + ft.IconButton( + icon=ft.icons.EDIT, + on_click=lambda e, p=product: self.edit_product(p) + ), + ft.IconButton( + icon=ft.icons.DELETE, + on_click=lambda e, p=product: self.delete_product(p) + ) + ]) + ), + padding=10 + ) + ) + ) + + # Add product button + add_button = ft.ElevatedButton( + "商品を追加", + icon=ft.icons.ADD, + on_click=self.add_product_dialog + ) + + products_content = ft.Column([ + ft.Row([ + ft.Text("商品管理", size=24, weight=ft.FontWeight.BOLD), + add_button + ], alignment=ft.MainAxisAlignment.SPACE_BETWEEN), + ft.Divider(), + product_list + ], scroll=ft.ScrollMode.AUTO) + + self.content_area.content = ft.Container(products_content, padding=20) + self.page.update() + + def show_sales(self): + """Show sales management view""" + sales = self.get_sales() + + # Sales list + sales_list = ft.ListView( + expand=True, + spacing=10, + padding=10 + ) + + for sale in sales: + sales_list.controls.append( + ft.Card( + content=ft.Container( + content=ft.ListTile( + title=ft.Text(f"{sale['customer_name']} - {sale['product_name']}"), + subtitle=ft.Text(f"{sale['quantity']}個 × ¥{sale['unit_price']:,.0f} = ¥{sale['total_price']:,.0f}"), + trailing=ft.Text(sale['sale_date'].split()[0]) + ), + padding=10 + ) + ) + ) + + # Add sale button + add_button = ft.ElevatedButton( + "売上を追加", + icon=ft.icons.ADD, + on_click=self.add_sale_dialog + ) + + sales_content = ft.Column([ + ft.Row([ + ft.Text("売上管理", size=24, weight=ft.FontWeight.BOLD), + add_button + ], alignment=ft.MainAxisAlignment.SPACE_BETWEEN), + ft.Divider(), + sales_list + ], scroll=ft.ScrollMode.AUTO) + + self.content_area.content = ft.Container(sales_content, padding=20) + self.page.update() + + def get_customers(self) -> List[Dict]: + """Get all customers from database""" + conn = sqlite3.connect('sales.db') + cursor = conn.cursor() + cursor.execute("SELECT * FROM customers ORDER BY created_at DESC") + customers = [] + for row in cursor.fetchall(): + customers.append({ + 'id': row[0], + 'name': row[1], + 'company': row[2], + 'phone': row[3], + 'email': row[4], + 'address': row[5], + 'created_at': row[6] + }) + conn.close() + return customers + + def get_products(self) -> List[Dict]: + """Get all products from database""" + conn = sqlite3.connect('sales.db') + cursor = conn.cursor() + cursor.execute("SELECT * FROM products ORDER BY created_at DESC") + products = [] + for row in cursor.fetchall(): + products.append({ + 'id': row[0], + 'name': row[1], + 'code': row[2], + 'price': row[3], + 'stock': row[4], + 'description': row[5], + 'created_at': row[6] + }) + conn.close() + return products + + def get_sales(self) -> List[Dict]: + """Get all sales with customer and product names""" + conn = sqlite3.connect('sales.db') + cursor = conn.cursor() + cursor.execute(''' + SELECT s.*, c.name as customer_name, p.name as product_name + FROM sales s + LEFT JOIN customers c ON s.customer_id = c.id + LEFT JOIN products p ON s.product_id = p.id + ORDER BY s.sale_date DESC + ''') + sales = [] + for row in cursor.fetchall(): + sales.append({ + 'id': row[0], + 'customer_id': row[1], + 'product_id': row[2], + 'quantity': row[3], + 'unit_price': row[4], + 'total_price': row[5], + 'sale_date': row[6], + 'customer_name': row[7] or '不明', + 'product_name': row[8] or '不明' + }) + conn.close() + return sales + + def add_customer_dialog(self, e): + """Show add customer dialog""" + name_field = ft.TextField(label="氏名", autofocus=True) + company_field = ft.TextField(label="会社名") + phone_field = ft.TextField(label="電話番号") + email_field = ft.TextField(label="メールアドレス") + address_field = ft.TextField(label="住所") + + def save_customer(e): + conn = sqlite3.connect('sales.db') + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO customers (name, company, phone, email, address) + VALUES (?, ?, ?, ?, ?) + ''', (name_field.value, company_field.value, phone_field.value, + email_field.value, address_field.value)) + conn.commit() + conn.close() + dialog.open = False + self.show_customers() + self.page.update() + + dialog = ft.AlertDialog( + title=ft.Text("顧客を追加"), + content=ft.Column([ + name_field, + company_field, + phone_field, + email_field, + address_field + ], tight=True), + actions=[ + ft.TextButton("キャンセル", on_click=lambda e: self.close_dialog(dialog)), + ft.TextButton("保存", on_click=save_customer) + ], + actions_alignment=ft.MainAxisAlignment.END + ) + + self.page.dialog = dialog + dialog.open = True + self.page.update() + + def add_product_dialog(self, e): + """Show add product dialog""" + name_field = ft.TextField(label="商品名", autofocus=True) + code_field = ft.TextField(label="商品コード") + price_field = ft.TextField(label="価格", keyboard_type=ft.KeyboardType.NUMBER) + stock_field = ft.TextField(label="在庫数", keyboard_type=ft.KeyboardType.NUMBER) + description_field = ft.TextField(label="説明", multiline=True) + + def save_product(e): + conn = sqlite3.connect('sales.db') + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO products (name, code, price, stock, description) + VALUES (?, ?, ?, ?, ?) + ''', (name_field.value, code_field.value, float(price_field.value or 0), + int(stock_field.value or 0), description_field.value)) + conn.commit() + conn.close() + dialog.open = False + self.show_products() + self.page.update() + + dialog = ft.AlertDialog( + title=ft.Text("商品を追加"), + content=ft.Column([ + name_field, + code_field, + price_field, + stock_field, + description_field + ], tight=True), + actions=[ + ft.TextButton("キャンセル", on_click=lambda e: self.close_dialog(dialog)), + ft.TextButton("保存", on_click=save_product) + ], + actions_alignment=ft.MainAxisAlignment.END + ) + + self.page.dialog = dialog + dialog.open = True + self.page.update() + + def add_sale_dialog(self, e): + """Show add sale dialog""" + customers = self.get_customers() + products = self.get_products() + + customer_dropdown = ft.Dropdown( + label="顧客", + options=[ft.dropdown.Option(c['name'], key=str(c['id'])) for c in customers] + ) + product_dropdown = ft.Dropdown( + label="商品", + options=[ft.dropdown.Option(p['name'], key=str(p['id'])) for p in products], + on_change=lambda e: self.update_price_display(e, product_dropdown, price_field, quantity_field, total_field) + ) + quantity_field = ft.TextField( + label="数量", + value="1", + keyboard_type=ft.KeyboardType.NUMBER, + on_change=lambda e: self.calculate_total(price_field, quantity_field, total_field) + ) + price_field = ft.TextField( + label="単価", + keyboard_type=ft.KeyboardType.NUMBER, + on_change=lambda e: self.calculate_total(price_field, quantity_field, total_field) + ) + total_field = ft.TextField(label="合計", read_only=True) + + def save_sale(e): + conn = sqlite3.connect('sales.db') + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO sales (customer_id, product_id, quantity, unit_price, total_price) + VALUES (?, ?, ?, ?, ?) + ''', (int(customer_dropdown.value), int(product_dropdown.value), + int(quantity_field.value), float(price_field.value), + float(total_field.value))) + conn.commit() + conn.close() + dialog.open = False + self.show_sales() + self.page.update() + + dialog = ft.AlertDialog( + title=ft.Text("売上を追加"), + content=ft.Column([ + customer_dropdown, + product_dropdown, + quantity_field, + price_field, + total_field + ], tight=True), + actions=[ + ft.TextButton("キャンセル", on_click=lambda e: self.close_dialog(dialog)), + ft.TextButton("保存", on_click=save_sale) + ], + actions_alignment=ft.MainAxisAlignment.END + ) + + self.page.dialog = dialog + dialog.open = True + self.page.update() + + def update_price_display(self, e, product_dropdown, price_field, quantity_field, total_field): + """Update price when product is selected""" + if product_dropdown.value: + conn = sqlite3.connect('sales.db') + cursor = conn.cursor() + cursor.execute("SELECT price FROM products WHERE id = ?", (int(product_dropdown.value),)) + result = cursor.fetchone() + conn.close() + if result: + price_field.value = str(result[0]) + self.calculate_total(price_field, quantity_field, total_field) + self.page.update() + + def calculate_total(self, price_field, quantity_field, total_field): + """Calculate total price""" + try: + price = float(price_field.value or 0) + quantity = int(quantity_field.value or 1) + total_field.value = str(price * quantity) + except ValueError: + total_field.value = "0" + self.page.update() + + def close_dialog(self, dialog): + """Close dialog""" + dialog.open = False + self.page.update() + + def edit_customer(self, customer): + """Edit customer (placeholder)""" + self.page.snack_bar = ft.SnackBar(content=ft.Text("顧客編集機能は準備中です")) + self.page.snack_bar.open = True + self.page.update() + + def delete_customer(self, customer): + """Delete customer""" + def confirm_delete(e): + conn = sqlite3.connect('sales.db') + cursor = conn.cursor() + cursor.execute("DELETE FROM customers WHERE id = ?", (customer['id'],)) + conn.commit() + conn.close() + dialog.open = False + self.show_customers() + self.page.update() + + dialog = ft.AlertDialog( + title=ft.Text("確認"), + content=ft.Text(f"顧客「{customer['name']}」を削除してもよろしいですか?"), + actions=[ + ft.TextButton("キャンセル", on_click=lambda e: self.close_dialog(dialog)), + ft.TextButton("削除", on_click=confirm_delete) + ], + actions_alignment=ft.MainAxisAlignment.END + ) + + self.page.dialog = dialog + dialog.open = True + self.page.update() + + def edit_product(self, product): + """Edit product (placeholder)""" + self.page.snack_bar = ft.SnackBar(content=ft.Text("商品編集機能は準備中です")) + self.page.snack_bar.open = True + self.page.update() + + def delete_product(self, product): + """Delete product""" + def confirm_delete(e): + conn = sqlite3.connect('sales.db') + cursor = conn.cursor() + cursor.execute("DELETE FROM products WHERE id = ?", (product['id'],)) + conn.commit() + conn.close() + dialog.open = False + self.show_products() + self.page.update() + + dialog = ft.AlertDialog( + title=ft.Text("確認"), + content=ft.Text(f"商品「{product['name']}」を削除してもよろしいですか?"), + actions=[ + ft.TextButton("キャンセル", on_click=lambda e: self.close_dialog(dialog)), + ft.TextButton("削除", on_click=confirm_delete) + ], + actions_alignment=ft.MainAxisAlignment.END + ) + + self.page.dialog = dialog + dialog.open = True + self.page.update() + + def show_data_export(self): + """データ出力画面を表示""" + export_content = ft.Column([ + ft.Text("データ出力", size=24, weight=ft.FontWeight.BOLD), + ft.Divider(), + ft.Card( + content=ft.Container( + content=ft.Column([ + ft.Text("JSON形式で出力", size=16, weight=ft.FontWeight.BOLD), + ft.Text("全データをJSON形式でエクスポートします"), + ft.ElevatedButton( + "JSON出力", + icon=ft.icons.DOWNLOAD, + on_click=self.export_json + ) + ]), + padding=20 + ) + ), + ft.Card( + content=ft.Container( + content=ft.Column([ + ft.Text("CSV形式で出力", size=16, weight=ft.FontWeight.BOLD), + ft.Text("各テーブルをCSV形式でエクスポートします"), + ft.ElevatedButton( + "CSV出力", + icon=ft.icons.DOWNLOAD, + on_click=self.export_csv + ) + ]), + padding=20 + ) + ) + ], scroll=ft.ScrollMode.AUTO) + + self.content_area.content = ft.Container(export_content, padding=20) + self.page.update() + + def show_compliance(self): + """コンプライアンス画面を表示""" + compliance_content = ft.Column([ + ft.Text("電子帳簿保存法対応", size=24, weight=ft.FontWeight.BOLD), + ft.Divider(), + ft.Card( + content=ft.Container( + content=ft.Column([ + ft.Text("データ整合性チェック", size=16, weight=ft.FontWeight.BOLD), + ft.Text("データの整合性を検証し、チェックサムを生成します"), + ft.ElevatedButton( + "整合性チェック", + icon=ft.icons.CHECK_CIRCLE, + on_click=self.check_integrity + ) + ]), + padding=20 + ) + ), + ft.Card( + content=ft.Container( + content=ft.Column([ + ft.Text("古いデータのアーカイブ", size=16, weight=ft.FontWeight.BOLD), + ft.Text("7年以上前のデータをアーカイブします(10年保存)"), + ft.ElevatedButton( + "データアーカイブ", + icon=ft.icons.ARCHIVE, + on_click=self.archive_data + ) + ]), + padding=20 + ) + ), + ft.Card( + content=ft.Container( + content=ft.Column([ + ft.Text("コンプライアンスレポート", size=16, weight=ft.FontWeight.BOLD), + ft.Text("電子帳簿保存法対応レポートを生成します"), + ft.ElevatedButton( + "レポート生成", + icon=ft.icons.ASSIGNMENT, + on_click=self.generate_compliance_report + ) + ]), + padding=20 + ) + ) + ], scroll=ft.ScrollMode.AUTO) + + self.content_area.content = ft.Container(compliance_content, padding=20) + self.page.update() + + def export_json(self, e): + """JSON形式でデータをエクスポート""" + try: + file_path = self.exporter.export_to_json() + self.page.snack_bar = ft.SnackBar( + content=ft.Text(f"JSON出力完了: {file_path}"), + bgcolor=ft.colors.GREEN + ) + except Exception as ex: + self.page.snack_bar = ft.SnackBar( + content=ft.Text(f"JSON出力エラー: {str(ex)}"), + bgcolor=ft.colors.RED + ) + + self.page.snack_bar.open = True + self.page.update() + + def export_csv(self, e): + """CSV形式でデータをエクスポート""" + try: + files = self.exporter.export_to_csv() + file_list = "\n".join([f"{k}: {v}" for k, v in files.items()]) + self.page.snack_bar = ft.SnackBar( + content=ft.Text(f"CSV出力完了:\n{file_list}"), + bgcolor=ft.colors.GREEN + ) + except Exception as ex: + self.page.snack_bar = ft.SnackBar( + content=ft.Text(f"CSV出力エラー: {str(ex)}"), + bgcolor=ft.colors.RED + ) + + self.page.snack_bar.open = True + self.page.update() + + def check_integrity(self, e): + """データ整合性チェック""" + try: + results = [] + for table in ["customers", "products", "sales"]: + result = self.compliance.verify_data_integrity(table) + results.append(f"{table}: {result['status']} ({result['record_count']}件)") + + message = "整合性チェック完了:\n" + "\n".join(results) + self.page.snack_bar = ft.SnackBar( + content=ft.Text(message), + bgcolor=ft.colors.GREEN + ) + except Exception as ex: + self.page.snack_bar = ft.SnackBar( + content=ft.Text(f"整合性チェックエラー: {str(ex)}"), + bgcolor=ft.colors.RED + ) + + self.page.snack_bar.open = True + self.page.update() + + def archive_data(self, e): + """古いデータをアーカイブ""" + try: + result = self.compliance.archive_old_data() + if result["status"] == "SUCCESS": + message = f"アーカイブ完了: {result['archived_count']}件" + bgcolor = ft.colors.GREEN + else: + message = f"アーカイブエラー: {result['error']}" + bgcolor = ft.colors.RED + + self.page.snack_bar = ft.SnackBar( + content=ft.Text(message), + bgcolor=bgcolor + ) + except Exception as ex: + self.page.snack_bar = ft.SnackBar( + content=ft.Text(f"アーカイブエラー: {str(ex)}"), + bgcolor=ft.colors.RED + ) + + self.page.snack_bar.open = True + self.page.update() + + def generate_compliance_report(self, e): + """コンプライアンスレポートを生成""" + try: + report = self.compliance.generate_compliance_report() + + # レポート内容を表示 + dialog = ft.AlertDialog( + title=ft.Text("コンプライアンスレポート"), + content=ft.Container( + content=ft.Column([ + ft.Text(f"生成日時: {report['generated_date']}"), + ft.Text(f"データベース: {report['database_path']}"), + ft.Divider(), + ft.Text("テーブル状況:", weight=ft.FontWeight.BOLD), + *[ft.Text(f" {table}: {info['record_count']}件") + for table, info in report['tables'].items()], + ft.Divider(), + ft.Text("監査ログ:", weight=ft.FontWeight.BOLD), + *[ft.Text(f" {op}: {count}件") + for op, count in report['audit_summary'].items()] + ], scroll=ft.ScrollMode.AUTO), + height=400 + ), + actions=[ + ft.TextButton("閉じる", on_click=lambda e: self.close_dialog(dialog)) + ] + ) + + self.page.dialog = dialog + dialog.open = True + self.page.update() + + except Exception as ex: + self.page.snack_bar = ft.SnackBar( + content=ft.Text(f"レポート生成エラー: {str(ex)}"), + bgcolor=ft.colors.RED + ) + self.page.snack_bar.open = True + self.page.update() + +def main(page: ft.Page): + app = SalesAssistant(page) + +if __name__ == "__main__": + ft.app(target=main) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c24f134 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +flet>=0.21.0