diff --git a/app/__init__.py b/app/__init__.py index 78bfa455c89eb6dac846baaa62245ec87ac32114..f5d1c0aed19865919095c73b0af7ccd76c2706c6 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -5,11 +5,13 @@ from flask_sqlalchemy import SQLAlchemy db = SQLAlchemy() -def create_app(): +def create_app(test_config=None): app = Flask(__name__) app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///mes.db" app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False + if test_config: + app.config.update(test_config) db.init_app(app) @@ -30,10 +32,9 @@ def create_app(): ) ) - db.session.commit() + db.session.commit() - @app.route("/") - def index(): - return "Manufacturing Execution System - MES Prototype" + from app.routes import bp + app.register_blueprint(bp) return app \ No newline at end of file diff --git a/app/models.py b/app/models.py index 28ad520bb434c03c98b2b81f3f6a69b2a2fefe28..a20f49ecdc88904ee66da70ab188b40ce2d1bcad 100644 --- a/app/models.py +++ b/app/models.py @@ -9,4 +9,96 @@ class Station(db.Model): enabled = db.Column(db.Boolean, nullable=False, default=True) def __repr__(self): - return f"" \ No newline at end of file + return f"" + + +class Part(db.Model): + __tablename__ = "parts" + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), unique=True, nullable=False) + unit = db.Column(db.String(20), nullable=False, default="pcs") + + step_usages = db.relationship("StepPart", back_populates="part") + + def __repr__(self): + return f"" + + +class Job(db.Model): + __tablename__ = "jobs" + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(120), nullable=False) + quantity = db.Column(db.Integer, nullable=False) + threshold_percent = db.Column(db.Float, nullable=False, default=20.0) + status = db.Column(db.String(20), nullable=False, default="Draft") + + steps = db.relationship( + "JobStep", + back_populates="job", + cascade="all, delete-orphan", + order_by="JobStep.step_number", + ) + + def __repr__(self): + return f"" + + +class JobStep(db.Model): + __tablename__ = "job_steps" + + id = db.Column(db.Integer, primary_key=True) + job_id = db.Column(db.Integer, db.ForeignKey("jobs.id"), nullable=False) + step_number = db.Column(db.Integer, nullable=False) + instruction = db.Column(db.Text, nullable=False) + station_id = db.Column( + db.String(5), + db.ForeignKey("stations.id"), + nullable=False, + ) + photo_url = db.Column(db.String(255)) + + job = db.relationship("Job", back_populates="steps") + station = db.relationship("Station") + parts = db.relationship( + "StepPart", + back_populates="step", + cascade="all, delete-orphan", + ) + + __table_args__ = ( + db.UniqueConstraint( + "job_id", + "step_number", + name="uq_job_step_number", + ), + ) + + +class StepPart(db.Model): + __tablename__ = "step_parts" + + id = db.Column(db.Integer, primary_key=True) + step_id = db.Column( + db.Integer, + db.ForeignKey("job_steps.id"), + nullable=False, + ) + part_id = db.Column( + db.Integer, + db.ForeignKey("parts.id"), + nullable=False, + ) + quantity = db.Column(db.Integer, nullable=False) + + step = db.relationship("JobStep", back_populates="parts") + part = db.relationship("Part", back_populates="step_usages") + + __table_args__ = ( + db.UniqueConstraint( + "step_id", + "part_id", + name="uq_step_part", + ), + ) \ No newline at end of file diff --git a/app/routes.py b/app/routes.py new file mode 100644 index 0000000000000000000000000000000000000000..b8be6dcdab801a1d430af4a3a59edd7893b34723 --- /dev/null +++ b/app/routes.py @@ -0,0 +1,91 @@ +from flask import Blueprint, abort, redirect, render_template, request, url_for + +from app import db +from app.models import Job, JobStep, Part, Station, StepPart + + +bp = Blueprint("main", __name__) + + +@bp.route("/") +def index(): + return redirect(url_for("main.admin_jobs")) + + +@bp.route("/admin/jobs", methods=["GET", "POST"]) +def admin_jobs(): + if request.method == "POST": + name = request.form["name"].strip() + quantity = int(request.form["quantity"]) + + threshold_value = request.form.get("threshold_percent", "").strip() + threshold_percent = float(threshold_value) if threshold_value else 20.0 + + job = Job( + name=name, + quantity=quantity, + threshold_percent=threshold_percent, + status="Draft", + ) + + db.session.add(job) + db.session.commit() + + return redirect(url_for("main.job_detail", job_id=job.id)) + + jobs = Job.query.order_by(Job.id.desc()).all() + return render_template("admin_jobs.html", jobs=jobs) + + +@bp.route("/admin/jobs/", methods=["GET", "POST"]) +def job_detail(job_id): + job = db.get_or_404(Job, job_id) + stations = Station.query.order_by(Station.id).all() + + if request.method == "POST": + station_id = request.form["station_id"] + station = db.session.get(Station, station_id) + + if station is None: + abort(400) + + part_name = request.form["part_name"].strip() + part_quantity = int(request.form["part_quantity"]) + + part = Part.query.filter_by(name=part_name).first() + + if part is None: + part = Part(name=part_name) + db.session.add(part) + + next_step_number = max( + (step.step_number for step in job.steps), + default=0, + ) + 1 + + step = JobStep( + job=job, + step_number=next_step_number, + instruction=request.form["instruction"].strip(), + station=station, + photo_url=request.form.get("photo_url", "").strip() or None, + ) + + db.session.add(step) + + step_part = StepPart( + step=step, + part=part, + quantity=part_quantity, + ) + + db.session.add(step_part) + db.session.commit() + + return redirect(url_for("main.job_detail", job_id=job.id)) + + return render_template( + "job_detail.html", + job=job, + stations=stations, + ) \ No newline at end of file diff --git a/app/templates/admin_jobs.html b/app/templates/admin_jobs.html new file mode 100644 index 0000000000000000000000000000000000000000..02a4421df170b19760d97d2baecc355bf9e62507 --- /dev/null +++ b/app/templates/admin_jobs.html @@ -0,0 +1,56 @@ + + + + + MES - Production Jobs + + +

Production Jobs

+ +

Create Production Job

+ +
+

+
+ +

+ +

+
+ +

+ +

+
+ +

+ + +
+ +

Existing Jobs

+ + {% if jobs %} +
    + {% for job in jobs %} +
  • + + {{ job.name }} + + - Quantity: {{ job.quantity }} + - Status: {{ job.status }} +
  • + {% endfor %} +
+ {% else %} +

No production jobs created yet.

+ {% endif %} + + \ No newline at end of file diff --git a/app/templates/job_detail.html b/app/templates/job_detail.html new file mode 100644 index 0000000000000000000000000000000000000000..fd25713228055c1b7d08cf0f4452ae6459e2f3f4 --- /dev/null +++ b/app/templates/job_detail.html @@ -0,0 +1,85 @@ + + + + + MES - {{ job.name }} + + +

+ Back to Jobs +

+ +

{{ job.name }}

+ +

Status: {{ job.status }}

+

Production Quantity: {{ job.quantity }}

+

Low-stock Threshold: {{ job.threshold_percent }}%

+ +

Production Steps

+ + {% if job.steps %} + {% for step in job.steps %} +
+

Step {{ step.step_number }}

+ +

Station: {{ step.station.name }}

+

Instruction: {{ step.instruction }}

+ + {% if step.photo_url %} +

Photo: {{ step.photo_url }}

+ {% endif %} + +

Required Parts:

+
    + {% for usage in step.parts %} +
  • + {{ usage.part.name }} x {{ usage.quantity }} +
  • + {% endfor %} +
+
+ {% endfor %} + {% else %} +

No steps added yet.

+ {% endif %} + +
+ +

Add Production Step

+ +
+

+
+ +

+ +

+
+ +

+ +

+
+ +

+ +

+
+ +

+ +

+
+ +

+ + +
+ + \ No newline at end of file diff --git a/tests/test_tc01_job_creation.py b/tests/test_tc01_job_creation.py new file mode 100644 index 0000000000000000000000000000000000000000..0d21363e5f65dbd96ffcb12175f30a54c6f7ec2d --- /dev/null +++ b/tests/test_tc01_job_creation.py @@ -0,0 +1,70 @@ +from app import create_app, db +from app.models import Job + + +def test_tc01_create_production_job(): + app = create_app( + { + "TESTING": True, + "SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:", + } + ) + + client = app.test_client() + + response = client.post( + "/admin/jobs", + data={ + "name": "Test LEGO Assembly", + "quantity": "2", + "threshold_percent": "20", + }, + follow_redirects=True, + ) + + assert response.status_code == 200 + + with app.app_context(): + job = Job.query.filter_by(name="Test LEGO Assembly").one() + + assert job.status == "Draft" + assert job.quantity == 2 + assert job.threshold_percent == 20.0 + + job_id = job.id + + response = client.post( + f"/admin/jobs/{job_id}", + data={ + "station_id": "ST-01", + "instruction": "Attach Part A", + "photo_url": "photo-step-1.jpg", + "part_name": "Part A", + "part_quantity": "1", + }, + follow_redirects=True, + ) + + assert response.status_code == 200 + + page = response.get_data(as_text=True) + + assert "Test LEGO Assembly" in page + assert "Draft" in page + assert "Station 1" in page + assert "Attach Part A" in page + assert "photo-step-1.jpg" in page + assert "Part A x 1" in page + + with app.app_context(): + job = db.session.get(Job, job_id) + + assert len(job.steps) == 1 + + step = job.steps[0] + + assert step.station_id == "ST-01" + assert step.instruction == "Attach Part A" + assert step.photo_url == "photo-step-1.jpg" + assert step.parts[0].part.name == "Part A" + assert step.parts[0].quantity == 1 \ No newline at end of file