Compare commits

..

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

14 changed files with 40 additions and 90 deletions

View File

@ -4,47 +4,19 @@ on:
tags: tags:
- '*' - '*'
jobs: jobs:
release: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: https://gitea.com/actions/checkout@master - uses: actions/checkout@master
- name: Zip Artifacts - name: Archive Server
uses: https://github.com/thedoctor0/zip-release@master uses: 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: https://gitea.com/actions/gitea-release-action@v1 uses: ncipollo/release-action@v1
with: with:
server_url: https://gitea.wpgcommunity.net allowUpdates: true
files: 'server.zip' artifacts: "server.zip"
docker: token: ${{ secrets.GITHUB_TOKEN }}
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 }}

View File

@ -1,5 +1,5 @@
FROM python@sha256:c66cf219ac0083a9af2ff90e16530f16cd503c59eb7909feb3b8f3524dc1a87e FROM python@sha256:21c9f0b22213295a13bd678c5b45aa587ff6cb01cd99b6cf0e6928f4c777006b
# python:3.12.2-slim-bullseye (amd64) # python:3.11.4-slim-bullseye (arm/v7)
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 .

View File

@ -20,4 +20,3 @@ 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"

View File

@ -1,38 +0,0 @@
"""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 ###

View File

@ -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(255), nullable=False) password_hash = db.Column(db.String(128), 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",

View File

@ -38,8 +38,6 @@ 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)

View File

@ -10,4 +10,5 @@ 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)

View File

@ -73,4 +73,6 @@ 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

View File

@ -1,5 +1,5 @@
from datetime import date from datetime import date
from flask import abort, current_app, request, url_for from flask import abort, 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

View File

@ -1,5 +1,5 @@
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask_wtf.file import FileAllowed, FileField from flask_wtf.file import FileAllowed, FileField, FileRequired
from wtforms import DateField, SelectField, SubmitField from wtforms import DateField, SelectField, SubmitField
from models import Establishment from models import Establishment

View File

@ -1,4 +1,4 @@
from flask import abort, current_app, redirect, request, url_for from flask import abort, 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:

View File

@ -1,5 +1,5 @@
import fitz import fitz
from datetime import date from datetime import datetime, 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(language = 'deu'), sort=True)) words.extend(page.get_text("words", textpage=page.get_textpage_ocr(), sort=True))
return words return words
def _getStoreName(words: list[tuple]) -> str: def _getStoreName(words: list[tuple]) -> str:
@ -36,6 +36,22 @@ 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)