Updates default chart of accounts

Expands the default chart of accounts with a more
detailed hierarchical structure. This includes new
accounts for fixed assets, livestock, equity
contributions, and detailed expense categories.
The migration script only adds accounts that don't
already exist, ensuring a smooth update process.
This commit is contained in:
padreug 2025-11-07 22:24:23 +01:00
parent 6f62c52c68
commit 88aaf0e28e
3 changed files with 89 additions and 29 deletions

View file

@ -190,26 +190,51 @@ def migrate_account_name(old_name: str, account_type: AccountType) -> str:
# Default chart of accounts with hierarchical names # Default chart of accounts with hierarchical names
DEFAULT_HIERARCHICAL_ACCOUNTS = [ DEFAULT_HIERARCHICAL_ACCOUNTS = [
# Assets # Assets
("Assets:Cash", AccountType.ASSET, "Cash on hand"),
("Assets:Bank", AccountType.ASSET, "Bank account"), ("Assets:Bank", AccountType.ASSET, "Bank account"),
("Assets:Lightning:Balance", AccountType.ASSET, "Lightning Network balance"), ("Assets:Bitcoin", AccountType.ASSET, "Bitcoin holdings"),
("Assets:Bitcoin:Lightning", AccountType.ASSET, "Lightning Network balance"),
("Assets:Bitcoin:OnChain", AccountType.ASSET, "On-chain Bitcoin wallet"),
("Assets:Cash", AccountType.ASSET, "Cash on hand"),
("Assets:FixedAssets:Equipment", AccountType.ASSET, "Equipment and machinery"),
("Assets:FixedAssets:FarmEquipment", AccountType.ASSET, "Farm equipment"),
("Assets:FixedAssets:Network", AccountType.ASSET, "Network infrastructure"),
("Assets:FixedAssets:ProductionFacility", AccountType.ASSET, "Production facilities"),
("Assets:Inventory", AccountType.ASSET, "Inventory and stock"),
("Assets:Livestock", AccountType.ASSET, "Livestock and animals"),
("Assets:Receivable", AccountType.ASSET, "Money owed to the Castle"), ("Assets:Receivable", AccountType.ASSET, "Money owed to the Castle"),
("Assets:Tools", AccountType.ASSET, "Tools and hand equipment"),
# Liabilities # Liabilities
("Liabilities:Payable", AccountType.LIABILITY, "Money owed by the Castle"), ("Liabilities:Payable", AccountType.LIABILITY, "Money owed by the Castle"),
# Equity # Equity
("Equity:MemberEquity", AccountType.EQUITY, "Member contributions"), ("Equity", AccountType.EQUITY, "Owner's equity and member contributions"),
("Equity:RetainedEarnings", AccountType.EQUITY, "Accumulated profits"),
# Revenue (Income in Beancount terminology) # Revenue (Income in Beancount terminology)
("Income:Accommodation", AccountType.REVENUE, "Revenue from stays"), ("Income:Accommodation:Guests", AccountType.REVENUE, "Revenue from guest accommodation"),
("Income:Service", AccountType.REVENUE, "Revenue from services"), ("Income:Service", AccountType.REVENUE, "Revenue from services"),
("Income:Other", AccountType.REVENUE, "Other revenue"), ("Income:Other", AccountType.REVENUE, "Other revenue"),
# Expenses # Expenses
("Expenses:Utilities", AccountType.EXPENSE, "Electricity, water, internet"), ("Expenses:Administrative", AccountType.EXPENSE, "Administrative expenses"),
("Expenses:Food:Supplies", AccountType.EXPENSE, "Food and supplies"), ("Expenses:Construction:Materials", AccountType.EXPENSE, "Construction materials"),
("Expenses:Maintenance", AccountType.EXPENSE, "Repairs and maintenance"), ("Expenses:Furniture", AccountType.EXPENSE, "Furniture and furnishings"),
("Expenses:Other", AccountType.EXPENSE, "Miscellaneous expenses"), ("Expenses:Garden", AccountType.EXPENSE, "Garden supplies and materials"),
("Expenses:Gas:Kitchen", AccountType.EXPENSE, "Kitchen gas"),
("Expenses:Gas:Vehicle", AccountType.EXPENSE, "Vehicle gas and fuel"),
("Expenses:Groceries", AccountType.EXPENSE, "Groceries and food"),
("Expenses:Hardware", AccountType.EXPENSE, "Hardware and tools"),
("Expenses:Housewares", AccountType.EXPENSE, "Housewares and household items"),
("Expenses:Insurance", AccountType.EXPENSE, "Insurance premiums"),
("Expenses:Kitchen", AccountType.EXPENSE, "Kitchen supplies and equipment"),
("Expenses:Maintenance:Car", AccountType.EXPENSE, "Car maintenance and repairs"),
("Expenses:Maintenance:Garden", AccountType.EXPENSE, "Garden maintenance"),
("Expenses:Maintenance:Property", AccountType.EXPENSE, "Property maintenance and repairs"),
("Expenses:Membership", AccountType.EXPENSE, "Membership fees"),
("Expenses:Supplies", AccountType.EXPENSE, "General supplies"),
("Expenses:Tools", AccountType.EXPENSE, "Tools and equipment"),
("Expenses:Utilities:Electric", AccountType.EXPENSE, "Electricity"),
("Expenses:Utilities:Internet", AccountType.EXPENSE, "Internet service"),
("Expenses:WebHosting:Domain", AccountType.EXPENSE, "Domain registration"),
("Expenses:WebHosting:Wix", AccountType.EXPENSE, "Wix hosting service"),
] ]

View file

@ -452,3 +452,37 @@ async def m011_account_permissions(db):
WHERE expires_at IS NOT NULL; WHERE expires_at IS NOT NULL;
""" """
) )
async def m012_update_default_accounts(db):
"""
Update default chart of accounts to include more detailed hierarchical structure.
Adds new accounts for fixed assets, livestock, equity contributions, and detailed expenses.
Only adds accounts that don't already exist.
"""
import uuid
from .account_utils import DEFAULT_HIERARCHICAL_ACCOUNTS
for name, account_type, description in DEFAULT_HIERARCHICAL_ACCOUNTS:
# Check if account already exists
existing = await db.fetchone(
"""
SELECT id FROM accounts WHERE name = :name
""",
{"name": name}
)
if not existing:
# Create new account
await db.execute(
f"""
INSERT INTO accounts (id, name, account_type, description, created_at)
VALUES (:id, :name, :type, :description, {db.timestamp_now})
""",
{
"id": str(uuid.uuid4()),
"name": name,
"type": account_type.value,
"description": description
}
)

View file

@ -1,3 +1,4 @@
<!DOCTYPE html>
{% extends "base.html" %} {% extends "base.html" %}
{% from "macros.jinja" import window_vars with context %} {% from "macros.jinja" import window_vars with context %}
@ -41,21 +42,21 @@
<!-- Tabs for different views --> <!-- Tabs for different views -->
<q-tabs v-model="activeTab" class="text-primary" dense> <q-tabs v-model="activeTab" class="text-primary" dense>
<q-tab name="by-user" icon="people" label="By User" /> <q-tab name="by-user" icon="people" label="By User"></q-tab>
<q-tab name="by-account" icon="account_balance" label="By Account" /> <q-tab name="by-account" icon="account_balance" label="By Account"></q-tab>
</q-tabs> </q-tabs>
<q-separator /> <q-separator></q-separator>
<q-tab-panels v-model="activeTab" animated> <q-tab-panels v-model="activeTab" animated>
<!-- By User View --> <!-- By User View -->
<q-tab-panel name="by-user"> <q-tab-panel name="by-user">
<div v-if="loading" class="row justify-center q-pa-md"> <div v-if="loading" class="row justify-center q-pa-md">
<q-spinner color="primary" size="3em" /> <q-spinner color="primary" size="3em"></q-spinner>
</div> </div>
<div v-else-if="permissionsByUser.size === 0" class="text-center q-pa-md"> <div v-else-if="permissionsByUser.size === 0" class="text-center q-pa-md">
<q-icon name="info" size="3em" color="grey-5" /> <q-icon name="info" size="3em" color="grey-5"></q-icon>
<p class="text-grey-6">No permissions granted yet</p> <p class="text-grey-6">No permissions granted yet</p>
</div> </div>
@ -63,14 +64,14 @@
<q-card v-for="[userId, userPerms] in permissionsByUser" :key="userId" flat bordered> <q-card v-for="[userId, userPerms] in permissionsByUser" :key="userId" flat bordered>
<q-card-section> <q-card-section>
<div class="text-h6 q-mb-sm"> <div class="text-h6 q-mb-sm">
<q-icon name="person" class="q-mr-sm" /> <q-icon name="person" class="q-mr-sm"></q-icon>
User: {% raw %}{{ userId }}{% endraw %} User: {% raw %}{{ userId }}{% endraw %}
</div> </div>
<q-list separator> <q-list separator>
<q-item v-for="perm in userPerms" :key="perm.id"> <q-item v-for="perm in userPerms" :key="perm.id">
<q-item-section avatar> <q-item-section avatar>
<q-avatar :color="getPermissionColor(perm.permission_type)" text-color="white" size="sm"> <q-avatar :color="getPermissionColor(perm.permission_type)" text-color="white" size="sm">
<q-icon :name="getPermissionIcon(perm.permission_type)" /> <q-icon :name="getPermissionIcon(perm.permission_type)"></q-icon>
</q-avatar> </q-avatar>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
@ -112,11 +113,11 @@
<!-- By Account View --> <!-- By Account View -->
<q-tab-panel name="by-account"> <q-tab-panel name="by-account">
<div v-if="loading" class="row justify-center q-pa-md"> <div v-if="loading" class="row justify-center q-pa-md">
<q-spinner color="primary" size="3em" /> <q-spinner color="primary" size="3em"></q-spinner>
</div> </div>
<div v-else-if="permissionsByAccount.size === 0" class="text-center q-pa-md"> <div v-else-if="permissionsByAccount.size === 0" class="text-center q-pa-md">
<q-icon name="info" size="3em" color="grey-5" /> <q-icon name="info" size="3em" color="grey-5"></q-icon>
<p class="text-grey-6">No permissions granted yet</p> <p class="text-grey-6">No permissions granted yet</p>
</div> </div>
@ -124,14 +125,14 @@
<q-card v-for="[accountId, accountPerms] in permissionsByAccount" :key="accountId" flat bordered> <q-card v-for="[accountId, accountPerms] in permissionsByAccount" :key="accountId" flat bordered>
<q-card-section> <q-card-section>
<div class="text-h6 q-mb-sm"> <div class="text-h6 q-mb-sm">
<q-icon name="account_balance" class="q-mr-sm" /> <q-icon name="account_balance" class="q-mr-sm"></q-icon>
{% raw %}{{ getAccountName(accountId) }}{% endraw %} {% raw %}{{ getAccountName(accountId) }}{% endraw %}
</div> </div>
<q-list separator> <q-list separator>
<q-item v-for="perm in accountPerms" :key="perm.id"> <q-item v-for="perm in accountPerms" :key="perm.id">
<q-item-section avatar> <q-item-section avatar>
<q-avatar :color="getPermissionColor(perm.permission_type)" text-color="white" size="sm"> <q-avatar :color="getPermissionColor(perm.permission_type)" text-color="white" size="sm">
<q-icon :name="getPermissionIcon(perm.permission_type)" /> <q-icon :name="getPermissionIcon(perm.permission_type)"></q-icon>
</q-avatar> </q-avatar>
</q-item-section> </q-item-section>
<q-item-section> <q-item-section>
@ -196,7 +197,7 @@
:rules="[val => !!val || 'User ID is required']" :rules="[val => !!val || 'User ID is required']"
> >
<template v-slot:prepend> <template v-slot:prepend>
<q-icon name="person" /> <q-icon name="person"></q-icon>
</template> </template>
</q-input> </q-input>
@ -215,7 +216,7 @@
:rules="[val => !!val || 'Account is required']" :rules="[val => !!val || 'Account is required']"
> >
<template v-slot:prepend> <template v-slot:prepend>
<q-icon name="account_balance" /> <q-icon name="account_balance"></q-icon>
</template> </template>
</q-select> </q-select>
@ -234,7 +235,7 @@
:rules="[val => !!val || 'Permission type is required']" :rules="[val => !!val || 'Permission type is required']"
> >
<template v-slot:prepend> <template v-slot:prepend>
<q-icon name="security" /> <q-icon name="security"></q-icon>
</template> </template>
<template v-slot:option="scope"> <template v-slot:option="scope">
<q-item v-bind="scope.itemProps"> <q-item v-bind="scope.itemProps">
@ -256,7 +257,7 @@
dense dense
> >
<template v-slot:prepend> <template v-slot:prepend>
<q-icon name="event" /> <q-icon name="event"></q-icon>
</template> </template>
</q-input> </q-input>
@ -271,13 +272,13 @@
rows="3" rows="3"
> >
<template v-slot:prepend> <template v-slot:prepend>
<q-icon name="note" /> <q-icon name="note"></q-icon>
</template> </template>
</q-input> </q-input>
</q-card-section> </q-card-section>
<q-card-actions align="right"> <q-card-actions align="right">
<q-btn flat label="Cancel" color="grey" v-close-popup /> <q-btn flat label="Cancel" color="grey" v-close-popup></q-btn>
<q-btn <q-btn
unelevated unelevated
label="Grant Permission" label="Grant Permission"
@ -285,7 +286,7 @@
@click="grantPermission" @click="grantPermission"
:loading="granting" :loading="granting"
:disable="!isGrantFormValid" :disable="!isGrantFormValid"
/> ></q-btn>
</q-card-actions> </q-card-actions>
</q-card> </q-card>
</q-dialog> </q-dialog>
@ -322,14 +323,14 @@
</q-card-section> </q-card-section>
<q-card-actions align="right"> <q-card-actions align="right">
<q-btn flat label="Cancel" color="grey" v-close-popup /> <q-btn flat label="Cancel" color="grey" v-close-popup></q-btn>
<q-btn <q-btn
unelevated unelevated
label="Revoke" label="Revoke"
color="negative" color="negative"
@click="revokePermission" @click="revokePermission"
:loading="revoking" :loading="revoking"
/> ></q-btn>
</q-card-actions> </q-card-actions>
</q-card> </q-card>
</q-dialog> </q-dialog>