From 0094428b72f392d8eb6bd269c52b4f36e67d91fe Mon Sep 17 00:00:00 2001 From: Dries Peeters Date: Sat, 29 Nov 2025 07:22:15 +0100 Subject: [PATCH] Update routes, services, and tests --- app/__init__.py | 10 +++++++++- app/routes/inventory.py | 4 ++-- app/routes/invoices_refactored.py | 1 + app/routes/projects.py | 2 +- app/routes/projects_refactored_example.py | 4 ++-- app/routes/quotes.py | 12 +++++++++++- app/services/ai_categorization_service.py | 2 +- tests/test_admin_settings_logo.py | 7 +++++-- tests/test_pdf_layout.py | 7 ++++++- tests/test_time_entry_duplication.py | 10 +++++++++- tests/test_uploads_persistence.py | 5 ++++- 11 files changed, 51 insertions(+), 13 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 07930f2..cad181a 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -632,7 +632,15 @@ def create_app(config=None): app.logger.warning(f"Failed to initialize PostHog: {e}") # Fail-fast on weak/missing secret in production - if not app.debug and app.config.get("FLASK_ENV", "production") == "production": + # Skip validation in testing or debug mode + is_testing = app.config.get("TESTING", False) + # Check both config and environment variable for FLASK_ENV + flask_env_config = app.config.get("FLASK_ENV") + flask_env_env = os.getenv("FLASK_ENV", "production") + flask_env = flask_env_config if flask_env_config else flask_env_env + is_production_env = flask_env == "production" and not is_testing + + if not app.debug and is_production_env: secret = app.config.get("SECRET_KEY") placeholder_values = { "dev-secret-key-change-in-production", diff --git a/app/routes/inventory.py b/app/routes/inventory.py index 610f6ba..fcd727a 100644 --- a/app/routes/inventory.py +++ b/app/routes/inventory.py @@ -882,7 +882,7 @@ def new_transfer(): reason = f"Transfer from {from_warehouse.code} to {to_warehouse.code}" # Create negative movement (from source warehouse) - out_movement, _ = StockMovement.record_movement( + out_movement, _unused = StockMovement.record_movement( movement_type="transfer", stock_item_id=stock_item_id, warehouse_id=from_warehouse_id, @@ -896,7 +896,7 @@ def new_transfer(): ) # Create positive movement (to destination warehouse) - in_movement, _ = StockMovement.record_movement( + in_movement, _unused = StockMovement.record_movement( movement_type="transfer", stock_item_id=stock_item_id, warehouse_id=to_warehouse_id, diff --git a/app/routes/invoices_refactored.py b/app/routes/invoices_refactored.py index f863d39..52140b8 100644 --- a/app/routes/invoices_refactored.py +++ b/app/routes/invoices_refactored.py @@ -14,6 +14,7 @@ from app import db, log_event, track_event from app.services import InvoiceService, ProjectService from app.repositories import InvoiceRepository, ProjectRepository from app.models import Invoice, Project, Settings +from app.utils.db import safe_commit from app.utils.api_responses import success_response, error_response, paginated_response from app.utils.event_bus import emit_event from app.constants import WebhookEvent, InvoiceStatus diff --git a/app/routes/projects.py b/app/routes/projects.py index c820fd3..573959a 100644 --- a/app/routes/projects.py +++ b/app/routes/projects.py @@ -349,7 +349,7 @@ def create_project(): entity_type="project", entity_id=project.id, entity_name=project.name, - description=f'Created project "{project.name}" for {client.name}', + description=f'Created project "{project.name}" for {project.client.name}', ip_address=request.remote_addr, user_agent=request.headers.get("User-Agent"), ) diff --git a/app/routes/projects_refactored_example.py b/app/routes/projects_refactored_example.py index 2c7c550..03ddb84 100644 --- a/app/routes/projects_refactored_example.py +++ b/app/routes/projects_refactored_example.py @@ -11,8 +11,8 @@ from flask_login import login_required, current_user from sqlalchemy.orm import joinedload from app import db from app.services import ProjectService -from app.repositories import ProjectRepository, ClientRepository -from app.models import Project, Client, UserFavoriteProject +from app.repositories import ProjectRepository, ClientRepository, TimeEntryRepository +from app.models import Project, Client, UserFavoriteProject, TimeEntry from app.utils.permissions import admin_or_permission_required projects_bp = Blueprint("projects", __name__) diff --git a/app/routes/quotes.py b/app/routes/quotes.py index ec4bf14..2031f9d 100644 --- a/app/routes/quotes.py +++ b/app/routes/quotes.py @@ -2,11 +2,12 @@ from flask import Blueprint, render_template, request, redirect, url_for, flash, from flask_babel import gettext as _ from flask_login import login_required, current_user from app import db, log_event, track_event -from app.models import Quote, QuoteItem, QuoteAttachment, Client, Project, Invoice +from app.models import Quote, QuoteItem, QuoteAttachment, Client, Project, Invoice, QuoteTemplate from datetime import datetime from decimal import Decimal, InvalidOperation from app.utils.db import safe_commit from app.utils.permissions import admin_or_permission_required, permission_required +from app.utils.config_manager import get_setting quotes_bp = Blueprint("quotes", __name__) @@ -61,6 +62,11 @@ def create_quote(): valid_until = request.form.get("valid_until", "").strip() notes = request.form.get("notes", "").strip() terms = request.form.get("terms", "").strip() + payment_terms = request.form.get("payment_terms", "").strip() + discount_type = request.form.get("discount_type", "").strip() + discount_amount = request.form.get("discount_amount", "").strip() + discount_reason = request.form.get("discount_reason", "").strip() + coupon_code = request.form.get("coupon_code", "").strip() try: current_app.logger.info( @@ -1084,6 +1090,10 @@ def create_template(): @admin_or_permission_required("create_quotes") def save_template_from_quote(template_id): """Save current quote as a template""" + quote_id = request.form.get("quote_id", type=int) + if not quote_id: + flash(_("Quote ID is required"), "error") + return redirect(url_for("quotes.list_templates")) quote = Quote.query.get_or_404(quote_id) # Check permissions diff --git a/app/services/ai_categorization_service.py b/app/services/ai_categorization_service.py index 4dd2d12..57d39c6 100644 --- a/app/services/ai_categorization_service.py +++ b/app/services/ai_categorization_service.py @@ -4,7 +4,7 @@ Uses pattern matching and heuristics (can be extended with actual AI APIs) """ from typing import Dict, List, Any, Optional -from datetime import datetime +from datetime import datetime, timedelta from app import db from app.models import TimeEntry, Project, Task, Client from sqlalchemy import func diff --git a/tests/test_admin_settings_logo.py b/tests/test_admin_settings_logo.py index b7d6458..b5dcbb7 100644 --- a/tests/test_admin_settings_logo.py +++ b/tests/test_admin_settings_logo.py @@ -23,8 +23,11 @@ def admin_user(app): @pytest.fixture def authenticated_admin_client(client, admin_user): """Create an authenticated admin client.""" - # Use the actual login endpoint to properly authenticate - client.post("/login", data={"username": admin_user.username}, follow_redirects=True) + from flask_login import login_user + + with client.session_transaction() as sess: + # Use Flask-Login's login_user directly for tests + login_user(admin_user) return client diff --git a/tests/test_pdf_layout.py b/tests/test_pdf_layout.py index 2377396..d74b56d 100644 --- a/tests/test_pdf_layout.py +++ b/tests/test_pdf_layout.py @@ -277,7 +277,7 @@ def test_pdf_generation_with_default_template(app, sample_invoice): @pytest.mark.smoke @pytest.mark.admin -def test_pdf_layout_navigation_link_exists(admin_authenticated_client): +def test_pdf_layout_navigation_link_exists(admin_authenticated_client, app): """Test that PDF layout link exists in admin navigation.""" # Access admin dashboard or any admin page response = admin_authenticated_client.get("/admin/settings") @@ -285,6 +285,11 @@ def test_pdf_layout_navigation_link_exists(admin_authenticated_client): assert response.status_code == 200 # Should contain link to PDF layout page # The link might be in the navigation or as a menu item + html = response.get_data(as_text=True) + # Check for PDF layout link - it's in a dropdown menu + with app.app_context(): + pdf_layout_url = url_for("admin.pdf_layout") + assert "admin.pdf_layout" in html or "pdf-layout" in html or "PDF Templates" in html or pdf_layout_url in html @pytest.mark.smoke diff --git a/tests/test_time_entry_duplication.py b/tests/test_time_entry_duplication.py index 6747b11..2c41480 100644 --- a/tests/test_time_entry_duplication.py +++ b/tests/test_time_entry_duplication.py @@ -7,6 +7,7 @@ previous time entries with pre-filled data. import pytest from datetime import datetime, timedelta +from flask import url_for from app import db from app.models import TimeEntry, User, Project, Task @@ -272,12 +273,19 @@ def test_admin_can_duplicate_any_entry(admin_authenticated_client, user, project def test_duplicate_button_on_dashboard(authenticated_client, time_entry_with_all_fields, app): """Smoke test: Duplicate button should appear on dashboard.""" with app.app_context(): + # Clear any cache that might affect the dashboard + from app.utils.cache import get_cache + cache = get_cache() + cache.delete(f"dashboard:{time_entry_with_all_fields.user_id}") + response = authenticated_client.get("/dashboard") assert response.status_code == 200 html = response.get_data(as_text=True) # Check for duplicate button/link (may use icon or text) - assert "fa-copy" in html or "duplicate" in html.lower() + # The button uses fa-copy icon and duplicate_timer route + duplicate_url = url_for("timer.duplicate_timer", timer_id=time_entry_with_all_fields.id) + assert "fa-copy" in html or "duplicate" in html.lower() or "duplicate_timer" in html or duplicate_url in html @pytest.mark.smoke diff --git a/tests/test_uploads_persistence.py b/tests/test_uploads_persistence.py index 1a7cf9f..cab1d8e 100644 --- a/tests/test_uploads_persistence.py +++ b/tests/test_uploads_persistence.py @@ -34,8 +34,11 @@ def admin_user(app): @pytest.fixture def authenticated_admin_client(client, admin_user): """Create an authenticated admin client.""" + from flask_login import login_user + with client.session_transaction() as sess: - sess["_user_id"] = str(admin_user.id) + # Use Flask-Login's login_user directly for tests + login_user(admin_user) return client