164 lines
7.0 KiB
Python
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.") |