diff --git a/app/models.py b/app/models.py index 1c173720d8df51fee070c4bf6ba9a73c6f9558e0..4c0be23c4b86ff6faa71083de5e0c721fec50dfc 100644 --- a/app/models.py +++ b/app/models.py @@ -190,4 +190,41 @@ class ProductUnit(db.Model): "unit_number", name="uq_job_unit_number", ), + ) + + +class UnitStepProgress(db.Model): + __tablename__ = "unit_step_progress" + + id = db.Column(db.Integer, primary_key=True) + + product_unit_id = db.Column( + db.Integer, + db.ForeignKey("product_units.id"), + nullable=False, + ) + + job_step_id = db.Column( + db.Integer, + db.ForeignKey("job_steps.id"), + nullable=False, + ) + + status = db.Column( + db.String(20), + nullable=False, + default="Pending", + ) + + completed_at = db.Column(db.DateTime) + + product_unit = db.relationship("ProductUnit") + job_step = db.relationship("JobStep") + + __table_args__ = ( + db.UniqueConstraint( + "product_unit_id", + "job_step_id", + name="uq_unit_step_progress", + ), ) \ No newline at end of file diff --git a/app/routes.py b/app/routes.py index 0c673de3496b68f7e6e13180c4758e4fbe430e35..455b4701fb70b216b92f2c40f9552344fd53464e 100644 --- a/app/routes.py +++ b/app/routes.py @@ -13,12 +13,13 @@ from app.models import ( ) from app.services import ( activate_job, + complete_current_step, fulfill_transfer_request, + get_current_unit_and_step, scan_job_barcode, set_station_inventory, ) - bp = Blueprint("main", __name__) @@ -218,7 +219,10 @@ def worker_kiosk(): selected_station_id = "" entered_barcode = "" step = None + current_unit = None error = None + message = None + stock_message = None if request.method == "POST": selected_station_id = request.form.get( @@ -231,10 +235,61 @@ def worker_kiosk(): "", ).strip() - step, error = scan_job_barcode( - entered_barcode, - selected_station_id, - ) + action = request.form.get("action", "scan") + + if action == "complete": + normalized_barcode = entered_barcode.upper() + + job = Job.query.filter_by( + barcode=normalized_barcode, + ).first() + + if job is None: + error = "Barcode not recognized." + else: + result, error = complete_current_step( + job, + selected_station_id, + ) + + if error is None: + completed_unit = result["completed_unit"] + completed_step = result["completed_step"] + + message = ( + f"Unit {completed_unit.unit_number}, " + f"Step {completed_step.step_number} completed." + ) + + if result["transfer_requests"]: + stock_message = ( + "Station stock is below the remaining " + "production requirement. A transfer " + "request was created." + ) + + next_unit = result["next_unit"] + next_step = result["next_step"] + + if ( + next_unit is not None + and next_step is not None + and next_step.station_id + == selected_station_id + ): + current_unit = next_unit + step = next_step + + else: + step, error = scan_job_barcode( + entered_barcode, + selected_station_id, + ) + + if error is None: + current_unit, step = get_current_unit_and_step( + step.job + ) return render_template( "worker_kiosk.html", @@ -242,5 +297,8 @@ def worker_kiosk(): selected_station_id=selected_station_id, entered_barcode=entered_barcode, step=step, + current_unit=current_unit, error=error, + message=message, + stock_message=stock_message, ) \ No newline at end of file diff --git a/app/services.py b/app/services.py index e103a1809d8c6d0cce7888553d8ac825f487996c..dc23177ca8f4cdfaebe7823ff42324180add8526 100644 --- a/app/services.py +++ b/app/services.py @@ -7,6 +7,7 @@ from app.models import ( ProductUnit, StationInventory, TransferRequest, + UnitStepProgress, ) @@ -159,6 +160,25 @@ def fulfill_transfer_request(request_id, transferred_quantity): db.session.commit() return transfer_request +def get_current_unit_and_step(job): + units = ProductUnit.query.filter_by( + job_id=job.id, + ).order_by(ProductUnit.unit_number).all() + + for unit in units: + completed_step_ids = { + progress.job_step_id + for progress in UnitStepProgress.query.filter_by( + product_unit_id=unit.id, + status="Completed", + ).all() + } + + for step in job.steps: + if step.id not in completed_step_ids: + return unit, step + + return None, None def scan_job_barcode(barcode, station_id): normalized_barcode = (barcode or "").strip().upper() normalized_station_id = (station_id or "").strip().upper() @@ -179,12 +199,183 @@ def scan_job_barcode(barcode, station_id): if job.status != "Active": return None, "This production job is not active." - current_step = job.steps[0] if job.steps else None - - if current_step is None: + if not job.steps: return None, "This production job has no production steps." + current_unit, current_step = get_current_unit_and_step(job) + + if current_unit is None or current_step is None: + return None, "All product units are complete." + + if current_step.station_id != normalized_station_id: + return None, "This job is not ready at this station." + + return current_step, None +def calculate_remaining_part_requirement( + job, + station_id, + part_id, +): + remaining_quantity = 0 + + units = ProductUnit.query.filter_by( + job_id=job.id, + ).all() + + for unit in units: + completed_step_ids = { + progress.job_step_id + for progress in UnitStepProgress.query.filter_by( + product_unit_id=unit.id, + status="Completed", + ).all() + } + + for step in job.steps: + if step.id in completed_step_ids: + continue + + if step.station_id != station_id: + continue + + for usage in step.parts: + if usage.part_id == part_id: + remaining_quantity += usage.quantity + + return remaining_quantity + + +def create_remaining_material_requests( + job, + station_id, + part_ids, +): + created_requests = [] + + for part_id in part_ids: + inventory = StationInventory.query.filter_by( + station_id=station_id, + part_id=part_id, + ).first() + + available_quantity = ( + inventory.quantity + if inventory is not None + else 0 + ) + + remaining_quantity = calculate_remaining_part_requirement( + job, + station_id, + part_id, + ) + + if available_quantity >= remaining_quantity: + continue + + existing_request = TransferRequest.query.filter( + TransferRequest.job_id == job.id, + TransferRequest.station_id == station_id, + TransferRequest.part_id == part_id, + TransferRequest.status.in_( + ["Pending", "Partially Fulfilled"] + ), + ).first() + + if existing_request is not None: + continue + + transfer_request = TransferRequest( + job_id=job.id, + station_id=station_id, + part_id=part_id, + requested_quantity=( + remaining_quantity - available_quantity + ), + status="Pending", + ) + + db.session.add(transfer_request) + created_requests.append(transfer_request) + + return created_requests +def complete_current_step(job, station_id): + normalized_station_id = (station_id or "").strip().upper() + + if job.status != "Active": + return None, "This production job is not active." + + current_unit, current_step = get_current_unit_and_step(job) + + if current_unit is None or current_step is None: + return None, "All product units are complete." + if current_step.station_id != normalized_station_id: return None, "This job is not ready at this station." - return current_step, None \ No newline at end of file + inventory_usages = [] + + for usage in current_step.parts: + inventory = StationInventory.query.filter_by( + station_id=current_step.station_id, + part_id=usage.part_id, + ).first() + + available_quantity = ( + inventory.quantity + if inventory is not None + else 0 + ) + + if available_quantity < usage.quantity: + return ( + None, + f"Insufficient station inventory for {usage.part.name}.", + ) + + inventory_usages.append((inventory, usage)) + + for inventory, usage in inventory_usages: + inventory.quantity -= usage.quantity + + progress = UnitStepProgress.query.filter_by( + product_unit_id=current_unit.id, + job_step_id=current_step.id, + ).first() + + if progress is None: + progress = UnitStepProgress( + product_unit_id=current_unit.id, + job_step_id=current_step.id, + ) + db.session.add(progress) + + progress.status = "Completed" + progress.completed_at = datetime.now(timezone.utc) + current_unit.status = "In Progress" + + db.session.flush() + + transfer_requests = create_remaining_material_requests( + job, + current_step.station_id, + { + usage.part_id + for usage in current_step.parts + }, + ) + + db.session.commit() + + next_unit, next_step = get_current_unit_and_step(job) + + return ( + { + "completed_unit": current_unit, + "completed_step": current_step, + "next_unit": next_unit, + "next_step": next_step, + "transfer_requests": transfer_requests, + }, + None, + ) \ No newline at end of file diff --git a/app/templates/worker_kiosk.html b/app/templates/worker_kiosk.html index 29f870f956a42f0ed93f2215e6f777869fd9606f..469684b4c537dc99c99a6469cd24d757f4d44e86 100644 --- a/app/templates/worker_kiosk.html +++ b/app/templates/worker_kiosk.html @@ -5,15 +5,17 @@
Error: {{ error }}
++ Error: {{ error }} +
+ {% endif %} + + {% if message %} ++ {{ message }} +
{% endif %} - {% if step %} + {% if stock_message %} ++ Material Alert: + {{ stock_message }} +
+ {% endif %} + + {% if step and current_unit %}+ Product Unit: + {{ current_unit.unit_number }} + of {{ step.job.quantity }} +
+Station: {{ step.station.name }} @@ -91,6 +114,28 @@ {% else %}
No parts required for this step.
{% endif %} + + {% endif %}