Skip to main content

Key Features

Muebles Roble provides a complete suite of features designed specifically for furniture manufacturing operations. Built on Flask with a layered architecture, the system offers robust catalog management, inventory control, and production tracking.

Catalog Management

The heart of Muebles Roble is its comprehensive catalog system, managing all reference data used throughout production.

Available Catalogs

Colors

Manage finish colors like Natural, White, Black, Walnut, Mahogany, and custom colors.

Wood Types

Track different wood species: Pine, Cedar, Oak, Mahogany, and more with specific properties.

Units of Measure

Manage measurement units: kilograms, square meters, pieces, linear meters, etc.

User Roles

Define user roles for access control and permission management.

Catalog Operations

Each catalog supports full CRUD operations:
1

Create

Add new catalog entries through validated forms with CSRF protection and duplicate detection.
2

Read/List

View all catalog entries with active/inactive status filtering and sorting capabilities.
3

Update

Edit existing entries with pre-populated forms and conflict detection for name uniqueness.
4

Delete (Soft)

Logical deletion that preserves data for audit purposes while marking records inactive.

Example: Color Catalog

Here’s how the color catalog is implemented:
# Model: app/models/color.py
class Color(db.Model):
    __tablename__ = 'colors'
    
    id_color = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=False, unique=True)
    active = db.Column(db.Boolean, nullable=False, default=True)
    
    # Audit fields
    created_at = db.Column(db.TIMESTAMP, server_default=func.current_timestamp())
    updated_at = db.Column(db.TIMESTAMP, server_onupdate=func.current_timestamp())
    deleted_at = db.Column(db.TIMESTAMP, nullable=True)
    
    created_by = db.Column(db.String(100))
    updated_by = db.Column(db.String(100))
    deleted_by = db.Column(db.String(100))
All catalog models follow this same structure, ensuring consistency across the application.

Form Handling & Validation

The system uses Flask-WTF for robust form handling with multiple validation layers.

Multi-Layer Validation

HTML5 form attributes provide immediate feedback:
<input type="text" name="name" required minlength="2" maxlength="50">
WTForms validators ensure data quality:
# app/catalogs/colors/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms.validators import DataRequired, Length

class ColorForm(FlaskForm):
    name = StringField(
        'Color Name',
        validators=[
            DataRequired(message='Color name is required'),
            Length(min=2, max=50, message='Name must be 2-50 characters')
        ]
    )
Service layer enforces business rules:
# app/catalogs/colors/services.py
@staticmethod
def create(data: dict) -> dict:
    name = data.get('name')
    if not name or not name.strip():
        raise ValidationError('Color name is required')
    
    # Check for duplicates
    existing = Color.query.filter_by(name=name.strip()).first()
    if existing:
        raise ConflictError(f"Color '{name}' already exists")
    
    color = Color(name=name.strip())
    db.session.add(color)
    db.session.commit()
    return color.to_dict()
Database enforces final integrity:
name = db.Column(db.String(50), nullable=False, unique=True)

CSRF Protection

All forms are automatically protected against Cross-Site Request Forgery attacks:
<!-- templates/colors/create.html -->
<form method="POST" action="{{ url_for('colors.create_color') }}">
    {{ form.hidden_tag() }}  <!-- CSRF token -->
    
    {{ form.name.label }}
    {{ form.name }}
    
    <button type="submit">Create Color</button>
</form>
The form.hidden_tag() is required in every form to include the CSRF token. Forms without it will be rejected.

Request Flow & Architecture

The layered architecture ensures clean separation of concerns:

Creating a New Color

1

Route Receives Request

# app/catalogs/colors/routes.py
@colors_bp.route("/create", methods=["GET", "POST"])
def create_color():
    form = ColorForm()
    
    if form.validate_on_submit():
        data = {"name": form.name.data}
        try:
            ColorService.create(data)
            flash("Color created successfully", "success")
            return redirect(url_for("colors.create_color"))
        except ConflictError as e:
            flash(e.message, "error")
    
    return render_template("colors/create.html", form=form)
2

Service Applies Business Logic

The service validates business rules, checks for duplicates, and manages the transaction.
3

Model Persists Data

SQLAlchemy ORM creates the database record with automatic timestamp and audit field population.
4

Template Renders Result

Jinja2 template displays success message and either shows the form again or redirects.

Updating an Existing Color

# app/catalogs/colors/routes.py
@colors_bp.route("/<int:id_color>/edit", methods=["GET", "POST"])
def edit_color(id_color: int):
    try:
        color = ColorService.get_by_id(id_color)
    except NotFoundError as e:
        flash(e.message, "error")
        return redirect(url_for("colors.list_colors"))
    
    form = ColorForm()
    
    if form.validate_on_submit():
        data = {"name": form.name.data}
        try:
            ColorService.update(id_color, data)
            flash("Color updated successfully", "success")
            return redirect(url_for("colors.list_colors"))
        except (ConflictError, ValidationError) as e:
            flash(e.message, "error")
    
    elif request.method == "GET":
        # Pre-populate form on GET requests
        form.name.data = color.name
    
    return render_template("colors/edit.html", form=form, color=color)
The system uses the PRG (Post-Redirect-Get) pattern to prevent duplicate form submissions.

Error Handling

Comprehensive error handling with user-friendly messages:

Exception Hierarchy

# app/exceptions.py
class AppException(Exception):
    """Base exception for all application errors."""
    def __init__(self, message: str, status_code: int):
        self.message = message
        self.status_code = status_code

class ValidationError(AppException):
    """Raised when input validation fails (400)."""
    def __init__(self, message: str):
        super().__init__(message, 400)

class NotFoundError(AppException):
    """Raised when a resource is not found (404)."""
    def __init__(self, message: str):
        super().__init__(message, 404)

class ConflictError(AppException):
    """Raised when a resource conflict occurs (409)."""
    def __init__(self, message: str):
        super().__init__(message, 409)

Error Flow

1

Exception Raised in Service

Business logic detects an issue and raises a specific exception:
if existing:
    raise ConflictError(f"Color '{name}' already exists")
2

Caught in Route

The route catches the exception and displays a user-friendly message:
try:
    ColorService.create(data)
    flash("Color created successfully", "success")
except ConflictError as e:
    flash(e.message, "error")
3

Displayed in Template

Flash messages appear in the UI with appropriate styling (success/error).

Database Features

MySQL with SQLAlchemy ORM

The system uses MySQL as the database with SQLAlchemy ORM for object-relational mapping:
# config.py
SQLALCHEMY_DATABASE_URI = (
    f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
)

Connection Management

# run.py - Database connection test
with app.app_context():
    try:
        connection = db.engine.connect()
        print("Database connection successful!")
        connection.close()
    except Exception as e:
        print(f"Database connection failed: {e}")

Migrations with Flask-Migrate

Database schema changes are managed through Alembic migrations:
# Create a new migration
flask db migrate -m "Add new column to colors table"

# Apply migrations
flask db upgrade

# Rollback migrations
flask db downgrade

Audit Trail & Compliance

Every record includes comprehensive audit information:

Audit Fields

FieldTypePurpose
created_atTIMESTAMPWhen the record was created (auto-populated)
updated_atTIMESTAMPWhen the record was last modified (auto-updated)
deleted_atTIMESTAMPWhen the record was soft-deleted (nullable)
created_byVARCHAR(100)User who created the record
updated_byVARCHAR(100)User who last updated the record
deleted_byVARCHAR(100)User who deleted the record
activeBOOLEANWhether the record is active (soft delete flag)

Soft Delete Pattern

# Soft delete in service layer
@staticmethod
def delete(id_color: int) -> None:
    color = Color.query.get(id_color)
    if not color:
        raise NotFoundError(f"Color with ID {id_color} not found")
    
    # Mark as inactive instead of deleting
    color.active = False
    color.deleted_at = func.current_timestamp()
    # color.deleted_by = current_user.username  # When auth is implemented
    
    db.session.commit()
Soft deletes preserve data integrity and allow for audit trails and potential data recovery.

Security Features

Environment-Based Configuration

Sensitive data is stored in environment variables, never in code:
# config.py
class Config:
    DB_USER = os.getenv("DB_USER")
    DB_PASSWORD = os.getenv("DB_PASSWORD")
    DB_HOST = os.getenv("DB_HOST")
    DB_PORT = os.getenv("DB_PORT")
    DB_NAME = os.getenv("DB_NAME")
    
    SECRET_KEY = os.getenv("SECRET_KEY")
    if not SECRET_KEY:
        if os.getenv("FLASK_ENV") == "production":
            raise ValueError("SECRET_KEY must be set in production")
        SECRET_KEY = "dev-secret-key-change-in-production"
In production, the SECRET_KEY environment variable MUST be set, or the application will refuse to start.

SQL Injection Prevention

SQLAlchemy ORM uses parameterized queries automatically:
# Safe from SQL injection
color = Color.query.filter_by(name=user_input).first()

# SQLAlchemy generates: SELECT * FROM colors WHERE name = ?
# With user_input properly escaped

Blueprint Organization

The application uses Flask Blueprints for modular organization:
# app/__init__.py
def create_app():
    app = Flask(__name__)
    
    # Register blueprints
    from .catalogs.colors import colors_bp
    app.register_blueprint(colors_bp, url_prefix='/colors')
    
    from .catalogs.wood_types import woods_types_bp
    app.register_blueprint(woods_types_bp, url_prefix='/wood-types')
    
    from .catalogs.unit_of_measures import unit_of_measures_bp
    app.register_blueprint(unit_of_measures_bp, url_prefix='/unit-of-measures')
    
    from .catalogs.roles import roles_bp
    app.register_blueprint(roles_bp, url_prefix='/roles')
    
    return app

Available Routes

Each blueprint provides RESTful routes:
RouteMethodPurpose
/colors/GETList all colors
/colors/createGET, POSTShow form / Create color
/colors/<id>/editGET, POSTShow form / Update color
/colors/<id>/deletePOSTSoft delete color
The same route structure applies to all catalogs: wood-types, unit-of-measures, roles, and furniture-type.

Template System

Jinja2 templates with inheritance for consistent UI:
<!-- templates/base.html - Base template -->
<!DOCTYPE html>
<html lang="es">
<head>
    <title>{% block title %}Muebles Roble{% endblock %}</title>
</head>
<body>
    <nav>
        <!-- Navigation menu -->
    </nav>
    
    {% with messages = get_flashed_messages(with_categories=true) %}
        {% if messages %}
            {% for category, message in messages %}
                <div class="alert alert-{{ category }}">
                    {{ message }}
                </div>
            {% endfor %}
        {% endif %}
    {% endwith %}
    
    <main>
        {% block content %}{% endblock %}
    </main>
</body>
</html>
<!-- templates/colors/create.html - Child template -->
{% extends "base.html" %}

{% block title %}Create Color - Muebles Roble{% endblock %}

{% block content %}
<h1>Create New Color</h1>
<form method="POST">
    {{ form.hidden_tag() }}
    {{ form.name.label }}
    {{ form.name }}
    <button type="submit">Create</button>
</form>
{% endblock %}

Next Steps

Installation

Install Python, create virtual environment, and set up dependencies

Configuration

Configure database connection and environment variables

First Steps

Run the application and create your first catalog entries

Build docs developers (and LLMs) love