diff --git a/app/routes.py b/app/routes.py
index 374cacaff12201ac91850fcab90d7bea677560a1..5ea611a3a70f22b47d2c2fbb33e531376250d2ec 100644
--- a/app/routes.py
+++ b/app/routes.py
@@ -14,8 +14,11 @@ from app.models import (
from app.services import (
activate_job,
complete_current_step,
+ create_station_part,
fulfill_transfer_request,
get_current_unit_and_step,
+ get_or_create_part,
+ get_or_create_station_inventory,
scan_job_barcode,
set_station_inventory,
)
@@ -114,6 +117,7 @@ def admin_jobs():
def job_detail(job_id):
job = db.get_or_404(Job, job_id)
stations = Station.query.order_by(Station.id).all()
+ parts = Part.query.order_by(Part.name).all()
if request.method == "POST":
if job.status != "Draft":
@@ -175,11 +179,15 @@ def job_detail(job_id):
),
)
- part = Part.query.filter_by(name=part_name).first()
+ try:
+ part = get_or_create_part(part_name)
+ except ValueError as error:
+ abort(400, description=str(error))
- if part is None:
- part = Part(name=part_name)
- db.session.add(part)
+ get_or_create_station_inventory(
+ station.id,
+ part.id,
+ )
next_step_number = max(
(step.step_number for step in job.steps),
@@ -224,11 +232,11 @@ def job_detail(job_id):
"job_detail.html",
job=job,
stations=stations,
+ parts=parts,
product_units=product_units,
transfer_requests=transfer_requests,
)
-
@bp.route(
"/admin/jobs/
- Production Jobs
+
+ Production Jobs
+
|
Warehouse Requests
|
- Worker Kiosk
+ Worker Kiosk
- No parts exist yet. Create a production job and add a step first.
+ No parts exist yet. Use the form above to add
+ the first part.
Station Inventory
+ Add Part to Station Inventory
+
+
+
Update Station Inventory
{% if parts %}
{% else %}
@@ -85,6 +176,7 @@
Station
Part
+ Unit
Quantity
{% endfor %}
diff --git a/app/templates/job_detail.html b/app/templates/job_detail.html
index 4fcace8aae96f73157e9099ad10140ae0e435003..b87293c623b984167d05b5453167a2c005256c08 100644
--- a/app/templates/job_detail.html
+++ b/app/templates/job_detail.html
@@ -153,8 +153,28 @@
{{ inventory.station.name }}
{{ inventory.part.name }}
+ {{ inventory.part.unit }}
{{ inventory.quantity }}
-
-
+
+
+
+
+
+
+ Select an existing part or enter a new name.
+
diff --git a/tests/test_part_management.py b/tests/test_part_management.py new file mode 100644 index 0000000000000000000000000000000000000000..7884f1f67303606c99c70c957c712bbcf813c2fb --- /dev/null +++ b/tests/test_part_management.py @@ -0,0 +1,305 @@ +import pytest + +from app import create_app, db +from app.models import ( + Job, + JobStep, + Part, + StationInventory, + StepPart, +) + + +@pytest.fixture +def app(): + test_app = create_app( + { + "TESTING": True, + "SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:", + } + ) + + yield test_app + + +def create_draft_job(name="Part Management Test"): + job = Job( + name=name, + quantity=1, + threshold_percent=20.0, + status="Draft", + ) + + db.session.add(job) + db.session.commit() + + return job.id + + +def test_tc19_create_part_directly_with_zero_quantity(app): + client = app.test_client() + + response = client.post( + "/admin/inventory", + data={ + "action": "create_part", + "part_name": "Direct Inventory Part", + "part_unit": "pcs", + "station_id": "ST-04", + "initial_quantity": "0", + }, + ) + + assert response.status_code == 302 + + with app.app_context(): + part = Part.query.filter_by( + name="Direct Inventory Part", + ).one() + + inventory = StationInventory.query.filter_by( + station_id="ST-04", + part_id=part.id, + ).one() + + assert part.unit == "pcs" + assert inventory.quantity == 0 + + +def test_tc20_existing_part_can_be_assigned_to_another_station(app): + with app.app_context(): + part = Part( + name="Reusable Part", + unit="kg", + ) + + db.session.add(part) + db.session.flush() + + db.session.add( + StationInventory( + station_id="ST-01", + part_id=part.id, + quantity=4, + ) + ) + + db.session.commit() + + part_id = part.id + + client = app.test_client() + + response = client.post( + "/admin/inventory", + data={ + "action": "create_part", + "part_name": " reusable PART ", + "part_unit": "KG", + "station_id": "ST-05", + "initial_quantity": "2", + }, + ) + + assert response.status_code == 302 + + with app.app_context(): + assert Part.query.count() == 1 + + original_inventory = ( + StationInventory.query.filter_by( + station_id="ST-01", + part_id=part_id, + ).one() + ) + + new_inventory = ( + StationInventory.query.filter_by( + station_id="ST-05", + part_id=part_id, + ).one() + ) + + assert original_inventory.quantity == 4 + assert new_inventory.quantity == 2 + + +def test_tc21_duplicate_station_part_is_rejected(app): + with app.app_context(): + part = Part( + name="Protected Part", + unit="pcs", + ) + + db.session.add(part) + db.session.flush() + + inventory = StationInventory( + station_id="ST-02", + part_id=part.id, + quantity=7, + ) + + db.session.add(inventory) + db.session.commit() + + part_id = part.id + + client = app.test_client() + + response = client.post( + "/admin/inventory", + data={ + "action": "create_part", + "part_name": "protected part", + "part_unit": "pcs", + "station_id": "ST-02", + "initial_quantity": "0", + }, + ) + + assert response.status_code == 400 + + with app.app_context(): + assert Part.query.count() == 1 + assert StationInventory.query.count() == 1 + + inventory = StationInventory.query.filter_by( + station_id="ST-02", + part_id=part_id, + ).one() + + assert inventory.quantity == 7 + + +def test_tc22_job_step_reuses_existing_part(app): + with app.app_context(): + part = Part( + name="Catalogue Part", + unit="kg", + ) + + db.session.add(part) + db.session.commit() + + part_id = part.id + job_id = create_draft_job( + "Existing Catalogue Part Test" + ) + + client = app.test_client() + + response = client.post( + f"/admin/jobs/{job_id}", + data={ + "station_id": "ST-03", + "instruction": "Install catalogue part", + "photo_url": "", + "part_name": " catalogue PART ", + "part_quantity": "1", + }, + ) + + assert response.status_code == 302 + + with app.app_context(): + assert Part.query.count() == 1 + + step = JobStep.query.one() + usage = StepPart.query.one() + + assert usage.step_id == step.id + assert usage.part_id == part_id + + inventory = StationInventory.query.filter_by( + station_id="ST-03", + part_id=part_id, + ).one() + + assert inventory.quantity == 0 + + +def test_tc23_job_step_creates_new_part_with_zero_stock(app): + with app.app_context(): + job_id = create_draft_job( + "New Job Part Test" + ) + + client = app.test_client() + + response = client.post( + f"/admin/jobs/{job_id}", + data={ + "station_id": "ST-04", + "instruction": "Install new part", + "photo_url": "", + "part_name": "New Step Part", + "part_quantity": "1", + }, + ) + + assert response.status_code == 302 + + with app.app_context(): + part = Part.query.filter_by( + name="New Step Part", + ).one() + + inventory = StationInventory.query.filter_by( + station_id="ST-04", + part_id=part.id, + ).one() + + assert part.unit == "pcs" + assert inventory.quantity == 0 + + +def test_tc24_job_step_form_contains_part_suggestions(app): + with app.app_context(): + db.session.add_all( + [ + Part(name="PA1", unit="pcs"), + Part(name="PA2", unit="kg"), + ] + ) + + db.session.commit() + + job_id = create_draft_job( + "Part Suggestion Test" + ) + + client = app.test_client() + + response = client.get( + f"/admin/jobs/{job_id}" + ) + + assert response.status_code == 200 + assert b'list="part_catalogue"' in response.data + assert b'value="PA1"' in response.data + assert b'value="PA2"' in response.data + assert b"Select an existing part or enter a new name." in ( + response.data + ) + + +def test_tc25_negative_initial_quantity_is_rejected(app): + client = app.test_client() + + response = client.post( + "/admin/inventory", + data={ + "action": "create_part", + "part_name": "Invalid Negative Part", + "part_unit": "pcs", + "station_id": "ST-01", + "initial_quantity": "-1", + }, + ) + + assert response.status_code == 400 + + with app.app_context(): + assert Part.query.count() == 0 + assert StationInventory.query.count() == 0 \ No newline at end of file