PaperNest: Architecture and Runtime Guide

This document explains how this project is structured and how it behaves at runtime. After reading it, you should be able to follow the full path from app startup to user actions, persistence, OCR, and exports.

Companion versions:

1) What the app is

PaperNest is a KivyMD app that lets a user:

  • capture or select receipt images,
  • auto-detect date and amount using OCR,
  • save receipts in SQLite,
  • view timeline totals,
  • edit/delete records,
  • export timeline CSV and receipt images.

Primary runtime stack:

  • UI framework: Kivy + KivyMD
  • Business logic: pure Python service layer
  • Persistence: sqlite3
  • OCR engines:
    • Desktop/non-Android: Tesseract via pytesseract + Pillow
    • Android: Google ML Kit Text Recognition via pyjnius

2) High-level architecture

The codebase follows a layered approach:

  1. Entry/App layer
    • main.py
    • receipt_app/views/app.py
  2. UI layer (screens + components)
    • receipt_app/views/screens/*
    • receipt_app/views/components/*
    • receipt_app/views/shared/*
  3. Business layer
    • receipt_app/business/receipt_service.py
    • receipt_app/business/ocr_service.py
    • receipt_app/business/image_storage_service.py
    • receipt_app/business/csv_export_service.py
  4. Data layer
    • receipt_app/data/receipt_repository.py
  5. Core shared types/errors
    • receipt_app/core/models.py
    • receipt_app/core/exceptions.py
  6. Compatibility facade modules (legacy import paths)
    • receipt_app/models.py, exceptions.py, repository.py, services.py, ui.py

Important design idea:

  • UI talks to ReceiptService (not directly to SQLite).
  • ReceiptService orchestrates repository + storage + OCR + export services.
  • ReceiptRepository only handles SQL operations.

3) Startup sequence (exact runtime path)

  1. main.py runs ReceiptTrackerApp().run().
  2. Kivy calls ReceiptTrackerApp.build() in receipt_app/views/app.py.
  3. build() does dependency wiring:
    • creates data directory under Kivy user_data_dir
    • instantiates:
      • ReceiptRepository
      • ImageStorageService
      • CsvExportService
      • OCRService
      • ReceiptService(repository=..., image_storage=..., ocr_service=..., csv_export_service=...)
  4. build() loads KV files in _load_kv_files() with Builder.load_file(...).
  5. build() creates root widget:
    • ReceiptRoot(receipt_service=receipt_service, image_storage=image_storage)
  6. ReceiptRoot.on_kv_post() configures screens:
    • AddReceiptScreen gets receipt_service + image_storage
    • TimelineScreen gets receipt_service
    • GalleryScreen gets receipt_service

This is constructor injection at app startup: dependencies are created once and passed down.

4) Python language features used in this project

4.1 Inheritance

Examples:

  • ReceiptTrackerApp(MDApp)
  • AddReceiptScreen(Screen), TimelineScreen(Screen), GalleryScreen(Screen)
  • OutlinedCard(MDCard)
  • TappableOutlinedCard(OutlinedCard)
  • TapToEditTextInput(TextInput)

Why it is used:

  • to extend framework classes with app-specific behavior while reusing Kivy/KivyMD features.

4.2 Dependency injection

Main example:

  • ReceiptService is initialized with dependencies in ReceiptTrackerApp.build():
    • repository
    • image storage
    • OCR service
    • CSV export service

Why this matters:

  • business logic is not tightly coupled to concrete global singletons.
  • easier to test and replace pieces.

4.3 Dataclasses

In receipt_app/core/models.py:

  • Receipt and OCRSuggestion are @dataclass(frozen=True, slots=True).

What that gives:

  • concise immutable-style data objects (frozen=True),
  • lower memory footprint (slots=True),
  • clear typing between layers.

4.4 Properties and descriptors (Kivy)

Screens define Kivy properties such as StringProperty, NumericProperty, BooleanProperty. Example: AddReceiptScreen.status_message.

Why:

  • KV bindings auto-refresh UI when these values change.

4.5 Package-level __init__.py

Used for:

  • package markers,
  • re-exporting selected symbols via __all__.

There are also compatibility __init__ and facade modules preserving old import paths.

5) Data model and persistence

5.1 Receipt model

Receipt fields:

  • id
  • receipt_date
  • amount
  • image_path
  • created_at

Receipt.from_row() converts sqlite rows to typed objects.

5.2 Database schema

In ReceiptRepository._initialize_schema():

  • table receipts
  • index on receipt_date

Schema choices:

  • receipt_date and created_at stored as ISO text
  • amount stored as REAL with non-negative CHECK

5.3 Repository behavior

ReceiptRepository opens a fresh sqlite connection per operation (_connect). This simplifies thread use, because workers do not share one sqlite connection object.

Main operations:

  • add/get/update/delete receipt
  • count by image path (for image cleanup safety)
  • query date range and totals
  • list all receipts (gallery)

6) Business layer behavior

6.1 ReceiptService is the orchestrator

ReceiptService validates inputs and coordinates lower-level services.

Key methods:

  • add_receipt()
    • parse date/amount
    • validate/copy image
    • insert DB row
    • rollback copied file if DB insert fails
  • suggest_fields_from_image()
    • validates image path
    • delegates OCR
  • update_receipt()
  • delete_receipt()
    • delete DB row
    • remove image file only if no other record references it
  • get_timeline_summary()
  • export_timeline_csv()
  • export_receipt_images()
  • get_gallery_grouped_by_year_month()

6.2 OCRService engine selection

At initialization (OCRService.__init__ -> _setup_engine):

  • If Android env detected, tries ML Kit setup first.
  • Otherwise tries pytesseract/Pillow and checks tesseract binary availability.

available and unavailable_reason are exposed to UI.

OCR output pipeline:

  1. extract raw text (ML Kit or pytesseract)
  2. parse date candidates
  3. parse and rank amount candidates
  4. choose top suggestions
  5. return OCRSuggestion

Android OCR uses a fast-first path: it scans the original sampled bitmap, stops when date/amount confidence is sufficient, and prepares enhanced fallback variants only for weak or partial results. Temporary variants are dimension-bounded before filtering, and the path includes GC hints to reduce memory pressure from large phone photos.

7) UI architecture and KV setup

7.1 Root layout

ReceiptRoot in root_screen.kv contains:

  • ScreenManager with three active screens:
    • add_receipt
    • timeline
    • gallery
  • bottom navigation bar to switch current screen
  • global busy indicator card bound to app-level busy state

Busy indicator data source:

  • ReceiptTrackerApp.begin_busy() / end_busy()
  • busy_task_count, busy_message

7.2 KV loading strategy

All KV files are loaded explicitly in ReceiptTrackerApp._load_kv_files(). This avoids implicit loading surprises and ensures custom widgets/rules are available.

7.3 Shared reusable UI pieces

  • OutlinedCard, TappableOutlinedCard: custom card visuals/tap handling
  • popup_utils.py: themed popup factory + button styling
  • file_picker_helper.py: reusable file/folder picker popups
  • amount_numpad_popup.py: numeric keypad popup for amount entry
  • receipt_editor.py: shared edit popup used by timeline and gallery

8) Main user flows

8.1 Add Receipt flow

Screen: receipt_app/views/screens/add_receipt/add_receipt_screen.py

Flow:

  1. User captures photo or chooses existing image(s).
  2. Selected images become ReceiptDraft objects in ReceiptDraftManager.
  3. Screen auto-triggers OCR scan for one or all selected drafts.
  4. OCR suggestions populate date/amount and candidate lists.
  5. User can edit using date picker and numpad.
  6. User taps Save.
  7. Save runs in worker thread.
  8. On success:
    • draft removed from in-memory batch
    • timeline and gallery are refreshed

Special details:

  • supports multiple images in one selection batch
  • supports swipe left/right preview navigation
  • uses _scan_generation to ignore stale OCR results from old scans

8.2 Timeline flow

Screen: timeline_screen.py

Flow:

  1. User sets start/end date.
  2. refresh_summary() queries service for total + receipts.
  3. Results rendered as row cards.
  4. User can edit/delete a row.
  5. User can export:
    • CSV report
    • receipt image files grouped by year/month-name

Screen: gallery_screen.py

Flow:

  1. Worker refresh loads grouped hierarchy year -> month -> receipts.
  2. UI shows year list, then month list, then receipt cards.
  3. User opens options popup on a receipt card.
  4. Edit/delete actions call shared editor/service methods.
  5. Timeline screen is notified and refreshed after changes.

9) Threading model: when threads start and end

Only a few places use Python threads, mostly to keep UI responsive. All worker threads are daemon threads and hand results back with Clock.schedule_once(...).

9.1 AddReceiptScreen OCR scan thread

Start:

  • AddReceiptScreen.run_ocr() creates threading.Thread(target=_scan_receipts_worker, daemon=True).start().

Thread work:

  • loops selected image batch
  • calls receipt_service.suggest_fields_from_image(...)
  • collects results/errors
  • schedules _apply_scan_results_safely(...) on main thread

End:

  • thread exits when _scan_receipts_worker returns.
  • finally block detaches jnius thread on Android.
  • UI loading state is ended by main-thread callbacks (s(False)).

9.2 AddReceiptScreen save thread

Start:

  • save_receipt() starts Thread(target=_save_receipt_worker, daemon=True).

Thread work:

  • calls receipt_service.add_receipt(...)
  • schedules _apply_saved_receipt(...) or _handle_save_error(...) on main thread

End:

  • exits after one save attempt.
  • loading state cleared in scheduled UI callback.

9.3 GalleryScreen refresh thread

Start:

  • refresh_gallery() starts Thread(target=_refresh_gallery_worker, daemon=True).

Thread work:

  • fetches grouped data from service
  • schedules _apply_refreshed_gallery(...) or error handler

End:

  • exits after one refresh attempt.
  • loading popup + busy token cleared in UI callback.

9.4 GalleryScreen delete thread

Start:

  • delete_receipt() starts Thread(target=_delete_receipt_worker, daemon=True).

Thread work:

  • calls service delete
  • schedules success/failure callback

End:

  • exits after delete attempt.
  • loading UI cleared on callback.

9.5 Thread safety notes

  • UI updates are correctly marshaled to Kivy main thread with Clock.schedule_once.
  • Repository opens separate sqlite connection per call, reducing shared-connection thread issues.
  • Android pyjnius attach/detach is explicitly handled in OCR worker thread.

10) Purpose of __init__ methods in this project

10.1 Class __init__ methods

They mostly do one of these:

  • establish default runtime state (indices, flags, caches),
  • store injected dependencies,
  • prepare helper objects (draft manager, editor, popups),
  • create directories for persistence services.

Examples:

  • ReceiptTrackerApp.__init__: initializes global busy token bookkeeping.
  • ReceiptRepository.__init__: stores DB path, ensures parent dir, initializes schema.
  • ImageStorageService.__init__: ensures receipt image directory exists.
  • AddReceiptScreen.__init__: initializes draft manager, Android capture state, scan generation.
  • GalleryScreen.__init__: initializes hierarchy cache and loading state.

10.2 Package __init__.py files

They define import surfaces with __all__ and re-export selected classes. This keeps imports tidy and supports stable API boundaries across modules.

11) Error handling strategy

Error classes in core/exceptions.py are domain-specific:

  • ValidationError
  • RepositoryError
  • ImageStorageError
  • OCRServiceError
  • ExportError
  • NotFoundError

Pattern:

  • lower layers raise typed exceptions,
  • screen layer catches and shows user-friendly popups,
  • unexpected exceptions are logged and surfaced with safe generic messages.

12) Android-specific behavior

Android-specific paths include:

  • runtime permission requests before camera/media actions,
  • native camera intent result handling,
  • content URI copy into app-owned local files,
  • OCR through ML Kit via pyjnius,
  • explicit jnius attach() and detach() in OCR worker thread.

Build settings (buildozer.spec) include:

  • Android permissions,
  • ML Kit Gradle dependency,
  • target/min API settings,
  • architecture targets.

13) Compatibility and non-primary modules

13.1 Compatibility facade modules

receipt_app/models.py, exceptions.py, repository.py, services.py, ui.py re-export modern modules. Purpose: keep old import paths working while project moved to layered structure.

13.2 Batch review screen status

BatchReviewScreen exists but is not currently wired into root ScreenManager and not exported by views/screens/__init__.py. It is available as a feature module but not part of current runtime navigation.

13.3 Image processing test utility

image_processing_test_ui.py and business/image_processing_service.py provide OCR preprocessing experimentation tools. They are useful for tuning/diagnostics and are not required for normal app runtime.

14) Mental model summary

If you want one compact model of this app, use this:

  • App startup builds one dependency graph in ReceiptTrackerApp.
  • Screens are stateful controllers over Kivy properties + KV views.
  • Screens call ReceiptService for all business actions.
  • ReceiptService coordinates repository/OCR/storage/export services.
  • Long operations run in daemon threads.
  • Worker threads never mutate UI directly; they schedule main-thread callbacks.
  • Persistence is SQLite + file system paths under Kivy user data dir.
  • Domain exceptions flow upward and are shown to users as popups.

That is the core architecture this project follows.