Initial commit: Clean Mold Cost Calculator application
- Flask-based mold cost calculation application - PostgreSQL database with SQLAlchemy ORM - Docker/Podman containerization - Comprehensive documentation in docs/ - Clean, organized codebase without obsolete files - Production-ready deployment configuration - Enhanced pytest configuration with coverage reporting - Master/Development branch structure
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
from .. import db
|
||||
from .user import User
|
||||
from .quotation import Quotation
|
||||
from .cost_factor import CostFactor, CostFactorHistory
|
||||
|
||||
__all__ = ['db', 'User', 'Quotation', 'CostFactor', 'CostFactorHistory']
|
||||
@@ -0,0 +1,66 @@
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy import event
|
||||
from .. import db
|
||||
|
||||
class CostFactor(db.Model):
|
||||
"""Model for storing cost factors that can be managed via admin interface"""
|
||||
__tablename__ = 'cost_factors'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
category = db.Column(db.String(50), nullable=False) # 'mold', 'complexity', 'texture', 'tax'
|
||||
subcategory = db.Column(db.String(50)) # For mold types: 'Rubber', 'CMEVA', etc.
|
||||
name = db.Column(db.String(100), nullable=False) # Factor name
|
||||
factor_value = db.Column(db.Float, nullable=False) # The actual factor value
|
||||
calculation_formula = db.Column(db.String(200)) # For complexity factors
|
||||
percentage = db.Column(db.String(20)) # For display purposes
|
||||
description = db.Column(db.String(500))
|
||||
is_active = db.Column(db.Boolean, default=True)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
updated_by = db.Column(db.Integer, db.ForeignKey('enterprise_users.id'))
|
||||
|
||||
# Relationship
|
||||
updater = db.relationship('User', backref='cost_factor_updates')
|
||||
|
||||
__table_args__ = (
|
||||
db.Index('ix_cost_factor_category', category),
|
||||
db.Index('ix_cost_factor_name', name),
|
||||
db.Index('ix_cost_factor_active', is_active),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<CostFactor {self.category}:{self.name}={self.factor_value}>'
|
||||
|
||||
class CostFactorHistory(db.Model):
|
||||
"""Model for tracking changes to cost factors"""
|
||||
__tablename__ = 'cost_factor_history'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
cost_factor_id = db.Column(db.Integer, db.ForeignKey('cost_factors.id'), nullable=False)
|
||||
old_value = db.Column(db.Float)
|
||||
new_value = db.Column(db.Float, nullable=False)
|
||||
changed_by = db.Column(db.Integer, db.ForeignKey('enterprise_users.id'), nullable=False)
|
||||
changed_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
change_reason = db.Column(db.String(500))
|
||||
|
||||
# Relationships
|
||||
cost_factor = db.relationship('CostFactor', backref='history')
|
||||
user = db.relationship('User', backref='cost_factor_changes')
|
||||
|
||||
__table_args__ = (
|
||||
db.Index('ix_cost_factor_history_factor_id', cost_factor_id),
|
||||
db.Index('ix_cost_factor_history_changed_at', changed_at),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<CostFactorHistory {self.cost_factor_id}:{self.old_value}->{self.new_value}>'
|
||||
|
||||
# Event listeners for model changes
|
||||
@event.listens_for(CostFactor, 'before_insert')
|
||||
def set_cost_factor_timestamps(mapper, connection, target):
|
||||
target.created_at = datetime.now(timezone.utc)
|
||||
target.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
@event.listens_for(CostFactor, 'before_update')
|
||||
def set_cost_factor_update_timestamp(mapper, connection, target):
|
||||
target.updated_at = datetime.now(timezone.utc)
|
||||
@@ -0,0 +1,63 @@
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy import event
|
||||
from .. import db
|
||||
|
||||
class Quotation(db.Model):
|
||||
__tablename__ = 'mold_quotations'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('enterprise_users.id'), nullable=False)
|
||||
|
||||
# Factory Information
|
||||
mold_shop_name = db.Column(db.String(100), nullable=False)
|
||||
mold_shop_country = db.Column(db.String(50), nullable=False)
|
||||
t1_shoe_factory_name = db.Column(db.String(100), nullable=False)
|
||||
t1_shoe_factory_country = db.Column(db.String(50), nullable=False)
|
||||
t2_component_factory_name = db.Column(db.String(100), nullable=False)
|
||||
t2_component_factory_country = db.Column(db.String(50), nullable=False)
|
||||
|
||||
# Mold Information
|
||||
model_name = db.Column(db.String(100), nullable=False)
|
||||
tooling_id = db.Column(db.String(5), nullable=False)
|
||||
season = db.Column(db.String(10), nullable=False)
|
||||
stage = db.Column(db.String(10), nullable=False)
|
||||
type = db.Column(db.String(20), nullable=False)
|
||||
|
||||
# Dates and Approvals
|
||||
issue_date = db.Column(db.Date, nullable=False)
|
||||
approved_by_mold_shop = db.Column(db.String(100), nullable=False)
|
||||
approved_by_t1_tooling = db.Column(db.String(100), nullable=False)
|
||||
approved_by_lo_tooling = db.Column(db.String(100), nullable=False)
|
||||
|
||||
# Mold Specifications
|
||||
mold_main_type = db.Column(db.String(50), nullable=False)
|
||||
mold_sub_type = db.Column(db.String(50), nullable=False)
|
||||
mold_specific_type = db.Column(db.String(50), nullable=False)
|
||||
complexity_high_sidewall = db.Column(db.Boolean, default=False)
|
||||
sliders_count = db.Column(db.Integer, nullable=False)
|
||||
cavity_count = db.Column(db.Float, nullable=False)
|
||||
digital_texture_type = db.Column(db.String(50), nullable=False)
|
||||
|
||||
# Calculation Results
|
||||
base_price = db.Column(db.Float, nullable=False)
|
||||
tax_rate = db.Column(db.Float)
|
||||
total_cost = db.Column(db.Float, nullable=False)
|
||||
|
||||
# Metadata
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
is_archived = db.Column(db.Boolean, default=False)
|
||||
|
||||
__table_args__ = (
|
||||
db.Index('ix_quotation_user_id', user_id),
|
||||
db.Index('ix_quotation_created_at', created_at),
|
||||
db.Index('ix_quotation_issue_date', issue_date),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Quotation {self.id}>'
|
||||
|
||||
# Event listeners for model changes
|
||||
@event.listens_for(Quotation, 'before_insert')
|
||||
def set_quotation_timestamps(mapper, connection, target):
|
||||
target.created_at = datetime.now(timezone.utc)
|
||||
@@ -0,0 +1,116 @@
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from flask_login import UserMixin
|
||||
from sqlalchemy import event, func
|
||||
from .. import db
|
||||
import secrets
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
__tablename__ = 'enterprise_users'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(25), unique=True, nullable=False)
|
||||
email = db.Column(db.String(120), unique=True, nullable=False)
|
||||
company = db.Column(db.String(100), nullable=False)
|
||||
password_hash = db.Column(db.String(128), nullable=False)
|
||||
created_at = db.Column(db.DateTime)
|
||||
password_changed_at = db.Column(db.DateTime)
|
||||
is_admin = db.Column(db.Boolean, default=False)
|
||||
last_login = db.Column(db.DateTime)
|
||||
is_active = db.Column(db.Boolean, default=True)
|
||||
failed_login_attempts = db.Column(db.Integer, default=0)
|
||||
last_failed_login = db.Column(db.DateTime)
|
||||
reset_token = db.Column(db.String(100), unique=True)
|
||||
reset_token_expires = db.Column(db.DateTime)
|
||||
|
||||
# Relationships
|
||||
quotations = db.relationship('Quotation', backref='user', lazy=True)
|
||||
|
||||
def set_password(self, password):
|
||||
try:
|
||||
self.password_hash = generate_password_hash(password, method='pbkdf2:sha256')
|
||||
self.password_changed_at = datetime.now(timezone.utc)
|
||||
self.reset_token = None
|
||||
self.reset_token_expires = None
|
||||
except Exception as e:
|
||||
logger.error(f'Error setting password: {str(e)}', exc_info=True)
|
||||
raise
|
||||
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
def generate_reset_token(self):
|
||||
token = secrets.token_urlsafe(16) # Shorter token for display
|
||||
self.reset_token = token
|
||||
self.reset_token_expires = datetime.now(timezone.utc) + timedelta(minutes=15) # 15-minute expiry
|
||||
return token
|
||||
|
||||
def verify_reset_token(self, token):
|
||||
if not self.reset_token or not self.reset_token_expires:
|
||||
return False
|
||||
if datetime.now(timezone.utc) > self.reset_token_expires:
|
||||
return False
|
||||
return secrets.compare_digest(self.reset_token, token)
|
||||
|
||||
@classmethod
|
||||
def authenticate(cls, email, password):
|
||||
user = cls.query.filter(func.lower(cls.email) == email.lower()).first()
|
||||
if user and user.check_password(password):
|
||||
if not user.is_active:
|
||||
return None
|
||||
user.failed_login_attempts = 0
|
||||
user.last_failed_login = None
|
||||
db.session.commit()
|
||||
return user
|
||||
if user:
|
||||
user.failed_login_attempts = (user.failed_login_attempts or 0) + 1
|
||||
user.last_failed_login = datetime.now(timezone.utc)
|
||||
db.session.commit()
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def create(cls, username, email, company, password):
|
||||
try:
|
||||
logger.info(f'Checking if username {username} exists...')
|
||||
if cls.query.filter(func.lower(cls.username) == username.lower()).first():
|
||||
logger.warning(f'Username {username} already taken')
|
||||
raise ValueError('Username already taken')
|
||||
|
||||
logger.info(f'Checking if email {email} exists...')
|
||||
if cls.query.filter(func.lower(cls.email) == email.lower()).first():
|
||||
logger.warning(f'Email {email} already registered')
|
||||
raise ValueError('Email already registered')
|
||||
|
||||
logger.info('Creating new user...')
|
||||
user = cls(
|
||||
username=username,
|
||||
email=email,
|
||||
company=company,
|
||||
is_active=True,
|
||||
failed_login_attempts=0,
|
||||
is_admin=False
|
||||
)
|
||||
|
||||
logger.info('Setting password...')
|
||||
user.set_password(password)
|
||||
|
||||
logger.info('Adding user to session...')
|
||||
db.session.add(user)
|
||||
|
||||
logger.info('User created successfully')
|
||||
return user
|
||||
except Exception as e:
|
||||
logger.error(f'Error creating user: {str(e)}', exc_info=True)
|
||||
raise
|
||||
|
||||
def __repr__(self):
|
||||
return f'<User {self.username}>'
|
||||
|
||||
# Event listeners for model changes
|
||||
@event.listens_for(User, 'before_insert')
|
||||
def set_timestamps(mapper, connection, target):
|
||||
target.created_at = datetime.now(timezone.utc)
|
||||
target.password_changed_at = datetime.now(timezone.utc)
|
||||
Reference in New Issue
Block a user