import datetime from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash from dateutil.relativedelta import relativedelta # pip install python-dateutil db = SQLAlchemy() class User(db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) password_hash = db.Column(db.String(128), nullable=False) # Settings stored with the user expiry_warning_days = db.Column(db.Integer, default=2, nullable=False) low_stock_threshold_value = db.Column(db.Float) # Value can be ml, L, or items low_stock_threshold_unit = db.Column(db.String(10)) # 'ml', 'L', 'items' notify_expiry_warning = db.Column(db.Boolean, default=True, nullable=False) notify_expired_alert = db.Column(db.Boolean, default=True, nullable=False) notify_low_stock = db.Column(db.Boolean, default=True, nullable=False) created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow) # Relationships milk_batches = db.relationship('MilkBatch', backref='user', lazy=True, cascade="all, delete-orphan") consumption_records = db.relationship('ConsumptionRecord', backref='user', lazy=True, cascade="all, delete-orphan") push_devices = db.relationship('PushDevice', backref='user', lazy=True, cascade="all, delete-orphan") def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) def to_dict(self): # Used for returning user profile/settings return { "id": self.id, "username": self.username, "expiryWarningDays": self.expiry_warning_days, "lowStockThresholdValue": self.low_stock_threshold_value, "lowStockThresholdUnit": self.low_stock_threshold_unit, "notificationsEnabled": { "expiry": self.notify_expiry_warning, "expired": self.notify_expired_alert, # Changed key slightly for clarity "lowStock": self.notify_low_stock, } } class MilkBatch(db.Model): __tablename__ = 'milk_batches' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) production_date = db.Column(db.Date) # Store as Date object expiry_date = db.Column(db.Date, nullable=False) # Store as Date object initial_quantity_items = db.Column(db.Integer, nullable=False) # e.g., 2 (bottles) volume_per_item = db.Column(db.Float, nullable=False) # e.g., 1000 volume_unit = db.Column(db.String(5), nullable=False) # 'ml' or 'L' note = db.Column(db.String(200)) # Store remaining volume always in the primary volume_unit (ml or L) remaining_volume = db.Column(db.Float, nullable=False) created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow) is_deleted = db.Column(db.Boolean, default=False, nullable=False) # Use for soft delete consumption_records = db.relationship('ConsumptionRecord', backref='milk_batch', lazy=True) @property def days_remaining(self): """Calculates days remaining until expiry.""" if self.expiry_date: return (self.expiry_date - datetime.date.today()).days return None # Should not happen if expiry_date is required @property def status(self): """Determines status based on expiry date and user settings.""" days = self.days_remaining if days is None: return 'normal' warning_days = self.user.expiry_warning_days if self.user else 2 if days < 0: return 'expired' elif days <= warning_days: return 'warning' else: return 'normal' @property def initial_total_volume(self): """Calculate initial total volume in the batch's unit.""" return self.initial_quantity_items * self.volume_per_item @property def remaining_items_approx(self): """Calculate approximate remaining items.""" if self.volume_per_item > 0: # Use a small tolerance to handle floating point issues near zero if self.remaining_volume < 0.001: return 0 return max(0, round(self.remaining_volume / self.volume_per_item, 2)) return 0 def to_dict(self): """Serializes the object to a dictionary.""" return { "id": self.id, "userId": self.user_id, "productionDate": self.production_date.isoformat() if self.production_date else None, "expiryDate": self.expiry_date.isoformat(), "initialQuantityItems": self.initial_quantity_items, "volumePerItem": self.volume_per_item, "volumeUnit": self.volume_unit, "note": self.note, "remainingVolume": round(self.remaining_volume, 2), # Round for display "initialTotalVolume": round(self.initial_total_volume, 2), "remainingItemsApprox": self.remaining_items_approx, "createdAt": self.created_at.isoformat(), "isDeleted": self.is_deleted, # Calculated properties "daysRemaining": self.days_remaining, "status": self.status, } class ConsumptionRecord(db.Model): __tablename__ = 'consumption_records' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) milk_batch_id = db.Column(db.Integer, db.ForeignKey('milk_batches.id'), nullable=False) amount_consumed = db.Column(db.Float, nullable=False) # Store the unit as entered by user for history, but calculation uses batch unit unit_consumed = db.Column(db.String(10), nullable=False) # 'ml', 'L', 'items' consumed_at = db.Column(db.DateTime, nullable=False) # Store as DateTime object created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow) def to_dict(self): return { "id": self.id, "userId": self.user_id, "milkBatchId": self.milk_batch_id, "amountConsumed": self.amount_consumed, "unitConsumed": self.unit_consumed, "consumedAt": self.consumed_at.isoformat(), "createdAt": self.created_at.isoformat(), } class PushDevice(db.Model): __tablename__ = 'push_devices' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) device_type = db.Column(db.String(10), nullable=False) # 'web', 'ios', 'android' # Token can be very long, especially for web push subscriptions (JSON) token = db.Column(db.Text, unique=True, nullable=False) created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow) # Helper function to initialize DB (call once when setting up) def init_database(app): with app.app_context(): db.create_all() print("Database tables created/verified.")