commit 38cf8c2cfc4d51e12d7ebbbf5d97a9c773d09668 Author: Suherdy Yacob Date: Sun Aug 17 21:31:29 2025 +0700 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..d106907 --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +# Mini Basic Manufacture App + +A simple manufacturing application for small businesses, designed for ease of use by users 50 years and older with a modern UI. + +## Features + +- **User Authentication & Permissions**: Secure login system with role-based access control +- **Database Management**: Supports both SQLite (default) and PostgreSQL with backup/restore/duplicate functionality +- **Core Modules**: + - Purchase Management + - Basic Manufacturing (simple production tracking) + - Sales Management + - Inventory Tracking +- **User & Group Configuration**: Admin panel for managing users and permissions +- **Dashboard**: Overview of key business metrics +- **Excel Export**: Export all data to Excel format for external analysis +- **Modern UI**: Clean, modern interface using customtkinter +- **Cross-platform**: Works on Windows, macOS, and Linux + +## Requirements + +- Python 3.6 or higher +- SQLite (included with Python) +- Optional: PostgreSQL for production use + +## Installation + +1. Clone or download this repository +2. Install the required packages: + ``` + pip install -r requirements.txt + ``` +3. Run the application: + ``` + python main.py + ``` + +## First Run + +1. On first run, you'll be prompted to create an admin account +2. Log in with your admin credentials +3. Configure database settings if needed (SQLite is default) +4. Start using the application modules + +## Database Configuration + +Admin users can configure the database connection: +- **SQLite**: File-based database, no additional setup required +- **PostgreSQL**: For production environments, requires PostgreSQL server + +Database management features: +- Backup: Create a backup of the current database +- Restore: Restore from a previous backup +- Duplicate: Create a copy of the database + +## User Permissions + +The application supports: +- Admin users: Full access to all features +- Regular users: Access based on group permissions +- User groups: Assign permissions to groups of users + +## Modules + +### Purchase +- Create and manage purchase orders +- Track supplier information +- Monitor order status +- View purchase reports with monthly charts +- **Export**: Export purchase orders to Excel format + +### Manufacturing +- Create manufacturing orders for end products +- Track production progress +- Simple workflow without complex BOM +- **Export**: Export manufacturing orders to Excel format + +### Sales +- Create and manage sales orders +- Track customer information +- Monitor order status +- View sales reports with monthly charts +- **Export**: Export sales orders to Excel format + +### Inventory +- Track product quantities +- View stock levels +- Adjust inventory manually +- **Export**: Export inventory data and stock movements to Excel format + +## Dashboard + +The dashboard provides an overview of key business metrics: +- Quick stats on pending orders and low inventory +- Financial summary with revenue, costs, and profit +- Monthly sales and purchase charts +- Recent activities log + +## Excel Export Features + +All modules support Excel export functionality: +- **Purchase Orders**: Export all purchase order data including suppliers, items, and status +- **Manufacturing Orders**: Export production orders with product details and progress +- **Sales Orders**: Export sales data including customers, products, and order status +- **Inventory**: Export current stock levels and detailed stock movement history + +Exported files are saved with timestamps in the filename for easy organization. + +## Development + +This application is built with: +- Python 3 +- **customtkinter** for the modern GUI (replaced standard tkinter) +- SQLAlchemy for database operations +- Matplotlib for charting +- SQLite/PostgreSQL for data storage +- **pandas** and **openpyxl** for Excel export functionality + +## License + +This project is open source and available under the MIT License. \ No newline at end of file diff --git a/__pycache__/test_export.cpython-312.pyc b/__pycache__/test_export.cpython-312.pyc new file mode 100644 index 0000000..e5e409c Binary files /dev/null and b/__pycache__/test_export.cpython-312.pyc differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..00be942 --- /dev/null +++ b/main.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +""" +Mini Basic Manufacture App +A simple manufacturing application for small businesses +""" + +import customtkinter as ctk +from tkinter import messagebox +import sys +import os + +# Add the src directory to the path +sys.path.append(os.path.join(os.path.dirname(__file__), 'src')) + +from src.app import ManufacturingApp + + +def main(): + """Main entry point for the application""" + try: + app = ManufacturingApp() + app.run() + except Exception as e: + messagebox.showerror("Error", f"Failed to start application: {str(e)}") + sys.exit(1) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/manufacturing.db b/manufacturing.db new file mode 100644 index 0000000..9c5ccdc Binary files /dev/null and b/manufacturing.db differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9d2f616 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,13 @@ +# Core dependencies +sqlalchemy>=2.0.0 +bcrypt>=4.0.0 + +# UI dependencies +customtkinter>=5.2.0 + +# Excel export dependencies +openpyxl>=3.1.0 +pandas>=2.0.0 + +# Development dependencies (optional) +pytest>=7.0.0 \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/__pycache__/__init__.cpython-312.pyc b/src/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..e21a336 Binary files /dev/null and b/src/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/__pycache__/app.cpython-312.pyc b/src/__pycache__/app.cpython-312.pyc new file mode 100644 index 0000000..4ea15cd Binary files /dev/null and b/src/__pycache__/app.cpython-312.pyc differ diff --git a/src/__pycache__/auth.cpython-312.pyc b/src/__pycache__/auth.cpython-312.pyc new file mode 100644 index 0000000..824f85c Binary files /dev/null and b/src/__pycache__/auth.cpython-312.pyc differ diff --git a/src/__pycache__/dao.cpython-312.pyc b/src/__pycache__/dao.cpython-312.pyc new file mode 100644 index 0000000..19d492d Binary files /dev/null and b/src/__pycache__/dao.cpython-312.pyc differ diff --git a/src/__pycache__/dao_items.cpython-312.pyc b/src/__pycache__/dao_items.cpython-312.pyc new file mode 100644 index 0000000..cfce035 Binary files /dev/null and b/src/__pycache__/dao_items.cpython-312.pyc differ diff --git a/src/__pycache__/dao_stock.cpython-312.pyc b/src/__pycache__/dao_stock.cpython-312.pyc new file mode 100644 index 0000000..742fcbd Binary files /dev/null and b/src/__pycache__/dao_stock.cpython-312.pyc differ diff --git a/src/__pycache__/database.cpython-312.pyc b/src/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000..54d6e1d Binary files /dev/null and b/src/__pycache__/database.cpython-312.pyc differ diff --git a/src/__pycache__/models.cpython-312.pyc b/src/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000..79692e0 Binary files /dev/null and b/src/__pycache__/models.cpython-312.pyc differ diff --git a/src/__pycache__/services.cpython-312.pyc b/src/__pycache__/services.cpython-312.pyc new file mode 100644 index 0000000..7127773 Binary files /dev/null and b/src/__pycache__/services.cpython-312.pyc differ diff --git a/src/app.py b/src/app.py new file mode 100644 index 0000000..ee6fa26 --- /dev/null +++ b/src/app.py @@ -0,0 +1,232 @@ +import customtkinter as ctk +from tkinter import messagebox +import os +import sys + +# Add the parent directory to the path to allow imports +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from src.database import DatabaseManager +from src.auth import AuthManager +from src.ui.main_window import MainWindow + +class ManufacturingApp: + """Main application class for the manufacturing app""" + + def __init__(self): + """Initialize the application""" + ctk.set_appearance_mode("light") + ctk.set_default_color_theme("blue") + + self.root = ctk.CTk() + self.root.title("Mini Basic Manufacture App") + self.root.geometry("400x500") + self.root.resizable(False, False) + + # Center the window + self.root.update_idletasks() + width = self.root.winfo_width() + height = self.root.winfo_height() + x = (self.root.winfo_screenwidth() // 2) - (width // 2) + y = (self.root.winfo_screenheight() // 2) - (height // 2) + self.root.geometry(f'{width}x{height}+{x}+{y}') + + # Initialize managers + self.db_manager = DatabaseManager() + self.auth_manager = AuthManager(self.db_manager) + + # Initialize services + self.product_service = None + self.supplier_service = None + self.customer_service = None + self.purchase_order_service = None + self.manufacturing_order_service = None + self.sales_order_service = None + self.inventory_service = None + + # Initialize main window + self.main_window = None + + def run(self): + """Run the application""" + # Show login window first + self.show_login() + self.root.mainloop() + + def show_login(self): + """Show the login window""" + # Clear any existing widgets + for widget in self.root.winfo_children(): + widget.destroy() + + # Initialize database first + try: + self.db_manager.initialize_database() + # Initialize default groups + self.auth_manager.initialize_default_groups() + except Exception as e: + messagebox.showerror("Database Error", f"Failed to initialize database: {str(e)}") + return + + # Create login frame + login_frame = ctk.CTkFrame(self.root) + login_frame.pack(fill="both", expand=True, padx=20, pady=20) + + # Title + title_label = ctk.CTkLabel(login_frame, text="Mini Basic Manufacture App", + font=("Arial", 24, "bold")) + title_label.pack(pady=(20, 30)) + + # Logo placeholder + logo_frame = ctk.CTkFrame(login_frame, fg_color="transparent") + logo_frame.pack(pady=(0, 20)) + + logo_label = ctk.CTkLabel(logo_frame, text="🏭", font=("Arial", 48)) + logo_label.pack() + + # Username + username_label = ctk.CTkLabel(login_frame, text="Username:", font=("Arial", 14)) + username_label.pack(anchor="w", padx=20, pady=(0, 5)) + + self.username_entry = ctk.CTkEntry(login_frame, width=300, height=40, + font=("Arial", 14)) + self.username_entry.pack(pady=(0, 15)) + self.username_entry.focus() + + # Password + password_label = ctk.CTkLabel(login_frame, text="Password:", font=("Arial", 14)) + password_label.pack(anchor="w", padx=20, pady=(0, 5)) + + self.password_entry = ctk.CTkEntry(login_frame, width=300, height=40, + font=("Arial", 14), show="*") + self.password_entry.pack(pady=(0, 20)) + + # Login button + login_button = ctk.CTkButton(login_frame, text="Login", command=self.login, + width=300, height=40, font=("Arial", 14)) + login_button.pack(pady=(0, 10)) + + # Bind Enter key to login + self.password_entry.bind("", lambda e: self.login()) + + # Admin setup button (only shown if no users exist) + try: + if not self.auth_manager.has_users(): + setup_button = ctk.CTkButton(login_frame, text="Setup Admin Account", + command=self.show_admin_setup, + width=300, height=40, + fg_color="green", hover_color="dark green") + setup_button.pack(pady=(10, 20)) + except Exception as e: + messagebox.showerror("Error", f"Failed to check users: {str(e)}") + + def login(self): + """Handle login""" + username = self.username_entry.get() + password = self.password_entry.get() + + if not username or not password: + messagebox.showerror("Error", "Please enter both username and password") + return + + user = self.auth_manager.authenticate(username, password) + if user: + messagebox.showinfo("Success", f"Welcome, {user['username']}!") + self.show_main_window(user) + else: + messagebox.showerror("Error", "Invalid username or password") + + def show_admin_setup(self): + """Show admin account setup""" + # Clear any existing widgets + for widget in self.root.winfo_children(): + widget.destroy() + + # Create setup frame + setup_frame = ctk.CTkFrame(self.root) + setup_frame.pack(fill="both", expand=True, padx=20, pady=20) + + # Title + title_label = ctk.CTkLabel(setup_frame, text="Admin Account Setup", + font=("Arial", 24, "bold")) + title_label.pack(pady=(20, 30)) + + # Username + username_label = ctk.CTkLabel(setup_frame, text="Admin Username:", + font=("Arial", 14)) + username_label.pack(anchor="w", padx=20, pady=(0, 5)) + + self.admin_username_entry = ctk.CTkEntry(setup_frame, width=300, height=40, + font=("Arial", 14)) + self.admin_username_entry.pack(pady=(0, 15)) + self.admin_username_entry.focus() + + # Password + password_label = ctk.CTkLabel(setup_frame, text="Admin Password:", + font=("Arial", 14)) + password_label.pack(anchor="w", padx=20, pady=(0, 5)) + + self.admin_password_entry = ctk.CTkEntry(setup_frame, width=300, height=40, + font=("Arial", 14), show="*") + self.admin_password_entry.pack(pady=(0, 15)) + + # Confirm Password + confirm_label = ctk.CTkLabel(setup_frame, text="Confirm Password:", + font=("Arial", 14)) + confirm_label.pack(anchor="w", padx=20, pady=(0, 5)) + + self.admin_confirm_entry = ctk.CTkEntry(setup_frame, width=300, height=40, + font=("Arial", 14), show="*") + self.admin_confirm_entry.pack(pady=(0, 20)) + + # Setup button + setup_button = ctk.CTkButton(setup_frame, text="Create Admin Account", + command=self.create_admin, + width=300, height=40, font=("Arial", 14)) + setup_button.pack(pady=(0, 10)) + + # Back to login button + back_button = ctk.CTkButton(setup_frame, text="Back to Login", + command=self.show_login, + width=300, height=40, font=("Arial", 14), + fg_color="gray", hover_color="dark gray") + back_button.pack(pady=(10, 20)) + + def create_admin(self): + """Create admin account""" + username = self.admin_username_entry.get() + password = self.admin_password_entry.get() + confirm = self.admin_confirm_entry.get() + + if not username or not password: + messagebox.showerror("Error", "Please enter both username and password") + return + + if password != confirm: + messagebox.showerror("Error", "Passwords do not match") + return + + try: + self.auth_manager.create_user(username, password, is_admin=True) + messagebox.showinfo("Success", "Admin account created successfully!") + self.show_login() + except Exception as e: + messagebox.showerror("Error", f"Failed to create admin account: {str(e)}") + + def show_main_window(self, user): + """Show the main application window""" + # Clear any existing widgets + for widget in self.root.winfo_children(): + widget.destroy() + + # Resize window for main application + self.root.geometry("1200x800") + + # Create main window + self.main_window = MainWindow(self.root, user, self.db_manager, self) + + def restart(self): + """Restart the application""" + self.root.destroy() + self.__init__() + self.run() \ No newline at end of file diff --git a/src/auth.py b/src/auth.py new file mode 100644 index 0000000..50df084 --- /dev/null +++ b/src/auth.py @@ -0,0 +1,357 @@ +import hashlib +import sqlite3 +import os + + +class AuthManager: + """Authentication manager for the manufacturing app""" + + def __init__(self, db_manager): + """Initialize the authentication manager""" + self.db_manager = db_manager + self.current_user = None + + def hash_password(self, password): + """Hash a password using SHA-256""" + return hashlib.sha256(password.encode()).hexdigest() + + def create_user(self, username, password, is_admin=False): + """Create a new user""" + if not self.db_manager.connect(): + raise Exception("Database connection failed") + + try: + cursor = self.db_manager.connection.cursor() + password_hash = self.hash_password(password) + + cursor.execute(''' + INSERT INTO users (username, password_hash, is_admin) + VALUES (?, ?, ?) + ''', (username, password_hash, is_admin)) + + self.db_manager.connection.commit() + return True + except sqlite3.IntegrityError: + raise Exception("Username already exists") + except Exception as e: + raise Exception(f"Failed to create user: {str(e)}") + finally: + self.db_manager.disconnect() + + def authenticate(self, username, password): + """Authenticate a user""" + if not self.db_manager.connect(): + return None + + try: + cursor = self.db_manager.connection.cursor() + password_hash = self.hash_password(password) + + cursor.execute(''' + SELECT id, username, is_admin FROM users + WHERE username = ? AND password_hash = ? + ''', (username, password_hash)) + + user = cursor.fetchone() + if user: + self.current_user = { + 'id': user[0], + 'username': user[1], + 'is_admin': user[2] + } + return self.current_user + return None + except Exception as e: + print(f"Authentication error: {e}") + return None + finally: + self.db_manager.disconnect() + + def has_users(self): + """Check if any users exist""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT COUNT(*) FROM users') + count = cursor.fetchone()[0] + return count > 0 + except Exception as e: + print(f"Error checking users: {e}") + return False + finally: + self.db_manager.disconnect() + + def get_user_permissions(self, user_id): + """Get user permissions""" + if not self.db_manager.connect(): + return [] + + try: + cursor = self.db_manager.connection.cursor() + + # Get user groups + cursor.execute(''' + SELECT g.permissions FROM user_groups g + JOIN user_group_members m ON g.id = m.group_id + WHERE m.user_id = ? + ''', (user_id,)) + + permissions = [] + for row in cursor.fetchall(): + if row[0]: + permissions.extend(row[0].split(',')) + + return permissions + except Exception as e: + print(f"Error getting user permissions: {e}") + return [] + finally: + self.db_manager.disconnect() + + def user_has_permission(self, user_id, permission): + """Check if user has a specific permission""" + permissions = self.get_user_permissions(user_id) + return permission in permissions or self.is_admin(user_id) + + def is_admin(self, user_id): + """Check if user is admin""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT is_admin FROM users WHERE id = ?', (user_id,)) + result = cursor.fetchone() + return result[0] if result else False + except Exception as e: + print(f"Error checking admin status: {e}") + return False + finally: + self.db_manager.disconnect() + + def get_all_users(self): + """Get all users""" + if not self.db_manager.connect(): + return [] + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT id, username, is_admin FROM users') + users = [] + for row in cursor.fetchall(): + users.append({ + 'id': row[0], + 'username': row[1], + 'is_admin': row[2] + }) + return users + except Exception as e: + print(f"Error getting users: {e}") + return [] + finally: + self.db_manager.disconnect() + + def delete_user(self, user_id): + """Delete a user""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + + # Delete user group memberships first + cursor.execute('DELETE FROM user_group_members WHERE user_id = ?', (user_id,)) + + # Delete user + cursor.execute('DELETE FROM users WHERE id = ?', (user_id,)) + + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error deleting user: {e}") + return False + finally: + self.db_manager.disconnect() + + # Group management methods + def create_group(self, name, permissions=None): + """Create a new user group""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + permissions_str = ','.join(permissions) if permissions else None + + cursor.execute(''' + INSERT INTO user_groups (name, permissions) + VALUES (?, ?) + ''', (name, permissions_str)) + + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error creating group: {e}") + return False + finally: + self.db_manager.disconnect() + + def get_all_groups(self): + """Get all user groups""" + if not self.db_manager.connect(): + return [] + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT id, name, permissions FROM user_groups') + groups = [] + for row in cursor.fetchall(): + permissions = row[2].split(',') if row[2] else [] + groups.append({ + 'id': row[0], + 'name': row[1], + 'permissions': permissions + }) + return groups + except Exception as e: + print(f"Error getting groups: {e}") + return [] + finally: + self.db_manager.disconnect() + + def delete_group(self, group_id): + """Delete a user group""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + + # Delete group memberships first + cursor.execute('DELETE FROM user_group_members WHERE group_id = ?', (group_id,)) + + # Delete group + cursor.execute('DELETE FROM user_groups WHERE id = ?', (group_id,)) + + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error deleting group: {e}") + return False + finally: + self.db_manager.disconnect() + + def add_user_to_group(self, user_id, group_id): + """Add a user to a group""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute(''' + INSERT OR IGNORE INTO user_group_members (user_id, group_id) + VALUES (?, ?) + ''', (user_id, group_id)) + + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error adding user to group: {e}") + return False + finally: + self.db_manager.disconnect() + + def remove_user_from_group(self, user_id, group_id): + """Remove a user from a group""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute(''' + DELETE FROM user_group_members + WHERE user_id = ? AND group_id = ? + ''', (user_id, group_id)) + + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error removing user from group: {e}") + return False + finally: + self.db_manager.disconnect() + + def get_user_groups(self, user_id): + """Get all groups for a user""" + if not self.db_manager.connect(): + return [] + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute(''' + SELECT g.id, g.name, g.permissions + FROM user_groups g + JOIN user_group_members m ON g.id = m.group_id + WHERE m.user_id = ? + ''', (user_id,)) + + groups = [] + for row in cursor.fetchall(): + permissions = row[2].split(',') if row[2] else [] + groups.append({ + 'id': row[0], + 'name': row[1], + 'permissions': permissions + }) + return groups + except Exception as e: + print(f"Error getting user groups: {e}") + return [] + finally: + self.db_manager.disconnect() + + def initialize_default_groups(self): + """Initialize default groups with permissions""" + # Check if default groups already exist + groups = self.get_all_groups() + default_group_names = ['Purchase Manager', 'Manufacturing Manager', 'Sales Manager', 'Inventory Manager', 'Reports Viewer'] + + # Check if all default groups exist + existing_group_names = [group['name'] for group in groups] + if all(name in existing_group_names for name in default_group_names): + return True + + # Create default groups + default_groups = [ + { + 'name': 'Purchase Manager', + 'permissions': ['view_dashboard', 'view_purchase_orders', 'edit_purchase_orders', 'manage_suppliers', 'receive_purchase_orders'] + }, + { + 'name': 'Manufacturing Manager', + 'permissions': ['view_dashboard', 'view_manufacturing_orders', 'edit_manufacturing_orders', 'complete_manufacturing_orders'] + }, + { + 'name': 'Sales Manager', + 'permissions': ['view_dashboard', 'view_sales_orders', 'edit_sales_orders', 'manage_customers', 'deliver_sales_orders'] + }, + { + 'name': 'Inventory Manager', + 'permissions': ['view_dashboard', 'view_inventory', 'adjust_inventory', 'view_stock_movements'] + }, + { + 'name': 'Reports Viewer', + 'permissions': ['view_dashboard', 'view_reports', 'view_inventory_report', 'view_stock_movement_report'] + } + ] + + for group in default_groups: + # Skip if group already exists + if group['name'] in existing_group_names: + continue + if not self.create_group(group['name'], group['permissions']): + return False + + return True \ No newline at end of file diff --git a/src/dao.py b/src/dao.py new file mode 100644 index 0000000..5556f40 --- /dev/null +++ b/src/dao.py @@ -0,0 +1,901 @@ +import sqlite3 +from datetime import datetime, date +from typing import List, Optional +from src.models import Product, Supplier, Customer, PurchaseOrder, PurchaseOrderItem, ManufacturingOrder, SalesOrder, SalesOrderItem, Inventory + + +class BaseDAO: + """Base DAO class with common database operations""" + + def __init__(self, db_manager): + self.db_manager = db_manager + + +class ProductDAO(BaseDAO): + """Data access object for Product model""" + + def create(self, product: Product) -> bool: + """Create a new product""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute(''' + INSERT INTO products (name, description, unit_price, sku, min_stock) + VALUES (?, ?, ?, ?, ?) + ''', (product.name, product.description, product.unit_price, product.sku, product.min_stock)) + + product.id = cursor.lastrowid + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error creating product: {e}") + return False + finally: + self.db_manager.disconnect() + + def get_by_id(self, product_id: int) -> Optional[Product]: + """Get product by ID""" + if not self.db_manager.connect(): + return None + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT * FROM products WHERE id = ?', (product_id,)) + row = cursor.fetchone() + + if row: + return Product( + id=row['id'], + name=row['name'], + description=row['description'], + unit_price=row['unit_price'], + sku=row['sku'], + min_stock=row['min_stock'] if 'min_stock' in row.keys() else 0 + ) + return None + except Exception as e: + print(f"Error getting product: {e}") + return None + finally: + self.db_manager.disconnect() + + def get_all(self) -> List[Product]: + """Get all products""" + if not self.db_manager.connect(): + return [] + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT * FROM products ORDER BY name') + rows = cursor.fetchall() + + products = [] + for row in rows: + products.append(Product( + id=row['id'], + name=row['name'], + description=row['description'], + unit_price=row['unit_price'], + sku=row['sku'], + min_stock=row['min_stock'] if 'min_stock' in row.keys() else 0 + )) + return products + except Exception as e: + print(f"Error getting products: {e}") + return [] + finally: + self.db_manager.disconnect() + + def update(self, product: Product) -> bool: + """Update a product""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute(''' + UPDATE products + SET name = ?, description = ?, unit_price = ?, sku = ?, min_stock = ? + WHERE id = ? + ''', (product.name, product.description, product.unit_price, product.sku, product.min_stock, product.id)) + + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error updating product: {e}") + return False + finally: + self.db_manager.disconnect() + + def delete(self, product_id: int) -> bool: + """Delete a product""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('DELETE FROM products WHERE id = ?', (product_id,)) + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error deleting product: {e}") + return False + finally: + self.db_manager.disconnect() + + +class SupplierDAO(BaseDAO): + """Data access object for Supplier model""" + + def create(self, supplier: Supplier) -> bool: + """Create a new supplier""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute(''' + INSERT INTO suppliers (name, contact_person, phone, email, address) + VALUES (?, ?, ?, ?, ?) + ''', (supplier.name, supplier.contact_person, supplier.phone, supplier.email, supplier.address)) + + supplier.id = cursor.lastrowid + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error creating supplier: {e}") + return False + finally: + self.db_manager.disconnect() + + def get_by_id(self, supplier_id: int) -> Optional[Supplier]: + """Get supplier by ID""" + if not self.db_manager.connect(): + return None + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT * FROM suppliers WHERE id = ?', (supplier_id,)) + row = cursor.fetchone() + + if row: + return Supplier( + id=row['id'], + name=row['name'], + contact_person=row['contact_person'], + phone=row['phone'], + email=row['email'], + address=row['address'] + ) + return None + except Exception as e: + print(f"Error getting supplier: {e}") + return None + finally: + self.db_manager.disconnect() + + def get_all(self) -> List[Supplier]: + """Get all suppliers""" + if not self.db_manager.connect(): + return [] + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT * FROM suppliers ORDER BY name') + rows = cursor.fetchall() + + suppliers = [] + for row in rows: + suppliers.append(Supplier( + id=row['id'], + name=row['name'], + contact_person=row['contact_person'], + phone=row['phone'], + email=row['email'], + address=row['address'] + )) + return suppliers + except Exception as e: + print(f"Error getting suppliers: {e}") + return [] + finally: + self.db_manager.disconnect() + + def update(self, supplier: Supplier) -> bool: + """Update a supplier""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute(''' + UPDATE suppliers + SET name = ?, contact_person = ?, phone = ?, email = ?, address = ? + WHERE id = ? + ''', (supplier.name, supplier.contact_person, supplier.phone, supplier.email, supplier.address, supplier.id)) + + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error updating supplier: {e}") + return False + finally: + self.db_manager.disconnect() + + def delete(self, supplier_id: int) -> bool: + """Delete a supplier""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('DELETE FROM suppliers WHERE id = ?', (supplier_id,)) + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error deleting supplier: {e}") + return False + finally: + self.db_manager.disconnect() + + +class CustomerDAO(BaseDAO): + """Data access object for Customer model""" + + def create(self, customer: Customer) -> bool: + """Create a new customer""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute(''' + INSERT INTO customers (name, contact_person, phone, email, address) + VALUES (?, ?, ?, ?, ?) + ''', (customer.name, customer.contact_person, customer.phone, customer.email, customer.address)) + + customer.id = cursor.lastrowid + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error creating customer: {e}") + return False + finally: + self.db_manager.disconnect() + + def get_by_id(self, customer_id: int) -> Optional[Customer]: + """Get customer by ID""" + if not self.db_manager.connect(): + return None + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT * FROM customers WHERE id = ?', (customer_id,)) + row = cursor.fetchone() + + if row: + return Customer( + id=row['id'], + name=row['name'], + contact_person=row['contact_person'], + phone=row['phone'], + email=row['email'], + address=row['address'] + ) + return None + except Exception as e: + print(f"Error getting customer: {e}") + return None + finally: + self.db_manager.disconnect() + + def get_all(self) -> List[Customer]: + """Get all customers""" + if not self.db_manager.connect(): + return [] + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT * FROM customers ORDER BY name') + rows = cursor.fetchall() + + customers = [] + for row in rows: + customers.append(Customer( + id=row['id'], + name=row['name'], + contact_person=row['contact_person'], + phone=row['phone'], + email=row['email'], + address=row['address'] + )) + return customers + except Exception as e: + print(f"Error getting customers: {e}") + return [] + finally: + self.db_manager.disconnect() + + def update(self, customer: Customer) -> bool: + """Update a customer""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute(''' + UPDATE customers + SET name = ?, contact_person = ?, phone = ?, email = ?, address = ? + WHERE id = ? + ''', (customer.name, customer.contact_person, customer.phone, customer.email, customer.address, customer.id)) + + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error updating customer: {e}") + return False + finally: + self.db_manager.disconnect() + + def delete(self, customer_id: int) -> bool: + """Delete a customer""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('DELETE FROM customers WHERE id = ?', (customer_id,)) + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error deleting customer: {e}") + return False + finally: + self.db_manager.disconnect() + + +class PurchaseOrderDAO(BaseDAO): + """Data access object for PurchaseOrder model""" + + def create(self, purchase_order: PurchaseOrder) -> bool: + """Create a new purchase order""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute(''' + INSERT INTO purchase_orders (supplier_id, order_date, expected_delivery, status, total_amount) + VALUES (?, ?, ?, ?, ?) + ''', (purchase_order.supplier_id, + purchase_order.order_date.isoformat() if purchase_order.order_date else None, + purchase_order.expected_delivery.isoformat() if purchase_order.expected_delivery else None, + purchase_order.status, + purchase_order.total_amount)) + + purchase_order.id = cursor.lastrowid + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error creating purchase order: {e}") + return False + finally: + self.db_manager.disconnect() + + def get_by_id(self, po_id: int) -> Optional[PurchaseOrder]: + """Get purchase order by ID""" + if not self.db_manager.connect(): + return None + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT * FROM purchase_orders WHERE id = ?', (po_id,)) + row = cursor.fetchone() + + if row: + order_date = datetime.fromisoformat(row['order_date']).date() if row['order_date'] else None + expected_delivery = datetime.fromisoformat(row['expected_delivery']).date() if row['expected_delivery'] else None + + return PurchaseOrder( + id=row['id'], + supplier_id=row['supplier_id'], + order_date=order_date, + expected_delivery=expected_delivery, + status=row['status'], + total_amount=row['total_amount'] + ) + return None + except Exception as e: + print(f"Error getting purchase order: {e}") + return None + finally: + self.db_manager.disconnect() + + def get_all(self) -> List[PurchaseOrder]: + """Get all purchase orders""" + if not self.db_manager.connect(): + return [] + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT * FROM purchase_orders ORDER BY order_date DESC') + rows = cursor.fetchall() + + purchase_orders = [] + for row in rows: + order_date = datetime.fromisoformat(row['order_date']).date() if row['order_date'] else None + expected_delivery = datetime.fromisoformat(row['expected_delivery']).date() if row['expected_delivery'] else None + + purchase_orders.append(PurchaseOrder( + id=row['id'], + supplier_id=row['supplier_id'], + order_date=order_date, + expected_delivery=expected_delivery, + status=row['status'], + total_amount=row['total_amount'] + )) + return purchase_orders + except Exception as e: + print(f"Error getting purchase orders: {e}") + return [] + finally: + self.db_manager.disconnect() + + def update(self, purchase_order: PurchaseOrder) -> bool: + """Update a purchase order""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute(''' + UPDATE purchase_orders + SET supplier_id = ?, order_date = ?, expected_delivery = ?, status = ?, total_amount = ? + WHERE id = ? + ''', (purchase_order.supplier_id, + purchase_order.order_date.isoformat() if purchase_order.order_date else None, + purchase_order.expected_delivery.isoformat() if purchase_order.expected_delivery else None, + purchase_order.status, + purchase_order.total_amount, + purchase_order.id)) + + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error updating purchase order: {e}") + return False + finally: + self.db_manager.disconnect() + + def delete(self, po_id: int) -> bool: + """Delete a purchase order""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + # Delete items first + cursor.execute('DELETE FROM purchase_order_items WHERE purchase_order_id = ?', (po_id,)) + # Delete order + cursor.execute('DELETE FROM purchase_orders WHERE id = ?', (po_id,)) + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error deleting purchase order: {e}") + return False + finally: + self.db_manager.disconnect() + + +class ManufacturingOrderDAO(BaseDAO): + """Data access object for ManufacturingOrder model""" + + def create(self, manufacturing_order: ManufacturingOrder) -> bool: + """Create a new manufacturing order""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute(''' + INSERT INTO manufacturing_orders (product_id, quantity, start_date, end_date, status) + VALUES (?, ?, ?, ?, ?) + ''', (manufacturing_order.product_id, + manufacturing_order.quantity, + manufacturing_order.start_date.isoformat() if manufacturing_order.start_date else None, + manufacturing_order.end_date.isoformat() if manufacturing_order.end_date else None, + manufacturing_order.status)) + + manufacturing_order.id = cursor.lastrowid + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error creating manufacturing order: {e}") + return False + finally: + self.db_manager.disconnect() + + def get_by_id(self, mo_id: int) -> Optional[ManufacturingOrder]: + """Get manufacturing order by ID""" + if not self.db_manager.connect(): + return None + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT * FROM manufacturing_orders WHERE id = ?', (mo_id,)) + row = cursor.fetchone() + + if row: + start_date = datetime.fromisoformat(row['start_date']).date() if row['start_date'] else None + end_date = datetime.fromisoformat(row['end_date']).date() if row['end_date'] else None + + return ManufacturingOrder( + id=row['id'], + product_id=row['product_id'], + quantity=row['quantity'], + start_date=start_date, + end_date=end_date, + status=row['status'] + ) + return None + except Exception as e: + print(f"Error getting manufacturing order: {e}") + return None + finally: + self.db_manager.disconnect() + + def get_all(self) -> List[ManufacturingOrder]: + """Get all manufacturing orders""" + if not self.db_manager.connect(): + return [] + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT * FROM manufacturing_orders ORDER BY start_date DESC') + rows = cursor.fetchall() + + manufacturing_orders = [] + for row in rows: + start_date = datetime.fromisoformat(row['start_date']).date() if row['start_date'] else None + end_date = datetime.fromisoformat(row['end_date']).date() if row['end_date'] else None + + manufacturing_orders.append(ManufacturingOrder( + id=row['id'], + product_id=row['product_id'], + quantity=row['quantity'], + start_date=start_date, + end_date=end_date, + status=row['status'] + )) + return manufacturing_orders + except Exception as e: + print(f"Error getting manufacturing orders: {e}") + return [] + finally: + self.db_manager.disconnect() + + def update(self, manufacturing_order: ManufacturingOrder) -> bool: + """Update a manufacturing order""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute(''' + UPDATE manufacturing_orders + SET product_id = ?, quantity = ?, start_date = ?, end_date = ?, status = ? + WHERE id = ? + ''', (manufacturing_order.product_id, + manufacturing_order.quantity, + manufacturing_order.start_date.isoformat() if manufacturing_order.start_date else None, + manufacturing_order.end_date.isoformat() if manufacturing_order.end_date else None, + manufacturing_order.status, + manufacturing_order.id)) + + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error updating manufacturing order: {e}") + return False + finally: + self.db_manager.disconnect() + + def delete(self, mo_id: int) -> bool: + """Delete a manufacturing order""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('DELETE FROM manufacturing_orders WHERE id = ?', (mo_id,)) + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error deleting manufacturing order: {e}") + return False + finally: + self.db_manager.disconnect() + + +class SalesOrderDAO(BaseDAO): + """Data access object for SalesOrder model""" + + def create(self, sales_order: SalesOrder) -> bool: + """Create a new sales order""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute(''' + INSERT INTO sales_orders (customer_id, order_date, expected_delivery, status, total_amount) + VALUES (?, ?, ?, ?, ?) + ''', (sales_order.customer_id, + sales_order.order_date.isoformat() if sales_order.order_date else None, + sales_order.expected_delivery.isoformat() if sales_order.expected_delivery else None, + sales_order.status, + sales_order.total_amount)) + + sales_order.id = cursor.lastrowid + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error creating sales order: {e}") + return False + finally: + self.db_manager.disconnect() + + def get_by_id(self, so_id: int) -> Optional[SalesOrder]: + """Get sales order by ID""" + if not self.db_manager.connect(): + return None + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT * FROM sales_orders WHERE id = ?', (so_id,)) + row = cursor.fetchone() + + if row: + order_date = datetime.fromisoformat(row['order_date']).date() if row['order_date'] else None + expected_delivery = datetime.fromisoformat(row['expected_delivery']).date() if row['expected_delivery'] else None + + return SalesOrder( + id=row['id'], + customer_id=row['customer_id'], + order_date=order_date, + expected_delivery=expected_delivery, + status=row['status'], + total_amount=row['total_amount'] + ) + return None + except Exception as e: + print(f"Error getting sales order: {e}") + return None + finally: + self.db_manager.disconnect() + + def get_all(self) -> List[SalesOrder]: + """Get all sales orders""" + if not self.db_manager.connect(): + return [] + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT * FROM sales_orders ORDER BY order_date DESC') + rows = cursor.fetchall() + + sales_orders = [] + for row in rows: + order_date = datetime.fromisoformat(row['order_date']).date() if row['order_date'] else None + expected_delivery = datetime.fromisoformat(row['expected_delivery']).date() if row['expected_delivery'] else None + + sales_orders.append(SalesOrder( + id=row['id'], + customer_id=row['customer_id'], + order_date=order_date, + expected_delivery=expected_delivery, + status=row['status'], + total_amount=row['total_amount'] + )) + return sales_orders + except Exception as e: + print(f"Error getting sales orders: {e}") + return [] + finally: + self.db_manager.disconnect() + + def update(self, sales_order: SalesOrder) -> bool: + """Update a sales order""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute(''' + UPDATE sales_orders + SET customer_id = ?, order_date = ?, expected_delivery = ?, status = ?, total_amount = ? + WHERE id = ? + ''', (sales_order.customer_id, + sales_order.order_date.isoformat() if sales_order.order_date else None, + sales_order.expected_delivery.isoformat() if sales_order.expected_delivery else None, + sales_order.status, + sales_order.total_amount, + sales_order.id)) + + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error updating sales order: {e}") + return False + finally: + self.db_manager.disconnect() + + def delete(self, so_id: int) -> bool: + """Delete a sales order""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + # Delete items first + cursor.execute('DELETE FROM sales_order_items WHERE sales_order_id = ?', (so_id,)) + # Delete order + cursor.execute('DELETE FROM sales_orders WHERE id = ?', (so_id,)) + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error deleting sales order: {e}") + return False + finally: + self.db_manager.disconnect() + + +class InventoryDAO(BaseDAO): + """Data access object for Inventory model""" + + def create(self, inventory: Inventory) -> bool: + """Create a new inventory record""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute(''' + INSERT OR IGNORE INTO inventory (product_id, quantity, reserved_quantity) + VALUES (?, ?, ?) + ''', (inventory.product_id, inventory.quantity, inventory.reserved_quantity)) + + # If the record already exists, update it + cursor.execute(''' + UPDATE inventory + SET quantity = ?, reserved_quantity = ?, last_updated = CURRENT_TIMESTAMP + WHERE product_id = ? + ''', (inventory.quantity, inventory.reserved_quantity, inventory.product_id)) + + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error creating inventory record: {e}") + return False + finally: + self.db_manager.disconnect() + + def get_by_product_id(self, product_id: int) -> Optional[Inventory]: + """Get inventory by product ID""" + if not self.db_manager.connect(): + return None + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT * FROM inventory WHERE product_id = ?', (product_id,)) + row = cursor.fetchone() + + if row: + last_updated = datetime.fromisoformat(row['last_updated']) if row['last_updated'] else None + + return Inventory( + id=row['id'], + product_id=row['product_id'], + quantity=row['quantity'], + reserved_quantity=row['reserved_quantity'] + ) + return None + except Exception as e: + print(f"Error getting inventory: {e}") + return None + finally: + self.db_manager.disconnect() + + def get_all(self) -> List[Inventory]: + """Get all inventory records""" + if not self.db_manager.connect(): + return [] + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT * FROM inventory ORDER BY product_id') + rows = cursor.fetchall() + + inventory_records = [] + for row in rows: + last_updated = datetime.fromisoformat(row['last_updated']) if row['last_updated'] else None + + inventory_records.append(Inventory( + id=row['id'], + product_id=row['product_id'], + quantity=row['quantity'], + reserved_quantity=row['reserved_quantity'] + )) + return inventory_records + except Exception as e: + print(f"Error getting inventory records: {e}") + return [] + finally: + self.db_manager.disconnect() + + def update(self, inventory: Inventory) -> bool: + """Update an inventory record""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute(''' + UPDATE inventory + SET quantity = ?, reserved_quantity = ?, last_updated = CURRENT_TIMESTAMP + WHERE product_id = ? + ''', (inventory.quantity, inventory.reserved_quantity, inventory.product_id)) + + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error updating inventory: {e}") + return False + finally: + self.db_manager.disconnect() + + def delete(self, product_id: int) -> bool: + """Delete an inventory record""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('DELETE FROM inventory WHERE product_id = ?', (product_id,)) + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error deleting inventory: {e}") + return False + finally: + self.db_manager.disconnect() + + def adjust_quantity(self, product_id: int, quantity_change: int) -> bool: + """Adjust inventory quantity""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute(''' + UPDATE inventory + SET quantity = quantity + ?, last_updated = CURRENT_TIMESTAMP + WHERE product_id = ? + ''', (quantity_change, product_id)) + + # If no rows were updated, insert a new record + if cursor.rowcount == 0: + cursor.execute(''' + INSERT INTO inventory (product_id, quantity, reserved_quantity) + VALUES (?, ?, 0) + ''', (product_id, quantity_change)) + + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error adjusting inventory: {e}") + return False + finally: + self.db_manager.disconnect() \ No newline at end of file diff --git a/src/dao_items.py b/src/dao_items.py new file mode 100644 index 0000000..a9e9c72 --- /dev/null +++ b/src/dao_items.py @@ -0,0 +1,295 @@ +import sqlite3 +from datetime import datetime, date +from typing import List, Optional +from src.models import PurchaseOrderItem, SalesOrderItem + + +class BaseDAO: + """Base DAO class with common database operations""" + + def __init__(self, db_manager): + self.db_manager = db_manager + + +class PurchaseOrderItemDAO(BaseDAO): + """Data access object for PurchaseOrderItem model""" + + def create(self, item: PurchaseOrderItem) -> bool: + """Create a new purchase order item""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute(''' + INSERT INTO purchase_order_items (purchase_order_id, product_id, quantity, unit_price, total_price) + VALUES (?, ?, ?, ?, ?) + ''', (item.purchase_order_id, item.product_id, item.quantity, item.unit_price, item.total_price)) + + item.id = cursor.lastrowid + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error creating purchase order item: {e}") + return False + finally: + self.db_manager.disconnect() + + def get_by_id(self, item_id: int) -> Optional[PurchaseOrderItem]: + """Get purchase order item by ID""" + if not self.db_manager.connect(): + return None + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT * FROM purchase_order_items WHERE id = ?', (item_id,)) + row = cursor.fetchone() + + if row: + return PurchaseOrderItem( + id=row['id'], + purchase_order_id=row['purchase_order_id'], + product_id=row['product_id'], + quantity=row['quantity'], + unit_price=row['unit_price'], + total_price=row['total_price'] + ) + return None + except Exception as e: + print(f"Error getting purchase order item: {e}") + return None + finally: + self.db_manager.disconnect() + + def get_by_purchase_order_id(self, po_id: int) -> List[PurchaseOrderItem]: + """Get all items for a purchase order""" + if not self.db_manager.connect(): + return [] + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT * FROM purchase_order_items WHERE purchase_order_id = ?', (po_id,)) + rows = cursor.fetchall() + + items = [] + for row in rows: + items.append(PurchaseOrderItem( + id=row['id'], + purchase_order_id=row['purchase_order_id'], + product_id=row['product_id'], + quantity=row['quantity'], + unit_price=row['unit_price'], + total_price=row['total_price'] + )) + return items + except Exception as e: + print(f"Error getting purchase order items: {e}") + return [] + finally: + self.db_manager.disconnect() + + def get_all(self) -> List[PurchaseOrderItem]: + """Get all purchase order items""" + if not self.db_manager.connect(): + return [] + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT * FROM purchase_order_items') + rows = cursor.fetchall() + + items = [] + for row in rows: + items.append(PurchaseOrderItem( + id=row['id'], + purchase_order_id=row['purchase_order_id'], + product_id=row['product_id'], + quantity=row['quantity'], + unit_price=row['unit_price'], + total_price=row['total_price'] + )) + return items + except Exception as e: + print(f"Error getting purchase order items: {e}") + return [] + finally: + self.db_manager.disconnect() + + def update(self, item: PurchaseOrderItem) -> bool: + """Update a purchase order item""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute(''' + UPDATE purchase_order_items + SET purchase_order_id = ?, product_id = ?, quantity = ?, unit_price = ?, total_price = ? + WHERE id = ? + ''', (item.purchase_order_id, item.product_id, item.quantity, item.unit_price, item.total_price, item.id)) + + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error updating purchase order item: {e}") + return False + finally: + self.db_manager.disconnect() + + def delete(self, item_id: int) -> bool: + """Delete a purchase order item""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('DELETE FROM purchase_order_items WHERE id = ?', (item_id,)) + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error deleting purchase order item: {e}") + return False + finally: + self.db_manager.disconnect() + + +class SalesOrderItemDAO(BaseDAO): + """Data access object for SalesOrderItem model""" + + def create(self, item: SalesOrderItem) -> bool: + """Create a new sales order item""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute(''' + INSERT INTO sales_order_items (sales_order_id, product_id, quantity, unit_price, total_price) + VALUES (?, ?, ?, ?, ?) + ''', (item.sales_order_id, item.product_id, item.quantity, item.unit_price, item.total_price)) + + item.id = cursor.lastrowid + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error creating sales order item: {e}") + return False + finally: + self.db_manager.disconnect() + + def get_by_id(self, item_id: int) -> Optional[SalesOrderItem]: + """Get sales order item by ID""" + if not self.db_manager.connect(): + return None + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT * FROM sales_order_items WHERE id = ?', (item_id,)) + row = cursor.fetchone() + + if row: + return SalesOrderItem( + id=row['id'], + sales_order_id=row['sales_order_id'], + product_id=row['product_id'], + quantity=row['quantity'], + unit_price=row['unit_price'], + total_price=row['total_price'] + ) + return None + except Exception as e: + print(f"Error getting sales order item: {e}") + return None + finally: + self.db_manager.disconnect() + + def get_by_sales_order_id(self, so_id: int) -> List[SalesOrderItem]: + """Get all items for a sales order""" + if not self.db_manager.connect(): + return [] + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT * FROM sales_order_items WHERE sales_order_id = ?', (so_id,)) + rows = cursor.fetchall() + + items = [] + for row in rows: + items.append(SalesOrderItem( + id=row['id'], + sales_order_id=row['sales_order_id'], + product_id=row['product_id'], + quantity=row['quantity'], + unit_price=row['unit_price'], + total_price=row['total_price'] + )) + return items + except Exception as e: + print(f"Error getting sales order items: {e}") + return [] + finally: + self.db_manager.disconnect() + + def get_all(self) -> List[SalesOrderItem]: + """Get all sales order items""" + if not self.db_manager.connect(): + return [] + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT * FROM sales_order_items') + rows = cursor.fetchall() + + items = [] + for row in rows: + items.append(SalesOrderItem( + id=row['id'], + sales_order_id=row['sales_order_id'], + product_id=row['product_id'], + quantity=row['quantity'], + unit_price=row['unit_price'], + total_price=row['total_price'] + )) + return items + except Exception as e: + print(f"Error getting sales order items: {e}") + return [] + finally: + self.db_manager.disconnect() + + def update(self, item: SalesOrderItem) -> bool: + """Update a sales order item""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute(''' + UPDATE sales_order_items + SET sales_order_id = ?, product_id = ?, quantity = ?, unit_price = ?, total_price = ? + WHERE id = ? + ''', (item.sales_order_id, item.product_id, item.quantity, item.unit_price, item.total_price, item.id)) + + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error updating sales order item: {e}") + return False + finally: + self.db_manager.disconnect() + + def delete(self, item_id: int) -> bool: + """Delete a sales order item""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('DELETE FROM sales_order_items WHERE id = ?', (item_id,)) + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error deleting sales order item: {e}") + return False + finally: + self.db_manager.disconnect() \ No newline at end of file diff --git a/src/dao_stock.py b/src/dao_stock.py new file mode 100644 index 0000000..41c2a88 --- /dev/null +++ b/src/dao_stock.py @@ -0,0 +1,156 @@ +import sqlite3 +from datetime import datetime, date +from typing import List, Optional +from src.models import StockMovement + + +class BaseDAO: + """Base DAO class with common database operations""" + + def __init__(self, db_manager): + self.db_manager = db_manager + + +class StockMovementDAO(BaseDAO): + """Data access object for StockMovement model""" + + def create(self, movement: StockMovement) -> bool: + """Create a new stock movement record""" + if not self.db_manager.connect(): + return False + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute(''' + INSERT INTO stock_movements (product_id, movement_type, quantity, reference_type, reference_id) + VALUES (?, ?, ?, ?, ?) + ''', (movement.product_id, movement.movement_type, movement.quantity, + movement.reference_type, movement.reference_id)) + + movement.id = cursor.lastrowid + self.db_manager.connection.commit() + return True + except Exception as e: + print(f"Error creating stock movement: {e}") + return False + finally: + self.db_manager.disconnect() + + def get_by_id(self, movement_id: int) -> Optional[StockMovement]: + """Get stock movement by ID""" + if not self.db_manager.connect(): + return None + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT * FROM stock_movements WHERE id = ?', (movement_id,)) + row = cursor.fetchone() + + if row: + created_at = datetime.fromisoformat(row['created_at']) if row['created_at'] else None + + return StockMovement( + id=row['id'], + product_id=row['product_id'], + movement_type=row['movement_type'], + quantity=row['quantity'], + reference_type=row['reference_type'], + reference_id=row['reference_id'] + ) + return None + except Exception as e: + print(f"Error getting stock movement: {e}") + return None + finally: + self.db_manager.disconnect() + + def get_by_product_id(self, product_id: int) -> List[StockMovement]: + """Get all stock movements for a product""" + if not self.db_manager.connect(): + return [] + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT * FROM stock_movements WHERE product_id = ? ORDER BY created_at DESC', (product_id,)) + rows = cursor.fetchall() + + movements = [] + for row in rows: + created_at = datetime.fromisoformat(row['created_at']) if row['created_at'] else None + + movements.append(StockMovement( + id=row['id'], + product_id=row['product_id'], + movement_type=row['movement_type'], + quantity=row['quantity'], + reference_type=row['reference_type'], + reference_id=row['reference_id'] + )) + return movements + except Exception as e: + print(f"Error getting stock movements: {e}") + return [] + finally: + self.db_manager.disconnect() + + def get_all(self) -> List[StockMovement]: + """Get all stock movements""" + if not self.db_manager.connect(): + return [] + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute('SELECT * FROM stock_movements ORDER BY created_at DESC') + rows = cursor.fetchall() + + movements = [] + for row in rows: + created_at = datetime.fromisoformat(row['created_at']) if row['created_at'] else None + + movements.append(StockMovement( + id=row['id'], + product_id=row['product_id'], + movement_type=row['movement_type'], + quantity=row['quantity'], + reference_type=row['reference_type'], + reference_id=row['reference_id'] + )) + return movements + except Exception as e: + print(f"Error getting stock movements: {e}") + return [] + finally: + self.db_manager.disconnect() + + def get_by_date_range(self, start_date: date, end_date: date) -> List[StockMovement]: + """Get stock movements within a date range""" + if not self.db_manager.connect(): + return [] + + try: + cursor = self.db_manager.connection.cursor() + cursor.execute(''' + SELECT * FROM stock_movements + WHERE DATE(created_at) BETWEEN ? AND ? + ORDER BY created_at DESC + ''', (start_date.isoformat(), end_date.isoformat())) + rows = cursor.fetchall() + + movements = [] + for row in rows: + created_at = datetime.fromisoformat(row['created_at']) if row['created_at'] else None + + movements.append(StockMovement( + id=row['id'], + product_id=row['product_id'], + movement_type=row['movement_type'], + quantity=row['quantity'], + reference_type=row['reference_type'], + reference_id=row['reference_id'] + )) + return movements + except Exception as e: + print(f"Error getting stock movements by date range: {e}") + return [] + finally: + self.db_manager.disconnect() \ No newline at end of file diff --git a/src/database.py b/src/database.py new file mode 100644 index 0000000..1345630 --- /dev/null +++ b/src/database.py @@ -0,0 +1,325 @@ +import sqlite3 +import os +import json +from datetime import datetime +import shutil + + +class DatabaseManager: + """Database manager for the manufacturing app""" + + def __init__(self, db_path=None): + """Initialize the database manager""" + self.db_path = db_path or "manufacturing.db" + self.config_path = "db_config.json" + self.connection = None + self.load_config() + + def load_config(self): + """Load database configuration""" + if os.path.exists(self.config_path): + with open(self.config_path, 'r') as f: + config = json.load(f) + self.db_type = config.get('db_type', 'sqlite') + self.db_host = config.get('db_host', '') + self.db_port = config.get('db_port', '') + self.db_name = config.get('db_name', '') + self.db_user = config.get('db_user', '') + self.db_password = config.get('db_password', '') + else: + # Default to SQLite + self.db_type = 'sqlite' + self.db_host = '' + self.db_port = '' + self.db_name = self.db_path + self.db_user = '' + self.db_password = '' + + def save_config(self): + """Save database configuration""" + config = { + 'db_type': self.db_type, + 'db_host': self.db_host, + 'db_port': self.db_port, + 'db_name': self.db_name, + 'db_user': self.db_user, + 'db_password': self.db_password + } + + with open(self.config_path, 'w') as f: + json.dump(config, f, indent=2) + + def connect(self): + """Connect to the database""" + try: + if self.db_type == 'sqlite': + self.connection = sqlite3.connect(self.db_name) + self.connection.row_factory = sqlite3.Row + elif self.db_type == 'postgresql': + # For PostgreSQL, we would use psycopg2 + # This is a placeholder implementation + import psycopg2 + self.connection = psycopg2.connect( + host=self.db_host, + port=self.db_port, + database=self.db_name, + user=self.db_user, + password=self.db_password + ) + return True + except Exception as e: + print(f"Database connection error: {e}") + return False + + def disconnect(self): + """Disconnect from the database""" + if self.connection: + self.connection.close() + self.connection = None + + def initialize_database(self): + """Initialize the database with required tables""" + if not self.connect(): + return False + + try: + cursor = self.connection.cursor() + + # Create users table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + is_admin BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Create user_groups table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS user_groups ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + permissions TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Create user_group_members table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS user_group_members ( + user_id INTEGER, + group_id INTEGER, + FOREIGN KEY (user_id) REFERENCES users (id), + FOREIGN KEY (group_id) REFERENCES user_groups (id), + PRIMARY KEY (user_id, group_id) + ) + ''') + + # Create products table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS products ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + unit_price REAL DEFAULT 0, + sku TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Create suppliers table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS suppliers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + contact_person TEXT, + phone TEXT, + email TEXT, + address TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Create customers table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS customers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + contact_person TEXT, + phone TEXT, + email TEXT, + address TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Create purchase_orders table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS purchase_orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + supplier_id INTEGER, + order_date DATE, + expected_delivery DATE, + status TEXT DEFAULT 'pending', + total_amount REAL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (supplier_id) REFERENCES suppliers (id) + ) + ''') + + # Create purchase_order_items table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS purchase_order_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + purchase_order_id INTEGER, + product_id INTEGER, + quantity INTEGER, + unit_price REAL, + total_price REAL, + FOREIGN KEY (purchase_order_id) REFERENCES purchase_orders (id), + FOREIGN KEY (product_id) REFERENCES products (id) + ) + ''') + + # Create manufacturing_orders table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS manufacturing_orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + product_id INTEGER, + quantity INTEGER, + start_date DATE, + end_date DATE, + status TEXT DEFAULT 'pending', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (product_id) REFERENCES products (id) + ) + ''') + + # Create sales_orders table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS sales_orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + customer_id INTEGER, + order_date DATE, + expected_delivery DATE, + status TEXT DEFAULT 'pending', + total_amount REAL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (customer_id) REFERENCES customers (id) + ) + ''') + + # Create sales_order_items table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS sales_order_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + sales_order_id INTEGER, + product_id INTEGER, + quantity INTEGER, + unit_price REAL, + total_price REAL, + FOREIGN KEY (sales_order_id) REFERENCES sales_orders (id), + FOREIGN KEY (product_id) REFERENCES products (id) + ) + ''') + + # Create inventory table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS inventory ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + product_id INTEGER UNIQUE, + quantity INTEGER DEFAULT 0, + reserved_quantity INTEGER DEFAULT 0, + last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (product_id) REFERENCES products (id) + ) + ''') + + # Create stock_movements table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS stock_movements ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + product_id INTEGER, + movement_type TEXT, -- 'IN' or 'OUT' + quantity INTEGER, + reference_type TEXT, -- 'PO', 'SO', 'MO', 'ADJUSTMENT' + reference_id INTEGER, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (product_id) REFERENCES products (id) + ) + ''') + + # Create stock movements table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS stock_movements ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + product_id INTEGER, + movement_type TEXT, -- 'IN' for receipts, 'OUT' for issues + quantity INTEGER, + reference_type TEXT, -- 'PO', 'SO', 'MO', 'ADJUSTMENT' + reference_id INTEGER, -- ID of the related document + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (product_id) REFERENCES products (id) + ) + ''') + + self.connection.commit() + return True + except Exception as e: + print(f"Database initialization error: {e}") + return False + finally: + self.disconnect() + + def backup_database(self, backup_path=None): + """Backup the database""" + try: + if not backup_path: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + backup_path = f"backup_{timestamp}.db" + + if self.db_type == 'sqlite': + shutil.copy2(self.db_name, backup_path) + return backup_path + else: + # For other databases, implement appropriate backup logic + return None + except Exception as e: + print(f"Database backup error: {e}") + return None + + def restore_database(self, backup_path): + """Restore the database from backup""" + try: + if self.db_type == 'sqlite': + shutil.copy2(backup_path, self.db_name) + return True + else: + # For other databases, implement appropriate restore logic + return False + except Exception as e: + print(f"Database restore error: {e}") + return False + + def duplicate_database(self, new_name): + """Duplicate the database""" + try: + if self.db_type == 'sqlite': + shutil.copy2(self.db_name, new_name) + return new_name + else: + # For other databases, implement appropriate duplication logic + return None + except Exception as e: + print(f"Database duplication error: {e}") + return None + + def get_connection_string(self): + """Get database connection string""" + if self.db_type == 'sqlite': + return f"sqlite:///{self.db_name}" + elif self.db_type == 'postgresql': + return f"postgresql://{self.db_user}:{self.db_password}@{self.db_host}:{self.db_port}/{self.db_name}" + return "" \ No newline at end of file diff --git a/src/models.py b/src/models.py new file mode 100644 index 0000000..9d3ee81 --- /dev/null +++ b/src/models.py @@ -0,0 +1,240 @@ +import sqlite3 +from datetime import datetime, date +from typing import List, Optional + + +class Product: + """Product model""" + + def __init__(self, id=None, name="", description="", unit_price=0.0, sku=None, min_stock=0): + self.id = id + self.name = name + self.description = description + self.unit_price = unit_price + self.sku = sku + self.min_stock = min_stock + self.created_at = datetime.now() + + def to_dict(self): + return { + 'id': self.id, + 'name': self.name, + 'description': self.description, + 'unit_price': self.unit_price, + 'sku': self.sku, + 'min_stock': self.min_stock, + 'created_at': self.created_at.isoformat() if self.created_at else None + } + + +class Supplier: + """Supplier model""" + + def __init__(self, id=None, name="", contact_person="", phone="", email="", address=""): + self.id = id + self.name = name + self.contact_person = contact_person + self.phone = phone + self.email = email + self.address = address + self.created_at = datetime.now() + + def to_dict(self): + return { + 'id': self.id, + 'name': self.name, + 'contact_person': self.contact_person, + 'phone': self.phone, + 'email': self.email, + 'address': self.address, + 'created_at': self.created_at.isoformat() if self.created_at else None + } + + +class Customer: + """Customer model""" + + def __init__(self, id=None, name="", contact_person="", phone="", email="", address=""): + self.id = id + self.name = name + self.contact_person = contact_person + self.phone = phone + self.email = email + self.address = address + self.created_at = datetime.now() + + def to_dict(self): + return { + 'id': self.id, + 'name': self.name, + 'contact_person': self.contact_person, + 'phone': self.phone, + 'email': self.email, + 'address': self.address, + 'created_at': self.created_at.isoformat() if self.created_at else None + } + + +class PurchaseOrder: + """Purchase order model""" + + def __init__(self, id=None, supplier_id=None, order_date=None, expected_delivery=None, + status="pending", total_amount=0.0): + self.id = id + self.supplier_id = supplier_id + self.order_date = order_date or date.today() + self.expected_delivery = expected_delivery + self.status = status + self.total_amount = total_amount + self.created_at = datetime.now() + self.items: List[PurchaseOrderItem] = [] + + def to_dict(self): + return { + 'id': self.id, + 'supplier_id': self.supplier_id, + 'order_date': self.order_date.isoformat() if self.order_date else None, + 'expected_delivery': self.expected_delivery.isoformat() if self.expected_delivery else None, + 'status': self.status, + 'total_amount': self.total_amount, + 'created_at': self.created_at.isoformat() if self.created_at else None + } + + +class PurchaseOrderItem: + """Purchase order item model""" + + def __init__(self, id=None, purchase_order_id=None, product_id=None, quantity=0, unit_price=0.0, total_price=0.0): + self.id = id + self.purchase_order_id = purchase_order_id + self.product_id = product_id + self.quantity = quantity + self.unit_price = unit_price + self.total_price = total_price + + def to_dict(self): + return { + 'id': self.id, + 'purchase_order_id': self.purchase_order_id, + 'product_id': self.product_id, + 'quantity': self.quantity, + 'unit_price': self.unit_price, + 'total_price': self.total_price + } + + +class ManufacturingOrder: + """Manufacturing order model""" + + def __init__(self, id=None, product_id=None, quantity=0, start_date=None, end_date=None, status="pending"): + self.id = id + self.product_id = product_id + self.quantity = quantity + self.start_date = start_date or date.today() + self.end_date = end_date + self.status = status + self.created_at = datetime.now() + + def to_dict(self): + return { + 'id': self.id, + 'product_id': self.product_id, + 'quantity': self.quantity, + 'start_date': self.start_date.isoformat() if self.start_date else None, + 'end_date': self.end_date.isoformat() if self.end_date else None, + 'status': self.status, + 'created_at': self.created_at.isoformat() if self.created_at else None + } + + +class SalesOrder: + """Sales order model""" + + def __init__(self, id=None, customer_id=None, order_date=None, expected_delivery=None, + status="pending", total_amount=0.0): + self.id = id + self.customer_id = customer_id + self.order_date = order_date or date.today() + self.expected_delivery = expected_delivery + self.status = status + self.total_amount = total_amount + self.created_at = datetime.now() + self.items: List[SalesOrderItem] = [] + + def to_dict(self): + return { + 'id': self.id, + 'customer_id': self.customer_id, + 'order_date': self.order_date.isoformat() if self.order_date else None, + 'expected_delivery': self.expected_delivery.isoformat() if self.expected_delivery else None, + 'status': self.status, + 'total_amount': self.total_amount, + 'created_at': self.created_at.isoformat() if self.created_at else None + } + + +class SalesOrderItem: + """Sales order item model""" + + def __init__(self, id=None, sales_order_id=None, product_id=None, quantity=0, unit_price=0.0, total_price=0.0): + self.id = id + self.sales_order_id = sales_order_id + self.product_id = product_id + self.quantity = quantity + self.unit_price = unit_price + self.total_price = total_price + + def to_dict(self): + return { + 'id': self.id, + 'sales_order_id': self.sales_order_id, + 'product_id': self.product_id, + 'quantity': self.quantity, + 'unit_price': self.unit_price, + 'total_price': self.total_price + } + + +class Inventory: + """Inventory model""" + + def __init__(self, id=None, product_id=None, quantity=0, reserved_quantity=0): + self.id = id + self.product_id = product_id + self.quantity = quantity + self.reserved_quantity = reserved_quantity + self.last_updated = datetime.now() + + def to_dict(self): + return { + 'id': self.id, + 'product_id': self.product_id, + 'quantity': self.quantity, + 'reserved_quantity': self.reserved_quantity, + 'last_updated': self.last_updated.isoformat() if self.last_updated else None + } + + +class StockMovement: + """Stock movement model""" + + def __init__(self, id=None, product_id=None, movement_type="", quantity=0, + reference_type="", reference_id=None): + self.id = id + self.product_id = product_id + self.movement_type = movement_type # 'IN' or 'OUT' + self.quantity = quantity + self.reference_type = reference_type # 'PO', 'SO', 'MO', 'ADJUSTMENT' + self.reference_id = reference_id + self.created_at = datetime.now() + + def to_dict(self): + return { + 'id': self.id, + 'product_id': self.product_id, + 'movement_type': self.movement_type, + 'quantity': self.quantity, + 'reference_type': self.reference_type, + 'reference_id': self.reference_id, + 'created_at': self.created_at.isoformat() if self.created_at else None + } \ No newline at end of file diff --git a/src/services.py b/src/services.py new file mode 100644 index 0000000..ad01e76 --- /dev/null +++ b/src/services.py @@ -0,0 +1,699 @@ +from typing import List, Optional +import pandas as pd +from datetime import datetime, date +import os + +from src.models import Product, Supplier, Customer, PurchaseOrder, PurchaseOrderItem, ManufacturingOrder, SalesOrder, SalesOrderItem, Inventory, StockMovement +from src.dao import ProductDAO, SupplierDAO, CustomerDAO, PurchaseOrderDAO, ManufacturingOrderDAO, SalesOrderDAO, InventoryDAO +from src.dao_items import PurchaseOrderItemDAO, SalesOrderItemDAO +from src.dao_stock import StockMovementDAO + + +class ProductService: + """Service for managing products""" + + def __init__(self, db_manager): + self.dao = ProductDAO(db_manager) + + def create_product(self, name: str, description: str = "", unit_price: float = 0.0) -> Optional[Product]: + """Create a new product""" + product = Product(name=name, description=description, unit_price=unit_price) + if self.dao.create(product): + return product + return None + + def get_product(self, product_id: int) -> Optional[Product]: + """Get product by ID""" + return self.dao.get_by_id(product_id) + + def get_all_products(self) -> List[Product]: + """Get all products""" + return self.dao.get_all() + + def update_product(self, product: Product) -> bool: + """Update a product""" + return self.dao.update(product) + + def delete_product(self, product_id: int) -> bool: + """Delete a product""" + return self.dao.delete(product_id) + + +class SupplierService: + """Service for managing suppliers""" + + def __init__(self, db_manager): + self.dao = SupplierDAO(db_manager) + + def create_supplier(self, name: str, contact_person: str = "", phone: str = "", + email: str = "", address: str = "") -> Optional[Supplier]: + """Create a new supplier""" + supplier = Supplier(name=name, contact_person=contact_person, phone=phone, + email=email, address=address) + if self.dao.create(supplier): + return supplier + return None + + def get_supplier(self, supplier_id: int) -> Optional[Supplier]: + """Get supplier by ID""" + return self.dao.get_by_id(supplier_id) + + def get_all_suppliers(self) -> List[Supplier]: + """Get all suppliers""" + return self.dao.get_all() + + def update_supplier(self, supplier: Supplier) -> bool: + """Update a supplier""" + return self.dao.update(supplier) + + def delete_supplier(self, supplier_id: int) -> bool: + """Delete a supplier""" + return self.dao.delete(supplier_id) + + +class CustomerService: + """Service for managing customers""" + + def __init__(self, db_manager): + self.dao = CustomerDAO(db_manager) + + def create_customer(self, name: str, contact_person: str = "", phone: str = "", + email: str = "", address: str = "") -> Optional[Customer]: + """Create a new customer""" + customer = Customer(name=name, contact_person=contact_person, phone=phone, + email=email, address=address) + if self.dao.create(customer): + return customer + return None + + def get_customer(self, customer_id: int) -> Optional[Customer]: + """Get customer by ID""" + return self.dao.get_by_id(customer_id) + + def get_all_customers(self) -> List[Customer]: + """Get all customers""" + return self.dao.get_all() + + def update_customer(self, customer: Customer) -> bool: + """Update a customer""" + return self.dao.update(customer) + + def delete_customer(self, customer_id: int) -> bool: + """Delete a customer""" + return self.dao.delete(customer_id) + + +class PurchaseOrderService: + """Service for managing purchase orders""" + + def __init__(self, db_manager): + self.dao = PurchaseOrderDAO(db_manager) + self.item_dao = PurchaseOrderItemDAO(db_manager) + self.product_service = ProductService(db_manager) + self.supplier_service = SupplierService(db_manager) + self.inventory_service = InventoryService(db_manager) + + def create_purchase_order(self, supplier_id: int, order_date=None, expected_delivery=None, + status: str = "pending", total_amount: float = 0.0) -> Optional[PurchaseOrder]: + """Create a new purchase order""" + # Verify supplier exists + if not self.supplier_service.get_supplier(supplier_id): + return None + + po = PurchaseOrder(supplier_id=supplier_id, order_date=order_date, + expected_delivery=expected_delivery, status=status, + total_amount=total_amount) + if self.dao.create(po): + return po + return None + + def add_purchase_order_item(self, po_id: int, product_id: int, quantity: int, + unit_price: float, total_price: float) -> bool: + """Add an item to a purchase order""" + # Verify purchase order exists + po = self.get_purchase_order(po_id) + if not po: + return False + + # Verify product exists + if not self.product_service.get_product(product_id): + return False + + # Create item + item = PurchaseOrderItem( + purchase_order_id=po_id, + product_id=product_id, + quantity=quantity, + unit_price=unit_price, + total_price=total_price + ) + + return self.item_dao.create(item) + + def get_purchase_order(self, po_id: int) -> Optional[PurchaseOrder]: + """Get purchase order by ID""" + return self.dao.get_by_id(po_id) + + def get_all_purchase_orders(self) -> List[PurchaseOrder]: + """Get all purchase orders""" + return self.dao.get_all() + + def get_pending_purchase_orders(self) -> List[PurchaseOrder]: + """Get all pending purchase orders""" + all_orders = self.get_all_purchase_orders() + return [order for order in all_orders if order.status == "pending"] + + def get_recent_purchase_orders(self, limit: int = 5) -> List[PurchaseOrder]: + """Get recent purchase orders""" + all_orders = self.get_all_purchase_orders() + # Sort by created_at in descending order and return the limit + sorted_orders = sorted(all_orders, key=lambda x: x.created_at, reverse=True) + return sorted_orders[:limit] + + def update_purchase_order(self, po: PurchaseOrder) -> bool: + """Update a purchase order""" + return self.dao.update(po) + + def delete_purchase_order(self, po_id: int) -> bool: + """Delete a purchase order""" + return self.dao.delete(po_id) + + def receive_purchase_order(self, po_id: int) -> bool: + """Receive products from a purchase order and update inventory""" + po = self.get_purchase_order(po_id) + if not po or po.status != "pending": + return False + + # Get all items for this purchase order + items = self.item_dao.get_by_purchase_order_id(po_id) + + # Update inventory for each item + for item in items: + if not self.inventory_service.adjust_inventory(item.product_id, item.quantity, "PO", po_id): + return False + + # Update order status + po.status = "completed" + if self.update_purchase_order(po): + return True + return False + + def get_purchase_order_cost(self, po_id: int) -> float: + """Get total cost for a purchase order""" + po = self.get_purchase_order(po_id) + if not po: + return 0.0 + return po.total_amount + + def get_all_costs(self) -> float: + """Get total costs from all completed purchase orders""" + total_cost = 0.0 + purchase_orders = self.get_all_purchase_orders() + for po in purchase_orders: + if po.status == "completed": + total_cost += po.total_amount + return total_cost + + def get_total_costs(self) -> float: + """Get total costs from all completed purchase orders""" + return self.get_all_costs() + + def export_to_excel(self, filename: str = None, start_date: date = None, end_date: date = None) -> str: + """Export purchase orders to Excel with optional date range filtering""" + if filename is None: + filename = f"purchase_orders_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" + + purchase_orders = self.get_all_purchase_orders() + + # Filter by date range if provided + if start_date and end_date: + purchase_orders = [po for po in purchase_orders if start_date <= po.order_date <= end_date] + elif start_date: + purchase_orders = [po for po in purchase_orders if po.order_date >= start_date] + elif end_date: + purchase_orders = [po for po in purchase_orders if po.order_date <= end_date] + + # Prepare data for DataFrame + data = [] + for po in purchase_orders: + supplier = self.supplier_service.get_supplier(po.supplier_id) + supplier_name = supplier.name if supplier else "Unknown" + + data.append({ + 'Order ID': po.id, + 'Supplier': supplier_name, + 'Order Date': po.order_date, + 'Expected Delivery': po.expected_delivery, + 'Status': po.status, + 'Total Amount': po.total_amount + }) + + # Create DataFrame and export + df = pd.DataFrame(data) + + # Ensure directory exists + os.makedirs(os.path.dirname(filename) if os.path.dirname(filename) else '.', exist_ok=True) + + df.to_excel(filename, index=False, engine='openpyxl') + return filename + + def export_purchase_report(self, start_date: date = None, end_date: date = None) -> str: + """Export purchase report to Excel with optional date range""" + return self.export_to_excel(start_date=start_date, end_date=end_date) + + +class ManufacturingOrderService: + """Service for managing manufacturing orders""" + + def __init__(self, db_manager): + self.dao = ManufacturingOrderDAO(db_manager) + self.product_service = ProductService(db_manager) + self.inventory_service = InventoryService(db_manager) + + def create_manufacturing_order(self, product_id: int, quantity: int, start_date=None, + end_date=None, status: str = "pending") -> Optional[ManufacturingOrder]: + """Create a new manufacturing order""" + # Verify product exists + if not self.product_service.get_product(product_id): + return None + + mo = ManufacturingOrder(product_id=product_id, quantity=quantity, + start_date=start_date, end_date=end_date, status=status) + if self.dao.create(mo): + return mo + return None + + def get_manufacturing_order(self, mo_id: int) -> Optional[ManufacturingOrder]: + """Get manufacturing order by ID""" + return self.dao.get_by_id(mo_id) + + def get_all_manufacturing_orders(self) -> List[ManufacturingOrder]: + """Get all manufacturing orders""" + return self.dao.get_all() + + def get_active_manufacturing_orders(self) -> List[ManufacturingOrder]: + """Get all active manufacturing orders""" + all_orders = self.get_all_manufacturing_orders() + return [order for order in all_orders if order.status == "pending"] + + def get_recent_manufacturing_orders(self, limit: int = 5) -> List[ManufacturingOrder]: + """Get recent manufacturing orders""" + all_orders = self.get_all_manufacturing_orders() + # Sort by created_at in descending order and return the limit + sorted_orders = sorted(all_orders, key=lambda x: x.created_at, reverse=True) + return sorted_orders[:limit] + + def update_manufacturing_order(self, mo: ManufacturingOrder) -> bool: + """Update a manufacturing order""" + return self.dao.update(mo) + + def delete_manufacturing_order(self, mo_id: int) -> bool: + """Delete a manufacturing order""" + return self.dao.delete(mo_id) + + def complete_manufacturing_order(self, mo_id: int) -> bool: + """Complete a manufacturing order and update inventory""" + mo = self.get_manufacturing_order(mo_id) + if not mo or mo.status != "pending": + return False + + # Update inventory + if self.inventory_service.adjust_inventory(mo.product_id, mo.quantity, "MO", mo_id): + # Update order status + mo.status = "completed" + return self.update_manufacturing_order(mo) + return False + + def export_to_excel(self, filename: str = None, start_date: date = None, end_date: date = None) -> str: + """Export manufacturing orders to Excel with optional date range filtering""" + if filename is None: + filename = f"manufacturing_orders_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" + + manufacturing_orders = self.get_all_manufacturing_orders() + + # Filter by date range if provided + if start_date and end_date: + manufacturing_orders = [mo for mo in manufacturing_orders if start_date <= mo.start_date <= end_date] + elif start_date: + manufacturing_orders = [mo for mo in manufacturing_orders if mo.start_date >= start_date] + elif end_date: + manufacturing_orders = [mo for mo in manufacturing_orders if mo.start_date <= end_date] + + # Prepare data for DataFrame + data = [] + for mo in manufacturing_orders: + product = self.product_service.get_product(mo.product_id) + product_name = product.name if product else "Unknown" + + data.append({ + 'Order ID': mo.id, + 'Product': product_name, + 'Quantity': mo.quantity, + 'Start Date': mo.start_date, + 'End Date': mo.end_date, + 'Status': mo.status + }) + + # Create DataFrame and export + df = pd.DataFrame(data) + + # Ensure directory exists + os.makedirs(os.path.dirname(filename) if os.path.dirname(filename) else '.', exist_ok=True) + + df.to_excel(filename, index=False, engine='openpyxl') + return filename + + def export_manufacturing_report(self, start_date: date = None, end_date: date = None) -> str: + """Export manufacturing report to Excel with optional date range""" + return self.export_to_excel(start_date=start_date, end_date=end_date) + + def get_total_costs(self) -> float: + """Get total costs from all completed manufacturing orders""" + total_cost = 0.0 + manufacturing_orders = self.get_all_manufacturing_orders() + for mo in manufacturing_orders: + if mo.status == "completed": + # Get the product to calculate cost + product = self.product_service.get_product(mo.product_id) + if product: + total_cost += product.unit_price * mo.quantity + return total_cost + + +class SalesOrderService: + """Service for managing sales orders""" + + def __init__(self, db_manager): + self.dao = SalesOrderDAO(db_manager) + self.item_dao = SalesOrderItemDAO(db_manager) + self.product_service = ProductService(db_manager) + self.customer_service = CustomerService(db_manager) + self.inventory_service = InventoryService(db_manager) + + def create_sales_order(self, customer_id: int, order_date=None, expected_delivery=None, + status: str = "pending", total_amount: float = 0.0) -> Optional[SalesOrder]: + """Create a new sales order""" + # Verify customer exists + if not self.customer_service.get_customer(customer_id): + return None + + so = SalesOrder(customer_id=customer_id, order_date=order_date, + expected_delivery=expected_delivery, status=status, + total_amount=total_amount) + if self.dao.create(so): + return so + return None + + def add_sales_order_item(self, so_id: int, product_id: int, quantity: int, + unit_price: float, total_price: float) -> bool: + """Add an item to a sales order""" + # Verify sales order exists + so = self.get_sales_order(so_id) + if not so: + return False + + # Verify product exists + if not self.product_service.get_product(product_id): + return False + + # Create item + item = SalesOrderItem( + sales_order_id=so_id, + product_id=product_id, + quantity=quantity, + unit_price=unit_price, + total_price=total_price + ) + + return self.item_dao.create(item) + + def get_sales_order(self, so_id: int) -> Optional[SalesOrder]: + """Get sales order by ID""" + return self.dao.get_by_id(so_id) + + def get_all_sales_orders(self) -> List[SalesOrder]: + """Get all sales orders""" + return self.dao.get_all() + + def get_pending_sales_orders(self) -> List[SalesOrder]: + """Get all pending sales orders""" + all_orders = self.get_all_sales_orders() + return [order for order in all_orders if order.status == "pending"] + + def get_recent_sales_orders(self, limit: int = 5) -> List[SalesOrder]: + """Get recent sales orders""" + all_orders = self.get_all_sales_orders() + # Sort by created_at in descending order and return the limit + sorted_orders = sorted(all_orders, key=lambda x: x.created_at, reverse=True) + return sorted_orders[:limit] + + def update_sales_order(self, so: SalesOrder) -> bool: + """Update a sales order""" + return self.dao.update(so) + + def delete_sales_order(self, so_id: int) -> bool: + """Delete a sales order""" + return self.dao.delete(so_id) + + def deliver_sales_order(self, so_id: int) -> bool: + """Deliver products from a sales order and update inventory""" + so = self.get_sales_order(so_id) + if not so or so.status != "pending": + return False + + # Get all items for this sales order + items = self.item_dao.get_by_sales_order_id(so_id) + + # Update inventory for each item (decrease quantity) + for item in items: + if not self.inventory_service.adjust_inventory(item.product_id, -item.quantity, "SO", so_id): + return False + + # Update order status + so.status = "completed" + if self.update_sales_order(so): + return True + return False + + def get_sales_order_revenue(self, so_id: int) -> float: + """Get total revenue for a sales order""" + so = self.get_sales_order(so_id) + if not so: + return 0.0 + return so.total_amount + + def get_all_revenue(self) -> float: + """Get total revenue from all completed sales orders""" + total_revenue = 0.0 + sales_orders = self.get_all_sales_orders() + for so in sales_orders: + if so.status == "completed": + total_revenue += so.total_amount + return total_revenue + + def get_total_revenue(self) -> float: + """Get total revenue from all completed sales orders""" + return self.get_all_revenue() + + def export_to_excel(self, filename: str = None, start_date: date = None, end_date: date = None) -> str: + """Export sales orders to Excel with optional date range filtering""" + if filename is None: + filename = f"sales_orders_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" + + sales_orders = self.get_all_sales_orders() + + # Filter by date range if provided + if start_date and end_date: + sales_orders = [so for so in sales_orders if start_date <= so.order_date <= end_date] + elif start_date: + sales_orders = [so for so in sales_orders if so.order_date >= start_date] + elif end_date: + sales_orders = [so for so in sales_orders if so.order_date <= end_date] + + # Prepare data for DataFrame + data = [] + for so in sales_orders: + customer = self.customer_service.get_customer(so.customer_id) + customer_name = customer.name if customer else "Unknown" + + data.append({ + 'Order ID': so.id, + 'Customer': customer_name, + 'Order Date': so.order_date, + 'Expected Delivery': so.expected_delivery, + 'Status': so.status, + 'Total Amount': so.total_amount + }) + + # Create DataFrame and export + df = pd.DataFrame(data) + + # Ensure directory exists + os.makedirs(os.path.dirname(filename) if os.path.dirname(filename) else '.', exist_ok=True) + + df.to_excel(filename, index=False, engine='openpyxl') + return filename + + def export_sales_report(self, start_date: date = None, end_date: date = None) -> str: + """Export sales report to Excel with optional date range""" + return self.export_to_excel(start_date=start_date, end_date=end_date) + + +class InventoryService: + """Service for managing inventory""" + + def __init__(self, db_manager): + self.dao = InventoryDAO(db_manager) + self.product_service = ProductService(db_manager) + self.movement_dao = StockMovementDAO(db_manager) + + def create_inventory_record(self, product_id: int, quantity: int = 0, + reserved_quantity: int = 0) -> Optional[Inventory]: + """Create a new inventory record""" + # Verify product exists + if not self.product_service.get_product(product_id): + return None + + inventory = Inventory(product_id=product_id, quantity=quantity, + reserved_quantity=reserved_quantity) + if self.dao.create(inventory): + return inventory + return None + + def get_inventory_by_product(self, product_id: int) -> Optional[Inventory]: + """Get inventory by product ID""" + return self.dao.get_by_product_id(product_id) + + def get_all_inventory(self) -> List[Inventory]: + """Get all inventory records""" + return self.dao.get_all() + + def update_inventory(self, inventory: Inventory) -> bool: + """Update an inventory record""" + return self.dao.update(inventory) + + def delete_inventory(self, product_id: int) -> bool: + """Delete an inventory record""" + return self.dao.delete(product_id) + + def adjust_inventory(self, product_id: int, quantity_change: int, + reference_type: str = "ADJUSTMENT", reference_id: int = None) -> bool: + """Adjust inventory quantity and track movement""" + # Adjust inventory using DAO + if not self.dao.adjust_quantity(product_id, quantity_change): + return False + + # Track stock movement + if quantity_change != 0: + movement_type = "IN" if quantity_change > 0 else "OUT" + movement = StockMovement( + product_id=product_id, + movement_type=movement_type, + quantity=abs(quantity_change), + reference_type=reference_type, + reference_id=reference_id + ) + self.movement_dao.create(movement) + + return True + + def get_low_stock_products(self, threshold: int = 10) -> List[dict]: + """Get products with low stock""" + inventory_records = self.get_all_inventory() + low_stock = [] + + for record in inventory_records: + if record.quantity <= threshold: + product = self.product_service.get_product(record.product_id) + if product: + low_stock.append({ + 'product': product, + 'current_stock': record.quantity + }) + return low_stock + + def get_low_stock_items(self) -> List[dict]: + """Get low stock items""" + return self.get_low_stock_products() + + def get_current_stock(self, product_id: int) -> int: + """Get current stock for a product""" + inventory = self.get_inventory_by_product(product_id) + return inventory.quantity if inventory else 0 + + def get_stock_movements(self, product_id: int = None) -> List[StockMovement]: + """Get stock movements for a product or all products""" + if product_id: + return self.movement_dao.get_by_product_id(product_id) + else: + return self.movement_dao.get_all() + + def export_to_excel(self, filename: str = None) -> str: + """Export all inventory records to Excel""" + if filename is None: + filename = f"inventory_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" + + inventory_records = self.get_all_inventory() + + # Prepare data for DataFrame + data = [] + for record in inventory_records: + product = self.product_service.get_product(record.product_id) + product_name = product.name if product else "Unknown" + + data.append({ + 'Product ID': record.product_id, + 'Product Name': product_name, + 'Current Stock': record.quantity, + 'Reserved Quantity': record.reserved_quantity, + 'Available Stock': record.quantity - record.reserved_quantity + }) + + # Create DataFrame and export + df = pd.DataFrame(data) + + # Ensure directory exists + os.makedirs(os.path.dirname(filename) if os.path.dirname(filename) else '.', exist_ok=True) + + df.to_excel(filename, index=False, engine='openpyxl') + return filename + + def export_stock_movement_report(self) -> str: + """Export stock movement report to Excel""" + return self.export_stock_movements_to_excel() + + def export_inventory_report(self) -> str: + """Export inventory report to Excel""" + return self.export_to_excel() + + def export_stock_movements_to_excel(self, filename: str = None) -> str: + """Export all stock movements to Excel""" + if filename is None: + filename = f"stock_movements_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" + + movements = self.get_stock_movements() + + # Prepare data for DataFrame + data = [] + for movement in movements: + product = self.product_service.get_product(movement.product_id) + product_name = product.name if product else "Unknown" + + data.append({ + 'Movement ID': movement.id, + 'Product': product_name, + 'Movement Type': movement.movement_type, + 'Quantity': movement.quantity, + 'Date': movement.created_at, + 'Reference Type': movement.reference_type, + 'Reference ID': movement.reference_id + }) + + # Create DataFrame and export + df = pd.DataFrame(data) + + # Ensure directory exists + os.makedirs(os.path.dirname(filename) if os.path.dirname(filename) else '.', exist_ok=True) + + df.to_excel(filename, index=False, engine='openpyxl') + return filename \ No newline at end of file diff --git a/src/ui/__init__.py b/src/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/__pycache__/__init__.cpython-312.pyc b/src/ui/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..c817a4b Binary files /dev/null and b/src/ui/__pycache__/__init__.cpython-312.pyc differ diff --git a/src/ui/__pycache__/customer_dialog.cpython-312.pyc b/src/ui/__pycache__/customer_dialog.cpython-312.pyc new file mode 100644 index 0000000..787393a Binary files /dev/null and b/src/ui/__pycache__/customer_dialog.cpython-312.pyc differ diff --git a/src/ui/__pycache__/date_picker_dialog.cpython-312.pyc b/src/ui/__pycache__/date_picker_dialog.cpython-312.pyc new file mode 100644 index 0000000..e2c7739 Binary files /dev/null and b/src/ui/__pycache__/date_picker_dialog.cpython-312.pyc differ diff --git a/src/ui/__pycache__/date_range_dialog.cpython-312.pyc b/src/ui/__pycache__/date_range_dialog.cpython-312.pyc new file mode 100644 index 0000000..fd1e687 Binary files /dev/null and b/src/ui/__pycache__/date_range_dialog.cpython-312.pyc differ diff --git a/src/ui/__pycache__/main_window.cpython-312.pyc b/src/ui/__pycache__/main_window.cpython-312.pyc new file mode 100644 index 0000000..db42750 Binary files /dev/null and b/src/ui/__pycache__/main_window.cpython-312.pyc differ diff --git a/src/ui/__pycache__/manufacturing_order_dialog.cpython-312.pyc b/src/ui/__pycache__/manufacturing_order_dialog.cpython-312.pyc new file mode 100644 index 0000000..db98327 Binary files /dev/null and b/src/ui/__pycache__/manufacturing_order_dialog.cpython-312.pyc differ diff --git a/src/ui/__pycache__/product_dialog.cpython-312.pyc b/src/ui/__pycache__/product_dialog.cpython-312.pyc new file mode 100644 index 0000000..206126f Binary files /dev/null and b/src/ui/__pycache__/product_dialog.cpython-312.pyc differ diff --git a/src/ui/__pycache__/purchase_order_dialog.cpython-312.pyc b/src/ui/__pycache__/purchase_order_dialog.cpython-312.pyc new file mode 100644 index 0000000..ecdbc2b Binary files /dev/null and b/src/ui/__pycache__/purchase_order_dialog.cpython-312.pyc differ diff --git a/src/ui/__pycache__/sales_order_dialog.cpython-312.pyc b/src/ui/__pycache__/sales_order_dialog.cpython-312.pyc new file mode 100644 index 0000000..d4703c2 Binary files /dev/null and b/src/ui/__pycache__/sales_order_dialog.cpython-312.pyc differ diff --git a/src/ui/__pycache__/supplier_dialog.cpython-312.pyc b/src/ui/__pycache__/supplier_dialog.cpython-312.pyc new file mode 100644 index 0000000..1613693 Binary files /dev/null and b/src/ui/__pycache__/supplier_dialog.cpython-312.pyc differ diff --git a/src/ui/__pycache__/user_management_dialog.cpython-312.pyc b/src/ui/__pycache__/user_management_dialog.cpython-312.pyc new file mode 100644 index 0000000..f464910 Binary files /dev/null and b/src/ui/__pycache__/user_management_dialog.cpython-312.pyc differ diff --git a/src/ui/customer_dialog.py b/src/ui/customer_dialog.py new file mode 100644 index 0000000..3982d49 --- /dev/null +++ b/src/ui/customer_dialog.py @@ -0,0 +1,108 @@ +import customtkinter as ctk +from tkinter import messagebox +from typing import Optional +from src.models import Customer +from src.services import CustomerService + +class CustomerDialog(ctk.CTkToplevel): + def __init__(self, parent, customer_service: CustomerService, + customer: Optional[Customer] = None): + super().__init__(parent) + self.customer_service = customer_service + self.customer = customer + + self.title("New Customer" if not customer else "Edit Customer") + self.geometry("400x350") + + # Make dialog modal + self.transient(parent) + self.grab_set() + + self.setup_ui() + + def setup_ui(self): + # Name + ctk.CTkLabel(self, text="Name:").grid(row=0, column=0, padx=10, pady=5, sticky="w") + self.name_entry = ctk.CTkEntry(self, width=250) + self.name_entry.grid(row=0, column=1, padx=10, pady=5) + + # Email + ctk.CTkLabel(self, text="Email:").grid(row=1, column=0, padx=10, pady=5, sticky="w") + self.email_entry = ctk.CTkEntry(self, width=250) + self.email_entry.grid(row=1, column=1, padx=10, pady=5) + + # Phone + ctk.CTkLabel(self, text="Phone:").grid(row=2, column=0, padx=10, pady=5, sticky="w") + self.phone_entry = ctk.CTkEntry(self, width=250) + self.phone_entry.grid(row=2, column=1, padx=10, pady=5) + + # Contact Person + ctk.CTkLabel(self, text="Contact Person:").grid(row=3, column=0, padx=10, pady=5, sticky="w") + self.contact_person_entry = ctk.CTkEntry(self, width=250) + self.contact_person_entry.grid(row=3, column=1, padx=10, pady=5) + + # Address + ctk.CTkLabel(self, text="Address:").grid(row=4, column=0, padx=10, pady=5, sticky="nw") + self.address_text = ctk.CTkTextbox(self, height=80, width=250) + self.address_text.grid(row=4, column=1, padx=10, pady=5) + + # Buttons + button_frame = ctk.CTkFrame(self) + button_frame.grid(row=5, column=0, columnspan=2, pady=20) + + ctk.CTkButton(button_frame, text="Save", command=self.save_customer).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="Cancel", command=self.destroy).pack(side="left", padx=5) + + # Load data if editing + if self.customer: + self.load_customer_data() + + def load_customer_data(self): + self.name_entry.insert(0, self.customer.name) + self.email_entry.insert(0, self.customer.email or "") + self.phone_entry.insert(0, self.customer.phone or "") + self.contact_person_entry.insert(0, self.customer.contact_person or "") + self.address_text.insert("1.0", self.customer.address or "") + + def save_customer(self): + name = self.name_entry.get().strip() + if not name: + messagebox.showerror("Error", "Please enter customer name") + return + + email = self.email_entry.get().strip() + phone = self.phone_entry.get().strip() + contact_person = self.contact_person_entry.get().strip() + address = self.address_text.get("1.0", "end-1c").strip() + + if self.customer: + # Update existing customer + updated = self.customer_service.update_customer( + customer_id=self.customer.id, + name=name, + contact_person=contact_person or None, + email=email or None, + phone=phone or None, + address=address or None + ) + + if updated: + messagebox.showinfo("Success", "Customer updated successfully") + self.destroy() + else: + messagebox.showerror("Error", "Failed to update customer") + else: + # Create new customer + customer = self.customer_service.create_customer( + name=name, + contact_person=contact_person or None, + email=email or None, + phone=phone or None, + address=address or None + ) + + if customer: + messagebox.showinfo("Success", f"Customer '{customer.name}' created successfully") + self.destroy() + else: + messagebox.showerror("Error", "Failed to create customer") \ No newline at end of file diff --git a/src/ui/date_picker_dialog.py b/src/ui/date_picker_dialog.py new file mode 100644 index 0000000..2d42a64 --- /dev/null +++ b/src/ui/date_picker_dialog.py @@ -0,0 +1,148 @@ +import customtkinter as ctk +from tkinter import messagebox +from datetime import datetime, date +from typing import Optional + +class DatePickerDialog(ctk.CTkToplevel): + def __init__(self, parent, title="Select Date", initial_date: Optional[date] = None): + super().__init__(parent) + self.title(title) + self.geometry("300x200") + + # Make dialog modal + self.transient(parent) + self.grab_set() + + # Initialize variables + self.selected_date = None + + # Set initial date or use today + if initial_date: + self.current_date = initial_date + else: + self.current_date = date.today() + + self.setup_ui() + + def setup_ui(self): + # Header with navigation + header_frame = ctk.CTkFrame(self) + header_frame.pack(fill="x", padx=10, pady=10) + + # Previous month button + ctk.CTkButton(header_frame, text="<", width=30, height=30, + command=self.prev_month).pack(side="left", padx=5) + + # Month/Year label + self.month_year_label = ctk.CTkLabel(header_frame, text="", font=("Arial", 14, "bold")) + self.month_year_label.pack(side="left", expand=True) + + # Next month button + ctk.CTkButton(header_frame, text=">", width=30, height=30, + command=self.next_month).pack(side="right", padx=5) + + # Calendar frame + self.calendar_frame = ctk.CTkFrame(self) + self.calendar_frame.pack(fill="both", expand=True, padx=10, pady=5) + + # Buttons frame + button_frame = ctk.CTkFrame(self) + button_frame.pack(fill="x", padx=10, pady=10) + + ctk.CTkButton(button_frame, text="Today", command=self.select_today).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="OK", command=self.ok).pack(side="right", padx=5) + ctk.CTkButton(button_frame, text="Cancel", command=self.cancel).pack(side="right", padx=5) + + # Display initial calendar + self.display_calendar() + + def display_calendar(self): + # Clear existing calendar + for widget in self.calendar_frame.winfo_children(): + widget.destroy() + + # Update month/year label + self.month_year_label.configure(text=f"{self.current_date.strftime('%B %Y')}") + + # Create calendar grid + # Weekday headers + weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] + for i, day in enumerate(weekdays): + ctk.CTkLabel(self.calendar_frame, text=day, width=30, height=25).grid( + row=0, column=i, padx=2, pady=2) + + # Get first day of month and number of days + first_day = self.current_date.replace(day=1) + first_weekday = first_day.weekday() # 0=Monday, 6=Sunday + # Adjust for Sunday as first day (0=Sunday, 6=Saturday) + first_weekday = (first_weekday + 1) % 7 + + # Get number of days in month + if self.current_date.month == 12: + next_month = self.current_date.replace(year=self.current_date.year + 1, month=1, day=1) + else: + next_month = self.current_date.replace(month=self.current_date.month + 1, day=1) + days_in_month = (next_month - first_day).days + + # Display days + day = 1 + for week in range(6): + for weekday in range(7): + row = week + 1 + if (week == 0 and weekday < first_weekday) or day > days_in_month: + # Empty cell + ctk.CTkLabel(self.calendar_frame, text="", width=30, height=25).grid( + row=row, column=weekday, padx=2, pady=2) + else: + # Day button + day_date = self.current_date.replace(day=day) + is_selected = (self.selected_date and day_date == self.selected_date) + is_today = (day_date == date.today()) + + btn = ctk.CTkButton( + self.calendar_frame, + text=str(day), + width=30, + height=25, + fg_color="green" if is_selected else ("#2CC985" if is_today else "transparent"), + text_color="white" if is_selected or is_today else "black", + hover_color="#2CC985", + command=lambda d=day_date: self.select_date(d) + ) + btn.grid(row=row, column=weekday, padx=2, pady=2) + day += 1 + + def select_date(self, date_obj): + self.selected_date = date_obj + self.display_calendar() + + def select_today(self): + self.selected_date = date.today() + self.current_date = self.selected_date + self.display_calendar() + + def prev_month(self): + if self.current_date.month == 1: + self.current_date = self.current_date.replace(year=self.current_date.year - 1, month=12) + else: + self.current_date = self.current_date.replace(month=self.current_date.month - 1) + self.display_calendar() + + def next_month(self): + if self.current_date.month == 12: + self.current_date = self.current_date.replace(year=self.current_date.year + 1, month=1) + else: + self.current_date = self.current_date.replace(month=self.current_date.month + 1) + self.display_calendar() + + def ok(self): + if not self.selected_date: + self.selected_date = self.current_date + self.destroy() + + def cancel(self): + self.selected_date = None + self.destroy() + + def get_selected_date(self): + return self.selected_date \ No newline at end of file diff --git a/src/ui/date_range_dialog.py b/src/ui/date_range_dialog.py new file mode 100644 index 0000000..5ea426c --- /dev/null +++ b/src/ui/date_range_dialog.py @@ -0,0 +1,145 @@ +import customtkinter as ctk +from tkinter import messagebox +from datetime import datetime, date +from typing import Optional, Tuple + +class DateRangeDialog(ctk.CTkToplevel): + def __init__(self, parent, title="Select Date Range", ok_button_text="OK"): + super().__init__(parent) + self.title(title) + self.geometry("350x250") + + # Make dialog modal + self.transient(parent) + self.grab_set() + + # Initialize variables + self.start_date = date.today().replace(day=1) # First day of current month + self.end_date = date.today() + self.ok_button_text = ok_button_text + + self.setup_ui() + + def setup_ui(self): + # Title + ctk.CTkLabel(self, text="Select Date Range", font=("Arial", 16, "bold")).pack(pady=10) + + # Start date + start_frame = ctk.CTkFrame(self) + start_frame.pack(fill="x", padx=20, pady=10) + + ctk.CTkLabel(start_frame, text="Start Date:").pack(anchor="w") + date_frame1 = ctk.CTkFrame(start_frame) + date_frame1.pack(fill="x", pady=5) + self.start_date_entry = ctk.CTkEntry(date_frame1, width=150) + self.start_date_entry.insert(0, self.start_date.strftime("%Y-%m-%d")) + self.start_date_entry.pack(side="left") + ctk.CTkButton(date_frame1, text="...", width=30, command=self.select_start_date).pack(side="left", padx=(5, 0)) + + # End date + end_frame = ctk.CTkFrame(self) + end_frame.pack(fill="x", padx=20, pady=10) + + ctk.CTkLabel(end_frame, text="End Date:").pack(anchor="w") + date_frame2 = ctk.CTkFrame(end_frame) + date_frame2.pack(fill="x", pady=5) + self.end_date_entry = ctk.CTkEntry(date_frame2, width=150) + self.end_date_entry.insert(0, self.end_date.strftime("%Y-%m-%d")) + self.end_date_entry.pack(side="left") + ctk.CTkButton(date_frame2, text="...", width=30, command=self.select_end_date).pack(side="left", padx=(5, 0)) + + # Buttons + button_frame = ctk.CTkFrame(self) + button_frame.pack(fill="x", padx=20, pady=20) + + ctk.CTkButton(button_frame, text="This Month", command=self.set_this_month).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="Last Month", command=self.set_last_month).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text=self.ok_button_text, command=self.ok).pack(side="right", padx=5) + ctk.CTkButton(button_frame, text="Cancel", command=self.cancel).pack(side="right", padx=5) + + def select_start_date(self): + """Open date picker for start date""" + try: + current_date = datetime.strptime(self.start_date_entry.get(), "%Y-%m-%d").date() + except ValueError: + current_date = date.today() + + from src.ui.date_picker_dialog import DatePickerDialog + dialog = DatePickerDialog(self, "Select Start Date", current_date) + self.wait_window(dialog) + + selected_date = dialog.get_selected_date() + if selected_date: + self.start_date_entry.delete(0, 'end') + self.start_date_entry.insert(0, selected_date.strftime("%Y-%m-%d")) + + def select_end_date(self): + """Open date picker for end date""" + try: + current_date = datetime.strptime(self.end_date_entry.get(), "%Y-%m-%d").date() + except ValueError: + current_date = date.today() + + from src.ui.date_picker_dialog import DatePickerDialog + dialog = DatePickerDialog(self, "Select End Date", current_date) + self.wait_window(dialog) + + selected_date = dialog.get_selected_date() + if selected_date: + self.end_date_entry.delete(0, 'end') + self.end_date_entry.insert(0, selected_date.strftime("%Y-%m-%d")) + + def set_this_month(self): + """Set date range to this month""" + today = date.today() + self.start_date = today.replace(day=1) + self.end_date = today + + self.start_date_entry.delete(0, 'end') + self.start_date_entry.insert(0, self.start_date.strftime("%Y-%m-%d")) + self.end_date_entry.delete(0, 'end') + self.end_date_entry.insert(0, self.end_date.strftime("%Y-%m-%d")) + + def set_last_month(self): + """Set date range to last month""" + today = date.today() + if today.month == 1: + self.start_date = today.replace(year=today.year - 1, month=12, day=1) + self.end_date = today.replace(year=today.year - 1, month=12, day=31) + else: + self.start_date = today.replace(month=today.month - 1, day=1) + # Get last day of last month + if today.month == 1: + self.end_date = today.replace(year=today.year - 1, month=12, day=31) + else: + first_day_this_month = today.replace(day=1) + self.end_date = first_day_this_month - date.resolution + + self.start_date_entry.delete(0, 'end') + self.start_date_entry.insert(0, self.start_date.strftime("%Y-%m-%d")) + self.end_date_entry.delete(0, 'end') + self.end_date_entry.insert(0, self.end_date.strftime("%Y-%m-%d")) + + def ok(self): + """Validate and close dialog""" + try: + self.start_date = datetime.strptime(self.start_date_entry.get(), "%Y-%m-%d").date() + self.end_date = datetime.strptime(self.end_date_entry.get(), "%Y-%m-%d").date() + + if self.start_date > self.end_date: + messagebox.showerror("Error", "Start date must be before end date") + return + + self.destroy() + except ValueError: + messagebox.showerror("Error", "Please enter valid dates in YYYY-MM-DD format") + + def cancel(self): + """Cancel and close dialog""" + self.start_date = None + self.end_date = None + self.destroy() + + def get_date_range(self) -> Tuple[Optional[date], Optional[date]]: + """Return the selected date range""" + return self.start_date, self.end_date \ No newline at end of file diff --git a/src/ui/main_window.py b/src/ui/main_window.py new file mode 100644 index 0000000..a27ab0c --- /dev/null +++ b/src/ui/main_window.py @@ -0,0 +1,1218 @@ +import customtkinter as ctk +import tkinter as tk +from tkinter import messagebox +import sys +import os + +# Add the project root to the path +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + +from src.auth import AuthManager +from src.services import ManufacturingOrderService, PurchaseOrderService, SalesOrderService, InventoryService +from src.database import DatabaseManager +from src.services import ProductService, CustomerService, SupplierService +from src.ui.product_dialog import ProductDialog +from src.ui.purchase_order_dialog import PurchaseOrderDialog +from src.ui.manufacturing_order_dialog import ManufacturingOrderDialog +from src.ui.sales_order_dialog import SalesOrderDialog +from src.ui.customer_dialog import CustomerDialog +from src.ui.supplier_dialog import SupplierDialog + +class MainWindow: + def __init__(self, root, user, db_manager, app=None): + self.root = root + self.user = user + self.db_manager = db_manager + self.app = app + self.auth_manager = AuthManager(db_manager) + + # Configure the main window + self.root.title("Manufacturing Management System") + self.root.geometry("1200x800") + ctk.set_appearance_mode("light") # Options: "light", "dark", "system" + ctk.set_default_color_theme("blue") # Options: "blue", "green", "dark-blue" + + # Initialize services + self.manufacturing_service = ManufacturingOrderService(self.db_manager) + self.purchase_service = PurchaseOrderService(self.db_manager) + self.sales_service = SalesOrderService(self.db_manager) + self.inventory_service = InventoryService(self.db_manager) + self.product_service = ProductService(self.db_manager) + self.customer_service = CustomerService(self.db_manager) + self.supplier_service = SupplierService(self.db_manager) + + # Create the main interface + self.create_menu() + self.create_toolbar() + self.create_content_area() + self.create_status_bar() + + # Show dashboard by default + self.root.after(100, self.show_dashboard) + + def create_menu(self): + """Create the main menu using tkinter's native menu system""" + menubar = tk.Menu(self.root, bg="#f0f0f0", fg="black") + self.root.config(menu=menubar) + + # File menu + file_menu = tk.Menu(menubar, tearoff=0, bg="#f0f0f0", fg="black") + menubar.add_cascade(label="File", menu=file_menu) + file_menu.add_command(label="Logout", command=self.logout) + file_menu.add_separator() + file_menu.add_command(label="Exit", command=self.root.quit) + + # Modules menu + modules_menu = tk.Menu(menubar, tearoff=0, bg="#f0f0f0", fg="black") + menubar.add_cascade(label="Modules", menu=modules_menu) + modules_menu.add_command(label="Dashboard", command=self.show_dashboard) + modules_menu.add_separator() + modules_menu.add_command(label="Purchase", command=self.show_purchase) + modules_menu.add_command(label="Manufacture", command=self.show_manufacturing) + modules_menu.add_command(label="Sales", command=self.show_sales) + modules_menu.add_command(label="Inventory", command=self.show_inventory) + modules_menu.add_separator() + modules_menu.add_command(label="Suppliers", command=self.show_supplier_list) + modules_menu.add_command(label="Customers", command=self.show_customer_list) + + # Configuration menu (admin only) + if self.user['is_admin']: + config_menu = tk.Menu(menubar, tearoff=0, bg="#f0f0f0", fg="black") + menubar.add_cascade(label="Configuration", menu=config_menu) + config_menu.add_command(label="User Management", command=self.manage_users) + + # Reports menu + has_report_permissions = ( + self.auth_manager.user_has_permission(self.user['id'], 'view_reports') or + self.auth_manager.user_has_permission(self.user['id'], 'view_inventory_report') or + self.auth_manager.user_has_permission(self.user['id'], 'view_stock_movements') or + self.user['is_admin'] + ) + + if has_report_permissions: + reports_menu = tk.Menu(menubar, tearoff=0, bg="#f0f0f0", fg="black") + menubar.add_cascade(label="Reports", menu=reports_menu) + if self.auth_manager.user_has_permission(self.user['id'], 'view_inventory_report') or self.user['is_admin']: + reports_menu.add_command(label="Inventory Report", command=self.show_inventory_report) + if self.auth_manager.user_has_permission(self.user['id'], 'view_reports') or self.user['is_admin']: + reports_menu.add_command(label="Sales Report", command=self.show_sales_report) + reports_menu.add_command(label="Purchase Report", command=self.show_purchase_report) + reports_menu.add_command(label="Manufacturing Report", command=self.show_manufacturing_report) + if self.auth_manager.user_has_permission(self.user['id'], 'view_stock_movements') or self.user['is_admin']: + reports_menu.add_command(label="Stock Movement Report", command=self.show_stock_movement_report) + + def create_toolbar(self): + """Create the toolbar""" + toolbar = ctk.CTkFrame(self.root, height=60, fg_color="#e0e0e0") + toolbar.pack(fill="x", padx=5, pady=5) + + # Navigation buttons + button_frame = ctk.CTkFrame(toolbar, fg_color="transparent") + button_frame.pack(side="left", padx=10, pady=10) + + if self.auth_manager.user_has_permission(self.user['id'], 'view_dashboard') or self.user['is_admin']: + ctk.CTkButton(button_frame, text="Dashboard", command=self.show_dashboard, + width=100, height=32).pack(side="left", padx=5) + + ctk.CTkButton(button_frame, text="Purchase", command=self.show_purchase, + width=100, height=32).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="Manufacture", command=self.show_manufacturing, + width=100, height=32).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="Sales", command=self.show_sales, + width=100, height=32).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="Inventory", command=self.show_inventory, + width=100, height=32).pack(side="left", padx=5) + + # User info + user_frame = ctk.CTkFrame(toolbar, fg_color="transparent") + user_frame.pack(side="right", padx=10, pady=10) + + user_label = ctk.CTkLabel(user_frame, text=f"User: {self.user['username']}", + font=("Arial", 12, "bold")) + user_label.pack(side="right", padx=10) + + def create_content_area(self): + """Create the main content area""" + self.content_frame = ctk.CTkFrame(self.root) + self.content_frame.pack(fill="both", expand=True, padx=10, pady=10) + + def create_status_bar(self): + """Create the status bar""" + self.status_bar = ctk.CTkLabel(self.root, text="Ready", anchor="w", height=30) + self.status_bar.pack(fill="x", side="bottom", padx=10, pady=(0, 10)) + + def clear_content(self): + """Clear the content frame""" + for widget in self.content_frame.winfo_children(): + widget.destroy() + + def show_dashboard(self): + """Show the dashboard""" + self.clear_content() + self.status_bar.configure(text="Dashboard") + + # Title + title = ctk.CTkLabel(self.content_frame, text="Dashboard", + font=("Arial", 24, "bold")) + title.pack(pady=(20, 30)) + + # Main dashboard frame + dashboard_frame = ctk.CTkFrame(self.content_frame) + dashboard_frame.pack(fill="both", expand=True, padx=20, pady=20) + + # Welcome message + welcome_text = f"Welcome, {self.user.get('full_name', self.user.get('username', 'User'))}!" + welcome_label = ctk.CTkLabel(dashboard_frame, text=welcome_text, + font=("Arial", 16)) + welcome_label.pack(pady=(20, 10)) + + # Quick stats frame + stats_frame = ctk.CTkFrame(dashboard_frame) + stats_frame.pack(fill="x", padx=20, pady=10) + + # Get real data for stats + pending_pos = len(self.purchase_service.get_pending_purchase_orders()) + active_mos = len(self.manufacturing_service.get_active_manufacturing_orders()) + pending_sos = len(self.sales_service.get_pending_sales_orders()) + low_stock_items = len(self.inventory_service.get_low_stock_items()) + + # Create stats grid + stats_grid = ctk.CTkFrame(stats_frame) + stats_grid.pack(pady=20) + + stats = [ + ("Pending Purchase Orders", str(pending_pos)), + ("Active Manufacturing Orders", str(active_mos)), + ("Pending Sales Orders", str(pending_sos)), + ("Low Inventory Items", str(low_stock_items)) + ] + + for i, (label, value) in enumerate(stats): + frame = ctk.CTkFrame(stats_grid, fg_color="transparent") + frame.grid(row=0, column=i, padx=20, pady=10) + + ctk.CTkLabel(frame, text=label, font=("Arial", 12)).pack() + ctk.CTkLabel(frame, text=value, font=("Arial", 20, "bold")).pack() + + # Financial summary + financial_frame = ctk.CTkFrame(dashboard_frame) + financial_frame.pack(fill="x", padx=20, pady=10) + + # Get financial data + total_revenue = self.sales_service.get_total_revenue() + total_costs = self.purchase_service.get_total_costs() + self.manufacturing_service.get_total_costs() + profit = total_revenue - total_costs + + fin_grid = ctk.CTkFrame(financial_frame) + fin_grid.pack(pady=20) + + financial_stats = [ + ("Total Revenue", f"Rp{total_revenue:,.0f}".replace(",", "."), "green"), + ("Total Costs", f"Rp{total_costs:,.0f}".replace(",", "."), "red"), + ("Net Profit", f"Rp{profit:,.0f}".replace(",", "."), "green" if profit >= 0 else "red") + ] + + for i, (label, value, color) in enumerate(financial_stats): + frame = ctk.CTkFrame(fin_grid, fg_color="transparent") + frame.grid(row=0, column=i, padx=20, pady=10) + + ctk.CTkLabel(frame, text=label, font=("Arial", 12)).pack() + ctk.CTkLabel(frame, text=value, font=("Arial", 16, "bold"), + text_color=color).pack() + + # Recent activities + activity_frame = ctk.CTkFrame(dashboard_frame) + activity_frame.pack(fill="both", expand=True, padx=20, pady=10) + + ctk.CTkLabel(activity_frame, text="Recent Activities", + font=("Arial", 16, "bold")).pack(pady=(10, 5)) + + # Create scrollable frame for activities + activity_scroll = ctk.CTkScrollableFrame(activity_frame, height=200) + activity_scroll.pack(fill="both", expand=True, padx=10, pady=10) + + activities = self.get_recent_activities() + if activities: + for activity in activities: + ctk.CTkLabel(activity_scroll, text=activity, + font=("Arial", 11)).pack(anchor="w", pady=2) + else: + ctk.CTkLabel(activity_scroll, text="No recent activities", + font=("Arial", 11)).pack(pady=20) + + # Reports section + reports_frame = ctk.CTkFrame(dashboard_frame) + reports_frame.pack(fill="x", padx=20, pady=10) + + ctk.CTkLabel(reports_frame, text="Quick Reports", + font=("Arial", 16, "bold")).pack(pady=(10, 5)) + + reports_grid = ctk.CTkFrame(reports_frame) + reports_grid.pack(pady=10) + + report_buttons = [] + + if self.auth_manager.user_has_permission(self.user['id'], 'view_inventory_report') or self.user['is_admin']: + report_buttons.append(("Inventory Report", self.show_inventory_report)) + + if self.auth_manager.user_has_permission(self.user['id'], 'view_reports') or self.user['is_admin']: + report_buttons.extend([ + ("Sales Report", self.show_sales_report), + ("Purchase Report", self.show_purchase_report), + ("Manufacturing Report", self.show_manufacturing_report), + ("Stock Movement Report", self.show_stock_movement_report) + ]) + + for i, (text, command) in enumerate(report_buttons): + ctk.CTkButton(reports_grid, text=text, command=command, + width=150, height=32).grid(row=0, column=i, padx=5, pady=5) + + def show_purchase(self): + """Show purchase management""" + self.clear_content() + self.status_bar.configure(text="Purchase Management") + + # Title + title = ctk.CTkLabel(self.content_frame, text="Purchase Management", + font=("Arial", 24, "bold")) + title.pack(pady=(20, 30)) + + # Main frame + purchase_frame = ctk.CTkFrame(self.content_frame) + purchase_frame.pack(fill="both", expand=True, padx=20, pady=20) + + # Buttons frame + button_frame = ctk.CTkFrame(purchase_frame) + button_frame.pack(pady=20) + + ctk.CTkButton(button_frame, text="New Purchase Order", + command=self.new_purchase_order, width=150, height=32).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="View Purchase Orders", + command=self.view_purchase_orders, width=150, height=32).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="Manage Suppliers", + command=self.show_supplier_list, width=150, height=32).pack(side="left", padx=5) + + # Show purchase orders by default + self.show_purchase_orders() + + def show_manufacturing(self): + """Show manufacturing management""" + self.clear_content() + self.status_bar.configure(text="Manufacturing Management") + + # Title + title = ctk.CTkLabel(self.content_frame, text="Manufacturing Management", + font=("Arial", 24, "bold")) + title.pack(pady=(20, 30)) + + # Main frame + manufacturing_frame = ctk.CTkFrame(self.content_frame) + manufacturing_frame.pack(fill="both", expand=True, padx=20, pady=20) + + # Buttons frame + button_frame = ctk.CTkFrame(manufacturing_frame) + button_frame.pack(pady=20) + + ctk.CTkButton(button_frame, text="New Manufacturing Order", + command=self.new_manufacturing_order, width=180, height=32).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="View Manufacturing Orders", + command=self.view_manufacturing_orders, width=180, height=32).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="Production Planning", + command=self.production_planning, width=180, height=32).pack(side="left", padx=5) + + # Show manufacturing orders by default + self.show_manufacturing_orders() + + def show_sales(self): + """Show sales management""" + self.clear_content() + self.status_bar.configure(text="Sales Management") + + # Title + title = ctk.CTkLabel(self.content_frame, text="Sales Management", + font=("Arial", 24, "bold")) + title.pack(pady=(20, 30)) + + # Main frame + sales_frame = ctk.CTkFrame(self.content_frame) + sales_frame.pack(fill="both", expand=True, padx=20, pady=20) + + # Buttons frame + button_frame = ctk.CTkFrame(sales_frame) + button_frame.pack(pady=20) + + ctk.CTkButton(button_frame, text="New Sales Order", + command=self.new_sales_order, width=150, height=32).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="View Sales Orders", + command=self.view_sales_orders, width=150, height=32).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="Manage Customers", + command=self.show_customer_list, width=150, height=32).pack(side="left", padx=5) + + # Show sales orders by default + self.show_sales_orders() + + def show_inventory(self): + """Show inventory management""" + self.clear_content() + self.status_bar.configure(text="Inventory Management") + + # Title + title = ctk.CTkLabel(self.content_frame, text="Inventory Management", + font=("Arial", 24, "bold")) + title.pack(pady=(20, 30)) + + # Main frame + inventory_frame = ctk.CTkFrame(self.content_frame) + inventory_frame.pack(fill="both", expand=True, padx=20, pady=20) + + # Buttons frame + button_frame = ctk.CTkFrame(inventory_frame) + button_frame.pack(pady=20) + + ctk.CTkButton(button_frame, text="View Inventory", + command=self.view_inventory, width=150, height=32).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="Stock Adjustment", + command=self.stock_adjustment, width=150, height=32).pack(side="left", padx=5) + + # Show inventory by default + self.show_inventory_management() + + def get_recent_activities(self): + """Get recent activities from all modules""" + activities = [] + + # Get recent purchase orders + recent_pos = self.purchase_service.get_recent_purchase_orders(limit=3) + for po in recent_pos: + supplier = self.supplier_service.get_supplier(po.supplier_id) + supplier_name = supplier.name if supplier else "Unknown Supplier" + activities.append(f"Purchase Order #{po.id} created for {supplier_name}") + + # Get recent manufacturing orders + recent_mos = self.manufacturing_service.get_recent_manufacturing_orders(limit=3) + for mo in recent_mos: + product = self.product_service.get_product(mo.product_id) + product_name = product.name if product else "Unknown Product" + activities.append(f"Manufacturing Order #{mo.id} started for {product_name}") + + # Get recent sales orders + recent_sos = self.sales_service.get_recent_sales_orders(limit=3) + for so in recent_sos: + customer = self.customer_service.get_customer(so.customer_id) + customer_name = customer.name if customer else "Unknown Customer" + activities.append(f"Sales Order #{so.id} created for {customer_name}") + + return activities + + def logout(self): + """Logout the current user""" + if messagebox.askyesno("Logout", "Are you sure you want to logout?"): + # Use the app's restart method if available + if self.app: + self.app.restart() + else: + self.root.quit() + + # Module command methods + def new_purchase_order(self): + """Create a new purchase order""" + dialog = PurchaseOrderDialog(self.root, self.purchase_service, self.product_service, self.supplier_service) + dialog.grab_set() + + def view_purchase_orders(self): + """View all purchase orders""" + self.show_purchase_orders() + + def manage_suppliers(self): + """Manage suppliers""" + dialog = SupplierDialog(self.root, self.supplier_service) + dialog.grab_set() + + def new_manufacturing_order(self): + """Create a new manufacturing order""" + dialog = ManufacturingOrderDialog(self.root, self.manufacturing_service, self.product_service, self.inventory_service) + dialog.grab_set() + + def view_manufacturing_orders(self): + """View all manufacturing orders""" + self.show_manufacturing_orders() + + def production_planning(self): + """Production planning functionality""" + messagebox.showinfo("Info", "Production Planning functionality will be implemented") + + def new_sales_order(self): + """Create a new sales order""" + dialog = SalesOrderDialog(self.root, self.sales_service, self.product_service, self.customer_service) + dialog.grab_set() + + def view_sales_orders(self): + """View all sales orders""" + self.show_sales_orders() + + def manage_customers(self): + """Manage customers""" + dialog = CustomerDialog(self.root, self.customer_service) + dialog.grab_set() + + def show_purchase_orders(self): + """Show purchase orders management interface""" + self.clear_content() + self.status_bar.configure(text="Purchase Orders") + + # Title + title = ctk.CTkLabel(self.content_frame, text="Purchase Orders Management", + font=("Arial", 24, "bold")) + title.pack(pady=(20, 30)) + + # Main frame + purchase_frame = ctk.CTkFrame(self.content_frame) + purchase_frame.pack(fill="both", expand=True, padx=20, pady=20) + + # Buttons frame + button_frame = ctk.CTkFrame(purchase_frame) + button_frame.pack(pady=20) + + ctk.CTkButton(button_frame, text="New Purchase Order", + command=self.new_purchase_order, width=150, height=32).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="Manage Suppliers", + command=self.manage_suppliers, width=150, height=32).pack(side="left", padx=5) + + # Content area for purchase orders + self.purchase_content_frame = ctk.CTkFrame(purchase_frame) + self.purchase_content_frame.pack(fill="both", expand=True, padx=20, pady=20) + + # Load purchase orders + self.load_purchase_orders() + + def load_purchase_orders(self): + """Load and display purchase orders""" + # Clear existing content + for widget in self.purchase_content_frame.winfo_children(): + widget.destroy() + + # Get all purchase orders + orders = self.purchase_service.get_all_purchase_orders() + + if not orders: + ctk.CTkLabel(self.purchase_content_frame, + text="No purchase orders found. Click 'New Purchase Order' to create one.", + font=("Arial", 14)).pack(pady=50) + return + + # Create scrollable frame for orders + scroll_frame = ctk.CTkScrollableFrame(self.purchase_content_frame, height=400) + scroll_frame.pack(fill="both", expand=True, padx=10, pady=10) + + # Create headers + headers_frame = ctk.CTkFrame(scroll_frame) + headers_frame.pack(fill="x", padx=5, pady=5) + + headers = ["Order #", "Supplier", "Date", "Status", "Total", "Actions"] + for i, header in enumerate(headers): + ctk.CTkLabel(headers_frame, text=header, font=("Arial", 12, "bold")).grid( + row=0, column=i, padx=5, pady=5, sticky="w") + + # Display orders + for idx, order in enumerate(orders): + row_frame = ctk.CTkFrame(scroll_frame, height=24) + row_frame.pack(fill="x", padx=5, pady=2) + row_frame.pack_propagate(False) # Prevent frame from shrinking + + ctk.CTkLabel(row_frame, text=f"PO-{order.id}").grid( + row=0, column=0, padx=5, pady=5, sticky="w") + supplier = self.supplier_service.get_supplier(order.supplier_id) + supplier_name = supplier.name if supplier else "Unknown" + ctk.CTkLabel(row_frame, text=supplier_name).grid( + row=0, column=1, padx=5, pady=5, sticky="w") + ctk.CTkLabel(row_frame, text=order.order_date.strftime("%Y-%m-%d")).grid( + row=0, column=2, padx=5, pady=5, sticky="w") + ctk.CTkLabel(row_frame, text=order.status).grid( + row=0, column=3, padx=5, pady=5, sticky="w") + ctk.CTkLabel(row_frame, text=f"Rp{order.total_amount:,.0f}".replace(",", ".")).grid( + row=0, column=4, padx=5, pady=5, sticky="w") + + # Actions + actions_frame = ctk.CTkFrame(row_frame, fg_color="transparent") + actions_frame.grid(row=0, column=5, padx=5, pady=5, sticky="w") + + # Add status change button for pending orders + if order.status == "pending": + ctk.CTkButton(actions_frame, text="Complete", + command=lambda o=order: self.complete_purchase_order(o), + width=70, height=24, fg_color="green").pack(side="left", padx=2) + + def show_manufacturing_orders(self): + """Show manufacturing orders management interface""" + self.clear_content() + self.status_bar.configure(text="Manufacturing Orders") + + # Title + title = ctk.CTkLabel(self.content_frame, text="Manufacturing Orders Management", + font=("Arial", 24, "bold")) + title.pack(pady=(20, 30)) + + # Main frame + manufacturing_frame = ctk.CTkFrame(self.content_frame) + manufacturing_frame.pack(fill="both", expand=True, padx=20, pady=20) + + # Buttons frame + button_frame = ctk.CTkFrame(manufacturing_frame) + button_frame.pack(pady=20) + + ctk.CTkButton(button_frame, text="New Manufacturing Order", + command=self.new_manufacturing_order, width=180, height=32).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="Production Planning", + command=self.production_planning, width=150, height=32).pack(side="left", padx=5) + + # Content area for manufacturing orders + self.manufacturing_content_frame = ctk.CTkFrame(manufacturing_frame) + self.manufacturing_content_frame.pack(fill="both", expand=True, padx=20, pady=20) + + # Load manufacturing orders + self.load_manufacturing_orders() + + def load_manufacturing_orders(self): + """Load and display manufacturing orders""" + # Clear existing content + for widget in self.manufacturing_content_frame.winfo_children(): + widget.destroy() + + # Get all manufacturing orders + orders = self.manufacturing_service.get_all_manufacturing_orders() + + if not orders: + ctk.CTkLabel(self.manufacturing_content_frame, + text="No manufacturing orders found. Click 'New Manufacturing Order' to create one.", + font=("Arial", 14)).pack(pady=50) + return + + # Create scrollable frame for orders + scroll_frame = ctk.CTkScrollableFrame(self.manufacturing_content_frame, height=400) + scroll_frame.pack(fill="both", expand=True, padx=10, pady=10) + + # Create headers + headers_frame = ctk.CTkFrame(scroll_frame) + headers_frame.pack(fill="x", padx=5, pady=5) + + headers = ["Order #", "Product", "Quantity", "Start Date", "Status", "Actions"] + for i, header in enumerate(headers): + ctk.CTkLabel(headers_frame, text=header, font=("Arial", 12, "bold")).grid( + row=0, column=i, padx=5, pady=5, sticky="w") + + # Display orders + for idx, order in enumerate(orders): + row_frame = ctk.CTkFrame(scroll_frame, height=24) + row_frame.pack(fill="x", padx=5, pady=2) + row_frame.pack_propagate(False) # Prevent frame from shrinking + + ctk.CTkLabel(row_frame, text=f"MO-{order.id}").grid( + row=0, column=0, padx=5, pady=5, sticky="w") + product = self.product_service.get_product(order.product_id) + product_name = product.name if product else "Unknown" + ctk.CTkLabel(row_frame, text=product_name).grid( + row=0, column=1, padx=5, pady=5, sticky="w") + ctk.CTkLabel(row_frame, text=str(order.quantity)).grid( + row=0, column=2, padx=5, pady=5, sticky="w") + ctk.CTkLabel(row_frame, text=order.start_date.strftime("%Y-%m-%d")).grid( + row=0, column=3, padx=5, pady=5, sticky="w") + ctk.CTkLabel(row_frame, text=order.status).grid( + row=0, column=4, padx=5, pady=5, sticky="w") + + # Actions + actions_frame = ctk.CTkFrame(row_frame, fg_color="transparent") + actions_frame.grid(row=0, column=5, padx=5, pady=5, sticky="w") + + # Add status change button for pending orders + if order.status == "pending": + ctk.CTkButton(actions_frame, text="Complete", + command=lambda o=order: self.complete_manufacturing_order(o), + width=70, height=24, fg_color="green").pack(side="left", padx=2) + + def show_sales_orders(self): + """Show sales orders management interface""" + self.clear_content() + self.status_bar.configure(text="Sales Orders") + + # Title + title = ctk.CTkLabel(self.content_frame, text="Sales Orders Management", + font=("Arial", 24, "bold")) + title.pack(pady=(20, 30)) + + # Main frame + sales_frame = ctk.CTkFrame(self.content_frame) + sales_frame.pack(fill="both", expand=True, padx=20, pady=20) + + # Buttons frame + button_frame = ctk.CTkFrame(sales_frame) + button_frame.pack(pady=20) + + ctk.CTkButton(button_frame, text="New Sales Order", + command=self.new_sales_order, width=150, height=32).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="Manage Customers", + command=self.manage_customers, width=150, height=32).pack(side="left", padx=5) + + # Content area for sales orders + self.sales_content_frame = ctk.CTkFrame(sales_frame) + self.sales_content_frame.pack(fill="both", expand=True, padx=20, pady=20) + + # Load sales orders + self.load_sales_orders() + + def load_sales_orders(self): + """Load and display sales orders""" + # Clear existing content + for widget in self.sales_content_frame.winfo_children(): + widget.destroy() + + # Get all sales orders + orders = self.sales_service.get_all_sales_orders() + + if not orders: + ctk.CTkLabel(self.sales_content_frame, + text="No sales orders found. Click 'New Sales Order' to create one.", + font=("Arial", 14)).pack(pady=50) + return + + # Create scrollable frame for orders + scroll_frame = ctk.CTkScrollableFrame(self.sales_content_frame, height=400) + scroll_frame.pack(fill="both", expand=True, padx=10, pady=10) + + # Create headers + headers_frame = ctk.CTkFrame(scroll_frame) + headers_frame.pack(fill="x", padx=5, pady=5) + + headers = ["Order #", "Customer", "Date", "Status", "Total", "Actions"] + for i, header in enumerate(headers): + ctk.CTkLabel(headers_frame, text=header, font=("Arial", 12, "bold")).grid( + row=0, column=i, padx=5, pady=5, sticky="w") + + # Display orders + for idx, order in enumerate(orders): + row_frame = ctk.CTkFrame(scroll_frame, height=24) + row_frame.pack(fill="x", padx=5, pady=2) + row_frame.pack_propagate(False) # Prevent frame from shrinking + + ctk.CTkLabel(row_frame, text=f"SO-{order.id}").grid( + row=0, column=0, padx=5, pady=5, sticky="w") + customer = self.customer_service.get_customer(order.customer_id) + customer_name = customer.name if customer else "Unknown" + ctk.CTkLabel(row_frame, text=customer_name).grid( + row=0, column=1, padx=5, pady=5, sticky="w") + ctk.CTkLabel(row_frame, text=order.order_date.strftime("%Y-%m-%d")).grid( + row=0, column=2, padx=5, pady=5, sticky="w") + ctk.CTkLabel(row_frame, text=order.status).grid( + row=0, column=3, padx=5, pady=5, sticky="w") + ctk.CTkLabel(row_frame, text=f"Rp{order.total_amount:,.0f}".replace(",", ".")).grid( + row=0, column=4, padx=5, pady=5, sticky="w") + + # Actions + actions_frame = ctk.CTkFrame(row_frame, fg_color="transparent") + actions_frame.grid(row=0, column=5, padx=5, pady=5, sticky="w") + + # Add status change button for pending orders + if order.status == "pending": + ctk.CTkButton(actions_frame, text="Complete", + command=lambda o=order: self.complete_sales_order(o), + width=70, height=24, fg_color="green").pack(side="left", padx=2) + + def view_purchase_order(self, order): + """View details of a specific purchase order""" + messagebox.showinfo("Purchase Order", f"Viewing Purchase Order #{order.id}") + + def view_manufacturing_order(self, order): + """View details of a specific manufacturing order""" + messagebox.showinfo("Manufacturing Order", f"Viewing Manufacturing Order #{order.id}") + + def view_sales_order(self, order): + """View details of a specific sales order""" + messagebox.showinfo("Sales Order", f"Viewing Sales Order #{order.id}") + + def view_inventory(self): + """View inventory""" + self.show_inventory_management() + + def stock_adjustment(self): + """Adjust stock levels""" + self.show_inventory_management() + + def show_inventory_management(self): + """Show detailed inventory management interface""" + self.clear_content() + self.status_bar.configure(text="Inventory Management") + + # Title + title = ctk.CTkLabel(self.content_frame, text="Inventory Management", + font=("Arial", 24, "bold")) + title.pack(pady=(20, 30)) + + # Main frame + inventory_frame = ctk.CTkFrame(self.content_frame) + inventory_frame.pack(fill="both", expand=True, padx=20, pady=20) + + # Buttons frame + button_frame = ctk.CTkFrame(inventory_frame) + button_frame.pack(pady=20) + + ctk.CTkButton(button_frame, text="Manage Products", + command=self.manage_products, width=150, height=32).pack(side="left", padx=5) + + # Content area for inventory + self.inventory_content_frame = ctk.CTkFrame(inventory_frame) + self.inventory_content_frame.pack(fill="both", expand=True, padx=20, pady=20) + + # Load inventory data + self.load_inventory_data() + + def load_inventory_data(self): + """Load and display inventory data""" + # Clear existing content + for widget in self.inventory_content_frame.winfo_children(): + widget.destroy() + + # Get all products + products = self.product_service.get_all_products() + + if not products: + ctk.CTkLabel(self.inventory_content_frame, + text="No products found. Click 'Manage Products' to add products.", + font=("Arial", 14)).pack(pady=50) + return + + # Create scrollable frame for products + scroll_frame = ctk.CTkScrollableFrame(self.inventory_content_frame, height=400) + scroll_frame.pack(fill="both", expand=True, padx=10, pady=10) + + # Create headers + headers_frame = ctk.CTkFrame(scroll_frame) + headers_frame.pack(fill="x", padx=5, pady=5) + + headers = ["Product", "SKU", "Current Stock", "Min Stock", "Status", "Actions"] + for i, header in enumerate(headers): + ctk.CTkLabel(headers_frame, text=header, font=("Arial", 12, "bold")).grid( + row=0, column=i, padx=5, pady=5, sticky="w") + + # Display products + for idx, product in enumerate(products): + row_frame = ctk.CTkFrame(scroll_frame, height=24) + row_frame.pack(fill="x", padx=5, pady=2) + row_frame.pack_propagate(False) # Prevent frame from shrinking + + # Product info + ctk.CTkLabel(row_frame, text=product.name).grid( + row=0, column=0, padx=5, pady=5, sticky="w") + ctk.CTkLabel(row_frame, text=product.sku or "-").grid( + row=0, column=1, padx=5, pady=5, sticky="w") + + # Get current stock + current_stock = self.inventory_service.get_current_stock(product.id) + ctk.CTkLabel(row_frame, text=str(current_stock)).grid( + row=0, column=2, padx=5, pady=5, sticky="w") + + ctk.CTkLabel(row_frame, text=str(product.min_stock)).grid( + row=0, column=3, padx=5, pady=5, sticky="w") + + # Status + status = "Low Stock" if current_stock <= product.min_stock else "OK" + status_color = "red" if current_stock <= product.min_stock else "green" + ctk.CTkLabel(row_frame, text=status, text_color=status_color).grid( + row=0, column=4, padx=5, pady=5, sticky="w") + + # Actions + actions_frame = ctk.CTkFrame(row_frame, fg_color="transparent") + actions_frame.grid(row=0, column=5, padx=5, pady=5, sticky="w") + + ctk.CTkButton(actions_frame, text="Edit", + command=lambda p=product: self.edit_product(p), + width=60, height=24).pack(side="left", padx=2) + ctk.CTkButton(actions_frame, text="Adjust", + command=lambda p=product: self.adjust_product_stock(p), + width=60, height=24).pack(side="left", padx=2) + + def manage_products(self): + """Open product management dialog""" + dialog = ProductDialog(self.root, self.product_service) + dialog.grab_set() + + def adjust_stock(self): + """Open stock adjustment interface""" + self.show_inventory_management() + + def edit_product(self, product): + """Edit a specific product""" + dialog = ProductDialog(self.root, self.product_service, product) + dialog.grab_set() + + def adjust_product_stock(self, product): + """Adjust stock for a specific product""" + # Create a simple stock adjustment dialog + dialog = ctk.CTkToplevel(self.root) + dialog.title(f"Adjust Stock - {product.name}") + dialog.geometry("300x200") + dialog.transient(self.root) + dialog.grab_set() + + ctk.CTkLabel(dialog, text=f"Current Stock: {self.inventory_service.get_current_stock(product.id)}").pack(pady=10) + + ctk.CTkLabel(dialog, text="Adjustment Amount:").pack() + adjustment_entry = ctk.CTkEntry(dialog) + adjustment_entry.pack(pady=5) + + def adjust(): + try: + amount = int(adjustment_entry.get()) + self.inventory_service.adjust_inventory(product.id, amount, "Manual adjustment") + messagebox.showinfo("Success", f"Stock adjusted by {amount}") + dialog.destroy() + self.load_inventory_data() + except ValueError: + messagebox.showerror("Error", "Please enter a valid number") + + ctk.CTkButton(dialog, text="Adjust", command=adjust).pack(pady=10) + ctk.CTkButton(dialog, text="Cancel", command=dialog.destroy).pack(pady=5) + + + def manage_users(self): + """User management (admin only)""" + from src.ui.user_management_dialog import UserManagementDialog + dialog = UserManagementDialog(self.root, self.auth_manager) + self.root.wait_window(dialog) + + + def show_inventory_report(self): + """Show inventory report""" + try: + filename = self.inventory_service.export_inventory_report() + messagebox.showinfo("Success", f"Inventory report exported to {filename}") + except Exception as e: + messagebox.showerror("Error", f"Failed to export inventory report: {str(e)}") + + def show_sales_report(self): + """Show sales report with date range selection""" + from src.ui.date_range_dialog import DateRangeDialog + dialog = DateRangeDialog(self.root, "Select Date Range for Sales Report", "Export") + self.root.wait_window(dialog) + + start_date, end_date = dialog.get_date_range() + if start_date and end_date: + try: + filename = self.sales_service.export_sales_report(start_date, end_date) + messagebox.showinfo("Success", f"Sales report exported to {filename}") + except Exception as e: + messagebox.showerror("Error", f"Failed to export sales report: {str(e)}") + + def show_purchase_report(self): + """Show purchase report with date range selection""" + from src.ui.date_range_dialog import DateRangeDialog + dialog = DateRangeDialog(self.root, "Select Date Range for Purchase Report", "Export") + self.root.wait_window(dialog) + + start_date, end_date = dialog.get_date_range() + if start_date and end_date: + try: + filename = self.purchase_service.export_purchase_report(start_date, end_date) + messagebox.showinfo("Success", f"Purchase report exported to {filename}") + except Exception as e: + messagebox.showerror("Error", f"Failed to export purchase report: {str(e)}") + + def show_manufacturing_report(self): + """Show manufacturing report with date range selection""" + from src.ui.date_range_dialog import DateRangeDialog + dialog = DateRangeDialog(self.root, "Select Date Range for Manufacturing Report", "Export") + self.root.wait_window(dialog) + + start_date, end_date = dialog.get_date_range() + if start_date and end_date: + try: + filename = self.manufacturing_service.export_manufacturing_report(start_date, end_date) + messagebox.showinfo("Success", f"Manufacturing report exported to {filename}") + except Exception as e: + messagebox.showerror("Error", f"Failed to export manufacturing report: {str(e)}") + + def show_stock_movement_report(self): + """Show stock movement report""" + try: + filename = self.inventory_service.export_stock_movement_report() + messagebox.showinfo("Success", f"Stock movement report exported to {filename}") + except Exception as e: + messagebox.showerror("Error", f"Failed to export stock movement report: {str(e)}") + + def complete_purchase_order(self, order): + """Complete a purchase order""" + if messagebox.askyesno("Confirm", f"Are you sure you want to complete purchase order #{order.id}?"): + if self.purchase_service.receive_purchase_order(order.id): + messagebox.showinfo("Success", f"Purchase order #{order.id} completed successfully") + self.load_purchase_orders() + else: + messagebox.showerror("Error", f"Failed to complete purchase order #{order.id}") + + def complete_manufacturing_order(self, order): + """Complete a manufacturing order""" + if messagebox.askyesno("Confirm", f"Are you sure you want to complete manufacturing order #{order.id}?"): + if self.manufacturing_service.complete_manufacturing_order(order.id): + messagebox.showinfo("Success", f"Manufacturing order #{order.id} completed successfully") + self.load_manufacturing_orders() + else: + messagebox.showerror("Error", f"Failed to complete manufacturing order #{order.id}") + + def complete_sales_order(self, order): + """Complete a sales order""" + if messagebox.askyesno("Confirm", f"Are you sure you want to complete sales order #{order.id}?"): + if self.sales_service.deliver_sales_order(order.id): + messagebox.showinfo("Success", f"Sales order #{order.id} completed successfully") + self.load_sales_orders() + else: + messagebox.showerror("Error", f"Failed to complete sales order #{order.id}") + + def view_purchase_order(self, order): + """View details of a specific purchase order""" + messagebox.showinfo("Purchase Order", f"Viewing Purchase Order #{order.id}") + + def view_manufacturing_order(self, order): + """View details of a specific manufacturing order""" + messagebox.showinfo("Manufacturing Order", f"Viewing Manufacturing Order #{order.id}") + + def view_sales_order(self, order): + """View details of a specific sales order""" + messagebox.showinfo("Sales Order", f"Viewing Sales Order #{order.id}") + + def show_supplier_list(self): + """Show supplier management interface""" + self.clear_content() + self.status_bar.configure(text="Supplier Management") + + # Title + title = ctk.CTkLabel(self.content_frame, text="Supplier Management", + font=("Arial", 24, "bold")) + title.pack(pady=(20, 30)) + + # Main frame + supplier_frame = ctk.CTkFrame(self.content_frame) + supplier_frame.pack(fill="both", expand=True, padx=20, pady=20) + + # Buttons frame + button_frame = ctk.CTkFrame(supplier_frame) + button_frame.pack(pady=20) + + ctk.CTkButton(button_frame, text="Add New Supplier", + command=self.add_supplier, width=150, height=32).pack(side="left", padx=5) + + # Content area for suppliers + self.supplier_content_frame = ctk.CTkFrame(supplier_frame) + self.supplier_content_frame.pack(fill="both", expand=True, padx=20, pady=20) + + # Load suppliers + self.load_suppliers() + + def load_suppliers(self): + """Load and display suppliers""" + # Clear existing content + for widget in self.supplier_content_frame.winfo_children(): + widget.destroy() + + # Get all suppliers + suppliers = self.supplier_service.get_all_suppliers() + + if not suppliers: + ctk.CTkLabel(self.supplier_content_frame, + text="No suppliers found. Click 'Add New Supplier' to create one.", + font=("Arial", 14)).pack(pady=50) + return + + # Create scrollable frame for suppliers + scroll_frame = ctk.CTkScrollableFrame(self.supplier_content_frame, height=400) + scroll_frame.pack(fill="both", expand=True, padx=10, pady=10) + + # Create headers + headers_frame = ctk.CTkFrame(scroll_frame) + headers_frame.pack(fill="x", padx=5, pady=5) + + headers = ["Name", "Contact Person", "Phone", "Email", "Actions"] + for i, header in enumerate(headers): + ctk.CTkLabel(headers_frame, text=header, font=("Arial", 12, "bold")).grid( + row=0, column=i, padx=5, pady=5, sticky="w") + + # Display suppliers + for idx, supplier in enumerate(suppliers): + row_frame = ctk.CTkFrame(scroll_frame, height=24) + row_frame.pack(fill="x", padx=5, pady=2) + row_frame.pack_propagate(False) # Prevent frame from shrinking + + ctk.CTkLabel(row_frame, text=supplier.name).grid( + row=0, column=0, padx=5, pady=5, sticky="w") + ctk.CTkLabel(row_frame, text=supplier.contact_person or "-").grid( + row=0, column=1, padx=5, pady=5, sticky="w") + ctk.CTkLabel(row_frame, text=supplier.phone or "-").grid( + row=0, column=2, padx=5, pady=5, sticky="w") + ctk.CTkLabel(row_frame, text=supplier.email or "-").grid( + row=0, column=3, padx=5, pady=5, sticky="w") + + # Actions + actions_frame = ctk.CTkFrame(row_frame, fg_color="transparent") + actions_frame.grid(row=0, column=4, padx=5, pady=5, sticky="w") + + ctk.CTkButton(actions_frame, text="Edit", + command=lambda s=supplier: self.edit_supplier(s), + width=60, height=24).pack(side="left", padx=2) + ctk.CTkButton(actions_frame, text="Delete", + command=lambda s=supplier: self.delete_supplier(s), + width=60, height=24, fg_color="red", hover_color="dark red").pack(side="left", padx=2) + + def add_supplier(self): + """Add a new supplier""" + from src.ui.supplier_dialog import SupplierDialog + dialog = SupplierDialog(self.root, self.supplier_service) + self.root.wait_window(dialog) + self.load_suppliers() + + def edit_supplier(self, supplier): + """Edit a supplier""" + from src.ui.supplier_dialog import SupplierDialog + dialog = SupplierDialog(self.root, self.supplier_service, supplier) + self.root.wait_window(dialog) + self.load_suppliers() + + def delete_supplier(self, supplier): + """Delete a supplier""" + if messagebox.askyesno("Confirm Delete", f"Are you sure you want to delete supplier '{supplier.name}'?"): + if self.supplier_service.delete_supplier(supplier.id): + messagebox.showinfo("Success", f"Supplier '{supplier.name}' deleted successfully") + self.load_suppliers() + else: + messagebox.showerror("Error", f"Failed to delete supplier '{supplier.name}'") + + def show_customer_list(self): + """Show customer management interface""" + self.clear_content() + self.status_bar.configure(text="Customer Management") + + # Title + title = ctk.CTkLabel(self.content_frame, text="Customer Management", + font=("Arial", 24, "bold")) + title.pack(pady=(20, 30)) + + # Main frame + customer_frame = ctk.CTkFrame(self.content_frame) + customer_frame.pack(fill="both", expand=True, padx=20, pady=20) + + # Buttons frame + button_frame = ctk.CTkFrame(customer_frame) + button_frame.pack(pady=20) + + ctk.CTkButton(button_frame, text="Add New Customer", + command=self.add_customer, width=150, height=32).pack(side="left", padx=5) + + # Content area for customers + self.customer_content_frame = ctk.CTkFrame(customer_frame) + self.customer_content_frame.pack(fill="both", expand=True, padx=20, pady=20) + + # Load customers + self.load_customers() + + def load_customers(self): + """Load and display customers""" + # Clear existing content + for widget in self.customer_content_frame.winfo_children(): + widget.destroy() + + # Get all customers + customers = self.customer_service.get_all_customers() + + if not customers: + ctk.CTkLabel(self.customer_content_frame, + text="No customers found. Click 'Add New Customer' to create one.", + font=("Arial", 14)).pack(pady=50) + return + + # Create scrollable frame for customers + scroll_frame = ctk.CTkScrollableFrame(self.customer_content_frame, height=400) + scroll_frame.pack(fill="both", expand=True, padx=10, pady=10) + + # Create headers + headers_frame = ctk.CTkFrame(scroll_frame) + headers_frame.pack(fill="x", padx=5, pady=5) + + headers = ["Name", "Contact Person", "Phone", "Email", "Actions"] + for i, header in enumerate(headers): + ctk.CTkLabel(headers_frame, text=header, font=("Arial", 12, "bold")).grid( + row=0, column=i, padx=5, pady=5, sticky="w") + + # Display customers + for idx, customer in enumerate(customers): + row_frame = ctk.CTkFrame(scroll_frame, height=24) + row_frame.pack(fill="x", padx=5, pady=2) + row_frame.pack_propagate(False) # Prevent frame from shrinking + + ctk.CTkLabel(row_frame, text=customer.name).grid( + row=0, column=0, padx=5, pady=5, sticky="w") + ctk.CTkLabel(row_frame, text=customer.contact_person or "-").grid( + row=0, column=1, padx=5, pady=5, sticky="w") + ctk.CTkLabel(row_frame, text=customer.phone or "-").grid( + row=0, column=2, padx=5, pady=5, sticky="w") + ctk.CTkLabel(row_frame, text=customer.email or "-").grid( + row=0, column=3, padx=5, pady=5, sticky="w") + + # Actions + actions_frame = ctk.CTkFrame(row_frame, fg_color="transparent") + actions_frame.grid(row=0, column=4, padx=5, pady=5, sticky="w") + + ctk.CTkButton(actions_frame, text="Edit", + command=lambda c=customer: self.edit_customer(c), + width=60, height=24).pack(side="left", padx=2) + ctk.CTkButton(actions_frame, text="Delete", + command=lambda c=customer: self.delete_customer(c), + width=60, height=24, fg_color="red", hover_color="dark red").pack(side="left", padx=2) + + def add_customer(self): + """Add a new customer""" + from src.ui.customer_dialog import CustomerDialog + dialog = CustomerDialog(self.root, self.customer_service) + self.root.wait_window(dialog) + self.load_customers() + + def edit_customer(self, customer): + """Edit a customer""" + from src.ui.customer_dialog import CustomerDialog + dialog = CustomerDialog(self.root, self.customer_service, customer) + self.root.wait_window(dialog) + self.load_customers() + + def delete_customer(self, customer): + """Delete a customer""" + if messagebox.askyesno("Confirm Delete", f"Are you sure you want to delete customer '{customer.name}'?"): + if self.customer_service.delete_customer(customer.id): + messagebox.showinfo("Success", f"Customer '{customer.name}' deleted successfully") + self.load_customers() + else: + messagebox.showerror("Error", f"Failed to delete customer '{customer.name}'") + + def show_product_list(self): + """Show product management interface""" + self.clear_content() + self.status_bar.configure(text="Product Management") + + # Title + title = ctk.CTkLabel(self.content_frame, text="Product Management", + font=("Arial", 24, "bold")) + title.pack(pady=(20, 30)) + + # Main frame + product_frame = ctk.CTkFrame(self.content_frame) + product_frame.pack(fill="both", expand=True, padx=20, pady=20) + + # Buttons frame + button_frame = ctk.CTkFrame(product_frame) + button_frame.pack(pady=20) + + ctk.CTkButton(button_frame, text="Add New Product", + command=self.add_product, width=150, height=32).pack(side="left", padx=5) + + # Show inventory management which displays product list + self.show_inventory_management() + + def add_product(self): + """Add a new product""" + from src.ui.product_dialog import ProductDialog + dialog = ProductDialog(self.root, self.product_service) + self.root.wait_window(dialog) + self.load_inventory_data() \ No newline at end of file diff --git a/src/ui/manufacturing_order_dialog.py b/src/ui/manufacturing_order_dialog.py new file mode 100644 index 0000000..eca604c --- /dev/null +++ b/src/ui/manufacturing_order_dialog.py @@ -0,0 +1,107 @@ +import customtkinter as ctk +from tkinter import messagebox +from datetime import datetime +from typing import Optional, List +from src.models import ManufacturingOrder, Product +from src.services import ManufacturingOrderService, ProductService + +class ManufacturingOrderDialog(ctk.CTkToplevel): + def __init__(self, parent, manufacturing_service: ManufacturingOrderService, + product_service: ProductService, + manufacturing_order: Optional[ManufacturingOrder] = None): + super().__init__(parent) + self.manufacturing_service = manufacturing_service + self.product_service = product_service + self.manufacturing_order = manufacturing_order + + self.title("New Manufacturing Order" if not manufacturing_order else "Edit Manufacturing Order") + self.geometry("500x400") + + # Make dialog modal + self.transient(parent) + self.grab_set() + + self.setup_ui() + + def setup_ui(self): + # Product selection + ctk.CTkLabel(self, text="Product:").grid(row=0, column=0, padx=10, pady=5, sticky="w") + self.product_var = ctk.StringVar() + self.product_combo = ctk.CTkComboBox(self, variable=self.product_var, width=200) + self.product_combo.grid(row=0, column=1, padx=10, pady=5) + + # Quantity + ctk.CTkLabel(self, text="Quantity:").grid(row=1, column=0, padx=10, pady=5, sticky="w") + self.quantity_entry = ctk.CTkEntry(self, width=200) + self.quantity_entry.grid(row=1, column=1, padx=10, pady=5) + + # Start date + ctk.CTkLabel(self, text="Start Date:").grid(row=2, column=0, padx=10, pady=5, sticky="w") + self.start_date_entry = ctk.CTkEntry(self, width=200) + self.start_date_entry.insert(0, datetime.now().strftime("%Y-%m-%d")) + self.start_date_entry.grid(row=2, column=1, padx=10, pady=5) + + # Expected completion + ctk.CTkLabel(self, text="Expected Completion:").grid(row=3, column=0, padx=10, pady=5, sticky="w") + self.expected_completion_entry = ctk.CTkEntry(self, width=200) + self.expected_completion_entry.grid(row=3, column=1, padx=10, pady=5) + + # Status + ctk.CTkLabel(self, text="Status:").grid(row=4, column=0, padx=10, pady=5, sticky="w") + self.status_var = ctk.StringVar(value="planned") + self.status_combo = ctk.CTkComboBox(self, variable=self.status_var, + values=["planned", "in_progress", "completed", "cancelled"]) + self.status_combo.grid(row=4, column=1, padx=10, pady=5) + + # Notes + ctk.CTkLabel(self, text="Notes:").grid(row=5, column=0, padx=10, pady=5, sticky="nw") + self.notes_text = ctk.CTkTextbox(self, height=100, width=200) + self.notes_text.grid(row=5, column=1, padx=10, pady=5) + + # Buttons + button_frame = ctk.CTkFrame(self) + button_frame.grid(row=6, column=0, columnspan=2, pady=20) + + ctk.CTkButton(button_frame, text="Save", command=self.save_order).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="Cancel", command=self.destroy).pack(side="left", padx=5) + + # Load data + self.load_products() + + def load_products(self): + products = self.product_service.get_all_products() + product_names = [p.name for p in products] + self.product_combo.configure(values=product_names) + + def save_order(self): + product_name = self.product_var.get() + try: + quantity = int(self.quantity_entry.get()) + except ValueError: + messagebox.showerror("Error", "Please enter a valid quantity") + return + + if quantity <= 0: + messagebox.showerror("Error", "Quantity must be positive") + return + + product = next((p for p in self.product_service.get_all_products() if p.name == product_name), None) + if not product: + messagebox.showerror("Error", "Please select a valid product") + return + + # Create manufacturing order + mo = self.manufacturing_service.create_manufacturing_order( + product_id=product.id, + quantity=quantity, + start_date=self.start_date_entry.get(), + expected_completion=self.expected_completion_entry.get() or None, + status=self.status_var.get(), + notes=self.notes_text.get("1.0", "end-1c") + ) + + if mo: + messagebox.showinfo("Success", f"Manufacturing order #{mo.id} created successfully") + self.destroy() + else: + messagebox.showerror("Error", "Failed to create manufacturing order") \ No newline at end of file diff --git a/src/ui/product_dialog.py b/src/ui/product_dialog.py new file mode 100644 index 0000000..e099d69 --- /dev/null +++ b/src/ui/product_dialog.py @@ -0,0 +1,83 @@ +import customtkinter as ctk +from tkinter import messagebox +from typing import Optional +from src.models import Product +from src.services import ProductService + +class ProductDialog(ctk.CTkToplevel): + def __init__(self, parent, product_service: ProductService, product: Optional[Product] = None): + super().__init__(parent) + self.product_service = product_service + self.product = product + + self.title("Add Product" if not product else "Edit Product") + self.geometry("400x300") + + # Make dialog modal + self.transient(parent) + self.grab_set() + + self.setup_ui() + + def setup_ui(self): + # Name + ctk.CTkLabel(self, text="Name:").grid(row=0, column=0, padx=10, pady=5, sticky="w") + self.name_entry = ctk.CTkEntry(self, width=250) + self.name_entry.grid(row=0, column=1, padx=10, pady=5) + + # Description + ctk.CTkLabel(self, text="Description:").grid(row=1, column=0, padx=10, pady=5, sticky="nw") + self.description_text = ctk.CTkTextbox(self, width=250, height=80) + self.description_text.grid(row=1, column=1, padx=10, pady=5) + + # Unit Price + ctk.CTkLabel(self, text="Unit Price:").grid(row=2, column=0, padx=10, pady=5, sticky="w") + self.price_entry = ctk.CTkEntry(self, width=250) + self.price_entry.grid(row=2, column=1, padx=10, pady=5) + + # Buttons + button_frame = ctk.CTkFrame(self) + button_frame.grid(row=3, column=0, columnspan=2, pady=20) + + ctk.CTkButton(button_frame, text="Save", command=self.save_product).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="Cancel", command=self.destroy).pack(side="left", padx=5) + + # Load data if editing + if self.product: + self.name_entry.insert(0, self.product.name) + self.description_text.insert("1.0", self.product.description) + self.price_entry.insert(0, str(self.product.unit_price)) + + def save_product(self): + name = self.name_entry.get().strip() + description = self.description_text.get("1.0", "end-1c").strip() + + try: + unit_price = float(self.price_entry.get()) + except ValueError: + messagebox.showerror("Error", "Please enter a valid price") + return + + if not name: + messagebox.showerror("Error", "Please enter a product name") + return + + if self.product: + # Update existing product + self.product.name = name + self.product.description = description + self.product.unit_price = unit_price + + if self.product_service.update_product(self.product): + messagebox.showinfo("Success", "Product updated successfully") + self.destroy() + else: + messagebox.showerror("Error", "Failed to update product") + else: + # Create new product + product = self.product_service.create_product(name, description, unit_price) + if product: + messagebox.showinfo("Success", "Product created successfully") + self.destroy() + else: + messagebox.showerror("Error", "Failed to create product") \ No newline at end of file diff --git a/src/ui/purchase_order_dialog.py b/src/ui/purchase_order_dialog.py new file mode 100644 index 0000000..5f0e5b9 --- /dev/null +++ b/src/ui/purchase_order_dialog.py @@ -0,0 +1,233 @@ +import customtkinter as ctk +from tkinter import messagebox +from datetime import datetime, date +from typing import Optional, List +from src.models import PurchaseOrder, PurchaseOrderItem, Product, Supplier +from src.services import PurchaseOrderService, ProductService, SupplierService +from src.ui.date_picker_dialog import DatePickerDialog + +class PurchaseOrderDialog(ctk.CTkToplevel): + def __init__(self, parent, purchase_service: PurchaseOrderService, + product_service: ProductService, supplier_service: SupplierService, + purchase_order: Optional[PurchaseOrder] = None): + super().__init__(parent) + self.purchase_service = purchase_service + self.product_service = product_service + self.supplier_service = supplier_service + self.purchase_order = purchase_order + + self.title("New Purchase Order" if not purchase_order else "Edit Purchase Order") + self.geometry("600x500") + + # Make dialog modal + self.transient(parent) + self.grab_set() + + self.items = [] + self.setup_ui() + + def setup_ui(self): + # Supplier selection + ctk.CTkLabel(self, text="Supplier:").grid(row=0, column=0, padx=10, pady=5, sticky="w") + self.supplier_var = ctk.StringVar() + self.supplier_combo = ctk.CTkComboBox(self, variable=self.supplier_var, width=200) + self.supplier_combo.grid(row=0, column=1, padx=10, pady=5) + + # Order date + ctk.CTkLabel(self, text="Order Date:").grid(row=1, column=0, padx=10, pady=5, sticky="w") + date_frame1 = ctk.CTkFrame(self) + date_frame1.grid(row=1, column=1, padx=10, pady=5, sticky="w") + self.order_date_entry = ctk.CTkEntry(date_frame1, width=150) + self.order_date_entry.insert(0, datetime.now().strftime("%Y-%m-%d")) + self.order_date_entry.pack(side="left") + ctk.CTkButton(date_frame1, text="...", width=30, command=self.select_order_date).pack(side="left", padx=(5, 0)) + + # Expected delivery + ctk.CTkLabel(self, text="Expected Delivery:").grid(row=2, column=0, padx=10, pady=5, sticky="w") + date_frame2 = ctk.CTkFrame(self) + date_frame2.grid(row=2, column=1, padx=10, pady=5, sticky="w") + self.expected_delivery_entry = ctk.CTkEntry(date_frame2, width=150) + self.expected_delivery_entry.pack(side="left") + ctk.CTkButton(date_frame2, text="...", width=30, command=self.select_expected_delivery).pack(side="left", padx=(5, 0)) + + # Items frame + items_frame = ctk.CTkFrame(self) + items_frame.grid(row=3, column=0, columnspan=2, padx=10, pady=10, sticky="nsew") + + # Add item controls + add_frame = ctk.CTkFrame(items_frame) + add_frame.pack(fill="x", padx=5, pady=5) + + ctk.CTkLabel(add_frame, text="Product:").pack(side="left", padx=5) + self.product_var = ctk.StringVar() + self.product_combo = ctk.CTkComboBox(add_frame, variable=self.product_var, width=150) + self.product_combo.pack(side="left", padx=5) + + ctk.CTkLabel(add_frame, text="Qty:").pack(side="left", padx=5) + self.quantity_entry = ctk.CTkEntry(add_frame, width=50) + self.quantity_entry.pack(side="left", padx=5) + + ctk.CTkLabel(add_frame, text="Price:").pack(side="left", padx=5) + self.price_entry = ctk.CTkEntry(add_frame, width=80) + self.price_entry.pack(side="left", padx=5) + + ctk.CTkButton(add_frame, text="Add", command=self.add_item, width=60).pack(side="left", padx=5) + + # Items list + self.items_text = ctk.CTkTextbox(items_frame, height=150) + self.items_text.pack(fill="both", expand=True, padx=5, pady=5) + + # Total + self.total_label = ctk.CTkLabel(self, text="Total: Rp0", font=("Arial", 14, "bold")) + self.total_label.grid(row=4, column=0, columnspan=2, pady=10) + + # Buttons + button_frame = ctk.CTkFrame(self) + button_frame.grid(row=5, column=0, columnspan=2, pady=10) + + ctk.CTkButton(button_frame, text="Save", command=self.save_order).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="Cancel", command=self.destroy).pack(side="left", padx=5) + + # Load data + self.load_suppliers() + self.load_products() + + def load_suppliers(self): + suppliers = self.supplier_service.get_all_suppliers() + supplier_names = [s.name for s in suppliers] + self.supplier_combo.configure(values=supplier_names) + + def load_products(self): + products = self.product_service.get_all_products() + product_names = [p.name for p in products] + self.product_combo.configure(values=product_names) + + def add_item(self): + product_name = self.product_var.get() + try: + quantity = int(self.quantity_entry.get()) + price = float(self.price_entry.get()) + except ValueError: + messagebox.showerror("Error", "Please enter valid quantity and price") + return + + if quantity <= 0 or price <= 0: + messagebox.showerror("Error", "Quantity and price must be positive") + return + + product = next((p for p in self.product_service.get_all_products() if p.name == product_name), None) + if not product: + messagebox.showerror("Error", "Please select a valid product") + return + + total = quantity * price + self.items.append({ + 'product': product, + 'quantity': quantity, + 'price': price, + 'total': total + }) + + self.update_items_display() + self.update_total() + + # Clear inputs + self.quantity_entry.delete(0, 'end') + self.price_entry.delete(0, 'end') + + def update_items_display(self): + self.items_text.delete("1.0", "end") + for item in self.items: + self.items_text.insert("end", + f"{item['product'].name} - {item['quantity']} x Rp{item['price']:,.0f} = Rp{item['total']:,.0f}\n") + + def update_total(self): + total = sum(item['total'] for item in self.items) + self.total_label.configure(text=f"Total: Rp{total:,.0f}") + + def save_order(self): + supplier_name = self.supplier_var.get() + if not supplier_name: + messagebox.showerror("Error", "Please select a supplier") + return + + if not self.items: + messagebox.showerror("Error", "Please add at least one item") + return + + supplier = next((s for s in self.supplier_service.get_all_suppliers() if s.name == supplier_name), None) + if not supplier: + messagebox.showerror("Error", "Please select a valid supplier") + return + + total = sum(item['total'] for item in self.items) + + # Create purchase order + # Convert string dates to date objects + try: + order_date = datetime.strptime(self.order_date_entry.get(), "%Y-%m-%d").date() + except ValueError: + order_date = datetime.now().date() + + expected_delivery = None + if self.expected_delivery_entry.get(): + try: + expected_delivery = datetime.strptime(self.expected_delivery_entry.get(), "%Y-%m-%d").date() + except ValueError: + pass + + po = self.purchase_service.create_purchase_order( + supplier_id=supplier.id, + order_date=order_date, + expected_delivery=expected_delivery, + total_amount=total + ) + + if po: + # Add items + for item in self.items: + self.purchase_service.add_purchase_order_item( + po_id=po.id, + product_id=item['product'].id, + quantity=item['quantity'], + unit_price=item['price'], + total_price=item['total'] + ) + + messagebox.showinfo("Success", f"Purchase order #{po.id} created successfully") + self.destroy() + else: + messagebox.showerror("Error", "Failed to create purchase order") + + def select_order_date(self): + """Open date picker for order date""" + try: + current_date = datetime.strptime(self.order_date_entry.get(), "%Y-%m-%d").date() + except ValueError: + current_date = date.today() + + dialog = DatePickerDialog(self, "Select Order Date", current_date) + self.wait_window(dialog) + + selected_date = dialog.get_selected_date() + if selected_date: + self.order_date_entry.delete(0, 'end') + self.order_date_entry.insert(0, selected_date.strftime("%Y-%m-%d")) + + def select_expected_delivery(self): + """Open date picker for expected delivery date""" + if self.expected_delivery_entry.get(): + try: + current_date = datetime.strptime(self.expected_delivery_entry.get(), "%Y-%m-%d").date() + except ValueError: + current_date = date.today() + else: + current_date = date.today() + + dialog = DatePickerDialog(self, "Select Expected Delivery Date", current_date) + self.wait_window(dialog) + + selected_date = dialog.get_selected_date() + if selected_date: + self.expected_delivery_entry.delete(0, 'end') + self.expected_delivery_entry.insert(0, selected_date.strftime("%Y-%m-%d")) \ No newline at end of file diff --git a/src/ui/sales_order_dialog.py b/src/ui/sales_order_dialog.py new file mode 100644 index 0000000..b71c63b --- /dev/null +++ b/src/ui/sales_order_dialog.py @@ -0,0 +1,233 @@ +import customtkinter as ctk +from tkinter import messagebox +from datetime import datetime, date +from typing import Optional, List +from src.models import SalesOrder, SalesOrderItem, Product, Customer +from src.services import SalesOrderService, ProductService, CustomerService +from src.ui.date_picker_dialog import DatePickerDialog + +class SalesOrderDialog(ctk.CTkToplevel): + def __init__(self, parent, sales_service: SalesOrderService, + product_service: ProductService, customer_service: CustomerService, + sales_order: Optional[SalesOrder] = None): + super().__init__(parent) + self.sales_service = sales_service + self.product_service = product_service + self.customer_service = customer_service + self.sales_order = sales_order + + self.title("New Sales Order" if not sales_order else "Edit Sales Order") + self.geometry("600x500") + + # Make dialog modal + self.transient(parent) + self.grab_set() + + self.items = [] + self.setup_ui() + + def setup_ui(self): + # Customer selection + ctk.CTkLabel(self, text="Customer:").grid(row=0, column=0, padx=10, pady=5, sticky="w") + self.customer_var = ctk.StringVar() + self.customer_combo = ctk.CTkComboBox(self, variable=self.customer_var, width=200) + self.customer_combo.grid(row=0, column=1, padx=10, pady=5) + + # Order date + ctk.CTkLabel(self, text="Order Date:").grid(row=1, column=0, padx=10, pady=5, sticky="w") + date_frame1 = ctk.CTkFrame(self) + date_frame1.grid(row=1, column=1, padx=10, pady=5, sticky="w") + self.order_date_entry = ctk.CTkEntry(date_frame1, width=150) + self.order_date_entry.insert(0, datetime.now().strftime("%Y-%m-%d")) + self.order_date_entry.pack(side="left") + ctk.CTkButton(date_frame1, text="...", width=30, command=self.select_order_date).pack(side="left", padx=(5, 0)) + + # Expected delivery + ctk.CTkLabel(self, text="Expected Delivery:").grid(row=2, column=0, padx=10, pady=5, sticky="w") + date_frame2 = ctk.CTkFrame(self) + date_frame2.grid(row=2, column=1, padx=10, pady=5, sticky="w") + self.expected_delivery_entry = ctk.CTkEntry(date_frame2, width=150) + self.expected_delivery_entry.pack(side="left") + ctk.CTkButton(date_frame2, text="...", width=30, command=self.select_expected_delivery).pack(side="left", padx=(5, 0)) + + # Items frame + items_frame = ctk.CTkFrame(self) + items_frame.grid(row=3, column=0, columnspan=2, padx=10, pady=10, sticky="nsew") + + # Add item controls + add_frame = ctk.CTkFrame(items_frame) + add_frame.pack(fill="x", padx=5, pady=5) + + ctk.CTkLabel(add_frame, text="Product:").pack(side="left", padx=5) + self.product_var = ctk.StringVar() + self.product_combo = ctk.CTkComboBox(add_frame, variable=self.product_var, width=150) + self.product_combo.pack(side="left", padx=5) + + ctk.CTkLabel(add_frame, text="Qty:").pack(side="left", padx=5) + self.quantity_entry = ctk.CTkEntry(add_frame, width=50) + self.quantity_entry.pack(side="left", padx=5) + + ctk.CTkLabel(add_frame, text="Price:").pack(side="left", padx=5) + self.price_entry = ctk.CTkEntry(add_frame, width=80) + self.price_entry.pack(side="left", padx=5) + + ctk.CTkButton(add_frame, text="Add", command=self.add_item, width=60).pack(side="left", padx=5) + + # Items list + self.items_text = ctk.CTkTextbox(items_frame, height=150) + self.items_text.pack(fill="both", expand=True, padx=5, pady=5) + + # Total + self.total_label = ctk.CTkLabel(self, text="Total: Rp0", font=("Arial", 14, "bold")) + self.total_label.grid(row=4, column=0, columnspan=2, pady=10) + + # Buttons + button_frame = ctk.CTkFrame(self) + button_frame.grid(row=5, column=0, columnspan=2, pady=10) + + ctk.CTkButton(button_frame, text="Save", command=self.save_order).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="Cancel", command=self.destroy).pack(side="left", padx=5) + + # Load data + self.load_customers() + self.load_products() + + def load_customers(self): + customers = self.customer_service.get_all_customers() + customer_names = [c.name for c in customers] + self.customer_combo.configure(values=customer_names) + + def load_products(self): + products = self.product_service.get_all_products() + product_names = [p.name for p in products] + self.product_combo.configure(values=product_names) + + def add_item(self): + product_name = self.product_var.get() + try: + quantity = int(self.quantity_entry.get()) + price = float(self.price_entry.get()) + except ValueError: + messagebox.showerror("Error", "Please enter valid quantity and price") + return + + if quantity <= 0 or price <= 0: + messagebox.showerror("Error", "Quantity and price must be positive") + return + + product = next((p for p in self.product_service.get_all_products() if p.name == product_name), None) + if not product: + messagebox.showerror("Error", "Please select a valid product") + return + + total = quantity * price + self.items.append({ + 'product': product, + 'quantity': quantity, + 'price': price, + 'total': total + }) + + self.update_items_display() + self.update_total() + + # Clear inputs + self.quantity_entry.delete(0, 'end') + self.price_entry.delete(0, 'end') + + def update_items_display(self): + self.items_text.delete("1.0", "end") + for item in self.items: + self.items_text.insert("end", + f"{item['product'].name} - {item['quantity']} x Rp{item['price']:,.0f} = Rp{item['total']:,.0f}\n") + + def update_total(self): + total = sum(item['total'] for item in self.items) + self.total_label.configure(text=f"Total: Rp{total:,.0f}") + + def save_order(self): + customer_name = self.customer_var.get() + if not customer_name: + messagebox.showerror("Error", "Please select a customer") + return + + if not self.items: + messagebox.showerror("Error", "Please add at least one item") + return + + customer = next((c for c in self.customer_service.get_all_customers() if c.name == customer_name), None) + if not customer: + messagebox.showerror("Error", "Please select a valid customer") + return + + total = sum(item['total'] for item in self.items) + + # Create sales order + # Convert string dates to date objects + try: + order_date = datetime.strptime(self.order_date_entry.get(), "%Y-%m-%d").date() + except ValueError: + order_date = datetime.now().date() + + expected_delivery = None + if self.expected_delivery_entry.get(): + try: + expected_delivery = datetime.strptime(self.expected_delivery_entry.get(), "%Y-%m-%d").date() + except ValueError: + pass + + so = self.sales_service.create_sales_order( + customer_id=customer.id, + order_date=order_date, + expected_delivery=expected_delivery, + total_amount=total + ) + + if so: + # Add items + for item in self.items: + self.sales_service.add_sales_order_item( + so_id=so.id, + product_id=item['product'].id, + quantity=item['quantity'], + unit_price=item['price'], + total_price=item['total'] + ) + + messagebox.showinfo("Success", f"Sales order #{so.id} created successfully") + self.destroy() + else: + messagebox.showerror("Error", "Failed to create sales order") + + def select_order_date(self): + """Open date picker for order date""" + try: + current_date = datetime.strptime(self.order_date_entry.get(), "%Y-%m-%d").date() + except ValueError: + current_date = date.today() + + dialog = DatePickerDialog(self, "Select Order Date", current_date) + self.wait_window(dialog) + + selected_date = dialog.get_selected_date() + if selected_date: + self.order_date_entry.delete(0, 'end') + self.order_date_entry.insert(0, selected_date.strftime("%Y-%m-%d")) + + def select_expected_delivery(self): + """Open date picker for expected delivery date""" + if self.expected_delivery_entry.get(): + try: + current_date = datetime.strptime(self.expected_delivery_entry.get(), "%Y-%m-%d").date() + except ValueError: + current_date = date.today() + else: + current_date = date.today() + + dialog = DatePickerDialog(self, "Select Expected Delivery Date", current_date) + self.wait_window(dialog) + + selected_date = dialog.get_selected_date() + if selected_date: + self.expected_delivery_entry.delete(0, 'end') + self.expected_delivery_entry.insert(0, selected_date.strftime("%Y-%m-%d")) \ No newline at end of file diff --git a/src/ui/supplier_dialog.py b/src/ui/supplier_dialog.py new file mode 100644 index 0000000..911e72c --- /dev/null +++ b/src/ui/supplier_dialog.py @@ -0,0 +1,108 @@ +import customtkinter as ctk +from tkinter import messagebox +from typing import Optional +from src.models import Supplier +from src.services import SupplierService + +class SupplierDialog(ctk.CTkToplevel): + def __init__(self, parent, supplier_service: SupplierService, + supplier: Optional[Supplier] = None): + super().__init__(parent) + self.supplier_service = supplier_service + self.supplier = supplier + + self.title("New Supplier" if not supplier else "Edit Supplier") + self.geometry("400x350") + + # Make dialog modal + self.transient(parent) + self.grab_set() + + self.setup_ui() + + def setup_ui(self): + # Name + ctk.CTkLabel(self, text="Name:").grid(row=0, column=0, padx=10, pady=5, sticky="w") + self.name_entry = ctk.CTkEntry(self, width=250) + self.name_entry.grid(row=0, column=1, padx=10, pady=5) + + # Email + ctk.CTkLabel(self, text="Email:").grid(row=1, column=0, padx=10, pady=5, sticky="w") + self.email_entry = ctk.CTkEntry(self, width=250) + self.email_entry.grid(row=1, column=1, padx=10, pady=5) + + # Phone + ctk.CTkLabel(self, text="Phone:").grid(row=2, column=0, padx=10, pady=5, sticky="w") + self.phone_entry = ctk.CTkEntry(self, width=250) + self.phone_entry.grid(row=2, column=1, padx=10, pady=5) + + # Contact Person + ctk.CTkLabel(self, text="Contact Person:").grid(row=3, column=0, padx=10, pady=5, sticky="w") + self.contact_person_entry = ctk.CTkEntry(self, width=250) + self.contact_person_entry.grid(row=3, column=1, padx=10, pady=5) + + # Address + ctk.CTkLabel(self, text="Address:").grid(row=4, column=0, padx=10, pady=5, sticky="nw") + self.address_text = ctk.CTkTextbox(self, height=80, width=250) + self.address_text.grid(row=4, column=1, padx=10, pady=5) + + # Buttons + button_frame = ctk.CTkFrame(self) + button_frame.grid(row=5, column=0, columnspan=2, pady=20) + + ctk.CTkButton(button_frame, text="Save", command=self.save_supplier).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="Cancel", command=self.destroy).pack(side="left", padx=5) + + # Load data if editing + if self.supplier: + self.load_supplier_data() + + def load_supplier_data(self): + self.name_entry.insert(0, self.supplier.name) + self.email_entry.insert(0, self.supplier.email or "") + self.phone_entry.insert(0, self.supplier.phone or "") + self.contact_person_entry.insert(0, self.supplier.contact_person or "") + self.address_text.insert("1.0", self.supplier.address or "") + + def save_supplier(self): + name = self.name_entry.get().strip() + if not name: + messagebox.showerror("Error", "Please enter supplier name") + return + + email = self.email_entry.get().strip() + phone = self.phone_entry.get().strip() + contact_person = self.contact_person_entry.get().strip() + address = self.address_text.get("1.0", "end-1c").strip() + + if self.supplier: + # Update existing supplier + updated = self.supplier_service.update_supplier( + supplier_id=self.supplier.id, + name=name, + contact_person=contact_person or None, + email=email or None, + phone=phone or None, + address=address or None + ) + + if updated: + messagebox.showinfo("Success", "Supplier updated successfully") + self.destroy() + else: + messagebox.showerror("Error", "Failed to update supplier") + else: + # Create new supplier + supplier = self.supplier_service.create_supplier( + name=name, + contact_person=contact_person or None, + email=email or None, + phone=phone or None, + address=address or None + ) + + if supplier: + messagebox.showinfo("Success", f"Supplier '{supplier.name}' created successfully") + self.destroy() + else: + messagebox.showerror("Error", "Failed to create supplier") \ No newline at end of file diff --git a/src/ui/user_management_dialog.py b/src/ui/user_management_dialog.py new file mode 100644 index 0000000..60dc6f9 --- /dev/null +++ b/src/ui/user_management_dialog.py @@ -0,0 +1,460 @@ +import customtkinter as ctk +from tkinter import messagebox +from typing import Optional +from src.auth import AuthManager + + +class UserManagementDialog(ctk.CTkToplevel): + def __init__(self, parent, auth_manager: AuthManager): + super().__init__(parent) + self.auth_manager = auth_manager + self.selected_user = None + self.selected_groups = [] + + self.title("User Management") + self.geometry("800x600") + + # Make dialog modal + self.transient(parent) + self.grab_set() + + self.setup_ui() + self.load_users() + self.load_groups() + + def setup_ui(self): + # Title + ctk.CTkLabel(self, text="User Management", font=("Arial", 16, "bold")).pack(pady=10) + + # Main frame + main_frame = ctk.CTkFrame(self) + main_frame.pack(fill="both", expand=True, padx=20, pady=10) + + # Users list frame + users_frame = ctk.CTkFrame(main_frame) + users_frame.pack(side="left", fill="both", expand=True, padx=(0, 10)) + + ctk.CTkLabel(users_frame, text="Users", font=("Arial", 14, "bold")).pack(pady=5) + + # Users list + self.users_listbox = ctk.CTkScrollableFrame(users_frame, height=300) + self.users_listbox.pack(fill="both", expand=True, padx=10, pady=10) + + # User buttons + user_buttons_frame = ctk.CTkFrame(users_frame) + user_buttons_frame.pack(fill="x", padx=10, pady=5) + + ctk.CTkButton(user_buttons_frame, text="Add User", command=self.add_user).pack(side="left", padx=5) + ctk.CTkButton(user_buttons_frame, text="Edit User", command=self.edit_user).pack(side="left", padx=5) + ctk.CTkButton(user_buttons_frame, text="Delete User", command=self.delete_user).pack(side="left", padx=5) + + # Groups list frame + groups_frame = ctk.CTkFrame(main_frame) + groups_frame.pack(side="right", fill="both", expand=True, padx=(10, 0)) + + ctk.CTkLabel(groups_frame, text="Groups", font=("Arial", 14, "bold")).pack(pady=5) + + # Groups list + self.groups_listbox = ctk.CTkScrollableFrame(groups_frame, height=300) + self.groups_listbox.pack(fill="both", expand=True, padx=10, pady=10) + + # Group buttons + group_buttons_frame = ctk.CTkFrame(groups_frame) + group_buttons_frame.pack(fill="x", padx=10, pady=5) + + ctk.CTkButton(group_buttons_frame, text="Add Group", command=self.add_group).pack(side="left", padx=5) + ctk.CTkButton(group_buttons_frame, text="Edit Group", command=self.edit_group).pack(side="left", padx=5) + ctk.CTkButton(group_buttons_frame, text="Delete Group", command=self.delete_group).pack(side="left", padx=5) + + # Assign/Unassign buttons + assign_frame = ctk.CTkFrame(self) + assign_frame.pack(fill="x", padx=20, pady=10) + + ctk.CTkButton(assign_frame, text="Assign to Group", command=self.assign_to_group).pack(side="left", padx=5) + ctk.CTkButton(assign_frame, text="Unassign from Group", command=self.unassign_from_group).pack(side="left", padx=5) + + # Close button + ctk.CTkButton(self, text="Close", command=self.destroy).pack(pady=10) + + def load_users(self): + # Clear existing widgets + for widget in self.users_listbox.winfo_children(): + widget.destroy() + + users = self.auth_manager.get_all_users() + self.user_widgets = [] + + for user in users: + user_frame = ctk.CTkFrame(self.users_listbox) + user_frame.pack(fill="x", padx=5, pady=2) + + # Get user groups + user_groups = self.auth_manager.get_user_groups(user['id']) + group_names = [group['name'] for group in user_groups] + groups_text = f" ({', '.join(group_names)})" if group_names else " (No groups)" + admin_text = " (Admin)" if user['is_admin'] else "" + + user_label = ctk.CTkLabel(user_frame, text=f"{user['username']}{admin_text}{groups_text}", wraplength=250, anchor="w") + user_label.pack(side="left", padx=10, pady=5) + + # Bind click event + user_frame.bind("", lambda e, u=user: self.select_user(u)) + user_label.bind("", lambda e, u=user: self.select_user(u)) + + self.user_widgets.append((user_frame, user_label, user)) + + def load_groups(self): + # Clear existing widgets + for widget in self.groups_listbox.winfo_children(): + widget.destroy() + + groups = self.auth_manager.get_all_groups() + self.group_widgets = [] + + for group in groups: + group_frame = ctk.CTkFrame(self.groups_listbox) + group_frame.pack(fill="x", padx=5, pady=2) + + group_label = ctk.CTkLabel(group_frame, text=group['name']) + group_label.pack(side="left", padx=10, pady=5) + + # Bind click event + group_frame.bind("", lambda e, g=group: self.select_group(g)) + group_label.bind("", lambda e, g=group: self.select_group(g)) + + self.group_widgets.append((group_frame, group_label, group)) + + def select_user(self, user): + self.selected_user = user + # Update visual selection + for widget_frame, widget_label, widget_user in self.user_widgets: + if widget_user == user: + widget_frame.configure(fg_color="blue") + widget_label.configure(text_color="white") + else: + widget_frame.configure(fg_color="transparent") + widget_label.configure(text_color="black") + + def refresh_user_list(self): + """Refresh the user list to show updated group memberships""" + self.load_users() + + def select_group(self, group): + self.selected_group = group + # Update visual selection + for widget_frame, widget_label, widget_group in self.group_widgets: + if widget_group == group: + widget_frame.configure(fg_color="blue") + widget_label.configure(text_color="white") + else: + widget_frame.configure(fg_color="transparent") + widget_label.configure(text_color="black") + + def add_user(self): + dialog = AddUserDialog(self, self.auth_manager) + self.wait_window(dialog) + self.load_users() + + def edit_user(self): + if not self.selected_user: + messagebox.showerror("Error", "Please select a user to edit") + return + + dialog = EditUserDialog(self, self.auth_manager, self.selected_user) + self.wait_window(dialog) + self.load_users() + + def delete_user(self): + if not self.selected_user: + messagebox.showerror("Error", "Please select a user to delete") + return + + if messagebox.askyesno("Confirm Delete", f"Are you sure you want to delete user '{self.selected_user['username']}'?"): + if self.auth_manager.delete_user(self.selected_user['id']): + messagebox.showinfo("Success", f"User '{self.selected_user['username']}' deleted successfully") + self.selected_user = None + self.load_users() + else: + messagebox.showerror("Error", "Failed to delete user") + + def add_group(self): + dialog = AddGroupDialog(self, self.auth_manager) + self.wait_window(dialog) + self.load_groups() + + def edit_group(self): + if not hasattr(self, 'selected_group'): + messagebox.showerror("Error", "Please select a group to edit") + return + + dialog = EditGroupDialog(self, self.auth_manager, self.selected_group) + self.wait_window(dialog) + self.load_groups() + + def delete_group(self): + if not hasattr(self, 'selected_group'): + messagebox.showerror("Error", "Please select a group to delete") + return + + if messagebox.askyesno("Confirm Delete", f"Are you sure you want to delete group '{self.selected_group['name']}'?"): + if self.auth_manager.delete_group(self.selected_group['id']): + messagebox.showinfo("Success", f"Group '{self.selected_group['name']}' deleted successfully") + delattr(self, 'selected_group') + self.load_groups() + else: + messagebox.showerror("Error", "Failed to delete group") + + def assign_to_group(self): + if not self.selected_user: + messagebox.showerror("Error", "Please select a user") + return + + if not hasattr(self, 'selected_group'): + messagebox.showerror("Error", "Please select a group") + return + + if self.auth_manager.add_user_to_group(self.selected_user['id'], self.selected_group['id']): + messagebox.showinfo("Success", f"User '{self.selected_user['username']}' assigned to group '{self.selected_group['name']}'") + self.refresh_user_list() + else: + messagebox.showerror("Error", "Failed to assign user to group") + + def unassign_from_group(self): + if not self.selected_user: + messagebox.showerror("Error", "Please select a user") + return + + if not hasattr(self, 'selected_group'): + messagebox.showerror("Error", "Please select a group") + return + + if self.auth_manager.remove_user_from_group(self.selected_user['id'], self.selected_group['id']): + messagebox.showinfo("Success", f"User '{self.selected_user['username']}' unassigned from group '{self.selected_group['name']}'") + self.refresh_user_list() + else: + messagebox.showerror("Error", "Failed to unassign user from group") + + +class AddUserDialog(ctk.CTkToplevel): + def __init__(self, parent, auth_manager: AuthManager): + super().__init__(parent) + self.auth_manager = auth_manager + + self.title("Add User") + self.geometry("400x300") + + # Make dialog modal + self.transient(parent) + self.grab_set() + + self.setup_ui() + + def setup_ui(self): + # Title + ctk.CTkLabel(self, text="Add New User", font=("Arial", 16, "bold")).pack(pady=10) + + # Username + ctk.CTkLabel(self, text="Username:").pack(anchor="w", padx=20, pady=(10, 0)) + self.username_entry = ctk.CTkEntry(self, width=300) + self.username_entry.pack(pady=5) + + # Password + ctk.CTkLabel(self, text="Password:").pack(anchor="w", padx=20, pady=(10, 0)) + self.password_entry = ctk.CTkEntry(self, width=300, show="*") + self.password_entry.pack(pady=5) + + # Confirm Password + ctk.CTkLabel(self, text="Confirm Password:").pack(anchor="w", padx=20, pady=(10, 0)) + self.confirm_password_entry = ctk.CTkEntry(self, width=300, show="*") + self.confirm_password_entry.pack(pady=5) + + # Admin checkbox + self.is_admin_var = ctk.BooleanVar() + ctk.CTkCheckBox(self, text="Admin User", variable=self.is_admin_var).pack(pady=10) + + # Buttons + button_frame = ctk.CTkFrame(self) + button_frame.pack(pady=20) + + ctk.CTkButton(button_frame, text="Save", command=self.save_user).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="Cancel", command=self.destroy).pack(side="left", padx=5) + + def save_user(self): + username = self.username_entry.get().strip() + password = self.password_entry.get() + confirm_password = self.confirm_password_entry.get() + is_admin = self.is_admin_var.get() + + if not username: + messagebox.showerror("Error", "Please enter a username") + return + + if not password: + messagebox.showerror("Error", "Please enter a password") + return + + if password != confirm_password: + messagebox.showerror("Error", "Passwords do not match") + return + + try: + self.auth_manager.create_user(username, password, is_admin) + messagebox.showinfo("Success", f"User '{username}' created successfully") + self.destroy() + except Exception as e: + messagebox.showerror("Error", f"Failed to create user: {str(e)}") + + +class EditUserDialog(ctk.CTkToplevel): + def __init__(self, parent, auth_manager: AuthManager, user): + super().__init__(parent) + self.auth_manager = auth_manager + self.user = user + + self.title("Edit User") + self.geometry("400x250") + + # Make dialog modal + self.transient(parent) + self.grab_set() + + self.setup_ui() + + def setup_ui(self): + # Title + ctk.CTkLabel(self, text="Edit User", font=("Arial", 16, "bold")).pack(pady=10) + + # Username + ctk.CTkLabel(self, text="Username:").pack(anchor="w", padx=20, pady=(10, 0)) + self.username_entry = ctk.CTkEntry(self, width=300) + self.username_entry.insert(0, self.user['username']) + self.username_entry.pack(pady=5) + + # Admin checkbox + self.is_admin_var = ctk.BooleanVar(value=self.user['is_admin']) + ctk.CTkCheckBox(self, text="Admin User", variable=self.is_admin_var).pack(pady=10) + + # Buttons + button_frame = ctk.CTkFrame(self) + button_frame.pack(pady=20) + + ctk.CTkButton(button_frame, text="Save", command=self.save_user).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="Cancel", command=self.destroy).pack(side="left", padx=5) + + def save_user(self): + username = self.username_entry.get().strip() + is_admin = self.is_admin_var.get() + + if not username: + messagebox.showerror("Error", "Please enter a username") + return + + # Note: In a real application, you would update the user in the database + # For now, we'll just show a message + messagebox.showinfo("Info", "User update functionality would be implemented in a full application") + self.destroy() + + +class AddGroupDialog(ctk.CTkToplevel): + def __init__(self, parent, auth_manager: AuthManager): + super().__init__(parent) + self.auth_manager = auth_manager + + self.title("Add Group") + self.geometry("400x300") + + # Make dialog modal + self.transient(parent) + self.grab_set() + + self.setup_ui() + + def setup_ui(self): + # Title + ctk.CTkLabel(self, text="Add New Group", font=("Arial", 16, "bold")).pack(pady=10) + + # Group name + ctk.CTkLabel(self, text="Group Name:").pack(anchor="w", padx=20, pady=(10, 0)) + self.name_entry = ctk.CTkEntry(self, width=300) + self.name_entry.pack(pady=5) + + # Permissions + ctk.CTkLabel(self, text="Permissions (comma separated):").pack(anchor="w", padx=20, pady=(10, 0)) + self.permissions_entry = ctk.CTkEntry(self, width=300) + self.permissions_entry.pack(pady=5) + + # Buttons + button_frame = ctk.CTkFrame(self) + button_frame.pack(pady=20) + + ctk.CTkButton(button_frame, text="Save", command=self.save_group).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="Cancel", command=self.destroy).pack(side="left", padx=5) + + def save_group(self): + name = self.name_entry.get().strip() + permissions = self.permissions_entry.get().strip() + + if not name: + messagebox.showerror("Error", "Please enter a group name") + return + + permissions_list = [p.strip() for p in permissions.split(",")] if permissions else [] + + if self.auth_manager.create_group(name, permissions_list): + messagebox.showinfo("Success", f"Group '{name}' created successfully") + self.destroy() + else: + messagebox.showerror("Error", "Failed to create group") + + +class EditGroupDialog(ctk.CTkToplevel): + def __init__(self, parent, auth_manager: AuthManager, group): + super().__init__(parent) + self.auth_manager = auth_manager + self.group = group + + self.title("Edit Group") + self.geometry("400x300") + + # Make dialog modal + self.transient(parent) + self.grab_set() + + self.setup_ui() + + def setup_ui(self): + # Title + ctk.CTkLabel(self, text="Edit Group", font=("Arial", 16, "bold")).pack(pady=10) + + # Group name + ctk.CTkLabel(self, text="Group Name:").pack(anchor="w", padx=20, pady=(10, 0)) + self.name_entry = ctk.CTkEntry(self, width=300) + self.name_entry.insert(0, self.group['name']) + self.name_entry.pack(pady=5) + + # Permissions + ctk.CTkLabel(self, text="Permissions (comma separated):").pack(anchor="w", padx=20, pady=(10, 0)) + permissions_str = ",".join(self.group['permissions']) if self.group['permissions'] else "" + self.permissions_entry = ctk.CTkEntry(self, width=300) + self.permissions_entry.insert(0, permissions_str) + self.permissions_entry.pack(pady=5) + + # Buttons + button_frame = ctk.CTkFrame(self) + button_frame.pack(pady=20) + + ctk.CTkButton(button_frame, text="Save", command=self.save_group).pack(side="left", padx=5) + ctk.CTkButton(button_frame, text="Cancel", command=self.destroy).pack(side="left", padx=5) + + def save_group(self): + name = self.name_entry.get().strip() + permissions = self.permissions_entry.get().strip() + + if not name: + messagebox.showerror("Error", "Please enter a group name") + return + + # Note: In a real application, you would update the group in the database + # For now, we'll just show a message + messagebox.showinfo("Info", "Group update functionality would be implemented in a full application") + self.destroy() \ No newline at end of file diff --git a/test_export.py b/test_export.py new file mode 100644 index 0000000..7eea227 --- /dev/null +++ b/test_export.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +""" +Test script to verify Excel export functionality +""" + +import os +import sys +from datetime import datetime + +# Add src to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) + +from database import DatabaseManager +from services import ( + ProductService, SupplierService, CustomerService, + PurchaseOrderService, ManufacturingOrderService, + SalesOrderService, InventoryService +) + +def test_excel_exports(): + """Test all Excel export functionality""" + print("Testing Excel Export Functionality...") + + # Initialize database + db_manager = DatabaseManager() + + # Initialize services + product_service = ProductService(db_manager) + supplier_service = SupplierService(db_manager) + customer_service = CustomerService(db_manager) + purchase_service = PurchaseOrderService(db_manager) + manufacturing_service = ManufacturingOrderService(db_manager) + sales_service = SalesOrderService(db_manager) + inventory_service = InventoryService(db_manager) + + # Create test data + print("Creating test data...") + + # Create a product + product = product_service.create_product("Test Product", "Test Description", 100.0) + if product: + print(f"Created product: {product.name}") + + # Create a supplier + supplier = supplier_service.create_supplier("Test Supplier", "John Doe", "1234567890") + if supplier: + print(f"Created supplier: {supplier.name}") + + # Create a customer + customer = customer_service.create_customer("Test Customer", "Jane Doe", "0987654321") + if customer: + print(f"Created customer: {customer.name}") + + # Create inventory record + inventory = inventory_service.create_inventory_record(product.id, 50) + if inventory: + print(f"Created inventory record for product {product.id}") + + # Create purchase order + po = purchase_service.create_purchase_order(supplier.id) + if po: + print(f"Created purchase order: {po.id}") + + # Create manufacturing order + mo = manufacturing_service.create_manufacturing_order(product.id, 10) + if mo: + print(f"Created manufacturing order: {mo.id}") + + # Create sales order + so = sales_service.create_sales_order(customer.id) + if so: + print(f"Created sales order: {so.id}") + + # Test exports + print("\nTesting Excel exports...") + + try: + # Test purchase order export + po_filename = purchase_service.export_to_excel("test_purchase_orders.xlsx") + print(f"[OK] Purchase orders exported to: {po_filename}") + + # Test manufacturing order export + mo_filename = manufacturing_service.export_to_excel("test_manufacturing_orders.xlsx") + print(f"[OK] Manufacturing orders exported to: {mo_filename}") + + # Test sales order export + so_filename = sales_service.export_to_excel("test_sales_orders.xlsx") + print(f"[OK] Sales orders exported to: {so_filename}") + + # Test inventory export + inv_filename = inventory_service.export_to_excel("test_inventory.xlsx") + print(f"[OK] Inventory exported to: {inv_filename}") + + # Test stock movements export + sm_filename = inventory_service.export_stock_movements_to_excel("test_stock_movements.xlsx") + print(f"[OK] Stock movements exported to: {sm_filename}") + + print("\n[SUCCESS] All Excel export tests passed!") + + # Clean up test files + test_files = [po_filename, mo_filename, so_filename, inv_filename, sm_filename] + for file in test_files: + if os.path.exists(file): + os.remove(file) + print(f"Cleaned up: {file}") + + except Exception as e: + print(f"[ERROR] Error during export: {str(e)}") + return False + + return True + +if __name__ == "__main__": + success = test_excel_exports() + if success: + print("\n[SUCCESS] All tests completed successfully!") + else: + print("\n[ERROR] Some tests failed!") + sys.exit(1) \ No newline at end of file diff --git a/test_ui_and_exports.py b/test_ui_and_exports.py new file mode 100644 index 0000000..40a8d18 --- /dev/null +++ b/test_ui_and_exports.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +""" +Test script to verify the revamped UI and export features +""" + +import os +import sys +import subprocess +import time +import threading +from pathlib import Path + +def test_dependencies(): + """Test that all required dependencies are installed""" + print("Testing dependencies...") + + try: + import customtkinter + print("[OK] customtkinter installed") + except ImportError: + print("[FAIL] customtkinter not installed") + return False + + try: + import pandas + print("[OK] pandas installed") + except ImportError: + print("[FAIL] pandas not installed") + return False + + try: + import openpyxl + print("[OK] openpyxl installed") + except ImportError: + print("[FAIL] openpyxl not installed") + return False + + try: + import sqlalchemy + print("[OK] sqlalchemy installed") + except ImportError: + print("[FAIL] sqlalchemy not installed") + return False + + try: + import bcrypt + print("[OK] bcrypt installed") + except ImportError: + print("[FAIL] bcrypt not installed") + return False + + return True + +def test_exports(): + """Test Excel export functionality""" + print("\nTesting Excel export functionality...") + + # Import and run the existing test_export.py + try: + from test_export import test_excel_exports + test_excel_exports() + print("[OK] All Excel export tests passed") + return True + except Exception as e: + print(f"[FAIL] Excel export tests failed: {e}") + return False + +def test_ui_startup(): + """Test that the UI can start without errors""" + print("\nTesting UI startup...") + + try: + # Test that we can import the main modules + from src.app import ManufacturingApp + from src.ui.main_window import MainWindow + print("[OK] All UI modules import successfully") + + # Test database initialization + from src.database import DatabaseManager + db_manager = DatabaseManager() + db_manager.initialize_database() + print("[OK] Database initialization successful") + + return True + except Exception as e: + print(f"✗ UI startup test failed: {e}") + return False + +def main(): + """Run all tests""" + print("=== Testing Revamped Manufacturing App ===\n") + + success = True + success &= test_dependencies() + success &= test_ui_startup() + success &= test_exports() + + if success: + print("\n[SUCCESS] All tests passed! The revamped UI and export features are working correctly.") + else: + print("\n[ERROR] Some tests failed. Please check the output above.") + + return success + +if __name__ == "__main__": + success = main() + sys.exit(0 if success else 1) \ No newline at end of file