Skip to content

Commit 09e86ee

Browse files
committed
Add bills
1 parent e150a6c commit 09e86ee

File tree

5 files changed

+188
-9
lines changed

5 files changed

+188
-9
lines changed

src/pyfirefly/models.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,3 +254,57 @@ class Budget(DataClassORJSONMixin):
254254
type: str
255255
id: str
256256
attributes: BudgetAttributes
257+
258+
259+
@dataclass
260+
class BillPaidDate(DataClassORJSONMixin):
261+
"""Model for a Firefly bill paid date."""
262+
263+
transaction_group_id: str | None = None
264+
transaction_journal_id: str | None = None
265+
date: str | None = None
266+
267+
268+
@dataclass
269+
class BillAttributes(DataClassORJSONMixin): # pylint: disable=too-many-instance-attributes
270+
"""Attributes of a Firefly bill."""
271+
272+
created_at: str | None = None
273+
updated_at: str | None = None
274+
currency_id: str | None = None
275+
currency_code: str | None = None
276+
currency_symbol: str | None = None
277+
currency_decimal_places: int | None = None
278+
native_currency_id: str | None = None
279+
native_currency_code: str | None = None
280+
native_currency_symbol: str | None = None
281+
native_currency_decimal_places: int | None = None
282+
name: str | None = None
283+
amount_min: str | None = None
284+
amount_max: str | None = None
285+
native_amount_min: str | None = None
286+
native_amount_max: str | None = None
287+
date: str | None = None
288+
end_date: str | None = None
289+
extension_date: str | None = None
290+
repeat_freq: str | None = None
291+
skip: int | None = None
292+
active: bool | None = None
293+
order: int | None = None
294+
notes: str | None = None
295+
next_expected_match: str | None = None
296+
next_expected_match_diff: str | None = None
297+
object_group_id: str | None = None
298+
object_group_order: int | None = None
299+
object_group_title: str | None = None
300+
pay_dates: list[str] | None = None
301+
paid_dates: list[BillPaidDate] | None = None
302+
303+
304+
@dataclass
305+
class Bill(DataClassORJSONMixin):
306+
"""Model for a Firefly bill."""
307+
308+
type: str
309+
id: str
310+
attributes: BillAttributes

src/pyfirefly/pyfirefly.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
FireflyNotFoundError,
2121
FireflyTimeoutError,
2222
)
23-
from pyfirefly.models import About, Account, Budget, Category, Transaction
23+
from pyfirefly.models import About, Account, Bill, Budget, Category, Transaction
2424

2525
try:
2626
VERSION = metadata.version(__package__)
@@ -253,7 +253,7 @@ async def get_categories(self, category_id: int, start: str | None = None, end:
253253
return Category.from_dict(category["data"])
254254

255255
async def get_budgets(self, start: str | None = None, end: str | None = None) -> list[Budget]:
256-
"""Get budgets for the Firefly server.
256+
"""Get budgets for the Firefly server. Both start and end dates are required for date range filtering.
257257
258258
Args:
259259
start: The start date for the budgets.
@@ -264,18 +264,42 @@ async def get_budgets(self, start: str | None = None, end: str | None = None) ->
264264
265265
"""
266266
params: dict[str, str] = {}
267-
if start:
267+
if start and end:
268268
params["start"] = start
269-
if end:
270269
params["end"] = end
271270

272271
budgets = await self._request(uri="budgets", params=params)
273272
return [Budget.from_dict(budget) for budget in budgets["data"]]
274273

275-
@property
276-
def api_url(self) -> str:
277-
"""Return the API URL."""
278-
return f"{self._api_scheme}://{self._api_host}:{self._api_port}"
274+
async def get_bills(self, start: str | None = None, end: str | None = None) -> list[Bill]:
275+
"""Get bills for the Firefly server. Both start and end dates are required for date range filtering.
276+
277+
Args:
278+
start: The start date for the bills.
279+
end: The end date for the bills.
280+
281+
Returns:
282+
A list of Bill containing bill information.
283+
284+
"""
285+
bills: list[dict[str, Any]] = []
286+
next_page: int | None = 1
287+
params: dict[str, str] = {"page": str(next_page)}
288+
if start and end:
289+
params["start"] = start
290+
params["end"] = end
291+
292+
while next_page:
293+
response = await self._request(uri="bills", params=params)
294+
bills.extend(response["data"])
295+
296+
pagination = response.get("meta", {}).get("pagination", {})
297+
current_page = int(pagination.get("current_page", 1) or 1)
298+
total_pages = int(pagination.get("total_pages", 1) or 1)
299+
300+
next_page = current_page + 1 if current_page < total_pages else None
301+
302+
return [Bill.from_dict(bill) for bill in bills]
279303

280304
async def close(self) -> None:
281305
"""Close open client session."""

tests/__snapshots__/test_models.ambr

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@
1919
Account(type='accounts', id='4', attributes=AccountAttributes(created_at='2021-05-10T09:30:00+01:00', updated_at='2022-05-10T09:30:00+01:00', active=True, name='Credit Card', type='liability', account_role='creditCard', currency_id='14', currency_code='GBP', currency_symbol='£', currency_decimal_places=2, native_currency_id='14', native_currency_code='GBP', native_currency_symbol='£', native_currency_decimal_places=2, current_balance='-250.00', native_current_balance='-250.00', current_balance_date='2022-05-10T09:30:00+01:00', order=3, notes='Credit card account', monthly_payment_date='2022-05-15T09:30:00+01:00', credit_card_type='monthlyFull', account_number='9876543210', iban='GB29NWBK60161331926819', bic='NWBKGB2L', virtual_balance='0.00', native_virtual_balance='0.00', opening_balance='0.00', native_opening_balance='0.00', opening_balance_date='2021-05-10T09:30:00+01:00', liability_type='credit', liability_direction='debit', interest='19.99', interest_period='monthly', current_debt='250.00', include_net_worth=False, longitude=0.1278, latitude=51.5074, zoom_level=10, last_activity=None)),
2020
])
2121
# ---
22+
# name: test_bills_model
23+
list([
24+
Bill(type='bills', id='2', attributes=BillAttributes(created_at='2018-09-17T12:46:47+01:00', updated_at='2018-09-17T12:46:47+01:00', currency_id='5', currency_code='EUR', currency_symbol='$', currency_decimal_places=2, native_currency_id='5', native_currency_code='EUR', native_currency_symbol='$', native_currency_decimal_places=2, name='Rent', amount_min='123.45', amount_max='123.45', native_amount_min='123.45', native_amount_max='123.45', date='2018-09-17T12:46:47+01:00', end_date='2018-09-17T12:46:47+01:00', extension_date='2018-09-17T12:46:47+01:00', repeat_freq='monthly', skip=0, active=True, order=1, notes='Some example notes', next_expected_match='2018-09-17T12:46:47+01:00', next_expected_match_diff='today', object_group_id='5', object_group_order=5, object_group_title='Example Group', pay_dates=['2018-09-17T12:46:47+01:00'], paid_dates=[BillPaidDate(transaction_group_id='123', transaction_journal_id='123', date='2018-09-17T12:46:47+01:00')])),
25+
])
26+
# ---
27+
# name: test_bills_model.1
28+
list([
29+
Bill(type='bills', id='2', attributes=BillAttributes(created_at='2018-09-17T12:46:47+01:00', updated_at='2018-09-17T12:46:47+01:00', currency_id='5', currency_code='EUR', currency_symbol='$', currency_decimal_places=2, native_currency_id='5', native_currency_code='EUR', native_currency_symbol='$', native_currency_decimal_places=2, name='Rent', amount_min='123.45', amount_max='123.45', native_amount_min='123.45', native_amount_max='123.45', date='2018-09-17T12:46:47+01:00', end_date='2018-09-17T12:46:47+01:00', extension_date='2018-09-17T12:46:47+01:00', repeat_freq='monthly', skip=0, active=True, order=1, notes='Some example notes', next_expected_match='2018-09-17T12:46:47+01:00', next_expected_match_diff='today', object_group_id='5', object_group_order=5, object_group_title='Example Group', pay_dates=['2018-09-17T12:46:47+01:00'], paid_dates=[BillPaidDate(transaction_group_id='123', transaction_journal_id='123', date='2018-09-17T12:46:47+01:00')])),
30+
])
31+
# ---
2232
# name: test_budgets_model
2333
list([
2434
Budget(type='budgets', id='2', attributes=BudgetAttributes(created_at='2018-09-17T12:46:47+01:00', updated_at='2018-09-17T12:46:47+01:00', name='Bills', active=False, notes='Some notes', order=5, auto_budget_type='reset', currency_id='12', currency_code='EUR', currency_symbol='$', currency_decimal_places=2, native_currency_id='5', native_currency_code='EUR', native_currency_symbol='$', native_currency_decimal_places=2, auto_budget_amount='-1012.12', native_auto_budget_amount='-1012.12', auto_budget_period='monthly', spent=[BudgetSpent(sum='123.45', currency_id='5', currency_code='USD', currency_symbol='$', currency_decimal_places=2)])),

tests/fixtures/bills.json

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{
2+
"data": [
3+
{
4+
"type": "bills",
5+
"id": "2",
6+
"attributes": {
7+
"created_at": "2018-09-17T12:46:47+01:00",
8+
"updated_at": "2018-09-17T12:46:47+01:00",
9+
"currency_id": "5",
10+
"currency_code": "EUR",
11+
"currency_symbol": "$",
12+
"currency_decimal_places": 2,
13+
"native_currency_id": "5",
14+
"native_currency_code": "EUR",
15+
"native_currency_symbol": "$",
16+
"native_currency_decimal_places": 2,
17+
"name": "Rent",
18+
"amount_min": "123.45",
19+
"amount_max": "123.45",
20+
"native_amount_min": "123.45",
21+
"native_amount_max": "123.45",
22+
"date": "2018-09-17T12:46:47+01:00",
23+
"end_date": "2018-09-17T12:46:47+01:00",
24+
"extension_date": "2018-09-17T12:46:47+01:00",
25+
"repeat_freq": "monthly",
26+
"skip": 0,
27+
"active": true,
28+
"order": 1,
29+
"notes": "Some example notes",
30+
"next_expected_match": "2018-09-17T12:46:47+01:00",
31+
"next_expected_match_diff": "today",
32+
"object_group_id": "5",
33+
"object_group_order": 5,
34+
"object_group_title": "Example Group",
35+
"pay_dates": [
36+
"2018-09-17T12:46:47+01:00"
37+
],
38+
"paid_dates": [
39+
{
40+
"transaction_group_id": "123",
41+
"transaction_journal_id": "123",
42+
"date": "2018-09-17T12:46:47+01:00"
43+
}
44+
]
45+
}
46+
}
47+
],
48+
"meta": {
49+
"pagination": {
50+
"total": 3,
51+
"count": 20,
52+
"per_page": 100,
53+
"current_page": 1,
54+
"total_pages": 1
55+
}
56+
}
57+
}

tests/test_models.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,38 @@ async def test_budgets_model(
163163
budgets = await firefly_client.get_budgets()
164164
assert budgets == snapshot
165165

166-
# Now for all budgets
166+
167+
async def test_bills_model(
168+
aresponses: ResponsesMockServer,
169+
firefly_client: Firefly,
170+
snapshot: SnapshotAssertion,
171+
) -> None:
172+
"""Test the Bills model."""
173+
aresponses.add(
174+
"localhost:9000",
175+
"/api/v1/bills",
176+
"GET",
177+
aresponses.Response(
178+
status=200,
179+
headers={"Content-Type": "application/vnd.api+json"},
180+
text=load_fixtures("bills.json"),
181+
),
182+
)
183+
184+
bills = await firefly_client.get_bills(start="2025-01-01", end="2025-12-31")
185+
assert bills == snapshot
186+
187+
# Now without a date range
188+
aresponses.add(
189+
"localhost:9000",
190+
"/api/v1/bills",
191+
"GET",
192+
aresponses.Response(
193+
status=200,
194+
headers={"Content-Type": "application/vnd.api+json"},
195+
text=load_fixtures("bills.json"),
196+
),
197+
)
198+
199+
bills = await firefly_client.get_bills()
200+
assert bills == snapshot

0 commit comments

Comments
 (0)