Merge branch 'feat_receipt_upload'

This commit is contained in:
Lunaresk 2023-03-19 12:56:08 +01:00
commit 8069a794e2
130 changed files with 14163 additions and 597 deletions

9
.dockerignore Normal file
View File

@ -0,0 +1,9 @@
# Ignore pycaches
logs
__pycache__
.flaskenv
.env*
.flaskenv*
!.env.project
!.env.vault

12
.env.vault Normal file
View File

@ -0,0 +1,12 @@
#################################################################################
# #
# This file uniquely identifies your project in dotenv-vault. #
# You SHOULD commit this file to source control. #
# #
# Generated with 'npx dotenv-vault new' #
# #
# Learn more at https://dotenv.org/env-vault #
# #
#################################################################################
DOTENV_VAULT=vlt_54536d38674329461d10b74ec7e56be43df6021bb5267bedec1f774758371784

226
.gitignore vendored
View File

@ -1,3 +1,45 @@
# Created by https://www.toptal.com/developers/gitignore/api/flask,python,git,visualstudiocode,angular,venv
# Edit at https://www.toptal.com/developers/gitignore?templates=flask,python,git,visualstudiocode,angular,venv
### Angular ###
## Angular ##
# compiled output
dist/
tmp/
app/**/*.js
app/**/*.js.map
# dependencies
node_modules/
bower_components/
# IDEs and editors
.idea/
# misc
.sass-cache/
connect.lock/
coverage/
libpeerconnection.log/
npm-debug.log
testem.log
typings/
.angular/
# e2e
e2e/*.js
e2e/*.map
# System Files
.DS_Store/
### Flask ###
instance/*
!instance/.gitignore
.webassets-cache
.env
### Flask.Python Stack ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@ -10,7 +52,6 @@ __pycache__/
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
@ -20,7 +61,6 @@ parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
@ -50,6 +90,7 @@ coverage.xml
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
@ -63,7 +104,6 @@ db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
@ -72,6 +112,7 @@ instance/
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
@ -82,7 +123,9 @@ profile_default/
ipython_config.py
# pyenv
.python-version
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
@ -91,7 +134,22 @@ ipython_config.py
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
@ -102,9 +160,7 @@ celerybeat.pid
*.sage.py
# Environments
.env
.venv
.flaskenv
env/
venv/
ENV/
@ -129,6 +185,157 @@ dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### Git ###
# Created by git for backups. To disable backups in Git:
# $ git config --global mergetool.keepBackup false
*.orig
# Created by git when using merge tools for conflicts
*.BACKUP.*
*.BASE.*
*.LOCAL.*
*.REMOTE.*
*_BACKUP_*.txt
*_BASE_*.txt
*_LOCAL_*.txt
*_REMOTE_*.txt
### Python ###
# Byte-compiled / optimized / DLL files
# C extensions
# Distribution / packaging
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
# Installer logs
# Unit test / coverage reports
# Translations
# Django stuff:
# Flask stuff:
# Scrapy stuff:
# Sphinx documentation
# PyBuilder
# Jupyter Notebook
# IPython
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
# Celery stuff
# SageMath parsed files
# Environments
# Spyder project settings
# Rope project settings
# mkdocs documentation
# mypy
# Pyre type checker
# pytype static type analyzer
# Cython debug symbols
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
### venv ###
# Virtualenv
# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
[Bb]in
[Ii]nclude
[Ll]ib
[Ll]ib64
[Ll]ocal
[Ss]cripts
pyvenv.cfg
pip-selfcheck.json
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
# End of https://www.toptal.com/developers/gitignore/api/flask,python,git,visualstudiocode,angular,venv
# Misc
config.yaml
scans.json
@ -136,3 +343,8 @@ test.*
*.db
.vscode
*.tar
*.code-workspace
.env*
.flaskenv*
!.env.project
!.env.vault

View File

@ -1,27 +1,25 @@
FROM python@sha256:38000b248a186dcae150fe2f64d23bd44a0730347d1e5e4d1faedd449a9a4913
FROM python@sha256:53da4973924b6b3da6eb34f98e4e9dffdaf1cc05b468da73c69e3a862c36ee19
# python:3.10.10-slim-bullseye
RUN useradd costhive
RUN useradd scan2kasse
WORKDIR /home/costhive
WORKDIR /home/scan2kasse
RUN apt update && apt upgrade
RUN apt update && apt -y upgrade
RUN apt install -y libpq-dev gcc g++
COPY requirements.txt requirements.txt
RUN python -m venv venv
RUN venv/bin/pip install -r requirements.txt
RUN venv/bin/pip install gunicorn
RUN python -m pip install pipenv
COPY app app
COPY migrations migrations
COPY configs configs
COPY run.py boot.sh ./
COPY boot.sh ./
RUN chmod +x boot.sh
COPY backend backend
ENV FLASK_APP run.py
RUN chown -R scan2kasse:scan2kasse ./
USER scan2kasse
RUN chown -R costhive:costhive ./
USER costhive
RUN cd backend && pipenv install && pipenv install gunicorn && cd ..
EXPOSE 5000
ENTRYPOINT ["./boot.sh"]

View File

@ -1 +1 @@
# WG_Scan2Kasse
# CostHive

View File

@ -1,5 +0,0 @@
from flask import Blueprint
bp = Blueprint('api', __name__)
from app.api import routes

View File

@ -1,5 +0,0 @@
from flask import Blueprint
bp = Blueprint('auth', __name__)
from app.auth import forms, routes

View File

@ -1,5 +0,0 @@
from flask import Blueprint
bp = Blueprint('errors', __name__)
from app.errors import handlers

View File

@ -1,116 +0,0 @@
from app import db, LOGGER
from app.main.forms import NewItemForm
from app.main import bp
from app.models import AmountChange, Brand, Establishment, LoginToken, Item, PriceChange
from app.utils import view_utils, database_utils
from app.utils.routes_utils import render_custom_template as render_template
from datetime import date
from flask import abort, redirect, request, url_for
from flask.json import jsonify
from flask_login import current_user, login_required
@bp.route('/')
@bp.route('/index')
def index():
return render_template("base.html")
# @bp.route('/')
# def test():
# return "Hello World"
@bp.route('/overview', methods=['GET'])
@login_required
def get_report_from_user():
if current_user.is_anonymous:
abort(403)
if 'month' in request.args:
try:
month = int(request.args['month'])
except Exception as e:
LOGGER.exception("")
abort(400)
else:
if (month > 12 or month < 1):
abort(400)
LOGGER.info("Getting results.")
results = database_utils.get_report(**request.args)
LOGGER.debug(f"Results received.")
# LOGGER.debug(str(results))
if results:
result_list = view_utils.group_results(results)
else:
result_list = []
if request.content_type == "application/json":
return jsonify(result_list)
else:
if "establishment" in request.args:
return render_template("main/overview.html", results=result_list, establishment = Establishment.query.get(int(request.args['establishment'])))
else:
return render_template("main/overview.html", results=result_list)
@bp.route('/token_authorization')
def token_authorization():
LOGGER.debug("Token Login")
if not request.json or 'login' not in request.json:
abort(400)
if not LoginToken.query.filter_by(token=request.json['login']).first():
abort(403)
return jsonify({}), 200
@bp.route('/token_insert', methods=['POST'])
def insert():
match request.json:
case {'user': user, 'items': items, 'date': date}:
failed = database_utils.insert_bought_items(user, items, date)
case {'user': user, 'items': items}:
failed = database_utils.insert_bought_items(user, items)
case _:
abort(400)
if failed:
return jsonify(failed), 400
return jsonify({'inserted': True}), 201
@bp.route('/new_item', methods=['GET', 'POST'])
@login_required
def new_item():
if current_user.is_anonymous:
abort(403)
form=NewItemForm.new()
if form.is_submitted():
LOGGER.debug("submitted")
if form.validate():
LOGGER.debug("valid")
else:
LOGGER.debug(form.errors)
if form.validate_on_submit():
LOGGER.debug("valid form")
brand = Brand.query.get(form.brand.data)
new_item = Item(id = form.id.data, name = form.name.data, brand = brand.id, description = form.description.data)
# if form.category.data:
# category = Category.query.get(id = form.category.data)
# new_item.Category = category
new_item.PriceChange = [PriceChange(Item = new_item, date = date(2021, 12, 1), price = form.price_change.data)]
if form.amount_change.data:
new_item.AmountChange = [AmountChange(Item = new_item, date = date(2021, 12, 1), amount = form.amount_change.data)]
db.session.add(new_item)
db.session.commit()
return redirect(url_for('index'))
return render_template('main/new_item.html', form=form)
@bp.route('/overview/register_boughts', methods=['GET'])
@login_required
def check_unregistered_items():
if current_user.is_anonymous or not request.args or 'establishment' not in request.args:
abort(403)
establishment = Establishment.query.get(int(request.args['establishment']))
if current_user.id != establishment.owner:
abort(403)
results = database_utils.get_unregistered_and_register(establishment.id)
if results:
result_list = view_utils.group_results(results)
else:
result_list = []
if request.content_type == "application/json":
return jsonify(result_list)
else:
return render_template("main/overview.html", results=result_list)

View File

@ -1,143 +0,0 @@
import jwt
from app import db, login
from datetime import date
from flask import current_app
from flask_login import UserMixin
from time import time
from werkzeug.security import generate_password_hash, check_password_hash
item_category = db.Table("item_category",
db.Column("item", db.ForeignKey("item.id"), primary_key=True, server_onupdate=db.FetchedValue()),
db.Column("category", db.ForeignKey("category.id"), primary_key=True, server_onupdate=db.FetchedValue())
)
class User(UserMixin, db.Model):
id = db.Column(db.BigInteger, primary_key=True)
email = db.Column(db.String(255), nullable=False, unique=True)
password_hash = db.Column(db.String(128), nullable=False)
LoginToken = db.relationship("LoginToken", backref='User', lazy='dynamic')
Bought = db.relationship("Bought", secondary="login_token",
lazy='dynamic', overlaps="User,LoginToken")
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def get_reset_password_token(self, expires_in=600):
return jwt.encode(
{'reset_password': self.id, 'exp': time() + expires_in},
current_app.config['SECRET_KEY'], algorithm='HS256')
@staticmethod
def verify_reset_password_token(token):
try:
id = jwt.decode(token, current_app.config['SECRET_KEY'],
algorithms=['HS256'])['reset_password']
except:
return
return User.query.get(id)
def __repr__(self) -> str:
return f"<User {self.id} ({self.email})>"
class Establishment(db.Model):
id = db.Column(db.BigInteger, primary_key=True)
name = db.Column(db.String(64), nullable=False)
owner = db.Column(db.ForeignKey('user.id'), nullable=False)
LoginToken = db.relationship("LoginToken", backref='Establishment', lazy='dynamic')
Bought = db.relationship("Bought", secondary="login_token",
lazy='dynamic', overlaps="Establishment,LoginToken,Bought")
def __repr__(self) -> str:
return f"<Establishment {self.id} ({self.name})>"
class LoginToken(db.Model):
user = db.Column(db.ForeignKey('user.id'), primary_key=True, server_onupdate=db.FetchedValue())
establishment = db.Column(db.ForeignKey('establishment.id'), primary_key=True, server_onupdate=db.FetchedValue())
token = db.Column(db.String(15), nullable=True, unique=True)
paid = db.Column(db.BigInteger, nullable=False, server_default=str(0))
def __repr__(self) -> str:
return f"LoginToken {self.token}"
class Brand(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(32), nullable=False)
def __repr__(self) -> str:
return f"<Brand {self.id} ({self.name})>"
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(32), nullable=False, unique=True)
Item = db.relationship("Item", secondary=item_category, lazy="dynamic", back_populates="Category")
def __repr__(self) -> str:
return f"<Category {self.id} ({self.name})>"
class Item(db.Model):
id = db.Column(db.BigInteger, primary_key=True, autoincrement=False)
name = db.Column(db.String(64), nullable=False)
brand = db.Column(db.ForeignKey('brand.id'), nullable=False, server_onupdate=db.FetchedValue())
description = db.Column(db.Text, nullable=False)
Category = db.relationship("Category", secondary=item_category, lazy="dynamic", back_populates="Item")
Bought = db.relationship("Bought", backref='Item', lazy='dynamic')
PriceChange = db.relationship("PriceChange", backref='Item', lazy='dynamic')
AmountChange = db.relationship("AmountChange", backref='Item', lazy='dynamic')
def __repr__(self) -> str:
return f"<Item {self.id} ({self.name})>"
class Bought(db.Model):
token = db.Column(db.ForeignKey('login_token.token'), primary_key=True, server_onupdate=db.FetchedValue())
item = db.Column(db.ForeignKey('item.id'), primary_key=True, server_onupdate=db.FetchedValue())
date = db.Column(db.Date, primary_key=True)
amount = db.Column(db.SmallInteger, nullable=False)
registered = db.Column(db.Boolean, nullable=False, server_default=str(False))
def __repr__(self) -> str:
return f"<Bought Object>"
class PriceChange(db.Model):
item = db.Column(db.ForeignKey('item.id'), primary_key=True, server_onupdate=db.FetchedValue())
date = db.Column(db.Date, primary_key=True, server_default=str(date(2021, 12, 1)))
price = db.Column(db.SmallInteger, nullable=False)
def __repr__(self) -> str:
return f"<Price_Change {self.item} ({self.date})>"
class AmountChange(db.Model):
item = db.Column(db.ForeignKey('item.id'), primary_key=True, server_onupdate=db.FetchedValue())
date = db.Column(db.Date, primary_key=True, server_default=str(date(2021, 12, 1)))
amount = db.Column(db.SmallInteger, nullable=False, server_default=str(1))
def __repr__(self) -> str:
return f"<Amount_Change {self.item} ({self.date})>"
class Receipt(db.Model):
id = db.Column(db.Numeric(precision=24, scale=0), primary_key=True, autoincrement=False)
date = db.Column(db.Date, nullable=False)
from_user = db.Column(db.ForeignKey("login_token.token"), server_onupdate=db.FetchedValue())
registered = db.Column(db.Boolean, nullable=False, server_default=str(False))
def __repr__(self) -> str:
return f"<Receipt {self.id}>"
class ItemReceipt(db.Model):
receipt = db.Column(db.ForeignKey("receipt.id"), primary_key=True, server_onupdate=db.FetchedValue())
item = db.Column(db.ForeignKey("item.id"), primary_key=True, server_onupdate=db.FetchedValue())
amount = db.Column(db.SmallInteger, nullable=False)
def __repr__(self) -> str:
return f"<ItemReceipt {self.receipt}: {self.item}>"
@login.user_loader
def load_user(id):
return User.query.get(int(id))

View File

@ -1,19 +0,0 @@
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>Register</h1>
<form action="" method="post">
{{ form.hidden_tag() }}
<p>
{{ wtf.form_field(form.email) }}
</p>
<p>
{{ wtf.form_field(form.password) }}
</p>
<p>
{{ wtf.form_field(form.password2) }}
</p>
{{ wtf.form_field(form.submit, class=form.submit.render_kw["class"]) }}
</form>
{% endblock %}

View File

@ -1,116 +0,0 @@
{% extends "bootstrap/base.html" %}
{% block title %}
{% if title %}
{{ title }}
{% else %}
Scan2Kasse
{% endif %}
{% endblock %}
{% block metas %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{% endblock %}
{% block styles %}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<link rel="stylesheet" href={{ url_for('static', filename="sidebars.css")}}>
{% endblock %}
{% block navbar %}
<main>
<div class="flex-shrink-0 p-3 bg-black bg-opacity-10 scrollbar-primary h-100 position-fixed" style="width: 280px;">
<a href="/" class="d-flex align-items-center pb-3 mb-3 link-dark text-decoration-none border-bottom border-dark">
<svg class="bi me-2" width="30" height="24"><use xlink:href="#bootstrap"></use></svg>
<span class="fs-5 fw-semibold">Scan2Kasse</span>
</a>
<ul class="list-unstyled ps-0">
<li class="mb-1">
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#home-collapse" aria-expanded="true">
Home (WIP)
</button>
<div class="collapse show" id="home-collapse">
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
<li><a href="#" class="link-dark rounded">Übersicht</a></li>
<li><a href="#" class="link-dark rounded">Updates</a></li>
<li><a href="#" class="link-dark rounded">Reports</a></li>
</ul>
</div>
</li>
<li class="mb-1">
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#dashboard-collapse" aria-expanded="false">
Übersicht
</button>
<div class="collapse" id="dashboard-collapse">
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
{% if establishments %}
<li><a href="#" class="link-dark rounded">Allgemein</a></li>
{% for establishment in establishments %}
<li><a href="{{ url_for('main.get_report_from_user', establishment=establishment.id) }}" class="link-dark rounded">{{ establishment.name }}</a></li>
{% endfor %}
{% endif %}
</ul>
</div>
</li>
<!-- <li class="mb-1">
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#orders-collapse" aria-expanded="false">
Orders
</button>
<div class="collapse" id="orders-collapse">
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
<li><a href="#" class="link-dark rounded">New</a></li>
<li><a href="#" class="link-dark rounded">Processed</a></li>
<li><a href="#" class="link-dark rounded">Shipped</a></li>
<li><a href="#" class="link-dark rounded">Returned</a></li>
</ul>
</div>
</li> -->
<li class="border-top border-dark my-3"></li>
<li class="mb-1">
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse" data-bs-target="#account-collapse" aria-expanded="false">
Account
</button>
<div class="collapse" id="account-collapse" style="">
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
<!-- <li><a href="#" class="link-dark rounded">New...</a></li>
<li><a href="#" class="link-dark rounded">Settings</a></li> -->
{% if current_user.is_authenticated %}
<!-- <li><a href="#" class="link-dark rounded">Profile</a></li> -->
<li><a href={{ url_for('auth.web_logout') }} class="link-dark rounded">Sign out</a></li>
{% else %}
<li><a href={{ url_for('auth.web_register') }} class="link-dark rounded">Register</a></li>
<li><a href={{ url_for('auth.web_login') }} class="link-dark rounded">Sign in</a></li>
{% endif %}
</ul>
</div>
</li>
</ul>
</div>
{% endblock %}
{% block content %}
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<div style="width: 280px;"></div>
<div class="container">
<div class="row my-3"></div>
<div class="row-md-3">
{% block app_content %}{% endblock %}
</div>
<div class="row my-3"></div>
</div>
</main>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> -->
{% endblock %}

View File

@ -1,24 +0,0 @@
{% extends "base.html" %}
{% block app_content %}
<form action="" method="post">
{{ form.hidden_tag() }}
<div class="row">{{form.id.label}}</div>
<div class="row">{{form.id()}}</div>
<div class="row">{{form.name.label}}</div>
<div class="row">{{form.name()}}</div>
<div class="row">{{form.description.label}}</div>
<div class="row">{{form.description()}}</div>
<div class="row">{{form.date.label}}</div>
<div class="row">{{form.date()}}</div>
<div class="row">{{form.price_change.label}}</div>
<div class="row">{{form.price_change()}}</div>
<div class="row">{{form.amount_change.label}}</div>
<div class="row">{{form.amount_change()}}</div>
<div class="row">{{form.category.label}}</div>
<div class="row">{{form.category()}}</div>
<div class="row">{{form.brand.label}}</div>
<div class="row">{{form.brand()}}</div>
<div class="row">{{form.submit()}}</div>
</form>
{% endblock %}

View File

@ -1,38 +0,0 @@
{% extends "base.html" %}
{% block app_content %}
{% if establishment %}
{% if current_user.id == establishment.owner %}
<button type="button" class="btn btn-outline-dark px-2" data-bs-toggle="button" autocomplete="off" onclick="window.location.href='{{ url_for('main.check_unregistered_items', establishment=establishment.id) }}'">
Abrechnung
</button>
{% endif %}
{% endif %}
{% for user in results %}
<div class="card">
<button class="btn btn-primary" data-bs-toggle="collapse" data-bs-target="#b{{ user.id }}" aria-expanded="true">
<div class="card-header">
<h3>{{ user.email }}: {{ user.sum/100 }} €</h3>
</div>
</button>
<div class="collapse" id="b{{ user.id }}">
{% for item_infos in user.item_infos %}
<div class="card-body">
<div class="col-sm-1"></div>
<div class="col">
<h4>{{ item_infos.date }}</h4>
{% for item in item_infos.item_list %}
<div class="row">
<div class="col-sm-1"></div>
<div class="col">
{{ item.amount }}x {{ item.name }} je {{ item.price/100 }} €
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% endblock %}

25
backend/Pipfile Normal file
View File

@ -0,0 +1,25 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
email-validator = "*"
flask = "*"
flask-bootstrap = "*"
flask-login = "*"
flask-mail = "*"
flask-migrate = "*"
flask-sqlalchemy = "*"
flask-wtf = "*"
psycopg2-binary = "*"
pyjwt = "*"
python-dotenv = "*"
pymupdf = "*"
sqlalchemy-utils = "*"
[dev-packages]
[requires]
python_version = "3.10"
python_full_version = "3.10.10"

500
backend/Pipfile.lock generated Normal file
View File

@ -0,0 +1,500 @@
{
"_meta": {
"hash": {
"sha256": "92648b89cfca30fdd2c1a1258fd13c9249dffdcd9450c342a5c8bdf5d433b852"
},
"pipfile-spec": 6,
"requires": {
"python_full_version": "3.10.10",
"python_version": "3.10"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"alembic": {
"hashes": [
"sha256:4d3bd32ecdbb7bbfb48a9fe9e6d6fd6a831a1b59d03e26e292210237373e7db5",
"sha256:6f1c2207369bf4f49f952057a33bb017fbe5c148c2a773b46906b806ea6e825f"
],
"markers": "python_version >= '3.7'",
"version": "==1.9.4"
},
"blinker": {
"hashes": [
"sha256:1eb563df6fdbc39eeddc177d953203f99f097e9bf0e2b8f9f3cf18b6ca425e36",
"sha256:923e5e2f69c155f2cc42dafbbd70e16e3fde24d2d4aa2ab72fbe386238892462"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==1.5"
},
"click": {
"hashes": [
"sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e",
"sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"
],
"markers": "python_version >= '3.7'",
"version": "==8.1.3"
},
"colorama": {
"hashes": [
"sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44",
"sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"
],
"markers": "platform_system == 'Windows'",
"version": "==0.4.6"
},
"dnspython": {
"hashes": [
"sha256:224e32b03eb46be70e12ef6d64e0be123a64e621ab4c0822ff6d450d52a540b9",
"sha256:89141536394f909066cabd112e3e1a37e4e654db00a25308b0f130bc3152eb46"
],
"markers": "python_version >= '3.7' and python_version < '4.0'",
"version": "==2.3.0"
},
"dominate": {
"hashes": [
"sha256:520101360892ebf9d0553f67d37e359ff92403d8a1e33814030503088a05da49",
"sha256:5fe4258614687c6d3de67b0bbd881ed435a93a19742ae187344055db17052402"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.7.0"
},
"email-validator": {
"hashes": [
"sha256:49a72f5fa6ed26be1c964f0567d931d10bf3fdeeacdf97bc26ef1cd2a44e0bda",
"sha256:d178c5c6fa6c6824e9b04f199cf23e79ac15756786573c190d2ad13089411ad2"
],
"index": "pypi",
"version": "==1.3.1"
},
"flask": {
"hashes": [
"sha256:7eb373984bf1c770023fce9db164ed0c3353cd0b53f130f4693da0ca756a2e6d",
"sha256:c0bec9477df1cb867e5a67c9e1ab758de9cb4a3e52dd70681f59fa40a62b3f2d"
],
"index": "pypi",
"version": "==2.2.3"
},
"flask-bootstrap": {
"hashes": [
"sha256:cb08ed940183f6343a64e465e83b3a3f13c53e1baabb8d72b5da4545ef123ac8"
],
"index": "pypi",
"version": "==3.3.7.1"
},
"flask-login": {
"hashes": [
"sha256:1ef79843f5eddd0f143c2cd994c1b05ac83c0401dc6234c143495af9a939613f",
"sha256:c0a7baa9fdc448cdd3dd6f0939df72eec5177b2f7abe6cb82fc934d29caac9c3"
],
"index": "pypi",
"version": "==0.6.2"
},
"flask-mail": {
"hashes": [
"sha256:22e5eb9a940bf407bcf30410ecc3708f3c56cc44b29c34e1726fe85006935f41"
],
"index": "pypi",
"version": "==0.9.1"
},
"flask-migrate": {
"hashes": [
"sha256:73293d40b10ac17736e715b377e7b7bde474cb8105165d77474df4c3619b10b3",
"sha256:77580f27ab39bc68be4906a43c56d7674b45075bc4f883b1d0b985db5164d58f"
],
"index": "pypi",
"version": "==4.0.4"
},
"flask-sqlalchemy": {
"hashes": [
"sha256:2764335f3c9d7ebdc9ed6044afaf98aae9fa50d7a074cef55dde307ec95903ec",
"sha256:add5750b2f9cd10512995261ee2aa23fab85bd5626061aa3c564b33bb4aa780a"
],
"index": "pypi",
"version": "==3.0.3"
},
"flask-wtf": {
"hashes": [
"sha256:41c4244e9ae626d63bed42ae4785b90667b885b1535d5a4095e1f63060d12aa9",
"sha256:7887d6f1ebb3e17bf648647422f0944c9a469d0fcf63e3b66fb9a83037e38b2c"
],
"index": "pypi",
"version": "==1.1.1"
},
"greenlet": {
"hashes": [
"sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a",
"sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a",
"sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43",
"sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33",
"sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8",
"sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088",
"sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca",
"sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343",
"sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645",
"sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db",
"sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df",
"sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3",
"sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86",
"sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2",
"sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a",
"sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf",
"sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7",
"sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394",
"sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40",
"sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3",
"sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6",
"sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74",
"sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0",
"sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3",
"sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91",
"sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5",
"sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9",
"sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8",
"sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b",
"sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6",
"sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb",
"sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73",
"sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b",
"sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df",
"sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9",
"sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f",
"sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0",
"sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857",
"sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a",
"sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249",
"sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30",
"sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292",
"sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b",
"sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d",
"sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b",
"sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c",
"sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca",
"sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7",
"sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75",
"sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae",
"sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b",
"sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470",
"sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564",
"sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9",
"sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099",
"sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0",
"sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5",
"sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19",
"sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1",
"sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"
],
"markers": "platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))",
"version": "==2.0.2"
},
"idna": {
"hashes": [
"sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
"sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
],
"markers": "python_version >= '3.5'",
"version": "==3.4"
},
"itsdangerous": {
"hashes": [
"sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44",
"sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"
],
"markers": "python_version >= '3.7'",
"version": "==2.1.2"
},
"jinja2": {
"hashes": [
"sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
"sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
],
"markers": "python_version >= '3.7'",
"version": "==3.1.2"
},
"mako": {
"hashes": [
"sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818",
"sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"
],
"markers": "python_version >= '3.7'",
"version": "==1.2.4"
},
"markupsafe": {
"hashes": [
"sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed",
"sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc",
"sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2",
"sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460",
"sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7",
"sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0",
"sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1",
"sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa",
"sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03",
"sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323",
"sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65",
"sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013",
"sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036",
"sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f",
"sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4",
"sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419",
"sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2",
"sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619",
"sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a",
"sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a",
"sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd",
"sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7",
"sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666",
"sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65",
"sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859",
"sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625",
"sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff",
"sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156",
"sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd",
"sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba",
"sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f",
"sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1",
"sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094",
"sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a",
"sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513",
"sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed",
"sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d",
"sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3",
"sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147",
"sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c",
"sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603",
"sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601",
"sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a",
"sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1",
"sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d",
"sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3",
"sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54",
"sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2",
"sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6",
"sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"
],
"markers": "python_version >= '3.7'",
"version": "==2.1.2"
},
"psycopg2-binary": {
"hashes": [
"sha256:00475004e5ed3e3bf5e056d66e5dcdf41a0dc62efcd57997acd9135c40a08a50",
"sha256:01ad49d68dd8c5362e4bfb4158f2896dc6e0c02e87b8a3770fc003459f1a4425",
"sha256:024030b13bdcbd53d8a93891a2cf07719715724fc9fee40243f3bd78b4264b8f",
"sha256:02551647542f2bf89073d129c73c05a25c372fc0a49aa50e0de65c3c143d8bd0",
"sha256:043a9fd45a03858ff72364b4b75090679bd875ee44df9c0613dc862ca6b98460",
"sha256:05b3d479425e047c848b9782cd7aac9c6727ce23181eb9647baf64ffdfc3da41",
"sha256:0775d6252ccb22b15da3b5d7adbbf8cfe284916b14b6dc0ff503a23edb01ee85",
"sha256:1764546ffeaed4f9428707be61d68972eb5ede81239b46a45843e0071104d0dd",
"sha256:1e491e6489a6cb1d079df8eaa15957c277fdedb102b6a68cfbf40c4994412fd0",
"sha256:212757ffcecb3e1a5338d4e6761bf9c04f750e7d027117e74aa3cd8a75bb6fbd",
"sha256:215d6bf7e66732a514f47614f828d8c0aaac9a648c46a831955cb103473c7147",
"sha256:25382c7d174c679ce6927c16b6fbb68b10e56ee44b1acb40671e02d29f2fce7c",
"sha256:2abccab84d057723d2ca8f99ff7b619285d40da6814d50366f61f0fc385c3903",
"sha256:2d964eb24c8b021623df1c93c626671420c6efadbdb8655cb2bd5e0c6fa422ba",
"sha256:2ec46ed947801652c9643e0b1dc334cfb2781232e375ba97312c2fc256597632",
"sha256:2ef892cabdccefe577088a79580301f09f2a713eb239f4f9f62b2b29cafb0577",
"sha256:33e632d0885b95a8b97165899006c40e9ecdc634a529dca7b991eb7de4ece41c",
"sha256:3520d7af1ebc838cc6084a3281145d5cd5bdd43fdef139e6db5af01b92596cb7",
"sha256:3d790f84201c3698d1bfb404c917f36e40531577a6dda02e45ba29b64d539867",
"sha256:3fc33295cfccad697a97a76dec3f1e94ad848b7b163c3228c1636977966b51e2",
"sha256:422e3d43b47ac20141bc84b3d342eead8d8099a62881a501e97d15f6addabfe9",
"sha256:426c2ae999135d64e6a18849a7d1ad0e1bd007277e4a8f4752eaa40a96b550ff",
"sha256:46512486be6fbceef51d7660dec017394ba3e170299d1dc30928cbedebbf103a",
"sha256:46850a640df62ae940e34a163f72e26aca1f88e2da79148e1862faaac985c302",
"sha256:484405b883630f3e74ed32041a87456c5e0e63a8e3429aa93e8714c366d62bd1",
"sha256:4e7904d1920c0c89105c0517dc7e3f5c20fb4e56ba9cdef13048db76947f1d79",
"sha256:56b2957a145f816726b109ee3d4e6822c23f919a7d91af5a94593723ed667835",
"sha256:5c6527c8efa5226a9e787507652dd5ba97b62d29b53c371a85cd13f957fe4d42",
"sha256:5cbc554ba47ecca8cd3396ddaca85e1ecfe3e48dd57dc5e415e59551affe568e",
"sha256:5d28ecdf191db558d0c07d0f16524ee9d67896edf2b7990eea800abeb23ebd61",
"sha256:5fc447058d083b8c6ac076fc26b446d44f0145308465d745fba93a28c14c9e32",
"sha256:63e318dbe52709ed10d516a356f22a635e07a2e34c68145484ed96a19b0c4c68",
"sha256:68d81a2fe184030aa0c5c11e518292e15d342a667184d91e30644c9d533e53e1",
"sha256:6e63814ec71db9bdb42905c925639f319c80e7909fb76c3b84edc79dadef8d60",
"sha256:6f8a9bcab7b6db2e3dbf65b214dfc795b4c6b3bb3af922901b6a67f7cb47d5f8",
"sha256:70831e03bd53702c941da1a1ad36c17d825a24fbb26857b40913d58df82ec18b",
"sha256:74eddec4537ab1f701a1647214734bc52cee2794df748f6ae5908e00771f180a",
"sha256:7b3751857da3e224f5629400736a7b11e940b5da5f95fa631d86219a1beaafec",
"sha256:7cf1d44e710ca3a9ce952bda2855830fe9f9017ed6259e01fcd71ea6287565f5",
"sha256:7d07f552d1e412f4b4e64ce386d4c777a41da3b33f7098b6219012ba534fb2c2",
"sha256:7d88db096fa19d94f433420eaaf9f3c45382da2dd014b93e4bf3215639047c16",
"sha256:7ee3095d02d6f38bd7d9a5358fcc9ea78fcdb7176921528dd709cc63f40184f5",
"sha256:902844f9c4fb19b17dfa84d9e2ca053d4a4ba265723d62ea5c9c26b38e0aa1e6",
"sha256:937880290775033a743f4836aa253087b85e62784b63fd099ee725d567a48aa1",
"sha256:95076399ec3b27a8f7fa1cc9a83417b1c920d55cf7a97f718a94efbb96c7f503",
"sha256:9c38d3869238e9d3409239bc05bc27d6b7c99c2a460ea337d2814b35fb4fea1b",
"sha256:9e32cedc389bcb76d9f24ea8a012b3cb8385ee362ea437e1d012ffaed106c17d",
"sha256:9ffdc51001136b699f9563b1c74cc1f8c07f66ef7219beb6417a4c8aaa896c28",
"sha256:a0adef094c49f242122bb145c3c8af442070dc0e4312db17e49058c1702606d4",
"sha256:a36a0e791805aa136e9cbd0ffa040d09adec8610453ee8a753f23481a0057af5",
"sha256:a7e518a0911c50f60313cb9e74a169a65b5d293770db4770ebf004245f24b5c5",
"sha256:af0516e1711995cb08dc19bbd05bec7dbdebf4185f68870595156718d237df3e",
"sha256:b8104f709590fff72af801e916817560dbe1698028cd0afe5a52d75ceb1fce5f",
"sha256:b911dfb727e247340d36ae20c4b9259e4a64013ab9888ccb3cbba69b77fd9636",
"sha256:b9a794cef1d9c1772b94a72eec6da144c18e18041d294a9ab47669bc77a80c1d",
"sha256:b9c33d4aef08dfecbd1736ceab8b7b3c4358bf10a0121483e5cd60d3d308cc64",
"sha256:b9d38a4656e4e715d637abdf7296e98d6267df0cc0a8e9a016f8ba07e4aa3eeb",
"sha256:bcda1c84a1c533c528356da5490d464a139b6e84eb77cc0b432e38c5c6dd7882",
"sha256:bef7e3f9dc6f0c13afdd671008534be5744e0e682fb851584c8c3a025ec09720",
"sha256:c15ba5982c177bc4b23a7940c7e4394197e2d6a424a2d282e7c236b66da6d896",
"sha256:c5254cbd4f4855e11cebf678c1a848a3042d455a22a4ce61349c36aafd4c2267",
"sha256:c5682a45df7d9642eff590abc73157c887a68f016df0a8ad722dcc0f888f56d7",
"sha256:c5e65c6ac0ae4bf5bef1667029f81010b6017795dcb817ba5c7b8a8d61fab76f",
"sha256:d4c7b3a31502184e856df1f7bbb2c3735a05a8ce0ade34c5277e1577738a5c91",
"sha256:d892bfa1d023c3781a3cab8dd5af76b626c483484d782e8bd047c180db590e4c",
"sha256:dbc332beaf8492b5731229a881807cd7b91b50dbbbaf7fe2faf46942eda64a24",
"sha256:dc85b3777068ed30aff8242be2813038a929f2084f69e43ef869daddae50f6ee",
"sha256:e59137cdb970249ae60be2a49774c6dfb015bd0403f05af1fe61862e9626642d",
"sha256:e67b3c26e9b6d37b370c83aa790bbc121775c57bfb096c2e77eacca25fd0233b",
"sha256:e72c91bda9880f097c8aa3601a2c0de6c708763ba8128006151f496ca9065935",
"sha256:f95b8aca2703d6a30249f83f4fe6a9abf2e627aa892a5caaab2267d56be7ab69"
],
"index": "pypi",
"version": "==2.9.5"
},
"pyjwt": {
"hashes": [
"sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd",
"sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14"
],
"index": "pypi",
"version": "==2.6.0"
},
"pymupdf": {
"hashes": [
"sha256:06860a8e60ba14f25aac5db90803d59b1a2fdac24c2b65dc6f9876ff36df586e",
"sha256:069ba56c28cb9b603d016c63f23a21bf22a00c5b2a92286bf3dd6a2759e119d4",
"sha256:0b08b6afbb656f0e4582a8ee44c2ce0ab6235bb81830ec92d79f6e4893db639c",
"sha256:0c85efbc08c83c8d91ae1cc7bb7e06c7cd7e203e9d0ed806ff22db173999f30c",
"sha256:18fbdcb35c144b26a3a507bfa73f98401a27819f29dca496eb470b9a1ec09fad",
"sha256:19fe49966f3a072a258cdfbab3d8c5b11cb7c2fb7c8e6abf3398d9e55af6f1ca",
"sha256:1fc4c8aee186326b3f138be8a4ac16a343e76c8ec45b5afab2d105a5e3b02d80",
"sha256:2a870f02b2767e9371bbb3d0263c3994f59679a64b1b9dd1a6b0d9a15c963d68",
"sha256:2bb5f9ce33054ebfbf63c41e68606d51e3ad5d85def0af5ebb6f0dd276e61037",
"sha256:2fbb96336c5cc065f6e5359f034a689e4d64ca7697dc5965b03f105b2ecb7ba1",
"sha256:36181c4cb27740791d611d0224dd18ac78e23a1f06aa2151394c5b9194b4f885",
"sha256:7e5a1ed49df5eee7834fb37adb5c94354e98b23fa997e67034b157790a31bbfc",
"sha256:909afad284c24f25a831b37e433e444dc4cb5b3e38b3f761b1e4acad5c65327d",
"sha256:ac2f61632bcd4c9532a26b52684ca05a4c8b7b96d6be947134b3e01a420904fd",
"sha256:b2b197bb0f00159f4584f51c7bf4d897aa5fb88de3c1f964347047a710c1f1be",
"sha256:bf204723782ca481cd120e1ab124b0d59fd387103e15675ae462415bfa22b3a2",
"sha256:c040c9ac98b7a1afc06fe55ffedbf6a305c24bf1c97597838f208c68035971f4",
"sha256:c09cbc7924ddf99452bb0d70438d64753815d75526d6d94293249f1665691d84",
"sha256:d85c360abd72c14e302a2cefd02ebd01568f5c669700c7fc1ed056e9cdf3c285",
"sha256:dab0459791175dea813e6130e08b9026d3b9d66854f0dbb7ab51ab1619875d24",
"sha256:dddd3fe6fa20b3e2642c10e66c97cfd3727010d3dafe653f16dce52396ef0208",
"sha256:e334e7baea701d4029faa034520c722c9c55e8315b7ccf4d1b686eba8e293f32",
"sha256:e5273ff0c3bf08428ef956c4a5e5e0b475667cc3e1cb7b41d9f5131636996e59",
"sha256:eb29cbfd34d5ef99c657539c4f48953ea15f4b1e4e162b48efcffa5c4101ce1d",
"sha256:f3ffa9a8c643da39aba80e787d2c1771d5be07b0e15bf193ddd1fda888bbca88",
"sha256:f815741a435c62a0036bbcbf5fa6c533567bd69c5338d413714fc57b22db93e0",
"sha256:f99e6c2109c38c17cd5f1b82c9d35f1e815a71a3464b765f41f3c8cf875dcf4c",
"sha256:fa0334247549def667d9f5358e558ec93815c78cebdb927a81dda38d681bf550",
"sha256:fdba6bf77d1d3cd57ea93181c494f4b138523000ff74b25c523be7dce0058ac9",
"sha256:ff7d01cb563c3ca18880ba6a2661351f07b8a54f6599272be2e3568524e5e721"
],
"index": "pypi",
"version": "==1.21.1"
},
"python-dotenv": {
"hashes": [
"sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba",
"sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"
],
"index": "pypi",
"version": "==1.0.0"
},
"sqlalchemy": {
"hashes": [
"sha256:011ef3c33f30bae5637c575f30647e0add98686642d237f0c3a1e3d9b35747fa",
"sha256:0adca8a3ca77234a142c5afed29322fb501921f13d1d5e9fa4253450d786c160",
"sha256:1644c603558590f465b3fa16e4557d87d3962bc2c81fd7ea85b582ecf4676b31",
"sha256:2267c004e78e291bba0dc766a9711c389649cf3e662cd46eec2bc2c238c637bd",
"sha256:25e4e54575f9d2af1eab82d3a470fca27062191c48ee57b6386fe09a3c0a6a33",
"sha256:2a2f9120eb32190bdba31d1022181ef08f257aed4f984f3368aa4e838de72bc0",
"sha256:2c82395e2925639e6d320592943608070678e7157bd1db2672a63be9c7889434",
"sha256:3f927340b37fe65ec42e19af7ce15260a73e11c6b456febb59009bfdfec29a35",
"sha256:54aa9f40d88728dd058e951eeb5ecc55241831ba4011e60c641738c1da0146b7",
"sha256:57dcd9eed52413f7270b22797aa83c71b698db153d1541c1e83d45ecdf8e95e7",
"sha256:582053571125895d008d4b8d9687d12d4bd209c076cdbab3504da307e2a0a2bd",
"sha256:59cf0cdb29baec4e074c7520d7226646a8a8f856b87d8300f3e4494901d55235",
"sha256:6363697c938b9a13e07f1bc2cd433502a7aa07efd55b946b31d25b9449890621",
"sha256:662a79e80f3e9fe33b7861c19fedf3d8389fab2413c04bba787e3f1139c22188",
"sha256:67901b91bf5821482fcbe9da988cb16897809624ddf0fde339cd62365cc50032",
"sha256:679b9bd10bb32b8d3befed4aad4356799b6ec1bdddc0f930a79e41ba5b084124",
"sha256:738c80705e11c1268827dbe22c01162a9cdc98fc6f7901b429a1459db2593060",
"sha256:77a380bf8721b416782c763e0ff66f80f3b05aee83db33ddfc0eac20bcb6791f",
"sha256:77d05773d5c79f2d3371d81697d54ee1b2c32085ad434ce9de4482e457ecb018",
"sha256:817aab80f7e8fe581696dae7aaeb2ceb0b7ea70ad03c95483c9115970d2a9b00",
"sha256:81f1ea264278fcbe113b9a5840f13a356cb0186e55b52168334124f1cd1bc495",
"sha256:8a88b32ce5b69d18507ffc9f10401833934ebc353c7b30d1e056023c64f0a736",
"sha256:8ff0a7c669ec7cdb899eae7e622211c2dd8725b82655db2b41740d39e3cda466",
"sha256:918c2b553e3c78268b187f70983c9bc6f91e451a4f934827e9c919e03d258bd7",
"sha256:954f1ad73b78ea5ba5a35c89c4a5dfd0f3a06c17926503de19510eb9b3857bde",
"sha256:95a18e1a6af2114dbd9ee4f168ad33070d6317e11bafa28d983cc7b585fe900b",
"sha256:9946ee503962859f1a9e1ad17dff0859269b0cb453686747fe87f00b0e030b34",
"sha256:9a7ecaf90fe9ec8e45c86828f4f183564b33c9514e08667ca59e526fea63893a",
"sha256:a42e6831e82dfa6d16b45f0c98c69e7b0defc64d76213173456355034450c414",
"sha256:b01dce097cf6f145da131a53d4cce7f42e0bfa9ae161dd171a423f7970d296d0",
"sha256:b5deafb4901618b3f98e8df7099cd11edd0d1e6856912647e28968b803de0dae",
"sha256:b67d6e626caa571fb53accaac2fba003ef4f7317cb3481e9ab99dad6e89a70d6",
"sha256:c1e8edc49b32483cd5d2d015f343e16be7dfab89f4aaf66b0fa6827ab356880d",
"sha256:c621f05859caed5c0aab032888a3d3bde2cae3988ca151113cbecf262adad976",
"sha256:ce54965a94673a0ebda25e7c3a05bf1aa74fd78cc452a1a710b704bf73fb8402",
"sha256:d8efdda920988bcade542f53a2890751ff680474d548f32df919a35a21404e3f",
"sha256:dc7b9f55c2f72c13b2328b8a870ff585c993ba1b5c155ece5c9d3216fa4b18f6",
"sha256:dd801375f19a6e1f021dabd8b1714f2fdb91cbc835cd13b5dd0bd7e9860392d7",
"sha256:f342057422d6bcfdd4996e34cd5c7f78f7e500112f64b113f334cdfc6a0c593d",
"sha256:f696828784ab2c07b127bfd2f2d513f47ec58924c29cff5b19806ac37acee31c",
"sha256:fdb2686eb01f670cdc6c43f092e333ff08c1cf0b646da5256c1237dc4ceef4ae"
],
"markers": "python_version >= '3.7'",
"version": "==2.0.4"
},
"sqlalchemy-utils": {
"hashes": [
"sha256:4c7098d4857d5cad1248bf7cd940727aecb75b596a5574b86a93b37079929520",
"sha256:af803089a7929803faeb6173b90f29d1a67ad02f1d1e732f40b054a8eb3c7370"
],
"index": "pypi",
"version": "==0.40.0"
},
"typing-extensions": {
"hashes": [
"sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb",
"sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"
],
"markers": "python_version >= '3.7'",
"version": "==4.5.0"
},
"visitor": {
"hashes": [
"sha256:2c737903b2b6864ebc6167eef7cf3b997126f1aa94bdf590f90f1436d23e480a"
],
"version": "==0.1.3"
},
"werkzeug": {
"hashes": [
"sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe",
"sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612"
],
"markers": "python_version >= '3.7'",
"version": "==2.2.3"
},
"wtforms": {
"hashes": [
"sha256:6b351bbb12dd58af57ffef05bc78425d08d1914e0fd68ee14143b7ade023c5bc",
"sha256:837f2f0e0ca79481b92884962b914eba4e72b7a2daaf1f939c890ed0124b834b"
],
"markers": "python_version >= '3.7'",
"version": "==3.0.1"
}
},
"develop": {}
}

0
backend/README.md Normal file
View File

View File

@ -6,12 +6,12 @@ load_dotenv(os.path.join(basedir, '.env'))
class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or "MY_5€cr37_K€Y"
SECRET_KEY = os.environ.get('SECRET_KEY') or "s0m37h!n6-obfu5c471ng"
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', '').replace(
'postgres://', 'postgresql://') or \
(f"postgresql://{os.environ.get('DATABASE_USER', 'scan2kasse')}:{os.environ.get('DATABASE_PASS', 'asdf1337')}"
f"@{os.environ.get('DATABASE_HOST', 'localhost')}:{os.environ.get('DATABASE_PORT', '5432')}"
f"/{os.environ.get('DATABASE_DB', '') or os.environ.get('DATABASE_USER', 'scan2kasse')}")
f"@{os.environ.get('DATABASE_HOST', 'localhost')}:{os.environ.get('DATABASE_PORT', '5432')}"
f"/{os.environ.get('DATABASE_DB', '') or os.environ.get('DATABASE_USER', 'scan2kasse')}")
SQLALCHEMY_TRACK_MODIFICATIONS = False
MAIL_SERVER = os.environ.get('MAIL_SERVER')
MAIL_PORT = int(os.environ.get('MAIL_PORT'))
@ -19,3 +19,4 @@ class Config(object):
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
ADMINS = ['postmaster@wpgcommunity.net']
POSTS_PER_PAGE = 10

View File

@ -2,19 +2,25 @@
keys=root, main
[handlers]
keys=console, file
keys=console, file, void
[formatters]
keys=stdout
[logger_root]
level = DEBUG
handlers = void
level = CRITICAL
[logger_main]
handlers = console, file
level = DEBUG
qualname = main
[handler_void]
class = logging.StreamHandler
level = CRITICAL
formatter = stdout
[handler_console]
class = logging.StreamHandler
level = DEBUG

View File

@ -8,8 +8,8 @@ Create Date: 2022-02-20 16:33:01.900378
from alembic import op
import sqlalchemy as sa
from app import db
from app.utils.view_utils import selectable_price_per_amount_view, selectable_bought_with_prices_view
from src import db
from src.utils.view_utils import selectable_price_per_amount_view, selectable_bought_with_prices_view
from sqlalchemy_utils import create_view
# revision identifiers, used by Alembic.
@ -20,6 +20,7 @@ depends_on = None
def upgrade():
op.rename_table("item_receipt", "receipt_item")
metadata = sa.MetaData()
create_view('price_per_amount',
selectable_price_per_amount_view(),
@ -36,3 +37,4 @@ def downgrade():
with db.engine.connect() as con:
con.execute("DROP VIEW bought_with_prices;")
con.execute("DROP VIEW price_per_amount;")
op.rename_table("receipt_item", "item_receipt")

View File

@ -0,0 +1,53 @@
"""establishment candidate table
Revision ID: 610854a6e2a0
Revises: 2be4d1ae5493
Create Date: 2023-03-18 13:36:12.953900
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '610854a6e2a0'
down_revision = '2be4d1ae5493'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('establishment_candidate',
sa.Column('user', sa.BigInteger(), nullable=False),
sa.Column('establishment', sa.BigInteger(), nullable=False),
sa.ForeignKeyConstraint(['establishment'], ['establishment.id'], ),
sa.ForeignKeyConstraint(['user'], ['user.id'], ),
sa.PrimaryKeyConstraint('user', 'establishment')
)
op.create_table('payment',
sa.Column('id', sa.BigInteger(), nullable=False),
sa.Column('token', sa.String(length=15), nullable=False),
sa.Column('date', sa.Date(), server_default=sa.text('now()'), nullable=False),
sa.Column('amount', sa.BigInteger(), server_default='0', nullable=False),
sa.ForeignKeyConstraint(['token'], ['login_token.token'], ),
sa.PrimaryKeyConstraint('id', 'token')
)
with op.batch_alter_table('login_token', schema=None) as batch_op:
batch_op.alter_column('token',
existing_type=sa.VARCHAR(length=15),
nullable=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('login_token', schema=None) as batch_op:
batch_op.alter_column('token',
existing_type=sa.VARCHAR(length=15),
nullable=True)
op.drop_table('payment')
op.drop_table('establishment_candidate')
# ### end Alembic commands ###

View File

@ -1,5 +1,5 @@
from app import create_app, db
from app.models import *
from src import create_app, db
from src.models import Bought, Establishment, Item, LoginToken, Receipt, User
app = create_app()

View File

@ -42,15 +42,21 @@ def create_app(config_class=Config):
mail.init_app(app)
migrate.init_app(app, db, render_as_batch=True)
from app.auth import bp as auth_bp
app.register_blueprint(auth_bp, url_prefix='/auth')
from app.errors import bp as errors_bp
app.register_blueprint(errors_bp)
from app.main import bp as main_bp
from src.main import bp as main_bp
app.register_blueprint(main_bp)
from app.api import bp as api_bp
app.register_blueprint(api_bp, url_prefix="/api")
from src.auth import bp as auth_bp
app.register_blueprint(auth_bp)
from src.errors import bp as error_bp
app.register_blueprint(error_bp)
from src.api import bp as api_bp
app.register_blueprint(api_bp)
from src.receipts import bp as receipts_bp
app.register_blueprint(receipts_bp)
from src.item import bp as item_bp
app.register_blueprint(item_bp)
from src.establishment import bp as establishment_bp
app.register_blueprint(establishment_bp)
return app
from app import models
from src.models import *

View File

@ -0,0 +1,6 @@
from flask import Blueprint
bp = Blueprint('api', __name__, url_prefix='/api')
from src.api.v1 import bp as bp_v1
bp.register_blueprint(bp_v1)

View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('v1', __name__, url_prefix='/v1')
from src.api.v1 import routes

View File

@ -1,11 +1,11 @@
from app import LOGGER
from app.main import bp
from app.models import LoginToken
from app.utils import database_utils
from src import LOGGER
from src.api import bp
from src.models.login_token import LoginToken
from src.utils import database_utils
from flask import abort, request
from flask.json import jsonify
@bp.route('/auth')
@bp.route('/')
def token_authorization():
LOGGER.debug("Token Login")
if not request.json or 'login' not in request.json:

View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('auth', __name__, url_prefix='/auth')
from src.auth import forms, routes

View File

@ -1,6 +1,6 @@
from flask import current_app
from app.utils.email import send_email
from app.utils.routes_utils import render_custom_template as render_template
from src.utils.email import send_email
from src.utils.routes_utils import render_custom_template as render_template
def send_password_reset_email(user):

View File

@ -1,6 +1,6 @@
from app.models import User
from src.models.user import User
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms import BooleanField, PasswordField, StringField, SubmitField
from wtforms.validators import DataRequired, Email, EqualTo, ValidationError
class LoginForm(FlaskForm):

View File

@ -1,14 +1,14 @@
from app import db
from app.auth import bp
from app.auth.email import send_password_reset_email
from app.auth.forms import LoginForm, RegistrationForm, ResetPasswordForm, ResetPasswordRequestForm
from app.models import User
from app.utils.routes_utils import render_custom_template as render_template
from src import db
from src.auth import bp
from src.auth.email import send_password_reset_email
from src.auth.forms import LoginForm, RegistrationForm, ResetPasswordForm, ResetPasswordRequestForm
from src.models.user import User
from src.utils.routes_utils import render_custom_template as render_template
from flask import flash, redirect, request, url_for
from flask_login import current_user, login_user, logout_user
from werkzeug.urls import url_parse
@bp.route(f'/register', methods=['GET', 'POST'])
@bp.route('/register', methods=['GET', 'POST'])
def web_register():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
@ -22,7 +22,7 @@ def web_register():
return redirect(url_for('auth.web_login'))
return render_template('auth/register.html', title='Register', form=form)
@bp.route(f'/login', methods=['GET', 'POST'])
@bp.route('/login', methods=['GET', 'POST'])
def web_login():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
@ -39,7 +39,7 @@ def web_login():
return redirect(next_page)
return render_template('auth/login.html', title='Sign In', form=form)
@bp.route(f'/logout')
@bp.route('/logout')
def web_logout():
logout_user()
return redirect(url_for('main.index'))

View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('error', __name__, url_prefix='/error')
from src.errors import handlers

View File

@ -1,7 +1,11 @@
from app import db
from app.errors import bp
from src import db
from src.errors import bp
from flask import render_template
@bp.app_errorhandler(403)
def not_allowed_error(error):
return render_template('errors/403.html'), 403
@bp.app_errorhandler(404)
def not_found_error(error):
return render_template('errors/404.html'), 404

View File

@ -0,0 +1,11 @@
from flask import Blueprint
bp = Blueprint('establishment', __name__, url_prefix='/establishment')
from src.establishment.new import bp as bp_new
bp.register_blueprint(bp_new)
from src.establishment.list import bp as bp_list
bp.register_blueprint(bp_list)
from src.establishment.overview import bp as bp_overview
bp.register_blueprint(bp_overview)
from src.establishment.candidates import bp as bp_candidates
bp.register_blueprint(bp_candidates)

View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint("candidates", __name__, url_prefix='candidates')
from src.establishment.candidates import routes

View File

@ -0,0 +1,7 @@
from flask_wtf import FlaskForm
from wtforms import HiddenField, SubmitField
class EvaluateCandidateForm(FlaskForm):
candidate_id = HiddenField("X")
accept = SubmitField("Akzeptieren", render_kw={"class": "btn btn-success mt-3"})
deny = SubmitField("Ablehnen", render_kw={"class": "btn btn-danger mt-3"})

View File

@ -0,0 +1,26 @@
from flask import abort, redirect, url_for
from flask_login import current_user, login_required
from src import db, LOGGER
from src.establishment.candidates import bp
from src.establishment.candidates.forms import EvaluateCandidateForm
from src.models import Establishment, EstablishmentCandidate, LoginToken, User
from src.utils.routes_utils import render_custom_template as render_template
from src.utils.database_utils import generate_token
@bp.route("/<establishment_id>", methods=["GET", "POST"])
@login_required
def candidates(establishment_id):
establishment = Establishment.query.get_or_404(establishment_id)
if(current_user == establishment.User):
form = EvaluateCandidateForm()
establishment_candidates = EstablishmentCandidate.query.filter_by(establishment = establishment.id).all()
if(form.validate_on_submit()):
if(form.accept.data):
login_token = LoginToken(Establishment = establishment, User = User.query.get(form.candidate_id.data), token = generate_token())
db.session.add(login_token)
establishment_candidate = EstablishmentCandidate.query.filter_by(establishment = establishment.id, user = form.candidate_id.data).first()
db.session.delete(establishment_candidate)
db.session.commit()
return redirect(url_for('.candidates', establishment_id = establishment_id))
return render_template("establishment/candidates/candidates.html", establishment_candidates=establishment_candidates, form=form)
abort(403)

View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint("list", __name__, url_prefix='list')
from src.establishment.list import routes

View File

@ -0,0 +1,7 @@
from flask_wtf import FlaskForm
from wtforms import HiddenField, SubmitField
class JoinEstablishmentForm(FlaskForm):
id = HiddenField("X")
submit = SubmitField("Anfragen")

View File

@ -0,0 +1,33 @@
# from src import db, LOGGER
from flask import abort, current_app, redirect, request, url_for
from flask_login import current_user
from src import db
from src.models import Establishment
from src.establishment.list import bp
from src.establishment.list.forms import JoinEstablishmentForm
from src.establishment.list.utils import backend_validation
from src.models import EstablishmentCandidate
from src.utils.routes_utils import render_custom_template as render_template
@bp.route('/show_establishments', methods=["GET", "POST"])
def show_establishments():
page = request.args.get('page', 1, type=int)
establishments = Establishment.query.order_by(Establishment.id.asc()).paginate(
page=page, per_page=current_app.config['POSTS_PER_PAGE'], error_out=False)
next_url = url_for(".show_establishments",
page=establishments.next_num) if establishments.has_next else None
prev_url = url_for(".show_establishments",
page=establishments.prev_num) if establishments.has_prev else None
if current_user.is_authenticated:
form = JoinEstablishmentForm()
if (form.validate_on_submit()):
if (backend_validation(form)):
establishment_candidate = EstablishmentCandidate(
user=current_user.id, establishment=form.id.data)
db.session.add(establishment_candidate)
db.session.commit()
return redirect(url_for('establishment.list.show_establishments'))
return abort(403)
return render_template("establishment/list/establishment_list.html", establishments=establishments.items, form=form, next_url=next_url, prev_url=prev_url)
return render_template("establishment/list/establishment_list.html", establishments=establishments.items, next_url=next_url, prev_url=prev_url)

View File

@ -0,0 +1,7 @@
from flask_login import current_user
from src.models import Establishment
def backend_validation(join_establishment_form):
establishment = Establishment.query.get(join_establishment_form.id.data)
return not (establishment.LoginToken.filter_by(user=current_user.id).first() or current_user.EstablishmentCandidate.filter_by(establishment=join_establishment_form.id.data).first())

View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint("new", __name__, url_prefix='new')
from src.establishment.new import forms, routes

View File

@ -0,0 +1,7 @@
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
class NewEstablishmentForm(FlaskForm):
name = StringField("Name", validators=[DataRequired()])
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})

View File

@ -0,0 +1,24 @@
from src import db, LOGGER
from src.models import Establishment, LoginToken
from src.establishment.new import bp
from src.establishment.new.forms import NewEstablishmentForm
from src.utils import database_utils
from src.utils.routes_utils import render_custom_template as render_template
from flask import redirect, url_for
from flask_login import current_user, login_required
@bp.route('/create_establishment', methods=['GET', 'POST'])
@login_required
def create_new_establishment():
form = NewEstablishmentForm()
if form.validate_on_submit():
LOGGER.debug("valid form")
establishment = Establishment(name=form.name.data, owner=current_user.id)
db.session.add(establishment)
db.session.commit()
new_token = database_utils.generate_token()
login_token = LoginToken(user=current_user.id, establishment=establishment.id, token=new_token)
db.session.add(login_token)
db.session.commit()
return redirect(url_for('main.index'))
return render_template('establishment/new/new_establishment.html', form=form)

View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('overview', __name__, url_prefix='/overview')
from src.establishment.overview import routes

View File

@ -0,0 +1,11 @@
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
class NewCategoryForm(FlaskForm):
name = StringField("Name", validators=[DataRequired()])
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})
class NewBrandForm(FlaskForm):
name = StringField("Name", validators=[DataRequired()])
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})

View File

@ -0,0 +1,36 @@
from flask import abort, request
from flask.json import jsonify
from flask_login import current_user, login_required
from src import LOGGER
from src.establishment.overview import bp
from src.models import Establishment
from src.utils import view_utils, database_utils
from src.utils.routes_utils import render_custom_template as render_template
@bp.route('/<establishment_id>', methods=['GET'])
@login_required
def get_report_from_user(establishment_id):
establishment = Establishment.query.filter_by(id=int(establishment_id)).first_or_404()
if current_user.is_anonymous:
abort(403)
if 'month' in request.args:
try:
month = int(request.args['month'])
except Exception as e:
LOGGER.exception("")
abort(400)
else:
if (month > 12 or month < 1):
abort(400)
LOGGER.info("Getting results.")
results = database_utils.get_report(**request.args, **{"establishment": establishment_id})
LOGGER.debug(f"Results received.")
# LOGGER.debug(str(results))
if results:
result_list = view_utils.group_results(results)
else:
result_list = []
if request.content_type == "application/json":
return jsonify(result_list)
else:
return render_template("establishment/overview/overview.html", results=result_list, establishment=Establishment.query.get(int(establishment_id)))

View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('item', __name__, url_prefix='/item')
from src.item.new import bp as bp_new_item
bp.register_blueprint(bp_new_item)

View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('new', __name__, url_prefix='/new')
from src.item.new import forms, routes

View File

@ -1,18 +1,19 @@
from app.models import Brand, Category
from src.models.brand import Brand
from src.models.category import Category
from flask_wtf import FlaskForm
from wtforms import DateField, FloatField, IntegerField, SelectField, SelectMultipleField, StringField, SubmitField
from wtforms.validators import DataRequired, Optional
class NewItemForm(FlaskForm):
id = IntegerField("Product EAN", validators=[DataRequired()])
name = StringField("Name", validators=[DataRequired()])
description = StringField("Description", validators=[DataRequired()])
date = DateField("Insert Date", validators=[DataRequired()])
price_change = FloatField("Price", validators=[DataRequired()])
amount_change = IntegerField("Amount", validators=[Optional()])
category = SelectMultipleField("Categories", choices=[], validators=[Optional()])
brand = SelectField("Brand", choices=[], validators=[DataRequired()])
submit = SubmitField("Submit")
id = IntegerField("Product EAN", validators=[DataRequired()], render_kw={"class": "form-control"})
name = StringField("Name", validators=[DataRequired()], render_kw={"class": "form-control"})
description = StringField("Description", validators=[DataRequired()], render_kw={"class": "form-control"})
date = DateField("Insert Date", validators=[DataRequired()], render_kw={"class": "form-control"})
price_change = FloatField("Price", validators=[DataRequired()], render_kw={"class": "form-control"})
amount_change = IntegerField("Amount", validators=[Optional()], render_kw={"class": "form-control"})
category = SelectMultipleField("Categories", choices=[], validators=[Optional()], render_kw={"class": "form-control"})
brand = SelectField("Brand", choices=[], validators=[DataRequired()], render_kw={"class": "form-control"})
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})
@classmethod
def new(cls):
@ -20,18 +21,3 @@ class NewItemForm(FlaskForm):
form.category.choices = [(c.id, c.name) for c in Category.query.order_by("name").all()]
form.brand.choices = [(b.id, b.name) for b in Brand.query.order_by("name").all()]
return form
class NewCategoryForm(FlaskForm):
name = StringField("Name", validators=[DataRequired()])
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})
class NewBrandForm(FlaskForm):
name = StringField("Name", validators=[DataRequired()])
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})
class NewEstablishmentForm(FlaskForm):
establishment_name = StringField("Name", validators=[DataRequired()])
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})
class JoinEstablishmentForm(FlaskForm):
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})

View File

@ -0,0 +1,38 @@
from datetime import date
from flask import abort, redirect, url_for
from flask_login import current_user, login_required
from src import db, LOGGER
from src.models import AmountChange, Brand, Item, PriceChange
from src.item.new import bp
from src.item.new.forms import NewItemForm
from src.utils.routes_utils import render_custom_template as render_template
@bp.route('/new_item', methods=['GET', 'POST'])
@login_required
def new_item():
if current_user.is_anonymous:
abort(403)
form = NewItemForm.new()
if form.is_submitted():
LOGGER.debug("submitted")
if form.validate():
LOGGER.debug("valid")
else:
LOGGER.debug(form.errors)
if form.validate_on_submit():
LOGGER.debug("valid form")
brand = Brand.query.get(form.brand.data)
new_item = Item(id=form.id.data, name=form.name.data,
brand=brand.id, description=form.description.data)
# if form.category.data:
# category = Category.query.get(id = form.category.data)
# new_item.Category = category
new_item.PriceChange = [PriceChange(Item=new_item, date=date(
2021, 12, 1), price=form.price_change.data)]
if form.amount_change.data:
new_item.AmountChange = [AmountChange(Item=new_item, date=date(
2021, 12, 1), amount=form.amount_change.data)]
db.session.add(new_item)
db.session.commit()
return redirect(url_for('main.index'))
return render_template('item/new/new_item.html', form=form)

View File

@ -2,4 +2,4 @@ from flask import Blueprint
bp = Blueprint('main', __name__)
from app.main import forms, routes
from src.main import forms, routes

11
backend/src/main/forms.py Normal file
View File

@ -0,0 +1,11 @@
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
class NewCategoryForm(FlaskForm):
name = StringField("Name", validators=[DataRequired()])
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})
class NewBrandForm(FlaskForm):
name = StringField("Name", validators=[DataRequired()])
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})

View File

@ -0,0 +1,7 @@
from src.main import bp
from src.utils.routes_utils import render_custom_template as render_template
@bp.route('/')
@bp.route('/index')
def index():
return render_template("base.html")

View File

@ -0,0 +1,14 @@
from src.models.amount_change import AmountChange
from src.models.bought import Bought
from src.models.brand import Brand
from src.models.category import Category
from src.models.establishment import Establishment
from src.models.establishment_candidate import EstablishmentCandidate
from src.models.item_category import item_category
from src.models.item import Item
from src.models.login_token import LoginToken
from src.models.payment import Payment
from src.models.price_change import PriceChange
from src.models.receipt_item import ReceiptItem
from src.models.receipt import Receipt
from src.models.user import User

View File

@ -0,0 +1,10 @@
from src import db
from datetime import date
class AmountChange(db.Model):
item = db.Column(db.ForeignKey('item.id'), primary_key=True, server_onupdate=db.FetchedValue())
date = db.Column(db.Date, primary_key=True, server_default=str(date(2021, 12, 1)))
amount = db.Column(db.SmallInteger, nullable=False, server_default=str(1))
def __repr__(self) -> str:
return f"<Amount_Change {self.item} ({self.date})>"

View File

@ -0,0 +1,11 @@
from src import db
class Bought(db.Model):
token = db.Column(db.ForeignKey('login_token.token'), primary_key=True, server_onupdate=db.FetchedValue())
item = db.Column(db.ForeignKey('item.id'), primary_key=True, server_onupdate=db.FetchedValue())
date = db.Column(db.Date, primary_key=True)
amount = db.Column(db.SmallInteger, nullable=False)
registered = db.Column(db.Boolean, nullable=False, server_default=str(False))
def __repr__(self) -> str:
return f"<Bought Object>"

View File

@ -0,0 +1,10 @@
from src import db
class Brand(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(32), nullable=False)
Item = db.relationship("Item", backref='Brand', lazy='dynamic')
def __repr__(self) -> str:
return f"<Brand {self.id} ({self.name})>"

View File

@ -0,0 +1,11 @@
from src import db
from src.models.item_category import item_category
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(32), nullable=False, unique=True)
Item = db.relationship("Item", secondary=item_category, lazy="dynamic", back_populates="Category")
def __repr__(self) -> str:
return f"<Category {self.id} ({self.name})>"

View File

@ -0,0 +1,14 @@
from src import db
class Establishment(db.Model):
id = db.Column(db.BigInteger, primary_key=True)
name = db.Column(db.String(64), nullable=False)
owner = db.Column(db.ForeignKey('user.id'), nullable=False)
LoginToken = db.relationship("LoginToken", backref='Establishment', lazy='dynamic')
Bought = db.relationship("Bought", secondary="login_token",
lazy='dynamic', overlaps="Establishment,LoginToken,Bought")
EstablishmentCandidate = db.relationship("EstablishmentCandidate", backref='Establishment', lazy='dynamic')
def __repr__(self) -> str:
return f"<Establishment {self.id} ({self.name})>"

View File

@ -0,0 +1,8 @@
from src import db
class EstablishmentCandidate(db.Model):
user = db.Column(db.ForeignKey('user.id'), primary_key=True)
establishment = db.Column(db.ForeignKey('establishment.id'), primary_key=True)
def __repr__(self) -> str:
return f"<EstablishmentCandidate {self.user} ({self.establishment})>"

View File

@ -0,0 +1,17 @@
from src import db
from src.models.item_category import item_category
class Item(db.Model):
id = db.Column(db.BigInteger, primary_key=True, autoincrement=False)
name = db.Column(db.String(64), nullable=False)
brand = db.Column(db.ForeignKey('brand.id'), nullable=False, server_onupdate=db.FetchedValue())
description = db.Column(db.Text, nullable=False)
AmountChange = db.relationship("AmountChange", backref='Item', lazy='dynamic')
Bought = db.relationship("Bought", backref='Item', lazy='dynamic')
Category = db.relationship("Category", secondary=item_category, lazy="dynamic", back_populates="Item")
PriceChange = db.relationship("PriceChange", backref='Item', lazy='dynamic')
ReceiptItem = db.relationship("ReceiptItem", backref='Item', lazy='dynamic')
def __repr__(self) -> str:
return f"<Item {self.id} ({self.name})>"

View File

@ -0,0 +1,6 @@
from src import db
item_category = db.Table("item_category",
db.Column("item", db.ForeignKey("item.id"), primary_key=True, server_onupdate=db.FetchedValue()),
db.Column("category", db.ForeignKey("category.id"), primary_key=True, server_onupdate=db.FetchedValue())
)

View File

@ -0,0 +1,12 @@
from src import db
class LoginToken(db.Model):
user = db.Column(db.ForeignKey('user.id'), primary_key=True, server_onupdate=db.FetchedValue())
establishment = db.Column(db.ForeignKey('establishment.id'), primary_key=True, server_onupdate=db.FetchedValue())
token = db.Column(db.String(15), nullable=False, unique=True)
Payment = db.relationship("Payment", backref='LoginToken', lazy='dynamic')
Receipt = db.relationship("Receipt", backref='LoginToken', lazy='dynamic')
def __repr__(self) -> str:
return f"<LoginToken {self.token}>"

View File

@ -0,0 +1,10 @@
from src import db
class Payment(db.Model):
id = db.Column(db.BigInteger, primary_key=True)
token = db.Column(db.ForeignKey('login_token.token'), primary_key=True, server_onupdate=db.FetchedValue())
date = db.Column(db.Date, nullable=False, server_default=db.func.now())
amount = db.Column(db.BigInteger, nullable=False, server_default=str(0))
def __repr__(self) -> str:
return f"<Payment {self.id}>"

View File

@ -0,0 +1,10 @@
from src import db
from datetime import date
class PriceChange(db.Model):
item = db.Column(db.ForeignKey('item.id'), primary_key=True, server_onupdate=db.FetchedValue())
date = db.Column(db.Date, primary_key=True, server_default=str(date(2021, 12, 1)))
price = db.Column(db.SmallInteger, nullable=False)
def __repr__(self) -> str:
return f"<Price_Change {self.item} ({self.date})>"

View File

@ -0,0 +1,12 @@
from src import db
class Receipt(db.Model):
id = db.Column(db.Numeric(precision=24, scale=0), primary_key=True, autoincrement=False)
date = db.Column(db.Date, nullable=False)
from_user = db.Column(db.ForeignKey("login_token.token"), server_onupdate=db.FetchedValue())
registered = db.Column(db.Boolean, nullable=False, server_default=str(False))
ReceiptItem = db.relationship("ReceiptItem", backref='Receipt', lazy='dynamic')
def __repr__(self) -> str:
return f"<Receipt {self.id}>"

View File

@ -0,0 +1,9 @@
from src import db
class ReceiptItem(db.Model):
receipt = db.Column(db.ForeignKey("receipt.id"), primary_key=True, server_onupdate=db.FetchedValue())
item = db.Column(db.ForeignKey("item.id"), primary_key=True, server_onupdate=db.FetchedValue())
amount = db.Column(db.SmallInteger, nullable=False)
def __repr__(self) -> str:
return f"<ReceiptItem {self.receipt}: {self.item}>"

View File

@ -0,0 +1,45 @@
import jwt
from src import db, login
from flask import current_app
from flask_login import UserMixin
from time import time
from werkzeug.security import generate_password_hash, check_password_hash
class User(UserMixin, db.Model):
id = db.Column(db.BigInteger, primary_key=True)
email = db.Column(db.String(255), nullable=False, unique=True)
password_hash = db.Column(db.String(128), nullable=False)
LoginToken = db.relationship("LoginToken", backref='User', lazy='dynamic')
Bought = db.relationship("Bought", secondary="login_token",
lazy='dynamic', overlaps="User,LoginToken")
EstablishmentCandidate = db.relationship("EstablishmentCandidate", backref='User', lazy='dynamic')
Establishment = db.relationship("Establishment", backref="User", lazy="dynamic")
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def get_reset_password_token(self, expires_in=600):
return jwt.encode(
{'reset_password': self.id, 'exp': time() + expires_in},
current_app.config['SECRET_KEY'], algorithm='HS256')
@staticmethod
def verify_reset_password_token(token):
try:
id = jwt.decode(token, current_app.config['SECRET_KEY'],
algorithms=['HS256'])['reset_password']
except:
return
return User.query.get(id)
def __repr__(self) -> str:
return f"<User {self.id} ({self.email})>"
@login.user_loader
def load_user(id):
return User.query.get(int(id))

View File

@ -0,0 +1,5 @@
from flask import Blueprint
bp = Blueprint('receipts', __name__, url_prefix='/receipts')
from src.receipts import forms, routes

View File

@ -0,0 +1,24 @@
from flask_wtf import FlaskForm
from flask_wtf.file import FileAllowed, FileField, FileRequired
from wtforms import BooleanField, SelectMultipleField, SubmitField, widgets
from wtforms.validators import Optional
class MultiCheckboxField(SelectMultipleField):
widget = widgets.ListWidget(prefix_label=False)
option_widget = widgets.CheckboxInput()
class UploadReceiptForm(FlaskForm):
pdfReceipt = FileField("PDF", validators=[FileRequired(), FileAllowed(["pdf"], "Invalid Format, must be .pdf")])
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})
class CheckItemsForm(FlaskForm):
items = MultiCheckboxField("Items")
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})
#TODO create new() method which loads the form for a specific receipt
@classmethod
def new(cls, itemArray):
"""
"""
form = cls()
return form

View File

@ -0,0 +1,62 @@
from src import db, LOGGER
from src.models.receipt import Receipt
from src.models.login_token import LoginToken
from src.receipts import bp
from src.receipts.forms import CheckItemsForm, UploadReceiptForm
from src.utils.pdf_receipt_parser import PDFReceipt
from src.utils.routes_utils import render_custom_template as render_template
from flask import abort, request, url_for
from flask_login import current_user, login_required
PDFDir = "./"
@bp.route('/upload_receipt', methods=['GET', 'POST'])
@login_required
def upload_receipt():
"""Upload of a receipt."""
if current_user.is_anonymous:
abort(403)
if "establishment" in request.args:
if LoginToken.query.filter_by(establishment=request.args['establishment'], user=current_user.id).first():
form = UploadReceiptForm()
LOGGER.debug(form.pdfReceipt.data)
if form.is_submitted():
LOGGER.debug("submitted")
if form.validate():
LOGGER.debug("valid")
else:
LOGGER.debug(form.errors)
if form.validate_on_submit():
receipt = PDFReceipt(form.pdfReceipt.data)
dbReceipt = Receipt(id = receipt.id, date = receipt.date,
from_user = LoginToken.query.filter_by(establishment=request.args['establishment'], user=current_user.id).first().token)
form.pdfReceipt.data.save(PDFDir + f"{str(receipt.date)}_{receipt.id}.pdf")
db.session.add(dbReceipt)
db.session.commit()
return receipt.text.replace("\n", "<br>")
return render_template("receipts/upload.html", form = form)
abort(403)
@bp.route('/confirm_receipt', methods=['GET', 'POST'])
@login_required
def confirm_receipt_items():
"""Check items from a receipt if they should be accounted for payment."""
if "receipt" in request.args:
receipt_details = Receipt.query.get(request.args['receipt'])
if current_user.is_anonymous and current_user.id == receipt_details.LoginToken.Establishment.owner:
receipt = PDFReceipt._getPDFReceiptFromFile(PDFDir + f"{receipt.date}_{receipt.id}.pdf")
form = CheckItemsForm()
# TODO: Precheck if items are already in database. If yes, check if item is present only once or multiple
# times and provide dropdown menu if necessary. If not, provide input field.
temp_choices = []
for item in receipt.items:
match item:
case {"itemname": itemname, "price": price}:
temp_choices.append((itemname.replace(" ", "_"), f"{itemname, price}"))
case {"itemname": itemname, "price": price, "amount": amount}:
temp_choices.append((itemname.replace(" ", "_"), f"{itemname}, {price} * {amount}"))
form.choices = temp_choices
if form.validate_on_submit():
pass # TODO
return render_template("receipts/confirm_items.html")
abort(403)

View File

@ -5,16 +5,10 @@
<h1>Sign In</h1>
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<p>
{{ wtf.form_field(form.email) }}
</p>
<p>
{{ wtf.form_field(form.password) }}
</p>
<p>
{{ wtf.form_field(form.remember_me) }}
</p>
{{ wtf.form_field(form.submit, class=form.submit.render_kw["class"]) }}
{{ wtf.form_field(form.email, class=form.email.render_kw["class"] or "form-control mb-3") }}
{{ wtf.form_field(form.password, class=form.password.render_kw["class"] or "form-control mb-3") }}
{{ wtf.form_field(form.remember_me, class=form.remember_me.render_kw["class"] or "mb-3") }}
{{ wtf.form_field(form.submit, class=form.submit.render_kw["class"] or "btn btn-primary mt-3") }}
</form>
<p>
Forgot Your Password?

View File

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>Register</h1>
<form action="" method="post">
{{ form.hidden_tag() }}
{{ wtf.form_field(form.email, class=form.email.render_kw["class"] or "form-control mb-3") }}
{{ wtf.form_field(form.password, class=form.password.render_kw["class"] or "form-control mb-3") }}
{{ wtf.form_field(form.password2, class=form.password2.render_kw["class"] or "form-control mb-3") }}
{{ wtf.form_field(form.submit, class=form.submit.render_kw["class"] or "btn btn-primary mt-3") }}
</form>
{% endblock %}

View File

@ -0,0 +1,126 @@
{% extends "bootstrap/base.html" %}
{% block title %}
{% if title %}
{{ title }}
{% else %}
CostHive
{% endif %}
{% endblock %}
{% block metas %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{% endblock %}
{% block styles %}
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<link rel="stylesheet" href={{ url_for('static', filename="sidebars.css")}}>
{% endblock %}
{% block navbar %}
<main>
<div class="flex-shrink-0 p-3 bg-black bg-opacity-10 scrollbar-primary h-100 position-fixed" style="width: 280px;">
<a href="/"
class="d-flex align-items-center pb-3 mb-3 link-dark text-decoration-none border-bottom border-dark">
<svg class="bi me-2" width="30" height="24">
<use xlink:href="#bootstrap"></use>
</svg>
<span class="fs-5 fw-semibold">
{% if title %}
{{ title }}
{% else %}
CostHive
{% endif %}
</span>
</a>
<ul class="list-unstyled ps-0">
<li class="mb-1">
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse"
data-bs-target="#home-collapse" aria-expanded="true">
Home (WIP)
</button>
<div class="collapse show" id="home-collapse">
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
<li><a href="{{ url_for('establishment.list.show_establishments') }}"
class="link-dark rounded">Einrichtungen</a></li>
<li><a href="#" class="link-dark rounded">Inventar</a></li>
</ul>
</div>
</li>
{% if current_user.is_authenticated %}
<li class="mb-1">
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse"
data-bs-target="#dashboard-collapse" aria-expanded="false">
Übersicht
</button>
<div class="collapse" id="dashboard-collapse">
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
{% if current_user_establishments %}
{% for establishment in current_user_establishments %}
<li>
<a href="{{ url_for('establishment.overview.get_report_from_user', establishment_id=establishment.id) }}"
class="link-dark rounded">{{ establishment.name }}</a>
</li>
{% endfor %}
{% endif %}
<li>
<a href="{{ url_for('establishment.new.create_new_establishment') }}"
class="link-dark rounded"> Neu</a>
</li>
</ul>
</div>
</li>
{% endif %}
<li class="border-top border-dark my-3"></li>
<li class="mb-1">
<button class="btn btn-toggle align-items-center rounded collapsed" data-bs-toggle="collapse"
data-bs-target="#account-collapse" aria-expanded="false">
Account
</button>
<div class="collapse" id="account-collapse" style="">
<ul class="btn-toggle-nav list-unstyled fw-normal pb-1 small">
<!-- <li><a href="#" class="link-dark rounded">New...</a></li>
<li><a href="#" class="link-dark rounded">Settings</a></li> -->
{% if current_user.is_authenticated %}
<!-- <li><a href="#" class="link-dark rounded">Profile</a></li> -->
<li><a href={{ url_for('auth.web_logout') }} class="link-dark rounded">Sign out</a></li>
{% else %}
<li><a href={{ url_for('auth.web_register') }} class="link-dark rounded">Register</a></li>
<li><a href={{ url_for('auth.web_login') }} class="link-dark rounded">Sign in</a></li>
{% endif %}
</ul>
</div>
</li>
</ul>
</div>
{% endblock %}
{% block content %}
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<div style="width: 280px;"></div>
<div class="container">
<div class="row my-3"></div>
<div class="row-md-3">
{% block app_content %}{% endblock %}
</div>
<div class="row my-3"></div>
</div>
</main>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
crossorigin="anonymous"></script>
<!-- <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> -->
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block app_content %}
<h1>You are not allowed to do this!</h1>
<h4>how dare you</h4>
<p><a href="{{ url_for('main.index') }}">Back</a></p>
{% endblock %}

View File

@ -0,0 +1,6 @@
<form action="" method="post">
{{ form.csrf_token }}
{{ form.candidate_id(value=candidate.user) }}
{{ form.accept }}
{{ form.deny }}
</form>

View File

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<table class="table">
<tbody>
{% for candidate in establishment_candidates %}
<tr>
<td>{{ candidate.User.email }}</td>
<td>{% include "establishment/candidates/_candidate_evaluation.html" %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% include "utils/general/_page_navigation.html" %}
{% endblock %}

View File

@ -0,0 +1,27 @@
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<table class="table">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Name</th>
{% if current_user.is_authenticated %}
<th scope="col">Anfrage senden</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for establishment in establishments %}
<tr>
<th scope="row">{{ establishment.id }}</th>
<td>{{ establishment.name }}</td>
{% if current_user.is_authenticated %}
<td>{% include 'establishment/list/establishment_request.html' %}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{% include "utils/general/_page_navigation.html" %}
{% endblock %}

View File

@ -0,0 +1,11 @@
<form action="" method="post">
{{ form.csrf_token }}
{{ form.id(value=establishment.id) }}
{% if current_user.EstablishmentCandidate.filter_by(establishment = establishment.id).first() %}
{{ form.submit(value="Bereits angefragt", disabled=True) }}
{% elif establishment.LoginToken.filter_by(user = current_user.id).first() %}
{{ form.submit(value="Bereits Mitglied", disabled=True) }}
{% else %}
{{ form.submit }}
{% endif %}
</form>

View File

@ -0,0 +1,6 @@
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
{% include 'establishment/new/new_establishment_form.html' %}
{% endblock %}

View File

@ -0,0 +1,5 @@
<form action="" method="post">
{{ form.hidden_tag() }}
{{ wtf.form_field(form.name, class=form.name.render_kw["class"] or "form-control mb-3") }}
{{ wtf.form_field(form.submit, class=form.submit.render_kw["class"] or "btn btn-primary mt-3") }}
</form>

View File

@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block app_content %}
{% if establishment %}
{% if current_user.id == establishment.owner %}
<button type="button" class="btn btn-outline-dark px-2" data-bs-toggle="button" autocomplete="off"
onclick="window.location.href='{{ url_for('establishment.overview.get_report_from_user', establishment_id=establishment.id) }}'">
Abrechnung
</button>
{% endif %}
{% endif %}
{% for user in results %}
<div class="card">
<button class="btn btn-primary" data-bs-toggle="collapse" data-bs-target="#b{{ user.id }}" aria-expanded="true">
<div class="card-header">
<h3>{{ user.email }}: {{ user.sum/100 }} €</h3>
</div>
</button>
<div class="collapse" id="b{{ user.id }}">
{% for item_infos in user.item_infos %}
<div class="card-body">
<div class="col-sm-1"></div>
<div class="col">
<h4>{{ item_infos.date }}</h4>
{% for item in item_infos.item_list %}
<div class="row">
<div class="col-sm-1"></div>
<div class="col">
{{ item.amount }}x {{ item.name }} je {{ item.price/100 }} €
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,6 @@
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
{% include 'item/new/new_item_form.html' %}
{% endblock %}

View File

@ -0,0 +1,13 @@
{% from 'utils/form/_render_field.html' import render_field %}
<form action="" method="post">
{{ form.hidden_tag() }}
{{ render_field(form.id) }}
{{ render_field(form.name) }}
{{ render_field(form.description) }}
{{ render_field(form.date) }}
{{ render_field(form.price_change) }}
{{ render_field(form.amount_change) }}
{{ render_field(form.category) }}
{{ render_field(form.brand) }}
{{ form.submit }}
</form>

View File

@ -0,0 +1,4 @@
{% extends "base.html" %}
{% block app_content %}
{% endblock %}

View File

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<form action="" method="post" novalidate enctype="multipart/form-data">
{{ form.hidden_tag() }}
{{ wtf.form_field(form.submit, class=form.submit.render_kw["class"] or "btn btn-primary mt-3") }}
</form>
{% endblock %}

View File

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<form action="" method="post" novalidate enctype="multipart/form-data">
{{ form.hidden_tag() }}
{{ wtf.form_field(form.pdfReceipt, class=form.pdfReceipt.render_kw["class"] or "form-control") }}
{{ wtf.form_field(form.submit, class=form.submit.render_kw["class"] or "btn btn-primary mt-3") }}
</form>
{% endblock %}

View File

@ -0,0 +1,11 @@
{% macro render_field(field) %}
<div class="form-group">
{{ field.label }} {{ field(**kwargs)|safe }} {% if field.errors %}
<ul class="errors">
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endmacro %}

Some files were not shown because too many files have changed in this diff Show More