Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f24880c09a | |||
| 3622cbca05 | |||
| 637a5f7ad5 | |||
| 15ba3c060a | |||
| bde9a50767 | |||
| 60e221146a | |||
| d34f2adb2f | |||
| a6f229e7fb |
44
.github/workflows/main.yml
vendored
44
.github/workflows/main.yml
vendored
@ -4,19 +4,47 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- '*'
|
- '*'
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@master
|
- uses: https://gitea.com/actions/checkout@master
|
||||||
- name: Archive Server
|
- name: Zip Artifacts
|
||||||
uses: thedoctor0/zip-release@master
|
uses: https://github.com/thedoctor0/zip-release@master
|
||||||
with:
|
with:
|
||||||
type: 'zip'
|
type: 'zip'
|
||||||
filename: 'server.zip'
|
filename: 'server.zip'
|
||||||
exclusions: '*.git*'
|
exclusions: '*.git*'
|
||||||
- name: Release Archive
|
- name: Release Archive
|
||||||
uses: ncipollo/release-action@v1
|
uses: https://gitea.com/actions/gitea-release-action@v1
|
||||||
with:
|
with:
|
||||||
allowUpdates: true
|
server_url: https://gitea.wpgcommunity.net
|
||||||
artifacts: "server.zip"
|
files: 'server.zip'
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: https://gitea.com/actions/checkout@master
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
${{ vars.DOCKER_REPO }}/costhive
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
config-inline: |
|
||||||
|
[registry."${{ vars.DOCKER_REPO }}"]
|
||||||
|
http = true
|
||||||
|
insecure = true
|
||||||
|
- name: Login to Gitea Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ vars.DOCKER_REPO }}
|
||||||
|
username: ${{ vars.DOCKER_USER }}
|
||||||
|
password: ${{ secrets.DOCKER_PASS }}
|
||||||
|
- name: Build and push
|
||||||
|
uses: https://github.com/docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
push: true
|
||||||
|
context: .
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
FROM python@sha256:21c9f0b22213295a13bd678c5b45aa587ff6cb01cd99b6cf0e6928f4c777006b
|
FROM python@sha256:c66cf219ac0083a9af2ff90e16530f16cd503c59eb7909feb3b8f3524dc1a87e
|
||||||
# python:3.11.4-slim-bullseye (arm/v7)
|
# python:3.12.2-slim-bullseye (amd64)
|
||||||
RUN useradd costhive
|
RUN useradd costhive
|
||||||
|
|
||||||
WORKDIR /home/costhive
|
WORKDIR /home/costhive
|
||||||
@ -16,7 +16,7 @@ RUN python -m venv venv; \
|
|||||||
|
|
||||||
COPY backend backend
|
COPY backend backend
|
||||||
|
|
||||||
ENV FLASK_APP run.py
|
ENV FLASK_APP=run.py
|
||||||
|
|
||||||
RUN chmod +x boot.sh; \
|
RUN chmod +x boot.sh; \
|
||||||
chown -R costhive:costhive .
|
chown -R costhive:costhive .
|
||||||
|
|||||||
@ -20,3 +20,4 @@ class Config(object):
|
|||||||
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
|
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
|
||||||
ADMINS = ['postmaster@wpgcommunity.net']
|
ADMINS = ['postmaster@wpgcommunity.net']
|
||||||
POSTS_PER_PAGE = 15
|
POSTS_PER_PAGE = 15
|
||||||
|
RECEIPT_FOLDER = f"{basedir}/../PDFReceipts"
|
||||||
BIN
backend/configs/tessdata/deu.traineddata
Normal file
BIN
backend/configs/tessdata/deu.traineddata
Normal file
Binary file not shown.
Binary file not shown.
@ -0,0 +1,38 @@
|
|||||||
|
"""raise password char length
|
||||||
|
|
||||||
|
Revision ID: 782a2409df41
|
||||||
|
Revises: 926395732c3e
|
||||||
|
Create Date: 2025-06-03 21:01:23.169897
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '782a2409df41'
|
||||||
|
down_revision = '926395732c3e'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||||
|
batch_op.alter_column('password_hash',
|
||||||
|
existing_type=sa.VARCHAR(length=128),
|
||||||
|
type_=sa.String(length=255),
|
||||||
|
existing_nullable=False)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||||
|
batch_op.alter_column('password_hash',
|
||||||
|
existing_type=sa.String(length=255),
|
||||||
|
type_=sa.VARCHAR(length=128),
|
||||||
|
existing_nullable=False)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@ -9,7 +9,7 @@ from werkzeug.security import generate_password_hash, check_password_hash
|
|||||||
class User(UserMixin, db.Model):
|
class User(UserMixin, db.Model):
|
||||||
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
|
id = db.Column(db.BigInteger, primary_key=True, autoincrement=True)
|
||||||
email = db.Column(db.String(255), nullable=False, unique=True)
|
email = db.Column(db.String(255), nullable=False, unique=True)
|
||||||
password_hash = db.Column(db.String(128), nullable=False)
|
password_hash = db.Column(db.String(255), nullable=False)
|
||||||
|
|
||||||
LoginToken = db.relationship("LoginToken", backref='User', lazy='dynamic')
|
LoginToken = db.relationship("LoginToken", backref='User', lazy='dynamic')
|
||||||
Bought = db.relationship("Bought", secondary="login_token",
|
Bought = db.relationship("Bought", secondary="login_token",
|
||||||
|
|||||||
@ -38,6 +38,8 @@ migrate = Migrate(transaction_per_migration=True)
|
|||||||
|
|
||||||
|
|
||||||
def create_app(config_class=Config):
|
def create_app(config_class=Config):
|
||||||
|
if not exists(config_class.RECEIPT_FOLDER):
|
||||||
|
makedirs(config_class.RECEIPT_FOLDER)
|
||||||
app = Flask(__name__, template_folder="../web/templates", static_folder="../web/static")
|
app = Flask(__name__, template_folder="../web/templates", static_folder="../web/static")
|
||||||
app.config.from_object(config_class)
|
app.config.from_object(config_class)
|
||||||
bootstrap.init_app(app)
|
bootstrap.init_app(app)
|
||||||
|
|||||||
@ -10,5 +10,4 @@ def show_item(item: int):
|
|||||||
itemschema = ItemSchema().dump(itemobj)
|
itemschema = ItemSchema().dump(itemobj)
|
||||||
itemschema['PriceChange'].sort(key=lambda d: d['date'], reverse=True)
|
itemschema['PriceChange'].sort(key=lambda d: d['date'], reverse=True)
|
||||||
itemschema['AmountChange'].sort(key=lambda d: d['date'], reverse=True)
|
itemschema['AmountChange'].sort(key=lambda d: d['date'], reverse=True)
|
||||||
print(itemschema)
|
|
||||||
return render_template('item/details/show_item.html', item = itemschema)
|
return render_template('item/details/show_item.html', item = itemschema)
|
||||||
@ -73,6 +73,4 @@ class CheckItemsForm(FlaskForm):
|
|||||||
item['itemname'], item['price'], item['amount'] if 'amount' in item else 1, 0))
|
item['itemname'], item['price'], item['amount'] if 'amount' in item else 1, 0))
|
||||||
check_items = CheckItems(check_items_entry)
|
check_items = CheckItems(check_items_entry)
|
||||||
form = cls(obj=check_items)
|
form = cls(obj=check_items)
|
||||||
|
|
||||||
print(f"{form.items.entries}")
|
|
||||||
return form
|
return form
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
from flask import abort, request, url_for
|
from flask import abort, current_app, request, url_for
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
from . import bp
|
from . import bp
|
||||||
from .forms import CheckCustomItemsEntryForm, CheckItemsEntryForm, CheckItemsForm, get_choices
|
from .forms import CheckCustomItemsEntryForm, CheckItemsEntryForm, CheckItemsForm, get_choices
|
||||||
@ -9,16 +9,16 @@ from models import AmountChange, Item, LoginToken, PriceChange, Receipt, Receipt
|
|||||||
from src.utils.modules.receipt_parser.pdf_receipt_parser import PDFReceipt
|
from src.utils.modules.receipt_parser.pdf_receipt_parser import PDFReceipt
|
||||||
from src.utils.routes_utils import render_custom_template as render_template
|
from src.utils.routes_utils import render_custom_template as render_template
|
||||||
|
|
||||||
PDFDir = "./"
|
|
||||||
|
|
||||||
@bp.route('/<int:receipt_id>', methods=['GET', 'POST'])
|
@bp.route('/<int:receipt_id>', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def confirm_receipt_items(receipt_id: int):
|
def confirm_receipt_items(receipt_id: int):
|
||||||
"""Check items from a receipt if they should be accounted for payment.
|
"""Check items from a receipt if they should be accounted for payment.
|
||||||
Get those items from the receipt PDF itself."""
|
Get those items from the receipt PDF itself."""
|
||||||
|
PDFDir: str = current_app.config["RECEIPT_FOLDER"]
|
||||||
receipt_details: Receipt = Receipt.query.get(receipt_id)
|
receipt_details: Receipt = Receipt.query.get(receipt_id)
|
||||||
if current_user.is_authenticated and current_user.id == receipt_details.LoginToken.Establishment.owner:
|
if current_user.is_authenticated and current_user.id == receipt_details.LoginToken.Establishment.owner:
|
||||||
receipt: PDFReceipt = PDFReceipt.getPDFReceiptFromFile(PDFDir + f"{receipt_details.id}.pdf")
|
receipt: PDFReceipt = PDFReceipt.getPDFReceiptFromFile(PDFDir + f"/{receipt_details.id}.pdf")
|
||||||
form: CheckItemsForm = CheckItemsForm.new(receipt.items)
|
form: CheckItemsForm = CheckItemsForm.new(receipt.items)
|
||||||
_template = CheckCustomItemsEntryForm(prefix="custom_items-_-")
|
_template = CheckCustomItemsEntryForm(prefix="custom_items-_-")
|
||||||
# TODO: Precheck if items are already in database. If yes, check if item is present only once or multiple
|
# TODO: Precheck if items are already in database. If yes, check if item is present only once or multiple
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from flask_wtf.file import FileAllowed, FileField, FileRequired
|
from flask_wtf.file import FileAllowed, FileField
|
||||||
from wtforms import DateField, SelectField, SubmitField
|
from wtforms import DateField, SelectField, SubmitField
|
||||||
from models import Establishment
|
from models import Establishment
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from flask import abort, redirect, request, url_for
|
from flask import abort, current_app, redirect, request, url_for
|
||||||
from flask_login import current_user, login_required
|
from flask_login import current_user, login_required
|
||||||
from os import rename
|
from os import rename
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
@ -10,11 +10,11 @@ from models.login_token import LoginToken
|
|||||||
from src.utils.modules.receipt_parser.pdf_receipt_parser import PDFReceipt
|
from src.utils.modules.receipt_parser.pdf_receipt_parser import PDFReceipt
|
||||||
from src.utils.routes_utils import render_custom_template as render_template
|
from src.utils.routes_utils import render_custom_template as render_template
|
||||||
|
|
||||||
PDFDir = "./"
|
|
||||||
@bp.route('/<int:establishment>', methods=['GET', 'POST'])
|
@bp.route('/<int:establishment>', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def upload_receipt(establishment: int):
|
def upload_receipt(establishment: int):
|
||||||
"""Upload of a receipt."""
|
"""Upload of a receipt."""
|
||||||
|
PDFDir: str = current_app.config["RECEIPT_FOLDER"]
|
||||||
if current_user.is_anonymous:
|
if current_user.is_anonymous:
|
||||||
abort(403)
|
abort(403)
|
||||||
if LoginToken.query.filter_by(establishment=establishment, user=current_user.id).first():
|
if LoginToken.query.filter_by(establishment=establishment, user=current_user.id).first():
|
||||||
@ -39,7 +39,7 @@ def upload_receipt(establishment: int):
|
|||||||
db.session.add(dbReceipt)
|
db.session.add(dbReceipt)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
if pdfReceipt:
|
if pdfReceipt:
|
||||||
rename(f"{PDFDir}/temp.pdf", f"{PDFDir}{secure_filename(f'{dbReceipt.id}.pdf')}")
|
rename(f"{PDFDir}/temp.pdf", f"{PDFDir}/{secure_filename(f'{dbReceipt.id}.pdf')}")
|
||||||
LOGGER.debug(receipt.words)
|
LOGGER.debug(receipt.words)
|
||||||
return redirect(url_for("receipts.check_items.confirm_receipt_items", receipt_id = dbReceipt.id))
|
return redirect(url_for("receipts.check_items.confirm_receipt_items", receipt_id = dbReceipt.id))
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import fitz
|
import fitz
|
||||||
from datetime import datetime, date
|
from datetime import date
|
||||||
from .edeka.edeka_parser import getDictFromWords as edekaparser
|
from .edeka.edeka_parser import getDictFromWords as edekaparser
|
||||||
from .kaufland.kaufland_parser import getDictFromWords as kauflandparser
|
from .kaufland.kaufland_parser import getDictFromWords as kauflandparser
|
||||||
from re import search
|
from re import search
|
||||||
@ -27,7 +27,7 @@ class PDFReceipt:
|
|||||||
with fitz.open(file, filetype="pdf") as doc:
|
with fitz.open(file, filetype="pdf") as doc:
|
||||||
words = []
|
words = []
|
||||||
for page in doc:
|
for page in doc:
|
||||||
words.extend(page.get_text("words", textpage=page.get_textpage_ocr(), sort=True))
|
words.extend(page.get_text("words", textpage=page.get_textpage_ocr(language = 'deu'), sort=True))
|
||||||
return words
|
return words
|
||||||
|
|
||||||
def _getStoreName(words: list[tuple]) -> str:
|
def _getStoreName(words: list[tuple]) -> str:
|
||||||
@ -36,22 +36,6 @@ class PDFReceipt:
|
|||||||
return word[4].lower()
|
return word[4].lower()
|
||||||
return "unknown"
|
return "unknown"
|
||||||
|
|
||||||
def _getItemsTextFromText(text, start="", end=""):
|
|
||||||
return text[text.index(start)+len(start):text.index(end)].strip()
|
|
||||||
|
|
||||||
def _convertItemsTextToDict(text):
|
|
||||||
temp = text.split("\n")
|
|
||||||
resultsArr = []
|
|
||||||
i = 0
|
|
||||||
while i < len(temp):
|
|
||||||
if search("(\d+) x", temp[i]):
|
|
||||||
resultsArr.append({"itemname": temp[i+2], "price": temp[i+1], "amount": temp[i][:-2]})
|
|
||||||
i += 4
|
|
||||||
else:
|
|
||||||
resultsArr.append({"itemname": temp[i], "price": temp[i+1][:-2]})
|
|
||||||
i += 2
|
|
||||||
return resultsArr
|
|
||||||
|
|
||||||
def _getInfosFromText(words: str, store: str = "edeka"):
|
def _getInfosFromText(words: str, store: str = "edeka"):
|
||||||
if store == "edeka":
|
if store == "edeka":
|
||||||
result = edekaparser(words)
|
result = edekaparser(words)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user