django-ledger 0.7.3__py3-none-any.whl → 0.7.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of django-ledger might be problematic. Click here for more details.

Files changed (42) hide show
  1. django_ledger/__init__.py +1 -1
  2. django_ledger/contrib/django_ledger_graphene/journal_entry/schema.py +2 -3
  3. django_ledger/contrib/django_ledger_graphene/transaction/schema.py +9 -7
  4. django_ledger/forms/journal_entry.py +19 -12
  5. django_ledger/forms/transactions.py +8 -12
  6. django_ledger/io/io_core.py +14 -11
  7. django_ledger/io/io_library.py +3 -3
  8. django_ledger/migrations/0001_initial.py +1 -1
  9. django_ledger/migrations/0019_alter_transactionmodel_amount_and_more.py +33 -0
  10. django_ledger/models/bill.py +17 -2
  11. django_ledger/models/chart_of_accounts.py +4 -0
  12. django_ledger/models/closing_entry.py +8 -6
  13. django_ledger/models/invoice.py +12 -4
  14. django_ledger/models/journal_entry.py +843 -481
  15. django_ledger/models/ledger.py +45 -4
  16. django_ledger/models/transactions.py +303 -305
  17. django_ledger/models/unit.py +42 -22
  18. django_ledger/templates/django_ledger/account/tags/accounts_table.html +1 -1
  19. django_ledger/templates/django_ledger/bills/bill_detail.html +1 -1
  20. django_ledger/templates/django_ledger/invoice/invoice_detail.html +1 -1
  21. django_ledger/templates/django_ledger/journal_entry/je_create.html +2 -3
  22. django_ledger/templates/django_ledger/journal_entry/je_delete.html +2 -3
  23. django_ledger/templates/django_ledger/journal_entry/je_detail.html +1 -1
  24. django_ledger/templates/django_ledger/journal_entry/je_detail_txs.html +8 -8
  25. django_ledger/templates/django_ledger/journal_entry/je_list.html +16 -13
  26. django_ledger/templates/django_ledger/journal_entry/je_update.html +2 -3
  27. django_ledger/templates/django_ledger/journal_entry/tags/je_table.html +24 -24
  28. django_ledger/templates/django_ledger/journal_entry/tags/je_txs_table.html +17 -14
  29. django_ledger/templates/django_ledger/ledger/tags/ledgers_table.html +38 -37
  30. django_ledger/templates/django_ledger/transactions/tags/txs_table.html +69 -0
  31. django_ledger/templatetags/django_ledger.py +25 -45
  32. django_ledger/urls/account.py +4 -4
  33. django_ledger/views/account.py +7 -7
  34. django_ledger/views/journal_entry.py +84 -101
  35. django_ledger/views/ledger.py +16 -21
  36. django_ledger/views/mixins.py +11 -10
  37. {django_ledger-0.7.3.dist-info → django_ledger-0.7.4.1.dist-info}/METADATA +8 -3
  38. {django_ledger-0.7.3.dist-info → django_ledger-0.7.4.1.dist-info}/RECORD +42 -40
  39. {django_ledger-0.7.3.dist-info → django_ledger-0.7.4.1.dist-info}/AUTHORS.md +0 -0
  40. {django_ledger-0.7.3.dist-info → django_ledger-0.7.4.1.dist-info}/LICENSE +0 -0
  41. {django_ledger-0.7.3.dist-info → django_ledger-0.7.4.1.dist-info}/WHEEL +0 -0
  42. {django_ledger-0.7.3.dist-info → django_ledger-0.7.4.1.dist-info}/top_level.txt +0 -0
@@ -2,19 +2,23 @@
2
2
  Django Ledger created by Miguel Sanda <msanda@arrobalytics.com>.
3
3
  Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
4
4
 
5
- The TransactionModel is the lowest accounting level where financial information is recorded. Every transaction with a
6
- financial implication must be part of a JournalEntryModel, which encapsulates a collection of TransactionModels.
7
- Transaction models cannot exist without being part of a validated JournalEntryModel. Orphan TransactionModels are not
8
- allowed, and this is enforced by the database.
9
-
10
- A transaction must perform a CREDIT or a DEBIT to the underlying AccountModel. The IOMixIn is crucial for the
11
- production of financial statements and sets its foundation in the TransactionModel API. It allows for effective
12
- querying and aggregating transactions at the Database layer without pulling all TransactionModels into memory.
13
- This approach streamlines the production of financial statements. The IOMixIn in the TransactionModel API is essential
14
- for efficient and effective financial statement generation.
5
+ The TransactionModel serves as the foundational accounting entity where all financial transactions are recorded.
6
+ Every transaction must be associated with a JournalEntryModel, which represents a collection
7
+ of related transactions. This strict association ensures that standalone TransactionModels—or orphan transactions—do not
8
+ exist, a constraint enforced at the database level.
9
+
10
+ Each transaction performs either a CREDIT or a DEBIT operation on the designated AccountModel, upholding standard
11
+ accounting principles. The TransactionModel API integrates the IOMixIn, a critical component for generating financial
12
+ statements. This mixin facilitates efficient querying and aggregation directly at the database level, eliminating the need
13
+ to load all TransactionModels into memory. This database-driven approach significantly improves performance and simplifies
14
+ the process of generating accurate financial reports.
15
+
16
+ The TransactionModel, together with the IOMixIn, is essential for ensuring seamless, efficient, and reliable
17
+ financial statement production in the Django Ledger framework.
15
18
  """
19
+
16
20
  from datetime import datetime, date
17
- from typing import List, Union, Optional
21
+ from typing import List, Union, Optional, Set
18
22
  from uuid import uuid4, UUID
19
23
 
20
24
  from django.contrib.auth import get_user_model
@@ -26,11 +30,7 @@ from django.db.models.signals import pre_save
26
30
  from django.utils.translation import gettext_lazy as _
27
31
 
28
32
  from django_ledger.io.io_core import validate_io_timestamp
29
- from django_ledger.models.accounts import AccountModel
30
- from django_ledger.models.bill import BillModel
31
- from django_ledger.models.entity import EntityModel
32
- from django_ledger.models.invoice import InvoiceModel
33
- from django_ledger.models.ledger import LedgerModel
33
+ from django_ledger.models import AccountModel, BillModel, EntityModel, InvoiceModel, LedgerModel
34
34
  from django_ledger.models.mixins import CreateUpdateMixIn
35
35
  from django_ledger.models.unit import EntityUnitModel
36
36
  from django_ledger.models.utils import lazy_loader
@@ -44,21 +44,23 @@ class TransactionModelValidationError(ValidationError):
44
44
 
45
45
  class TransactionModelQuerySet(QuerySet):
46
46
  """
47
- A custom QuerySet class for TransactionModels implementing methods to effectively and safely read
48
- TransactionModels from the database.
47
+ A custom QuerySet class tailored for `TransactionModel` objects. It includes a collection
48
+ of methods to efficiently and safely retrieve and filter transactions from the database
49
+ based on common use cases.
49
50
  """
50
51
 
51
52
  def posted(self) -> QuerySet:
52
53
  """
53
- Fetches a QuerySet of posted transactions only.
54
- Posted transactions are must meet the following criteria:
55
- * Be bart of a *posted* JournalEntryModel.
56
- * The associated JournalEntryModel must be part of a *posted* LedgerModel.
54
+ Retrieves transactions that are part of a posted journal entry and ledger.
55
+
56
+ A transaction is considered "posted" if:
57
+ - It belongs to a journal entry marked as *posted*.
58
+ - Its associated journal entry is part of a ledger marked as *posted*.
57
59
 
58
60
  Returns
59
61
  -------
60
62
  TransactionModelQuerySet
61
- A QuerySet with applied filters.
63
+ A QuerySet containing only transactions that meet the "posted" criteria.
62
64
  """
63
65
  return self.filter(
64
66
  Q(journal_entry__posted=True) &
@@ -67,23 +69,24 @@ class TransactionModelQuerySet(QuerySet):
67
69
 
68
70
  def for_accounts(self, account_list: List[str or AccountModel]):
69
71
  """
70
- Fetches a QuerySet of TransactionModels which AccountModel has a specific role.
72
+ Filters transactions based on the accounts they are associated with.
71
73
 
72
74
  Parameters
73
75
  ----------
74
- account_list: list
75
- A string or list of strings representing the roles to be used as filter.
76
+ account_list : list of str or AccountModel
77
+ A list containing account codes (strings) or `AccountModel` instances.
78
+ Transactions will be filtered to match these accounts.
76
79
 
77
80
  Returns
78
81
  -------
79
82
  TransactionModelQuerySet
80
- Returns a TransactionModelQuerySet with applied filters.
83
+ A QuerySet filtered for transactions associated with the specified accounts.
81
84
  """
82
85
  if isinstance(account_list, list) > 0 and isinstance(account_list[0], str):
83
86
  return self.filter(account__code__in=account_list)
84
87
  return self.filter(account__in=account_list)
85
88
 
86
- def for_roles(self, role_list: Union[str, List[str]]):
89
+ def for_roles(self, role_list: Union[str, List[str], Set[str]]):
87
90
  """
88
91
  Fetches a QuerySet of TransactionModels which AccountModel has a specific role.
89
92
 
@@ -103,35 +106,35 @@ class TransactionModelQuerySet(QuerySet):
103
106
 
104
107
  def for_unit(self, unit_slug: Union[str, EntityUnitModel]):
105
108
  """
106
- Fetches a QuerySet of TransactionModels associated with a specific EntityUnitModel.
109
+ Filters transactions based on their associated entity unit.
107
110
 
108
111
  Parameters
109
112
  ----------
110
- unit_slug: str or EntityUnitModel
111
- A string representing the unit slug used to filter the QuerySet.
113
+ unit_slug : str or EntityUnitModel
114
+ A string representing the slug of the entity unit or an `EntityUnitModel` instance.
112
115
 
113
116
  Returns
114
117
  -------
115
118
  TransactionModelQuerySet
116
- Returns a TransactionModelQuerySet with applied filters.
119
+ A QuerySet filtered for transactions linked to the specified unit.
117
120
  """
118
121
  if isinstance(unit_slug, EntityUnitModel):
119
- return self.filter(journal_entry__ledger__unit=unit_slug)
120
- return self.filter(journal_entry__ledger__unit__slug__exact=unit_slug)
122
+ return self.filter(journal_entry__entity_unit=unit_slug)
123
+ return self.filter(journal_entry__entity_unit__slug__exact=unit_slug)
121
124
 
122
- def for_activity(self, activity_list: Union[str, List[str]]):
125
+ def for_activity(self, activity_list: Union[str, List[str], Set[str]]):
123
126
  """
124
- Fetches a QuerySet of TransactionModels associated with a specific activity or list of activities.
127
+ Filters transactions based on their associated activity or activities.
125
128
 
126
129
  Parameters
127
130
  ----------
128
- activity_list: str or list
129
- A string or list of strings representing the activity or activities used to filter the QuerySet.
131
+ activity_list : str or list of str
132
+ A single activity or a list of activities to filter transactions by.
130
133
 
131
134
  Returns
132
135
  -------
133
136
  TransactionModelQuerySet
134
- Returns a TransactionModelQuerySet with applied filters.
137
+ A QuerySet filtered for transactions linked to the specified activity or activities.
135
138
  """
136
139
  if isinstance(activity_list, str):
137
140
  return self.filter(journal_entry__activity__in=[activity_list])
@@ -139,20 +142,21 @@ class TransactionModelQuerySet(QuerySet):
139
142
 
140
143
  def to_date(self, to_date: Union[str, date, datetime]):
141
144
  """
142
- Fetches a QuerySet of TransactionModels associated with a maximum date or timestamp filter.
143
- May pass aware or naive date or timestamps. If naive is passed, it is assumed to be in localtime based
144
- on Django Settings.
145
+ Filters transactions occurring on or before a specific date or timestamp.
146
+
147
+ If `to_date` is a naive datetime (no timezone), it is assumed to be in local time
148
+ based on Django settings.
145
149
 
146
150
  Parameters
147
151
  ----------
148
- to_date: str or date or datetime
149
- A string, date or datetime representing the maximum point in time used to filter the QuerySet.
150
- If date is used, dates are inclusive. (i.e 12/20/2022 will also include the 20th day).
152
+ to_date : str, date, or datetime
153
+ The maximum date or timestamp for filtering. When using a date (not datetime),
154
+ the filter is inclusive (e.g., "2022-12-20" includes all transactions from that day).
151
155
 
152
156
  Returns
153
157
  -------
154
158
  TransactionModelQuerySet
155
- Returns a TransactionModelQuerySet with applied filters.
159
+ A QuerySet filtered to include transactions up to the specified date or timestamp.
156
160
  """
157
161
 
158
162
  if isinstance(to_date, str):
@@ -164,20 +168,21 @@ class TransactionModelQuerySet(QuerySet):
164
168
 
165
169
  def from_date(self, from_date: Union[str, date, datetime]):
166
170
  """
167
- Fetches a QuerySet of TransactionModels associated with a minimum date or timestamp filter.
168
- May pass aware or naive date or timestamps. If naive is passed, it is assumed to be in localtime based
169
- on Django Settings.
171
+ Filters transactions occurring on or after a specific date or timestamp.
172
+
173
+ If `from_date` is a naive datetime (no timezone), it is assumed to be in local time
174
+ based on Django settings.
170
175
 
171
176
  Parameters
172
177
  ----------
173
- from_date: str or date or datetime
174
- A string, date or datetime representing the minimum point in time used to filter the QuerySet.
175
- If date is used, dates are inclusive. (i.e 12/20/2022 will also include the 20th day).
178
+ from_date : str, date, or datetime
179
+ The minimum date or timestamp for filtering. When using a date (not datetime),
180
+ the filter is inclusive (e.g., "2022-12-20" includes all transactions from that day).
176
181
 
177
182
  Returns
178
183
  -------
179
184
  TransactionModelQuerySet
180
- Returns a TransactionModelQuerySet with applied filters.
185
+ A QuerySet filtered to include transactions from the specified date or timestamp onwards.
181
186
  """
182
187
  if isinstance(from_date, str):
183
188
  from_date = validate_io_timestamp(from_date)
@@ -189,319 +194,269 @@ class TransactionModelQuerySet(QuerySet):
189
194
 
190
195
  def not_closing_entry(self):
191
196
  """
192
- Filter the Transactions based on whether they are closing entries or not.
197
+ Filters transactions that are *not* part of a closing journal entry.
193
198
 
194
- Returns:
195
- QuerySet: A filtered QuerySet of entries where the journal_entry__is_closing_entry field is False.
199
+ Returns
200
+ -------
201
+ TransactionModelQuerySet
202
+ A QuerySet with transactions where the `journal_entry__is_closing_entry` field is False.
196
203
  """
197
204
  return self.filter(journal_entry__is_closing_entry=False)
198
205
 
199
206
  def is_closing_entry(self):
200
207
  """
201
- Filter the Transactions based on whether they are closing entries or not.
208
+ Filters transactions that are part of a closing journal entry.
202
209
 
203
- Returns:
204
- QuerySet: A filtered QuerySet of entries where the journal_entry__is_closing_entry field is True.
210
+ Returns
211
+ -------
212
+ TransactionModelQuerySet
213
+ A QuerySet with transactions where the `journal_entry__is_closing_entry` field is True.
205
214
  """
206
215
  return self.filter(journal_entry__is_closing_entry=True)
207
216
 
208
-
209
- class TransactionModelManager(Manager):
210
- """
211
- A manager class for the TransactionModel.
212
- """
213
-
214
- def get_queryset(self) -> TransactionModelQuerySet:
215
- qs = TransactionModelQuerySet(self.model, using=self._db)
216
- return qs.annotate(
217
- _coa_id=F('account__coa_model_id'),
218
- ).select_related(
219
- 'journal_entry',
220
- 'account',
221
- 'account__coa_model',
222
- )
223
-
224
- def for_user(self, user_model) -> TransactionModelQuerySet:
217
+ def for_ledger(self, ledger_model: Union[LedgerModel, UUID, str]):
225
218
  """
219
+ Filters transactions for a specific ledger under a given entity.
220
+
226
221
  Parameters
227
222
  ----------
228
- user_model : User model object
229
- The user model object representing the user for whom to filter the transactions.
223
+ ledger_model : Union[LedgerModel, UUID]
224
+ The ledger model or its UUID to filter by.
230
225
 
231
226
  Returns
232
227
  -------
233
228
  TransactionModelQuerySet
234
- A queryset of transaction models filtered based on the user's permissions.
235
-
236
- Raises
237
- ------
238
- None
239
-
240
- Description
241
- -----------
242
- This method filters the transactions based on the user's permissions.
243
- If the user is a superuser, all transactions are returned. Otherwise, the transactions are filtered based on
244
- the user's relationship to the entities associated with the transactions. Specifically, the transactions are
245
- filtered to include only those where either the user is an admin of the entity associated with the transaction's
246
- ledger or the user is one of the managers of the entity associated with the transaction's ledger.
229
+ A queryset containing transactions associated with the given ledger and entity.
247
230
  """
248
- qs = self.get_queryset()
249
- return qs.filter(
250
- Q(journal_entry__ledger__entity__admin=user_model) |
251
- Q(journal_entry__ledger__entity__managers__in=[user_model])
252
- )
231
+ if isinstance(ledger_model, UUID):
232
+ return self.filter(journal_entry__ledger__uuid__exact=ledger_model)
233
+ return self.filter(journal_entry__ledger=ledger_model)
253
234
 
254
- def for_entity(self,
255
- entity_slug: Union[EntityModel, str, UUID],
256
- user_model: Optional[UserModel] = None,
257
- ) -> TransactionModelQuerySet:
235
+ def for_journal_entry(self, je_model):
258
236
  """
237
+ Filters transactions for a specific journal entry under a given ledger and entity.
238
+
259
239
  Parameters
260
240
  ----------
261
- entity_slug : Union[EntityModel, str, UUID]
262
- The entity slug or ID for which to retrieve transactions.
263
- Can be an instance of EntityModel, a string representing the slug, or a UUID.
264
- user_model : Optional[UserModel], optional
265
- The user model for which to filter transactions.
266
- If provided, only transactions associated with the specified user will be returned.
267
- Defaults to None.
241
+ je_model : Union[JournalEntryModel, UUID]
242
+ The journal entry model or its UUID to filter by.
268
243
 
269
244
  Returns
270
245
  -------
271
246
  TransactionModelQuerySet
272
- A QuerySet of TransactionModel instances filtered by the provided parameters.
247
+ A queryset containing transactions associated with the given journal entry.
273
248
  """
249
+ if isinstance(je_model, lazy_loader.get_journal_entry_model()):
250
+ return self.filter(journal_entry=je_model)
251
+ return self.filter(journal_entry__uuid__exact=je_model)
274
252
 
275
- if user_model:
276
- qs = self.for_user(user_model=user_model)
277
- else:
278
- qs = self.get_queryset()
279
-
280
- if isinstance(entity_slug, EntityModel):
281
- return qs.filter(journal_entry__ledger__entity=entity_slug)
282
- elif isinstance(entity_slug, UUID):
283
- return qs.filter(journal_entry__ledger__entity_id=entity_slug)
284
- return qs.filter(journal_entry__ledger__entity__slug__exact=entity_slug)
285
-
286
- def for_ledger(self,
287
- entity_slug: Union[EntityModel, str],
288
- ledger_model: Union[LedgerModel, UUID],
289
- user_model: Optional[UserModel] = None):
253
+ def for_bill(self, bill_model: Union[BillModel, str, UUID]):
290
254
  """
255
+ Filters transactions for a specific bill under a given entity.
256
+
291
257
  Parameters
292
258
  ----------
293
- entity_slug : Union[EntityModel, str]
294
- The slug or instance of the entity for which to filter the ledger.
295
- ledger_model : Union[LedgerModel, UUID]
296
- The ledger model or UUID of the ledger for which to filter the journal entries.
297
- user_model : Optional[UserModel], optional
298
- The user model associated with the entity. Default is None.
259
+ bill_model : Union[BillModel, str, UUID]
260
+ The bill model or its UUID to filter by.
299
261
 
300
262
  Returns
301
263
  -------
302
- QuerySet
303
- The filtered QuerySet containing the journal entries for the specified entity and ledger.
264
+ TransactionModelQuerySet
265
+ A queryset containing transactions related to the specified bill.
304
266
  """
305
- qs = self.for_entity(user_model=user_model, entity_slug=entity_slug)
306
- if isinstance(ledger_model, UUID):
307
- return qs.filter(journal_entry__ledger__uuid__exact=ledger_model)
308
- return qs.filter(journal_entry__ledger=ledger_model)
267
+ if isinstance(bill_model, BillModel):
268
+ return self.filter(journal_entry__ledger__billmodel=bill_model)
269
+ return self.filter(journal_entry__ledger__billmodel__uuid__exact=bill_model)
309
270
 
310
- def for_unit(self,
311
- entity_slug: Union[EntityModel, str],
312
- unit_slug: str = Union[EntityUnitModel, str],
313
- user_model: Optional[UserModel] = None):
271
+ def for_invoice(self, invoice_model: Union[InvoiceModel, str, UUID]):
314
272
  """
315
- Returns the queryset filtered for the specified entity unit.
273
+ Filters transactions for a specific invoice under a given entity.
316
274
 
317
275
  Parameters
318
276
  ----------
319
- entity_slug : Union[EntityModel, str]
320
- The entity model or slug used to filter the queryset.
321
- unit_slug : Union[EntityUnitModel, str]
322
- The entity unit model or slug used to filter the queryset.
323
- user_model : Optional[UserModel], optional
324
- The user model to consider for filtering the queryset, by default None.
277
+ invoice_model : Union[InvoiceModel, str, UUID]
278
+ The invoice model or its UUID to filter by.
325
279
 
326
280
  Returns
327
281
  -------
328
- QuerySet
329
- The filtered queryset based on the specified entity unit.
330
-
331
- Notes
332
- -----
333
- - If `unit_slug` is an instance of `EntityUnitModel`, the queryset is filtered using `journal_entry__entity_unit=unit_slug`.
334
- - If `unit_slug` is a string, the queryset is filtered using `journal_entry__entity_unit__slug__exact=unit_slug`.
282
+ TransactionModelQuerySet
283
+ A queryset containing transactions related to the specified invoice.
335
284
  """
336
- qs = self.for_entity(user_model=user_model, entity_slug=entity_slug)
337
- if isinstance(unit_slug, EntityUnitModel):
338
- return qs.filter(journal_entry__entity_unit=unit_slug)
339
- return qs.filter(journal_entry__entity_unit__slug__exact=unit_slug)
285
+ if isinstance(invoice_model, InvoiceModel):
286
+ return self.filter(journal_entry__ledger__invoicemodel=invoice_model)
287
+ return self.filter(journal_entry__ledger__invoicemodel__uuid__exact=invoice_model)
288
+
289
+ def with_annotated_details(self):
290
+ return self.annotate(
291
+ entity_unit_name=F('journal_entry__entity_unit__name'),
292
+ account_code=F('account__code'),
293
+ account_name=F('account__name'),
294
+ timestamp=F('journal_entry__timestamp'),
295
+ )
296
+
297
+
298
+ class TransactionModelManager(Manager):
299
+ """
300
+ A custom manager for `TransactionModel` designed to add helper methods for
301
+ querying and filtering `TransactionModel` objects efficiently based on use cases like
302
+ user permissions, associated entities, ledgers, journal entries, and more.
303
+
304
+ This manager leverages `TransactionModelQuerySet` for complex query construction and
305
+ integrates advanced filtering options based on user roles, entities, and other relationships.
306
+ """
340
307
 
341
- def for_journal_entry(self,
342
- entity_slug: Union[EntityModel, str],
343
- ledger_model: Union[LedgerModel, str, UUID],
344
- je_model,
345
- user_model: Optional[UserModel] = None):
308
+ def get_queryset(self) -> TransactionModelQuerySet:
346
309
  """
347
- Parameters
348
- ----------
349
- entity_slug : Union[EntityModel, str]
350
- The entity slug or instance of EntityModel representing the entity for which the journal entry is requested.
351
- ledger_model : Union[LedgerModel, str, UUID]
352
- The ledger model or its identifier (str or UUID) representing the ledger for which the journal entry
353
- is requested.
354
- je_model : Type[JournalEntryModel]
355
- The journal entry model or its identifier (str or UUID) representing the journal entry to filter by.
356
- user_model : Optional[UserModel], default=None
357
- An optional user model instance representing the user for whom the journal entry is requested.
310
+ Retrieves the base queryset for `TransactionModel`, annotated and pre-loaded
311
+ with commonly used related fields.
358
312
 
359
313
  Returns
360
314
  -------
361
- QuerySet
362
- The filtered queryset of journal entries.
363
-
315
+ TransactionModelQuerySet
316
+ A custom queryset with essential annotations and relationships preloaded.
364
317
  """
365
- qs = self.for_ledger(user_model=user_model,
366
- entity_slug=entity_slug,
367
- ledger_model=ledger_model)
368
-
369
- if isinstance(je_model, lazy_loader.get_journal_entry_model()):
370
- return qs.filter(journal_entry=je_model)
371
- return qs.filter(journal_entry__uuid__exact=je_model)
318
+ qs = TransactionModelQuerySet(self.model, using=self._db)
319
+ return qs.annotate(
320
+ _coa_id=F('account__coa_model_id') # Annotates the `coa_model_id` from the related `account`.
321
+ ).select_related(
322
+ 'journal_entry', # Pre-loads the related Journal Entry.
323
+ 'account', # Pre-loads the Account associated with the Transaction.
324
+ 'account__coa_model', # Pre-loads the Chart of Accounts related to the Account.
325
+ )
372
326
 
373
- def for_bill(self,
374
- user_model,
375
- entity_slug: str,
376
- bill_model: Union[BillModel, str, UUID]):
327
+ def for_user(self, user_model) -> TransactionModelQuerySet:
377
328
  """
329
+ Filters transactions accessible to a specific user based on their permissions.
330
+
378
331
  Parameters
379
332
  ----------
380
- user_model : Type
381
- An instance of user model.
382
- entity_slug : str
383
- The slug of the entity.
384
- bill_model : Union[BillModel, str, UUID]
385
- An instance of bill model or a string/UUID representing the UUID of the bill model.
333
+ user_model : UserModel
334
+ The user object for which the transactions should be filtered.
386
335
 
387
336
  Returns
388
337
  -------
389
- FilterQuerySet
390
- A filtered queryset based on the user model, entity slug, and bill model.
338
+ TransactionModelQuerySet
339
+ A queryset containing transactions filtered by the user's access level.
391
340
 
341
+ Description
342
+ -----------
343
+ - Returns all `TransactionModel` objects for superusers.
344
+ - For regular users, it filters transactions where:
345
+ - The user is an admin of the entity associated with the ledger in the transaction.
346
+ - The user is a manager of the entity associated with the ledger in the transaction.
392
347
  """
393
- qs = self.for_entity(
394
- user_model=user_model,
395
- entity_slug=entity_slug)
396
- if isinstance(bill_model, BillModel):
397
- return qs.filter(journal_entry__ledger__billmodel=bill_model)
398
- return qs.filter(journal_entry__ledger__billmodel__uuid__exact=bill_model)
348
+ qs = self.get_queryset()
349
+ return qs.filter(
350
+ Q(journal_entry__ledger__entity__admin=user_model) |
351
+ Q(journal_entry__ledger__entity__managers__in=[user_model])
352
+ )
399
353
 
400
- def for_invoice(self,
401
- user_model,
402
- entity_slug: str,
403
- invoice_model: Union[InvoiceModel, str, UUID]):
354
+ def for_entity(self,
355
+ entity_slug: Union[EntityModel, str, UUID],
356
+ user_model: Optional[UserModel] = None) -> TransactionModelQuerySet:
404
357
  """
358
+ Filters transactions for a specific entity, optionally scoped to a specific user.
359
+
405
360
  Parameters
406
361
  ----------
407
- user_model : [type]
408
- The user model used for filtering entities.
409
- entity_slug : str
410
- The slug of the entity used for filtering.
411
- invoice_model : Union[InvoiceModel, str, UUID]
412
- The invoice model or its identifier used for filtering.
362
+ entity_slug : Union[EntityModel, str, UUID]
363
+ Identifier for the entity. This can be an `EntityModel` object, a slug (str), or a UUID.
364
+ user_model : Optional[UserModel], optional
365
+ The user for whom transactions should be filtered. If provided, applies user-specific
366
+ filtering. Defaults to None.
413
367
 
414
368
  Returns
415
369
  -------
416
- QuerySet
417
- The filtered queryset based on the specified parameters.
370
+ TransactionModelQuerySet
371
+ A queryset containing transactions associated with the specified entity.
372
+
373
+ Notes
374
+ -----
375
+ - If `user_model` is provided, only transactions accessible by the user are included.
376
+ - Supports flexible filtering by accepting different forms of `entity_slug`.
418
377
  """
419
- qs = self.for_entity(
420
- user_model=user_model,
421
- entity_slug=entity_slug)
422
- if isinstance(invoice_model, InvoiceModel):
423
- return qs.filter(journal_entry__ledger__invoicemodel=invoice_model)
424
- return qs.filter(journal_entry__ledger__invoicemodel__uuid__exact=invoice_model)
378
+ if user_model:
379
+ qs = self.for_user(user_model=user_model)
380
+ else:
381
+ qs = self.get_queryset()
382
+
383
+ if isinstance(entity_slug, EntityModel):
384
+ return qs.filter(journal_entry__ledger__entity=entity_slug)
385
+ elif isinstance(entity_slug, UUID):
386
+ return qs.filter(journal_entry__ledger__entity_id=entity_slug)
387
+ return qs.filter(journal_entry__ledger__entity__slug__exact=entity_slug)
425
388
 
426
389
 
427
390
  class TransactionModelAbstract(CreateUpdateMixIn):
428
391
  """
429
-
430
- TransactionModelAbstract
431
-
432
- An abstract class that represents a transaction in the ledger system.
433
-
434
- Attributes
435
- ----------
436
-
437
- - CREDIT: A constant representing a credit transaction.
438
- - DEBIT: A constant representing a debit transaction.
439
-
440
- - TX_TYPE: A list of tuples representing the transaction type choices.
441
-
442
- - uuid: A UUIDField representing the unique identifier of the transaction.
443
- This field is automatically generated and is not editable.
444
-
445
- - tx_type: A CharField representing the type of the transaction.
446
- It has a maximum length of 10 characters and accepts choices from the TX_TYPE list.
447
-
448
- - journal_entry: A ForeignKey representing the journal entry associated with the transaction.
449
- It references the 'django_ledger.JournalEntryModel' model.
450
-
451
- - account: A ForeignKey representing the account associated with the transaction.
452
- It references the 'django_ledger.AccountModel' model.
453
-
454
- - amount: A DecimalField representing the amount of the transaction.
455
- It has a maximum of 2 decimal places and a maximum of 20 digits.
456
- It defaults to 0.00 and accepts a minimum value of 0.
457
-
458
- - description: A CharField representing the description of the transaction.
459
- It has a maximum length of 100 characters and is optional.
460
-
461
- - objects: An instance of the TransactionModelAdmin class.
462
-
463
- Methods
464
- -------
465
-
466
- - clean(): Performs validation on the transaction instance.
467
- Raises a TransactionModelValidationError if the account is a root account.
468
-
392
+ Abstract model for representing a financial transaction in the ledger system.
393
+
394
+ This model defines the core structure and behavior that every transaction record is
395
+ expected to have, including fields like transaction type, associated account, amount,
396
+ and additional metadata used for validation and functionality.
397
+
398
+ Attributes:
399
+ -----------
400
+ Constants:
401
+ - CREDIT: Constant representing a credit transaction.
402
+ - DEBIT: Constant representing a debit transaction.
403
+ - TX_TYPE: A list of choices providing options for transaction types, including CREDIT and DEBIT.
404
+
405
+ Fields:
406
+ - uuid (UUIDField): The unique identifier for the transaction. Automatically generated, non-editable, and primary key.
407
+ - tx_type (CharField): Specifies the transaction type (CREDIT or DEBIT). Choices are based on the TX_TYPE constant. Maximum length is 10 characters.
408
+ - journal_entry (ForeignKey): References the related journal entry from the `django_ledger.JournalEntryModel`.
409
+ This field is not editable and is essential for linking transactions to journal entries.
410
+ - account (ForeignKey): References the associated account from `django_ledger.AccountModel`. Protected from being deleted.
411
+ - amount (DecimalField): Represents the transaction amount, up to 20 digits and 2 decimal places.
412
+ The default value is 0.00, and it enforces a minimum value of 0.
413
+ - description (CharField): Optional field for a brief description of the transaction.
414
+ The maximum length is 100 characters.
415
+ - cleared (BooleanField): Indicates whether the transaction has been cleared. Defaults to False.
416
+ - reconciled (BooleanField): Indicates whether the transaction has been reconciled. Defaults to False.
417
+ - objects (TransactionModelManager): Custom model manager providing advanced helper methods for querying and filtering transactions.
469
418
  """
470
419
 
471
420
  CREDIT = 'credit'
472
421
  DEBIT = 'debit'
473
-
474
422
  TX_TYPE = [
475
423
  (CREDIT, _('Credit')),
476
424
  (DEBIT, _('Debit'))
477
425
  ]
478
426
 
479
427
  uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
480
- tx_type = models.CharField(max_length=10, choices=TX_TYPE, verbose_name=_('Tx Type'))
481
- journal_entry = models.ForeignKey('django_ledger.JournalEntryModel',
482
- editable=False,
483
- verbose_name=_('Journal Entry'),
484
- help_text=_('Journal Entry to be associated with this transaction.'),
485
- on_delete=models.CASCADE)
486
- account = models.ForeignKey('django_ledger.AccountModel',
487
- verbose_name=_('Account'),
488
- help_text=_('Account from Chart of Accounts to be associated with this transaction.'),
489
- on_delete=models.PROTECT)
490
- amount = models.DecimalField(decimal_places=2,
491
- max_digits=20,
492
- default=0.00,
493
- verbose_name=_('Amount'),
494
- help_text=_('Account of the transaction.'),
495
- validators=[MinValueValidator(0)])
496
- description = models.CharField(max_length=100,
497
- null=True,
498
- blank=True,
499
- verbose_name=_('Tx Description'),
500
- help_text=_('A description to be included with this individual transaction'))
501
-
428
+ tx_type = models.CharField(max_length=10, choices=TX_TYPE, verbose_name=_('Transaction Type'))
429
+
430
+ journal_entry = models.ForeignKey(
431
+ 'django_ledger.JournalEntryModel',
432
+ editable=False,
433
+ verbose_name=_('Journal Entry'),
434
+ help_text=_('Journal Entry to be associated with this transaction.'),
435
+ on_delete=models.CASCADE
436
+ )
437
+ account = models.ForeignKey(
438
+ 'django_ledger.AccountModel',
439
+ verbose_name=_('Account'),
440
+ help_text=_('Account from Chart of Accounts to be associated with this transaction.'),
441
+ on_delete=models.PROTECT
442
+ )
443
+ amount = models.DecimalField(
444
+ decimal_places=2,
445
+ max_digits=20,
446
+ default=0.00,
447
+ verbose_name=_('Amount'),
448
+ help_text=_('Amount of the transaction.'),
449
+ validators=[MinValueValidator(0)]
450
+ )
451
+ description = models.CharField(
452
+ max_length=100,
453
+ null=True,
454
+ blank=True,
455
+ verbose_name=_('Transaction Description'),
456
+ help_text=_('A description to be included with this individual transaction.')
457
+ )
502
458
  cleared = models.BooleanField(default=False, verbose_name=_('Cleared'))
503
459
  reconciled = models.BooleanField(default=False, verbose_name=_('Reconciled'))
504
-
505
460
  objects = TransactionModelManager()
506
461
 
507
462
  class Meta:
@@ -520,14 +475,20 @@ class TransactionModelAbstract(CreateUpdateMixIn):
520
475
  ]
521
476
 
522
477
  def __str__(self):
523
- return '{x1}-{x2}/{x5}: {x3}/{x4}'.format(x1=self.account.code,
524
- x2=self.account.name,
525
- x3=self.amount,
526
- x4=self.tx_type,
527
- x5=self.account.balance_type)
478
+ return '{code}-{name}/{balance_type}: {amount}/{tx_type}'.format(
479
+ code=self.account.code,
480
+ name=self.account.name,
481
+ balance_type=self.account.balance_type,
482
+ amount=self.amount,
483
+ tx_type=self.tx_type
484
+ )
528
485
 
529
486
  @property
530
487
  def coa_id(self):
488
+ """
489
+ Fetch the Chart of Accounts (CoA) ID associated with the transaction's account.
490
+ Returns `None` if the account is not set.
491
+ """
531
492
  try:
532
493
  return getattr(self, '_coa_id')
533
494
  except AttributeError:
@@ -535,11 +496,11 @@ class TransactionModelAbstract(CreateUpdateMixIn):
535
496
  return None
536
497
  return self.account.coa_model_id
537
498
 
538
- def clean(self):
539
- if self.account_id and self.account.is_root_account():
540
- raise TransactionModelValidationError(
541
- message=_('Cannot transact on root accounts')
542
- )
499
+ def is_debit(self):
500
+ return self.tx_type == self.DEBIT
501
+
502
+ def is_credit(self):
503
+ return self.tx_type == self.CREDIT
543
504
 
544
505
 
545
506
  class TransactionModel(TransactionModelAbstract):
@@ -554,27 +515,65 @@ class TransactionModel(TransactionModelAbstract):
554
515
 
555
516
  def transactionmodel_presave(instance: TransactionModel, **kwargs):
556
517
  """
518
+ Pre-save validation for the TransactionModel instance.
519
+
520
+ This function is executed before saving a `TransactionModel` instance,
521
+ ensuring that certain conditions are met to maintain data integrity.
522
+
557
523
  Parameters
558
524
  ----------
559
525
  instance : TransactionModel
560
- The transaction model instance that is being saved.
526
+ The `TransactionModel` instance that is about to be saved.
561
527
  kwargs : dict
562
- Additional keyword arguments.
563
-
564
- Notes
565
- -----
566
- This method is called before saving a transaction model instance.
567
- It performs some checks before allowing the save operation.
528
+ Additional keyword arguments, such as the optional `bypass_account_state`.
529
+
530
+ Validations
531
+ -----------
532
+ The function performs the following validations:
533
+ 1. **Account Transactionality**:
534
+ If the `bypass_account_state` flag is not provided or set to `False`,
535
+ it verifies whether the associated account can process transactions
536
+ by calling `instance.account.can_transact()`. If the account cannot
537
+ process transactions, the save operation is interrupted to prevent
538
+ invalid data.
539
+
540
+ 2. **Journal Entry Lock**:
541
+ If the associated journal entry (`instance.journal_entry`) is locked,
542
+ the transaction cannot be modified. The save process is halted if the
543
+ journal entry is marked as locked.
568
544
 
569
545
  Raises
570
546
  ------
571
547
  TransactionModelValidationError
572
- If one of the following conditions is met:
573
- - `bypass_account_state` is False and the `can_transact` method of the associated account model returns False.
574
- - The journal entry associated with the transaction is locked.
575
-
548
+ Raised in the following scenarios:
549
+ - **Account Transactionality Failure**:
550
+ When `bypass_account_state` is `False` or not provided, and the
551
+ associated account (`instance.account`) cannot process transactions.
552
+ The exception contains a message identifying the account.
553
+
554
+ - **Locked Journal Entry**:
555
+ When the associated journal entry (`instance.journal_entry`) is locked,
556
+ preventing modification of any related transactions. The error message
557
+ describes the locked journal entry constraint.
558
+
559
+ Example
560
+ -------
561
+ ```python
562
+ instance = TransactionModel(...)
563
+ try:
564
+ transactionmodel_presave(instance)
565
+ instance.save() # Save proceeds if no validation error occurs
566
+ except TransactionModelValidationError as e:
567
+ handle_error(str(e)) # Handle validation exception
568
+ ```
576
569
  """
577
570
  bypass_account_state = kwargs.get('bypass_account_state', False)
571
+
572
+ if instance.account_id and instance.account.is_root_account():
573
+ raise TransactionModelValidationError(
574
+ message=_('Transactions cannot be linked to root accounts.')
575
+ )
576
+
578
577
  if all([
579
578
  not bypass_account_state,
580
579
  not instance.account.can_transact()
@@ -582,7 +581,6 @@ def transactionmodel_presave(instance: TransactionModel, **kwargs):
582
581
  raise TransactionModelValidationError(
583
582
  message=_(f'Cannot create or modify transactions on account model {instance.account}.')
584
583
  )
585
-
586
584
  if instance.journal_entry.is_locked():
587
585
  raise TransactionModelValidationError(
588
586
  message=_('Cannot modify transactions on locked journal entries.')