milk_manager/milk_app/database.py
2025-04-09 20:34:32 +08:00

164 lines
7.0 KiB
Python

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.")