diff --git a/crud.py b/crud.py index 2004264..57eea2c 100644 --- a/crud.py +++ b/crud.py @@ -395,6 +395,92 @@ async def count_journal_entries_by_user(user_id: str) -> int: return result["total"] if result else 0 +async def get_journal_entries_by_user_and_account_type( + user_id: str, account_type: str, limit: int = 100, offset: int = 0 +) -> list[JournalEntry]: + """Get journal entries that affect the user's accounts filtered by account type""" + # Get all user-specific accounts of the specified type + user_accounts = await db.fetchall( + "SELECT id FROM accounts WHERE user_id = :user_id AND account_type = :account_type", + {"user_id": user_id, "account_type": account_type}, + ) + + if not user_accounts: + return [] + + account_ids = [acc["id"] for acc in user_accounts] + + # Get all journal entries that have lines affecting these accounts + placeholders = ','.join([f":account_{i}" for i in range(len(account_ids))]) + params = {f"account_{i}": acc_id for i, acc_id in enumerate(account_ids)} + params["limit"] = limit + params["offset"] = offset + + entries_data = await db.fetchall( + f""" + SELECT DISTINCT je.* + FROM journal_entries je + JOIN entry_lines el ON je.id = el.journal_entry_id + WHERE el.account_id IN ({placeholders}) + ORDER BY je.entry_date DESC, je.created_at DESC + LIMIT :limit OFFSET :offset + """, + params, + ) + + entries = [] + for entry_data in entries_data: + # Parse flag and meta from database + from .models import JournalEntryFlag + flag = JournalEntryFlag(entry_data.get("flag", "*")) + meta = json.loads(entry_data.get("meta", "{}")) if entry_data.get("meta") else {} + + entry = JournalEntry( + id=entry_data["id"], + description=entry_data["description"], + entry_date=entry_data["entry_date"], + created_by=entry_data["created_by"], + created_at=entry_data["created_at"], + reference=entry_data["reference"], + flag=flag, + meta=meta, + lines=[], + ) + entry.lines = await get_entry_lines(entry.id) + entries.append(entry) + + return entries + + +async def count_journal_entries_by_user_and_account_type(user_id: str, account_type: str) -> int: + """Count journal entries that affect the user's accounts filtered by account type""" + # Get all user-specific accounts of the specified type + user_accounts = await db.fetchall( + "SELECT id FROM accounts WHERE user_id = :user_id AND account_type = :account_type", + {"user_id": user_id, "account_type": account_type}, + ) + + if not user_accounts: + return 0 + + account_ids = [acc["id"] for acc in user_accounts] + + # Count journal entries that have lines affecting these accounts + placeholders = ','.join([f":account_{i}" for i in range(len(account_ids))]) + params = {f"account_{i}": acc_id for i, acc_id in enumerate(account_ids)} + + result = await db.fetchone( + f""" + SELECT COUNT(DISTINCT je.id) as total + FROM journal_entries je + JOIN entry_lines el ON je.id = el.journal_entry_id + WHERE el.account_id IN ({placeholders}) + """, + params, + ) + return result["total"] if result else 0 + + # ===== BALANCE AND REPORTING ===== diff --git a/static/js/index.js b/static/js/index.js index 41ff3e4..cf9e262 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -18,7 +18,8 @@ window.app = Vue.createApp({ has_prev: false }, transactionFilter: { - user_id: null // For filtering by user + user_id: null, // For filtering by user + account_type: null // For filtering by receivable/payable (asset/liability) }, accounts: [], currencies: [], @@ -189,14 +190,21 @@ window.app = Vue.createApp({ transactionColumns() { return [ { name: 'flag', label: 'Status', field: 'flag', align: 'left', sortable: true }, + { name: 'username', label: 'User', field: 'username', align: 'left', sortable: true }, { name: 'date', label: 'Date', field: 'entry_date', align: 'left', sortable: true }, { name: 'description', label: 'Description', field: 'description', align: 'left', sortable: false }, - { name: 'username', label: 'User', field: 'username', align: 'left', sortable: true }, { name: 'amount', label: 'Amount (sats)', field: 'amount', align: 'right', sortable: false }, { name: 'fiat', label: 'Fiat Amount', field: 'fiat', align: 'right', sortable: false }, { name: 'reference', label: 'Reference', field: 'reference', align: 'left', sortable: false } ] }, + accountTypeOptions() { + return [ + { label: 'All Types', value: null }, + { label: 'Receivable (User owes Castle)', value: 'asset' }, + { label: 'Payable (Castle owes User)', value: 'liability' } + ] + }, expenseAccounts() { return this.accounts.filter(a => a.account_type === 'expense') }, @@ -344,11 +352,14 @@ window.app = Vue.createApp({ const limit = parseInt(this.transactionPagination.limit) || 20 - // Build query params with filter + // Build query params with filters let queryParams = `limit=${limit}&offset=${currentOffset}` if (this.transactionFilter.user_id) { queryParams += `&filter_user_id=${this.transactionFilter.user_id}` } + if (this.transactionFilter.account_type) { + queryParams += `&filter_account_type=${this.transactionFilter.account_type}` + } const response = await LNbits.api.request( 'GET', @@ -373,6 +384,7 @@ window.app = Vue.createApp({ }, clearTransactionFilter() { this.transactionFilter.user_id = null + this.transactionFilter.account_type = null this.transactionPagination.offset = 0 this.loadTransactions(0) }, diff --git a/templates/castle/index.html b/templates/castle/index.html index 2de6dfb..c9b4f87 100644 --- a/templates/castle/index.html +++ b/templates/castle/index.html @@ -357,12 +357,31 @@ -
+
+ + + +
+
diff --git a/views_api.py b/views_api.py index ecb4975..c82b7b2 100644 --- a/views_api.py +++ b/views_api.py @@ -268,30 +268,51 @@ async def api_get_user_entries( limit: int = 20, offset: int = 0, filter_user_id: str = None, + filter_account_type: str = None, # 'asset' for receivable, 'liability' for payable ) -> dict: """Get journal entries that affect the current user's accounts""" from lnbits.settings import settings as lnbits_settings from lnbits.core.crud.users import get_user - from .crud import count_all_journal_entries, count_journal_entries_by_user, get_account + from .crud import ( + count_all_journal_entries, + count_journal_entries_by_user, + count_journal_entries_by_user_and_account_type, + get_account, + get_journal_entries_by_user_and_account_type, + ) - # If super user, show all journal entries + # Determine which entries to fetch based on filters if wallet.wallet.user == lnbits_settings.super_user: - entries = await get_all_journal_entries(limit, offset) - total = await count_all_journal_entries() + # Super user with user_id filter + if filter_user_id: + # Filter by both user_id and account_type + if filter_account_type: + entries = await get_journal_entries_by_user_and_account_type( + filter_user_id, filter_account_type, limit, offset + ) + total = await count_journal_entries_by_user_and_account_type( + filter_user_id, filter_account_type + ) + else: + # Filter by user_id only + entries = await get_journal_entries_by_user(filter_user_id, limit, offset) + total = await count_journal_entries_by_user(filter_user_id) + else: + # No user filter, show all entries (account_type filter not supported for all entries) + entries = await get_all_journal_entries(limit, offset) + total = await count_all_journal_entries() else: - entries = await get_journal_entries_by_user(wallet.wallet.user, limit, offset) - total = await count_journal_entries_by_user(wallet.wallet.user) - - # Filter by user_id if specified (super user only) - if filter_user_id and wallet.wallet.user == lnbits_settings.super_user: - entries = [e for e in entries if any( - line.account_id in [acc["id"] for acc in await db.fetchall( - "SELECT id FROM accounts WHERE user_id = :user_id", - {"user_id": filter_user_id} - )] - for line in e.lines - )] - total = len(entries) + # Regular user + if filter_account_type: + entries = await get_journal_entries_by_user_and_account_type( + wallet.wallet.user, filter_account_type, limit, offset + ) + total = await count_journal_entries_by_user_and_account_type( + wallet.wallet.user, filter_account_type + ) + else: + entries = await get_journal_entries_by_user(wallet.wallet.user, limit, offset) + total = await count_journal_entries_by_user(wallet.wallet.user) # Enrich entries with username information enriched_entries = [] @@ -299,11 +320,13 @@ async def api_get_user_entries( # Find user_id from entry lines (look for user-specific accounts) entry_user_id = None entry_username = None + entry_account_type = None for line in entry.lines: account = await get_account(line.account_id) if account and account.user_id: entry_user_id = account.user_id + entry_account_type = account.account_type.value if hasattr(account.account_type, 'value') else account.account_type user = await get_user(account.user_id) entry_username = user.username if user and user.username else account.user_id[:16] + "..." break @@ -312,6 +335,7 @@ async def api_get_user_entries( **entry.dict(), "user_id": entry_user_id, "username": entry_username, + "account_type": entry_account_type, }) return {