major: add receipt upload
You can now upload receipts and check which ones should be accounted. Currently without function, needs to be implemented.
This commit is contained in:
parent
5e4a59e15f
commit
6c0fa5a58b
2
.gitignore
vendored
2
.gitignore
vendored
@ -339,6 +339,7 @@ pip-selfcheck.json
|
||||
# Misc
|
||||
config.yaml
|
||||
scans.json
|
||||
test_*.*
|
||||
test.*
|
||||
*.db
|
||||
.vscode
|
||||
@ -349,3 +350,4 @@ test.*
|
||||
!.env.project
|
||||
!.env.vault
|
||||
*.ps1
|
||||
*.pdf
|
||||
@ -5,14 +5,13 @@ RUN useradd costhive
|
||||
WORKDIR /home/costhive
|
||||
|
||||
RUN apt update && apt -y upgrade; \
|
||||
apt install -y libpq-dev gcc g++ swig make; \
|
||||
apt install -y libpq-dev gcc g++ swig make cmake m4; \
|
||||
rm -rf /var/lib/apt/lists
|
||||
|
||||
COPY boot.sh requirements.txt ./
|
||||
COPY boot.sh backend/requirements.txt ./
|
||||
RUN python -m venv venv; \
|
||||
venv/bin/pip install --upgrade pip; \
|
||||
venv/bin/pip install wheel; \
|
||||
venv/bin/pip install gunicorn; \
|
||||
venv/bin/pip install wheel gunicorn; \
|
||||
venv/bin/pip install -r requirements.txt
|
||||
|
||||
COPY backend backend
|
||||
@ -20,7 +19,7 @@ COPY backend backend
|
||||
ENV FLASK_APP run.py
|
||||
|
||||
RUN chmod +x boot.sh; \
|
||||
chown -R costhive:costhive ./
|
||||
chown -R costhive:costhive .
|
||||
|
||||
USER costhive
|
||||
|
||||
|
||||
@ -21,6 +21,7 @@ python-dotenv = "*"
|
||||
pymupdf = "*"
|
||||
requests = "*"
|
||||
sqlalchemy-utils = "*"
|
||||
wtforms-sqlalchemy = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
|
||||
88
backend/Pipfile.lock
generated
88
backend/Pipfile.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "6aa4fc89c41e2a25283179a13d468214fdebcbd824a74a7f2f79482b920eed78"
|
||||
"sha256": "5418d6b50788a542461e9a348ad0864d3e4d2046e48967df081cbaed9971ed0f"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@ -25,14 +25,6 @@
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.11.1"
|
||||
},
|
||||
"anyio": {
|
||||
"hashes": [
|
||||
"sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780",
|
||||
"sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.7.1"
|
||||
},
|
||||
"blinker": {
|
||||
"hashes": [
|
||||
"sha256:4afd3de66ef3a9f8067559fb7a1cbe555c17dcbe15971b05d1b625c3e7abe213",
|
||||
@ -43,11 +35,11 @@
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7",
|
||||
"sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"
|
||||
"sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082",
|
||||
"sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2023.5.7"
|
||||
"version": "==2023.7.22"
|
||||
},
|
||||
"charset-normalizer": {
|
||||
"hashes": [
|
||||
@ -132,11 +124,11 @@
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:4be4b1af8d665c6d942909916d31a213a106800c47d0eeba73d34da3cbc11367",
|
||||
"sha256:e576aa487d679441d7d30abb87e1b43d24fc53bffb8758443b1a9e1cee504548"
|
||||
"sha256:48ee849951919527a045bfe3bf7baa8a959c423134e1a5b98c05c20ba75a1cbd",
|
||||
"sha256:fa244bb30b3b5ee2cae3da8f55c9e5e0c0e86093306301fb418eb9dc40fbded5"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==8.1.5"
|
||||
"version": "==8.1.6"
|
||||
},
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
@ -148,11 +140,11 @@
|
||||
},
|
||||
"dnspython": {
|
||||
"hashes": [
|
||||
"sha256:46b4052a55b56beea3a3bdd7b30295c292bd6827dd442348bc116f2d35b17f0a",
|
||||
"sha256:758e691dbb454d5ccf4e1b154a19e52847f79e21a42fef17b969144af29a4e6c"
|
||||
"sha256:5b7488477388b8c0b70a8ce93b227c5603bc7b77f1565afe8e729c36c51447d7",
|
||||
"sha256:c33971c79af5be968bb897e95c2448e11a645ee84d93b265ce0b7aabe5dfdca8"
|
||||
],
|
||||
"markers": "python_version >= '3.8' and python_version < '4.0'",
|
||||
"version": "==2.4.0"
|
||||
"version": "==2.4.1"
|
||||
},
|
||||
"dominate": {
|
||||
"hashes": [
|
||||
@ -170,14 +162,6 @@
|
||||
"index": "pypi",
|
||||
"version": "==2.0.0.post2"
|
||||
},
|
||||
"exceptiongroup": {
|
||||
"hashes": [
|
||||
"sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5",
|
||||
"sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"
|
||||
],
|
||||
"markers": "python_version < '3.11'",
|
||||
"version": "==1.1.2"
|
||||
},
|
||||
"flask": {
|
||||
"hashes": [
|
||||
"sha256:77fd4e1249d8c9923de34907236b747ced06e5467ecac1a7bb7115ae0e9670b0",
|
||||
@ -314,22 +298,6 @@
|
||||
"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"
|
||||
},
|
||||
"h11": {
|
||||
"hashes": [
|
||||
"sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d",
|
||||
"sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.14.0"
|
||||
},
|
||||
"httpcore": {
|
||||
"hashes": [
|
||||
"sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888",
|
||||
"sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==0.17.3"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
|
||||
@ -420,11 +388,11 @@
|
||||
},
|
||||
"marshmallow": {
|
||||
"hashes": [
|
||||
"sha256:90032c0fd650ce94b6ec6dc8dfeb0e3ff50c144586462c389b81a07205bedb78",
|
||||
"sha256:93f0958568da045b0021ec6aeb7ac37c81bfcccbb9a0e7ed8559885070b3a19b"
|
||||
"sha256:5d2371bbe42000f2b3fb5eaa065224df7d8f8597bc19a1bbfa5bfe7fba8da889",
|
||||
"sha256:684939db93e80ad3561392f47be0230743131560a41c5110684c16e21ade0a5c"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.19.0"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==3.20.1"
|
||||
},
|
||||
"marshmallow-sqlalchemy": {
|
||||
"hashes": [
|
||||
@ -512,11 +480,11 @@
|
||||
},
|
||||
"pyjwt": {
|
||||
"hashes": [
|
||||
"sha256:ba2b425b15ad5ef12f200dc67dd56af4e26de2331f965c5439994dad075876e1",
|
||||
"sha256:bd6ca4a3c4285c1a2d4349e5a035fdf8fb94e04ccd0fcbe6ba289dae9cc3e074"
|
||||
"sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de",
|
||||
"sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.7.0"
|
||||
"version": "==2.8.0"
|
||||
},
|
||||
"pymupdf": {
|
||||
"hashes": [
|
||||
@ -570,14 +538,6 @@
|
||||
"index": "pypi",
|
||||
"version": "==2.31.0"
|
||||
},
|
||||
"sniffio": {
|
||||
"hashes": [
|
||||
"sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101",
|
||||
"sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
"sha256:024d2f67fb3ec697555e48caeb7147cfe2c08065a4f1a52d93c3d44fc8e6ad1c",
|
||||
@ -643,11 +603,11 @@
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:48e7fafa40319d358848e1bc6809b208340fafe2096f1725d05d67443d0483d1",
|
||||
"sha256:bee28b5e56addb8226c96f7f13ac28cb4c301dd5ea8a6ca179c0b9835e032825"
|
||||
"sha256:8d22f86aae8ef5e410d4f539fde9ce6b2113a001bb4d189e0aed70642d602b11",
|
||||
"sha256:de7df1803967d2c2a98e4b11bb7d6bd9210474c46e8a0401514e3a42a75ebde4"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.0.3"
|
||||
"version": "==2.0.4"
|
||||
},
|
||||
"visitor": {
|
||||
"hashes": [
|
||||
@ -670,6 +630,14 @@
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"wtforms-sqlalchemy": {
|
||||
"hashes": [
|
||||
"sha256:7ca42824ad7c453a036f502b42d9c17d4ad8398ff9248a62ed538d1ce0bc41c3",
|
||||
"sha256:90195d7592bf256d82498c42c79d416832e4a4e6fbca4f1e745a018f66d26c47"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.3"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
|
||||
@ -0,0 +1,54 @@
|
||||
"""receipt id serial
|
||||
|
||||
Revision ID: 0fa2ef37e440
|
||||
Revises: f6f97ed9c053
|
||||
Create Date: 2023-07-25 21:26:25.353435
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '0fa2ef37e440'
|
||||
down_revision = 'f6f97ed9c053'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.execute("CREATE SEQUENCE receipt_id_seq;")
|
||||
|
||||
with op.batch_alter_table('receipt', schema=None) as batch_op:
|
||||
batch_op.alter_column('id',
|
||||
existing_type=sa.BIGINT(),
|
||||
type_=sa.Integer(),
|
||||
existing_nullable=False,
|
||||
server_default=sa.sql.func.next_value(sa.Sequence('receipt_id_seq')))
|
||||
|
||||
with op.batch_alter_table('receipt_item', schema=None) as batch_op:
|
||||
batch_op.alter_column('receipt',
|
||||
existing_type=sa.BIGINT(),
|
||||
type_=sa.Integer(),
|
||||
existing_nullable=False)
|
||||
|
||||
op.execute("ALTER SEQUENCE receipt_id_seq OWNED BY receipt.id;")
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('receipt_item', schema=None) as batch_op:
|
||||
batch_op.alter_column('receipt',
|
||||
existing_type=sa.Integer(),
|
||||
type_=sa.BIGINT(),
|
||||
existing_nullable=False)
|
||||
|
||||
with op.batch_alter_table('receipt', schema=None) as batch_op:
|
||||
batch_op.alter_column('id',
|
||||
existing_type=sa.Integer(),
|
||||
type_=sa.BIGINT(),
|
||||
existing_nullable=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@ -9,3 +9,6 @@ class Brand(db.Model):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Brand {self.id} ({self.name})>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.name}"
|
||||
|
||||
@ -21,3 +21,6 @@ class Item(db.Model):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Item {self.id} ({self.name})>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"({self.id}) {self.description}"
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint('receipts', __name__, url_prefix='/receipts')
|
||||
|
||||
from . import forms, routes
|
||||
from .upload import bp as bp_upload
|
||||
bp.register_blueprint(bp_upload)
|
||||
from .check_items import bp as bp_check_items
|
||||
bp.register_blueprint(bp_check_items)
|
||||
5
backend/src/receipts/check_items/__init__.py
Normal file
5
backend/src/receipts/check_items/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from flask import Blueprint
|
||||
|
||||
bp = Blueprint('check_items', __name__, url_prefix='/check_items')
|
||||
|
||||
from . import forms, routes
|
||||
63
backend/src/receipts/check_items/forms.py
Normal file
63
backend/src/receipts/check_items/forms.py
Normal file
@ -0,0 +1,63 @@
|
||||
from collections import namedtuple
|
||||
from flask_wtf import FlaskForm
|
||||
from models import Brand, Item
|
||||
from src.utils.models.query_factories import all_brands, all_items
|
||||
from wtforms import BooleanField, HiddenField, FieldList, Form, FormField, IntegerField, RadioField, SelectField, StringField, SubmitField
|
||||
from wtforms.validators import DataRequired, Optional, ValidationError
|
||||
from wtforms_sqlalchemy.fields import QuerySelectField
|
||||
|
||||
|
||||
class CheckItemsEntryForm(Form):
|
||||
# TODO Fertig machen x.x
|
||||
itemname = HiddenField('itemname', validators=[DataRequired()])
|
||||
price = HiddenField('price', validators=[DataRequired()])
|
||||
requesting = BooleanField("", default=False, render_kw={
|
||||
"class": "form-check-input"})
|
||||
new_or_existing = RadioField("", choices=[
|
||||
(0, "New"), (1, "Existing")], render_kw={"class": "form-check-input", "style": "display:none"}, validate_choice=False)
|
||||
# Fields for new Item
|
||||
new_description = StringField("Description", render_kw={"class": "form-control"})
|
||||
new_amount_change = IntegerField("Amount", render_kw={"class": "form-control"})
|
||||
new_brand = QuerySelectField("Brand", query_factory=all_brands, render_kw={"class": "form-control"}, allow_blank=True)
|
||||
# Fields for existing Item
|
||||
existing_item = QuerySelectField("Item", query_factory=all_items, render_kw={"class": "form-control"}, allow_blank=True)
|
||||
|
||||
def validate_new_or_existing(self, new_or_existing):
|
||||
if (self.requesting.data and not new_or_existing.data):
|
||||
raise ValidationError(
|
||||
"Please choose if it's a new or existing Item.")
|
||||
|
||||
def validate_existing_item(self, existing_item):
|
||||
if (existing_item.data and self.new_or_existing.data == 0):
|
||||
raise ValidationError("You shouldn't be able to enter this.")
|
||||
|
||||
def validate_new_description(self, new_description):
|
||||
if (new_description.data and self.new_or_existing.data == 1):
|
||||
raise ValidationError("You shouldn't be able to enter this.")
|
||||
|
||||
def validate_new_amount_change(self, new_amount_change):
|
||||
if (new_amount_change.data and self.new_or_existing.data == 1):
|
||||
raise ValidationError("You shouldn't be able to enter this.")
|
||||
|
||||
def validate_new_brand(self, new_brand):
|
||||
if (new_brand.data and self.new_or_existing.data == 1):
|
||||
raise ValidationError("You shouldn't be able to enter this.")
|
||||
|
||||
|
||||
class CheckItemsForm(FlaskForm):
|
||||
items = FieldList(FormField(CheckItemsEntryForm))
|
||||
submit = SubmitField("Submit", render_kw={"class": "btn btn-primary mt-3"})
|
||||
|
||||
@classmethod
|
||||
def new(cls, itemarray):
|
||||
CheckItemsEntry = namedtuple(
|
||||
"CheckItemsEntry", ["itemname", "price", "new_brand"])
|
||||
CheckItems = namedtuple("CheckItems", ["items"])
|
||||
check_items_entry = []
|
||||
for item in itemarray:
|
||||
check_items_entry.append(CheckItemsEntry(item['itemname'], item['price'], 0))
|
||||
check_items = CheckItems(check_items_entry)
|
||||
form = cls(obj=check_items)
|
||||
|
||||
print(f"{form.items.entries}")
|
||||
return form
|
||||
41
backend/src/receipts/check_items/routes.py
Normal file
41
backend/src/receipts/check_items/routes.py
Normal file
@ -0,0 +1,41 @@
|
||||
from flask import abort, request, url_for
|
||||
from flask_login import current_user, login_required
|
||||
from . import bp
|
||||
from .forms import CheckItemsForm
|
||||
from src import db, LOGGER
|
||||
from models.receipt import Receipt
|
||||
from models.login_token import LoginToken
|
||||
from src.utils.pdf_receipt_parser import PDFReceipt
|
||||
from src.utils.routes_utils import render_custom_template as render_template
|
||||
|
||||
PDFDir = "./"
|
||||
|
||||
@bp.route('/<int:receipt_id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def confirm_receipt_items(receipt_id: int):
|
||||
"""Check items from a receipt if they should be accounted for payment.
|
||||
Get those items from the receipt PDF itself."""
|
||||
receipt_details = Receipt.query.get(receipt_id)
|
||||
if current_user.is_authenticated and current_user.id == receipt_details.LoginToken.Establishment.owner:
|
||||
receipt = PDFReceipt.getPDFReceiptFromFile(PDFDir + f"{receipt_details.id}.pdf")
|
||||
form = CheckItemsForm.new(receipt.items)
|
||||
# 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
|
||||
# print(form.data)
|
||||
for formitem in form.items:
|
||||
# print(formitem.new_brand.__dict__)
|
||||
print(formitem.data)
|
||||
if form.validate():
|
||||
print("valid")
|
||||
if form.validate_on_submit():
|
||||
return form.items.data
|
||||
return render_template("receipts/check_items.html", form=form)
|
||||
abort(403)
|
||||
@ -1,24 +0,0 @@
|
||||
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
|
||||
@ -1,35 +0,0 @@
|
||||
from flask import abort, request, url_for
|
||||
from flask_login import current_user, login_required
|
||||
from werkzeug.utils import secure_filename
|
||||
from . import bp
|
||||
from .forms import UploadReceiptForm
|
||||
from src import db, LOGGER
|
||||
from models.receipt import Receipt
|
||||
from models.login_token import LoginToken
|
||||
from src.utils.pdf_receipt_parser import PDFReceipt
|
||||
from src.utils.routes_utils import render_custom_template as render_template
|
||||
|
||||
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.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 + secure_filename(f"{str(receipt.date)}_{receipt.id}.pdf"))
|
||||
db.session.add(dbReceipt)
|
||||
db.session.commit()
|
||||
return receipt.text.replace("\n", "<br>")
|
||||
else:
|
||||
LOGGER.debug(form.errors)
|
||||
return render_template("receipts/upload.html", form = form)
|
||||
abort(403)
|
||||
@ -1,5 +1,6 @@
|
||||
from flask import abort, request, url_for
|
||||
from flask_login import current_user, login_required
|
||||
from os import rename
|
||||
from werkzeug.utils import secure_filename
|
||||
from . import bp
|
||||
from .forms import UploadReceiptForm
|
||||
@ -11,7 +12,7 @@ from src.utils.routes_utils import render_custom_template as render_template
|
||||
|
||||
PDFDir = "./"
|
||||
# TODO überarbeiten. PDFs müssen in der Datenbank eine eigene ID bekommen.
|
||||
# Quittungen haben eine USt.-ID. Die muss als Unique Key in der Datenbank
|
||||
# Quittungen haben eine gesonderte ID. Die muss als Unique Key in der Datenbank
|
||||
# hinterlegt sein.
|
||||
# Die laufende ID ist zum abspeichern der PDFs gedacht.
|
||||
@bp.route('/<int:establishment>', methods=['GET', 'POST'])
|
||||
@ -20,16 +21,19 @@ def upload_receipt(establishment: int):
|
||||
"""Upload of a receipt."""
|
||||
if current_user.is_anonymous:
|
||||
abort(403)
|
||||
if LoginToken.query.filter_by(establishment=request.args['establishment'], user=current_user.id).first():
|
||||
if LoginToken.query.filter_by(establishment=establishment, user=current_user.id).first():
|
||||
form = UploadReceiptForm()
|
||||
LOGGER.debug(form.pdfReceipt.data)
|
||||
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 + secure_filename(f"{str(receipt.date)}_{receipt.id}.pdf"))
|
||||
pdfReceipt = form.pdfReceipt.data
|
||||
pdfReceipt.save(f"{PDFDir}/temp.pdf")
|
||||
with open(f"{PDFDir}/temp.pdf") as doc:
|
||||
receipt = PDFReceipt(doc)
|
||||
dbReceipt = Receipt(bonid = receipt.id, date = receipt.date,
|
||||
from_user = LoginToken.query.filter_by(establishment=establishment, user=current_user.id).first().token)
|
||||
db.session.add(dbReceipt)
|
||||
db.session.commit()
|
||||
rename(f"{PDFDir}/temp.pdf", f"{PDFDir}{secure_filename(f'{dbReceipt.id}.pdf')}")
|
||||
return receipt.text.replace("\n", "<br>")
|
||||
else:
|
||||
LOGGER.debug(form.errors)
|
||||
|
||||
10
backend/src/utils/models/query_factories.py
Normal file
10
backend/src/utils/models/query_factories.py
Normal file
@ -0,0 +1,10 @@
|
||||
from models import Brand, Category, Item
|
||||
|
||||
def all_brands():
|
||||
return Brand.query.order_by("name")
|
||||
|
||||
def all_categories():
|
||||
return Category.query.order_by("name")
|
||||
|
||||
def all_items():
|
||||
return Item.query.order_by("id")
|
||||
@ -122,5 +122,5 @@ CostHive
|
||||
<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> -->
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
||||
{% endblock %}
|
||||
67
backend/web/templates/receipts/check_items.html
Normal file
67
backend/web/templates/receipts/check_items.html
Normal file
@ -0,0 +1,67 @@
|
||||
{% extends "base.html" %}
|
||||
{% import 'bootstrap/wtf.html' as wtf %}
|
||||
{% from 'utils/form/_render_field.html' import render_field %}
|
||||
|
||||
{% block app_content %}
|
||||
<form action="" method="post" novalidate enctype="multipart/form-data">
|
||||
{{ form.hidden_tag() }}
|
||||
{% for item in form.items %}
|
||||
<h4>{{ item.requesting() }} {{ item.data.itemname }} (€{{ item.data.price }})</h4>
|
||||
{{ render_field(item.itemname) }}
|
||||
{{ render_field(item.price) }}
|
||||
<div class="{{ item.new_or_existing.id }}" style="display: none;">
|
||||
{{ render_field(item.new_or_existing) }}
|
||||
</div>
|
||||
<div class="{{ item.id }}_new" style="display: none;">
|
||||
{{ render_field(item.new_description) }}
|
||||
{{ render_field(item.new_amount_change) }}
|
||||
{{ render_field(item.new_brand) }}
|
||||
</div>
|
||||
<div class="{{ item.id }}_existing" style="display: none;">
|
||||
{{ render_field(item.existing_item) }}
|
||||
</div>
|
||||
|
||||
<br>
|
||||
{% endfor %}
|
||||
{{ form.submit() }}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
{% for item in form.items %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$("#{{ item.requesting.id }}").change(function () {
|
||||
if ($(this).prop("checked")) {
|
||||
$(".{{ item.new_or_existing.id }}").show();
|
||||
if ($("#{{ item.new_or_existing.id }}-0").prop("checked")) {
|
||||
$(".{{ item.id }}_new").show();
|
||||
}
|
||||
if ($("#{{ item.new_or_existing.id }}-1").prop("checked")) {
|
||||
$(".{{ item.id }}_existing").show();
|
||||
}
|
||||
}
|
||||
else {
|
||||
$(".{{ item.new_or_existing.id }}").hide();
|
||||
$(".{{ item.id }}_new").hide();
|
||||
$(".{{ item.id }}_existing").hide();
|
||||
}
|
||||
});
|
||||
$("#{{ item.new_or_existing.id }}-0").change(function () {
|
||||
if ($(this).prop("checked")) {
|
||||
$(".{{ item.id }}_new").show();
|
||||
$(".{{ item.id }}_existing").hide();
|
||||
}
|
||||
});
|
||||
$("#{{ item.new_or_existing.id }}-1").change(function () {
|
||||
if ($(this).prop("checked")) {
|
||||
$(".{{ item.id }}_new").hide();
|
||||
$(".{{ item.id }}_existing").show();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@ -1,11 +1,13 @@
|
||||
{% from 'utils/form/_render_radio_button.html' import render_field as render_radio %}
|
||||
{% from 'utils/form/_render_generic_field.html' import render_field as render_generic %}
|
||||
{% from 'utils/form/_render_hidden_field.html' import render_field as render_hidden %}
|
||||
|
||||
{% 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>
|
||||
{% if field.__class__.__name__ == "RadioField" %}
|
||||
{{ render_radio(field) }}
|
||||
{% elif field.__class__.__name__ == "HiddenField" %}
|
||||
{{ render_hidden(field) }}
|
||||
{% else %}
|
||||
{{ render_generic(field)}}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
11
backend/web/templates/utils/form/_render_fieldlist.html
Normal file
11
backend/web/templates/utils/form/_render_fieldlist.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% macro render_fieldlist(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 %}
|
||||
11
backend/web/templates/utils/form/_render_generic_field.html
Normal file
11
backend/web/templates/utils/form/_render_generic_field.html
Normal 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 %}
|
||||
11
backend/web/templates/utils/form/_render_hidden_field.html
Normal file
11
backend/web/templates/utils/form/_render_hidden_field.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% macro render_field(field) %}
|
||||
<div class="form-group">
|
||||
{{ field(**kwargs)|safe }} {% if field.errors %}
|
||||
<ul class="errors">
|
||||
{% for error in field.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
16
backend/web/templates/utils/form/_render_radio_button.html
Normal file
16
backend/web/templates/utils/form/_render_radio_button.html
Normal file
@ -0,0 +1,16 @@
|
||||
{% macro render_field(field) %}
|
||||
{% for choice in field.choices %}
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="{{ field.render_kw['class'] }}" type="radio" name="{{ field.id }}" id="{{ field.id }}-{{ choice[0] }}"
|
||||
value="{{ choice[0] }}">
|
||||
<label class="form-check-label" for="{{ field.id }}-{{ choice[0] }}">{{ choice[1] }}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if field.errors %}
|
||||
<ul class="errors">
|
||||
{% for error in field.errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
BIN
requirements.txt.backup
Normal file
BIN
requirements.txt.backup
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user