Compare commits

...

No commits in common. "master" and "main" have entirely different histories.
master ... main

19 changed files with 707 additions and 3 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
venv/
__pycache__/
*.pyc
*.db
*.sqlite
.env

3
.idea/.gitignore generated vendored
View File

@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml

14
.idea/discord.xml generated Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT" />
<option name="description" value="" />
<option name="applicationTheme" value="default" />
<option name="iconsTheme" value="default" />
<option name="button1Title" value="" />
<option name="button1Url" value="" />
<option name="button2Title" value="" />
<option name="button2Url" value="" />
<option name="customApplicationId" value="" />
</component>
</project>

8
.idea/do-tracker.iml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.13 virtualenv at ~/Projects/do-tracker/venv" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.13 virtualenv at ~/Projects/do-tracker/venv" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 virtualenv at ~/Projects/do-tracker/venv" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/do-tracker.iml" filepath="$PROJECT_DIR$/.idea/do-tracker.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

167
.idea/workspace.xml generated Normal file
View File

@ -0,0 +1,167 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="5112d569-3a3f-4a44-9bd9-2c9295e7fe64" name="Changes" comment="added readme">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="HTML File" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="main" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="HighlightingSettingsPerFile">
<setting file="file://$PROJECT_DIR$/run.py" root0="FORCE_HIGHLIGHTING" />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 2
}</component>
<component name="ProjectId" id="2xzZKYTQ3ONQyWMQvvsDEzRY2pu" />
<component name="ProjectLevelVcsManager">
<ConfirmationsSetting value="2" id="Add" />
</component>
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"DefaultHtmlFileTemplate": "HTML File",
"ModuleVcsDetector.initialDetectionPerformed": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.git.unshallow": "true",
"git-widget-placeholder": "main",
"settings.editor.selected.configurable": "discord-application"
}
}]]></component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="5112d569-3a3f-4a44-9bd9-2c9295e7fe64" name="Changes" comment="" />
<created>1748942798494</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1748942798494</updated>
</task>
<task id="LOCAL-00001" summary="First Commit">
<option name="closed" value="true" />
<created>1748943061863</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1748943061863</updated>
</task>
<task id="LOCAL-00002" summary="First Commit">
<option name="closed" value="true" />
<created>1748943110291</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1748943110291</updated>
</task>
<task id="LOCAL-00003" summary="First Commit">
<option name="closed" value="true" />
<created>1748943342764</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1748943342764</updated>
</task>
<task id="LOCAL-00004" summary="Web front enabled basic flask setup">
<option name="closed" value="true" />
<created>1748943934876</created>
<option name="number" value="00004" />
<option name="presentableId" value="LOCAL-00004" />
<option name="project" value="LOCAL" />
<updated>1748943934876</updated>
</task>
<task id="LOCAL-00005" summary="Login and Dashboard added">
<option name="closed" value="true" />
<created>1748944937448</created>
<option name="number" value="00005" />
<option name="presentableId" value="LOCAL-00005" />
<option name="project" value="LOCAL" />
<updated>1748944937448</updated>
</task>
<task id="LOCAL-00006" summary="Tracking DOs and other stuff added">
<option name="closed" value="true" />
<created>1748945875048</created>
<option name="number" value="00006" />
<option name="presentableId" value="LOCAL-00006" />
<option name="project" value="LOCAL" />
<updated>1748945875048</updated>
</task>
<task id="LOCAL-00007" summary="added readme">
<option name="closed" value="true" />
<created>1748946041819</created>
<option name="number" value="00007" />
<option name="presentableId" value="LOCAL-00007" />
<option name="project" value="LOCAL" />
<updated>1748946041819</updated>
</task>
<option name="localTasksCounter" value="8" />
<servers />
</component>
<component name="Vcs.Log.Tabs.Properties">
<option name="RECENT_FILTERS">
<map>
<entry key="Branch">
<value>
<list>
<RecentGroup>
<option name="FILTER_VALUES">
<option value="origin/main" />
</option>
</RecentGroup>
</list>
</value>
</entry>
</map>
</option>
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State>
<option name="FILTERS">
<map>
<entry key="branch">
<value>
<list>
<option value="main" />
</list>
</value>
</entry>
</map>
</option>
</State>
</value>
</entry>
</map>
</option>
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="First Commit" />
<MESSAGE value="Web front enabled basic flask setup" />
<MESSAGE value="Login and Dashboard added" />
<MESSAGE value="Tracking DOs and other stuff added" />
<MESSAGE value="added readme" />
<option name="LAST_COMMIT_MESSAGE" value="added readme" />
</component>
</project>

112
README.md
View File

@ -0,0 +1,112 @@
# 🧾 DO Tracker & Manifest System
A lightweight Flask-based delivery order tracking and manifest system for inter-branch logistics. Built for internal use in multi-branch warehouse/distribution networks, such as plumbing or trade supply chains.
---
## 🚀 Features
- 🔐 Store-based login system (per-branch access control)
- 📦 Manual DO entry (DO Number + Delivery Number)
- 🚚 Track each DO across multiple store stopovers
- ✍️ Logs who handled the DO at each location
- 📅 Timestamps all arrivals and departures
- ✅ Final destination marking
- 🧼 Admin portal (WIP) for fixing errors
- 🔍 DO lookup by either DO Number or Delivery Number
---
## 🧱 Tech Stack
- Python 3
- Flask (Web Framework)
- SQLAlchemy (ORM)
- SQLite (default backend DB)
- Jinja2 (templating)
- PyCharm (dev environment)
- Gitea (self-hosted Git)
---
## 🗂 Folder Structure
```
do-tracker/
├── app/
│ ├── __init__.py
│ ├── routes.py
│ ├── models.py
│ ├── auth.py
│ ├── templates/
│ └── static/
├── run.py
├── requirements.txt
├── README.md
└── do_tracker.db (generated after first run)
```
---
## 🛠️ Setup Instructions
1. Clone repo:
```bash
git clone https://git.anubisdevelopments.com/buster_dylan/do-tracker.git
cd do-tracker
```
2. Create a Python virtual environment:
```bash
python -m venv venv
source venv/bin/activate
```
3. Install dependencies:
```bash
pip install -r requirements.txt
```
4. Initialize the database:
```bash
python
>>> from app import create_app, db
>>> app = create_app()
>>> app.app_context().push()
>>> db.create_all()
>>> exit()
```
5. Run the app:
```bash
python run.py
```
Visit: [http://127.0.0.1:5000](http://127.0.0.1:5000)
---
## ✅ Login Details (Example)
| Store | ID | Password |
|----------------|-----|-----------|
| West Gosford | 210 | gosford |
| Woy Woy | 230 | woywoy |
| Charmhaven | 220 | charms |
| Long Jetty | 250 | jetty |
| RDD Erina | 240 | erina |
| Commercial | 610 | com610 |
---
## 📌 TODO / Roadmap
- [ ] Admin panel to edit movements
- [ ] CSV export of DO logs
- [ ] Signature capture for deliveries
- [ ] Access logging & audit trail
- [ ] Move to Postgres for production
---
Built by Dylan Wright for operational and portfolio use.

View File

@ -0,0 +1,25 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
##from flask_login import LoginManager
db = SQLAlchemy()
#login_manager = LoginManager()
def create_app():
app = Flask(__name__)
app.config['SECRET_KEY'] = 'devkey' # Change this later
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///do_tracker.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db.init_app(app)
# login_manager.init_app(app)
from .routes import main as main_blueprint
app.register_blueprint(main_blueprint)
from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint)
from . import models # <-- 🔥 this is what was missing
return app

View File

@ -0,0 +1,26 @@
from flask import Blueprint, render_template, redirect, request, session, url_for, flash
from .models import Store
from . import db
auth = Blueprint('auth', __name__)
@auth.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
store_id = request.form.get('store_id')
password = request.form.get('password')
store = Store.query.filter_by(id=store_id, password=password).first()
if store:
session['store_id'] = store.id
session['store_name'] = store.name
flash(f"Logged in as {store.name}", "success")
return redirect(url_for('main.store_dashboard'))
else:
flash("Invalid store ID or password", "danger")
return render_template('login.html')
@auth.route('/logout')
def logout():
session.clear()
flash("Logged out successfully", "info")
return redirect(url_for('auth.login'))

View File

@ -0,0 +1,28 @@
from datetime import datetime
from . import db
class Store(db.Model):
id = db.Column(db.Integer, primary_key=True) # Store number like 210
name = db.Column(db.String(100), nullable=False)
password = db.Column(db.String(128), nullable=False) # plaintext for now (hash later)
class DeliveryOrder(db.Model):
id = db.Column(db.Integer, primary_key=True)
do_number = db.Column(db.String(50), unique=True, nullable=False)
delivery_number = db.Column(db.String(50), nullable=False)
final_location = db.Column(db.Integer, db.ForeignKey('store.id'), nullable=False)
created_by = db.Column(db.String(100), nullable=False)
status = db.Column(db.String(50), default='Ready for Collection')
created_at = db.Column(db.DateTime, default=datetime.utcnow)
collected_by = db.Column(db.String(100), nullable=True)
movements = db.relationship('Movement', backref='delivery_order', cascade='all, delete-orphan')
class Movement(db.Model):
id = db.Column(db.Integer, primary_key=True)
do_id = db.Column(db.Integer, db.ForeignKey('delivery_order.id'), nullable=False)
branch_id = db.Column(db.Integer, db.ForeignKey('store.id'), nullable=False)
arrived_at = db.Column(db.DateTime, default=datetime.utcnow)
departed_at = db.Column(db.DateTime, nullable=True)
handled_by = db.Column(db.String(100), nullable=False)
comment = db.Column(db.Text, nullable=True)

View File

@ -0,0 +1,152 @@
from flask import Blueprint, render_template, redirect, url_for, session, flash
from .models import DeliveryOrder
main = Blueprint('main', __name__)
@main.route('/')
def home():
return """
<h1>DO Tracker Online</h1>
<p><a href='/login'>Login as Store</a></p>
<p><a href='/store'>Store Dashboard</a></p>
"""
@main.route('/store')
def store_dashboard():
if 'store_id' not in session:
flash("You must be logged in to access the store dashboard.", "warning")
return redirect(url_for('auth.login'))
store_name = session.get('store_name')
return f"""
<h1>Welcome, {store_name}</h1>
<p><a href='/logout'>Logout</a></p>
<ul>
<li><a href='/store/do-entry'>Enter New DO</a> (coming next)</li>
<li><a href='/store/track'>Track DOs</a> (coming soon)</li>
</ul>
"""
from flask import request
from .models import DeliveryOrder, Store
from . import db
@main.route('/store/do-entry', methods=['GET', 'POST'])
def do_entry():
if 'store_id' not in session:
return redirect(url_for('auth.login'))
stores = Store.query.order_by(Store.name).all()
if request.method == 'POST':
do_number = request.form.get('do_number')
delivery_number = request.form.get('delivery_number')
final_location = int(request.form.get('final_location'))
created_by = session.get('store_name')
# Prevent duplicate DO numbers
existing = DeliveryOrder.query.filter_by(do_number=do_number).first()
if existing:
flash("DO already exists!", "danger")
return redirect(url_for('main.do_entry'))
new_do = DeliveryOrder(
do_number=do_number,
delivery_number=delivery_number,
final_location=final_location,
created_by=created_by,
status="Ready for Collection"
)
db.session.add(new_do)
db.session.commit()
flash("DO created successfully.", "success")
return redirect(url_for('main.store_dashboard'))
return render_template("do_entry.html", stores=stores)
@main.route('/store/track', methods=['GET', 'POST'])
def track_do():
if 'store_id' not in session:
return redirect(url_for('auth.login'))
do = None
movements = []
if request.method == 'POST':
search = request.form.get('search')
do = DeliveryOrder.query.filter(
(DeliveryOrder.do_number == search) |
(DeliveryOrder.delivery_number == search)
).first()
if do:
movements = do.movements
return render_template('track_do.html', do=do, movements=movements)
from datetime import datetime
from .models import Movement
@main.route('/store/move', methods=['GET', 'POST'])
def move_do():
if 'store_id' not in session:
return redirect(url_for('auth.login'))
message = None
if request.method == 'POST':
search = request.form.get('do_search')
do = DeliveryOrder.query.filter(
(DeliveryOrder.do_number == search) |
(DeliveryOrder.delivery_number == search)
).first()
if not do:
flash("DO not found", "danger")
return redirect(url_for('main.move_do'))
handled_by = request.form.get('handled_by')
comment = request.form.get('comment')
mark_departed = request.form.get('departed')
branch_id = session.get('store_id')
# Check if this store has already logged a movement
existing = Movement.query.filter_by(do_id=do.id, branch_id=branch_id).first()
if not existing:
# First time it arrived at this store
move = Movement(
do_id=do.id,
branch_id=branch_id,
handled_by=handled_by,
comment=comment
)
db.session.add(move)
else:
# Already exists → optionally mark departure
if mark_departed == "on" and not existing.departed_at:
existing.departed_at = datetime.utcnow()
existing.comment = (existing.comment or '') + f" | {comment}"
db.session.commit()
flash("Movement updated successfully.", "success")
return redirect(url_for('main.track_do'))
return render_template("move_do.html")
@main.route('/store/complete/<int:do_id>', methods=['POST'])
def mark_completed(do_id):
if 'store_id' not in session:
return redirect(url_for('auth.login'))
do = DeliveryOrder.query.get_or_404(do_id)
# Ensure only the final destination can mark complete
if session['store_id'] != do.final_location:
flash("You are not authorized to mark this DO as completed.", "danger")
return redirect(url_for('main.track_do'))
do.status = "Completed"
db.session.commit()
flash("DO marked as completed.", "success")
return redirect(url_for('main.track_do'))

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>New DO Entry</title>
</head>
<body>
<h1>Enter a New Delivery Order</h1>
<form method="POST">
<label>DO Number:</label><br>
<input type="text" name="do_number" required><br>
<label>Delivery Number:</label><br>
<input type="text" name="delivery_number" required><br>
<label>Final Destination:</label><br>
<select name="final_location">
{% for store in stores %}
<option value="{{ store.id }}">{{ store.name }} ({{ store.id }})</option>
{% endfor %}
</select><br><br>
<button type="submit">Create DO</button>
</form>
<p><a href="/store">Back to Dashboard</a></p>
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<p style="color:red;">{{ message }}</p>
{% endfor %}
{% endwith %}
</body>
</html>

21
app/templates/login.html Normal file
View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Store Login</title>
</head>
<body>
<h1>Store Login</h1>
<form method="POST">
<label>Store ID:</label><br>
<input type="text" name="store_id"><br>
<label>Password:</label><br>
<input type="password" name="password"><br><br>
<button type="submit">Login</button>
</form>
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<p style="color:red;">{{ message }}</p>
{% endfor %}
{% endwith %}
</body>
</html>

View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Mark DO Movement</title>
</head>
<body>
<h1>Mark Arrival/Departure</h1>
<form method="POST">
<label>DO Number or Delivery Number:</label><br>
<input type="text" name="do_search" required><br>
<label>Your Name (who handled it):</label><br>
<input type="text" name="handled_by" required><br>
<label>Comment (optional):</label><br>
<input type="text" name="comment"><br>
<label>
<input type="checkbox" name="departed">
Mark as Departed
</label><br><br>
<button type="submit">Submit</button>
</form>
<p><a href="/store">Back to Dashboard</a></p>
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<p style="color:red;">{{ message }}</p>
{% endfor %}
{% endwith %}
</body>
</html>

View File

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Track Delivery Order</title>
</head>
<body>
<h1>Track a Delivery Order</h1>
<form method="POST">
<label>Enter DO Number or Delivery Number:</label><br>
<input type="text" name="search" required>
<button type="submit">Search</button>
</form>
{% if do %}
<h2>DO: {{ do.do_number }} / {{ do.delivery_number }}</h2>
<p>Status: <strong>{{ do.status }}</strong></p>
<p>Created by: {{ do.created_by }}</p>
<p>Final Destination: {{ do.final_location }}</p>
<p>Collected By: {{ do.collected_by if do.collected_by else "Not collected yet" }}</p>
<p>Created: {{ do.created_at }}</p>
{% if do.status != "Completed" and session['store_id'] == do.final_location %}
<form action="/store/complete/{{ do.id }}" method="POST">
<button type="submit">✅ Mark as Delivered</button>
</form>
{% endif %}
<h3>Movement History:</h3>
{% if movements %}
<ul>
{% for m in movements %}
<li>
📍 {{ m.branch_id }} | Handled by {{ m.handled_by }} | Arrived: {{ m.arrived_at }}
{% if m.departed_at %}| Departed: {{ m.departed_at }}{% endif %}
{% if m.comment %}<br>📝 {{ m.comment }}{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<p>No movement data yet.</p>
{% endif %}
{% endif %}
<p><a href="/store">Back to Dashboard</a></p>
</body>
</html>

6
run.py
View File

@ -0,0 +1,6 @@
from app import create_app
app = create_app()
if __name__ == "__main__":
app.run(debug=True)