Update file metadata in manifest.json and change MinIO image source in docker-compose.yml
This commit is contained in:
@@ -17,9 +17,9 @@ from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.base import Base, PkMixin, TimestampMixin
|
||||
|
||||
ACCOUNT_KINDS = ("cash", "debit", "credit")
|
||||
ACCOUNT_KINDS = ("cash", "debit", "credit", "savings")
|
||||
ACCOUNT_STATUSES = ("active", "archived")
|
||||
OPERATION_TYPES = ("expense", "income", "transfer", "refund")
|
||||
OPERATION_TYPES = ("expense", "income", "transfer", "refund", "adjustment")
|
||||
OPERATION_STATUSES = ("confirmed", "voided")
|
||||
|
||||
|
||||
@@ -28,10 +28,13 @@ class Account(Base, PkMixin, TimestampMixin):
|
||||
|
||||
__tablename__ = "accounts"
|
||||
__table_args__ = (
|
||||
CheckConstraint("kind IN ('cash', 'debit', 'credit')", name="kind_valid"),
|
||||
CheckConstraint(
|
||||
"kind IN ('cash', 'debit', 'credit', 'savings')", name="kind_valid"
|
||||
),
|
||||
CheckConstraint("status IN ('active', 'archived')", name="status_valid"),
|
||||
CheckConstraint("currency = 'RUB'", name="currency_rub_only"),
|
||||
UniqueConstraint("space_id", "id", name="uq_accounts_space_id"),
|
||||
UniqueConstraint("space_id", "system_key", name="uq_accounts_space_system_key"),
|
||||
)
|
||||
|
||||
space_id: Mapped[uuid.UUID] = mapped_column(
|
||||
@@ -45,6 +48,7 @@ class Account(Base, PkMixin, TimestampMixin):
|
||||
bank_code: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||||
masked_identifier: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||||
fingerprint: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||||
system_key: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
status: Mapped[str] = mapped_column(String(16), nullable=False, default="active")
|
||||
version: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
|
||||
|
||||
@@ -56,7 +60,10 @@ class Operation(Base, PkMixin, TimestampMixin):
|
||||
|
||||
__tablename__ = "operations"
|
||||
__table_args__ = (
|
||||
CheckConstraint("type IN ('expense', 'income', 'transfer', 'refund')", name="type_valid"),
|
||||
CheckConstraint(
|
||||
"type IN ('expense', 'income', 'transfer', 'refund', 'adjustment')",
|
||||
name="type_valid",
|
||||
),
|
||||
CheckConstraint("status IN ('confirmed', 'voided')", name="status_valid"),
|
||||
CheckConstraint("amount_kopecks > 0", name="amount_positive"),
|
||||
CheckConstraint(
|
||||
@@ -114,6 +121,8 @@ class Operation(Base, PkMixin, TimestampMixin):
|
||||
description: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||
void_reason: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||
request_key: Mapped[str | None] = mapped_column(String(120), nullable=True)
|
||||
converted_from_expense: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
previous_category_id: Mapped[uuid.UUID | None] = mapped_column(Uuid, nullable=True)
|
||||
created_by_user_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||
Uuid, ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
@@ -168,6 +177,9 @@ class Category(Base, PkMixin, TimestampMixin):
|
||||
name="fk_categories_linked_account_tenant",
|
||||
),
|
||||
UniqueConstraint("space_id", "id", name="uq_categories_space_id"),
|
||||
UniqueConstraint(
|
||||
"space_id", "system_key", "linked_account_id", name="uq_categories_system_pair"
|
||||
),
|
||||
)
|
||||
|
||||
space_id: Mapped[uuid.UUID] = mapped_column(
|
||||
|
||||
@@ -52,6 +52,14 @@ class OperationPatchBody(BaseModel):
|
||||
category_id: uuid.UUID | None = Field(default=None, alias="categoryId")
|
||||
|
||||
|
||||
class AdjustBalanceBody(BaseModel):
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
target_balance_kopecks: int = Field(ge=0, alias="targetBalanceKopecks")
|
||||
occurred_on: date = Field(alias="occurredOn")
|
||||
reason: str = Field(min_length=1, max_length=500)
|
||||
|
||||
|
||||
def account_json(db: Db, account: Any) -> dict:
|
||||
return {
|
||||
"id": account.id,
|
||||
@@ -62,6 +70,8 @@ def account_json(db: Db, account: Any) -> dict:
|
||||
"openedOn": account.opened_on,
|
||||
"bankCode": account.bank_code,
|
||||
"maskedNumber": account.masked_identifier,
|
||||
"systemKey": account.system_key,
|
||||
"nameKey": f"account.{account.system_key}" if account.system_key else None,
|
||||
"status": account.status,
|
||||
"balanceKopecks": service.account_balance(db, account),
|
||||
"version": account.version,
|
||||
@@ -91,6 +101,7 @@ def operation_json(db: Db, operation: Any) -> dict:
|
||||
"postedOn": operation.posted_on,
|
||||
"description": operation.description,
|
||||
"voidReason": operation.void_reason,
|
||||
"convertedFromExpense": operation.converted_from_expense,
|
||||
"version": operation.version,
|
||||
"createdAt": operation.created_at,
|
||||
}
|
||||
@@ -276,3 +287,46 @@ def patch_operation(
|
||||
category_id=body.category_id,
|
||||
)
|
||||
return operation_json(db, operation)
|
||||
|
||||
|
||||
@router.post("/accounts/{account_id}/adjust-balance")
|
||||
def adjust_balance(
|
||||
account_id: uuid.UUID, body: AdjustBalanceBody, db: Db, context: Context, _csrf: Csrf
|
||||
) -> dict:
|
||||
from app.ledger import savings
|
||||
|
||||
operation, old_balance, new_balance = savings.adjust_balance(
|
||||
db,
|
||||
space_id=context.space.id,
|
||||
user=context.user,
|
||||
account_id=account_id,
|
||||
target_balance_kopecks=body.target_balance_kopecks,
|
||||
occurred_on=body.occurred_on,
|
||||
reason=body.reason,
|
||||
)
|
||||
payload = operation_json(db, operation)
|
||||
payload["oldBalanceKopecks"] = old_balance
|
||||
payload["newBalanceKopecks"] = new_balance
|
||||
return payload
|
||||
|
||||
|
||||
@router.post("/operations/{operation_id}/savings-transfer")
|
||||
def savings_transfer(operation_id: uuid.UUID, db: Db, context: Context, _csrf: Csrf) -> dict:
|
||||
from app.ledger import savings
|
||||
|
||||
operation = savings.convert_expense_to_savings(
|
||||
db, space_id=context.space.id, user=context.user, operation_id=operation_id
|
||||
)
|
||||
return operation_json(db, operation)
|
||||
|
||||
|
||||
@router.post("/operations/{operation_id}/savings-transfer/revert")
|
||||
def savings_transfer_revert(
|
||||
operation_id: uuid.UUID, db: Db, context: Context, _csrf: Csrf
|
||||
) -> dict:
|
||||
from app.ledger import savings
|
||||
|
||||
operation = savings.revert_savings_conversion(
|
||||
db, space_id=context.space.id, user=context.user, operation_id=operation_id
|
||||
)
|
||||
return operation_json(db, operation)
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
"""System savings account/envelope, balance adjustments and reversible conversions.
|
||||
|
||||
The savings "convert" identity is defined exclusively by ``system_key``:
|
||||
user-created accounts or categories that merely share the name «Накопления»
|
||||
are never captured. Every command is tenant-scoped, period-guarded and
|
||||
audited; balances stay derived (postings only, never stored mutations).
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import date
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.audit import service as audit
|
||||
from app.errors import bad_request, conflict, not_found
|
||||
from app.identity.models import User
|
||||
from app.ledger.models import Account, Category, Operation, Posting
|
||||
|
||||
SAVINGS_SYSTEM_KEY = "savings"
|
||||
SAVINGS_ACCOUNT_NAME = "Накопления"
|
||||
|
||||
|
||||
def get_savings_account(db: Session, *, space_id: uuid.UUID) -> Account | None:
|
||||
return (
|
||||
db.query(Account)
|
||||
.filter_by(space_id=space_id, system_key=SAVINGS_SYSTEM_KEY)
|
||||
.one_or_none()
|
||||
)
|
||||
|
||||
|
||||
def _savings_category(db: Session, *, space_id: uuid.UUID) -> Category | None:
|
||||
return (
|
||||
db.query(Category)
|
||||
.filter_by(space_id=space_id, system_key=SAVINGS_SYSTEM_KEY)
|
||||
.one_or_none()
|
||||
)
|
||||
|
||||
|
||||
def ensure_savings_resources(db: Session, *, space_id: uuid.UUID) -> tuple[Account, Category]:
|
||||
"""Idempotently return the single system savings account and its envelope.
|
||||
|
||||
Any duplicate or inconsistent system pair is an explicit conflict, never a
|
||||
random pick: identity is keyed by ``system_key`` only.
|
||||
"""
|
||||
accounts = db.query(Account).filter_by(space_id=space_id, system_key=SAVINGS_SYSTEM_KEY).all()
|
||||
if len(accounts) > 1:
|
||||
raise conflict(
|
||||
"LEDGER_SAVINGS_CONFLICT",
|
||||
"Multiple system savings accounts exist in this space",
|
||||
)
|
||||
account: Account | None = accounts[0] if accounts else None
|
||||
created = False
|
||||
if account is None:
|
||||
account = Account(
|
||||
space_id=space_id,
|
||||
name=SAVINGS_ACCOUNT_NAME,
|
||||
kind="savings",
|
||||
system_key=SAVINGS_SYSTEM_KEY,
|
||||
opened_on=date.today(),
|
||||
opening_amount_kopecks=0,
|
||||
)
|
||||
db.add(account)
|
||||
db.flush()
|
||||
created = True
|
||||
categories = (
|
||||
db.query(Category).filter_by(space_id=space_id, system_key=SAVINGS_SYSTEM_KEY).all()
|
||||
)
|
||||
if len(categories) > 1:
|
||||
raise conflict(
|
||||
"LEDGER_SAVINGS_CONFLICT",
|
||||
"Multiple system savings categories exist in this space",
|
||||
)
|
||||
category: Category | None = categories[0] if categories else None
|
||||
if category is not None and category.linked_account_id != account.id:
|
||||
raise conflict(
|
||||
"LEDGER_SAVINGS_CONFLICT",
|
||||
"System savings category is linked to a different account",
|
||||
)
|
||||
if category is None:
|
||||
category = Category(
|
||||
space_id=space_id,
|
||||
kind="savings_envelope",
|
||||
system_key=SAVINGS_SYSTEM_KEY,
|
||||
linked_account_id=account.id,
|
||||
)
|
||||
db.add(category)
|
||||
db.flush()
|
||||
created = True
|
||||
if created:
|
||||
audit.record(
|
||||
db,
|
||||
event_type="savings.resources_ensured",
|
||||
space_id=space_id,
|
||||
detail={
|
||||
"account_id": str(account.id),
|
||||
"category_id": str(category.id),
|
||||
},
|
||||
)
|
||||
return account, category
|
||||
|
||||
|
||||
def transfer_category_for_accounts(
|
||||
db: Session, *, space_id: uuid.UUID, source: Account, target: Account
|
||||
) -> Category | None:
|
||||
"""System category for a manual transfer touching the savings account."""
|
||||
if SAVINGS_SYSTEM_KEY not in {source.system_key, target.system_key}:
|
||||
return None
|
||||
_, category = ensure_savings_resources(db, space_id=space_id)
|
||||
return category
|
||||
|
||||
|
||||
def adjust_balance(
|
||||
db: Session,
|
||||
*,
|
||||
space_id: uuid.UUID,
|
||||
user: User,
|
||||
account_id: uuid.UUID,
|
||||
target_balance_kopecks: int,
|
||||
occurred_on: date,
|
||||
reason: str,
|
||||
) -> tuple[Operation, int, int]:
|
||||
"""Reconcile a derived balance with reality via one audited adjustment.
|
||||
|
||||
The operation carries one signed posting for the delta; ``amount_kopecks``
|
||||
stays positive and the sign lives in the posting. Adjustments never touch
|
||||
income/expense totals, category spend, cash flow or the budget pool.
|
||||
"""
|
||||
if not reason.strip():
|
||||
raise bad_request("LEDGER_ADJUST_REASON_REQUIRED", "An adjustment reason is required")
|
||||
account = (
|
||||
db.query(Account)
|
||||
.filter_by(id=account_id, space_id=space_id)
|
||||
.with_for_update()
|
||||
.one_or_none()
|
||||
)
|
||||
if account is None:
|
||||
raise not_found("LEDGER_ACCOUNT_NOT_FOUND", "Account not found")
|
||||
if account.status != "active":
|
||||
raise conflict("LEDGER_ACCOUNT_ARCHIVED", "Account is archived")
|
||||
if account.kind == "credit":
|
||||
raise conflict(
|
||||
"LEDGER_ADJUST_CREDIT_UNSUPPORTED",
|
||||
"Credit account balances cannot be adjusted with a non-negative target",
|
||||
)
|
||||
if target_balance_kopecks < 0:
|
||||
raise bad_request(
|
||||
"LEDGER_ADJUST_TARGET_NEGATIVE", "Target balance cannot be negative"
|
||||
)
|
||||
from app.ledger.service import account_balance, ensure_period_open_for_date
|
||||
|
||||
ensure_period_open_for_date(db, space_id=space_id, day=occurred_on)
|
||||
old_balance = account_balance(db, account)
|
||||
delta = target_balance_kopecks - old_balance
|
||||
if delta == 0:
|
||||
raise conflict(
|
||||
"LEDGER_ADJUST_NOOP",
|
||||
"Balance already equals the target; nothing to adjust",
|
||||
details={"balance_kopecks": old_balance},
|
||||
)
|
||||
operation = Operation(
|
||||
space_id=space_id,
|
||||
type="adjustment",
|
||||
amount_kopecks=abs(delta),
|
||||
account_id=account.id,
|
||||
occurred_on=occurred_on,
|
||||
description=reason.strip(),
|
||||
created_by_user_id=user.id,
|
||||
)
|
||||
db.add(operation)
|
||||
db.flush()
|
||||
db.add(
|
||||
Posting(
|
||||
space_id=space_id,
|
||||
operation_id=operation.id,
|
||||
account_id=account.id,
|
||||
amount_kopecks=delta,
|
||||
)
|
||||
)
|
||||
audit.record(
|
||||
db,
|
||||
event_type="account.balance_adjusted",
|
||||
space_id=space_id,
|
||||
user_id=user.id,
|
||||
detail={
|
||||
"account_id": str(account.id),
|
||||
"operation_id": str(operation.id),
|
||||
"old_balance_kopecks": old_balance,
|
||||
"new_balance_kopecks": target_balance_kopecks,
|
||||
"delta_kopecks": delta,
|
||||
"reason": reason.strip(),
|
||||
},
|
||||
)
|
||||
return operation, old_balance, target_balance_kopecks
|
||||
|
||||
|
||||
def convert_expense_to_savings(
|
||||
db: Session, *, space_id: uuid.UUID, user: User, operation_id: uuid.UUID
|
||||
) -> Operation:
|
||||
"""Turn a confirmed expense into a transfer into the system savings account.
|
||||
|
||||
Keeps the original negative posting plus description, payee, request key
|
||||
and import provenance; adds one positive posting on the savings account and
|
||||
remembers the previous category for exact reversion.
|
||||
"""
|
||||
operation = (
|
||||
db.query(Operation)
|
||||
.filter_by(id=operation_id, space_id=space_id)
|
||||
.with_for_update()
|
||||
.one_or_none()
|
||||
)
|
||||
if operation is None:
|
||||
raise not_found("LEDGER_OPERATION_NOT_FOUND", "Operation not found")
|
||||
if operation.status != "confirmed":
|
||||
raise conflict(
|
||||
"LEDGER_SAVINGS_CONVERT_INVALID",
|
||||
"Only confirmed operations can be converted",
|
||||
)
|
||||
if operation.type != "expense":
|
||||
raise conflict(
|
||||
"LEDGER_SAVINGS_CONVERT_INVALID",
|
||||
"Only expenses can be converted into savings transfers",
|
||||
)
|
||||
if operation.converted_from_expense:
|
||||
raise conflict(
|
||||
"LEDGER_SAVINGS_CONVERT_INVALID",
|
||||
"Operation is already a savings transfer",
|
||||
)
|
||||
source_account = (
|
||||
db.query(Account).filter_by(id=operation.account_id, space_id=space_id).one_or_none()
|
||||
)
|
||||
if source_account is None:
|
||||
raise not_found("LEDGER_ACCOUNT_NOT_FOUND", "Account not found")
|
||||
if source_account.system_key is not None:
|
||||
raise conflict(
|
||||
"LEDGER_SAVINGS_CONVERT_INVALID",
|
||||
"Operations on system accounts cannot be converted",
|
||||
)
|
||||
confirmed_refund = (
|
||||
db.query(Operation.id)
|
||||
.filter_by(
|
||||
space_id=space_id,
|
||||
parent_operation_id=operation.id,
|
||||
type="refund",
|
||||
status="confirmed",
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if confirmed_refund is not None:
|
||||
raise conflict(
|
||||
"LEDGER_SAVINGS_CONVERT_HAS_REFUNDS",
|
||||
"Expenses with confirmed refunds cannot be converted into savings transfers",
|
||||
)
|
||||
from app.ledger.service import ensure_period_open_for_date
|
||||
|
||||
ensure_period_open_for_date(db, space_id=space_id, day=operation.occurred_on)
|
||||
savings_account, savings_category = ensure_savings_resources(db, space_id=space_id)
|
||||
previous_category_id = operation.category_id
|
||||
operation.previous_category_id = previous_category_id
|
||||
operation.converted_from_expense = True
|
||||
operation.type = "transfer"
|
||||
operation.counter_account_id = savings_account.id
|
||||
operation.category_id = savings_category.id
|
||||
db.add(
|
||||
Posting(
|
||||
space_id=space_id,
|
||||
operation_id=operation.id,
|
||||
account_id=savings_account.id,
|
||||
amount_kopecks=operation.amount_kopecks,
|
||||
)
|
||||
)
|
||||
audit.record(
|
||||
db,
|
||||
event_type="operation.savings_transfer",
|
||||
space_id=space_id,
|
||||
user_id=user.id,
|
||||
detail={
|
||||
"operation_id": str(operation.id),
|
||||
"savings_account_id": str(savings_account.id),
|
||||
"amount_kopecks": operation.amount_kopecks,
|
||||
"previous_category_id": (
|
||||
str(previous_category_id) if previous_category_id else None
|
||||
),
|
||||
},
|
||||
)
|
||||
return operation
|
||||
|
||||
|
||||
def revert_savings_conversion(
|
||||
db: Session, *, space_id: uuid.UUID, user: User, operation_id: uuid.UUID
|
||||
) -> Operation:
|
||||
"""Undo a marked conversion: restore the expense type, category and postings."""
|
||||
operation = (
|
||||
db.query(Operation)
|
||||
.filter_by(id=operation_id, space_id=space_id)
|
||||
.with_for_update()
|
||||
.one_or_none()
|
||||
)
|
||||
if operation is None:
|
||||
raise not_found("LEDGER_OPERATION_NOT_FOUND", "Operation not found")
|
||||
if operation.status != "confirmed":
|
||||
raise conflict(
|
||||
"LEDGER_SAVINGS_REVERT_INVALID",
|
||||
"Only confirmed operations can be reverted",
|
||||
)
|
||||
if operation.type != "transfer" or not operation.converted_from_expense:
|
||||
raise conflict(
|
||||
"LEDGER_SAVINGS_REVERT_INVALID",
|
||||
"Only converted expenses can be reverted from savings",
|
||||
)
|
||||
from app.ledger.service import ensure_period_open_for_date
|
||||
|
||||
ensure_period_open_for_date(db, space_id=space_id, day=operation.occurred_on)
|
||||
counter_account_id = operation.counter_account_id
|
||||
if counter_account_id is not None:
|
||||
savings_posting = (
|
||||
db.query(Posting)
|
||||
.filter_by(operation_id=operation.id, account_id=counter_account_id)
|
||||
.one_or_none()
|
||||
)
|
||||
if savings_posting is not None:
|
||||
db.delete(savings_posting)
|
||||
operation.type = "expense"
|
||||
operation.counter_account_id = None
|
||||
operation.category_id = operation.previous_category_id
|
||||
operation.converted_from_expense = False
|
||||
operation.previous_category_id = None
|
||||
audit.record(
|
||||
db,
|
||||
event_type="operation.savings_transfer_reverted",
|
||||
space_id=space_id,
|
||||
user_id=user.id,
|
||||
detail={"operation_id": str(operation.id)},
|
||||
)
|
||||
return operation
|
||||
@@ -48,6 +48,15 @@ def _require_active_account(db: Session, *, space_id: uuid.UUID, account_id: uui
|
||||
return account
|
||||
|
||||
|
||||
def _reject_system_account(account: Account) -> None:
|
||||
"""System accounts (savings) are journal-internal and never statement-facing."""
|
||||
if account.system_key is not None:
|
||||
raise bad_request(
|
||||
"LEDGER_SYSTEM_ACCOUNT_NOT_EXTERNAL",
|
||||
"System accounts cannot host external income or expense operations",
|
||||
)
|
||||
|
||||
|
||||
def _validate_category(db: Session, *, space_id: uuid.UUID, category_id: uuid.UUID | None) -> None:
|
||||
if category_id is None:
|
||||
return
|
||||
@@ -58,6 +67,11 @@ def _validate_category(db: Session, *, space_id: uuid.UUID, category_id: uuid.UU
|
||||
raise not_found("BUDGET_CATEGORY_NOT_FOUND", "Category not found")
|
||||
if category.archived:
|
||||
raise conflict("BUDGET_CATEGORY_ARCHIVED", "Category is archived")
|
||||
if category.system_key is not None:
|
||||
raise conflict(
|
||||
"BUDGET_CATEGORY_SYSTEM",
|
||||
"System categories cannot be assigned to regular operations",
|
||||
)
|
||||
|
||||
|
||||
def account_balance(db: Session, account: Account) -> int:
|
||||
@@ -155,6 +169,7 @@ def create_expense(
|
||||
if existing is not None:
|
||||
return existing
|
||||
account = _require_active_account(db, space_id=space_id, account_id=account_id)
|
||||
_reject_system_account(account)
|
||||
_validate_category(db, space_id=space_id, category_id=category_id)
|
||||
ensure_period_open_for_date(db, space_id=space_id, day=occurred_on)
|
||||
operation = Operation(
|
||||
@@ -210,6 +225,7 @@ def create_income(
|
||||
if existing is not None:
|
||||
return existing
|
||||
account = _require_active_account(db, space_id=space_id, account_id=account_id)
|
||||
_reject_system_account(account)
|
||||
if income_source_id is not None:
|
||||
categorization.get_income_source(db, space_id=space_id, income_source_id=income_source_id)
|
||||
ensure_period_open_for_date(db, space_id=space_id, day=occurred_on)
|
||||
@@ -268,12 +284,16 @@ def create_transfer(
|
||||
source = _require_active_account(db, space_id=space_id, account_id=source_account_id)
|
||||
target = _require_active_account(db, space_id=space_id, account_id=target_account_id)
|
||||
ensure_period_open_for_date(db, space_id=space_id, day=occurred_on)
|
||||
from app.ledger.savings import transfer_category_for_accounts
|
||||
|
||||
category = transfer_category_for_accounts(db, space_id=space_id, source=source, target=target)
|
||||
operation = Operation(
|
||||
space_id=space_id,
|
||||
type="transfer",
|
||||
amount_kopecks=amount_kopecks,
|
||||
account_id=source.id,
|
||||
counter_account_id=target.id,
|
||||
category_id=category.id if category is not None else None,
|
||||
occurred_on=occurred_on,
|
||||
posted_on=posted_on,
|
||||
description=description,
|
||||
@@ -463,6 +483,8 @@ def archive_account(
|
||||
db: Session, *, space_id: uuid.UUID, user: User, account_id: uuid.UUID
|
||||
) -> Account:
|
||||
account = get_account(db, space_id=space_id, account_id=account_id)
|
||||
if account.system_key is not None:
|
||||
raise conflict("LEDGER_ACCOUNT_SYSTEM", "System accounts cannot be archived")
|
||||
if account.status == "archived":
|
||||
return account
|
||||
if account_balance(db, account) != 0:
|
||||
@@ -483,6 +505,8 @@ def archive_account(
|
||||
|
||||
def delete_account(db: Session, *, space_id: uuid.UUID, user: User, account_id: uuid.UUID) -> None:
|
||||
account = get_account(db, space_id=space_id, account_id=account_id)
|
||||
if account.system_key is not None:
|
||||
raise conflict("LEDGER_ACCOUNT_SYSTEM", "System accounts cannot be deleted")
|
||||
used = (
|
||||
db.query(Operation.id)
|
||||
.filter((Operation.account_id == account.id) | (Operation.counter_account_id == account.id))
|
||||
|
||||
Reference in New Issue
Block a user