Skip to content

Commit

Permalink
Merge pull request #168 from SFTtech/milo/group-settlements
Browse files Browse the repository at this point in the history
implement group settlements
  • Loading branch information
mikonse authored Aug 26, 2023
2 parents 573b955 + cf8d4df commit e7737d0
Show file tree
Hide file tree
Showing 31 changed files with 410 additions and 115 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/release-artifacts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:
- id: set-distros
run: |
# if we're running from a tag, get the full list of distros; otherwise just use debian:sid
dists='["debian:bullseye"]'
dists='["debian:bookworm"]'
tags="latest $GITHUB_SHA"
if [[ $GITHUB_REF == refs/tags/* ]]; then
dists=$(tools/build_debian_packages.py --show-dists-json)
Expand Down Expand Up @@ -175,7 +175,7 @@ jobs:
- name: Download all workflow run artifacts
uses: actions/download-artifact@v2
- name: Trigger demo deployment via webhook
run: curl ${{ secrets.DEMO_DEPLOY_WEBHOOK_URL }} -F "archive=@$(find -name 'abrechnung_*bullseye*_amd64.deb')" --fail
run: curl ${{ secrets.DEMO_DEPLOY_WEBHOOK_URL }} -F "archive=@$(find -name 'abrechnung_*bookworm*_amd64.deb')" --fail

# if it's a tag, create a release and attach the artifacts to it
attach-assets:
Expand Down
1 change: 0 additions & 1 deletion abrechnung/application/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1164,7 +1164,6 @@ async def sync_transaction(
async def sync_transactions(
self, *, user: User, group_id: int, transactions: list[RawTransaction]
) -> dict[int, int]:

all_transactions_in_same_group = all(
[a.group_id == group_id for a in transactions]
)
Expand Down
22 changes: 11 additions & 11 deletions abrechnung/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,23 @@ class ServiceConfig(BaseModel):


class DemoConfig(BaseModel):
enabled = False
wipe_interval = timedelta(hours=1)
enabled: bool = False
wipe_interval: timedelta = timedelta(hours=1)


class ApiConfig(BaseModel):
secret_key: str
host: str
port: int
id = "default"
max_uploadable_file_size = 1024
enable_cors = True
access_token_validity = timedelta(hours=1)
id: str = "default"
max_uploadable_file_size: int = 1024
enable_cors: bool = True
access_token_validity: timedelta = timedelta(hours=1)


class RegistrationConfig(BaseModel):
enabled = False
allow_guest_users = False
enabled: bool = False
allow_guest_users: bool = False
valid_email_domains: Optional[List[str]] = None


Expand All @@ -49,7 +49,7 @@ class AuthConfig(BaseModel):
address: str
host: str
port: int
mode = "smtp" # oneof "local" "smtp-ssl" "smtp-starttls" "smtp"
mode: str = "smtp" # oneof "local" "smtp-ssl" "smtp-starttls" "smtp"
auth: Optional[AuthConfig] = None


Expand All @@ -59,8 +59,8 @@ class Config(BaseModel):
database: DatabaseConfig
email: EmailConfig
# in case all params are optional this is needed to make the whole section optional
demo = DemoConfig()
registration = RegistrationConfig()
demo: DemoConfig = DemoConfig()
registration: RegistrationConfig = RegistrationConfig()


def read_config(config_path: Path) -> Config:
Expand Down
2 changes: 1 addition & 1 deletion abrechnung/http/routers/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class BaseAccountPayload(BaseModel):
date_info: Optional[date] = None
tags: Optional[List[str]] = None
owning_user_id: Optional[int] = None
clearing_shares: ClearingShares
clearing_shares: ClearingShares = None


class CreateAccountPayload(BaseAccountPayload):
Expand Down
2 changes: 1 addition & 1 deletion abrechnung/http/routers/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class VersionResponse(BaseModel):
patch_version: int

class Config:
schema_extra = {
json_schema_extra = {
"example": {
"version": "1.3.2",
"major_version": 1,
Expand Down
2 changes: 1 addition & 1 deletion abrechnung/http/routers/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class GroupPayload(BaseModel):
name: str
description: str = ""
currency_symbol: str
add_user_account_on_join = False
add_user_account_on_join: bool = False
terms: str = ""


Expand Down
6 changes: 6 additions & 0 deletions abrechnung/http/routers/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,12 @@ async def upload_file(
detail=f"Cannot read uploaded file: {e}",
)

if file.filename is None or file.content_type is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"File is missing filename or content type",
)

await transaction_service.upload_file(
user=user,
transaction_id=transaction_id,
Expand Down
10 changes: 8 additions & 2 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
abrechnung (0.11.0) stable; urgency=medium

* Abrechnung release 0.11.0

-- Michael Loipführer <[email protected]> Sun, 26 Aug 2023 20:00:00 +0200

abrechnung (0.10.1) stable; urgency=medium

* Abrechnung release 0.10.1

-- Michael Loipführer <[email protected]> Sun, 1 Jan 2022 18:00:00 +0100
-- Michael Loipführer <[email protected]> Sun, 1 Jan 2023 18:00:00 +0100

abrechnung (0.10.0) stable; urgency=medium

* Abrechnung release 0.10.0

-- Michael Loipführer <[email protected]> Sun, 1 Jan 2022 15:00:00 +0100
-- Michael Loipführer <[email protected]> Sun, 1 Jan 2023 15:00:00 +0100

abrechnung (0.9.0) stable; urgency=medium

Expand Down
2 changes: 1 addition & 1 deletion debian/copyright
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ Upstream-Name: abrechnung
Upstream-Contact: Michael Loipführer, <[email protected]>

Files: *
Copyright: 2021, Jonas Jelten <[email protected]>, Michael Enßlin <[email protected]>, Michael Loipführer <[email protected]>
Copyright: 2023, Jonas Jelten <[email protected]>, Michael Enßlin <[email protected]>, Michael Loipführer <[email protected]>
50 changes: 0 additions & 50 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@
import sys
from pathlib import Path

import pydantic
import yaml
from pydantic.fields import ModelField

HERE = Path(__file__).parent
sys.path[:0] = [str(HERE.parent), str(HERE / "_ext")]
BUILD_DIR = HERE / "_build"
Expand Down Expand Up @@ -115,50 +111,4 @@ def generate_openapi_json():
json.dump(api.api.openapi(), f)


def _generate_config_doc_rec(field: ModelField):
if issubclass(field.type_, pydantic.BaseModel):
sub = {}
for subfield in field.type_.__fields__.values():
sub[subfield.name] = _generate_config_doc_rec(subfield)
return sub

if (
field.outer_type_ is not None
and "List" in str(field.outer_type_)
or "list" in str(field.outer_type_)
):
verbose_type = f"<list[{field.type_.__name__}]>"
else:
verbose_type = f"<{field.type_.__name__}>"

out = f"{verbose_type}"
extra_info = []
# if description := field.metadata.get("description"):
# extra_info.append(description)

if field.default:
extra_info.append(f"default={field.default}")

if field.required is not None and not field.required:
extra_info.append("optional")

if not extra_info:
return out

return f"{out} # {'; '.join(extra_info)}"


def generate_config_doc_yaml():
output = {}
for field in Config.__fields__.values():
output[field.name] = _generate_config_doc_rec(field)

output_string = yaml.dump(output).replace("'", "").replace('"', "")

BUILD_DIR.mkdir(parents=True, exist_ok=True)
with open(BUILD_DIR / "config_schema.yaml", "w+", encoding="utf-8") as f:
f.write(output_string)


generate_openapi_json()
generate_config_doc_yaml()
7 changes: 0 additions & 7 deletions docs/usage/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,3 @@ Possible config options are
"imprintURL": "<string, optional>",
"sourceCodeURL": "https://github.com/SFTtech/abrechnung"
}
All Configuration Options
-------------------------

.. literalinclude :: ../_build/config_schema.yaml
:language: yaml
4 changes: 2 additions & 2 deletions docs/usage/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Installation

.. _abrechnung-installation-debian:

Debian Buster, Bullseye, Bookworm and Sid
Debian Bullseye, Bookworm, Trixie and Sid
-----------------------------------------
This is the recommended installation method as it also installs the prebuilt abrechnung web app.

Expand All @@ -25,7 +25,7 @@ as well as static web assets in ``/usr/share/abrechnung_web`` are installed.

The only remaining work to be done is to setup the database and customize the configuration (see :ref:`abrechnung-config`).

Ubuntu Focal, Hirsute and Impish
Ubuntu Jammy
--------------------------------

Follow the installation instructions for :ref:`Debian <abrechnung-installation-debian>`, just make sure to choose the correct
Expand Down
20 changes: 10 additions & 10 deletions frontend/apps/web/src/pages/accounts/AccountDetail/AccountInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ import { Account, AccountValidator } from "@abrechnung/types";
import { ChevronLeft, Delete, Edit } from "@mui/icons-material";
import { Button, Chip, Divider, Grid, IconButton, LinearProgress, TableCell } from "@mui/material";
import React from "react";
import { Link as RouterLink, useNavigate } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify";
import { typeToFlattenedError, z } from "zod";
import { DeleteAccountModal } from "../../../components/accounts/DeleteAccountModal";
import { DateInput } from "../../../components/DateInput";
import { ShareSelect } from "../../../components/ShareSelect";
import { TagSelector } from "../../../components/TagSelector";
import { TextInput } from "../../../components/TextInput";
import { api } from "../../../core/api";
import { selectAccountSlice, selectGroupSlice, useAppDispatch, useAppSelector } from "../../../store";
import { getAccountLink, getAccountListLink } from "../../../utils";
import { DeleteAccountModal } from "@/components/accounts/DeleteAccountModal";
import { DateInput } from "@/components/DateInput";
import { ShareSelect } from "@/components/ShareSelect";
import { TagSelector } from "@/components/TagSelector";
import { TextInput } from "@/components/TextInput";
import { api } from "@/core/api";
import { selectAccountSlice, selectGroupSlice, useAppDispatch, useAppSelector } from "@/store";
import { getAccountLink, getAccountListLink } from "@/utils";

interface Props {
groupId: number;
Expand Down Expand Up @@ -87,7 +87,7 @@ export const AccountInfo: React.FC<Props> = ({ groupId, accountId }) => {
.then(({ oldAccountId, account }) => {
setShowProgress(false);
if (oldAccountId !== account.id) {
navigate(getAccountLink(groupId, "clearing", account.id) + "?no-redirect=true");
navigate(getAccountLink(groupId, "clearing", account.id) + "?no-redirect=true", { replace: true });
}
})
.catch((err) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Alert,
AlertTitle,
Box,
Button,
Divider,
List,
ListItemText,
Expand All @@ -14,10 +15,10 @@ import {
} from "@mui/material";
import { TabContext, TabList, TabPanel } from "@mui/lab";
import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router-dom";
import BalanceTable from "./BalanceTable";
import { MobilePaper } from "../style/mobile";
import ListItemLink from "../style/ListItemLink";
import { useNavigate, Link as RouterLink } from "react-router-dom";
import BalanceTable from "../../components/accounts/BalanceTable";
import { MobilePaper } from "../../components/style/mobile";
import ListItemLink from "../../components/style/ListItemLink";
import { useTitle } from "../../core/utils";
import { selectGroupAccountsFiltered, selectGroupById, selectAccountBalances } from "@abrechnung/redux";
import { useAppSelector, selectAccountSlice, selectGroupSlice } from "../../store";
Expand Down Expand Up @@ -202,6 +203,12 @@ export const Balances: React.FC<Props> = ({ groupId }) => {
<BalanceTable groupId={groupId} />
</TabPanel>
</TabContext>
<Divider />
<Box sx={{ display: "flex", justifyContent: "center" }}>
<Button component={RouterLink} to={`/groups/${group.id}/settlement-plan`}>
Settle up
</Button>
</Box>
</MobilePaper>
);
};
Expand Down
71 changes: 71 additions & 0 deletions frontend/apps/web/src/pages/accounts/SettlementPlanDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { MobilePaper } from "@/components/style/mobile";
import { selectAccountSlice, selectGroupSlice, useAppDispatch, useAppSelector } from "@/store";
import {
createTransaction,
selectAccountIdToAccountMap,
selectGroupCurrencySymbol,
selectSettlementPlan,
} from "@abrechnung/redux";
import { Button, List, ListItem, ListItemSecondaryAction, ListItemText, Typography } from "@mui/material";
import * as React from "react";
import { SettlementPlanItem } from "@abrechnung/core";
import { useNavigate } from "react-router-dom";

interface Props {
groupId: number;
}

export const SettlementPlanDisplay: React.FC<Props> = ({ groupId }) => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const settlementPlan = useAppSelector((state) => selectSettlementPlan({ state, groupId }));
const currencySymbol = useAppSelector((state) =>
selectGroupCurrencySymbol({ state: selectGroupSlice(state), groupId })
);
const accountMap = useAppSelector((state) =>
selectAccountIdToAccountMap({ state: selectAccountSlice(state), groupId })
);

const onSettleClicked = (planItem: SettlementPlanItem) => {
dispatch(
createTransaction({
type: "transfer",
groupId,
data: {
name: "Settlement",
value: planItem.paymentAmount,
creditorShares: { [planItem.creditorId]: 1 },
debitorShares: { [planItem.debitorId]: 1 },
},
})
)
.unwrap()
.then(({ transaction }) => {
navigate(`/groups/${groupId}/transactions/${transaction.id}?no-redirect=true`);
});
};

return (
<MobilePaper>
<Typography variant="h5">Settle this groups balances</Typography>
<List>
{settlementPlan.map((planItem) => (
<ListItem key={`${planItem.creditorId}-${planItem.debitorId}`}>
<ListItemText
primary={
<span>
{accountMap[planItem.creditorId].name} pays {accountMap[planItem.debitorId].name}{" "}
{planItem.paymentAmount.toFixed(2)}
{currencySymbol}
</span>
}
/>
<ListItemSecondaryAction>
<Button onClick={() => onSettleClicked(planItem)}>Settle</Button>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
</MobilePaper>
);
};
2 changes: 1 addition & 1 deletion frontend/apps/web/src/pages/auth/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const Login: React.FC = () => {
setSubmitting(false);
})
.catch((err) => {
toast.error(err);
toast.error(err.message);
setSubmitting(false);
});
};
Expand Down
Loading

0 comments on commit e7737d0

Please sign in to comment.