release: publish saldo 0.1.0
This commit is contained in:
+462
@@ -0,0 +1,462 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from app.extensions import db
|
||||
from app.models import (
|
||||
Account,
|
||||
Category,
|
||||
CommunityAccount,
|
||||
CostParticipant,
|
||||
Entry,
|
||||
EntryShareRule,
|
||||
MonthlyIncome,
|
||||
NotificationPreference,
|
||||
User,
|
||||
)
|
||||
|
||||
|
||||
ACCOUNT_TREE = {
|
||||
"gemeinschaftskonto": {
|
||||
"name": "Budgets",
|
||||
"categories": {
|
||||
"wohnen": ["Miete", "Zusatzmiete"],
|
||||
"fixkosten": [
|
||||
"Internet",
|
||||
"Mobilfunk 1",
|
||||
"Mobilfunk 2",
|
||||
"Rechtsschutz",
|
||||
"Haftpflicht",
|
||||
"Zusatzversicherung",
|
||||
"Altersvorsorge",
|
||||
"Rundfunkbeitrag",
|
||||
],
|
||||
"mitgliedschaften": [
|
||||
"Vereinsbeitrag",
|
||||
"Magazin",
|
||||
"Lernplattform",
|
||||
"Streaming 1",
|
||||
"Streaming 2",
|
||||
"Streaming 3",
|
||||
"Cloudspeicher",
|
||||
"Tabler",
|
||||
],
|
||||
"technik": ["Server", "Hosting", "Domains", "Software"],
|
||||
"energie": ["Strom", "Gas"],
|
||||
"haushalt": ["Lebensmittel", "Drogerie", "Haushaltsbedarf"],
|
||||
"mobilitaet": ["Auto", "Fahrrad"],
|
||||
"finanzen": ["Bankgebuehren", "Kreditrate 1", "Kreditrate 2"],
|
||||
},
|
||||
},
|
||||
"sparen": {"name": "Sparen", "categories": {"sparen": ["Sparziel"]}},
|
||||
"urlaub": {"name": "Urlaub", "categories": {"urlaub": ["Reisebudget"]}},
|
||||
"freizeit": {"name": "Freizeit", "categories": {"freizeit": ["Freizeitbudget"]}},
|
||||
"persoenlich-flo": {"name": "Persönlich 1", "categories": {"persoenliche-auszahlung": ["Person 1"]}},
|
||||
"persoenlich-desi": {"name": "Persönlich 2", "categories": {"persoenliche-auszahlung": ["Person 2"]}},
|
||||
}
|
||||
|
||||
COMMUNITY_ACCOUNT_DEFAULTS = [
|
||||
{
|
||||
"name": "Hauptkonto",
|
||||
"slug": "hauptkonto",
|
||||
"account_type": "shared",
|
||||
"linked_account_slug": None,
|
||||
},
|
||||
{
|
||||
"name": "Privatkonto 1",
|
||||
"slug": "privatkonto-1",
|
||||
"account_type": "personal",
|
||||
"linked_account_slug": "persoenlich-flo",
|
||||
},
|
||||
{
|
||||
"name": "Privatkonto 2",
|
||||
"slug": "privatkonto-2",
|
||||
"account_type": "personal",
|
||||
"linked_account_slug": "persoenlich-desi",
|
||||
},
|
||||
]
|
||||
|
||||
LEGACY_COMMUNITY_ACCOUNT_SLUGS = {
|
||||
"hauptkonto": "gemeinschaftskonto",
|
||||
"privatkonto-1": "flo-privat",
|
||||
"privatkonto-2": "desi-privat",
|
||||
}
|
||||
|
||||
LEGACY_CATEGORY_SLUGS = {
|
||||
("gemeinschaftskonto", "wohnen"): "miete",
|
||||
("gemeinschaftskonto", "fixkosten"): "kommunikation",
|
||||
("gemeinschaftskonto", "mitgliedschaften"): "abos",
|
||||
("gemeinschaftskonto", "technik"): "server",
|
||||
("gemeinschaftskonto", "finanzen"): "schulden",
|
||||
("persoenlich-flo", "persoenliche-auszahlung"): "flo",
|
||||
("persoenlich-desi", "persoenliche-auszahlung"): "desi",
|
||||
}
|
||||
|
||||
LEGACY_ENTRY_NAMES = {
|
||||
("persoenlich-flo", "persoenliche-auszahlung", "Person 1"): "persönliche Auszahlung",
|
||||
("persoenlich-desi", "persoenliche-auszahlung", "Person 2"): "persönliche Auszahlung",
|
||||
}
|
||||
|
||||
ENTRY_TARGET_CATEGORY = {
|
||||
"Miete": "wohnen",
|
||||
"Zusatzmiete": "wohnen",
|
||||
"Internet": "fixkosten",
|
||||
"Mobilfunk 1": "fixkosten",
|
||||
"Mobilfunk 2": "fixkosten",
|
||||
"Rechtsschutz": "fixkosten",
|
||||
"Haftpflicht": "fixkosten",
|
||||
"Zusatzversicherung": "fixkosten",
|
||||
"Altersvorsorge": "fixkosten",
|
||||
"Rundfunkbeitrag": "fixkosten",
|
||||
"Lebensmittel": "haushalt",
|
||||
"Drogerie": "haushalt",
|
||||
"Haushaltsbedarf": "haushalt",
|
||||
"Auto": "mobilitaet",
|
||||
"Fahrrad": "mobilitaet",
|
||||
"Vereinsbeitrag": "mitgliedschaften",
|
||||
"Magazin": "mitgliedschaften",
|
||||
"Lernplattform": "mitgliedschaften",
|
||||
"Streaming 1": "mitgliedschaften",
|
||||
"Streaming 2": "mitgliedschaften",
|
||||
"Streaming 3": "mitgliedschaften",
|
||||
"Cloudspeicher": "mitgliedschaften",
|
||||
"Tabler": "mitgliedschaften",
|
||||
"Server": "technik",
|
||||
"Hosting": "technik",
|
||||
"Domains": "technik",
|
||||
"Software": "technik",
|
||||
"Bankgebuehren": "finanzen",
|
||||
"Kreditrate 1": "finanzen",
|
||||
"Kreditrate 2": "finanzen",
|
||||
}
|
||||
|
||||
|
||||
def slugify(value: str) -> str:
|
||||
return (
|
||||
value.lower()
|
||||
.replace(" ", "-")
|
||||
.replace("ö", "oe")
|
||||
.replace("ü", "ue")
|
||||
.replace("ä", "ae")
|
||||
.replace("/", "-")
|
||||
.replace("+", "plus")
|
||||
)
|
||||
|
||||
|
||||
def seed_data() -> None:
|
||||
# Basisdaten nur für die fachliche Grundstruktur, ohne Demo-Benutzer,
|
||||
# Beispiel-Personen oder vorausgefüllte Monatsdaten.
|
||||
community_accounts = {}
|
||||
for sort_order, data in enumerate(COMMUNITY_ACCOUNT_DEFAULTS, start=1):
|
||||
community_account = CommunityAccount.query.filter_by(slug=data["slug"]).first()
|
||||
if community_account is None and LEGACY_COMMUNITY_ACCOUNT_SLUGS.get(data["slug"]):
|
||||
community_account = CommunityAccount.query.filter_by(
|
||||
slug=LEGACY_COMMUNITY_ACCOUNT_SLUGS[data["slug"]]
|
||||
).first()
|
||||
if community_account is None:
|
||||
community_account = CommunityAccount(
|
||||
name=data["name"],
|
||||
slug=data["slug"],
|
||||
account_type=data["account_type"],
|
||||
linked_account_slug=data["linked_account_slug"],
|
||||
sort_order=sort_order,
|
||||
is_active=True,
|
||||
)
|
||||
db.session.add(community_account)
|
||||
else:
|
||||
community_account.name = data["name"]
|
||||
community_account.slug = data["slug"]
|
||||
community_account.account_type = data["account_type"]
|
||||
community_account.linked_account_slug = data["linked_account_slug"]
|
||||
community_account.sort_order = sort_order
|
||||
community_account.is_active = True
|
||||
community_accounts[data["slug"]] = community_account
|
||||
|
||||
db.session.flush()
|
||||
|
||||
sort_order = 1
|
||||
account_categories = {}
|
||||
for account_slug, account_data in ACCOUNT_TREE.items():
|
||||
account = Account.query.filter_by(slug=account_slug).first()
|
||||
if account is None:
|
||||
account = Account(
|
||||
name=account_data["name"],
|
||||
slug=account_slug,
|
||||
sort_order=sort_order,
|
||||
is_active=True,
|
||||
)
|
||||
db.session.add(account)
|
||||
db.session.flush()
|
||||
else:
|
||||
account.name = account_data["name"]
|
||||
account.sort_order = sort_order
|
||||
account.is_active = True
|
||||
if account_slug == "freizeit":
|
||||
account.sort_order = 2
|
||||
sort_order += 1
|
||||
category_sort = 1
|
||||
account_categories[account_slug] = {}
|
||||
for category_slug, entries in account_data["categories"].items():
|
||||
category = Category.query.filter_by(account_id=account.id, slug=category_slug).first()
|
||||
legacy_slug = LEGACY_CATEGORY_SLUGS.get((account_slug, category_slug))
|
||||
if category is None and legacy_slug:
|
||||
category = Category.query.filter_by(account_id=account.id, slug=legacy_slug).first()
|
||||
if category is None:
|
||||
category = Category(
|
||||
account_id=account.id,
|
||||
community_account_id=(
|
||||
community_accounts["hauptkonto"].id
|
||||
if account_slug == "gemeinschaftskonto"
|
||||
else None
|
||||
),
|
||||
name=category_slug.replace("-", " ").title(),
|
||||
slug=category_slug,
|
||||
sort_order=category_sort,
|
||||
is_active=True,
|
||||
)
|
||||
db.session.add(category)
|
||||
db.session.flush()
|
||||
else:
|
||||
category.name = category_slug.replace("-", " ").title()
|
||||
category.slug = category_slug
|
||||
category.sort_order = category_sort
|
||||
category.is_active = True
|
||||
if account_slug == "gemeinschaftskonto" and category.community_account_id is None:
|
||||
category.community_account_id = community_accounts["hauptkonto"].id
|
||||
account_categories[account_slug][category_slug] = category
|
||||
category_sort += 1
|
||||
for index, entry_name in enumerate(entries, start=1):
|
||||
default_amount = Decimal("0.00")
|
||||
if entry_name == "Miete":
|
||||
default_amount = Decimal("920.00")
|
||||
elif entry_name == "Lebensmittel":
|
||||
default_amount = Decimal("520.00")
|
||||
elif entry_name == "Streaming 1":
|
||||
default_amount = Decimal("17.99")
|
||||
elif entry_name in {"Sparziel", "Reisebudget", "Freizeitbudget", "persönliche Auszahlung"}:
|
||||
default_amount = Decimal("0.00")
|
||||
entry_slug = slugify(entry_name)
|
||||
entry = Entry.query.filter_by(category_id=category.id, slug=entry_slug).first()
|
||||
if entry is None:
|
||||
entry = (
|
||||
Entry.query.join(Category)
|
||||
.filter(
|
||||
Category.account_id == account.id,
|
||||
Entry.slug == entry_slug,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if entry is not None:
|
||||
entry.category_id = category.id
|
||||
legacy_entry_name = LEGACY_ENTRY_NAMES.get((account_slug, category_slug, entry_name))
|
||||
if entry is None and legacy_entry_name:
|
||||
entry = Entry.query.filter_by(
|
||||
category_id=category.id,
|
||||
slug=slugify(legacy_entry_name),
|
||||
).first()
|
||||
if entry is None:
|
||||
entry = Entry(
|
||||
category_id=category.id,
|
||||
name=entry_name,
|
||||
slug=entry_slug,
|
||||
default_amount=default_amount,
|
||||
amount_type="fixed",
|
||||
benefit_scope=(
|
||||
"all-users"
|
||||
),
|
||||
is_allocation_target=entry_name in {
|
||||
"Sparziel",
|
||||
"Reisebudget",
|
||||
"Freizeitbudget",
|
||||
"Person 1",
|
||||
"Person 2",
|
||||
},
|
||||
sort_order=index,
|
||||
is_active=True,
|
||||
)
|
||||
db.session.add(entry)
|
||||
db.session.flush()
|
||||
else:
|
||||
entry.name = entry_name
|
||||
entry.default_amount = default_amount
|
||||
entry.amount_type = "fixed"
|
||||
entry.benefit_scope = "all-users"
|
||||
entry.is_allocation_target = entry_name in {
|
||||
"Sparziel",
|
||||
"Reisebudget",
|
||||
"Freizeitbudget",
|
||||
"Person 1",
|
||||
"Person 2",
|
||||
}
|
||||
entry.sort_order = index
|
||||
entry.is_active = True
|
||||
|
||||
gemeinschaft = Account.query.filter_by(slug="gemeinschaftskonto").first()
|
||||
if gemeinschaft:
|
||||
target_categories = account_categories["gemeinschaftskonto"]
|
||||
with db.session.no_autoflush:
|
||||
for category in gemeinschaft.categories:
|
||||
for entry in list(category.entries):
|
||||
target_slug = ENTRY_TARGET_CATEGORY.get(entry.name)
|
||||
if not target_slug:
|
||||
continue
|
||||
target_category = target_categories.get(target_slug)
|
||||
if not target_category:
|
||||
continue
|
||||
existing_target_entry = Entry.query.filter_by(
|
||||
category_id=target_category.id,
|
||||
slug=entry.slug,
|
||||
).first()
|
||||
if existing_target_entry and existing_target_entry.id != entry.id:
|
||||
for monthly_value in entry.monthly_values:
|
||||
monthly_value.entry_id = existing_target_entry.id
|
||||
existing_rule_participants = {
|
||||
rule.participant_id for rule in existing_target_entry.share_rules
|
||||
}
|
||||
for rule in list(entry.share_rules):
|
||||
if rule.participant_id in existing_rule_participants:
|
||||
db.session.delete(rule)
|
||||
continue
|
||||
rule.entry_id = existing_target_entry.id
|
||||
db.session.delete(entry)
|
||||
continue
|
||||
entry.category_id = target_category.id
|
||||
entry.benefit_scope = "all-users"
|
||||
entry.is_allocation_target = entry.name in {
|
||||
"Sparziel",
|
||||
"Reisebudget",
|
||||
"Freizeitbudget",
|
||||
"Person 1",
|
||||
"Person 2",
|
||||
}
|
||||
for category in gemeinschaft.categories:
|
||||
if category.slug not in ACCOUNT_TREE["gemeinschaftskonto"]["categories"]:
|
||||
category.is_active = False
|
||||
elif category.community_account_id is None:
|
||||
category.community_account_id = community_accounts["hauptkonto"].id
|
||||
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def seed_demo_data() -> None:
|
||||
from datetime import date
|
||||
from flask import current_app
|
||||
|
||||
seed_data()
|
||||
|
||||
admin = User.query.filter_by(username="admin").first()
|
||||
if admin is None:
|
||||
admin = User(username="admin", display_name="Admin", email="admin@example.invalid", role="admin")
|
||||
admin.set_password("testpass")
|
||||
db.session.add(admin)
|
||||
|
||||
editor_a = User.query.filter_by(username="mitglied1").first()
|
||||
if editor_a is None:
|
||||
editor_a = User(
|
||||
username="mitglied1",
|
||||
display_name="Person A",
|
||||
email="person-a@example.invalid",
|
||||
role="editor",
|
||||
)
|
||||
editor_a.set_password("testpass")
|
||||
db.session.add(editor_a)
|
||||
|
||||
editor_b = User.query.filter_by(username="mitglied2").first()
|
||||
if editor_b is None:
|
||||
editor_b = User(
|
||||
username="mitglied2",
|
||||
display_name="Person B",
|
||||
email="person-b@example.invalid",
|
||||
role="editor",
|
||||
)
|
||||
editor_b.set_password("testpass")
|
||||
db.session.add(editor_b)
|
||||
|
||||
db.session.flush()
|
||||
|
||||
for user in [admin, editor_a, editor_b]:
|
||||
if user.notification_preference is None:
|
||||
db.session.add(NotificationPreference(user_id=user.id))
|
||||
|
||||
participants = {
|
||||
"Person A": CostParticipant.query.filter_by(name="Person A").first(),
|
||||
"Person B": CostParticipant.query.filter_by(name="Person B").first(),
|
||||
"Gast": CostParticipant.query.filter_by(name="Gast").first(),
|
||||
}
|
||||
if participants["Person A"] is None:
|
||||
participants["Person A"] = CostParticipant(
|
||||
name="Person A", is_app_user=True, linked_user_id=editor_a.id, is_external=False
|
||||
)
|
||||
db.session.add(participants["Person A"])
|
||||
participants["Person A"].is_app_user = True
|
||||
participants["Person A"].linked_user_id = editor_a.id
|
||||
participants["Person A"].is_external = False
|
||||
participants["Person A"].avatar_url = editor_a.avatar_url
|
||||
|
||||
if participants["Person B"] is None:
|
||||
participants["Person B"] = CostParticipant(
|
||||
name="Person B", is_app_user=True, linked_user_id=editor_b.id, is_external=False
|
||||
)
|
||||
db.session.add(participants["Person B"])
|
||||
participants["Person B"].is_app_user = True
|
||||
participants["Person B"].linked_user_id = editor_b.id
|
||||
participants["Person B"].is_external = False
|
||||
|
||||
if participants["Gast"] is None:
|
||||
participants["Gast"] = CostParticipant(name="Gast", is_app_user=False, is_external=True)
|
||||
db.session.add(participants["Gast"])
|
||||
participants["Gast"].is_app_user = False
|
||||
participants["Gast"].is_external = True
|
||||
|
||||
db.session.flush()
|
||||
|
||||
shared_entry = Entry.query.join(Category).filter(Entry.name == "Streaming 1").first()
|
||||
if shared_entry is not None:
|
||||
for person in participants.values():
|
||||
rule = EntryShareRule.query.filter_by(
|
||||
entry_id=shared_entry.id, participant_id=person.id
|
||||
).first()
|
||||
if rule is None:
|
||||
db.session.add(
|
||||
EntryShareRule(
|
||||
entry_id=shared_entry.id,
|
||||
participant_id=person.id,
|
||||
share_type="equal",
|
||||
)
|
||||
)
|
||||
|
||||
month_service = current_app.extensions["saldo.month_service"]
|
||||
month = month_service.ensure_month(date(2026, 4, 1))
|
||||
|
||||
while len(month.incomes) < 2:
|
||||
db.session.add(
|
||||
MonthlyIncome(
|
||||
month_id=month.id,
|
||||
label=f"Einkommen {len(month.incomes) + 1}",
|
||||
amount=Decimal("0.00"),
|
||||
sort_order=len(month.incomes) + 1,
|
||||
)
|
||||
)
|
||||
db.session.flush()
|
||||
|
||||
if not month.incomes:
|
||||
db.session.flush()
|
||||
for income in month.incomes:
|
||||
if income.sort_order == 1:
|
||||
income.label = "Einkommen 1"
|
||||
income.amount = Decimal("3100.00")
|
||||
else:
|
||||
income.label = "Einkommen 2"
|
||||
income.amount = Decimal("2450.00")
|
||||
for allocation in month.allocations:
|
||||
if allocation.target_account.slug == "sparen":
|
||||
allocation.amount = Decimal("350.00")
|
||||
elif allocation.target_account.slug == "urlaub":
|
||||
allocation.amount = Decimal("180.00")
|
||||
elif allocation.target_account.slug == "freizeit":
|
||||
allocation.amount = Decimal("120.00")
|
||||
else:
|
||||
allocation.amount = Decimal("220.00")
|
||||
|
||||
month_service.refresh_suggestions(month, "Demo-Daten initialisiert")
|
||||
db.session.commit()
|
||||
Reference in New Issue
Block a user