improved changes

This commit is contained in:
2025-11-26 20:01:52 +05:30
parent e5c5044ff3
commit 5ad65c3c59
68 changed files with 12520 additions and 827 deletions

View File

@ -53,6 +53,7 @@ def create_app():
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_ECHO'] = os.getenv('SQLALCHEMY_ECHO', 'False').lower() == 'true'
# Initialize extensions
db.init_app(app)

View File

@ -4,7 +4,35 @@ import time
import json
from werkzeug.security import generate_password_hash, check_password_hash
# Machine Model
class RefillerMachine(db.Model):
__tablename__ = 'refiller_machines'
id = db.Column(db.Integer, primary_key=True)
refiller_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'), nullable=False)
machine_id = db.Column(db.String(10), db.ForeignKey('machines.machine_id', ondelete='CASCADE'), nullable=False)
assigned_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
assigned_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
# Relationships
assigner = db.relationship('User', foreign_keys=[assigned_by], backref='machine_assignments_made')
# Unique constraint: one refiller can't be assigned to same machine twice
__table_args__ = (
db.UniqueConstraint('refiller_id', 'machine_id', name='unique_refiller_machine'),
)
def to_dict(self):
return {
'id': self.id,
'refiller_id': self.refiller_id,
'machine_id': self.machine_id,
'assigned_at': self.assigned_at.strftime("%Y-%m-%d %H:%M:%S") if self.assigned_at else None,
'assigned_by': self.assigned_by,
'assigned_by_username': self.assigner.username if self.assigner else None
}
# Machine Model - UPDATED
class Machine(db.Model):
__tablename__ = 'machines'
@ -20,10 +48,37 @@ class Machine(db.Model):
connection_status = db.Column(db.String(50), nullable=False)
created_on = db.Column(db.String(20), nullable=False)
password = db.Column(db.String(128), nullable=False)
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
# Relationships
slots = db.relationship('VendingSlot', backref='machine', lazy=True)
client = db.relationship('User', backref='machines')
client = db.relationship('User', foreign_keys=[client_id], backref='client_machines')
creator = db.relationship('User', foreign_keys=[created_by], backref='created_machines')
# ⭐ NEW: Many-to-many relationship with Refillers through RefillerMachine
assigned_refillers = db.relationship(
'User',
secondary='refiller_machines',
primaryjoin='Machine.machine_id == RefillerMachine.machine_id',
secondaryjoin='and_(User.id == RefillerMachine.refiller_id, User.roles == "Refiller")',
backref='assigned_machines_rel',
viewonly=True
)
def to_dict(self):
# Get assigned refillers
refiller_assignments = RefillerMachine.query.filter_by(machine_id=self.machine_id).all()
assigned_refillers = []
for assignment in refiller_assignments:
refiller = User.query.get(assignment.refiller_id)
if refiller:
assigned_refillers.append({
'id': refiller.id,
'username': refiller.username,
'email': refiller.email,
'assigned_at': assignment.assigned_at.strftime("%Y-%m-%d %H:%M:%S") if assignment.assigned_at else None
})
return {
'id': self.id,
'machine_id': self.machine_id,
@ -36,7 +91,10 @@ class Machine(db.Model):
'operation_status': self.operation_status,
'connection_status': self.connection_status,
'created_on': self.created_on,
'password': self.password
'password': self.password,
'created_by': self.created_by,
'created_by_username': self.creator.username if self.creator else None,
'assigned_refillers': assigned_refillers # ⭐ NEW
}
def set_password(self, password):
@ -46,7 +104,7 @@ class Machine(db.Model):
return check_password_hash(self.password, password)
# User Model - UPDATED with proper password hashing
# User Model - UPDATED WITH BOTH CLIENT AND MACHINE ASSIGNMENTS
class User(db.Model):
__tablename__ = 'users'
@ -57,7 +115,9 @@ class User(db.Model):
contact = db.Column(db.String(20), nullable=False)
roles = db.Column(db.String(50), nullable=False)
user_status = db.Column(db.String(50), nullable=False)
password = db.Column(db.String(255), nullable=False) # Increased length for hash
password = db.Column(db.String(255), nullable=False)
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
assigned_to = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) # Assigned to Client
# File storage fields
photo = db.Column(db.String(255), nullable=True)
@ -67,9 +127,27 @@ class User(db.Model):
# Timestamps
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
# Relationships
creator = db.relationship('User', remote_side=[id], backref='created_users', foreign_keys=[created_by])
assigned_client = db.relationship('User', remote_side=[id], backref='assigned_refillers', foreign_keys=[assigned_to])
def to_dict(self):
"""Convert user object to dictionary"""
# Get assigned machines for Refillers
assigned_machines = []
if self.roles == 'Refiller':
machine_assignments = RefillerMachine.query.filter_by(refiller_id=self.id).all()
for assignment in machine_assignments:
machine = Machine.query.filter_by(machine_id=assignment.machine_id).first()
if machine:
assigned_machines.append({
'machine_id': machine.machine_id,
'machine_model': machine.machine_model,
'branch_name': machine.branch_name,
'assigned_at': assignment.assigned_at.strftime("%Y-%m-%d %H:%M:%S") if assignment.assigned_at else None
})
return {
'id': self.id,
'user_id': self.user_id,
@ -83,7 +161,12 @@ class User(db.Model):
'documents': json.loads(self.documents) if self.documents else [],
'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S") if self.created_at else None,
'updated_at': self.updated_at.strftime("%Y-%m-%d %H:%M:%S") if self.updated_at else None,
'machines': [m.to_dict() for m in self.machines] if hasattr(self, 'machines') else []
'created_by': self.created_by,
'created_by_username': self.creator.username if self.creator else None,
'assigned_to': self.assigned_to,
'assigned_to_username': self.assigned_client.username if self.assigned_client else None,
'assigned_machines': assigned_machines, # ⭐ NEW
'assigned_machines_count': len(assigned_machines) # ⭐ NEW
}
def set_password(self, password):
@ -125,6 +208,13 @@ class Product(db.Model):
price = db.Column(db.Float, nullable=False)
product_image = db.Column(db.String(255), nullable=False)
created_date = db.Column(db.String(20), nullable=False)
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
# NEW: Billing and expiration dates
billing_date = db.Column(db.DateTime, nullable=True)
expiration_date = db.Column(db.DateTime, nullable=True)
# Relationship
creator = db.relationship('User', backref='created_products')
def to_dict(self):
return {
@ -133,7 +223,11 @@ class Product(db.Model):
'product_name': self.product_name,
'price': str(self.price),
'product_image': self.product_image,
'created_date': self.created_date
'created_date': self.created_date,
'billing_date': self.billing_date.strftime("%Y-%m-%d") if self.billing_date else None,
'expiration_date': self.expiration_date.strftime("%Y-%m-%d") if self.expiration_date else None,
'created_by': self.created_by,
'created_by_username': self.creator.username if self.creator else None
}
@ -191,7 +285,9 @@ class Transaction(db.Model):
'return_amount': self.return_amount,
'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S") if self.created_at else None
}
# Add to your models.py
# Role Model
class Role(db.Model):
__tablename__ = 'roles'
@ -199,15 +295,260 @@ class Role(db.Model):
name = db.Column(db.String(50), unique=True, nullable=False)
description = db.Column(db.String(255))
permissions = db.Column(db.Text) # JSON string of permission IDs
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
# Relationship
creator = db.relationship('User', backref='created_roles')
def to_dict(self):
return {
'id': self.id,
'name': self.name,
'description': self.description,
'permissions': json.loads(self.permissions) if self.permissions else [],
'created_by': self.created_by,
'created_by_username': self.creator.username if self.creator else None,
'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S") if self.created_at else None,
'updated_at': self.updated_at.strftime("%Y-%m-%d %H:%M:%S") if self.updated_at else None
}
}
# Branch Model
class Branch(db.Model):
__tablename__ = 'branches'
id = db.Column(db.Integer, primary_key=True)
branch_id = db.Column(db.String(50), unique=True, nullable=False)
code = db.Column(db.String(20), unique=True, nullable=False)
name = db.Column(db.String(100), nullable=False)
location = db.Column(db.String(100), nullable=False)
address = db.Column(db.Text, nullable=False)
contact = db.Column(db.String(20), nullable=False)
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
# Relationship
creator = db.relationship('User', backref='created_branches')
def to_dict(self):
return {
'id': self.id,
'branch_id': self.branch_id,
'code': self.code,
'name': self.name,
'location': self.location,
'address': self.address,
'contact': self.contact,
'created_by': self.created_by,
'created_by_username': self.creator.username if self.creator else None,
'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S") if self.created_at else None,
'updated_at': self.updated_at.strftime("%Y-%m-%d %H:%M:%S") if self.updated_at else None
}
@staticmethod
def generate_branch_id():
"""Generate unique branch ID with BR prefix"""
import uuid
while True:
branch_id = f"BR{uuid.uuid4().hex[:8].upper()}"
if not Branch.query.filter_by(branch_id=branch_id).first():
return branch_id
# Brand Model
class Brand(db.Model):
__tablename__ = 'brands'
id = db.Column(db.Integer, primary_key=True)
brand_id = db.Column(db.String(50), unique=True, nullable=False)
name = db.Column(db.String(100), nullable=False)
branch_id = db.Column(db.String(50), db.ForeignKey('branches.branch_id'), nullable=False)
branch_name = db.Column(db.String(100), nullable=False)
image = db.Column(db.String(255), nullable=True)
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
# Relationships
branch = db.relationship('Branch', backref='brands', foreign_keys=[branch_id])
creator = db.relationship('User', backref='created_brands')
def to_dict(self):
return {
'id': self.id,
'brand_id': self.brand_id,
'name': self.name,
'branch_id': self.branch_id,
'branch_name': self.branch_name,
'image': self.image,
'created_by': self.created_by,
'created_by_username': self.creator.username if self.creator else None,
'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S") if self.created_at else None,
'updated_at': self.updated_at.strftime("%Y-%m-%d %H:%M:%S") if self.updated_at else None
}
@staticmethod
def generate_brand_id(name):
"""
Generate brand ID from first 3 letters of name + 4-digit sequence
Example: CocaCola -> COC0001, COC0002, etc.
"""
import re
# Extract only letters from name and get first 3
letters_only = ''.join(filter(str.isalpha, name)).upper()
prefix = letters_only[:3].ljust(3, 'X') # Pad with X if less than 3 letters
# Find the highest existing sequence number for this prefix
existing_brands = Brand.query.filter(
Brand.brand_id.like(f"{prefix}%")
).all()
if not existing_brands:
sequence = 1
else:
# Extract sequence numbers and find max
sequences = []
for brand in existing_brands:
try:
seq_part = brand.brand_id[3:] # Get part after prefix
if seq_part.isdigit():
sequences.append(int(seq_part))
except:
continue
sequence = max(sequences) + 1 if sequences else 1
# Format: PREFIX + 4-digit sequence
brand_id = f"{prefix}{sequence:04d}"
return brand_id
# Category Model
class Category(db.Model):
__tablename__ = 'categories'
id = db.Column(db.Integer, primary_key=True)
category_id = db.Column(db.String(50), unique=True, nullable=False)
name = db.Column(db.String(100), nullable=False)
image = db.Column(db.String(255), nullable=True)
brand_id = db.Column(db.String(50), db.ForeignKey('brands.brand_id'), nullable=False)
brand_name = db.Column(db.String(100), nullable=False)
branch_id = db.Column(db.String(50), db.ForeignKey('branches.branch_id'), nullable=False)
branch_name = db.Column(db.String(100), nullable=False)
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
# Relationships
brand = db.relationship('Brand', backref='categories', foreign_keys=[brand_id])
branch = db.relationship('Branch', backref='categories', foreign_keys=[branch_id])
creator = db.relationship('User', backref='created_categories')
def to_dict(self):
return {
'id': self.id,
'category_id': self.category_id,
'name': self.name,
'image': self.image,
'brand_id': self.brand_id,
'brand_name': self.brand_name,
'branch_id': self.branch_id,
'branch_name': self.branch_name,
'created_by': self.created_by,
'created_by_username': self.creator.username if self.creator else None,
'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S") if self.created_at else None,
'updated_at': self.updated_at.strftime("%Y-%m-%d %H:%M:%S") if self.updated_at else None
}
@staticmethod
def generate_category_id(name):
"""
Generate category ID from first 3 letters of name + 4-digit sequence
Example: Beverages -> BEV0001, BEV0002, etc.
"""
import re
# Extract only letters from name and get first 3
letters_only = ''.join(filter(str.isalpha, name)).upper()
prefix = letters_only[:3].ljust(3, 'X') # Pad with X if less than 3 letters
# Find the highest existing sequence number for this prefix
existing_categories = Category.query.filter(
Category.category_id.like(f"{prefix}%")
).all()
if not existing_categories:
sequence = 1
else:
# Extract sequence numbers and find max
sequences = []
for category in existing_categories:
try:
seq_part = category.category_id[3:] # Get part after prefix
if seq_part.isdigit():
sequences.append(int(seq_part))
except:
continue
sequence = max(sequences) + 1 if sequences else 1
# Format: PREFIX + 4-digit sequence
category_id = f"{prefix}{sequence:04d}"
return category_id
class SubCategory(db.Model):
__tablename__ = 'sub_categories'
id = db.Column(db.Integer, primary_key=True)
sub_category_id = db.Column(db.String(50), unique=True, nullable=False)
name = db.Column(db.String(100), nullable=False)
image = db.Column(db.String(255), nullable=True)
category_id = db.Column(db.String(50), db.ForeignKey('categories.category_id'), nullable=False)
category_name = db.Column(db.String(100), nullable=False)
brand_id = db.Column(db.String(50), db.ForeignKey('brands.brand_id'), nullable=False)
brand_name = db.Column(db.String(100), nullable=False)
branch_id = db.Column(db.String(50), db.ForeignKey('branches.branch_id'), nullable=False)
branch_name = db.Column(db.String(100), nullable=False)
created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
# Relationships
category = db.relationship('Category', backref='sub_categories', foreign_keys=[category_id])
brand = db.relationship('Brand', backref='sub_categories', foreign_keys=[brand_id])
branch = db.relationship('Branch', backref='sub_categories', foreign_keys=[branch_id])
creator = db.relationship('User', backref='created_subcategories')
def to_dict(self):
return {
'id': self.id,
'sub_category_id': self.sub_category_id,
'name': self.name,
'image': self.image,
'category_id': self.category_id,
'category_name': self.category_name,
'brand_id': self.brand_id,
'brand_name': self.brand_name,
'branch_id': self.branch_id,
'branch_name': self.branch_name,
'created_by': self.created_by,
'created_by_username': self.creator.username if self.creator else None,
'created_at': self.created_at.strftime("%Y-%m-%d %H:%M:%S") if self.created_at else None,
'updated_at': self.updated_at.strftime("%Y-%m-%d %H:%M:%S") if self.updated_at else None
}
@staticmethod
def generate_sub_category_id(name):
letters_only = ''.join(filter(str.isalpha, name)).upper()
prefix = letters_only[:3].ljust(3, 'X')
existing = SubCategory.query.filter(SubCategory.sub_category_id.like(f"{prefix}%")).all()
if not existing:
sequence = 1
else:
sequences = [int(s.sub_category_id[3:]) for s in existing if s.sub_category_id[3:].isdigit()]
sequence = max(sequences) + 1 if sequences else 1
return f"{prefix}{sequence:04d}"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

View File

View File

@ -0,0 +1,255 @@
"""
SQLite Database Backup Script
Usage: python backup_db.py [backup|restore|list]
"""
import os
import shutil
import sqlite3
from datetime import datetime
from pathlib import Path
# Configuration
INSTANCE_DIR = Path(__file__).parent / 'instance'
DB_FILE = INSTANCE_DIR / 'machines.db'
BACKUP_DIR = INSTANCE_DIR / 'backups'
def ensure_backup_dir():
"""Create backup directory if it doesn't exist"""
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
def backup_database():
"""Create a timestamped backup of the database"""
if not DB_FILE.exists():
print(f"❌ Database not found: {DB_FILE}")
return False
ensure_backup_dir()
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
backup_file = BACKUP_DIR / f'machines_backup_{timestamp}.db'
try:
# Method 1: Simple file copy (faster)
shutil.copy2(DB_FILE, backup_file)
# Get file size
size = backup_file.stat().st_size
size_mb = size / (1024 * 1024)
print(f"✅ Backup created successfully!")
print(f" File: {backup_file.name}")
print(f" Size: {size_mb:.2f} MB")
print(f" Location: {backup_file}")
return True
except Exception as e:
print(f"❌ Backup failed: {e}")
return False
def backup_database_with_integrity():
"""Create a backup using SQLite's built-in backup API (slower but safer)"""
if not DB_FILE.exists():
print(f"❌ Database not found: {DB_FILE}")
return False
ensure_backup_dir()
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
backup_file = BACKUP_DIR / f'machines_backup_{timestamp}.db'
try:
# Connect to source database
source_conn = sqlite3.connect(str(DB_FILE))
# Connect to backup database
backup_conn = sqlite3.connect(str(backup_file))
# Perform backup
with backup_conn:
source_conn.backup(backup_conn)
source_conn.close()
backup_conn.close()
# Get file size
size = backup_file.stat().st_size
size_mb = size / (1024 * 1024)
print(f"✅ Backup created successfully (with integrity check)!")
print(f" File: {backup_file.name}")
print(f" Size: {size_mb:.2f} MB")
print(f" Location: {backup_file}")
return True
except Exception as e:
print(f"❌ Backup failed: {e}")
if backup_file.exists():
backup_file.unlink() # Delete partial backup
return False
def list_backups():
"""List all available backups"""
ensure_backup_dir()
backups = sorted(BACKUP_DIR.glob('machines_backup_*.db'), reverse=True)
if not backups:
print("📂 No backups found")
return
print(f"\n📂 Available Backups ({len(backups)} total):")
print("=" * 80)
for i, backup in enumerate(backups, 1):
size = backup.stat().st_size / (1024 * 1024)
mtime = datetime.fromtimestamp(backup.stat().st_mtime)
# Parse timestamp from filename
try:
parts = backup.stem.split('_')
date_str = parts[-2]
time_str = parts[-1]
backup_date = datetime.strptime(f"{date_str}_{time_str}", "%Y%m%d_%H%M%S")
date_display = backup_date.strftime("%Y-%m-%d %H:%M:%S")
except:
date_display = mtime.strftime("%Y-%m-%d %H:%M:%S")
print(f"{i:2d}. {backup.name}")
print(f" Date: {date_display}")
print(f" Size: {size:.2f} MB")
print()
def restore_database(backup_name=None):
"""Restore database from a backup"""
ensure_backup_dir()
backups = sorted(BACKUP_DIR.glob('machines_backup_*.db'), reverse=True)
if not backups:
print("❌ No backups found")
return False
# If no backup specified, show list and ask
if not backup_name:
list_backups()
print("=" * 80)
choice = input("Enter backup number to restore (or 'q' to quit): ").strip()
if choice.lower() == 'q':
print("❌ Restore cancelled")
return False
try:
index = int(choice) - 1
if index < 0 or index >= len(backups):
print("❌ Invalid backup number")
return False
backup_file = backups[index]
except ValueError:
print("❌ Invalid input")
return False
else:
# Find backup by name
backup_file = BACKUP_DIR / backup_name
if not backup_file.exists():
print(f"❌ Backup not found: {backup_name}")
return False
# Confirm restore
print(f"\n⚠️ WARNING: This will replace your current database!")
print(f" Current: {DB_FILE}")
print(f" Backup: {backup_file.name}")
confirm = input("\nType 'yes' to confirm restore: ").strip().lower()
if confirm != 'yes':
print("❌ Restore cancelled")
return False
try:
# Create a safety backup of current database
if DB_FILE.exists():
safety_backup = BACKUP_DIR / f'machines_before_restore_{datetime.now().strftime("%Y%m%d_%H%M%S")}.db'
shutil.copy2(DB_FILE, safety_backup)
print(f"✅ Safety backup created: {safety_backup.name}")
# Restore from backup
shutil.copy2(backup_file, DB_FILE)
print(f"✅ Database restored successfully!")
print(f" Restored from: {backup_file.name}")
print(f"\n⚠️ Remember to restart your Flask server!")
return True
except Exception as e:
print(f"❌ Restore failed: {e}")
return False
def cleanup_old_backups(keep_count=10):
"""Keep only the most recent N backups"""
ensure_backup_dir()
backups = sorted(BACKUP_DIR.glob('machines_backup_*.db'), reverse=True)
if len(backups) <= keep_count:
print(f"✅ No cleanup needed (found {len(backups)} backups, keeping {keep_count})")
return
to_delete = backups[keep_count:]
print(f"\n🗑️ Cleanup: Keeping {keep_count} most recent backups")
print(f" Deleting {len(to_delete)} old backups...")
for backup in to_delete:
try:
backup.unlink()
print(f" ✓ Deleted: {backup.name}")
except Exception as e:
print(f" ✗ Failed to delete {backup.name}: {e}")
print(f"✅ Cleanup complete!")
def main():
"""Main entry point"""
import sys
if len(sys.argv) < 2:
print("Usage: python backup_db.py [backup|restore|list|cleanup]")
print("\nCommands:")
print(" backup - Create a new backup")
print(" restore - Restore from a backup")
print(" list - List all available backups")
print(" cleanup - Delete old backups (keep 10 most recent)")
return
command = sys.argv[1].lower()
if command == 'backup':
backup_database()
elif command == 'backup-safe':
backup_database_with_integrity()
elif command == 'restore':
backup_name = sys.argv[2] if len(sys.argv) > 2 else None
restore_database(backup_name)
elif command == 'list':
list_backups()
elif command == 'cleanup':
keep = int(sys.argv[2]) if len(sys.argv) > 2 else 10
cleanup_old_backups(keep)
else:
print(f"❌ Unknown command: {command}")
print(" Use: backup, restore, list, or cleanup")
if __name__ == '__main__':
main()

View File

@ -0,0 +1,603 @@
"""
Complete Migration Script with Integrated Backup & Restore
Option 3: Client + Machine Assignment
FLOW:
1. Backup current database
2. Run migration
3. Verify migration success
4. On failure: Auto-restore from backup
Usage: python migrate_with_backup.py
Author: System
Date: 2025-01-25
Version: 2.0.0
"""
import sys
import os
import shutil
from datetime import datetime
from pathlib import Path
# Add parent directory to path
current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(current_dir)
sys.path.insert(0, parent_dir)
class DatabaseBackupManager:
"""Handles database backup and restore operations"""
def __init__(self, db_path):
self.db_path = Path(db_path)
self.backup_dir = self.db_path.parent / 'backups'
self.current_backup = None
def ensure_backup_dir(self):
"""Create backup directory if it doesn't exist"""
self.backup_dir.mkdir(parents=True, exist_ok=True)
def create_backup(self):
"""Create timestamped backup of database"""
if not self.db_path.exists():
raise FileNotFoundError(f"Database not found: {self.db_path}")
self.ensure_backup_dir()
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
backup_file = self.backup_dir / f'machines_backup_{timestamp}.db'
try:
# Create backup
shutil.copy2(self.db_path, backup_file)
self.current_backup = backup_file
# Get file size
size = backup_file.stat().st_size / (1024 * 1024)
print(f"✅ Backup created successfully!")
print(f" File: {backup_file.name}")
print(f" Size: {size:.2f} MB")
print(f" Location: {backup_file}")
return backup_file
except Exception as e:
raise Exception(f"Backup failed: {e}")
def restore_backup(self, backup_file=None):
"""Restore database from backup"""
if backup_file is None:
backup_file = self.current_backup
if backup_file is None:
raise ValueError("No backup file specified")
if not backup_file.exists():
raise FileNotFoundError(f"Backup file not found: {backup_file}")
try:
# Create safety backup of current database
if self.db_path.exists():
safety_backup = self.backup_dir / f'machines_before_restore_{datetime.now().strftime("%Y%m%d_%H%M%S")}.db'
shutil.copy2(self.db_path, safety_backup)
print(f"✅ Safety backup created: {safety_backup.name}")
# Restore from backup
shutil.copy2(backup_file, self.db_path)
print(f"✅ Database restored successfully!")
print(f" Restored from: {backup_file.name}")
return True
except Exception as e:
raise Exception(f"Restore failed: {e}")
def list_backups(self):
"""List all available backups"""
self.ensure_backup_dir()
backups = sorted(self.backup_dir.glob('machines_backup_*.db'), reverse=True)
return backups
def cleanup_old_backups(self, keep_count=10):
"""Keep only the most recent N backups"""
backups = self.list_backups()
if len(backups) <= keep_count:
print(f"✅ Cleanup: Keeping all {len(backups)} backups")
return
to_delete = backups[keep_count:]
print(f"🗑️ Cleanup: Keeping {keep_count} most recent backups")
print(f" Deleting {len(to_delete)} old backups...")
for backup in to_delete:
try:
backup.unlink()
print(f" ✓ Deleted: {backup.name}")
except Exception as e:
print(f" ✗ Failed to delete {backup.name}: {e}")
class MigrationError(Exception):
"""Custom exception for migration errors"""
pass
class Option3MigrationWithBackup:
"""Complete migration with integrated backup and restore"""
def __init__(self):
self.app = None
self.inspector = None
self.changes_made = []
self.backup_manager = None
self.backup_file = None
def print_header(self, title):
"""Print formatted header"""
print("\n" + "=" * 70)
print(f" {title}")
print("=" * 70)
def print_step(self, step_num, description):
"""Print step information"""
print(f"\n[Step {step_num}] {description}")
print("-" * 70)
def initialize(self):
"""Initialize Flask app and database"""
try:
from app import create_app, db
from sqlalchemy import inspect, text
self.app = create_app()
self.db = db
self.text = text
# Get database path
with self.app.app_context():
db_uri = self.app.config['SQLALCHEMY_DATABASE_URI']
if db_uri.startswith('sqlite:///'):
db_path = db_uri.replace('sqlite:///', '')
self.db_path = Path(db_path)
self.backup_manager = DatabaseBackupManager(self.db_path)
else:
raise ValueError("This script only supports SQLite databases")
self.inspector = inspect(db.engine)
return True
except ImportError as e:
print(f"❌ Error importing modules: {e}")
print("Make sure you're running this from the backend directory")
return False
except Exception as e:
print(f"❌ Initialization failed: {e}")
return False
def create_backup(self):
"""Create database backup"""
self.print_step(1, "Creating Database Backup")
try:
self.backup_file = self.backup_manager.create_backup()
return True
except Exception as e:
print(f"❌ Backup failed: {e}")
return False
def check_prerequisites(self):
"""Check if all required tables exist"""
self.print_step(2, "Checking Prerequisites")
with self.app.app_context():
tables = self.inspector.get_table_names()
required_tables = ['users', 'machines']
missing_tables = [t for t in required_tables if t not in tables]
if missing_tables:
raise MigrationError(
f"Required tables missing: {', '.join(missing_tables)}\n"
"Please ensure your database is properly initialized."
)
print("✅ All required tables exist")
print(f"✅ Found tables: {', '.join(tables)}")
def add_assigned_to_column(self):
"""Add assigned_to column to users table if not exists"""
self.print_step(3, "Adding assigned_to Column to Users Table")
with self.app.app_context():
columns = [col['name'] for col in self.inspector.get_columns('users')]
if 'assigned_to' in columns:
print("⚠️ assigned_to column already exists - skipping")
return False
try:
print("Creating assigned_to column...")
# Add column
self.db.session.execute(self.text("""
ALTER TABLE users
ADD COLUMN assigned_to INTEGER NULL
"""))
# Add foreign key constraint
self.db.session.execute(self.text("""
ALTER TABLE users
ADD CONSTRAINT fk_users_assigned_to
FOREIGN KEY (assigned_to)
REFERENCES users(id)
ON DELETE SET NULL
"""))
self.db.session.commit()
print("✅ assigned_to column created successfully")
print("✅ Foreign key constraint added")
self.changes_made.append('assigned_to_column')
return True
except Exception as e:
self.db.session.rollback()
raise MigrationError(f"Failed to add assigned_to column: {str(e)}")
def create_refiller_machines_table(self):
"""Create refiller_machines junction table if not exists"""
self.print_step(4, "Creating refiller_machines Junction Table")
with self.app.app_context():
tables = self.inspector.get_table_names()
if 'refiller_machines' in tables:
print("⚠️ refiller_machines table already exists - skipping")
return False
try:
print("Creating refiller_machines table...")
self.db.session.execute(self.text("""
CREATE TABLE refiller_machines (
id INTEGER PRIMARY KEY AUTOINCREMENT,
refiller_id INTEGER NOT NULL,
machine_id VARCHAR(10) NOT NULL,
assigned_at DATETIME DEFAULT CURRENT_TIMESTAMP,
assigned_by INTEGER NULL,
CONSTRAINT fk_refiller_machines_refiller
FOREIGN KEY (refiller_id)
REFERENCES users(id)
ON DELETE CASCADE,
CONSTRAINT fk_refiller_machines_machine
FOREIGN KEY (machine_id)
REFERENCES machines(machine_id)
ON DELETE CASCADE,
CONSTRAINT fk_refiller_machines_assigner
FOREIGN KEY (assigned_by)
REFERENCES users(id)
ON DELETE SET NULL,
CONSTRAINT unique_refiller_machine
UNIQUE (refiller_id, machine_id)
)
"""))
self.db.session.commit()
print("✅ refiller_machines table created successfully")
self.changes_made.append('refiller_machines_table')
return True
except Exception as e:
self.db.session.rollback()
raise MigrationError(f"Failed to create refiller_machines table: {str(e)}")
def verify_migration(self):
"""Verify all changes were applied correctly"""
self.print_step(5, "Verifying Migration")
with self.app.app_context():
# Refresh inspector
from sqlalchemy import inspect
self.inspector = inspect(self.db.engine)
# Check assigned_to column
user_columns = [col['name'] for col in self.inspector.get_columns('users')]
if 'assigned_to' in user_columns:
print("✅ users.assigned_to column exists")
else:
raise MigrationError("Verification failed: assigned_to column not found")
# Check refiller_machines table
tables = self.inspector.get_table_names()
if 'refiller_machines' in tables:
print("✅ refiller_machines table exists")
# Verify columns
rm_columns = [col['name'] for col in self.inspector.get_columns('refiller_machines')]
expected_columns = ['id', 'refiller_id', 'machine_id', 'assigned_at', 'assigned_by']
for col in expected_columns:
if col in rm_columns:
print(f" ✅ Column '{col}' exists")
else:
raise MigrationError(f"Verification failed: Column '{col}' not found")
else:
raise MigrationError("Verification failed: refiller_machines table not found")
def restore_on_failure(self):
"""Restore database from backup on migration failure"""
self.print_header("🔄 RESTORING FROM BACKUP")
try:
if self.backup_file:
self.backup_manager.restore_backup(self.backup_file)
print("✅ Database restored successfully from backup")
print(" Your data is safe!")
else:
print("⚠️ No backup file available for restore")
except Exception as e:
print(f"❌ Restore failed: {e}")
print(" Please manually restore from backup")
def print_summary(self):
"""Print migration summary"""
self.print_header("✅ MIGRATION COMPLETED SUCCESSFULLY")
if not self.changes_made:
print("\n⚠️ No changes were made - all structures already exist")
else:
print(f"\n✅ Successfully applied {len(self.changes_made)} change(s):")
for i, change in enumerate(self.changes_made, 1):
print(f" {i}. {change}")
# Show backup information
if self.backup_file:
print(f"\n📦 Backup Information:")
print(f" Location: {self.backup_file}")
print(f" You can restore this backup if needed using:")
print(f" python migrate_with_backup.py restore")
# Cleanup old backups
print()
self.backup_manager.cleanup_old_backups(keep_count=10)
print("\n" + "=" * 70)
print("NEXT STEPS")
print("=" * 70)
print("\n1. Update Backend Models (app/models/models.py)")
print("2. Update Backend Services (app/services/services.py)")
print("3. Update Backend Routes (app/routes/routes.py)")
print("4. Update Frontend (user and machine modules)")
print("5. Restart Backend: python app.py")
print("6. Test the implementation")
print("\n" + "=" * 70 + "\n")
def run_migration(self):
"""Run the complete migration process"""
try:
self.print_header("🚀 Option 3 Migration with Backup")
# Step 1: Create backup
if not self.create_backup():
print("❌ Cannot proceed without backup")
return False
# Step 2: Check prerequisites
self.check_prerequisites()
# Step 3: Add assigned_to column
self.add_assigned_to_column()
# Step 4: Create refiller_machines table
self.create_refiller_machines_table()
# Step 5: Verify migration
self.verify_migration()
# Step 6: Print summary
self.print_summary()
return True
except MigrationError as e:
print(f"\n❌ Migration Error: {str(e)}")
self.restore_on_failure()
return False
except Exception as e:
print(f"\n❌ Unexpected Error: {str(e)}")
import traceback
traceback.print_exc()
self.restore_on_failure()
return False
def list_backups():
"""List all available backups"""
print("\n" + "=" * 70)
print(" AVAILABLE BACKUPS")
print("=" * 70)
# Find database path
try:
from app import create_app
app = create_app()
with app.app_context():
db_uri = app.config['SQLALCHEMY_DATABASE_URI']
if db_uri.startswith('sqlite:///'):
db_path = db_uri.replace('sqlite:///', '')
backup_manager = DatabaseBackupManager(Path(db_path))
backups = backup_manager.list_backups()
if not backups:
print("\n📂 No backups found")
return
print(f"\n📂 Found {len(backups)} backup(s):")
print("=" * 70)
for i, backup in enumerate(backups, 1):
size = backup.stat().st_size / (1024 * 1024)
mtime = datetime.fromtimestamp(backup.stat().st_mtime)
print(f"{i:2d}. {backup.name}")
print(f" Date: {mtime.strftime('%Y-%m-%d %H:%M:%S')}")
print(f" Size: {size:.2f} MB")
print()
print("=" * 70 + "\n")
else:
print("❌ Only SQLite databases are supported")
except Exception as e:
print(f"❌ Error: {e}")
def restore_from_backup():
"""Interactive restore from backup"""
print("\n" + "=" * 70)
print(" RESTORE FROM BACKUP")
print("=" * 70)
try:
from app import create_app
app = create_app()
with app.app_context():
db_uri = app.config['SQLALCHEMY_DATABASE_URI']
if db_uri.startswith('sqlite:///'):
db_path = db_uri.replace('sqlite:///', '')
backup_manager = DatabaseBackupManager(Path(db_path))
backups = backup_manager.list_backups()
if not backups:
print("\n❌ No backups found")
return
# List backups
print(f"\n📂 Available backups:")
print("=" * 70)
for i, backup in enumerate(backups, 1):
size = backup.stat().st_size / (1024 * 1024)
mtime = datetime.fromtimestamp(backup.stat().st_mtime)
print(f"{i:2d}. {backup.name}")
print(f" Date: {mtime.strftime('%Y-%m-%d %H:%M:%S')}")
print(f" Size: {size:.2f} MB")
print()
print("=" * 70)
# Get user choice
choice = input("\nEnter backup number to restore (or 'q' to quit): ").strip()
if choice.lower() == 'q':
print("❌ Restore cancelled")
return
try:
index = int(choice) - 1
if index < 0 or index >= len(backups):
print("❌ Invalid backup number")
return
backup_file = backups[index]
# Confirm
print(f"\n⚠️ WARNING: This will replace your current database!")
print(f" Backup: {backup_file.name}")
confirm = input("\nType 'yes' to confirm restore: ").strip().lower()
if confirm != 'yes':
print("❌ Restore cancelled")
return
# Restore
backup_manager.restore_backup(backup_file)
print("\n⚠️ Remember to restart your Flask server!")
except ValueError:
print("❌ Invalid input")
except Exception as e:
print(f"❌ Restore failed: {e}")
else:
print("❌ Only SQLite databases are supported")
except Exception as e:
print(f"❌ Error: {e}")
def main():
"""Main entry point"""
print("\n" + "=" * 70)
print(" VENDING MACHINE MANAGEMENT SYSTEM")
print(" Database Migration with Backup & Restore")
print(" Version: 2.0.0")
print("=" * 70)
if len(sys.argv) > 1:
command = sys.argv[1].lower()
if command == 'list':
list_backups()
return
elif command == 'restore':
restore_from_backup()
return
elif command == 'migrate':
pass # Continue to migration
else:
print(f"\n❌ Unknown command: {command}")
print("\nUsage:")
print(" python migrate_with_backup.py # Run migration")
print(" python migrate_with_backup.py migrate # Run migration")
print(" python migrate_with_backup.py list # List backups")
print(" python migrate_with_backup.py restore # Restore from backup")
return
# Run migration
migration = Option3MigrationWithBackup()
if not migration.initialize():
print("\n❌ Failed to initialize. Please check your setup.")
sys.exit(1)
print("\nThis migration will:")
print(" 1. ✅ Create backup of current database")
print(" 2. ✅ Add users.assigned_to column")
print(" 3. ✅ Create refiller_machines table")
print(" 4. ✅ Verify all changes")
print(" 5. ✅ Auto-restore on failure")
response = input("\nProceed with migration? (yes/no): ").strip().lower()
if response != 'yes':
print("\n❌ Migration cancelled")
sys.exit(0)
success = migration.run_migration()
if success:
print("\n✅ Migration completed successfully!")
sys.exit(0)
else:
print("\n❌ Migration failed! Database has been restored from backup.")
sys.exit(1)
if __name__ == '__main__':
main()

View File

@ -1,38 +1,123 @@
import time
import sys
import os
from sqlalchemy import create_engine
from sqlalchemy.exc import OperationalError
"""
Database Migration Script for Categories Table
"""
def wait_for_db(max_retries=30, retry_delay=2):
"""Wait for database to be ready"""
from app import db, create_app
from sqlalchemy import text
def create_categories_table():
"""Create categories table"""
mysql_host = os.getenv('MYSQL_HOST', 'db')
mysql_user = os.getenv('MYSQL_USER', 'vendinguser')
mysql_password = os.getenv('MYSQL_PASSWORD', 'vendingpass')
mysql_db = os.getenv('MYSQL_DATABASE', 'vending')
create_table_sql = """
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
image VARCHAR(255),
brand_id VARCHAR(50) NOT NULL,
brand_name VARCHAR(100) NOT NULL,
branch_id VARCHAR(50) NOT NULL,
branch_name VARCHAR(100) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (brand_id) REFERENCES brands(brand_id),
FOREIGN KEY (branch_id) REFERENCES branches(branch_id)
);
"""
db_uri = f'mysql+pymysql://{mysql_user}:{mysql_password}@{mysql_host}:3306/{mysql_db}'
create_index_sqls = [
"CREATE INDEX IF NOT EXISTS idx_category_id ON categories(category_id);",
"CREATE INDEX IF NOT EXISTS idx_category_brand_id ON categories(brand_id);",
"CREATE INDEX IF NOT EXISTS idx_category_branch_id ON categories(branch_id);",
"CREATE INDEX IF NOT EXISTS idx_category_name ON categories(name);"
]
print(f"⏳ Waiting for MySQL at {mysql_host}:3306...")
try:
app = create_app()
with app.app_context():
with db.engine.connect() as connection:
# Create the categories table
connection.execute(text(create_table_sql))
# Create each index separately (SQLite limitation)
for sql in create_index_sqls:
connection.execute(text(sql))
connection.commit()
print("✓ Categories table created successfully!")
print("✓ Indexes created successfully!")
# Optional: add sample categories
add_sample = input("\nAdd sample categories? (y/n): ")
if add_sample.lower() == 'y':
add_sample_categories()
except Exception as e:
print(f"✗ Error creating table: {e}")
import traceback
traceback.print_exc()
def add_sample_categories():
"""Add sample category data"""
from app.models.models import Category, Brand, Branch
for attempt in range(1, max_retries + 1):
try:
engine = create_engine(db_uri)
connection = engine.connect()
connection.close()
print("✓ MySQL is ready!")
return True
except OperationalError as e:
if attempt < max_retries:
print(f"Waiting for MySQL... (attempt {attempt}/{max_retries})")
time.sleep(retry_delay)
else:
print(f"❌ Failed to connect to MySQL after {max_retries} attempts")
print(f"Error: {e}")
sys.exit(1)
brands = Brand.query.all()
branches = Branch.query.all()
return False
if not brands or not branches:
print("✗ Please create brands and branches first!")
return
sample_categories = [
{'name': 'Beverages', 'brand_id': brands[0].brand_id if len(brands) > 0 else None},
{'name': 'Snacks', 'brand_id': brands[1].brand_id if len(brands) > 1 else brands[0].brand_id},
{'name': 'Chocolates', 'brand_id': brands[1].brand_id if len(brands) > 1 else brands[0].brand_id}
]
try:
for cat_data in sample_categories:
if not cat_data['brand_id']:
continue
brand = Brand.query.filter_by(brand_id=cat_data['brand_id']).first()
if not brand:
continue
branch = Branch.query.filter_by(branch_id=brand.branch_id).first()
if not branch:
continue
category_id = Category.generate_category_id(cat_data['name'])
category = Category(
category_id=category_id,
name=cat_data['name'],
brand_id=brand.brand_id,
brand_name=brand.name,
branch_id=branch.branch_id,
branch_name=branch.name,
image=None
)
db.session.add(category)
db.session.commit()
print(f"✓ Added sample categories successfully!")
# Display all created categories
categories = Category.query.all()
print("\nCreated categories:")
for cat in categories:
print(f" - {cat.category_id}: {cat.name} @ {cat.brand_name} ({cat.branch_name})")
except Exception as e:
db.session.rollback()
print(f"✗ Error adding sample data: {e}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
wait_for_db()
create_categories_table()