Compare commits
13 Commits
afe89cfb03
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| ed37298a74 | |||
| 950090c762 | |||
| 3268f065d3 | |||
| c8eb7cd9bf | |||
| a81c4fbd7d | |||
| cd325154da | |||
| 4a823b7115 | |||
| a83711315f | |||
| 699009d21a | |||
| 440401049a | |||
| 148b1d00c4 | |||
| b5039f6468 | |||
| 65ecfcd919 |
@@ -7,7 +7,7 @@ from fastapi_filter.contrib.sqlalchemy import Filter
|
|||||||
from fastapi_pagination import Page
|
from fastapi_pagination import Page
|
||||||
from fastapi_pagination.ext.sqlmodel import paginate
|
from fastapi_pagination.ext.sqlmodel import paginate
|
||||||
|
|
||||||
from account.schemas import AccountCreate, AccountRead, AccountUpdate
|
from account.schemas import AccountCreate, AccountRead, AccountUpdate, OpeningTransaction, OpeningTransactionUpdate
|
||||||
from account.models import Account
|
from account.models import Account
|
||||||
from account.resource import AccountResource
|
from account.resource import AccountResource
|
||||||
|
|
||||||
@@ -65,6 +65,22 @@ def update_account(account_id: UUID, account: AccountUpdate, session: SessionDep
|
|||||||
account = AccountResource.update(session, db_account, account_data)
|
account = AccountResource.update(session, db_account, account_data)
|
||||||
return account
|
return account
|
||||||
|
|
||||||
|
@router.get("/{account_id}/opening_state")
|
||||||
|
def read_account_opening_state(account_id: UUID, session: SessionDep, current_user=Depends(get_current_user)) -> OpeningTransaction:
|
||||||
|
transaction = AccountResource.get_opening_transaction(session, account_id)
|
||||||
|
if not transaction:
|
||||||
|
raise HTTPException(status_code=404, detail="Account not found")
|
||||||
|
return transaction
|
||||||
|
|
||||||
|
@router.put("/{account_id}/opening_state")
|
||||||
|
def update_account_opening_state(account_id: UUID, opening_transaction: OpeningTransactionUpdate, session: SessionDep, current_user=Depends(get_current_user)) -> OpeningTransaction:
|
||||||
|
account = AccountResource.get(session, account_id)
|
||||||
|
if not account:
|
||||||
|
raise HTTPException(status_code=404, detail="Account not found")
|
||||||
|
|
||||||
|
transaction = AccountResource.update_opening_transaction(session, account, opening_transaction)
|
||||||
|
return transaction
|
||||||
|
|
||||||
@router.delete("/{account_id}")
|
@router.delete("/{account_id}")
|
||||||
def delete_account(account_id: UUID, session: SessionDep, current_user=Depends(get_current_user)):
|
def delete_account(account_id: UUID, session: SessionDep, current_user=Depends(get_current_user)):
|
||||||
account = AccountResource.get(session, account_id)
|
account = AccountResource.get(session, account_id)
|
||||||
|
|||||||
@@ -29,42 +29,42 @@ fixtures_account = [
|
|||||||
"name": "Current Assets",
|
"name": "Current Assets",
|
||||||
"parent_path": None,
|
"parent_path": None,
|
||||||
"type": "Asset",
|
"type": "Asset",
|
||||||
"opening_date": date(1970, 1, 1),
|
"opening_date": date(1970, 1, 2),
|
||||||
"opening_balance": Decimal("0.00"),
|
"opening_balance": Decimal("0.00"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Cash in Wallet",
|
"name": "Cash in Wallet",
|
||||||
"parent_path": "/Accounts/Asset/Current Assets/",
|
"parent_path": "/Accounts/Asset/Current Assets/",
|
||||||
"type": "Asset",
|
"type": "Asset",
|
||||||
"opening_date": date(1970, 1, 1),
|
"opening_date": date(1970, 1, 3),
|
||||||
"opening_balance": Decimal("0.00"),
|
"opening_balance": Decimal("0.00"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Checking Account",
|
"name": "Checking Account",
|
||||||
"parent_path": "/Accounts/Asset/Current Assets/",
|
"parent_path": "/Accounts/Asset/Current Assets/",
|
||||||
"type": "Asset",
|
"type": "Asset",
|
||||||
"opening_date": date(1970, 1, 1),
|
"opening_date": date(1970, 1, 4),
|
||||||
"opening_balance": Decimal("0.00"),
|
"opening_balance": Decimal("0.00"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Savings Account",
|
"name": "Savings Account",
|
||||||
"parent_path": "/Accounts/Asset/Current Assets/",
|
"parent_path": "/Accounts/Asset/Current Assets/",
|
||||||
"type": "Asset",
|
"type": "Asset",
|
||||||
"opening_date": date(1970, 1, 1),
|
"opening_date": date(1970, 1, 5),
|
||||||
"opening_balance": Decimal("0.00"),
|
"opening_balance": Decimal("0.00"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Debt Accounts",
|
"name": "Debt Accounts",
|
||||||
"parent_path": None,
|
"parent_path": None,
|
||||||
"type": "Liability",
|
"type": "Liability",
|
||||||
"opening_date": date(1970, 1, 1),
|
"opening_date": date(1970, 1, 6),
|
||||||
"opening_balance": Decimal("0.00"),
|
"opening_balance": Decimal("0.00"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Credit Card",
|
"name": "Credit Card",
|
||||||
"parent_path": "/Accounts/Liability/Debt Accounts/",
|
"parent_path": "/Accounts/Liability/Debt Accounts/",
|
||||||
"type": "Liability",
|
"type": "Liability",
|
||||||
"opening_date": date(1970, 1, 1),
|
"opening_date": date(1970, 1, 7),
|
||||||
"opening_balance": Decimal("0.00"),
|
"opening_balance": Decimal("0.00"),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
from sqlalchemy import literal_column
|
from datetime import date
|
||||||
|
|
||||||
|
from sqlalchemy import and_
|
||||||
from sqlalchemy.orm import aliased
|
from sqlalchemy.orm import aliased
|
||||||
from sqlmodel import select
|
from sqlmodel import select
|
||||||
|
|
||||||
from account.models import Account
|
from account.models import Account
|
||||||
|
from account.schemas import OpeningTransaction
|
||||||
from transaction.models import Split, Transaction
|
from transaction.models import Split, Transaction
|
||||||
|
from transaction.resource import TransactionResource
|
||||||
|
|
||||||
|
|
||||||
class AccountResource:
|
class AccountResource:
|
||||||
@@ -24,9 +28,10 @@ class AccountResource:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_opening_transaction(cls, session, account, schema):
|
def create_opening_transaction(cls, session, account, schema):
|
||||||
|
|
||||||
equity_account = cls.get_by_path(session, "/Equity/")
|
equity_account = cls.get_by_path(session, "/Equity/")
|
||||||
t = Transaction()
|
t = Transaction()
|
||||||
|
t.transaction_date = schema.opening_date
|
||||||
|
t.sequence = TransactionResource.get_sequence_number(session)
|
||||||
split_opening = Split()
|
split_opening = Split()
|
||||||
split_opening.id = 0
|
split_opening.id = 0
|
||||||
split_opening.transaction = t
|
split_opening.transaction = t
|
||||||
@@ -41,6 +46,56 @@ class AccountResource:
|
|||||||
|
|
||||||
account.transaction_splits.append(split_opening)
|
account.transaction_splits.append(split_opening)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fetch_opening_transaction(cls, session, account_id):
|
||||||
|
split_account = aliased(Split)
|
||||||
|
split_equity = aliased(Split)
|
||||||
|
account_equity = aliased(Account)
|
||||||
|
|
||||||
|
return session.execute(select(Transaction)
|
||||||
|
.join(split_account, and_(split_account.transaction_id == Transaction.id, split_account.account_id == account_id))
|
||||||
|
.join(split_equity, split_equity.transaction_id == Transaction.id)
|
||||||
|
.join(account_equity, and_(account_equity.id == split_equity.account_id, account_equity.path == "/Equity/"))
|
||||||
|
).first()[0]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_opening_transaction(cls, session, account_id):
|
||||||
|
transaction = cls.fetch_opening_transaction(session, account_id)
|
||||||
|
|
||||||
|
if transaction is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return OpeningTransaction(
|
||||||
|
opening_date=transaction.transaction_date,
|
||||||
|
opening_balance=transaction.splits[0].amount
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update_opening_transaction(cls, session, account, schema):
|
||||||
|
opening_transaction = cls.fetch_opening_transaction(session, account.id)
|
||||||
|
|
||||||
|
stmt = select(Transaction).join(Split) \
|
||||||
|
.where(Transaction.id != opening_transaction.id) \
|
||||||
|
.where(Split.account_id == account.id) \
|
||||||
|
.order_by(Transaction.transaction_date.asc())
|
||||||
|
|
||||||
|
first_transaction = session.exec(stmt).first()
|
||||||
|
if first_transaction and schema.opening_date > first_transaction[0].transaction_date:
|
||||||
|
raise ValueError("Account opening date is posterior to its first transaction date")
|
||||||
|
|
||||||
|
opening_transaction = cls.fetch_opening_transaction(session, account.id)
|
||||||
|
opening_transaction.transaction_date = schema.opening_date
|
||||||
|
opening_transaction.splits[0].amount = schema.opening_balance
|
||||||
|
opening_transaction.splits[1].amount = - schema.opening_balance
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
session.refresh(opening_transaction)
|
||||||
|
|
||||||
|
return OpeningTransaction(
|
||||||
|
opening_date=opening_transaction.transaction_date,
|
||||||
|
opening_balance=opening_transaction.splits[0].amount
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def schema_to_model(cls, session, schema, model=None):
|
def schema_to_model(cls, session, schema, model=None):
|
||||||
try:
|
try:
|
||||||
@@ -100,15 +155,7 @@ class AccountResource:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def select(cls):
|
def select(cls):
|
||||||
split_filter = aliased(Split)
|
return select(Account)
|
||||||
account_filter = aliased(Account)
|
|
||||||
columns = [Account.id, Account.name, Account.parent_account_id, Account.family, Account.type, Account.path,
|
|
||||||
Split.amount.label("opening_balance"), literal_column('"1970-01-01"').label("opening_date")]
|
|
||||||
return (select(*columns)
|
|
||||||
.join(Split)
|
|
||||||
.join(Transaction)
|
|
||||||
.join(split_filter)
|
|
||||||
.join(account_filter, account_filter.id == split_filter.account_id and Account.path == "/Equity/"))
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def list(cls, filters):
|
def list(cls, filters):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from typing import Optional
|
|||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
from pydantic import Field as PydField
|
from pydantic import Field as PydField, BaseModel
|
||||||
from pydantic.json_schema import SkipJsonSchema
|
from pydantic.json_schema import SkipJsonSchema
|
||||||
|
|
||||||
from account.enums import Asset, Liability, CategoryFamily
|
from account.enums import Asset, Liability, CategoryFamily
|
||||||
@@ -21,8 +21,7 @@ class AccountBaseId(AccountBase):
|
|||||||
path: str = Field(index=True)
|
path: str = Field(index=True)
|
||||||
|
|
||||||
class AccountRead(AccountBaseId):
|
class AccountRead(AccountBaseId):
|
||||||
opening_date: date = Field()
|
pass
|
||||||
opening_balance: Decimal = Field(decimal_places=2, default=0)
|
|
||||||
|
|
||||||
class BaseAccountWrite(AccountBase):
|
class BaseAccountWrite(AccountBase):
|
||||||
path: SkipJsonSchema[str] = Field(default="")
|
path: SkipJsonSchema[str] = Field(default="")
|
||||||
@@ -39,11 +38,10 @@ class AccountWrite(BaseAccountWrite):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
opening_date: date = Field()
|
|
||||||
opening_balance: MonetaryAmount = Field()
|
|
||||||
|
|
||||||
class AccountCreate(AccountWrite):
|
class AccountCreate(AccountWrite):
|
||||||
pass
|
opening_date: date = Field()
|
||||||
|
opening_balance: MonetaryAmount = Field()
|
||||||
|
|
||||||
class AccountUpdate(AccountWrite):
|
class AccountUpdate(AccountWrite):
|
||||||
pass
|
pass
|
||||||
@@ -63,11 +61,16 @@ class CategoryWrite(BaseAccountWrite):
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
class CategoryCreate(CategoryWrite):
|
||||||
opening_date: SkipJsonSchema[date] = Field(default=date(1970, 1, 1))
|
opening_date: SkipJsonSchema[date] = Field(default=date(1970, 1, 1))
|
||||||
opening_balance: SkipJsonSchema[Decimal] = Field(default=0)
|
opening_balance: SkipJsonSchema[Decimal] = Field(default=0)
|
||||||
|
|
||||||
class CategoryCreate(CategoryWrite):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class CategoryUpdate(CategoryWrite):
|
class CategoryUpdate(CategoryWrite):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class OpeningTransaction(BaseModel):
|
||||||
|
opening_date: date = Field()
|
||||||
|
opening_balance: MonetaryAmount = Field(default=0)
|
||||||
|
|
||||||
|
class OpeningTransactionUpdate(OpeningTransaction):
|
||||||
|
pass
|
||||||
|
|||||||
@@ -2,25 +2,33 @@ from dataclasses import dataclass
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import GetCoreSchemaHandler
|
from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
|
||||||
from pydantic_core import core_schema
|
from pydantic_core import core_schema
|
||||||
|
from pydantic.json_schema import JsonSchemaValue
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MonetaryAmount:
|
class MonetaryAmount:
|
||||||
|
@classmethod
|
||||||
|
def __get_pydantic_json_schema__(
|
||||||
|
cls, schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
|
||||||
|
) -> JsonSchemaValue:
|
||||||
|
json_schema = handler(schema)
|
||||||
|
if "anyOf" in json_schema:
|
||||||
|
for key, value in json_schema["anyOf"][0].items():
|
||||||
|
json_schema[key] = value
|
||||||
|
del json_schema["anyOf"]
|
||||||
|
|
||||||
|
json_schema["format"] = "monetary"
|
||||||
|
|
||||||
|
return json_schema
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __get_pydantic_core_schema__(
|
def __get_pydantic_core_schema__(
|
||||||
cls, source: type[Any], handler: GetCoreSchemaHandler
|
cls, source: type[Any], handler: GetCoreSchemaHandler
|
||||||
) -> core_schema.CoreSchema:
|
) -> core_schema.CoreSchema:
|
||||||
assert source is MonetaryAmount
|
assert source is MonetaryAmount
|
||||||
return core_schema.no_info_after_validator_function(
|
|
||||||
cls._validate,
|
return core_schema.decimal_schema(multiple_of=0.01)
|
||||||
core_schema.decimal_schema(multiple_of=0.01),
|
|
||||||
serialization=core_schema.plain_serializer_function_ser_schema(
|
|
||||||
cls._serialize,
|
|
||||||
info_arg=False,
|
|
||||||
return_schema=core_schema.decimal_schema(multiple_of=0.01),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import main
|
|||||||
|
|
||||||
from account.resource import AccountResource
|
from account.resource import AccountResource
|
||||||
from account.fixtures import inject_fixtures as account_inject_fixtures
|
from account.fixtures import inject_fixtures as account_inject_fixtures
|
||||||
|
from payee.fixtures import inject_fixtures as payee_inject_fixtures
|
||||||
|
from transaction.fixtures import inject_fixtures as transaction_inject_fixtures
|
||||||
from db import create_db_and_tables, get_session, drop_tables
|
from db import create_db_and_tables, get_session, drop_tables
|
||||||
from user import create_admin_user
|
from user import create_admin_user
|
||||||
|
|
||||||
@@ -12,3 +14,5 @@ session = get_session().__next__()
|
|||||||
create_admin_user(session)
|
create_admin_user(session)
|
||||||
AccountResource.create_equity_account(session)
|
AccountResource.create_equity_account(session)
|
||||||
account_inject_fixtures(session)
|
account_inject_fixtures(session)
|
||||||
|
payee_inject_fixtures(session)
|
||||||
|
transaction_inject_fixtures(session)
|
||||||
|
|||||||
0
api/app/ledger/__init__.py
Normal file
0
api/app/ledger/__init__.py
Normal file
26
api/app/ledger/resource.py
Normal file
26
api/app/ledger/resource.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from sqlalchemy import and_, select, func
|
||||||
|
from sqlalchemy.orm import aliased
|
||||||
|
|
||||||
|
from transaction.models import Split, Transaction
|
||||||
|
|
||||||
|
class LedgerResource:
|
||||||
|
@classmethod
|
||||||
|
def get_ledger(cls, account_id, filters):
|
||||||
|
split_account = aliased(Split)
|
||||||
|
split_balance = aliased(Split)
|
||||||
|
transaction_balance = aliased(Transaction)
|
||||||
|
|
||||||
|
balance_stmt = select(func.sum(split_balance.amount)) \
|
||||||
|
.join(transaction_balance) \
|
||||||
|
.where(and_(
|
||||||
|
split_balance.account_id == split_account.account_id,
|
||||||
|
transaction_balance.sequence <= Transaction.sequence)
|
||||||
|
).scalar_subquery()
|
||||||
|
|
||||||
|
stmt = select(Transaction,split_account,balance_stmt.label('balance')) \
|
||||||
|
.join(
|
||||||
|
split_account,
|
||||||
|
and_(Transaction.id == split_account.transaction_id, split_account.account_id == account_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
return filters.filter(stmt)
|
||||||
31
api/app/ledger/routes.py
Normal file
31
api/app/ledger/routes.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from fastapi_filter import FilterDepends
|
||||||
|
from fastapi_filter.contrib.sqlalchemy import Filter
|
||||||
|
from fastapi_pagination import Page
|
||||||
|
from fastapi_pagination.ext.sqlalchemy import paginate
|
||||||
|
from fastapi_pagination.utils import disable_installed_extensions_check
|
||||||
|
|
||||||
|
from db import SessionDep
|
||||||
|
from ledger.resource import LedgerResource
|
||||||
|
from ledger.schema import TransactionLedgerRead
|
||||||
|
from transaction.models import Transaction
|
||||||
|
from user.manager import get_current_user
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
class LedgerFilters(Filter):
|
||||||
|
|
||||||
|
class Constants(Filter.Constants):
|
||||||
|
model = Transaction
|
||||||
|
search_model_fields = ["id", "sequence"]
|
||||||
|
disable_installed_extensions_check()
|
||||||
|
|
||||||
|
@router.get("/{account_id}")
|
||||||
|
def read_ledger(account_id: UUID, session: SessionDep, filters: LedgerFilters = FilterDepends(LedgerFilters), current_user=Depends(get_current_user)) -> Page[TransactionLedgerRead]:
|
||||||
|
return paginate(
|
||||||
|
session,
|
||||||
|
LedgerResource.get_ledger(account_id, filters),
|
||||||
|
transformer=lambda items: [TransactionLedgerRead(transaction=transaction, account_split=split, balance=balance) for transaction, split, balance in items],
|
||||||
|
)
|
||||||
10
api/app/ledger/schema.py
Normal file
10
api/app/ledger/schema.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import decimal
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from transaction.models import TransactionRead, SplitRead
|
||||||
|
|
||||||
|
class TransactionLedgerRead(BaseModel):
|
||||||
|
transaction: TransactionRead = Field()
|
||||||
|
account_split: SplitRead = Field()
|
||||||
|
balance: decimal.Decimal = Field()
|
||||||
@@ -5,15 +5,14 @@ from fastapi.security import OAuth2PasswordBearer
|
|||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi_pagination import add_pagination
|
from fastapi_pagination import add_pagination
|
||||||
|
|
||||||
|
|
||||||
from db import create_db_and_tables
|
from db import create_db_and_tables
|
||||||
from user import user_router, auth_router, create_admin_user
|
from user import user_router, auth_router, create_admin_user
|
||||||
from account.account_routes import router as account_router
|
from account.account_routes import router as account_router
|
||||||
from account.category_routes import router as category_router
|
from account.category_routes import router as category_router
|
||||||
|
from ledger.routes import router as ledger_router
|
||||||
from payee.routes import router as payee_router
|
from payee.routes import router as payee_router
|
||||||
from transaction.routes import router as transaction_router
|
from transaction.routes import router as transaction_router
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
create_db_and_tables()
|
create_db_and_tables()
|
||||||
@@ -41,6 +40,7 @@ app.include_router(auth_router, prefix="/auth", tags=["auth"], )
|
|||||||
app.include_router(user_router, prefix="/users", tags=["users"])
|
app.include_router(user_router, prefix="/users", tags=["users"])
|
||||||
app.include_router(account_router, prefix="/accounts", tags=["accounts"])
|
app.include_router(account_router, prefix="/accounts", tags=["accounts"])
|
||||||
app.include_router(category_router, prefix="/categories", tags=["categories"])
|
app.include_router(category_router, prefix="/categories", tags=["categories"])
|
||||||
|
app.include_router(ledger_router, prefix="/ledgers", tags=["ledgers"])
|
||||||
app.include_router(payee_router, prefix="/payees", tags=["payees"])
|
app.include_router(payee_router, prefix="/payees", tags=["payees"])
|
||||||
app.include_router(transaction_router, prefix="/transactions", tags=["transactions"])
|
app.include_router(transaction_router, prefix="/transactions", tags=["transactions"])
|
||||||
|
|
||||||
|
|||||||
94
api/app/payee/fixtures.py
Normal file
94
api/app/payee/fixtures.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
from datetime import date
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from account.resource import AccountResource
|
||||||
|
from account.schemas import AccountCreate, CategoryCreate
|
||||||
|
from payee.models import PayeeCreate, Payee
|
||||||
|
|
||||||
|
|
||||||
|
def inject_fixtures(session):
|
||||||
|
for f in fixtures_payee:
|
||||||
|
# f = prepare_dict(session, f)
|
||||||
|
schema = PayeeCreate(**f)
|
||||||
|
Payee.create(schema, session)
|
||||||
|
|
||||||
|
def prepare_dict(session, entry):
|
||||||
|
if entry['parent_path']:
|
||||||
|
parent = AccountResource.get_by_path(session, entry['parent_path'])
|
||||||
|
entry['parent_account_id'] = parent.id
|
||||||
|
else:
|
||||||
|
entry['parent_account_id'] = None
|
||||||
|
del entry['parent_path']
|
||||||
|
return entry
|
||||||
|
|
||||||
|
fixtures_payee = [
|
||||||
|
{
|
||||||
|
"name": "PayeeEmployer",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PayeeSocialSecurity",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PayeeAssurance1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PayeeAssurance2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PayeeSupermarket1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PayeeSupermarket2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PayeeTaxes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PayeeRent",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
fixtures_account = [
|
||||||
|
{
|
||||||
|
"name": "Current Assets",
|
||||||
|
"parent_path": None,
|
||||||
|
"type": "Asset",
|
||||||
|
"opening_date": date(1970, 1, 2),
|
||||||
|
"opening_balance": Decimal("0.00"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cash in Wallet",
|
||||||
|
"parent_path": "/Accounts/Asset/Current Assets/",
|
||||||
|
"type": "Asset",
|
||||||
|
"opening_date": date(1970, 1, 3),
|
||||||
|
"opening_balance": Decimal("0.00"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Checking Account",
|
||||||
|
"parent_path": "/Accounts/Asset/Current Assets/",
|
||||||
|
"type": "Asset",
|
||||||
|
"opening_date": date(1970, 1, 4),
|
||||||
|
"opening_balance": Decimal("0.00"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Savings Account",
|
||||||
|
"parent_path": "/Accounts/Asset/Current Assets/",
|
||||||
|
"type": "Asset",
|
||||||
|
"opening_date": date(1970, 1, 5),
|
||||||
|
"opening_balance": Decimal("0.00"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debt Accounts",
|
||||||
|
"parent_path": None,
|
||||||
|
"type": "Liability",
|
||||||
|
"opening_date": date(1970, 1, 6),
|
||||||
|
"opening_balance": Decimal("0.00"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Credit Card",
|
||||||
|
"parent_path": "/Accounts/Liability/Debt Accounts/",
|
||||||
|
"type": "Liability",
|
||||||
|
"opening_date": date(1970, 1, 7),
|
||||||
|
"opening_balance": Decimal("0.00"),
|
||||||
|
},
|
||||||
|
]
|
||||||
36
api/app/transaction/fixtures.py
Normal file
36
api/app/transaction/fixtures.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import random
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
from account.resource import AccountResource
|
||||||
|
from transaction.models import TransactionCreate, Split
|
||||||
|
from transaction.resource import TransactionResource
|
||||||
|
|
||||||
|
|
||||||
|
def inject_fixtures(session):
|
||||||
|
for f in fixtures_transaction:
|
||||||
|
for i in range(f["count"]):
|
||||||
|
data = prepare_dict(session, f, i)
|
||||||
|
schema = TransactionCreate(**data)
|
||||||
|
schema.splits.append(Split(**data))
|
||||||
|
TransactionResource.create(session, schema)
|
||||||
|
|
||||||
|
def prepare_dict(session, entry, iteration):
|
||||||
|
account = AccountResource.get_by_path(session, entry['account_path'])
|
||||||
|
result = entry.copy()
|
||||||
|
|
||||||
|
result["account_id"] = account.id
|
||||||
|
result["payee_id"] = None
|
||||||
|
result["splits"] = []
|
||||||
|
result["transaction_date"] = entry["start_date"] + timedelta(days=(iteration / 2))
|
||||||
|
result["amount"] = [10, 100, 1000][random.Random().randint(0,2)] * [1, -1][random.Random().randint(0,1)]
|
||||||
|
result["id"] = 0
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
fixtures_transaction = [
|
||||||
|
{
|
||||||
|
"account_path": "/Accounts/Asset/Current Assets/Checking Account/",
|
||||||
|
"start_date": date(1970, 1, 5),
|
||||||
|
"count": 200
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
|
from datetime import date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from sqlmodel import Field, SQLModel, select, Relationship
|
from sqlmodel import Field, SQLModel, Relationship
|
||||||
from pydantic import Field as PydField
|
from pydantic import Field as PydField
|
||||||
|
|
||||||
from account.models import Account
|
from account.models import Account
|
||||||
@@ -11,43 +12,15 @@ from payee.models import Payee, PayeeRead
|
|||||||
|
|
||||||
|
|
||||||
class TransactionBase(SQLModel):
|
class TransactionBase(SQLModel):
|
||||||
pass
|
transaction_date: date = Field()
|
||||||
|
payment_date: Optional[date] = Field(default=None)
|
||||||
|
|
||||||
class TransactionBaseId(TransactionBase):
|
class TransactionBaseId(TransactionBase):
|
||||||
id: UUID | None = Field(default_factory=uuid4, primary_key=True)
|
id: UUID | None = Field(default_factory=uuid4, primary_key=True)
|
||||||
|
|
||||||
class Transaction(TransactionBaseId, table=True):
|
class Transaction(TransactionBaseId, table=True):
|
||||||
splits: list["Split"] = Relationship(back_populates="transaction")
|
splits: list["Split"] = Relationship(back_populates="transaction")
|
||||||
|
sequence: Optional[int] = Field(default=None)
|
||||||
@classmethod
|
|
||||||
def create(cls, transaction, session):
|
|
||||||
transaction_db = cls.model_validate(transaction)
|
|
||||||
session.add(transaction_db)
|
|
||||||
session.commit()
|
|
||||||
session.refresh(transaction_db)
|
|
||||||
|
|
||||||
return transaction_db
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def list(cls):
|
|
||||||
return select(Transaction).join(Split).join(Account)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get(cls, session, transaction_id):
|
|
||||||
return session.get(Transaction, transaction_id)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def update(cls, session, transaction_db, transaction_data):
|
|
||||||
transaction_db.sqlmodel_update(cls.model_validate(transaction_data))
|
|
||||||
session.add(transaction_db)
|
|
||||||
session.commit()
|
|
||||||
session.refresh(transaction_db)
|
|
||||||
return transaction_db
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def delete(cls, session, transaction):
|
|
||||||
session.delete(transaction)
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
class SplitBase(SQLModel):
|
class SplitBase(SQLModel):
|
||||||
account_id: UUID = Field(foreign_key="account.id")
|
account_id: UUID = Field(foreign_key="account.id")
|
||||||
@@ -60,7 +33,7 @@ class SplitBaseId(SplitBase):
|
|||||||
|
|
||||||
class SplitRead(SplitBaseId):
|
class SplitRead(SplitBaseId):
|
||||||
account: AccountRead
|
account: AccountRead
|
||||||
payee: PayeeRead
|
payee: PayeeRead | None
|
||||||
|
|
||||||
class TransactionRead(TransactionBaseId):
|
class TransactionRead(TransactionBaseId):
|
||||||
splits: list[SplitRead]
|
splits: list[SplitRead]
|
||||||
|
|||||||
44
api/app/transaction/resource.py
Normal file
44
api/app/transaction/resource.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
from sqlmodel import select, func
|
||||||
|
|
||||||
|
from account.models import Account
|
||||||
|
from transaction.models import Transaction, Split
|
||||||
|
|
||||||
|
class TransactionResource:
|
||||||
|
@classmethod
|
||||||
|
def get_sequence_number(cls, session):
|
||||||
|
stmt = select(func.max(Transaction.sequence)).select_from(Transaction)
|
||||||
|
|
||||||
|
sequence = session.execute(stmt).first()
|
||||||
|
|
||||||
|
return sequence[0] + 1 if sequence[0] else 1
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, session, transaction):
|
||||||
|
transaction_db = Transaction.model_validate(transaction)
|
||||||
|
transaction_db.sequence = cls.get_sequence_number(session)
|
||||||
|
session.add(transaction_db)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(transaction_db)
|
||||||
|
|
||||||
|
return transaction_db
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def list(cls):
|
||||||
|
return select(Transaction).join(Split).join(Account)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, session, transaction_id):
|
||||||
|
return session.get(Transaction, transaction_id)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update(cls, session, transaction_db, transaction_data):
|
||||||
|
transaction_db.sqlmodel_update(Transaction.model_validate(transaction_data))
|
||||||
|
session.add(transaction_db)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(transaction_db)
|
||||||
|
return transaction_db
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def delete(cls, session, transaction):
|
||||||
|
session.delete(transaction)
|
||||||
|
session.commit()
|
||||||
@@ -4,43 +4,44 @@ from fastapi import APIRouter, HTTPException, Depends
|
|||||||
from fastapi_pagination import Page
|
from fastapi_pagination import Page
|
||||||
from fastapi_pagination.ext.sqlmodel import paginate
|
from fastapi_pagination.ext.sqlmodel import paginate
|
||||||
|
|
||||||
from transaction.models import Transaction, TransactionCreate, TransactionRead, TransactionUpdate
|
|
||||||
from db import SessionDep
|
from db import SessionDep
|
||||||
|
from transaction.models import TransactionCreate, TransactionRead, TransactionUpdate
|
||||||
|
from transaction.resource import TransactionResource
|
||||||
from user.manager import get_current_user
|
from user.manager import get_current_user
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@router.post("")
|
@router.post("")
|
||||||
def create_transaction(transaction: TransactionCreate, session: SessionDep, current_user=Depends(get_current_user)) -> TransactionRead:
|
def create_transaction(transaction: TransactionCreate, session: SessionDep, current_user=Depends(get_current_user)) -> TransactionRead:
|
||||||
result = Transaction.create(transaction, session)
|
result = TransactionResource.create(transaction, session)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
def read_transactions(session: SessionDep, current_user=Depends(get_current_user)) -> Page[TransactionRead]:
|
def read_transactions(session: SessionDep, current_user=Depends(get_current_user)) -> Page[TransactionRead]:
|
||||||
return paginate(session, Transaction.list())
|
return paginate(session, TransactionResource.list())
|
||||||
|
|
||||||
@router.get("/{transaction_id}")
|
@router.get("/{transaction_id}")
|
||||||
def read_transaction(transaction_id: UUID, session: SessionDep, current_user=Depends(get_current_user)) -> TransactionRead:
|
def read_transaction(transaction_id: UUID, session: SessionDep, current_user=Depends(get_current_user)) -> TransactionRead:
|
||||||
transaction = Transaction.get(session, transaction_id)
|
transaction = TransactionResource.get(session, transaction_id)
|
||||||
if not transaction:
|
if not transaction:
|
||||||
raise HTTPException(status_code=404, detail="Transaction not found")
|
raise HTTPException(status_code=404, detail="Transaction not found")
|
||||||
return transaction
|
return transaction
|
||||||
|
|
||||||
@router.put("/{transaction_id}")
|
@router.put("/{transaction_id}")
|
||||||
def update_transaction(transaction_id: UUID, transaction: TransactionUpdate, session: SessionDep, current_user=Depends(get_current_user)) -> TransactionRead:
|
def update_transaction(transaction_id: UUID, transaction: TransactionUpdate, session: SessionDep, current_user=Depends(get_current_user)) -> TransactionRead:
|
||||||
db_transaction = Transaction.get(session, transaction_id)
|
db_transaction = TransactionResource.get(session, transaction_id)
|
||||||
if not db_transaction:
|
if not db_transaction:
|
||||||
raise HTTPException(status_code=404, detail="Transaction not found")
|
raise HTTPException(status_code=404, detail="Transaction not found")
|
||||||
|
|
||||||
transaction_data = transaction.model_dump(exclude_unset=True)
|
transaction_data = transaction.model_dump(exclude_unset=True)
|
||||||
transaction = Transaction.update(session, db_transaction, transaction_data)
|
transaction = TransactionResource.update(session, db_transaction, transaction_data)
|
||||||
return transaction
|
return transaction
|
||||||
|
|
||||||
@router.delete("/{transaction_id}")
|
@router.delete("/{transaction_id}")
|
||||||
def delete_transaction(transaction_id: UUID, session: SessionDep, current_user=Depends(get_current_user)):
|
def delete_transaction(transaction_id: UUID, session: SessionDep, current_user=Depends(get_current_user)):
|
||||||
transaction = Transaction.get(session, transaction_id)
|
transaction = TransactionResource.get(session, transaction_id)
|
||||||
if not transaction:
|
if not transaction:
|
||||||
raise HTTPException(status_code=404, detail="Transaction not found")
|
raise HTTPException(status_code=404, detail="Transaction not found")
|
||||||
|
|
||||||
Transaction.delete(session, transaction)
|
TransactionResource.delete(session, transaction)
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { AccountList, AccountCreate, AccountEdit } from "./pages/accounts";
|
|||||||
import { CategoryList, CategoryCreate, CategoryEdit } from "./pages/categories";
|
import { CategoryList, CategoryCreate, CategoryEdit } from "./pages/categories";
|
||||||
import { PayeeCreate, PayeeEdit, PayeeList } from "./pages/payees";
|
import { PayeeCreate, PayeeEdit, PayeeList } from "./pages/payees";
|
||||||
import { TransactionCreate, TransactionEdit, TransactionList } from "./pages/transactions";
|
import { TransactionCreate, TransactionEdit, TransactionList } from "./pages/transactions";
|
||||||
|
import {AccountLedger} from "./pages/accounts/ledger";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* mock auth credentials to simulate authentication
|
* mock auth credentials to simulate authentication
|
||||||
@@ -135,6 +136,7 @@ const App: React.FC = () => {
|
|||||||
<Route index element={<AccountList />} />
|
<Route index element={<AccountList />} />
|
||||||
<Route path="create" element={<AccountCreate />} />
|
<Route path="create" element={<AccountCreate />} />
|
||||||
<Route path="edit/:id" element={<AccountEdit />} />
|
<Route path="edit/:id" element={<AccountEdit />} />
|
||||||
|
<Route path="ledger/:id" element={<AccountLedger />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/categories">
|
<Route path="/categories">
|
||||||
<Route index element={<CategoryList />} />
|
<Route index element={<CategoryList />} />
|
||||||
|
|||||||
@@ -6,10 +6,17 @@ export const AccountEdit: React.FC = () => {
|
|||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CrudForm
|
<div>
|
||||||
schemaName={"AccountUpdate"}
|
<CrudForm
|
||||||
resource={"accounts"}
|
schemaName={"AccountUpdate"}
|
||||||
id={id}
|
resource={"accounts"}
|
||||||
/>
|
id={id}
|
||||||
|
/>
|
||||||
|
<CrudForm
|
||||||
|
schemaName={"OpeningTransactionUpdate"}
|
||||||
|
resource={`accounts/${id}/opening_state`}
|
||||||
|
id=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
69
gui/app/src/pages/accounts/ledger.tsx
Normal file
69
gui/app/src/pages/accounts/ledger.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import Table from '@mui/material/Table';
|
||||||
|
import TableBody from '@mui/material/TableBody';
|
||||||
|
import TableCell from '@mui/material/TableCell';
|
||||||
|
import TableContainer from '@mui/material/TableContainer';
|
||||||
|
import TableHead from '@mui/material/TableHead';
|
||||||
|
import TableRow from '@mui/material/TableRow';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
|
||||||
|
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
import {useList, useOne} from "@refinedev/core";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const AccountLedger: React.FC = () => {
|
||||||
|
const { id } = useParams()
|
||||||
|
|
||||||
|
const { data, isLoading, isError } = useList({
|
||||||
|
resource: `ledgers/${id}`,
|
||||||
|
});
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>Loading</div>
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<Table sx={{ minWidth: 650 }} aria-label="simple table">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell></TableCell>
|
||||||
|
<TableCell align="right">Date</TableCell>
|
||||||
|
<TableCell align="right">Deposit</TableCell>
|
||||||
|
<TableCell align="right">Payment</TableCell>
|
||||||
|
<TableCell align="right">Balance</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{data?.data.map((row: any) => {
|
||||||
|
const is_expense: boolean = row.account_split.amount < 0
|
||||||
|
const destination_accounts = []
|
||||||
|
for (const split of row.transaction.splits) {
|
||||||
|
if ((is_expense && split.amount >= 0) || (!is_expense && split.amount < 0)) {
|
||||||
|
destination_accounts.push(split)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let is_split = false;
|
||||||
|
if (destination_accounts) {
|
||||||
|
if (destination_accounts.length > 1) {
|
||||||
|
is_split = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return(
|
||||||
|
<TableRow
|
||||||
|
key={row.name}
|
||||||
|
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||||
|
>
|
||||||
|
<TableCell component="th" scope="row"></TableCell>
|
||||||
|
<TableCell align="right">{row.transaction.transaction_date}</TableCell>
|
||||||
|
<TableCell align="right">{ is_expense ? "" : row.account_split.amount }</TableCell>
|
||||||
|
<TableCell align="right">{ is_expense ? row.account_split.amount : "" }</TableCell>
|
||||||
|
<TableCell align="right">{ row.balance }</TableCell>
|
||||||
|
</TableRow>)
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { ButtonGroup } from "@mui/material";
|
import { Button, ButtonGroup } from "@mui/material";
|
||||||
import { DataGrid, type GridColDef } from "@mui/x-data-grid";
|
import { DataGrid, type GridColDef } from "@mui/x-data-grid";
|
||||||
|
import RequestQuoteIcon from '@mui/icons-material/RequestQuote';
|
||||||
|
|
||||||
import { DeleteButton, EditButton, List, useDataGrid} from "@refinedev/mui";
|
import { DeleteButton, EditButton, List, useDataGrid} from "@refinedev/mui";
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ export const AccountList: React.FC = () => {
|
|||||||
{ field: "id", headerName: "ID" },
|
{ field: "id", headerName: "ID" },
|
||||||
{ field: "name", headerName: "Name", flex: 1 },
|
{ field: "name", headerName: "Name", flex: 1 },
|
||||||
{ field: "path", headerName: "path", flex: 1 },
|
{ field: "path", headerName: "path", flex: 1 },
|
||||||
{ field: "type", headerName: "Type", flex: 0.3 },
|
{ field: "type", headerName: "Type", flex: 1 },
|
||||||
{
|
{
|
||||||
field: "actions",
|
field: "actions",
|
||||||
headerName: "Actions",
|
headerName: "Actions",
|
||||||
@@ -23,6 +24,7 @@ export const AccountList: React.FC = () => {
|
|||||||
renderCell: function render({ row }) {
|
renderCell: function render({ row }) {
|
||||||
return (
|
return (
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
|
<Button href={`/accounts/ledger/${row.id}`}><RequestQuoteIcon /></Button>
|
||||||
<EditButton hideText recordItemId={row.id} />
|
<EditButton hideText recordItemId={row.id} />
|
||||||
<DeleteButton hideText recordItemId={row.id} />
|
<DeleteButton hideText recordItemId={row.id} />
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
@@ -30,6 +32,7 @@ export const AccountList: React.FC = () => {
|
|||||||
},
|
},
|
||||||
align: "center",
|
align: "center",
|
||||||
headerAlign: "center",
|
headerAlign: "center",
|
||||||
|
flex: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[],
|
[],
|
||||||
|
|||||||
@@ -67,11 +67,7 @@ export const authProvider: AuthProvider = {
|
|||||||
onError: async (error) => {
|
onError: async (error) => {
|
||||||
if (error?.status === 401) {
|
if (error?.status === 401) {
|
||||||
localStorage.removeItem("access_token");
|
localStorage.removeItem("access_token");
|
||||||
return Promise<{
|
return {redirectTo: "/login"}
|
||||||
redirectTo: "/login",
|
|
||||||
logout: true,
|
|
||||||
error: { message: "Unauthorized" },
|
|
||||||
}>;
|
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ const fetcher = async (url: string, options?: RequestInit) => {
|
|||||||
|
|
||||||
export const dataProvider: DataProvider = {
|
export const dataProvider: DataProvider = {
|
||||||
getOne: async ({ resource, id, meta }) => {
|
getOne: async ({ resource, id, meta }) => {
|
||||||
const response = await fetcher(`${API_URL}/${resource}/${id}`);
|
const response = id !== "" ? await fetcher(`${API_URL}/${resource}/${id}`) : await fetcher(`${API_URL}/${resource}`);
|
||||||
|
|
||||||
if (response.status < 200 || response.status > 299) throw response;
|
if (response.status < 200 || response.status > 299) throw response;
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|||||||
Reference in New Issue
Block a user