Commit Graph

192 Commits

Author SHA1 Message Date
Dries Peeters 7446b86a36 fix(audit): reuse session connection when checking audit_logs table
Inspecting the engine directly opens a second connection, which under
SQLite can contend with a write lock held by the current flush and
cause spurious failures during tests. Bind the inspector to the
session's existing connection when available and fall back to the
engine otherwise.
2026-05-14 06:22:15 +02:00
Dries Peeters 2ed3bd6a88 feat(pdf-designer): multi-select, text alignment, guides, layers (#619)
Add multi-select with Shift/Ctrl toggles and marquee selection on the invoice
and quote PDF template editors, plus a shared transformer, group nudge with
snap-aware arrow keys, multi copy/paste/duplicate, align-to-page or
align-to-selection, horizontal/vertical distribute, and Ctrl/Cmd+G group /
Ctrl/Cmd+Shift+G ungroup.

Expose horizontal (left/center/right/justify) and vertical (top/middle/bottom)
text controls in the properties panel; export verticalAlign, group_id,
locked, and hidden in template JSON where set. Skip hidden elements in
ReportLab rendering; draw justified text on canvas via Paragraph; honor
verticalAlign when a text box height is present.

Introduce app/static/js/pdf_editor helpers (loaded with dynamic import from
the templates), pdf_editor.css, a layers panel with visibility and lock toggles,
smart-guide snapping, and optional ruler chrome. Document behaviour and manual
QA in docs/PDF_LAYOUT_EDITOR.md; add tests/test_pdf_template_schema.py for
optional template fields.

Closes #619.
2026-05-13 19:49:38 +02:00
Dries Peeters 1836cb3c2d chore(typing): resolve mypy errors and harden type checking
Drives ``mypy app/`` from 567 errors in 208 files to 0 errors across the
376 source files checked by ``./scripts/run-ci-local.sh code-quality``.

Configuration & dependencies
- pyproject.toml: enable implicit_optional (Flask-style ``x: str = None``
  defaults), silence truthy-function/truthy-bool (legitimate import-guard
  checks like ``KanbanColumn``), and disable warn_return_any (SQLAlchemy
  1.x ``Query`` API returns Any pervasively). Add module overrides for
  ``app.models.*``, repositories, base CRUD service, and known
  ``joinedload`` / ``Query.paginate`` callers where mypy cannot model the
  Flask-SQLAlchemy runtime API without a plugin.
- requirements-test.txt: pin ``types-requests``, ``types-bleach``,
  ``types-Markdown``, ``types-python-dateutil`` so mypy stops complaining
  about missing stubs.

Latent bugs fixed while driving mypy to zero
- app/utils/logger.py, app/utils/datetime_utils.py: drop imports of
  symbols that don't exist (``get_performance_metrics``,
  ``from_app_timezone``, ``to_app_timezone``) — these would have raised
  at import time on first use.
- app/services/currency_service.py: ``from typing import Decimal`` was a
  bug (typing has no Decimal); switch to ``decimal.Decimal`` and rename
  the ``D`` alias.
- app/utils/env_validation.py, app/utils/role_migration.py: ``Dict[str,
  any]`` → ``Dict[str, Any]`` (built-in ``any`` is not a type).
- app/utils/email.py: introduce ``send_template_email`` and update the
  three callers (``client_approval_service``,
  ``client_notification_service``, ``workflow_engine``) that were
  passing ``to=``/``template=``/etc. to ``send_email`` whose signature
  doesn't accept them — calls would have raised TypeError at runtime.
- app/services/permission_service.py: rewrite ``grant_permission`` /
  ``revoke_permission`` to use the actual ``Role`` ↔ ``Permission``
  many-to-many relationship; the old code referenced non-existent
  ``Permission.role_id`` / ``Permission.granted`` columns.
- app/services/gps_tracking_service.py: pass the required ``title`` and
  ``expense_date`` fields when creating mileage ``Expense`` rows.
- app/services/workflow_engine.py: ``_perform_action`` now forwards the
  ``rule`` argument to ``_action_log_time``, and ``_action_webhook``
  short-circuits when ``url`` is missing.
- app/services/time_tracking_service.py: validate ``start_time`` /
  ``end_time`` before comparing them.
- app/services/export_service.py: build CSV in a ``StringIO`` then wrap
  the bytes in ``BytesIO`` — ``csv.writer`` requires text I/O.
- app/integrations/peppol_smp.py: avoid attribute access on ``None`` in
  the SMP ``href`` fallback.
- app/integrations/{github,gitlab,slack}.py: coerce query-string params
  to strings so ``requests.get(params=...)`` matches the typed signature
  (and is what the HTTP layer expects anyway).
- app/integrations/{xero,quickbooks}.py: guard ``get_access_token()``
  returning ``None`` before calling private ``_api_request`` helpers.

Annotation-only changes
- Add ``Dict[str, Any]`` / ``list`` / ``Optional[...]`` annotations to
  service dict-literals that mypy could not infer from heterogeneous
  values (``ai_suggestion_service``, ``ai_categorization_service``,
  ``custom_report_service``, ``unpaid_hours_service``,
  ``integration_service``, ``invoice_service``, ``backup_service``,
  ``inventory_report_service``, ``analytics_service``, etc.).
- ``app/utils/event_bus.py``: ``emit_event`` accepts ``str |
  WebhookEvent`` and normalizes to ``str`` so all call-sites type-check.
- ``app/utils/api_responses.py``: introduce ``ApiResponse`` alias for
  ``Response | tuple[Response, int] | tuple[str, int]``.
- ``app/utils/budget_forecasting.py``: forecasting helpers return
  ``Optional[Dict]`` (they already returned ``None`` when the project
  was missing).
- ``app/utils/pdf_generator_reportlab.py``: ``_normalize_color`` is
  ``Optional[str]``.
- ``app/utils/pdfa3.py``: remove invalid ``force_version=None`` retry
  call.
- Narrow ``type: ignore`` markers on optional-dependency fallbacks
  (``redis``, ``bleach``, ``markdown``, ``babel``,
  ``powerpoint_export``) and on the documented ``requests.Session``
  / ``RotatingFileHandler`` typeshed limitations.
2026-05-13 10:32:06 +02:00
Dries Peeters 786d88bdba style: apply black 24.8.0 and isort across app/
Pure formatting pass to satisfy ``./scripts/run-ci-local.sh code-quality``:
no behavioural changes, just consistent line wrapping, import ordering,
and trailing-newline normalization across routes, models, services, and
utility modules.
2026-05-13 10:31:39 +02:00
Dries Peeters 1ddea89d40 Harden full-database restore and document operational behaviour
Admin restore runs in a background thread; the finally block must not use current_app.logger outside an application context. Use the captured Flask app instance for safe_file_remove logging instead.

While restore_backup runs (extract through Alembic upgrade), set a per-app _database_restore_in_progress flag and expose is_database_restore_in_progress(). The client portal blueprint registers a global app_context_processor; get_current_client() now skips database access during restore and catches SQLAlchemy errors with session rollback so error pages and login can still render when the schema is briefly torn on PostgreSQL.

Documentation: add docs/admin/BACKUP_AND_RESTORE.md, link it from the admin index and import/export docs, cross-reference from DATABASE_RECOVERY.md, and extend IMPORT_EXPORT_GUIDE.md with concurrent-restore guidance.
2026-05-11 07:13:04 +02:00
MacJediWizard d7a1eac247 style(template_filters): apply black 24.8.0 formatting
Match upstream's pinned black version so the file lands clean in
CI. All four hunks are line-joins under the 88-char limit, no
behavioural change.
2026-05-07 14:51:26 -04:00
MacJediWizard 6dd74a861a fix(markdown): render ~~strikethrough~~ in task and note descriptions
CommonMark and GFM both define ~~text~~ as strikethrough, and Toast UI
emits it when the user toggles strikethrough in the WYSIWYG editor.
Python markdown's 'extra' extension does not implement strikethrough,
so the wrapping tildes leak through to the rendered HTML and the user
sees ~~text~~ instead of struck-through text on /tasks/<id>, notes,
client view, etc.

Add a regex pass to _normalize_toastui_markdown that converts
~~text~~ to <del>text</del> before markdown parsing. The bleach
sanitizer already permits <del> via the existing allowed_tags list,
so the rendered HTML survives the sanitization pass intact.

The regex is non-greedy and stays on a single line so multiple
strikethroughs on the same line each get their own pair.
2026-05-07 11:41:36 -04:00
MacJediWizard 7394af7940 fix(markdown): unescape Toast UI Editor over-escaping in markdown filter
Task and note descriptions saved through the Toast UI WYSIWYG editor
came back wrapped in CommonMark-style escapes that Python markdown
either does not honour (\, → literal \,) or honours in a way that
breaks rendering (line-leading \- prevents list parsing).

The visible symptom on /tasks/<id>: bullet lists rendered as a single
flat paragraph with literal backslashes peppered between words.

Add a normaliser run before _md.markdown(...) that:
- Restores line-leading bullets that the editor escaped (\-/\*/\+
  followed by whitespace at start of line).
- Strips backslashes before punctuation Python markdown does not
  recognise as a valid escape (commas, colons, semicolons, etc.).

This handles existing rows in the DB without any data migration. The
normaliser leaves alone backslashes before punctuation Python markdown
does handle natively (\. \( \) \+ \- mid-line, \* \_ \# etc.)
so author-intent escapes still render correctly.

Strikethrough (~~text~~) still does not render because the 'extra'
extension does not include it; that is a separate enhancement.
2026-05-07 11:36:15 -04:00
Dries Peeters 887c93f00c fix(quotes): align list/detail scope with quote edit permissions
Ensure quote list/detail access uses shared quote scope resolution so users with quote-management permissions can view records they can edit, including post-edit redirects in web and API flows. Add regression coverage for non-admin edit_quotes behavior and document the scope-alignment requirement in advanced permissions docs.
2026-04-29 10:27:00 +02:00
Dries Peeters e34a668ddc feat(auth): add LDAP directory authentication
Introduce AUTH_METHOD values ldap and all, with LDAP_* environment settings, ldap3-based LDAPService (search, optional groupOfNames checks, user bind, DB sync), and users.auth_provider (local|oidc|ldap) via migration 153_add_user_auth_provider.

Login supports LDAP-only and combined all (local then LDAP where appropriate); OIDC callback sets auth_provider. Forgot/reset/change password flows skip LDAP-managed accounts. Admin System Settings gains a read-only LDAP summary and POST /admin/ldap/test. Production env validation requires core LDAP variables when LDAP is enabled; OIDC registration and docs recognize all.

Documentation: new docs/admin/configuration/LDAP_SETUP.md; updates to OIDC_SETUP, GETTING_STARTED, Docker guides, Render deploy notes, docs README, and CHANGELOG. Tests: tests/test_ldap_auth.py; test_oidc_logout allows auth_method all.
2026-04-27 19:08:08 +02:00
Dries Peeters ea913c6c4b feat(ai,security): add web AI helper, secret encryption, and 2FA
Introduce a web-first AI helper with admin-configurable providers (Ollama or hosted OpenAI-compatible), server-side context building, and confirmed write actions. Expose the feature via session /api/ai/* endpoints and scoped /api/v1/ai/* endpoints.

Harden security by requiring a strong SECRET_KEY for Docker Compose, adding optional settings encryption-at-rest (Fernet), and introducing TOTP-based 2FA plus password reset flows. Update admin UI, API docs, and install documentation.
2026-04-26 07:55:47 +02:00
Dries Peeters 89623682c8 fix(security): sandbox Jinja2 for database-backed PDF and email templates
Add render_sandboxed_string() using SandboxedEnvironment so stored invoice and
quote HTML, ReportLab text templates, admin PDF previews, and invoice email
HTML are not evaluated with Flask's full template globals (mitigating SSTI).

Add regression tests for sandbox behavior and demo user permissions.
2026-04-24 21:13:29 +02:00
Dries Peeters 568933c3b9 fix(auth): scope client portal users to their assigned client
Client-portal-enabled users (main app login, typically viewer) were not
included in get_allowed_client_ids(), so ProjectService and other callers
saw scope_client_ids=None and listed every project.

- Return [client_id] for is_client_portal_user in User.get_allowed_client_ids
- Derive get_allowed_project_ids from allowed client IDs for all non-admins
- Apply client/project scope and access checks from allowed IDs, not only
  subcontractor is_scope_restricted (fixes user_can_access_* for portal)

Fixes DRYTRIX/TimeTracker#592.

Tests: extend test_scope_filter with client_portal_scoped_user and API
isolation for GET /api/v1/projects.
2026-04-24 16:15:24 +02:00
Dries Peeters b746fa912e chore(analytics): remove dead PostHog feature-flag module
Delete app/utils/posthog_features.py. It was unused by routes, and
is_posthog_enabled() always returned False, so flags never activated and
the API was misleading.

Document the real model: deployment behavior uses environment variables
and app/config.py; per-user UI preferences stay on the user record.
Refresh PostHog monitoring guides and README accordingly, update the
incomplete-implementations note, and soften posthog_segmentation wording
so it does not imply an in-app PostHog flag layer.
2026-04-16 16:23:54 +02:00
Dries Peeters 999f5c6319 feat(api): clarify /api vs /api/v1 and reduce duplication
Document the dual HTTP surface everywhere integrators look: OpenAPI intro and servers, ARCHITECTURE, REST_API, API_VERSIONING (deprecated vs internal routes, shared modules), and CONTRIBUTING (v1-first rule).

Session JSON routes in app/routes/api.py that overlap REST v1 now return X-API-Deprecated and a Link header with rel successor-version, implemented via app/utils/api_deprecation.py.

Extract shared global search into app/services/global_search_service.py for both GET /api/search and GET /api/v1/search while preserving legacy short-query 200 empty responses and v1 400 validation.

Refactor legacy POST /api/timer/start, /api/timer/stop, and the start path of /api/timer/resume to use TimeTrackingService; keep existing socketio emits for the web UI.

Add tests/test_api_deprecation_headers.py and adjust search partial-failure tests to patch Project.query on the service module.
2026-04-16 15:36:01 +02:00
Dries Peeters ef9b66f5e7 fix(api): align client search, OpenAPI version, and Client construction
Global search referenced Client.company, which is not a column on Client, so client matches failed at runtime. Legacy and v1 search, plus search_clients(), now filter on name, email, description, and contact_person; result descriptions use the same fields. Legacy /api/search returns count: 0 for queries shorter than two characters so responses stay consistent.

OpenAPI info.version is taken from get_version_from_setup(), with a config fallback when the resolved version is unknown. get_version_from_setup() also honors TIMETRACKER_VERSION and APP_VERSION for CI and container builds.

Client.__init__ accepts custom_fields. ClientService no longer passes status= into Client(), which the initializer does not support.

Tests add HTTP route contract checks and OpenAPI version alignment, fix subcontractor search fixtures (Client/Task construction and v1 client fixture naming), and update related API integration tests.
2026-04-15 12:57:01 +02:00
Dries Peeters 91127bf188 feat: smart in-app notifications, value dashboard stats, and search scope helpers
Smart notifications (opt-in under user settings): NotificationService builds candidates from the user's local day and active timers; GET /api/notifications and POST /api/notifications/dismiss; migration 150 adds user columns and user_smart_notification_dismissals. /api/summary/today uses the same local-day totals. Client polls from smart-notifications.js; toastManager.show gains onDismiss for server dismiss sync. Config and env.example document SMART_NOTIFY_* variables.

Value dashboard: StatsService with Redis-backed caching, GET /api/stats/value-dashboard, dashboard template and dashboard-enhancements polling alongside existing widgets.

API v1 token search now uses apply_project_scope and apply_client_scope on queries; scope_filter adds apply_project_scope; tests extended for the new helper.
2026-04-15 12:15:23 +02:00
Dries Peeters b0dde80ba9 feat(web): high-visibility support modal, prompts, and supporter UX
Add a support modal with usage stats, tier and license links, share control, and offline-safe outbound CTAs. Surface support from the header, sidebar, user menu, dashboard card, and settings "Support & Community" section without hiding entry points when a supporter license is active.

Introduce UsageStatsService and a persisted users.support_stats_reports_generated counter incremented on key report exports and custom report views. Add SupportPromptService for session-scoped soft toasts (after export, dashboard milestones, long session via POST /donate/request-soft-prompt).

Wire consent-aware track_event names support.* and mirror funnel rows in DonationInteraction; fix has_recent_donation_click to treat link_clicked as a recent click. Document events and SUPPORT_* / migration notes in docs.

Tests: tests/test_support_services.py for prompt and usage stats behavior.
2026-04-15 10:55:37 +02:00
Dries Peeters 96955aee62 feat(admin): GitHub-based version update notification for admins
Add VersionService to fetch and cache the latest GitHub release, compare it to the installed semver (APP_VERSION when valid, else setup.py), and expose admin-only GET /api/version/check and POST /api/version/dismiss on the legacy /api blueprint (session or Bearer token).

Persist per-user dismissal in users.dismissed_release_version (Alembic 148) and show a non-blocking update card in base.html for administrators. Add packaging for semver parsing and tests for comparison, service, and routes.

Document configuration in docs/admin/deployment/VERSION_MANAGEMENT.md and endpoints in docs/api/REST_API.md and docs/API.md.
2026-04-15 09:39:32 +02:00
Dries Peeters 974f5cdd50 feat(quotes): invoice-style line items, costs, and extra goods (#585)
Add quote_items.line_kind (item | expense | good) plus display_name,
category, line_date, and sku so one table still drives totals, PDFs,
and acceptance stock logic.

- Migration 147: new columns with backfill line_kind=item
- Quote create/edit: three sections; stock and warehouse only when a
  line item is sourced from inventory; shared JS partial
- POST parsing, positions, and duplicate-quote copying for all fields
- API v1 quote items accept the new fields with defaults for old clients
- View, client portal, and PDF/fallback rendering use display_name and
  line metadata where relevant
- Integration tests: stock POST shape; expense and good line creation

Docs: extend INVENTORY_MANAGEMENT_PLAN QuoteItem and migration notes.
2026-04-12 14:00:12 +02:00
Dries Peeters 70510a6622 fix: quote create 500, line order, and Factur-X PDF parity
Quotes (#583):
- Add requires_approval, approval_level, and can_be_sent; wire create form
- Migrations 145 (approval columns) and 146 (quote_items.position)
- Order Quote.items by position; set positions on create/edit/duplicate/API
- Fix view template approval branch (not_required); add web regression test

Invoices / PEPPOL:
- Use the same Factur-X embed and PDF/A-3 normalization for export and
  email attachments; Associated File Data + text/xml metadata
- CII/UBL validators, pdfa3, zugferd, and invoice_pdf_postprocess helper
- Bundle compact sRGB ICC (app/resources/icc/); INVOICE_SRGB_ICC_PATH override
- Package data in setup.py; extend PEPPOL_EINVOICING.md and tests
2026-04-12 13:34:58 +02:00
Dries Peeters 9449a46a42 feat(integrations): Linear connector and shared HTTP/sync helpers
- Add Linear import (GraphQL, personal API key, optional team key filter).
- Centralize integration HTTP via integration_session and session_request.
- Add integration_sync_context for project/task refs and custom_fields metadata.
- Refactor Asana, GitHub, GitLab, Jira, Trello, ActivityWatch, and QuickBooks to use helpers.
- Extend integration UI, settings, and scheduled sync behavior as needed.
2026-04-05 08:39:18 +02:00
Dries Peeters 5df748b30e feat(api): v1 CSV import, bulk time entries, idempotency, and rate limits
- Add POST /api/v1/time-entries/import-csv and POST /api/v1/time-entries/bulk.
- Support Idempotency-Key on POST /api/v1/time-entries with 24h replay per token.
- Enforce per-token minute/hour limits (Redis when available, local fallback otherwise).
- Extract bulk and CSV import logic into dedicated services; trim legacy api route surface.
2026-04-05 08:39:14 +02:00
Dries Peeters 4007ee2ca8 feat(observability): add OpenTelemetry traces, OTLP metrics, and log correlation
Introduce a centralized OTel layer (app/telemetry/otel_setup.py) that reuses
the existing OTLP endpoint and token, exports traces and metrics over OTLP/HTTP,
and instruments Flask plus SQLAlchemy. Manual OTLP log export remains for base
and product analytics; log records now include trace_id, span_id, and
event_category where tracing is active.

Business spans and product metrics cover invoices, timers, reports, auth,
webhook delivery, and scheduled jobs. RED-style HTTP metrics are recorded in
after_request alongside existing Prometheus counters. ENABLE_TRACING and
ENABLE_METRICS default on when credentials exist; graceful no-op when they do not.

Privacy is preserved: user_id appears on traces only when detailed analytics is
opted in; metrics never carry user_id; _remove_pii behavior for analytics is
unchanged. Responses inject traceparent when tracing is enabled for future
browser correlation.

Tests: test_otel_integration.py and per-test reset_for_testing() in conftest
so each app factory can reinitialize OTel.
2026-03-28 17:32:18 +01:00
evilguy4000 1b5d019560 feat(admin): persist test recipient and send invoice email template tests
Add Settings.mail_test_recipient (migration 142) and wire Admin → Email Configuration to save, load, and prefill the SMTP test recipient field.

Extract invoice email PDF/HTML building into _build_invoice_email_payload for reuse. New send_invoice_template_test_email sends a real message with subject "Invoice template test: …", reusing production rendering and PDF attachment, without updating invoice status or creating InvoiceEmail records.

Add POST /admin/email-templates/<id>/send-test (optional recipient override, optional invoice_id) and Send test email UI on template view and edit pages. Extend smoke and unit tests.
2026-03-27 06:39:16 +01:00
Dries Peeters 905f6fbd37 feat(telemetry): migrate analytics pipeline from PostHog to OTLP
Switch product and installation telemetry to OTLP/Grafana across runtime config, CI injection, docs, and tests to unify telemetry transport and simplify privacy-focused opt-in behavior.
2026-03-26 17:01:55 +01:00
Dries Peeters 60d4d55027 feat(invoices): add fully configurable invoice number patterns
Implement issue #575 by introducing token-based invoice number patterns in settings and unifying number generation across invoice creation paths. This removes hardcoded INV/date formatting and aligns export filenames and bootstrap schemas with stored invoice numbers.
2026-03-26 14:51:55 +01:00
Dries Peeters f05d772dbb feat(api): add read:inventory and write:inventory scopes for inventory-only access
- New scopes read:inventory and write:inventory; existing read/write:projects
  still grant same inventory access for backward compatibility
- require_api_token() accepts tuple of scopes (any one required); inventory
  endpoints accept (read:inventory | read:projects) and (write:inventory | write:projects)
- ApiTokenService: add new scopes to allowed list; document in API_TOKEN_SCOPES.md
- Add tests for inventory report endpoints with scope checks
2026-03-16 16:43:08 +01:00
Dries Peeters db1b8823e4 chore(app): routes, utils, and bootstrap updates
- Update app bootstrap and route modules (admin, api, api_v1, audit_logs, clients, expenses, projects, settings, team_chat, timer)
- Add error_handling utility; update backup, client_lock, context_processors, data_import
2026-03-16 15:15:47 +01:00
Dries Peeters 0a1f551244 feat(settings): keyboard shortcut overrides and developer documentation
- Add keyboard_shortcuts_defaults utility for default bindings and overrides
- Update Settings keyboard shortcuts template for customization UI
- Add KEYBOARD_SHORTCUTS_DEVELOPER.md for implementation and extension
2026-03-16 15:15:34 +01:00
Dries Peeters cd0ccd61c7 feat(telemetry): add daily base heartbeat and trigger opt-in ping on enable
- Register send_base_telemetry_heartbeat_with_app cron at 03:00 UTC
- setup: call check_and_send_telemetry when user opts in during setup
- admin: call check_and_send_telemetry when toggling detailed analytics on
2026-03-16 13:01:09 +01:00
Dries Peeters 287020d30c feat(telemetry): gate product analytics on opt-in and send base first_seen at startup
- Delegate track_event, identify_user, track_page_view to telemetry service
- Only send detailed analytics when user has opted in (is_detailed_analytics_enabled)
- Call send_base_first_seen() once at app startup (idempotent per install)
- posthog_funnels: require telemetry_enabled for funnel tracking
- posthog_monitoring: require telemetry_enabled for error/performance events
2026-03-16 13:00:56 +01:00
Dries Peeters 5be0054157 feat(telemetry): add install_id UUID and consent-aware telemetry service
- Add get_install_id() and base_first_seen tracking in InstallationConfig
- Introduce app/telemetry package with TelemetryService abstraction
- Define minimal base telemetry schema (BASE_SCHEMA_KEYS)
- Implement send_base_telemetry, send_base_first_seen, send_base_heartbeat
- Implement send_analytics_event and identify_user gated by opt-in
- Unify install identity: get_installation_id() now returns get_install_id()
2026-03-16 13:00:49 +01:00
Dries Peeters b4486a627f fix: CI tests, code quality, and duplicate DB indexes
- Webhook models: remove duplicate index definitions so db.create_all()
  no longer raises 'index already exists' (columns already have index=True)
- ImportService: fix circular import by late-importing ClientService,
  ProjectService, TimeTrackingService in __init__
- reports: fix F823 by renaming unpack variable _ to _entry_count to avoid
  shadowing gettext _ in export_task_excel()
- Code quality: add .flake8 with extend-ignore so flake8 CI passes;
  simplify pyproject.toml isort config (drop unsupported options)
- Format: run black and isort on app/
- tests: restore minimal app fixture in test_import_export_models
2026-03-15 10:51:52 +01:00
Dries Peeters 8bb42ddd02 feat(app): recurring invoices, gantt/reporting services, license UI
- Add RecurringInvoiceRepository and RecurringInvoiceService; refactor recurring_invoice model
- Add GanttService and move gantt logic from route to service
- Expand ReportingService and simplify reports route
- Add license_utils and user license template/settings
- Refactor routes to use scope_filter, api_responses, and services (API v1, timer, admin, invoices, etc.)
- Extend invoice_service for recurring; cache and scope_filter utils; base/template updates
2026-03-15 09:37:00 +01:00
Dries Peeters 1d3a1541e2 feat(mileage,per_diem): add CSV/PDF export and filter-aware export (Issue #564)
- Mileage: Add GET /mileage/export/csv and /mileage/export/pdf with same
  filters as list (status, project, client, date range, search). Export
  buttons in list header; JS builds export URL from current filter form.
- Mileage PDF: New app/utils/mileage_pdf.py (ReportLab, landscape A4,
  totals row for distance and amount).
- Per diem: Add Client filter to list (with client-lock/single-client
  handling). Add GET /per-diem/export/csv and /per-diem/export/pdf.
- Per diem PDF: New app/utils/per_diem_pdf.py (same style as mileage).
- Export links always use current filters (no need to submit first).
- CHANGELOG and docs/import_export/README updated.
2026-03-11 19:18:20 +01:00
Dries Peeters ca0c181dc3 feat(overtime): add get_overtime_ytd and get_overtime_last_12_months helpers (Issue #560)
- get_overtime_ytd(user): returns overtime from Jan 1 through today
- get_overtime_last_12_months(user): returns rolling 12-month overtime
- Reuses calculate_period_overtime; no new DB columns
2026-03-11 17:38:56 +01:00
Dries Peeters 0a4a1535c1 refactor: split API v1 into sub-blueprints, slim bootstrap, move dashboard to AnalyticsService
Architecture and maintainability improvements per production-readiness plan:

- API v1: Split monolithic api_v1.py into per-resource blueprints
  (api_v1_projects, api_v1_tasks, api_v1_clients, api_v1_invoices,
  api_v1_expenses, api_v1_payments, api_v1_mileage, api_v1_deals,
  api_v1_leads, api_v1_contacts). Register all in blueprint_registry;
  keep info, health, auth and remaining routes in api_v1.py.

- Bootstrap: Move setup_logging to app/utils/setup_logging.py and
  legacy migrations (task management, issues tables) to
  app/utils/legacy_migrations.py. Use SQLAlchemy 2-compatible
  db.engine.begin() in legacy_migrations.

- Dashboard: Add AnalyticsService.get_dashboard_top_projects and
  get_time_by_project_chart; thin main dashboard route to call
  services only and remove inline TimeEntry aggregation.

- Docs: Update ARCHITECTURE.md (module table, API structure, data
  flow, design decisions), DEVELOPMENT.md (workflow, build steps,
  test examples), CHANGELOG.md (Unreleased refactor entry).
2026-03-11 11:54:04 +01:00
Dries Peeters f150b73b94 feat: product value improvements (dashboard, reports, timer, reminders)
Dashboard:
- Add time-by-project chart (last 7 days) with Chart.js horizontal bar; link to Summary report

Summary report:
- Add time-by-project (last 30d) bar chart and daily trend (14d) line chart
- Add one-page PDF export (today/week/month hours + top projects table)

Post-timer flow:
- After stop, show toast "Logged Xh on [Project]" with action link "View time entries"
- Toast manager: optional actionLink/actionLabel for action links in toasts
- Session carries timer_stopped_toast to dashboard; no duplicate flash

Remind to log:
- User setting "Remind me to log time at end of day" + time picker (Settings)
- Hourly job: send one email per day if user has <0.5h logged that day (user TZ)
- Migration 135: notification_remind_to_log, reminder_to_log_time on users
2026-03-11 08:59:13 +01:00
Dries Peeters c1a1592d2f fix(pdf): show decorative images in export when they overflow the page
- In _on_page(), only skip IMAGE elements when their rect does not
  intersect the page (was skipping when top-left was outside, which
  hid overflowing images).
- Coerce element x, y, width, height to float to avoid type issues.
- In _draw_image_on_canvas(), clip all image drawing to the page
  rectangle (beginPath + rect + clipPath) so the visible portion
  is shown and overflow is clipped like in the preview.
- Try template image file path before get_image_base64 for more
  reliable loading across deployments.
- Add INFO log when drawing template images for debugging.

Fixes #537 (export not showing decorative image when over border).
2026-03-09 20:49:06 +01:00
Dries Peeters 55f2ef29fd feat(overtime): allow overtime calculation by weekly hours (Issue #551) 2026-03-09 20:33:48 +01:00
Dries Peeters 4248a7e6d7 fix(admin): resolve database error when deleting user (#552)
Before deleting a user, explicitly remove their donation_interactions rows so the delete succeeds even when the table is missing (e.g. after support banner was removed via license). Also handle missing donation_interactions table in safe_commit like time_entry_approvals.
2026-03-09 20:32:47 +01:00
Dries Peeters 2e1c18a345 feat(invoicing): add Factur-X CII export validation and transport guidance
Switch embedded invoice PDFs to Factur-X CII payloads and tighten the PDF/A-3 and AS4 handling so exports better match the standards they advertise. Document the experimental native Peppol transport path and cover the new validation and embedding behavior with focused tests.
2026-03-06 22:15:29 +01:00
Dries Peeters 777d6ad3bf fix(invoicing): harden PEPPOL transport and PDF/A-3 export compliance
Implement native PEPPOL transport plumbing (identifier validation, SMP/SML discovery, and AS4 send path) and make ZUGFeRD/PDF export fail fast when embedding or PDF/A-3 normalization fails. Add settings, migrations, validators, tests, and docs so compliance issues are visible and verifiable.
2026-03-02 20:55:02 +01:00
Dries Peeters 552675ff55 fix(invoicing): ZUGFeRD/EN 16931 PDF and UBL fixes (Discussion #433)
- UBL: add unitCode C62 to InvoicedQuantity (required by EN 16931 / Peppol BR-CL-23)
- ZugFerd: attach XML as Associated File with relationship Alternative
- ZugFerd: inject ZUGFeRD XMP RDF for validators
- Docs: document AF/XMP, PDF/A-3 limitation, and validation (b2brouter, portinvoice)
- Tests: assert UBL contains InvoicedQuantity with unitCode in ZugFerd and Peppol tests
2026-03-01 07:36:18 +01:00
Dries Peeters 3c5a937234 Add task tags and categorization (fixes #539)
Implement the advertised 'Task tags and categorization' feature that was
listed in docs but missing from the task edit form.

- Add tags column to tasks table (String 500, comma-separated)
- Add tags field to task create and edit forms with validation
- Display tags as badges on task view page and list pages
- Add tags filter to main tasks list and My Tasks with AJAX support
- Include tags in task export (CSV) and data export
- Add tags to Task API (create, update, list with filter)
- Add tag_list property to Task model for convenient parsing

Also: fix(oidc) use domain instead of IP for HTTPS metadata fetch
to fix TLS SNI (Fixes #540)
2026-02-28 17:28:54 +01:00
Dries Peeters 0a4b29c50d feat: add development-only data seeding with inventory and finance
- Add run_seed() in app/utils/seed_dev_data.py: users, clients, projects,
  tasks, time entries, expenses, comments, warehouses, stock items,
  warehouse stock, stock movements, currencies, tax rules, invoices,
  invoice items, and payments. Only runs when FLASK_ENV=development.
- Register 'flask seed' CLI command with options (users, clients,
  projects-per-client, tasks-per-project, days-back).
- Add scripts/seed-dev-data.py and docker/seed-dev-data.sh for local
  and Docker runs. Include seed scripts in image via Dockerfile chmod.
- Document in docs/development/SEED_DEV_DATA.md; update
  DATABASE_RECOVERY.md, DOCKER_COMPOSE_SETUP.md, and development README.
2026-02-20 09:28:30 +01:00
Dries Peeters a6b60b16dd feat: Render deployment and demo mode for single-user demo
- Add render.yaml Blueprint: PostgreSQL + Python web service, auto-deploy on push
- Add demo mode (DEMO_MODE, DEMO_USERNAME, DEMO_PASSWORD): single fixed user only
- Login page shows demo credentials when demo mode is active
- Disable self-registration, admin user creation, and OIDC user creation in demo mode
- DB init creates demo user with password when DEMO_MODE is true
- Add docs/deploy/RENDER.md with deployment and demo mode instructions
2026-02-16 20:34:32 +01:00
Dries Peeters d39c5a2f37 fix(pdf-layout): decorative image persistence and PDF preview (Issue #432)
Decorative images now survive save/load and no longer cause a black PDF preview:

- Sync imageUrl onto groups before generateCode() so template_json has
  correct source (invoice and quote layout editors).
- Inject name/imageUrl into design_json with position-based matching so
  reordering does not swap or drop URLs.
- Restore name and imageUrl from saved JSON onto canvas on load
  (synchronous) so Konva custom attrs are not required.
- Omit decorative image elements with empty source from template_json;
  placeholders stay visible in the editor but are not sent to ReportLab.
- ReportLab: explicitly skip decorative images with empty source; validate
  base64 data URI payload and decode in try/except to avoid bad PDF output.

Documentation: PDF_LAYOUT_CUSTOMIZATION.md and PDF_EDITOR_ENHANCED_FEATURES.md
updated with decorative image description, state persistence details, and
troubleshooting. CHANGELOG.md updated under [Unreleased] Fixed.
2026-02-16 07:37:59 +01:00
Dries Peeters b0809e2f90 feat(invoicing): add ZugFerd/Factur-X support and document Peppol & ZugFerd
- Add optional embedding of EN 16931 UBL XML in invoice PDFs (ZugFerd/Factur-X)
  when 'Embed EN 16931 XML in invoice PDFs' is enabled in Admin > Peppol e-Invoicing.
  Exported PDFs then contain ZUGFeRD-invoice.xml for hybrid human- and machine-readable
  invoices; same UBL as Peppol, usable via Peppol or email.
- New setting invoices_zugferd_pdf (migration 128), pikepdf dependency, and
  app.utils.zugferd helper (best-effort supplier/customer from Settings and client).
- Wire embed in export_invoice_pdf (and fallback path); admin checkbox and persistence.
- Docs: PEPPOL_EINVOICING.md retitled to 'Peppol and ZugFerd', new section for
  ZugFerd embedding; README and CHANGELOG updated; migration 128 noted.
- Tests: test_zugferd.py (embed adds attachment with expected XML; invalid PDF
  returns original bytes and error).
2026-02-16 07:36:49 +01:00