Build QAForge QR Verification
Build a local Python app for QR-tracked output verification.
Introduction
30 Second Summary
Busy production handoffs often leave people hunting through rows of output details for the right batch. One wrong match can attach a quality decision to the wrong item.
In this project, you will build QAForge, a local browser app in Python for moving Store output requests through OQC. Each request receives a unique QR code linked to its stored SQLite record.
What You'll Build
You can demo a Store submission flowing into OQC through its QR label before the matching record closes with a timestamped decision.
By the end of this project, you'll have:
- A Store submission flow that returns a visible QA- identifier after you enter the output details.
- A downloadable QR label that lets OQC retrieve the matching product details from the database.
- A controlled OQC decision that records one final status with its operator, remarks, and UTC timestamp.
- Secret Mission: Break-test QAForge with blank Store data, an unknown QA identifier, and a duplicate verification attempt.
Are there any prerequisites?
You'll need Python, Visual Studio Code, and Git installed on Windows. Step 1 confirms that your Python version sits between 3.10 and 3.14 before Streamlit setup begins.
Before We Start
This is your moment to commit to the QAForge workflow before any hands-on work begins. A clear Store submission and one final OQC decision give every feature an operational purpose.
Set Up the Windows Workspace
QAForge will eventually turn Store details into traceable requests. That workflow needs a predictable Python workspace before you add any application code.
A virtual environment gives this project its own package boundary. It prevents QAForge from changing packages used by your other Python projects.
In this step, get ready to:
- Prepare the qaforge workspace in Visual Studio Code with a supported Python version.
- Build an isolated .venv environment from requirements.txt.
- Prove Streamlit works by opening its example app in your browser.
Open the QAForge workspace
A named folder keeps the environment file beside the project files that use it. Visual Studio Code also starts its integrated terminal inside the folder you open.
- Press the Windows key to open Windows search.
- Type Visual Studio Code into the search field.
- Press Enter to open Visual Studio Code.
You will see the Visual Studio Code welcome screen or your most recent workspace.
- Select File from the menu bar.
- Select Open Folder....
- Select Desktop in the folder picker.
- Select New Folder.
- Enter qaforge as the folder name.
- Press Enter to create the folder.
- Select the qaforge folder.
- Select Select Folder.
Seeing a workspace trust prompt?
Visual Studio Code may ask whether you trust the new folder. Select Yes, I trust the authors to enable all features in the workspace because you created this empty folder yourself.
That is the first piece in place. You should see qaforge at the top of the Explorer view.
The installed Python version determines whether the pinned packages can run. QAForge supports versions from 3.10 through 3.14.
- Select View from the menu bar.
- Select Terminal.
- Select the Launch Profile dropdown in the terminal panel.
- Select PowerShell to start a Windows PowerShell terminal.
- Check the installed Python version by running this command:
python --version
What does this command check?
The python command reports the version available to this terminal. A result from 3.10 through 3.14 can run the package versions selected for QAForge.
- Select the tab that matches your terminal result.
✔️ I see Python 3.10 through 3.14
Your installed Python version is supported. Keep this PowerShell terminal open for the environment commands in the next substep.
ⓧ I see an unsupported version
A version outside 3.10 through 3.14 cannot provide the supported Streamlit environment for this project.
- Use the official Streamlit installation guide to install a Python version from 3.10 through 3.14.
- Close every Visual Studio Code window after the Python installation finishes.
- Press the Windows key to reopen Windows search.
- Type Visual Studio Code into the search field.
- Press Enter to restart Visual Studio Code.
A fresh Visual Studio Code process can detect the newly installed Python version.
- Select File if the qaforge workspace is not restored.
- Select Open Folder....
- Select the qaforge folder on your Desktop.
- Select Select Folder.
- Open a fresh PowerShell terminal through View and Terminal.
- Check the new Python version by running this command:
python --version
What should you see?
The fresh terminal reads the updated Python installation. Continue when the result is from 3.10 through 3.14.
ⓧ Command not found
The terminal cannot currently find Python. Installing a supported release makes the python command available to the workspace.
- Use the official Streamlit installation guide to install a Python version from 3.10 through 3.14.
- Close every Visual Studio Code window after the Python installation finishes.
- Press the Windows key to reopen Windows search.
- Type Visual Studio Code into the search field.
- Press Enter to restart Visual Studio Code.
Restarting Visual Studio Code gives its terminal access to the new Python installation.
- Select File if the qaforge workspace is not restored.
- Select Open Folder....
- Select the qaforge folder on your Desktop.
- Select Select Folder.
- Open a fresh PowerShell terminal through View and Terminal.
- Confirm that Python is available by running this command:
python --version
What should you see?
The command now reports the installed Python version. Continue when the result is from 3.10 through 3.14.
Build the isolated environment
The requirements.txt file records the package versions that QAForge needs. This makes the environment reproducible whenever you rebuild it.
Streamlit provides the local browser interface. Segno supplies the QR generation capability used later in the workflow.
- Select the Explorer view in the Activity Bar.
- Select the New File... button beside the qaforge folder.
- Enter requirements.txt as the file name.
- Press Enter to create the file.
- Add the pinned packages by pasting this content into requirements.txt:
streamlit==1.63.0
segno==1.6.6
What does this file control?
- The first line pins Streamlit to version 1.63.0 for a consistent local interface.
- The second line pins Segno to version 1.6.6 for reproducible QR generation.
- Press Ctrl+S to save requirements.txt.
You should see requirements.txt inside qaforge in the Explorer view. Its editor tab should no longer show an unsaved-change indicator.
Can't find requirements.txt?
Confirm that the Explorer view shows qaforge as the open folder. Create requirements.txt directly under that folder.
If the file name or location looks different, help me place requirements.txt inside my qaforge folder.
✔️ Awesome, I've got everything!
Your saved requirements.txt file is ready to build the environment.
ⓧ I'd like to double check the full code
Compare your complete requirements.txt file with this reference.
streamlit==1.63.0
segno==1.6.6
Why should these lines match?
Matching both lines ensures that the installation reads the exact package versions selected for QAForge.
The environment itself lives in a .venv folder inside qaforge. Python keeps packages installed there separate from your other projects.
- Create the isolated environment by running this command in the PowerShell terminal:
python -m venv .venv
Why use this command?
The command asks Python to create an environment named .venv inside the current folder. That environment becomes the private package space for QAForge.
You should see a new .venv folder under qaforge in the Explorer view.
Don't see the environment folder?
Confirm that the terminal started inside the open qaforge workspace. Check that the earlier version command reported Python 3.10 through 3.14.
If the folder still does not appear, help me create .venv inside my qaforge folder.
- Activate the environment in Windows PowerShell by running this command:
.venv\Scripts\Activate.ps1
What does activation change?
Activation points the current terminal at the Python tools inside .venv. Package installations from this terminal now stay inside the QAForge environment.
- Select the tab that matches the activation result.
✔️ The environment is active
You should see the environment name at the start of the terminal prompt. The terminal is ready to install QAForge's packages.
ⓧ PowerShell blocks activation
PowerShell policies can block local activation scripts on some Windows devices. Command Prompt can activate the same environment through its batch file.
- Select the Launch Profile dropdown in the terminal panel.
- Select Command Prompt.
- Activate the existing environment by running this command:
.venv\Scripts\activate.bat
What does the fallback do?
The batch file activates the existing .venv environment through Command Prompt. It uses the same isolated Python tools as the PowerShell activation path.
You should see the environment name at the start of the Command Prompt line.
Still unable to activate the environment?
Confirm that .venv appears directly inside the open qaforge folder before retrying the fallback.
If neither activation path works, help me activate my QAForge virtual environment on Windows.
The active environment is now ready for the pinned dependencies. The installation can take a few minutes while pip downloads the packages.
- Install the packages from requirements.txt by running this command:
pip install -r requirements.txt
What does this command do?
The pip command reads both pinned entries from requirements.txt. It installs those packages inside the active environment.
You should see the installation finish without an error. The terminal prompt remains connected to the active environment.
Package installation failed?
Confirm that the environment name appears at the start of the terminal prompt. Check that requirements.txt is saved directly inside qaforge.
If the installation still fails, help me fix the QAForge package installation.
Launch the Streamlit example
The installed example gives you a visible test of the whole setup. A browser page proves that the active environment can start Streamlit successfully.
Before you run the final check, do you expect the example app to open automatically or stay only in the terminal?
- Start the Streamlit example from the active environment by running this command:
python -m streamlit hello
What does this command do?
The command starts Streamlit through the Python executable connected to your active environment. Streamlit serves its example app to your local browser.
You should see the Streamlit example app open in your browser. This confirms that the pinned installation can serve a local app.
Didn't see the example app?
Confirm that the package installation finished without an error. Check that the terminal still shows the active environment.
If the browser page still does not open, help me launch the Streamlit Hello app.
- Press Ctrl+C in the terminal to stop the Streamlit server.
You should see the terminal return to its prompt. Your browser example proved the environment works before you stopped the server.
Your isolated Windows workspace is ready. Next, you will turn Store output details into the first persistent QA request.
Capture Store Output
The Streamlit example from Step 1 proved that your environment can serve a local browser app. Your activated virtual environment is ready for QAForge.
QAForge now needs to turn Store input into a persistent request. The request must survive browser reruns. It also needs a unique identifier before QR lookup or OQC decisions can build on it.
In this step, get ready to:
- Create the app.py foundation that generates unique QA identifiers.
- Initialize a local SQLite database for persistent Store requests.
- Submit a sample output through the Store form.
Create the QAForge application file
The application foundation defines the database location. It also creates identifiers with a date plus random characters so each request can be traced separately.
- Click the New File icon beside the QAFORGE heading in the Explorer sidebar.
- Type app.py.
- Press Enter.
- Add the application foundation by pasting this code into app.py:
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from uuid import uuid4
import streamlit as st
DB_PATH = Path("qaforge.db")
PENDING_STATUS = "PENDING OQC"
FINAL_STATUSES = ("APPROVED", "REJECTED", "RETURNED")
def utc_now():
return datetime.now(timezone.utc).isoformat(timespec="seconds")
def make_qa_code():
date_part = datetime.now(timezone.utc).strftime("%Y%m%d")
random_part = uuid4().hex[:8].upper()
return f"QA-{date_part}-{random_part}"
def connect_db():
return sqlite3.connect(DB_PATH)
st.set_page_config(page_title="QAForge", layout="wide")
st.title("QAForge")
st.caption("Store output submission and OQC verification with QR traceability")
What does this code do?
- DB_PATH points every database operation at the local qaforge.db file.
- PENDING_STATUS gives every new request the same starting workflow state.
- FINAL_STATUSES reserves the three decisions that OQC can record later.
- utc_now() creates a timezone-aware UTC audit timestamp with second-level precision.
- make_qa_code() combines the current date with eight uppercase hexadecimal characters.
- connect_db() gives the remaining helpers one consistent route to the database.
- Save app.py by pressing Ctrl+S.
Streamlit keeps using the terminal while the development server runs. Leave that terminal running so saved changes can reach the browser.
- Switch back to the Windows PowerShell terminal from Step 1.
- Start QAForge by running this command:
python -m streamlit run app.py
What does this command do?
This starts app.py with Streamlit's local development server. The server renders the Python interface in your browser.
You should see the QAForge title above the Store output and OQC verification caption.
Good start. Your own Streamlit app is now running in the browser.
Does the QAForge page fail to load?
Check that the PowerShell prompt still begins with (.venv). Reactivate the environment from Step 1 if that prefix is missing.
Check that app.py is listed directly inside the qaforge folder.
Ask for help with my QAForge launch problem.
Add persistent request storage
A browser form can disappear during a rerun. The qa_requests table gives every submission a durable row with required fields plus an audit timestamp.
- In app.py find return sqlite3.connect(DB_PATH).
- Insert the database initializer below that line by pasting this code:
def init_db():
connection = connect_db()
connection.execute(
"""
CREATE TABLE IF NOT EXISTS qa_requests (
qa_code TEXT PRIMARY KEY,
product_code TEXT NOT NULL,
lot_number TEXT NOT NULL,
quantity INTEGER NOT NULL CHECK (quantity > 0),
submitted_by TEXT NOT NULL,
submitted_at TEXT NOT NULL,
status TEXT NOT NULL CHECK (
status IN ('PENDING OQC', 'APPROVED', 'REJECTED', 'RETURNED')
),
verified_by TEXT,
verified_at TEXT,
remarks TEXT
)
"""
)
connection.commit()
connection.close()
init_db()
How does the database initializer work?
- CREATE TABLE IF NOT EXISTS makes the setup safe to run each time Streamlit reruns the app.
- qa_code acts as the primary key so each request has one unique database identity.
- CHECK (quantity > 0) prevents the database from accepting a zero or negative output quantity.
- status accepts only the four workflow states defined by QAForge.
- commit() preserves the table creation before the connection closes.
- Save app.py.
- Return to the Explorer sidebar to confirm the new database file.
You should see qaforge.db inside the qaforge folder. That file proves SQLite initialized the persistent database.
The storage layer is alive. QAForge can now keep records beyond a single form submission.
Do you still only see app.py?
Confirm that init_db() sits above st.set_page_config(page_title="QAForge", layout="wide").
Save the file again. Check the PowerShell terminal for a Python syntax problem near the pasted database code.
Help me find why qaforge.db was not created.
- In app.py locate the standalone init_db() call.
- Insert the request-writing helper directly above that call by pasting this code:
def create_request(qa_code, product_code, lot_number, quantity, submitted_by):
connection = connect_db()
connection.execute(
"""
INSERT INTO qa_requests (
qa_code, product_code, lot_number, quantity, submitted_by,
submitted_at, status
) VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(qa_code, product_code, lot_number, quantity, submitted_by, utc_now(), PENDING_STATUS),
)
connection.commit()
connection.close()
How are requests saved safely?
- The SQL statement uses ? placeholders for parameterized SQL.
- utc_now() records when Store submitted the output.
- PENDING_STATUS saves the new row as PENDING OQC.
- commit() writes the inserted row to disk before the connection closes.
- Save app.py.
- Return to the browser to confirm QAForge still loads without a Python error.
You should still see the QAForge heading. This confirms that the new persistence helper loads successfully.
Does the app stop after the insert helper?
Check that every line inside create_request() uses four spaces of indentation. Make sure the function appears above the standalone init_db() call.
Help me fix my create_request function.
Build and submit the Store form
A Store operator needs four pieces of production context. A single form groups those values into one submission so the database receives a complete request.
- In app.py locate the caption below the QAForge title.
- Add the Store form below that caption by pasting this code:
with st.form("store_output_form"):
product_code = st.text_input("Product code")
lot_number = st.text_input("Batch or lot number")
quantity = st.number_input("Output quantity", min_value=1, value=1, step=1)
submitted_by = st.text_input("Store operator")
store_submitted = st.form_submit_button("Send to OQC")
What does the Store form collect?
- product_code identifies the product that Store produced.
- lot_number connects the request to one batch or lot.
- quantity starts at one. The input prevents values below one.
- submitted_by records the Store operator responsible for the submission.
- store_submitted becomes active when the operator submits the form.
- Save app.py.
- Return to the browser to inspect the Store form.
You should see fields for product code, batch or lot number, output quantity, and Store operator. You should also see the Send to OQC button.
Is the Store form missing?
Check that the form code appears below the caption. Make sure every field inside with st.form("store_output_form"): is indented by four spaces.
Help me restore the Store form.
The form now collects the data. The submission handler validates required text before it creates the QA identifier and writes the request.
- Add the submission handler below the form by pasting this code:
if store_submitted:
product_code = product_code.strip().upper()
lot_number = lot_number.strip().upper()
submitted_by = submitted_by.strip()
if not product_code or not lot_number or not submitted_by:
st.error("Complete every required Store field.")
else:
qa_code = make_qa_code()
create_request(qa_code, product_code, lot_number, int(quantity), submitted_by)
st.success(f"Request {qa_code} was sent to OQC.")
How does submission become a request?
- strip() removes accidental spaces from the text inputs.
- upper() normalizes the product code plus the lot number for consistent storage.
- The required-field check prevents incomplete text data from reaching SQLite.
- make_qa_code() creates the traceable identifier only after validation passes.
- create_request() saves the normalized values with the quantity plus Store operator.
- Save app.py.
Before you submit the sample, what prefix do you expect the new request identifier to use?
- Return to the QAForge page in your browser.
- Enter P-100 in the Product code field.
- Enter LOT-01 in the Batch or lot number field.
- Enter 25 in the Output quantity field.
- Enter your name in the Store operator field.
- Click Send to OQC.
You should see a success message containing a new identifier that begins with QA-. The inserted database row uses PENDING OQC as its starting status.
You have created QAForge's first persistent Store request. The identifier on screen now points to a record stored in qaforge.db.
Does the request fail to save?
Confirm that every text field contains a visible value. Check the PowerShell terminal for an indentation problem in the submission handler.
If the form clears without a success message, compare the create_request() arguments with the helper definition. Their order must match.
Help me debug my Store submission.
Use these tabs to confirm that your complete app.py matches the working version for this step.
✔️ Awesome, I've got everything!
Great. Keep the Streamlit server running. Make sure app.py is saved before moving on.
ⓧ I'd like to double check the full code
Compare your complete app.py with this reference:
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from uuid import uuid4
import streamlit as st
DB_PATH = Path("qaforge.db")
PENDING_STATUS = "PENDING OQC"
FINAL_STATUSES = ("APPROVED", "REJECTED", "RETURNED")
def utc_now():
return datetime.now(timezone.utc).isoformat(timespec="seconds")
def make_qa_code():
date_part = datetime.now(timezone.utc).strftime("%Y%m%d")
random_part = uuid4().hex[:8].upper()
return f"QA-{date_part}-{random_part}"
def connect_db():
return sqlite3.connect(DB_PATH)
def init_db():
connection = connect_db()
connection.execute(
"""
CREATE TABLE IF NOT EXISTS qa_requests (
qa_code TEXT PRIMARY KEY,
product_code TEXT NOT NULL,
lot_number TEXT NOT NULL,
quantity INTEGER NOT NULL CHECK (quantity > 0),
submitted_by TEXT NOT NULL,
submitted_at TEXT NOT NULL,
status TEXT NOT NULL CHECK (
status IN ('PENDING OQC', 'APPROVED', 'REJECTED', 'RETURNED')
),
verified_by TEXT,
verified_at TEXT,
remarks TEXT
)
"""
)
connection.commit()
connection.close()
def create_request(qa_code, product_code, lot_number, quantity, submitted_by):
connection = connect_db()
connection.execute(
"""
INSERT INTO qa_requests (
qa_code, product_code, lot_number, quantity, submitted_by,
submitted_at, status
) VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(qa_code, product_code, lot_number, quantity, submitted_by, utc_now(), PENDING_STATUS),
)
connection.commit()
connection.close()
init_db()
st.set_page_config(page_title="QAForge", layout="wide")
st.title("QAForge")
st.caption("Store output submission and OQC verification with QR traceability")
with st.form("store_output_form"):
product_code = st.text_input("Product code")
lot_number = st.text_input("Batch or lot number")
quantity = st.number_input("Output quantity", min_value=1, value=1, step=1)
submitted_by = st.text_input("Store operator")
store_submitted = st.form_submit_button("Send to OQC")
if store_submitted:
product_code = product_code.strip().upper()
lot_number = lot_number.strip().upper()
submitted_by = submitted_by.strip()
if not product_code or not lot_number or not submitted_by:
st.error("Complete every required Store field.")
else:
qa_code = make_qa_code()
create_request(qa_code, product_code, lot_number, int(quantity), submitted_by)
st.success(f"Request {qa_code} was sent to OQC.")
How should the full file be ordered?
The imports plus constants come first. The helper functions follow before init_db() starts the database and the Streamlit interface renders.
Your Store workflow now turns production details into persistent QA requests. Next, you will expose those pending requests to OQC and see why a queue alone cannot provide reliable scan-to-record lookup.
Show the Pending OQC Queue
Your Store form now turns production details into a persistent QA request. The saved P-100 request proves the Store side of the workflow works.
OQC needs a queue that makes every pending request visible. After several submissions, that table also reveals how difficult it is to find one specific request among the rows.
In this step, get ready to:
- Convert pending database rows into dictionaries with readable labels.
- Add separate Store Output and OQC Verification tabs.
- Submit a second request to test the limits of the pending queue.
Query pending requests
A SQLite query returns rows without the readable labels your table needs. Dictionaries pair each selected value with a column name that Streamlit can display.
- In the VS Code Explorer sidebar, select app.py.
- Find the closing line of create_request().
- Insert the row-conversion helper below create_request() by adding this code:
def cursor_rows_as_dicts(cursor):
columns = [column[0] for column in cursor.description]
return [dict(zip(columns, row)) for row in cursor.fetchall()]
What does this helper do?
- The columns list reads the selected column labels from the query cursor.
- The returned list pairs each label with the matching value from every database row.
- Each dictionary becomes one readable record for the pending table.
- Place the pending-request query directly below cursor_rows_as_dicts() by adding this code:
def get_pending_requests():
connection = connect_db()
cursor = connection.execute(
"""
SELECT
qa_code AS "QA Code",
product_code AS "Product",
lot_number AS "Lot",
quantity AS "Quantity",
submitted_by AS "Submitted By",
submitted_at AS "Submitted At"
FROM qa_requests
WHERE status = ?
ORDER BY submitted_at ASC
""",
(PENDING_STATUS,),
)
rows = cursor_rows_as_dicts(cursor)
connection.close()
return rows
How does the query protect the workflow?
- The WHERE status = ? condition limits the result to requests waiting for OQC.
- The separate PENDING_STATUS parameter keeps the status value outside the SQL text.
- The ORDER BY submitted_at ASC clause places the oldest pending request first.
- The readable aliases become the column headings OQC sees.
- Save app.py.
- Return to the local QAForge page in your browser.
You should still see the Store form without a Python traceback. This confirms both helpers load successfully.
Seeing a Python error?
Check that both helpers sit above init_db(). Python must define each function before the interface calls it.
Check the indentation inside get_pending_requests(). Every line in the function body uses four spaces.
Ask for help with the query error.
Split the interface into role tabs
Store and OQC perform different jobs in the same app. Tabs give each role a dedicated view while keeping both sides connected to qaforge.db.
- In app.py, select the existing Store form from with st.form("store_output_form"): through the store_submitted assignment.
- Replace the selected form block with this tabbed Store form:
store_tab, oqc_tab = st.tabs(["Store Output", "OQC Verification"])
with store_tab:
st.subheader("Submit production output")
with st.form("store_output_form"):
product_code = st.text_input("Product code")
lot_number = st.text_input("Batch or lot number")
quantity = st.number_input(
"Output quantity",
min_value=1,
value=1,
step=1,
)
submitted_by = st.text_input("Store operator")
store_submitted = st.form_submit_button("Send to OQC")
What changes in the interface?
The store_tab container holds the existing Store form. The new oqc_tab container reserves a separate view for quality verification.
The Store fields keep their original labels and validation settings. Existing submissions continue to use the same database helper.
- Find the existing if store_submitted: block below the form.
- Replace that block with this indented version:
if store_submitted:
product_code = product_code.strip().upper()
lot_number = lot_number.strip().upper()
submitted_by = submitted_by.strip()
if not product_code or not lot_number or not submitted_by:
st.error("Complete every required Store field.")
else:
qa_code = make_qa_code()
create_request(
qa_code,
product_code,
lot_number,
int(quantity),
submitted_by,
)
st.success(f"Request {qa_code} was sent to OQC.")
Why indent the submission block?
The extra indentation places the submission logic inside store_tab. Store validation messages and generated QA identifiers now stay in the Store Output view.
- Save app.py.
- Return to the local QAForge page in your browser.
You should see Store Output and OQC Verification at the top of the app. The Store form should appear inside the first tab.
Don't see both tabs?
Check that store_tab, oqc_tab = st.tabs(["Store Output", "OQC Verification"]) appears immediately after the caption.
Restart the Streamlit server with the same run command from the previous step if the browser still shows the older interface.
Ask for help with the tab layout.
Display the pending OQC queue
The OQC tab can now call get_pending_requests() on every app run. An interactive table turns the query result into a queue that OQC can inspect.
- In app.py, move to the end of the Store submission block.
- Add the OQC tab block below the final Store success message:
with oqc_tab:
st.subheader("Pending OQC requests")
pending_requests = get_pending_requests()
if pending_requests:
st.dataframe(pending_requests, width="stretch", hide_index=True)
else:
st.write("No requests are waiting for OQC verification.")
What does the queue show?
- The pending_requests value holds every record whose status is PENDING OQC.
- The interactive table displays the readable labels created by the SQL query.
- The empty-state message keeps the tab understandable when no pending work exists.
- Save app.py.
- Return to the QAForge page in your browser.
- Select the OQC Verification tab.
You should see the saved P-100 request as one row in the pending table. Its status is excluded because every displayed row is already waiting for OQC.
Is the pending table empty?
Return to Store Output to confirm that your sample request was submitted successfully. A successful submission displays its generated QA identifier.
Check that DB_PATH still points to qaforge.db in the same folder as app.py.
Ask for help with the empty queue.
✔️ Awesome, I've got everything!
Great. Save app.py before testing the queue with another request.
ⓧ I'd like to double check the full code
Compare your cumulative app.py with this version:
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from uuid import uuid4
import streamlit as st
DB_PATH = Path("qaforge.db")
PENDING_STATUS = "PENDING OQC"
FINAL_STATUSES = ("APPROVED", "REJECTED", "RETURNED")
def utc_now():
return datetime.now(timezone.utc).isoformat(timespec="seconds")
def make_qa_code():
date_part = datetime.now(timezone.utc).strftime("%Y%m%d")
random_part = uuid4().hex[:8].upper()
return f"QA-{date_part}-{random_part}"
def connect_db():
return sqlite3.connect(DB_PATH)
def init_db():
connection = connect_db()
connection.execute(
"""
CREATE TABLE IF NOT EXISTS qa_requests (
qa_code TEXT PRIMARY KEY,
product_code TEXT NOT NULL,
lot_number TEXT NOT NULL,
quantity INTEGER NOT NULL CHECK (quantity > 0),
submitted_by TEXT NOT NULL,
submitted_at TEXT NOT NULL,
status TEXT NOT NULL CHECK (
status IN ('PENDING OQC', 'APPROVED', 'REJECTED', 'RETURNED')
),
verified_by TEXT,
verified_at TEXT,
remarks TEXT
)
"""
)
connection.commit()
connection.close()
def create_request(qa_code, product_code, lot_number, quantity, submitted_by):
connection = connect_db()
connection.execute(
"""
INSERT INTO qa_requests (
qa_code,
product_code,
lot_number,
quantity,
submitted_by,
submitted_at,
status
) VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
qa_code,
product_code,
lot_number,
quantity,
submitted_by,
utc_now(),
PENDING_STATUS,
),
)
connection.commit()
connection.close()
def cursor_rows_as_dicts(cursor):
columns = [column[0] for column in cursor.description]
return [dict(zip(columns, row)) for row in cursor.fetchall()]
def get_pending_requests():
connection = connect_db()
cursor = connection.execute(
"""
SELECT
qa_code AS "QA Code",
product_code AS "Product",
lot_number AS "Lot",
quantity AS "Quantity",
submitted_by AS "Submitted By",
submitted_at AS "Submitted At"
FROM qa_requests
WHERE status = ?
ORDER BY submitted_at ASC
""",
(PENDING_STATUS,),
)
rows = cursor_rows_as_dicts(cursor)
connection.close()
return rows
init_db()
st.set_page_config(page_title="QAForge", layout="wide")
st.title("QAForge")
st.caption("Store output submission and OQC verification with QR traceability")
store_tab, oqc_tab = st.tabs(["Store Output", "OQC Verification"])
with store_tab:
st.subheader("Submit production output")
with st.form("store_output_form"):
product_code = st.text_input("Product code")
lot_number = st.text_input("Batch or lot number")
quantity = st.number_input(
"Output quantity",
min_value=1,
value=1,
step=1,
)
submitted_by = st.text_input("Store operator")
store_submitted = st.form_submit_button("Send to OQC")
if store_submitted:
product_code = product_code.strip().upper()
lot_number = lot_number.strip().upper()
submitted_by = submitted_by.strip()
if not product_code or not lot_number or not submitted_by:
st.error("Complete every required Store field.")
else:
qa_code = make_qa_code()
create_request(
qa_code,
product_code,
lot_number,
int(quantity),
submitted_by,
)
st.success(f"Request {qa_code} was sent to OQC.")
with oqc_tab:
st.subheader("Pending OQC requests")
pending_requests = get_pending_requests()
if pending_requests:
st.dataframe(pending_requests, width="stretch", hide_index=True)
else:
st.write("No requests are waiting for OQC verification.")
One row proves the queue can display pending work. A second request gives you enough data to test whether the table supports OQC's real retrieval task.
- Select the Store Output tab.
- Enter P-200 in the Product code field.
- Enter LOT-02 in the Batch or lot number field.
- Set Output quantity to 10.
- Enter your name in the Store operator field.
- Click Send to OQC.
Good work. The second visible QA identifier confirms another pending request is stored in qaforge.db.
Before you switch tabs, do you think the queue can retrieve only the new request? Keep your prediction in mind.
- Select the OQC Verification tab.
- Check whether the page offers a field for one QA identifier.
You should see both pending requests in the table. You should not see a scan-or-type lookup field for retrieving one record.
The limitation is intentional
The queue makes pending work visible to OQC. It still forces the operator to hunt through rows when they need one request.
You have now exposed the missing scan-to-record path through a real workflow test. That observed gap gives the next improvement a clear purpose.
Your pending queue now shows OQC every request waiting for review. Next, you will connect one label directly to its authoritative database record.
Connect QR Codes to OQC Lookup
Your pending queue makes every waiting request visible. However, an OQC operator still has to hunt through the rows to find one specific output.
In this step, you will add a QR code to each new request in your Streamlit app. The encoded identifier will retrieve the authoritative record from SQLite.
In this step, get ready to:
- Generate a downloadable PNG QR code for every new Store request.
- Add a scan-or-type lookup form to the OQC Verification tab.
- Retrieve the matching database record from its QA identifier.
Generate a QR code for each request
A QR image needs a binary buffer before Streamlit can display or download it. Segno creates that PNG in memory from the request's QA identifier.
- In app.py, replace the existing import group with this updated group:
import io
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from uuid import uuid4
import segno
import streamlit as st
What changed in the imports?
- The io module provides the in-memory buffer that receives the PNG data.
- The segno package generates the QR image from the QA identifier.
- Save app.py.
- Return to the QAForge browser tab from earlier.
You should see QAForge reload with the Store Output and OQC Verification tabs still available. This confirms the new imports load inside your active environment.
Does QAForge stop loading?
Check that the terminal running QAForge still uses the .venv environment from Step 1. Also confirm that segno==1.6.6 remains in requirements.txt.
If the import still fails, help me check which Python environment is running QAForge.
- Add the QR generator below get_pending_requests() in app.py by copying this function:
def make_qr_png(qa_code):
buffer = io.BytesIO()
qr_code = segno.make_qr(qa_code)
qr_code.save(buffer, kind="png", scale=8)
return buffer.getvalue()
What does this code do?
- The buffer holds the generated image in memory. This avoids creating a temporary file on disk.
- segno.make_qr() encodes only the stable QA identifier. Product details remain in the database as the authoritative record.
- qr_code.save() writes PNG data at a readable scale of 8.
- buffer.getvalue() returns the bytes that Streamlit can display or offer as a download.
- In the Store submission section of app.py, replace the complete if store_submitted: block with this updated version:
if store_submitted:
product_code = product_code.strip().upper()
lot_number = lot_number.strip().upper()
submitted_by = submitted_by.strip()
if not product_code or not lot_number or not submitted_by:
st.error("Complete every required Store field.")
else:
qa_code = make_qa_code()
create_request(
qa_code,
product_code,
lot_number,
int(quantity),
submitted_by,
)
qr_png = make_qr_png(qa_code)
st.success(f"Request {qa_code} was sent to OQC.")
st.image(qr_png, caption=qa_code, width=260)
st.download_button(
"Download QR PNG",
data=qr_png,
file_name=f"{qa_code}.png",
mime="image/png",
on_click="ignore",
)
How does the Store request become a QR image?
- make_qr_png() converts the newly generated qa_code into PNG bytes after the request is saved.
- st.image() displays the QR image with the same identifier as its caption.
- st.download_button() downloads the image with the QA identifier in its filename.
- Save app.py.
- Return to the Store Output tab.
- Enter P-100 in the Product code field.
- Enter LOT-01 in the Batch or lot number field.
- Set Output quantity to 25.
- Enter your name in the Store operator field.
- Click Send to OQC.
You should see a new identifier beginning with QA- above a QR image. Your Store request now carries a machine-readable link to its workflow record.
- Record the generated identifier here: your new QA code.
- Click Download QR PNG.
You should receive a file named in the QA-YYYYMMDD-XXXXXXXX.png format. The identifier in that filename matches the caption below the QR image.
Don't see the QR image?
Confirm that qr_png = make_qr_png(qa_code) sits inside the successful Store submission branch. Its indentation should match the nearby success message.
If the request saves without showing an image, help me check the QR generation block in app.py.
Build the OQC lookup
The QR code carries a lookup key. OQC now needs a parameterized query that uses that key to retrieve one complete request without trusting data from the image itself.
- In app.py, add this function between get_pending_requests() and make_qr_png():
def get_request(qa_code):
connection = connect_db()
cursor = connection.execute(
"""
SELECT
qa_code,
product_code,
lot_number,
quantity,
submitted_by,
submitted_at,
status,
verified_by,
verified_at,
remarks
FROM qa_requests
WHERE qa_code = ?
""",
(qa_code,),
)
columns = [column[0] for column in cursor.description]
row = cursor.fetchone()
connection.close()
return dict(zip(columns, row)) if row else None
What does this query retrieve?
- The query selects every Store field plus the current OQC status and audit fields.
- The ? placeholder keeps the QA identifier separate from the SQL statement.
- cursor.fetchone() returns the single matching row. A missing match becomes None.
- dict(zip(columns, row)) pairs each database column name with its stored value.
- Save app.py.
- Return to the running QAForge app.
You should see the app reload with the pending OQC queue intact. This confirms the database helper is valid before the interface starts calling it.
Does the app show a Python error?
Check that get_request() starts at the same indentation level as get_pending_requests(). Make sure the closing parenthesis after (qa_code,) remains in place.
If the helper still prevents the app from loading, help me find the syntax issue in get_request.
- In the with oqc_tab: section, find the pending-queue block that can display No requests are waiting for OQC verification..
- Add this lookup form immediately below that pending-queue block:
st.subheader("Scan or enter a QA code")
with st.form("oqc_lookup_form"):
scanned_code = st.text_input(
"QA code",
placeholder="QA-YYYYMMDD-XXXXXXXX",
key="oqc_lookup_code",
)
lookup_submitted = st.form_submit_button("Find request")
lookup_code = scanned_code.strip().upper()
if lookup_submitted and not lookup_code:
st.error("Scan or enter a QA code before searching.")
How does the lookup form prepare a scan?
- oqc_lookup_form groups the QA code field with its lookup button.
- scanned_code accepts text from a keyboard-style scanner or manual typing.
- strip().upper() removes surrounding spaces and normalizes letters to uppercase.
- The blank-input check stops an empty lookup before it reaches the database.
- Save app.py.
- Return to the OQC Verification tab.
- Leave the QA code field empty.
- Click Find request.
You should see Scan or enter a QA code before searching. below the form. The lookup now rejects a blank scan visibly.
Don't see the lookup form?
Check that the new block is indented inside with oqc_tab:. It should sit below the complete pending-request condition.
If the form appears in the wrong tab, help me fix the OQC lookup indentation.
Retrieve the authoritative record
The normalized identifier can now call get_request(). A match reveals the current database record that OQC needs to inspect.
- Add this record-handling block immediately below the blank lookup check in app.py:
if lookup_code:
request_record = get_request(lookup_code)
if request_record is None:
st.error("No QA request matches that code.")
else:
st.write(
{
"QA Code": request_record["qa_code"],
"Product": request_record["product_code"],
"Lot": request_record["lot_number"],
"Quantity": request_record["quantity"],
"Submitted By": request_record["submitted_by"],
"Submitted At": request_record["submitted_at"],
"Status": request_record["status"],
"Verified By": request_record["verified_by"],
"Verified At": request_record["verified_at"],
"Remarks": request_record["remarks"],
}
)
How is the result displayed?
- request_record holds the row returned by the database helper.
- A missing row produces the visible No QA request matches that code. message.
- A matching row displays its Store details alongside its current status and OQC audit fields.
- Save app.py.
✔️ Awesome, I've got everything!
Great. Make sure app.py is saved before testing the complete QR lookup.
ⓧ I'd like to double check the full code
Compare your cumulative app.py with this version:
import io
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from uuid import uuid4
import segno
import streamlit as st
DB_PATH = Path("qaforge.db")
PENDING_STATUS = "PENDING OQC"
def utc_now():
return datetime.now(timezone.utc).isoformat(timespec="seconds")
def make_qa_code():
date_part = datetime.now(timezone.utc).strftime("%Y%m%d")
random_part = uuid4().hex[:8].upper()
return f"QA-{date_part}-{random_part}"
def connect_db():
return sqlite3.connect(DB_PATH)
def init_db():
connection = connect_db()
connection.execute(
"""
CREATE TABLE IF NOT EXISTS qa_requests (
qa_code TEXT PRIMARY KEY,
product_code TEXT NOT NULL,
lot_number TEXT NOT NULL,
quantity INTEGER NOT NULL CHECK (quantity > 0),
submitted_by TEXT NOT NULL,
submitted_at TEXT NOT NULL,
status TEXT NOT NULL CHECK (
status IN ('PENDING OQC', 'APPROVED', 'REJECTED', 'RETURNED')
),
verified_by TEXT,
verified_at TEXT,
remarks TEXT
)
"""
)
connection.commit()
connection.close()
def create_request(qa_code, product_code, lot_number, quantity, submitted_by):
connection = connect_db()
connection.execute(
"""
INSERT INTO qa_requests (
qa_code,
product_code,
lot_number,
quantity,
submitted_by,
submitted_at,
status
) VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
qa_code,
product_code,
lot_number,
quantity,
submitted_by,
utc_now(),
PENDING_STATUS,
),
)
connection.commit()
connection.close()
def cursor_rows_as_dicts(cursor):
columns = [column[0] for column in cursor.description]
return [dict(zip(columns, row)) for row in cursor.fetchall()]
def get_pending_requests():
connection = connect_db()
cursor = connection.execute(
"""
SELECT
qa_code AS "QA Code",
product_code AS "Product",
lot_number AS "Lot",
quantity AS "Quantity",
submitted_by AS "Submitted By",
submitted_at AS "Submitted At"
FROM qa_requests
WHERE status = ?
ORDER BY submitted_at ASC
""",
(PENDING_STATUS,),
)
rows = cursor_rows_as_dicts(cursor)
connection.close()
return rows
def get_request(qa_code):
connection = connect_db()
cursor = connection.execute(
"""
SELECT
qa_code,
product_code,
lot_number,
quantity,
submitted_by,
submitted_at,
status,
verified_by,
verified_at,
remarks
FROM qa_requests
WHERE qa_code = ?
""",
(qa_code,),
)
columns = [column[0] for column in cursor.description]
row = cursor.fetchone()
connection.close()
return dict(zip(columns, row)) if row else None
def make_qr_png(qa_code):
buffer = io.BytesIO()
qr_code = segno.make_qr(qa_code)
qr_code.save(buffer, kind="png", scale=8)
return buffer.getvalue()
init_db()
st.set_page_config(page_title="QAForge", layout="wide")
st.title("QAForge")
st.caption("Store output submission and OQC verification with QR traceability")
store_tab, oqc_tab = st.tabs(["Store Output", "OQC Verification"])
with store_tab:
st.subheader("Submit production output")
with st.form("store_output_form"):
product_code = st.text_input("Product code")
lot_number = st.text_input("Batch or lot number")
quantity = st.number_input(
"Output quantity",
min_value=1,
value=1,
step=1,
)
submitted_by = st.text_input("Store operator")
store_submitted = st.form_submit_button("Send to OQC")
if store_submitted:
product_code = product_code.strip().upper()
lot_number = lot_number.strip().upper()
submitted_by = submitted_by.strip()
if not product_code or not lot_number or not submitted_by:
st.error("Complete every required Store field.")
else:
qa_code = make_qa_code()
create_request(
qa_code,
product_code,
lot_number,
int(quantity),
submitted_by,
)
qr_png = make_qr_png(qa_code)
st.success(f"Request {qa_code} was sent to OQC.")
st.image(qr_png, caption=qa_code, width=260)
st.download_button(
"Download QR PNG",
data=qr_png,
file_name=f"{qa_code}.png",
mime="image/png",
on_click="ignore",
)
with oqc_tab:
st.subheader("Pending OQC requests")
pending_requests = get_pending_requests()
if pending_requests:
st.dataframe(pending_requests, width="stretch", hide_index=True)
else:
st.write("No requests are waiting for OQC verification.")
st.subheader("Scan or enter a QA code")
with st.form("oqc_lookup_form"):
scanned_code = st.text_input(
"QA code",
placeholder="QA-YYYYMMDD-XXXXXXXX",
key="oqc_lookup_code",
)
lookup_submitted = st.form_submit_button("Find request")
lookup_code = scanned_code.strip().upper()
if lookup_submitted and not lookup_code:
st.error("Scan or enter a QA code before searching.")
if lookup_code:
request_record = get_request(lookup_code)
if request_record is None:
st.error("No QA request matches that code.")
else:
st.write(
{
"QA Code": request_record["qa_code"],
"Product": request_record["product_code"],
"Lot": request_record["lot_number"],
"Quantity": request_record["quantity"],
"Submitted By": request_record["submitted_by"],
"Submitted At": request_record["submitted_at"],
"Status": request_record["status"],
"Verified By": request_record["verified_by"],
"Verified At": request_record["verified_at"],
"Remarks": request_record["remarks"],
}
)
Use this full-file block only for comparison. Your saved file should contain the same helpers and interface sections.
Before you test the completed lookup, which Store details do you expect the QR identifier to retrieve?
- Return to the OQC Verification tab.
- Enter your new QA code in the QA code field.
- Click Find request.
You should see the matching product, lot, quantity, submitter, UTC submission time, current status, verifier, verification time, and remarks. The status should be PENDING OQC for your new request.
That closes the traceability gap. One stable identifier now takes OQC from a QR label to the current database record.
Does the lookup return no match?
Compare the value in the lookup field with the identifier shown below the QR image. Make sure every character after QA- is present.
If a valid saved identifier still returns no record, help me trace the QA code from the Store form to get_request.
Your QR label now opens the correct OQC record without a manual queue search. Next, you will add the controlled decision that completes the quality workflow.
Record the OQC Decision
Your QR lookup now connects each label to the authoritative SQLite record. OQC can inspect exactly what Store submitted before deciding its outcome.
A lookup still leaves the request unfinished. OQC needs a controlled decision that changes only a pending record.
This step adds the decision form. It also records the operator details behind one final status.
In this step, get ready to:
- Add a pending-only database update for final OQC decisions.
- Show a decision form when OQC retrieves a pending request.
- Prove that a completed record is locked against another decision.
Protect the final status in SQLite
A parameterized SQL update keeps values separate from the query. Its status condition limits the change to a request that is still waiting for OQC.
- Locate the get_request(qa_code) helper in app.py.
- Add the pending-only update helper below get_request(qa_code) by pasting this code:
def verify_request(qa_code, status, verified_by, remarks):
connection = connect_db()
connection.execute("UPDATE qa_requests SET status = ?, verified_by = ?, verified_at = ?, remarks = ? WHERE qa_code = ? AND status = ?", (status, verified_by, utc_now(), remarks, qa_code, PENDING_STATUS))
connection.commit()
connection.close()
How Does the Pending-Only Update Work?
- Each ? placeholder receives one value from the separate parameter tuple.
- The WHERE qa_code = ? AND status = ? condition targets one matching request that still has the pending status.
- The utc_now() call records when OQC made the decision in UTC.
- The commit preserves the final decision in qaforge.db.
- Save app.py.
- Return to the running QAForge browser tab.
- Look up the pending QA code from the previous step.
The running Streamlit app reruns automatically. You should still see the stored request without a traceback.
Does the App Show a Traceback?
Check that verify_request() starts at the left edge of app.py. Its lines must not remain indented inside get_request().
Confirm that the update tuple includes the values in the same order as the query placeholders.
help me debug the new pending-only SQLite update
Gate the OQC decision by status
The stored status now controls the interface. A pending request receives the decision form while a completed request receives a final-state message.
- Locate the st.write call that displays the matching request inside the OQC tab.
- Paste this conditional block directly below that st.write call:
if request_record["status"] == PENDING_STATUS:
with st.form(f"verification_form_{lookup_code}"):
verified_by = st.text_input("OQC operator")
verdict = st.selectbox("Decision", FINAL_STATUSES)
remarks = st.text_area("Inspection remarks")
verification_submitted = st.form_submit_button("Save OQC decision")
if verification_submitted:
verified_by = verified_by.strip()
remarks = remarks.strip()
if not verified_by:
st.error("Enter the OQC operator name.")
elif verdict in ("REJECTED", "RETURNED") and not remarks:
st.error("Remarks are required for rejected or returned output.")
else:
verify_request(lookup_code, verdict, verified_by, remarks)
st.success(f"{lookup_code} was recorded as {verdict}. Submit the lookup again to refresh the displayed status.")
else:
st.write("This request already has a final OQC decision.")
What Does This Interface Control?
- The status check displays the form only when the retrieved record matches PENDING_STATUS.
- The Decision menu limits the verdict to the values in FINAL_STATUSES.
- The operator check rejects a blank name before the database update runs.
- The remarks check requires an explanation for REJECTED or RETURNED output.
- A successful submission passes the cleaned values to verify_request().
- The final branch replaces the form with a message after the request leaves the pending state.
- Save app.py.
- Return to the running QAForge browser tab.
- Enter the pending QA identifier in the QA code field.
- Click Find request.
You should see OQC operator, Decision, and Inspection remarks beneath the pending record. The Save OQC decision button completes the form.
Is the Decision Form Missing?
Confirm that the retrieved record shows PENDING OQC as its current status. A request with a final status correctly hides the form.
Check that the new conditional block is aligned with the existing st.write call inside the matching-record branch.
help me find why the OQC decision form is missing
✔️ Awesome, I've got everything!
Your decision interface is now connected to the pending-only database update.
ⓧ I'd like to double check the full code
The complete app.py should match this reference:
import io
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from uuid import uuid4
import segno
import streamlit as st
DB_PATH = Path("qaforge.db")
PENDING_STATUS = "PENDING OQC"
FINAL_STATUSES = ("APPROVED", "REJECTED", "RETURNED")
def utc_now():
return datetime.now(timezone.utc).isoformat(timespec="seconds")
def make_qa_code():
date_part = datetime.now(timezone.utc).strftime("%Y%m%d")
random_part = uuid4().hex[:8].upper()
return f"QA-{date_part}-{random_part}"
def connect_db():
return sqlite3.connect(DB_PATH)
def init_db():
connection = connect_db()
connection.execute("""CREATE TABLE IF NOT EXISTS qa_requests (qa_code TEXT PRIMARY KEY, product_code TEXT NOT NULL, lot_number TEXT NOT NULL, quantity INTEGER NOT NULL CHECK (quantity > 0), submitted_by TEXT NOT NULL, submitted_at TEXT NOT NULL, status TEXT NOT NULL CHECK (status IN ('PENDING OQC', 'APPROVED', 'REJECTED', 'RETURNED')), verified_by TEXT, verified_at TEXT, remarks TEXT)""")
connection.commit()
connection.close()
def create_request(qa_code, product_code, lot_number, quantity, submitted_by):
connection = connect_db()
connection.execute("INSERT INTO qa_requests (qa_code, product_code, lot_number, quantity, submitted_by, submitted_at, status) VALUES (?, ?, ?, ?, ?, ?, ?)", (qa_code, product_code, lot_number, quantity, submitted_by, utc_now(), PENDING_STATUS))
connection.commit()
connection.close()
def cursor_rows_as_dicts(cursor):
columns = [column[0] for column in cursor.description]
return [dict(zip(columns, row)) for row in cursor.fetchall()]
def get_pending_requests():
connection = connect_db()
cursor = connection.execute("SELECT qa_code AS 'QA Code', product_code AS 'Product', lot_number AS 'Lot', quantity AS 'Quantity', submitted_by AS 'Submitted By', submitted_at AS 'Submitted At' FROM qa_requests WHERE status = ? ORDER BY submitted_at ASC", (PENDING_STATUS,))
rows = cursor_rows_as_dicts(cursor)
connection.close()
return rows
def get_request(qa_code):
connection = connect_db()
cursor = connection.execute("SELECT qa_code, product_code, lot_number, quantity, submitted_by, submitted_at, status, verified_by, verified_at, remarks FROM qa_requests WHERE qa_code = ?", (qa_code,))
columns = [column[0] for column in cursor.description]
row = cursor.fetchone()
connection.close()
return dict(zip(columns, row)) if row else None
def verify_request(qa_code, status, verified_by, remarks):
connection = connect_db()
connection.execute("UPDATE qa_requests SET status = ?, verified_by = ?, verified_at = ?, remarks = ? WHERE qa_code = ? AND status = ?", (status, verified_by, utc_now(), remarks, qa_code, PENDING_STATUS))
connection.commit()
connection.close()
def make_qr_png(qa_code):
buffer = io.BytesIO()
qr_code = segno.make_qr(qa_code)
qr_code.save(buffer, kind="png", scale=8)
return buffer.getvalue()
init_db()
st.set_page_config(page_title="QAForge", layout="wide")
st.title("QAForge")
st.caption("Store output submission and OQC verification with QR traceability")
store_tab, oqc_tab = st.tabs(["Store Output", "OQC Verification"])
with store_tab:
st.subheader("Submit production output")
with st.form("store_output_form"):
product_code = st.text_input("Product code")
lot_number = st.text_input("Batch or lot number")
quantity = st.number_input("Output quantity", min_value=1, value=1, step=1)
submitted_by = st.text_input("Store operator")
store_submitted = st.form_submit_button("Send to OQC")
if store_submitted:
product_code = product_code.strip().upper()
lot_number = lot_number.strip().upper()
submitted_by = submitted_by.strip()
if not product_code or not lot_number or not submitted_by:
st.error("Complete every required Store field.")
else:
qa_code = make_qa_code()
create_request(qa_code, product_code, lot_number, int(quantity), submitted_by)
qr_png = make_qr_png(qa_code)
st.success(f"Request {qa_code} was sent to OQC.")
st.image(qr_png, caption=qa_code, width=260)
st.download_button("Download QR PNG", data=qr_png, file_name=f"{qa_code}.png", mime="image/png", on_click="ignore")
with oqc_tab:
st.subheader("Pending OQC requests")
pending_requests = get_pending_requests()
if pending_requests:
st.dataframe(pending_requests, width="stretch", hide_index=True)
else:
st.write("No requests are waiting for OQC verification.")
st.subheader("Scan or enter a QA code")
with st.form("oqc_lookup_form"):
scanned_code = st.text_input("QA code", placeholder="QA-YYYYMMDD-XXXXXXXX", key="oqc_lookup_code")
lookup_submitted = st.form_submit_button("Find request")
lookup_code = scanned_code.strip().upper()
if lookup_submitted and not lookup_code:
st.error("Scan or enter a QA code before searching.")
if lookup_code:
request_record = get_request(lookup_code)
if request_record is None:
st.error("No QA request matches that code.")
else:
st.write({"QA Code": request_record["qa_code"], "Product": request_record["product_code"], "Lot": request_record["lot_number"], "Quantity": request_record["quantity"], "Submitted By": request_record["submitted_by"], "Submitted At": request_record["submitted_at"], "Status": request_record["status"], "Verified By": request_record["verified_by"], "Verified At": request_record["verified_at"], "Remarks": request_record["remarks"]})
if request_record["status"] == PENDING_STATUS:
with st.form(f"verification_form_{lookup_code}"):
verified_by = st.text_input("OQC operator")
verdict = st.selectbox("Decision", FINAL_STATUSES)
remarks = st.text_area("Inspection remarks")
verification_submitted = st.form_submit_button("Save OQC decision")
if verification_submitted:
verified_by = verified_by.strip()
remarks = remarks.strip()
if not verified_by:
st.error("Enter the OQC operator name.")
elif verdict in ("REJECTED", "RETURNED") and not remarks:
st.error("Remarks are required for rejected or returned output.")
else:
verify_request(lookup_code, verdict, verified_by, remarks)
st.success(f"{lookup_code} was recorded as {verdict}. Submit the lookup again to refresh the displayed status.")
else:
st.write("This request already has a final OQC decision.")
How Do the Pieces Fit Together?
The decision form passes an allowed status to verify_request(). The helper updates the record only while its status remains pending.
The next lookup reads the saved audit fields from the database. The final status then prevents the form from appearing again.
Prove the final decision is locked
A successful decision should update the stored record once. The next app rerun should remove that request from the pending queue.
- Enter OQC-01 in the OQC operator field.
- Select APPROVED in the Decision menu.
- Enter Count and label confirmed. in the Inspection remarks field.
- Click Save OQC decision.
You should see a success message confirming that the QA code was recorded as APPROVED.
Before you submit the lookup again, do you expect the completed request to remain eligible for another OQC decision?
- Submit the same lookup again by clicking Find request.
You should see APPROVED beside Status. You should also see OQC-01 beside Verified By.
The record now shows a Verified At timestamp. It also shows the saved inspection remarks.
You should see the final OQC message beneath the record. The decision form is no longer available.
- Scroll to the pending-request table at the top of the OQC tab.
The verified QA code no longer appears in the pending queue. This confirms that the next app rerun reads its final status from qaforge.db.
That closes the Store-to-OQC loop. QAForge now preserves one final quality decision with its operator details and verification time.
Secret mission
Break-Test the Workflow
Try blank Store data. Search for an unknown QA code. Reopen a finalized request to prove QAForge protects the original OQC decision.
Clean Up Your Resources
Clean Up Your Resources
Your QAForge project runs entirely on your Windows computer with no ongoing service charges. Choose whether to keep its local resources, pause the app for later, or delete them entirely.
Resources you used:
- The .venv local Python environment containing Streamlit plus the other project packages.
- The qaforge.db local SQLite database containing Store requests plus OQC decisions.
- The app.py application source.
- The requirements.txt dependency list.
- The local Streamlit server process when QAForge is running.
- Downloaded QR PNG files if you chose to keep them.
Keep everything running
No action is needed for the project files. Choose this option if you are still building QAForge or want to preserve its current QA records.
- Keep the qaforge folder so app.py, requirements.txt, .venv, and qaforge.db stay together.
- Keep any downloaded QR PNG files that you still need for demonstrations.
- Restart QAForge by running these commands in the Windows PowerShell terminal from your qaforge folder:
.venv\Scripts\Activate.ps1
python -m streamlit run app.py
What Do These Commands Do?
- The first line activates the local environment containing QAForge's installed packages.
- The second line starts the local browser app from app.py.
- Confirm that your browser opens QAForge with the Store Output tab plus the OQC Verification tab.
QAForge Does Not Reopen?
Confirm that Windows PowerShell is currently inside the qaforge folder. If activation is blocked, use the Command Prompt fallback from Step 1.
Help me restart QAForge from my qaforge folder.
Pause - I'll come back to this later
Shut down the running app to free local memory. Keep the project files unchanged so your workflow remains ready for another session.
- Stop Streamlit by pressing Ctrl+C in the terminal where the server is running.
- Leave the qaforge folder in place to preserve the source files plus the virtual environment.
- Keep qaforge.db to preserve the submitted requests plus their final OQC audit fields.
- Keep any downloaded QR PNG files that you want to scan again.
Your QAForge records remain on disk while the Streamlit server is stopped.
Delete - I don't want to use this again
Remove QAForge when you no longer need the prototype. Deleting the project folder permanently removes the app plus its saved QA records.
- Stop Streamlit with Ctrl+C in its terminal if the server is still running.
To reset the records only:
- Delete qaforge.db from the qaforge folder to clear the saved QA records.
- Keep app.py, requirements.txt, and .venv so you can rebuild the database later.
The next app launch creates a fresh qaforge.db database with an empty QA request table.
To remove QAForge completely:
- Use Windows search to open File Explorer.
- Locate the qaforge folder that you opened in Step 1.
- Delete the qaforge folder to remove app.py, requirements.txt, .venv, and qaforge.db.
- Locate the browser download location containing any saved QR PNG files.
- Delete each downloaded QR PNG file that belongs to QAForge.
Cleanup is complete. QAForge no longer has a local workspace or saved verification database on your computer.
Nice Work!
Nice Work!
You did it! You built QAForge as a complete local Store-to-OQC browser workflow.
You've learned how to:
- Build a two-role workflow with Streamlit. Store submits production output. OQC retrieves each request for verification.
- Generate unique QA identifiers. Turn each identifier into a downloadable PNG QR code. Link OQC back to the current record in SQLite.
- Protect each final OQC decision with a pending-only update using parameterized SQL. Preserve the verifier in the audit record. Preserve the inspection remarks. Stamp the decision with a UTC verification time.
- Secret Mission: Break-test blank Store submissions. Test unknown QA identifiers. Attempt a duplicate verification. Confirm that each final OQC record remains unchanged.
Ready to quiz yourself?