diff --git a/.gitignore b/.gitignore index c8fb93a..7b82e54 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,2 @@ **/__pycache__/ -instance/ -migrations/ static/ \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py index 02712ce..3eb4590 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -16,6 +16,3 @@ migrate.init_app(app,db) from app import models #.models import Vendor, LineItem, BudgetCategory from app import routes - -if __name__ == '__main__': - app.run(debug=True) diff --git a/app/models.py b/app/models.py index fc75980..262828e 100644 --- a/app/models.py +++ b/app/models.py @@ -3,18 +3,18 @@ from sqlalchemy import Column, INTEGER, String class Vendor(db.Model): __tablename__ = 'vendor' - id = Column('id', INTEGER(), primary_key=True) + id = Column('id', INTEGER(), primary_key=True, autoincrement=True) name = Column('name', String(), nullable=False) bc_id = Column('bc_id', INTEGER(), nullable=True) class BudgetCategory(db.Model): __tablename__ = 'budget_category' - id = Column('id', INTEGER(), primary_key=True) + id = Column('id', INTEGER(), primary_key=True, autoincrement=True) name = Column('name', String(), nullable=False) class LineItem(db.Model): __tablename__ = 'line_item' - id = Column('id', INTEGER(), primary_key=True) + id = Column('id', INTEGER(), primary_key=True, autoincrement=True) parent_line_item_id = Column('parent_line_item_id', INTEGER(), nullable=True) amount = Column('amount', INTEGER(), nullable=False) currency_type = Column('currency_type', String(), default='shekel') diff --git a/app/routes.py b/app/routes.py index e28f7c7..3b71427 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,8 +1,99 @@ -from app import app -from flask import render_template - +from app import app, db +from flask import render_template, redirect, url_for, request +from sqlalchemy import select, delete, update +from app.models import LineItem, BudgetCategory, Vendor +import datetime +import os +import openpyxl +import time @app.route('/') def home(): - return render_template('index.html') \ No newline at end of file + this_year = datetime.datetime.now().date().year + this_month = datetime.datetime.now().date().month + # line_item_dates = db.session.query(select(LineItem.date).order_by(LineItem.date)) + notes = db.session.execute(select(LineItem.note)).all() + vendors = db.session.execute(select(Vendor)).all() + budget_categories = db.session.execute(select(BudgetCategory)).all() + files = os.listdir('C:/Users/Lenovo/Desktop/BudgetingApp/app/static/uploadable') + return render_template('index.html', + files=files, + notes=notes, + vendors=vendors, + budget_categories=budget_categories) + +@app.route('/upload_file/') +def upload_file(filename): + + file_path = 'C:/Users/Lenovo/Desktop/BudgetingApp/app/static/uploadable/' + filename + xl = openpyxl.load_workbook(file_path, read_only=True) + wb = xl.worksheets[0] + + items_to_add = [] + + vendors = db.session.execute(select(Vendor.name)).all() + vendors_added = [vendor[0] for vendor in vendors] + + for line, row in enumerate(wb.rows): + row = [x.value for x in row] + if line == 0: + columns = {x: i for i,x in enumerate(row)} + continue + if row[columns['Charge']]: + amount=row[columns['Charge']] * -1 + else: + amount=row[columns['Deposit']] + + date = row[0] + date = time.mktime(date.timetuple()) + + if not row[columns['Vendor']] in vendors_added: + vendors_added.append(row[columns['Vendor']]) + vendor = Vendor( + name=row[columns['Vendor']] + ) + db.session.add(vendor) + db.session.commit() + + line_item = LineItem( + parent_line_item_id=None, + amount=amount, + currency_type='shekel', + vendor_id=None, + date=date, + confirmation_code=row[columns['Confirmation Code']], + note=row[columns['Note']] + ) + items_to_add.append(line_item) + + xl.close() + + db.session.add_all(items_to_add) + db.session.commit() + + # os.remove(file_path) + return redirect(url_for('home')) + +@app.route('/view_month//') +def view_month(year, month): + return render_template('view_month.html', + month=month, + year=year) + +@app.route('/add_budget_category', methods=['POST']) +def add_budget_category(): + if request.method == 'POST': + + category_name = request.form['category_name'] + bc = BudgetCategory(name=category_name) + db.session.add(bc) + db.session.commit() + return redirect(url_for('home')) + +@app.route('/delete_budget_category/', methods=['POST']) +def delete_budget_category(id): + db.session.execute(update(Vendor).where(Vendor.bc_id==id).values(bc_id=None)) + db.session.execute(delete(BudgetCategory).where(BudgetCategory.id==id)) + db.session.commit() + return {"status":'success'} \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html index cda7f5d..fce93cd 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1,11 +1,127 @@ - - - Budgeting + + + Budgeting + - +
+ {% if files %} +
+
+ File available in static folder for upload +
+
+
+ + + + + + + + {% for file in files %} + + + + {% endfor %} + +
Filename
{{file}}
+
+
+
+ {% else %} +
+
+ No files in the upload folder +
+
+ {% endif %} +
+
+ Budget Categories +
+
+ +
+
+ + +
+ +
+
+ + + + + + + + + + {% for bc in budget_categories %} + + + + + + {% endfor %} + +
IDNameDelete
{{bc[0].id}}{{bc[0].name}}
+
+
+
+
+
+ Vendors +
+
+
+ + + + + + + + + {% for vendor in vendors %} + + + + + {% endfor %} + +
IDName
{{vendor[0].id}}{{vendor[0].name}}
+
+
+
+
+
+ Notes +
+
+
+ + + {% for note in notes %} + {% if note[0] %} + + + + {% endif %} + {% endfor %} + +
{{note[0]}}
+
+
+
+
+ + + - \ No newline at end of file + diff --git a/app/templates/view_month.html b/app/templates/view_month.html new file mode 100644 index 0000000..780e1a7 --- /dev/null +++ b/app/templates/view_month.html @@ -0,0 +1,37 @@ + + + + + + Budgeting + + + +
+ {% if files %} +
+
+ {{month}} - {{year}} +
+
+
+ + + + + + + + + +
Line
+
+
+
+ {% endif %} +
+ + + + + diff --git a/instance/site.db b/instance/site.db new file mode 100644 index 0000000..132c548 Binary files /dev/null and b/instance/site.db differ diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/8960c74c29df_.py b/migrations/versions/8960c74c29df_.py new file mode 100644 index 0000000..71b40f1 --- /dev/null +++ b/migrations/versions/8960c74c29df_.py @@ -0,0 +1,51 @@ +"""empty message + +Revision ID: 8960c74c29df +Revises: +Create Date: 2024-09-29 08:46:22.567795 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '8960c74c29df' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('budget_category', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('line_item', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('parent_line_item_id', sa.INTEGER(), nullable=True), + sa.Column('amount', sa.INTEGER(), nullable=False), + sa.Column('currency_type', sa.String(), nullable=True), + sa.Column('vendor_id', sa.INTEGER(), nullable=True), + sa.Column('date', sa.INTEGER(), nullable=False), + sa.Column('confirmation_code', sa.INTEGER(), nullable=True), + sa.Column('note', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('vendor', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('bc_id', sa.INTEGER(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('vendor') + op.drop_table('line_item') + op.drop_table('budget_category') + # ### end Alembic commands ### diff --git a/pythonFiles/playground.py b/pythonFiles/playground.py new file mode 100644 index 0000000..8904127 --- /dev/null +++ b/pythonFiles/playground.py @@ -0,0 +1,67 @@ +import datetime +import openpyxl +import time +from sqlalchemy import create_engine, MetaData, Table +import os + +def engineer(): + path1 = 'C:/Users/Lenovo/Desktop/BudgetingApp/instance/site.db' + # path2 = '' + # path3 = '' + + for p in [path1]: + if os.path.exists(p): + path = p + break + engine = create_engine(f'sqlite:///{path}') + metadata_obj = MetaData() + metadata_obj.reflect(bind=engine) + return engine, metadata_obj + +def playground(): + engine, metadata_obj = engineer() + line_item_table = Table("line_item", metadata_obj, autoload_with=engine) + budget_category_table = Table("budget_category", metadata_obj, autoload_with=engine) + vendor_table = Table("vendor", metadata_obj, autoload_with=engine) + + # xl = openpyxl.load_workbook('C:/Users/Lenovo/Desktop/BudgetingApp/app/static/uploadable/Bulk_Line_Item_Upload.xlsx',read_only=True) + # wb = xl.worksheets[0] + # items_to_add = [] + # for line, row in enumerate(wb.rows): + # row = [x.value for x in row] + # if line == 0: + # columns = {x: i for i,x in enumerate(row)} + # continue + # if row[columns['Charge']]: + # amount=row[columns['Charge']] * -1 + # else: + # amount=row[columns['Deposit']] + # date = row[0] + # date = time.mktime(date.timetuple()) + # line_item = { + # 'parent_line_item_id':None, + # 'amount':amount, + # 'currency_type':'shekel', + # 'vendor_id':None, + # 'date':date, + # 'confirmation_code':row[columns['Confirmation Code']], + # 'note':row[columns['Note']] + # } + # items_to_add.append(line_item) + # print(len(items_to_add)) + # print(items_to_add[51]) + + # month = datetime.datetime.now().date().month + # year = datetime.datetime.now().date().year + # print(month) + # print(year) + # print(type(month)) + # print(type(year)) + # print(datetime.datetime.now(datetime.timezone.utc).timestamp()) + + # today = datetime.datetime.today().date() + # today = time.mktime(today.timetuple()) + # print(today) + +if __name__ == '__main__': + playground() \ No newline at end of file diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 0000000..0b835ac --- /dev/null +++ b/wsgi.py @@ -0,0 +1,5 @@ +from app import app + + +if __name__ == '__main__': + app.run(debug=True)