commit
857409c68d
32 changed files with 6950 additions and 0 deletions
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
import csv |
||||
import mysql.connector |
||||
import os |
||||
|
||||
conn = mysql.connector.connect( |
||||
host="localhost", |
||||
user="tom", |
||||
password="ZQsC644DGh", |
||||
database="stock_db" |
||||
) |
||||
|
||||
curs = conn.cursor() |
||||
|
||||
file = 'ProductList.csv' |
||||
|
||||
with open(file) as csv_file: |
||||
csv_reader = csv.reader(csv_file) |
||||
next(csv_reader) |
||||
for row in csv_reader: |
||||
curs.execute( |
||||
'INSERT INTO products(' |
||||
' prod_id,' |
||||
' prod_code,' |
||||
' prod_barcode,' |
||||
' prod_name,' |
||||
' prod_desc,' |
||||
' prod_cost' |
||||
' ) VALUES (' |
||||
' %s, %s, %s, %s, %s, %s)', |
||||
( |
||||
row[21], |
||||
row[15], |
||||
row[11], |
||||
row[1], |
||||
row[2], |
||||
row[5] |
||||
) |
||||
) |
||||
conn.commit() |
||||
conn.close() |
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
-- ---------------------------------- --------------------------------------- |
||||
0 TillOrder |
||||
1 Name AbbeySlim 120mm Gas Man Blk Hlight Coal |
||||
2 Description |
||||
3 CostPrice 205.51000 |
||||
4 TaxRate 20VAT |
||||
5 CostIncTax 246.61200 |
||||
6 SalePriceIncTax 0.00000 |
||||
7 IsVariablePrice False |
||||
8 MarginPerc 0.000000 |
||||
9 TaxExemptEligible False |
||||
10 RRPrice 0 |
||||
11 Barcode |
||||
12 Category Abbey |
||||
13 Brand Be Modern |
||||
14 Supplier Be Modern |
||||
15 OrderCode |
||||
16 ArticleCode |
||||
17 PopupNote |
||||
18 MultiChoiceNote |
||||
19 ButtonColour |
||||
20 SellOnTill True |
||||
21 ProductId 13478471 |
@ -0,0 +1,55 @@
@@ -0,0 +1,55 @@
|
||||
import os |
||||
import datetime |
||||
|
||||
from flask import Flask |
||||
import pymysql |
||||
pymysql.install_as_MySQLdb() |
||||
|
||||
from stock.db import db |
||||
|
||||
def create_app(test_config=None): |
||||
test_config = None |
||||
"""Create and configure an instance of the Flask application.""" |
||||
app = Flask(__name__, instance_relative_config=True) |
||||
app.config.from_mapping( |
||||
# a default secret that should be overridden by instance config |
||||
SECRET_KEY='dev', |
||||
SQLALCHEMY_DATABASE_URI = 'mysql://tom:ZQsC644DGh@localhost/stock_db', |
||||
SQLALCHEMY_TRACK_MODIFICATIONS=False |
||||
) |
||||
|
||||
if test_config is None: |
||||
# load the instance config, if it exists, when not testing |
||||
app.config.from_pyfile('config.py', silent=True) |
||||
else: |
||||
# load the test config if passed in |
||||
app.config.update(test_config) |
||||
|
||||
# ensure the instance folder exists |
||||
try: |
||||
os.makedirs(app.instance_path) |
||||
except OSError: |
||||
pass |
||||
|
||||
|
||||
@app.route('/hello') |
||||
def hello(): |
||||
return 'Hello, World!' |
||||
|
||||
|
||||
# register the database commands |
||||
from stock import db |
||||
db.init_app(app) |
||||
|
||||
# apply the blueprints to the app |
||||
from stock import auth, views |
||||
app.register_blueprint(auth.bp) |
||||
app.register_blueprint(views.bp) |
||||
|
||||
# make url_for('index') == url_for('blog.index') |
||||
# in another app, you might define a separate main index here with |
||||
# app.route, while giving the blog blueprint a url_prefix, but for |
||||
# the tutorial the blog will be the main index |
||||
app.add_url_rule('/', endpoint='index') |
||||
|
||||
return app |
@ -0,0 +1,110 @@
@@ -0,0 +1,110 @@
|
||||
import functools |
||||
|
||||
from flask import ( |
||||
Blueprint, flash, g, redirect, render_template, request, session, url_for |
||||
) |
||||
from werkzeug.security import check_password_hash, generate_password_hash |
||||
|
||||
from stock.db import get_db, User |
||||
from uuid import uuid4 |
||||
|
||||
bp = Blueprint('auth', __name__, url_prefix='/auth') |
||||
|
||||
|
||||
def login_required(view): |
||||
"""View decorator that redirects anonymous users to the login page.""" |
||||
@functools.wraps(view) |
||||
def wrapped_view(**kwargs): |
||||
if g.user is None: |
||||
return redirect(url_for('auth.login')) |
||||
|
||||
return view(**kwargs) |
||||
|
||||
return wrapped_view |
||||
|
||||
|
||||
@bp.before_app_request |
||||
def load_logged_in_user(): |
||||
"""If a user id is stored in the session, load the user object from |
||||
the database into ``g.user``.""" |
||||
user_id = session.get('user_id') |
||||
|
||||
if user_id is None: |
||||
g.user = None |
||||
else: |
||||
g.user = User.query.filter_by(user_id=user_id) |
||||
|
||||
|
||||
@bp.route('/login', methods=('GET', 'POST')) |
||||
def login(): |
||||
"""Log in a registered user by adding the user id to the session.""" |
||||
if request.method == 'POST': |
||||
username = request.form['user_name'] |
||||
password = request.form['user_pass'] |
||||
db = get_db() |
||||
error = None |
||||
user = User.query.filter_by(user_name = username).first() |
||||
|
||||
if user is None: |
||||
error = 'Incorrect username.' |
||||
elif not check_password_hash(user.user_pass, password): |
||||
error = 'Incorrect password.' |
||||
|
||||
if error is None: |
||||
# store the user id in a new session and return to the index |
||||
session.clear() |
||||
session['user_id'] = user.user_id |
||||
return redirect(url_for('index')) |
||||
|
||||
flash(error) |
||||
|
||||
return render_template('auth/login.html') |
||||
|
||||
|
||||
@bp.route('/logout') |
||||
def logout(): |
||||
"""Clear the current session, including the stored user id.""" |
||||
session.clear() |
||||
return redirect(url_for('index')) |
||||
|
||||
|
||||
@bp.route('/change', methods=['GET', 'POST']) |
||||
def change_pass(): |
||||
db = get_db() |
||||
|
||||
if request.method == 'POST': |
||||
|
||||
user = db.execute( |
||||
'SELECT * FROM user WHERE user_id = ?', (session['user_id'],) |
||||
).fetchone() |
||||
|
||||
username = user['user_name'] |
||||
password = user['user_pass'] |
||||
old_pass = request.form['old_password'] |
||||
new_pass = request.form['new_password'] |
||||
conf_pass = request.form['confirm_password'] |
||||
error = None |
||||
|
||||
if new_pass != conf_pass: |
||||
error = 'Passwords do not match' |
||||
elif not check_password_hash(password, old_pass): |
||||
error = 'Incorrect password' |
||||
|
||||
if error is None: |
||||
db.execute( |
||||
'UPDATE user' |
||||
' SET user_pass = ?' |
||||
' WHERE user_name = ?', |
||||
(generate_password_hash(new_pass), username,) |
||||
) |
||||
|
||||
db.commit() |
||||
|
||||
flash('Password changed successfully') |
||||
|
||||
return redirect(url_for('main.index')) |
||||
|
||||
else: |
||||
flash(error) |
||||
|
||||
return render_template('auth/change.html') |
@ -0,0 +1,128 @@
@@ -0,0 +1,128 @@
|
||||
import sqlite3 |
||||
|
||||
from werkzeug.security import check_password_hash, generate_password_hash |
||||
|
||||
import click |
||||
from flask import current_app, g |
||||
from flask.cli import with_appcontext |
||||
from flask_sqlalchemy import SQLAlchemy |
||||
|
||||
db = SQLAlchemy() |
||||
|
||||
import uuid |
||||
|
||||
|
||||
class User(db.Model): |
||||
user_id = db.Column( |
||||
db.String(36), |
||||
primary_key = True |
||||
) |
||||
user_name = db.Column( |
||||
db.String(80) |
||||
) |
||||
user_pass = db.Column( |
||||
db.String(1000) |
||||
) |
||||
|
||||
|
||||
class Products(db.Model): |
||||
prod_id = db.Column( |
||||
db.Integer, |
||||
primary_key=True |
||||
) |
||||
prod_code = db.Column( |
||||
db.String(100) |
||||
) |
||||
prod_barcode = db.Column( |
||||
db.String(100) |
||||
) |
||||
prod_name = db.Column( |
||||
db.String(250) |
||||
) |
||||
prod_desc = db.Column( |
||||
db.String(250) |
||||
) |
||||
prod_cost = db.Column( |
||||
db.Float() |
||||
) |
||||
count = db.relationship( |
||||
'Count' |
||||
) |
||||
|
||||
class Stock(db.Model): |
||||
stock_id = db.Column( |
||||
db.String(36), |
||||
primary_key=True |
||||
) |
||||
stock_date = db.Column( |
||||
db.DateTime |
||||
) |
||||
stock_user = db.Column( |
||||
db.String(100) |
||||
) |
||||
site_id_fk = db.Column( |
||||
db.String(100), |
||||
db.ForeignKey('sites.site_id') |
||||
) |
||||
count = db.relationship( |
||||
'Count', |
||||
) |
||||
|
||||
class Count(db.Model): |
||||
count_id = db.Column( |
||||
db.String(36), |
||||
primary_key = True |
||||
) |
||||
count_count = db.Column( |
||||
db.Integer |
||||
) |
||||
prod_id_fk = db.Column( |
||||
db.Integer, |
||||
db.ForeignKey('products.prod_id') |
||||
) |
||||
stock_id_fk = db.Column( |
||||
db.String(36), |
||||
db.ForeignKey('stock.stock_id') |
||||
) |
||||
|
||||
|
||||
class Sites(db.Model): |
||||
site_id = db.Column( |
||||
db.String(36), |
||||
primary_key = True |
||||
) |
||||
site_name = db.Column( |
||||
db.String(100) |
||||
) |
||||
stock = db.relationship( |
||||
'Stock', |
||||
) |
||||
|
||||
|
||||
|
||||
def get_db(): |
||||
if 'db' not in g: |
||||
g.db = db |
||||
|
||||
return g.db |
||||
|
||||
|
||||
def close_db(e=None): |
||||
db = d.pop('db', None) |
||||
|
||||
|
||||
def init_db(): |
||||
with current_app: |
||||
db.create_all() |
||||
db.session.commit() |
||||
|
||||
@click.command('init-db') |
||||
@with_appcontext |
||||
def init_db_command(): |
||||
init_db() |
||||
click.echo('Initialized the database') |
||||
|
||||
|
||||
def init_app(app): |
||||
app.teardown_appcontext(close_db) |
||||
app.cli.add_comment(init_db_command) |
@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
|
||||
User |
||||
user_id pk |
||||
user_name |
||||
user_pass |
||||
|
||||
Products (from EPOS) |
||||
prod_id |
||||
prod_code |
||||
prod_barcode |
||||
prod_name |
||||
prod_desc |
||||
prod_cost |
||||
|
||||
Stock |
||||
stock_id pk |
||||
stock_date |
||||
stock_user |
||||
site_id_fk |
||||
|
||||
Count |
||||
count_id pk |
||||
count_count |
||||
prod_id_fk |
||||
stock_id_fk |
||||
|
||||
Sites |
||||
site_id. pk |
||||
site_name |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
/**{ |
||||
border:solid 1px lime; |
||||
}*/ |
||||
|
||||
html { |
||||
font-family: "Open Sans"; |
||||
} |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
{ |
||||
"background_color": "purple", |
||||
"description": "Henderson Design Group Stock", |
||||
"display": "fullscreen", |
||||
"name": "Stock Counter", |
||||
"short_name": "Stock Counter", |
||||
"start_url": "/" |
||||
} |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
function myFunction() { |
||||
// Declare variables
|
||||
var input, filter, table, tr, td, i, txtValue; |
||||
input = document.getElementById("filter"); |
||||
filter = input.value.toUpperCase(); |
||||
table = document.getElementById("productTable"); |
||||
tr = table.getElementsByTagName("tr"); |
||||
|
||||
// Loop through all table rows, and hide those who don't match the search query
|
||||
for (i = 0; i < tr.length; i++) { |
||||
td = tr[i].getElementsByTagName("td")[0]; |
||||
if (td) { |
||||
txtValue = td.textContent || td.innerText; |
||||
if (txtValue.toUpperCase().indexOf(filter) > -1) { |
||||
tr[i].style.display = ""; |
||||
} else { |
||||
tr[i].style.display = "none"; |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
{% extends 'base.html' %} |
||||
|
||||
{% block header %} |
||||
<h2 class="text-center">Henderson Design Group Stock Counter</h2> |
||||
{% endblock %} |
||||
|
||||
{% block content %} |
||||
|
||||
<form method="post" class=""> |
||||
|
||||
<div class="form-group"> |
||||
<label class="form-label" for="user_name">Username</label> |
||||
<input id="user_name" name="user_name" class="form-input" placeholder="username" required> |
||||
</div> |
||||
|
||||
<div class="form-group"> |
||||
<label class="form-label" for="user_pass">Password</label> |
||||
<input type="user_pass" id="user_pass" name="user_pass" class="form-input" placeholder="password" required> |
||||
</div> |
||||
|
||||
<button type="submit" class="btn btn-block btn-primary">Login</button> |
||||
|
||||
{% if error != '' %} |
||||
<p><strong>{{ error }}</strong></p> |
||||
{% endif %} |
||||
|
||||
</form> |
||||
|
||||
{% endblock %} |
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
<!doctype html> |
||||
<html land="en"> |
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> |
||||
|
||||
<title>Henderson Design | Stock</title> |
||||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/spectre-exp.min.css') }}"> |
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/spectre-icons.min.css') }}"> |
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/spectre.min.css') }}"> |
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> |
||||
<link rel="manifest" href="{{ url_for('static',filename='manifest.webmanifest') }}"> |
||||
|
||||
</head> |
||||
|
||||
<body> |
||||
|
||||
<div class="container"> |
||||
|
||||
<header class=navbar> |
||||
<section class="navbar-section"> |
||||
<a class="navbar-brand" href="{{url_for('views.index')}}">Stock Counter</a> |
||||
</section> |
||||
|
||||
{% if g.user != None %} |
||||
<section class="navbar-section"> |
||||
<a href="{{url_for('views.index')}}" class="btn btn-link">Stock</a> |
||||
<a href="{{ url_for('views.products') }}" class="btn btn-link">Products</a> |
||||
</section> |
||||
{% endif %} |
||||
|
||||
</header> |
||||
|
||||
<div class="container"> |
||||
<div> |
||||
{% block header %}{% endblock %} |
||||
</div> |
||||
|
||||
<div> |
||||
{% block toolbar %}{% endblock %} |
||||
</div> |
||||
|
||||
<div> |
||||
{% block content %}{% endblock %} |
||||
</div> |
||||
|
||||
<div> |
||||
{% for message in get_flashed_messages() %} |
||||
<div>{{ message }}</div> |
||||
{% endfor %} |
||||
</div> |
||||
</div> |
||||
|
||||
</div> |
||||
</body> |
||||
|
||||
<script src="{{url_for('static', filename='stock.js') }}"></script> |
||||
|
||||
</html> |
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
{% extends 'base.html' %} |
||||
|
||||
{% block header %} |
||||
<h1 class="text-center">Product Search</h1> |
||||
{% endblock %} |
||||
|
||||
{% block content %} |
||||
<form method="post" class=""> |
||||
|
||||
<div class="form-group"> |
||||
<label class="form-label" for="prod_barcode">Barcode</label> |
||||
<input id="prod_barcode" name="prod_barcode" class="form-input" type="text"> |
||||
</div> |
||||
|
||||
<button type="submit" class="btn btn-block btn-primary">Search</button> |
||||
|
||||
{% if error != '' %} |
||||
<p><strong>{{ error }}</strong></p> |
||||
{% endif %} |
||||
|
||||
</form> |
||||
|
||||
{% endblock %} |
@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
|
||||
{% extends 'base.html' %} |
||||
|
||||
{% block header %} |
||||
<h1 class="text-center">Add Product Count</h1> |
||||
{% endblock %} |
||||
|
||||
{% block content %} |
||||
|
||||
<div class="columns"> |
||||
<span class="column">Barcode</span> |
||||
<span class="column">{{product.prod_barcode}}</span> |
||||
</div> |
||||
|
||||
<div class="columns"> |
||||
<span class="column">Product Code</span> |
||||
<span class="column">{{product.prod_code}}</span> |
||||
</div> |
||||
|
||||
<div class="columns"> |
||||
<span class="column">Name</span> |
||||
<span class="column">{{product.prod_name}}</span> |
||||
</div> |
||||
|
||||
|
||||
<form method="post" class=""> |
||||
|
||||
<!-- <div class="form-group"> |
||||
<label class="form-label" for="prod_barcode">Barcode</label> |
||||
<input id="prod_barcode" name="prod_barcode" class="form-input" type="text" required> |
||||
</div> --> |
||||
|
||||
<div class="form-group"> |
||||
<label class="form-label" for="prod_count">Count</label> |
||||
<input id="prod_count" name="prod_count" type="number" min=0 step=1 class="form-input"> |
||||
</div> |
||||
|
||||
<button type="submit" class="btn btn-block btn-primary">Create Product Count</button> |
||||
|
||||
{% if error != '' %} |
||||
<p><strong>{{ error }}</strong></p> |
||||
{% endif %} |
||||
|
||||
</form> |
||||
|
||||
{% endblock %} |
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
{% extends 'base.html' %} |
||||
|
||||
{% block header %} |
||||
<h1 class="text-center">Edit Product Count</h1> |
||||
{% endblock %} |
||||
|
||||
{% block content %} |
||||
|
||||
<div class="columns"> |
||||
<span class="column">Barcode</span> |
||||
<span class="column">{{count.prod_barcode}}</span> |
||||
</div> |
||||
|
||||
<div class="columns"> |
||||
<span class="column">Product Code</span> |
||||
<span class="column">{{count.prod_code}}</span> |
||||
</div> |
||||
|
||||
<div class="columns"> |
||||
<span class="column">Name</span> |
||||
<span class="column">{{count.prod_name}}</span> |
||||
</div> |
||||
|
||||
|
||||
<form method="post" class=""> |
||||
|
||||
<div class="form-group"> |
||||
<label class="form-label" for="prod_count">Count</label> |
||||
<input id="prod_count" name="prod_count" type="number" min=0 step=1 class="form-input" value="{{ count.count_count }}"> |
||||
</div> |
||||
|
||||
<button type="submit" class="btn btn-block btn-primary">Update Product Count</button> |
||||
|
||||
{% if error != '' %} |
||||
<p><strong>{{ error }}</strong></p> |
||||
{% endif %} |
||||
|
||||
</form> |
||||
|
||||
{% endblock %} |
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
{% extends 'base.html' %} |
||||
|
||||
{% block header %} |
||||
<h1 class="text-center">Confirm Deletion</h1> |
||||
{% endblock %} |
||||
|
||||
{% block content %} |
||||
|
||||
<p>Are you sure you want to delete the following count:</p> |
||||
|
||||
|
||||
<form method="post" class=""> |
||||
|
||||
<button name="submit" type="submit" class="btn btn-block" value="delete">Delete</button> |
||||
<br> |
||||
<button name="submit" type="submit" class="btn btn-primary btn-block" value="no-delete">Don't Delete</button> |
||||
|
||||
{% if error != '' %} |
||||
<p><strong>{{ error }}</strong></p> |
||||
{% endif %} |
||||
|
||||
</form> |
||||
|
||||
{% endblock %} |
@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
|
||||
{% extends 'base.html' %} |
||||
|
||||
{% block header %} |
||||
<h1 class="text-center">Create Product</h1> |
||||
{% endblock %} |
||||
|
||||
{% block content %} |
||||
|
||||
<p>The product you scanned was not found. Please add it.</p> |
||||
|
||||
<form method="post" class=""> |
||||
|
||||
<div class="form-group"> |
||||
<label class="form-label" for="prod_code">Product Code</label> |
||||
<input id="prod_code" name="prod_code" class="form-input" type="text"> |
||||
</div> |
||||
|
||||
<div class="form-group"> |
||||
<label class="form-label" for="prod_name">Product Name</label> |
||||
<input id="prod_name" name="prod_name" class="form-input" type="text"> |
||||
</div> |
||||
|
||||
<button type="submit" class="btn btn-block btn-primary">Create New Product</button> |
||||
|
||||
{% if error != '' %} |
||||
<p><strong>{{ error }}</strong></p> |
||||
{% endif %} |
||||
|
||||
</form> |
||||
|
||||
{% endblock %} |
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
{% extends 'base.html' %} |
||||
|
||||
{% block header %} |
||||
<h1 class="text-center">Create Stock Count</h1> |
||||
{% endblock %} |
||||
|
||||
{% block content %} |
||||
<form method="post" class=""> |
||||
|
||||
<div class="form-group"> |
||||
<label class="form-label" for="stock_date">Count Date</label> |
||||
<input id="stock_date" name="stock_date" class="form-input" type="date" required> |
||||
</div> |
||||
|
||||
<div class="form-group"> |
||||
<label class="form-label" for="stock_user">User</label> |
||||
<input id="stock_user" name="stock_user" class="form-input"> |
||||
</div> |
||||
|
||||
<div class="form-group"> |
||||
<label class="form-label" for="stock_site">Site</label> |
||||
<select class="form-select" name="stock_site"> |
||||
<option>---</option> |
||||
{% for site in sites %} |
||||
<option value="{{site.site_id}}">{{site.site_name}}</option> |
||||
{% endfor %} |
||||
</select> |
||||
</div> |
||||
|
||||
<button type="submit" class="btn btn-block btn-primary">Create Stock Count</button> |
||||
|
||||
{% if error != '' %} |
||||
<p><strong>{{ error }}</strong></p> |
||||
{% endif %} |
||||
|
||||
</form> |
||||
|
||||
{% endblock %} |
@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
|
||||
{% extends 'base.html' %} |
||||
|
||||
{% block header %} |
||||
<h1>Stock Counts</h1> |
||||
{% endblock %} |
||||
|
||||
{% block toolbar %} |
||||
|
||||
<a class="btn btn-primary" href="{{url_for('views.stock_create')}}">Create Stock Count</a> |
||||
|
||||
{% endblock %} |
||||
|
||||
|
||||
{% block content %} |
||||
|
||||
<table class="table"> |
||||
<thead> |
||||
<tr> |
||||
<th>Stock Date</th> |
||||
<th>Staff</th> |
||||
<th>Site</th> |
||||
<th></th> |
||||
</tr> |
||||
</thead> |
||||
|
||||
<tbody> |
||||
{% for count in stock_counts %} |
||||
<tr> |
||||
<td>{{count.stock_date.strftime('%Y-%m-%d')}}</td> |
||||
<td>{{count.stock_user}}</td> |
||||
<td>{{count.site_name}}</td> |
||||
<td> |
||||
<a href="{{url_for('views.stock_view', stock_id=count.stock_id)}}"> |
||||
<i class="icon icon-edit"></i> |
||||
</a> |
||||
</td> |
||||
</tr> |
||||
|
||||
{% endfor %} |
||||
|
||||
|
||||
</tbody> |
||||
</table> |
||||
|
||||
{% endblock %} |
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
{% extends 'base.html' %} |
||||
|
||||
{% block header %} |
||||
<h2 class="text-center">Product Search</h2> |
||||
{% endblock %} |
||||
|
||||
{% block content %} |
||||
|
||||
|
||||
|
||||
{{results.prod_id}} |
||||
{{results.prod_code}} |
||||
{{results.prod_name}} |
||||
{{results.prod_barcode}} |
||||
|
||||
|
||||
<!-- <form method="post" class=""> |
||||
|
||||
<div class="form-group"> |
||||
<label class="form-label" for="prod_barcode">Barcode</label> |
||||
<input id="prod_barcode" name="prod_barcode" class="form-input" type="text"> |
||||
</div> |
||||
|
||||
<button type="submit" class="btn btn-block btn-primary">Search</button> |
||||
|
||||
{% if error != '' %} |
||||
<p><strong>{{ error }}</strong></p> |
||||
{% endif %} |
||||
|
||||
</form> --> |
||||
|
||||
{% endblock %} |
@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
|
||||
{% extends 'base.html' %} |
||||
|
||||
{% block toolbar %} |
||||
|
||||
<a href="{{url_for( 'views.product_search')}}" class="btn btn-primary">Add Product</a> |
||||
|
||||
{% endblock %} |
||||
|
||||
{% block header%} |
||||
|
||||
<h1> Stock Listing</h1> |
||||
|
||||
{% endblock %} |
||||
|
||||
{% block content %} |
||||
|
||||
{% for count in counts %} |
||||
<div class="tile mt-2 bg-secondary p-2"> |
||||
|
||||
<div class="tile-content"> |
||||
<div class="tile-title">{{count.prod_name}}</div> |
||||
<div class="columns"> |
||||
<span class="column col-8">{{count.prod_barcode}}</span> |
||||
<span class="column col-4">Qty: {{count.count_count}}</span> |
||||
</div> |
||||
|
||||
</div> |
||||
<div class="tile-action"> |
||||
<a class="btn" href="{{url_for('views.count_edit', count_id=count.count_id)}}"> |
||||
<i class="icon icon-edit"></i> |
||||
</a> |
||||
<a class="btn" href="{{url_for('views.count_delete', count_id=count.count_id)}}"> |
||||
<i class="icon icon-cross"></i> |
||||
</a> |
||||
</div> |
||||
|
||||
</div> |
||||
|
||||
{% endfor %} |
||||
|
||||
{% endblock %} |
@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
|
||||
{% extends 'base.html' %} |
||||
|
||||
{% block header %} |
||||
<h1>EPOS Products</h1> |
||||
{% endblock %} |
||||
|
||||
{% block toolbar %} |
||||
<form method="post"> |
||||
<div class="form-group"> |
||||
<input type="text" id="search" name="search" class="form-input" placeholder="Search" required> |
||||
</div> |
||||
|
||||
<div class="form-group"> |
||||
<button type="submit" class="btn btn-block btn-primary">Search</button> |
||||
</div> |
||||
|
||||
<a class="btn btn-block" href="{{ url_for('views.products') }}">Clear</a> |
||||
</form> |
||||
{% endblock %} |
||||
|
||||
{% block content %} |
||||
|
||||
{% for prod in products %} |
||||
<details class="mt-2 bg-secondary p-1" id="{{prod.prod_name}}"> |
||||
<summary class="">{{prod.prod_name}}</summary> |
||||
<div class="p-2"> |
||||
<div class="columns"> |
||||
<span class="column"><strong>Product Code:</strong></span> |
||||
<span class="column text-right">{{prod.prod_code}}</span> |
||||
</div> |
||||
|
||||
<div class="columns"> |
||||
<span class="column"><strong>Barcode:</strong></span> |
||||
<span class="column text-right">{{prod.prod_barcode}}</span> |
||||
</div> |
||||
|
||||
<div class="columns"> |
||||
<span class="column"><strong>Price:</strong></span> |
||||
<span class="column text-right">ยฃ {{prod.prod_cost}}</span> |
||||
</div> |
||||
|
||||
<div class="columns"> |
||||
<span class="column"><strong>Description:</strong></span> |
||||
<span class="column text-right">{{prod.prod_desc}}</span> |
||||
</div> |
||||
|
||||
<div class="columns"> |
||||
<span class="column"><strong>EPOS ID:</strong></span> |
||||
<span class="column text-right">{{prod.prod_id}}</span> |
||||
</div> |
||||
</div> |
||||
</details> |
||||
|
||||
{% endfor %} |
||||
</tbody> |
||||
{% endblock %} |
@ -0,0 +1,325 @@
@@ -0,0 +1,325 @@
|
||||
from flask import ( |
||||
Blueprint, flash, g, redirect, render_template, request, url_for, session |
||||
) |
||||
|
||||
from stock.auth import login_required |
||||
from stock.db import ( |
||||
get_db, |
||||
db, |
||||
Products, |
||||
Stock, |
||||
Count, |
||||
Sites |
||||
) |
||||
|
||||
import datetime |
||||
from uuid import uuid4 |
||||
|
||||
|
||||
bp = Blueprint('views', __name__) |
||||
|
||||
@bp.route('/') |
||||
@login_required |
||||
def index(): |
||||
|
||||
db = get_db() |
||||
|
||||
stock_counts = Stock.query\ |
||||
.join(Sites)\ |
||||
.add_columns( |
||||
Stock.stock_id, |
||||
Stock.stock_date, |
||||
Stock.stock_user, |
||||
Sites.site_name |
||||
)\ |
||||
.all() |
||||
|
||||
return render_template( |
||||
'index.html', |
||||
stock_counts=stock_counts |
||||
) |
||||
|
||||
|
||||
@bp.route( |
||||
'/products', |
||||
methods=['GET', 'POST'] |
||||
) |
||||
@login_required |
||||
def products(): |
||||
|
||||
if request.method == 'POST': |
||||
|
||||
products = Products.query.filter( |
||||
Products.prod_name.like('%' + request.form['search'] + '%') | |
||||
Products.prod_code.like('%' + request.form['search'] + '%') |
||||
) |
||||
|
||||
else: |
||||
products = Products.query.all() |
||||
|
||||
return render_template( |
||||
'tables/products.html', |
||||
products=products |
||||
) |
||||
|
||||
|
||||
|
||||
@bp.route( |
||||
'/stock/counts', |
||||
) |
||||
@login_required |
||||
def stock_counts(): |
||||
|
||||
stock_counts = Stock.query.all() |
||||
|
||||
return render_template( |
||||
'tables/stock_counts.html', |
||||
stock_counts= stock_counts |
||||
) |
||||
|
||||
|
||||
@bp.route( |
||||
'/stock/create', |
||||
methods=['GET', 'POST'] |
||||
) |
||||
@login_required |
||||
def stock_create(): |
||||
|
||||
sites = Sites.query.all() |
||||
|
||||
if request.method == 'POST': |
||||
|
||||
stock = Stock( |
||||
stock_id = str(uuid4()), |
||||
stock_date = request.form['stock_date'], |
||||
stock_user = request.form['stock_user'], |
||||
site_id_fk = request.form['stock_site'] |
||||
) |
||||
|
||||
db.session.add(stock) |
||||
db.session.commit() |
||||
|
||||
return redirect( |
||||
url_for('views.index') |
||||
) |
||||
|
||||
return render_template( |
||||
'forms/stock_create.html', |
||||
sites= sites |
||||
) |
||||
|
||||
@bp.route( |
||||
'/stock/view/<stock_id>' |
||||
) |
||||
@login_required |
||||
def stock_view(stock_id): |
||||
|
||||
session['stock_id'] = stock_id |
||||
|
||||
product_counts = Stock.query\ |
||||
.join(Count, Count.stock_id_fk==Stock.stock_id)\ |
||||
.join(Products, Products.prod_id==Count.prod_id_fk)\ |
||||
.add_columns( |
||||
Stock.stock_id, |
||||
Count.count_id, |
||||
Count.count_count, |
||||
Products.prod_barcode, |
||||
Products.prod_name |
||||
)\ |
||||
.filter(Stock.stock_id==stock_id).all() |
||||
|
||||
|
||||
return render_template( |
||||
'tables/counts.html', |
||||
counts= product_counts, |
||||
stock_id = stock_id |
||||
) |
||||
|
||||
|
||||
|
||||
@bp.route( |
||||
'/count/create/<stock_id>/<barcode>', |
||||
methods=['POST', 'GET'] |
||||
) |
||||
@login_required |
||||
def count_create(stock_id, barcode): |
||||
|
||||
prod_search = Products.query.filter_by( |
||||
prod_barcode=barcode |
||||
).first() |
||||
|
||||
if request.method == 'POST': |
||||
|
||||
new_count = Count( |
||||
count_id=str(uuid4()), |
||||
count_count=request.form['prod_count'], |
||||
prod_id_fk=prod_search.prod_id, |
||||
stock_id_fk=session['stock_id'] |
||||
) |
||||
|
||||
db.session.add(new_count) |
||||
|
||||
db.session.commit() |
||||
|
||||
return redirect( |
||||
url_for( |
||||
'views.stock_view', |
||||
stock_id=stock_id |
||||
) |
||||
) |
||||
|
||||
return render_template( |
||||
'forms/count_create.html', |
||||
product= prod_search |
||||
) |
||||
|
||||
|
||||
|
||||
@bp.route( |
||||
'/products/search', |
||||
methods=['POST', 'GET'] |
||||
) |
||||
@login_required |
||||
def product_search(): |
||||
|
||||
if request.method=='POST': |
||||
|
||||
prod_search = Products.query.filter_by( |
||||
prod_barcode=request.form['prod_barcode'] |
||||
).first() |
||||
|
||||
if prod_search == None: |
||||
|
||||
return redirect( |
||||
url_for( |
||||
'views.product_add', |
||||
barcode = request.form['prod_barcode'] |
||||
) |
||||
) |
||||
|
||||
else: |
||||
return redirect( |
||||
url_for( |
||||
'views.count_create', |
||||
stock_id=session['stock_id'], |
||||
barcode=prod_search.prod_barcode |
||||
) |
||||
) |
||||
|
||||
return render_template( |
||||
'forms/barcode_search.html' |
||||
) |
||||
|
||||
|
||||
@bp.route( |
||||
'/products/add/<barcode>', |
||||
methods=['POST', 'GET'] |
||||
) |
||||
def product_add(barcode): |
||||
|
||||
if request.method=='POST': |
||||
|
||||
new_product = Products( |
||||
prod_code = request.form['prod_code'], |
||||
prod_barcode = barcode, |
||||
prod_name = request.form['prod_name'], |
||||
prod_desc = 'New Product' |
||||
) |
||||
|
||||
db.session.add(new_product) |
||||
db.session.commit() |
||||
|
||||
return redirect( |
||||
url_for( |
||||
'views.count_create', |
||||
stock_id=session['stock_id'], |
||||
barcode=barcode |
||||
) |
||||
) |
||||
|
||||
return render_template( |
||||
'forms/product_create.html' |
||||
) |
||||
|
||||
|
||||
@bp.route( |
||||
'/count/delete/<count_id>', |
||||
methods=['POST', 'GET'] |
||||
) |
||||
@login_required |
||||
def count_delete(count_id): |
||||
|
||||
count = Stock.query\ |
||||
.join(Count, Count.stock_id_fk==Stock.stock_id)\ |
||||
.join(Products, Products.prod_id==Count.prod_id_fk)\ |
||||
.add_columns( |
||||
Stock.stock_id, |
||||
Count.count_id, |
||||
Count.count_count, |
||||
Products.prod_barcode, |
||||
Products.prod_name |
||||
)\ |
||||
.filter(Count.count_id==count_id).first() |
||||
|
||||
if request.method == 'POST': |
||||
|
||||
if request.form['submit'] == 'delete': |
||||
|
||||
count = Count.query.filter_by(count_id=count_id).first() |
||||
db.session.delete(count) |
||||
db.session.commit() |
||||
|
||||
return redirect( |
||||
url_for( |
||||
'views.stock_view', |
||||
stock_id=session['stock_id'] |
||||
) |
||||
) |
||||
|
||||
|
||||
return render_template( |
||||
'forms/delete_confirm.html' |
||||
) |
||||
|
||||
|
||||
@bp.route( |
||||
'/count/edit/<count_id>', |
||||
methods=['POST', 'GET'] |
||||
) |
||||
@login_required |
||||
def count_edit(count_id): |
||||
|
||||
count = Count.query\ |
||||
.join(Products, Products.prod_id==Count.prod_id_fk)\ |
||||
.add_columns( |
||||
Products.prod_barcode, |
||||
Products.prod_code, |
||||
Products.prod_name, |
||||
Count.count_count |
||||
)\ |
||||
.filter(Count.count_id==count_id).first() |
||||
|
||||
if request.method=='POST': |
||||
|
||||
updated_count = Count.query\ |
||||
.filter_by(count_id=count_id)\ |
||||
.update(dict(count_count=request.form['prod_count'])) |
||||
|
||||
db.session.commit() |
||||
|
||||
return redirect( |
||||
url_for( |
||||
'views.stock_view', |
||||
stock_id=session['stock_id'] |
||||
) |
||||
) |
||||
|
||||
return render_template( |
||||
'forms/count_edit.html', |
||||
count=count |
||||
) |
||||
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,64 @@
@@ -0,0 +1,64 @@
|
||||
import os |
||||
import tempfile |
||||
|
||||
import pytest |
||||
from flaskr import create_app |
||||
from flaskr.db import get_db, init_db |
||||
|
||||
# read in SQL for populating test data |
||||
with open(os.path.join(os.path.dirname(__file__), 'data.sql'), 'rb') as f: |
||||
_data_sql = f.read().decode('utf8') |
||||
|
||||
|
||||
@pytest.fixture |
||||
def app(): |
||||
"""Create and configure a new app instance for each test.""" |
||||
# create a temporary file to isolate the database for each test |
||||
db_fd, db_path = tempfile.mkstemp() |
||||
# create the app with common test config |
||||
app = create_app({ |
||||
'TESTING': True, |
||||
'DATABASE': db_path, |
||||
}) |
||||
|
||||
# create the database and load test data |
||||
with app.app_context(): |
||||
init_db() |
||||
get_db().executescript(_data_sql) |
||||
|
||||
yield app |
||||
|
||||
# close and remove the temporary database |
||||
os.close(db_fd) |
||||
os.unlink(db_path) |
||||
|
||||
|
||||
@pytest.fixture |
||||
def client(app): |
||||
"""A test client for the app.""" |
||||
return app.test_client() |
||||
|
||||
|
||||
@pytest.fixture |
||||
def runner(app): |
||||
"""A test runner for the app's Click commands.""" |
||||
return app.test_cli_runner() |
||||
|
||||
|
||||
class AuthActions(object): |
||||
def __init__(self, client): |
||||
self._client = client |
||||
|
||||
def login(self, username='test', password='test'): |
||||
return self._client.post( |
||||
'/auth/login', |
||||
data={'username': username, 'password': password} |
||||
) |
||||
|
||||
def logout(self): |
||||
return self._client.get('/auth/logout') |
||||
|
||||
|
||||
@pytest.fixture |
||||
def auth(client): |
||||
return AuthActions(client) |
@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
|
||||
import pytest |
||||
from flask import g, session |
||||
from flaskr.db import get_db |
||||
|
||||
|
||||
def test_register(client, app): |
||||
# test that viewing the page renders without template errors |
||||
assert client.get('/auth/register').status_code == 200 |
||||
|
||||
# test that successful registration redirects to the login page |
||||
response = client.post( |
||||
'/auth/register', data={'username': 'a', 'password': 'a'} |
||||
) |
||||
assert 'http://localhost/auth/login' == response.headers['Location'] |
||||
|
||||
# test that the user was inserted into the database |
||||
with app.app_context(): |
||||
assert get_db().execute( |
||||
"select * from user where username = 'a'", |
||||
).fetchone() is not None |
||||
|
||||
|
||||
@pytest.mark.parametrize(('username', 'password', 'message'), ( |
||||
('', '', b'Username is required.'), |
||||
('a', '', b'Password is required.'), |
||||
('test', 'test', b'already registered'), |
||||
)) |
||||
def test_register_validate_input(client, username, password, message): |
||||
response = client.post( |
||||
'/auth/register', |
||||
data={'username': username, 'password': password} |
||||
) |
||||
assert message in response.data |
||||
|
||||
|
||||
def test_login(client, auth): |
||||
# test that viewing the page renders without template errors |
||||
assert client.get('/auth/login').status_code == 200 |
||||
|
||||
# test that successful login redirects to the index page |
||||
response = auth.login() |
||||
assert response.headers['Location'] == 'http://localhost/' |
||||
|
||||
# login request set the user_id in the session |
||||
# check that the user is loaded from the session |
||||
with client: |
||||
client.get('/') |
||||
assert session['user_id'] == 1 |
||||
assert g.user['username'] == 'test' |
||||
|
||||
|
||||
@pytest.mark.parametrize(('username', 'password', 'message'), ( |
||||
('a', 'test', b'Incorrect username.'), |
||||
('test', 'a', b'Incorrect password.'), |
||||
)) |
||||
def test_login_validate_input(auth, username, password, message): |
||||
response = auth.login(username, password) |
||||
assert message in response.data |
||||
|
||||
|
||||
def test_logout(client, auth): |
||||
auth.login() |
||||
|
||||
with client: |
||||
auth.logout() |
||||
assert 'user_id' not in session |
@ -0,0 +1,92 @@
@@ -0,0 +1,92 @@
|
||||
import pytest |
||||
from flaskr.db import get_db |
||||
|
||||
|
||||
def test_index(client, auth): |
||||
response = client.get('/') |
||||
assert b"Log In" in response.data |
||||
assert b"Register" in response.data |
||||
|
||||
auth.login() |
||||
response = client.get('/') |
||||
assert b'test title' in response.data |
||||
assert b'by test on 2018-01-01' in response.data |
||||
assert b'test\nbody' in response.data |
||||
assert b'href="/1/update"' in response.data |
||||
|
||||
|
||||
@pytest.mark.parametrize('path', ( |
||||
'/create', |
||||
'/1/update', |
||||
'/1/delete', |
||||
)) |
||||
def test_login_required(client, path): |
||||
response = client.post(path) |
||||
assert response.headers['Location'] == 'http://localhost/auth/login' |
||||
|
||||
|
||||
def test_author_required(app, client, auth): |
||||
# change the post author to another user |
||||
with app.app_context(): |
||||
db = get_db() |
||||
db.execute('UPDATE post SET author_id = 2 WHERE id = 1') |
||||
db.commit() |
||||
|
||||
auth.login() |
||||
# current user can't modify other user's post |
||||
assert client.post('/1/update').status_code == 403 |
||||
assert client.post('/1/delete').status_code == 403 |
||||
# current user doesn't see edit link |
||||
assert b'href="/1/update"' not in client.get('/').data |
||||
|
||||
|
||||
@pytest.mark.parametrize('path', ( |
||||
'/2/update', |
||||
'/2/delete', |
||||
)) |
||||
def test_exists_required(client, auth, path): |
||||
auth.login() |
||||
assert client.post(path).status_code == 404 |
||||
|
||||
|
||||
def test_create(client, auth, app): |
||||
auth.login() |
||||
assert client.get('/create').status_code == 200 |
||||
client.post('/create', data={'title': 'created', 'body': ''}) |
||||
|
||||
with app.app_context(): |
||||
db = get_db() |
||||
count = db.execute('SELECT COUNT(id) FROM post').fetchone()[0] |
||||
assert count == 2 |
||||
|
||||
|
||||
def test_update(client, auth, app): |
||||
auth.login() |
||||
assert client.get('/1/update').status_code == 200 |
||||
client.post('/1/update', data={'title': 'updated', 'body': ''}) |
||||
|
||||
with app.app_context(): |
||||
db = get_db() |
||||
post = db.execute('SELECT * FROM post WHERE id = 1').fetchone() |
||||
assert post['title'] == 'updated' |
||||
|
||||
|
||||
@pytest.mark.parametrize('path', ( |
||||
'/create', |
||||
'/1/update', |
||||
)) |
||||
def test_create_update_validate(client, auth, path): |
||||
auth.login() |
||||
response = client.post(path, data={'title': '', 'body': ''}) |
||||
assert b'Title is required.' in response.data |
||||
|
||||
|
||||
def test_delete(client, auth, app): |
||||
auth.login() |
||||
response = client.post('/1/delete') |
||||
assert response.headers['Location'] == 'http://localhost/' |
||||
|
||||
with app.app_context(): |
||||
db = get_db() |
||||
post = db.execute('SELECT * FROM post WHERE id = 1').fetchone() |
||||
assert post is None |
@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
|
||||
import sqlite3 |
||||
|
||||
import pytest |
||||
from flaskr.db import get_db |
||||
|
||||
|
||||
def test_get_close_db(app): |
||||
with app.app_context(): |
||||
db = get_db() |
||||
assert db is get_db() |
||||
|
||||
with pytest.raises(sqlite3.ProgrammingError) as e: |
||||
db.execute('SELECT 1') |
||||
|
||||
assert 'closed' in str(e) |
||||
|
||||
|
||||
def test_init_db_command(runner, monkeypatch): |
||||
class Recorder(object): |
||||
called = False |
||||
|
||||
def fake_init_db(): |
||||
Recorder.called = True |
||||
|
||||
monkeypatch.setattr('flaskr.db.init_db', fake_init_db) |
||||
result = runner.invoke(args=['init-db']) |
||||
assert 'Initialized' in result.output |