diff --git a/.env.local-test b/.env.local-test deleted file mode 100644 index 0e3c0452..00000000 --- a/.env.local-test +++ /dev/null @@ -1,36 +0,0 @@ -# Local Testing Environment Variables -# Copy this file to .env.local-test and modify as needed - -# Timezone (default: Europe/Brussels) -TZ=Europe/Brussels - -# Currency (default: EUR) -CURRENCY=EUR - -# Timer settings -ROUNDING_MINUTES=1 -SINGLE_ACTIVE_TIMER=true -IDLE_TIMEOUT_MINUTES=30 - -# User management -ALLOW_SELF_REGISTER=true -ADMIN_USERNAMES=admin -# Security (CHANGE THESE FOR PRODUCTION!) -SECRET_KEY=local-test-secret-key-change-this-please-override-32plus - -# Database (SQLite for local testing) -DATABASE_URL=sqlite:///data/timetracker.db - -# Logging -LOG_FILE=/app/logs/timetracker.log - -# Cookie settings (disabled for local testing) -SESSION_COOKIE_SECURE=false -REMEMBER_COOKIE_SECURE=false - -# Flask environment -FLASK_ENV=development -FLASK_DEBUG=true - -# License server (disabled for local testing) -LICENSE_SERVER_ENABLED=false \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8254a3f8..bda2c803 100644 --- a/.gitignore +++ b/.gitignore @@ -201,6 +201,7 @@ desktop/*.pvk # Environment backups .env.backup +.env.local-test # Node.js / Frontend build node_modules/ diff --git a/CHANGELOG.md b/CHANGELOG.md index c4bd507e..33027df8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [5.8.2] - 2026-06-15 + +### Fixed + +- **Invoice expenses** — Expense records from the Expenses module now link to the invoice Expenses section instead of being misrouted into invoice items or lost on save. Generate-from-time no longer wipes existing line items when only expenses are selected; the Add Expense flow focuses the expenses picker; edit-time expense sync is hardened (#662). + +### Documentation + +- **Version** — Documented release **5.8.2** to match `setup.py` (single source of truth for the application version). + ## [5.8.1] - 2026-06-10 ### Fixed diff --git a/README.md b/README.md index 967ccc01..dfde0a39 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,10 @@ TimeTracker has been continuously enhanced with powerful new features! Here's wh **Current version** is defined in `setup.py` (single source of truth). See [CHANGELOG.md](CHANGELOG.md) for versioned release history. +### ✨ Highlights of v5.8.2 + +**Patch (5.8.2):** **Invoice expenses** — billable expenses from the Expenses module link to the invoice Expenses section (not invoice items), stay separate with their descriptions, and are no longer wiped when adding expenses to an existing invoice. See [CHANGELOG.md](CHANGELOG.md#582---2026-06-15). + ### ✨ Highlights of v5.8.1 **Patch (5.8.1):** **Quote email** — sending a quote no longer flashes a false error after the email is delivered; fixes the follow-up regression to #652. See [CHANGELOG.md](CHANGELOG.md#581---2026-06-10). diff --git a/app/routes/invoices.py b/app/routes/invoices.py index 4c922088..ecd10c85 100644 --- a/app/routes/invoices.py +++ b/app/routes/invoices.py @@ -451,20 +451,24 @@ def edit_invoice(invoice_id): flash(f"Invalid quantity or price for item {i+1}", "error") continue - # Update expenses - expense_ids = request.form.getlist("expense_id[]") + # Update expenses (only when the expenses section was submitted) + if request.form.get("expenses_sync"): + expense_ids = request.form.getlist("expense_id[]") - # Unlink expenses not in the list - for expense in invoice.expenses.all(): - if str(expense.id) not in expense_ids: - expense.unmark_as_invoiced() + # Unlink expenses removed from this invoice + for expense in invoice.expenses.all(): + if expense.invoice_id == invoice.id and str(expense.id) not in expense_ids: + expense.unmark_as_invoiced() - # Link expenses in the list - if expense_ids: + # Link new expenses from the list for expense_id in expense_ids: try: expense = Expense.query.get(int(expense_id)) - if expense and not expense.invoiced: + if not expense: + continue + if expense.invoice_id == invoice.id: + continue + if not expense.invoiced: expense.mark_as_invoiced(invoice.id) except (ValueError, AttributeError): continue @@ -893,8 +897,9 @@ def generate_from_time(invoice_id): flash(_("No time entries, costs, expenses, or extra goods selected"), "error") return redirect(url_for("invoices.generate_from_time", invoice_id=invoice.id)) - # Clear existing items - invoice.items.delete() + # Only regenerate invoice items when time entries or project costs are selected + if selected_entries or selected_costs: + invoice.items.delete() total_prepaid_allocated = Decimal("0") prepaid_allocator = None @@ -997,6 +1002,8 @@ def generate_from_time(invoice_id): flash(_("Could not generate items due to a database error. Please check server logs."), "error") return redirect(url_for("invoices.edit_invoice", invoice_id=invoice.id)) + db.session.refresh(invoice) + # If invoice is already sent (not draft), mark time entries as paid if invoice.status != "draft": from app.services import InvoiceService @@ -1006,7 +1013,20 @@ def generate_from_time(invoice_id): if marked_count > 0: safe_commit("mark_time_entries_paid_from_invoice", {"invoice_id": invoice.id}) - flash(_("Invoice items generated successfully from time entries and costs"), "success") + added_parts = [] + if selected_entries or selected_costs: + added_parts.append(_("time entries and costs")) + if selected_expenses: + added_parts.append(_("expenses")) + if selected_goods: + added_parts.append(_("extra goods")) + if added_parts: + flash( + _("Successfully added %(parts)s to the invoice.", parts=", ".join(added_parts)), + "success", + ) + else: + flash(_("Invoice updated successfully"), "success") if total_prepaid_allocated and total_prepaid_allocated > 0: flash( _( @@ -1022,6 +1042,7 @@ def generate_from_time(invoice_id): from app.services import InvoiceService data = InvoiceService().get_unbilled_data_for_invoice(invoice) + focus = request.args.get("focus", "").strip() return render_template( "invoices/generate_from_time.html", invoice=invoice, @@ -1038,6 +1059,7 @@ def generate_from_time(invoice_id): prepaid_summary=data["prepaid_summary"], prepaid_plan_hours=data["prepaid_plan_hours"], prepaid_reset_day=data.get("prepaid_reset_day"), + focus=focus, ) diff --git a/app/templates/invoices/edit.html b/app/templates/invoices/edit.html index f2320ff4..b50b76da 100644 --- a/app/templates/invoices/edit.html +++ b/app/templates/invoices/edit.html @@ -166,7 +166,8 @@
{{ _('Select unbilled time entries, project costs, and extra goods to add to this invoice') }}
++ {% if focus == 'expenses' %} + {{ _('Select billable expenses to link to this invoice. They will appear in the Expenses section, not as invoice items.') }} + {% else %} + {{ _('Select unbilled time entries, project costs, and extra goods to add to this invoice') }} + {% endif %} +