Fix currency formatting and app structure. Add Woodpecker CI badge to README.
This commit is contained in:
parent
7e77951596
commit
1fbc281245
|
@ -1,5 +1,7 @@
|
||||||
# Project Ploughshares
|
# Project Ploughshares
|
||||||
|
|
||||||
|
[](https://woodpecker.nixc.us/repos/265)
|
||||||
|
|
||||||
Transaction Management System for Project Ploughshares.
|
Transaction Management System for Project Ploughshares.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
|
@ -5,7 +5,14 @@ from flask import Flask, render_template, request, redirect, url_for, flash, jso
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import locale
|
import locale
|
||||||
import decimal
|
import logging
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger('ploughshares')
|
||||||
|
|
||||||
# Get version from environment variable or VERSION file
|
# Get version from environment variable or VERSION file
|
||||||
VERSION = os.environ.get('APP_VERSION')
|
VERSION = os.environ.get('APP_VERSION')
|
||||||
|
@ -37,23 +44,12 @@ def currency_filter(value):
|
||||||
if value is None:
|
if value is None:
|
||||||
return "$0.00"
|
return "$0.00"
|
||||||
try:
|
try:
|
||||||
# Convert to Decimal for precise handling
|
# Convert to float first, then format to exactly 2 decimal places
|
||||||
if isinstance(value, str):
|
float_value = float(value)
|
||||||
value = decimal.Decimal(value.replace('$', '').replace(',', ''))
|
return f"${float_value:,.2f}"
|
||||||
else:
|
except (ValueError, TypeError):
|
||||||
value = decimal.Decimal(str(value))
|
# Fallback formatting
|
||||||
|
return "$0.00"
|
||||||
# Format with 2 decimal places
|
|
||||||
value = value.quantize(decimal.Decimal('0.01'), rounding=decimal.ROUND_HALF_UP)
|
|
||||||
return locale.currency(float(value), grouping=True)
|
|
||||||
except (ValueError, TypeError, decimal.InvalidOperation, locale.Error):
|
|
||||||
# Fallback formatting if locale doesn't work
|
|
||||||
try:
|
|
||||||
if isinstance(value, str):
|
|
||||||
value = float(value.replace('$', '').replace(',', ''))
|
|
||||||
return f"${float(value):,.2f}"
|
|
||||||
except:
|
|
||||||
return "$0.00"
|
|
||||||
|
|
||||||
# --- Database Connection ---
|
# --- Database Connection ---
|
||||||
def get_db_connection():
|
def get_db_connection():
|
||||||
|
@ -73,10 +69,10 @@ def get_db_connection():
|
||||||
cursor_factory=RealDictCursor
|
cursor_factory=RealDictCursor
|
||||||
)
|
)
|
||||||
conn.autocommit = True
|
conn.autocommit = True
|
||||||
print(f"Ploughshares v{VERSION} - Connected to PostgreSQL at {host}:{port}")
|
logger.info(f"Ploughshares v{VERSION} - Connected to PostgreSQL at {host}:{port}")
|
||||||
return conn
|
return conn
|
||||||
except psycopg2.OperationalError as e:
|
except psycopg2.OperationalError as e:
|
||||||
print(f"Error connecting to PostgreSQL: {e}")
|
logger.error(f"Error connecting to PostgreSQL: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# --- Routes ---
|
# --- Routes ---
|
||||||
|
@ -87,19 +83,17 @@ def index():
|
||||||
flash("Database connection error", "error")
|
flash("Database connection error", "error")
|
||||||
return render_template('index.html', transactions=[], version=VERSION)
|
return render_template('index.html', transactions=[], version=VERSION)
|
||||||
|
|
||||||
with conn.cursor() as cur:
|
try:
|
||||||
cur.execute('SELECT * FROM transactions ORDER BY id DESC')
|
with conn.cursor() as cur:
|
||||||
transactions = cur.fetchall()
|
cur.execute('SELECT * FROM transactions ORDER BY id DESC')
|
||||||
|
transactions = cur.fetchall()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Database error: {e}")
|
||||||
|
flash(f"Database error: {e}", "error")
|
||||||
|
transactions = []
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
# Ensure amount is properly formatted for the template
|
|
||||||
for transaction in transactions:
|
|
||||||
if 'amount' in transaction and transaction['amount'] is not None:
|
|
||||||
# Store the numeric value for sorting
|
|
||||||
transaction['amount_raw'] = float(transaction['amount'])
|
|
||||||
else:
|
|
||||||
transaction['amount_raw'] = 0.0
|
|
||||||
|
|
||||||
conn.close()
|
|
||||||
return render_template('index.html', transactions=transactions, version=VERSION)
|
return render_template('index.html', transactions=transactions, version=VERSION)
|
||||||
|
|
||||||
@app.route('/api-docs')
|
@app.route('/api-docs')
|
||||||
|
@ -110,12 +104,24 @@ def api_docs():
|
||||||
@app.route('/transaction/<int:id>')
|
@app.route('/transaction/<int:id>')
|
||||||
def view_transaction(id):
|
def view_transaction(id):
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
with conn.cursor() as cur:
|
if conn is None:
|
||||||
cur.execute('SELECT * FROM transactions WHERE id = %s', (id,))
|
flash("Database connection error", "error")
|
||||||
transaction = cur.fetchone()
|
abort(404)
|
||||||
conn.close()
|
|
||||||
|
transaction = None
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute('SELECT * FROM transactions WHERE id = %s', (id,))
|
||||||
|
transaction = cur.fetchone()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Database error: {e}")
|
||||||
|
flash(f"Database error: {e}", "error")
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
if transaction is None:
|
if transaction is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
return render_template('view_transaction.html', transaction=transaction, version=VERSION)
|
return render_template('view_transaction.html', transaction=transaction, version=VERSION)
|
||||||
|
|
||||||
@app.route('/transaction/add', methods=['GET', 'POST'])
|
@app.route('/transaction/add', methods=['GET', 'POST'])
|
||||||
|
@ -148,25 +154,41 @@ def create_transaction():
|
||||||
data['amount'] = 0.0
|
data['amount'] = 0.0
|
||||||
|
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
with conn.cursor() as cur:
|
if conn is None:
|
||||||
cur.execute(
|
flash("Database connection error", "error")
|
||||||
"""
|
return render_template('transaction_form.html', version=VERSION)
|
||||||
INSERT INTO transactions (
|
|
||||||
transaction_type, company_division, address_1, address_2,
|
try:
|
||||||
city, province, region, postal_code, is_primary, source_date, source_description,
|
with conn.cursor() as cur:
|
||||||
grant_type, description, amount, recipient, commodity_class, contract_number, comments
|
cur.execute(
|
||||||
) VALUES (
|
"""
|
||||||
%(transaction_type)s, %(company_division)s, %(address_1)s, %(address_2)s,
|
INSERT INTO transactions (
|
||||||
%(city)s, %(province)s, %(region)s, %(postal_code)s, %(is_primary)s, %(source_date)s,
|
transaction_type, company_division, address_1, address_2,
|
||||||
%(source_description)s, %(grant_type)s, %(description)s, %(amount)s, %(recipient)s,
|
city, province, region, postal_code, is_primary, source_date, source_description,
|
||||||
%(commodity_class)s, %(contract_number)s, %(comments)s
|
grant_type, description, amount, recipient, commodity_class, contract_number, comments
|
||||||
) RETURNING id
|
) VALUES (
|
||||||
""",
|
%(transaction_type)s, %(company_division)s, %(address_1)s, %(address_2)s,
|
||||||
data
|
%(city)s, %(province)s, %(region)s, %(postal_code)s, %(is_primary)s, %(source_date)s,
|
||||||
)
|
%(source_description)s, %(grant_type)s, %(description)s, %(amount)s, %(recipient)s,
|
||||||
new_transaction_id = cur.fetchone()['id']
|
%(commodity_class)s, %(contract_number)s, %(comments)s
|
||||||
conn.commit()
|
) RETURNING id
|
||||||
conn.close()
|
""",
|
||||||
|
data
|
||||||
|
)
|
||||||
|
result = cur.fetchone()
|
||||||
|
if result and 'id' in result:
|
||||||
|
new_transaction_id = result['id']
|
||||||
|
conn.commit()
|
||||||
|
else:
|
||||||
|
flash("Error creating transaction: Could not get transaction ID", "error")
|
||||||
|
return render_template('transaction_form.html', version=VERSION)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error creating transaction: {e}")
|
||||||
|
flash(f"Error creating transaction: {e}", "error")
|
||||||
|
return render_template('transaction_form.html', version=VERSION)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
return redirect(url_for('view_transaction', id=new_transaction_id))
|
return redirect(url_for('view_transaction', id=new_transaction_id))
|
||||||
|
|
||||||
return render_template('transaction_form.html', version=VERSION)
|
return render_template('transaction_form.html', version=VERSION)
|
||||||
|
@ -174,6 +196,10 @@ def create_transaction():
|
||||||
@app.route('/transaction/<int:id>/edit', methods=['GET', 'POST'])
|
@app.route('/transaction/<int:id>/edit', methods=['GET', 'POST'])
|
||||||
def update_transaction(id):
|
def update_transaction(id):
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
|
if conn is None:
|
||||||
|
flash("Database connection error", "error")
|
||||||
|
abort(404)
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
# Get form data and set defaults for missing fields
|
# Get form data and set defaults for missing fields
|
||||||
data = request.form.to_dict()
|
data = request.form.to_dict()
|
||||||
|
@ -202,64 +228,86 @@ def update_transaction(id):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
data['amount'] = 0.0
|
data['amount'] = 0.0
|
||||||
|
|
||||||
with conn.cursor() as cur:
|
try:
|
||||||
cur.execute(
|
with conn.cursor() as cur:
|
||||||
"""
|
cur.execute(
|
||||||
UPDATE transactions
|
"""
|
||||||
SET transaction_type = %(transaction_type)s,
|
UPDATE transactions
|
||||||
company_division = %(company_division)s,
|
SET transaction_type = %(transaction_type)s,
|
||||||
address_1 = %(address_1)s,
|
company_division = %(company_division)s,
|
||||||
address_2 = %(address_2)s,
|
address_1 = %(address_1)s,
|
||||||
city = %(city)s,
|
address_2 = %(address_2)s,
|
||||||
province = %(province)s,
|
city = %(city)s,
|
||||||
region = %(region)s,
|
province = %(province)s,
|
||||||
postal_code = %(postal_code)s,
|
region = %(region)s,
|
||||||
is_primary = %(is_primary)s,
|
postal_code = %(postal_code)s,
|
||||||
source_date = %(source_date)s,
|
is_primary = %(is_primary)s,
|
||||||
source_description = %(source_description)s,
|
source_date = %(source_date)s,
|
||||||
grant_type = %(grant_type)s,
|
source_description = %(source_description)s,
|
||||||
description = %(description)s,
|
grant_type = %(grant_type)s,
|
||||||
amount = %(amount)s,
|
description = %(description)s,
|
||||||
recipient = %(recipient)s,
|
amount = %(amount)s,
|
||||||
commodity_class = %(commodity_class)s,
|
recipient = %(recipient)s,
|
||||||
contract_number = %(contract_number)s,
|
commodity_class = %(commodity_class)s,
|
||||||
comments = %(comments)s
|
contract_number = %(contract_number)s,
|
||||||
WHERE id = %(id)s
|
comments = %(comments)s
|
||||||
""",
|
WHERE id = %(id)s
|
||||||
data
|
""",
|
||||||
)
|
data
|
||||||
conn.commit()
|
)
|
||||||
conn.close()
|
conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error updating transaction: {e}")
|
||||||
|
flash(f"Error updating transaction: {e}", "error")
|
||||||
|
return render_template('transaction_form.html', transaction=data, version=VERSION)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
return redirect(url_for('view_transaction', id=id))
|
return redirect(url_for('view_transaction', id=id))
|
||||||
|
|
||||||
with conn.cursor() as cur:
|
transaction = None
|
||||||
cur.execute('SELECT * FROM transactions WHERE id = %s', (id,))
|
try:
|
||||||
transaction = cur.fetchone()
|
with conn.cursor() as cur:
|
||||||
conn.close()
|
cur.execute('SELECT * FROM transactions WHERE id = %s', (id,))
|
||||||
|
transaction = cur.fetchone()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error retrieving transaction: {e}")
|
||||||
|
flash(f"Error retrieving transaction: {e}", "error")
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
if transaction is None:
|
if transaction is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
return render_template('transaction_form.html', transaction=transaction, version=VERSION)
|
return render_template('transaction_form.html', transaction=transaction, version=VERSION)
|
||||||
|
|
||||||
def bootstrap_database():
|
def bootstrap_database():
|
||||||
"""
|
"""
|
||||||
Checks if the database is empty. Test data can be loaded separately using the tests/generate_test_data.py script.
|
Checks if the database is empty. Test data can be loaded separately using the tests/generate_test_data.py script.
|
||||||
"""
|
"""
|
||||||
print(f"Ploughshares v{VERSION} - Checking database for existing data...")
|
logger.info(f"Ploughshares v{VERSION} - Checking database for existing data...")
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
if conn is None:
|
if conn is None:
|
||||||
print("Database connection failed. Exiting.")
|
logger.error("Database connection failed. Exiting.")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
with conn.cursor() as cur:
|
try:
|
||||||
cur.execute("SELECT COUNT(*) FROM transactions")
|
with conn.cursor() as cur:
|
||||||
count = cur.fetchone()['count']
|
cur.execute("SELECT COUNT(*) FROM transactions")
|
||||||
if count == 0:
|
result = cur.fetchone()
|
||||||
print(f"Ploughshares v{VERSION} - Database is empty. You can populate it with test data using the tests/generate_test_data.py script.")
|
if result and 'count' in result:
|
||||||
conn.close()
|
count = result['count']
|
||||||
|
if count == 0:
|
||||||
|
logger.info(f"Ploughshares v{VERSION} - Database is empty. You can populate it with test data using the tests/generate_test_data.py script.")
|
||||||
|
else:
|
||||||
|
logger.error("Could not get count from database")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error checking database: {e}")
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
print(f"Starting Ploughshares v{VERSION}")
|
logger.info(f"Starting Ploughshares v{VERSION}")
|
||||||
bootstrap_database()
|
bootstrap_database()
|
||||||
port = int(os.environ.get('FLASK_RUN_PORT', 5001))
|
port = int(os.environ.get('FLASK_RUN_PORT', 5001))
|
||||||
app.run(host='0.0.0.0', port=port)
|
app.run(host='0.0.0.0', port=port)
|
|
@ -1,220 +0,0 @@
|
||||||
import os
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, send_from_directory, abort
|
|
||||||
from werkzeug.utils import secure_filename
|
|
||||||
from datetime import datetime
|
|
||||||
import locale
|
|
||||||
|
|
||||||
# Get version from environment variable or VERSION file
|
|
||||||
VERSION = os.environ.get('APP_VERSION')
|
|
||||||
if not VERSION:
|
|
||||||
try:
|
|
||||||
with open(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'VERSION'), 'r') as f:
|
|
||||||
VERSION = f.read().strip()
|
|
||||||
except:
|
|
||||||
VERSION = "unknown"
|
|
||||||
|
|
||||||
# Initialize the Flask app
|
|
||||||
app = Flask(__name__)
|
|
||||||
app.secret_key = 'supersecretkey'
|
|
||||||
app.config['UPLOAD_FOLDER'] = 'uploads'
|
|
||||||
app.config['VERSION'] = VERSION
|
|
||||||
|
|
||||||
# Set locale for currency formatting
|
|
||||||
try:
|
|
||||||
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
|
|
||||||
except locale.Error:
|
|
||||||
try:
|
|
||||||
locale.setlocale(locale.LC_ALL, '') # Use system default locale
|
|
||||||
except locale.Error:
|
|
||||||
pass # If all fails, we'll use the fallback in the filter
|
|
||||||
|
|
||||||
# Custom filter for currency formatting
|
|
||||||
@app.template_filter('currency')
|
|
||||||
def currency_filter(value):
|
|
||||||
if value is None:
|
|
||||||
return "$0.00"
|
|
||||||
try:
|
|
||||||
return locale.currency(float(value), grouping=True)
|
|
||||||
except (ValueError, TypeError, locale.Error):
|
|
||||||
# Fallback formatting if locale doesn't work
|
|
||||||
return f"${float(value):,.2f}"
|
|
||||||
|
|
||||||
# --- Database Connection ---
|
|
||||||
def get_db_connection():
|
|
||||||
host = os.environ.get('POSTGRES_HOST', 'db')
|
|
||||||
port = os.environ.get('POSTGRES_PORT', '5432')
|
|
||||||
dbname = os.environ.get('POSTGRES_DB', 'ploughshares')
|
|
||||||
user = os.environ.get('POSTGRES_USER', 'ploughshares')
|
|
||||||
password = os.environ.get('POSTGRES_PASSWORD', 'ploughshares_password')
|
|
||||||
|
|
||||||
try:
|
|
||||||
conn = psycopg2.connect(
|
|
||||||
host=host,
|
|
||||||
port=port,
|
|
||||||
dbname=dbname,
|
|
||||||
user=user,
|
|
||||||
password=password,
|
|
||||||
cursor_factory=RealDictCursor
|
|
||||||
)
|
|
||||||
conn.autocommit = True
|
|
||||||
print(f"Ploughshares v{VERSION} - Connected to PostgreSQL at {host}:{port}")
|
|
||||||
return conn
|
|
||||||
except psycopg2.OperationalError as e:
|
|
||||||
print(f"Error connecting to PostgreSQL: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# --- Routes ---
|
|
||||||
@app.route('/')
|
|
||||||
def index():
|
|
||||||
conn = get_db_connection()
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute('SELECT * FROM transactions ORDER BY id DESC')
|
|
||||||
transactions = cur.fetchall()
|
|
||||||
conn.close()
|
|
||||||
return render_template('index.html', transactions=transactions, version=VERSION)
|
|
||||||
|
|
||||||
@app.route('/api-docs')
|
|
||||||
def api_docs():
|
|
||||||
server_name = request.host
|
|
||||||
return render_template('api_docs.html', server_name=server_name, version=VERSION)
|
|
||||||
|
|
||||||
@app.route('/transaction/<int:id>')
|
|
||||||
def view_transaction(id):
|
|
||||||
conn = get_db_connection()
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute('SELECT * FROM transactions WHERE id = %s', (id,))
|
|
||||||
transaction = cur.fetchone()
|
|
||||||
conn.close()
|
|
||||||
if transaction is None:
|
|
||||||
abort(404)
|
|
||||||
return render_template('view_transaction.html', transaction=transaction, version=VERSION)
|
|
||||||
|
|
||||||
@app.route('/transaction/add', methods=['GET', 'POST'])
|
|
||||||
def create_transaction():
|
|
||||||
if request.method == 'POST':
|
|
||||||
# Get form data and set defaults for missing fields
|
|
||||||
data = request.form.to_dict()
|
|
||||||
default_fields = {
|
|
||||||
'transaction_type': '', 'company_division': '', 'address_1': '', 'address_2': '',
|
|
||||||
'city': '', 'province': '', 'region': '', 'postal_code': '', 'source_date': None,
|
|
||||||
'source_description': '', 'grant_type': '', 'description': '', 'amount': 0,
|
|
||||||
'recipient': '', 'commodity_class': '', 'contract_number': '', 'comments': ''
|
|
||||||
}
|
|
||||||
|
|
||||||
# Fill in missing fields with defaults
|
|
||||||
for field, default in default_fields.items():
|
|
||||||
if field not in data:
|
|
||||||
data[field] = default
|
|
||||||
|
|
||||||
# Handle checkbox fields
|
|
||||||
data['is_primary'] = 'is_primary' in data
|
|
||||||
|
|
||||||
conn = get_db_connection()
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO transactions (
|
|
||||||
transaction_type, company_division, address_1, address_2,
|
|
||||||
city, province, region, postal_code, is_primary, source_date, source_description,
|
|
||||||
grant_type, description, amount, recipient, commodity_class, contract_number, comments
|
|
||||||
) VALUES (
|
|
||||||
%(transaction_type)s, %(company_division)s, %(address_1)s, %(address_2)s,
|
|
||||||
%(city)s, %(province)s, %(region)s, %(postal_code)s, %(is_primary)s, %(source_date)s,
|
|
||||||
%(source_description)s, %(grant_type)s, %(description)s, %(amount)s, %(recipient)s,
|
|
||||||
%(commodity_class)s, %(contract_number)s, %(comments)s
|
|
||||||
) RETURNING id
|
|
||||||
""",
|
|
||||||
data
|
|
||||||
)
|
|
||||||
new_transaction_id = cur.fetchone()['id']
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
return redirect(url_for('view_transaction', id=new_transaction_id))
|
|
||||||
|
|
||||||
return render_template('transaction_form.html', version=VERSION)
|
|
||||||
|
|
||||||
@app.route('/transaction/<int:id>/edit', methods=['GET', 'POST'])
|
|
||||||
def update_transaction(id):
|
|
||||||
conn = get_db_connection()
|
|
||||||
if request.method == 'POST':
|
|
||||||
# Get form data and set defaults for missing fields
|
|
||||||
data = request.form.to_dict()
|
|
||||||
default_fields = {
|
|
||||||
'transaction_type': '', 'company_division': '', 'address_1': '', 'address_2': '',
|
|
||||||
'city': '', 'province': '', 'region': '', 'postal_code': '', 'source_date': None,
|
|
||||||
'source_description': '', 'grant_type': '', 'description': '', 'amount': 0,
|
|
||||||
'recipient': '', 'commodity_class': '', 'contract_number': '', 'comments': ''
|
|
||||||
}
|
|
||||||
|
|
||||||
# Fill in missing fields with defaults
|
|
||||||
for field, default in default_fields.items():
|
|
||||||
if field not in data:
|
|
||||||
data[field] = default
|
|
||||||
|
|
||||||
# Handle checkbox fields
|
|
||||||
data['is_primary'] = 'is_primary' in data
|
|
||||||
data['id'] = id
|
|
||||||
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute(
|
|
||||||
"""
|
|
||||||
UPDATE transactions
|
|
||||||
SET transaction_type = %(transaction_type)s,
|
|
||||||
company_division = %(company_division)s,
|
|
||||||
address_1 = %(address_1)s,
|
|
||||||
address_2 = %(address_2)s,
|
|
||||||
city = %(city)s,
|
|
||||||
province = %(province)s,
|
|
||||||
region = %(region)s,
|
|
||||||
postal_code = %(postal_code)s,
|
|
||||||
is_primary = %(is_primary)s,
|
|
||||||
source_date = %(source_date)s,
|
|
||||||
source_description = %(source_description)s,
|
|
||||||
grant_type = %(grant_type)s,
|
|
||||||
description = %(description)s,
|
|
||||||
amount = %(amount)s,
|
|
||||||
recipient = %(recipient)s,
|
|
||||||
commodity_class = %(commodity_class)s,
|
|
||||||
contract_number = %(contract_number)s,
|
|
||||||
comments = %(comments)s
|
|
||||||
WHERE id = %(id)s
|
|
||||||
""",
|
|
||||||
data
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
return redirect(url_for('view_transaction', id=id))
|
|
||||||
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute('SELECT * FROM transactions WHERE id = %s', (id,))
|
|
||||||
transaction = cur.fetchone()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if transaction is None:
|
|
||||||
abort(404)
|
|
||||||
return render_template('transaction_form.html', transaction=transaction, version=VERSION)
|
|
||||||
|
|
||||||
def bootstrap_database():
|
|
||||||
"""
|
|
||||||
Checks if the database is empty. Test data can be loaded separately using the tests/generate_test_data.py script.
|
|
||||||
"""
|
|
||||||
print(f"Ploughshares v{VERSION} - Checking database for existing data...")
|
|
||||||
conn = get_db_connection()
|
|
||||||
if conn is None:
|
|
||||||
print("Database connection failed. Exiting.")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute("SELECT COUNT(*) FROM transactions")
|
|
||||||
count = cur.fetchone()['count']
|
|
||||||
if count == 0:
|
|
||||||
print(f"Ploughshares v{VERSION} - Database is empty. You can populate it with test data using the tests/generate_test_data.py script.")
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
print(f"Starting Ploughshares v{VERSION}")
|
|
||||||
bootstrap_database()
|
|
||||||
port = int(os.environ.get('FLASK_RUN_PORT', 5001))
|
|
||||||
app.run(host='0.0.0.0', port=port)
|
|
|
@ -3,28 +3,27 @@ DROP TABLE IF EXISTS transaction_documents;
|
||||||
DROP TABLE IF EXISTS transactions;
|
DROP TABLE IF EXISTS transactions;
|
||||||
|
|
||||||
-- Create transactions table
|
-- Create transactions table
|
||||||
CREATE TABLE transactions (
|
CREATE TABLE IF NOT EXISTS transactions (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
transaction_no SERIAL UNIQUE,
|
transaction_type VARCHAR(255),
|
||||||
transaction_type VARCHAR(50),
|
company_division VARCHAR(255),
|
||||||
company_division VARCHAR(100),
|
|
||||||
address_1 VARCHAR(255),
|
address_1 VARCHAR(255),
|
||||||
address_2 VARCHAR(255),
|
address_2 VARCHAR(255),
|
||||||
city VARCHAR(100),
|
city VARCHAR(255),
|
||||||
province VARCHAR(100),
|
province VARCHAR(255),
|
||||||
region VARCHAR(100),
|
region VARCHAR(255),
|
||||||
postal_code VARCHAR(20),
|
postal_code VARCHAR(50),
|
||||||
is_primary BOOLEAN,
|
is_primary BOOLEAN DEFAULT FALSE,
|
||||||
source_date DATE,
|
source_date DATE,
|
||||||
source_description TEXT,
|
source_description TEXT,
|
||||||
grant_type VARCHAR(100),
|
grant_type VARCHAR(255),
|
||||||
description TEXT,
|
description TEXT,
|
||||||
amount NUMERIC(15, 2),
|
amount NUMERIC(15, 2),
|
||||||
recipient VARCHAR(255),
|
recipient VARCHAR(255),
|
||||||
commodity_class VARCHAR(100),
|
commodity_class VARCHAR(255),
|
||||||
contract_number VARCHAR(100),
|
contract_number VARCHAR(255),
|
||||||
comments TEXT,
|
comments TEXT,
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Create table for document attachments
|
-- Create table for document attachments
|
||||||
|
@ -40,6 +39,7 @@ CREATE TABLE transaction_documents (
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Create indexes for better performance
|
-- Create indexes for better performance
|
||||||
CREATE INDEX idx_transaction_no ON transactions(transaction_no);
|
CREATE INDEX IF NOT EXISTS idx_transactions_type ON transactions(transaction_type);
|
||||||
CREATE INDEX idx_transaction_date ON transactions(source_date);
|
CREATE INDEX IF NOT EXISTS idx_transactions_division ON transactions(company_division);
|
||||||
CREATE INDEX idx_transaction_documents ON transaction_documents(transaction_id);
|
CREATE INDEX IF NOT EXISTS idx_transactions_recipient ON transactions(recipient);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_transaction_date ON transactions(source_date);
|
|
@ -28,7 +28,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for transaction in transactions %}
|
{% for transaction in transactions %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ transaction['transaction_no'] }}</td>
|
<td>{{ transaction['id'] }}</td>
|
||||||
<td>{{ transaction['transaction_type'] }}</td>
|
<td>{{ transaction['transaction_type'] }}</td>
|
||||||
<td>{{ transaction['company_division'] }}</td>
|
<td>{{ transaction['company_division'] }}</td>
|
||||||
<td class="amount-cell"><span class="currency-value">{{ transaction['amount']|currency }}</span></td>
|
<td class="amount-cell"><span class="currency-value">{{ transaction['amount']|currency }}</span></td>
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Transaction {{ transaction.transaction_no }} - Project Ploughshares{% endblock %}
|
{% block title %}Transaction {{ transaction['id'] }} - Project Ploughshares{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid mt-4">
|
<div class="container-fluid mt-4">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<h2>Transaction #{{ transaction.transaction_no }} - Project Ploughshares</h2>
|
<h2>Transaction #{{ transaction['id'] }} - Project Ploughshares</h2>
|
||||||
<div>
|
<div>
|
||||||
<a href="{{ url_for('update_transaction', id=transaction.id) }}" class="btn btn-warning">
|
<a href="{{ url_for('update_transaction', id=transaction['id']) }}" class="btn btn-warning">
|
||||||
<i class="bi bi-pencil"></i> Edit
|
<i class="bi bi-pencil"></i> Edit
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('index') }}" class="btn btn-secondary">
|
<a href="{{ url_for('index') }}" class="btn btn-secondary">
|
||||||
|
@ -29,60 +29,60 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 20%;">Transaction Type:</th>
|
<th style="width: 20%;">Transaction Type:</th>
|
||||||
<td>{{ transaction.transaction_type }}</td>
|
<td>{{ transaction['transaction_type'] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Company/Division:</th>
|
<th>Company/Division:</th>
|
||||||
<td>{{ transaction.company_division }}</td>
|
<td>{{ transaction['company_division'] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Address:</th>
|
<th>Address:</th>
|
||||||
<td>
|
<td>
|
||||||
{{ transaction.address_1 }}<br>
|
{{ transaction['address_1'] }}<br>
|
||||||
{% if transaction.address_2 %}{{ transaction.address_2 }}<br>{% endif %}
|
{% if transaction['address_2'] %}{{ transaction['address_2'] }}<br>{% endif %}
|
||||||
{{ transaction.city }}, {{ transaction.province }}, {{ transaction.postal_code }}<br>
|
{{ transaction['city'] }}, {{ transaction['province'] }}, {{ transaction['postal_code'] }}<br>
|
||||||
{{ transaction.region }}
|
{{ transaction['region'] }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Primary:</th>
|
<th>Primary:</th>
|
||||||
<td>{{ 'Yes' if transaction.is_primary else 'No' }}</td>
|
<td>{{ 'Yes' if transaction['is_primary'] else 'No' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Source Date:</th>
|
<th>Source Date:</th>
|
||||||
<td>{{ transaction.source_date.strftime('%Y-%m-%d') if transaction.source_date else 'N/A' }}</td>
|
<td>{{ transaction['source_date'].strftime('%Y-%m-%d') if transaction['source_date'] else 'N/A' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Source Description:</th>
|
<th>Source Description:</th>
|
||||||
<td>{{ transaction.source_description }}</td>
|
<td>{{ transaction['source_description'] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Grant Type:</th>
|
<th>Grant Type:</th>
|
||||||
<td>{{ transaction.grant_type }}</td>
|
<td>{{ transaction['grant_type'] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Description:</th>
|
<th>Description:</th>
|
||||||
<td>{{ transaction.description }}</td>
|
<td>{{ transaction['description'] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Amount:</th>
|
<th>Amount:</th>
|
||||||
<td class="amount-cell"><span class="currency-value">{{ transaction.amount|currency }}</span></td>
|
<td class="amount-cell"><span class="currency-value">{{ transaction['amount']|currency }}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Recipient:</th>
|
<th>Recipient:</th>
|
||||||
<td>{{ transaction.recipient }}</td>
|
<td>{{ transaction['recipient'] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Commodity Class:</th>
|
<th>Commodity Class:</th>
|
||||||
<td>{{ transaction.commodity_class }}</td>
|
<td>{{ transaction['commodity_class'] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Contract Number:</th>
|
<th>Contract Number:</th>
|
||||||
<td>{{ transaction.contract_number }}</td>
|
<td>{{ transaction['contract_number'] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Comments:</th>
|
<th>Comments:</th>
|
||||||
<td>{{ transaction.comments }}</td>
|
<td>{{ transaction['comments'] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
Loading…
Reference in New Issue