django-ledger 0.5.6.4__py3-none-any.whl → 0.6.0__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.

@@ -9,24 +9,23 @@ Contributions to this module:
9
9
  Chart Of Accounts
10
10
  _________________
11
11
 
12
- A Chart of Accounts (CoA) is a collection of accounts logically grouped into a distinct set within a
13
- ChartOfAccountModel. The CoA is the backbone of making of any financial statements and it consist of accounts of many
14
- roles, such as cash, accounts receivable, expenses, liabilities, income, etc. For instance, we can have a heading as
15
- "Fixed Assets" in the Balance Sheet, which will consists of Tangible, Intangible Assets. Further, the tangible assets
16
- will consists of multiple accounts like Building, Plant & Equipments, Machinery. So, aggregation of balances of
17
- individual accounts based on the Chart of Accounts and AccountModel roles, helps in preparation of the Financial
18
- Statements.
19
-
20
- All EntityModel must have a default CoA to be able to create any type of transaction. Throughout the application,
21
- when no explicit CoA is specified, the default behavior is to use the EntityModel default CoA. **Only ONE Chart of
22
- Accounts can be used when creating Journal Entries**. No commingling between CoAs is allowed in order to preserve the
23
- integrity of the Journal Entry.
12
+ A Chart of Accounts (CoA) is a crucial collection of logically grouped accounts within a ChartOfAccountModel,
13
+ forming the backbone of financial statements. The CoA includes various account roles such as cash, accounts receivable,
14
+ expenses, liabilities, and income. For example, the Balance Sheet may have a Fixed Assets heading consisting of
15
+ Tangible and Intangible Assets with multiple accounts like Building, Plant & Equipments, and Machinery under
16
+ tangible assets. Aggregation of individual account balances based on the Chart of Accounts and AccountModel roles is
17
+ essential for preparing Financial Statements.
18
+
19
+ All EntityModel must have a default CoA to create any type of transaction. When no explicit CoA is specified, the
20
+ default behavior is to use the EntityModel default CoA. Only ONE Chart of Accounts can be used when creating
21
+ Journal Entries. No commingling between CoAs is allowed to preserve the integrity of the Journal Entry.
24
22
  """
25
23
  from random import choices
26
24
  from string import ascii_lowercase, digits
27
- from typing import Optional, Union
25
+ from typing import Optional, Union, Dict
28
26
  from uuid import uuid4
29
27
 
28
+ from django.apps import apps
30
29
  from django.contrib.auth import get_user_model
31
30
  from django.core.exceptions import ValidationError
32
31
  from django.db import models
@@ -45,13 +44,20 @@ UserModel = get_user_model()
45
44
 
46
45
  SLUG_SUFFIX = ascii_lowercase + digits
47
46
 
47
+ app_config = apps.get_app_config('django_ledger')
48
+
48
49
 
49
50
  class ChartOfAccountsModelValidationError(ValidationError):
50
51
  pass
51
52
 
52
53
 
53
54
  class ChartOfAccountModelQuerySet(models.QuerySet):
54
- pass
55
+
56
+ def active(self):
57
+ """
58
+ QuerySet method to retrieve active items.
59
+ """
60
+ return self.filter(active=True)
55
61
 
56
62
 
57
63
  class ChartOfAccountModelManager(models.Manager):
@@ -72,13 +78,8 @@ class ChartOfAccountModelManager(models.Manager):
72
78
  user_model
73
79
  Logged in and authenticated django UserModel instance.
74
80
 
75
- Examples
76
- ________
77
- >>> request_user = self.request.user
78
- >>> coa_model_qs = ChartOfAccountModel.objects.for_user(user_model=request_user)
79
-
80
81
  Returns
81
- _______
82
+ -------
82
83
  ChartOfAccountQuerySet
83
84
  Returns a ChartOfAccountQuerySet with applied filters.
84
85
  """
@@ -104,58 +105,33 @@ class ChartOfAccountModelManager(models.Manager):
104
105
  user_model
105
106
  Logged in and authenticated django UserModel instance.
106
107
 
107
- Examples
108
- ________
109
-
110
- >>> request_user = self.request.user
111
- >>> slug = self.kwargs['entity_slug'] # may come from request kwargs
112
- >>> coa_model_qs = ChartOfAccountModelManager.objects.for_entity(user_model=request_user, entity_slug=slug)
113
-
114
108
  Returns
115
- _______
109
+ -------
116
110
  ChartOfAccountQuerySet
117
111
  Returns a ChartOfAccountQuerySet with applied filters.
118
112
  """
119
- qs = self.get_queryset()
113
+ qs = self.for_user(user_model)
120
114
  if isinstance(entity_slug, lazy_loader.get_entity_model()):
121
- return qs.filter(
122
- Q(entity=entity_slug) &
123
- (
124
- Q(entity__admin=user_model) |
125
- Q(entity__managers__in=[user_model])
126
- )
127
- )
128
- return qs.filter(
129
- Q(entity__slug__iexact=entity_slug) &
130
- (
131
- Q(entity__admin=user_model) |
132
- Q(entity__managers__in=[user_model])
133
- )
134
- ).select_related('entity')
115
+ return qs.filter(entity=entity_slug).select_related('entity')
116
+ return qs.filter(entity__slug__iexact=entity_slug).select_related('entity')
135
117
 
136
118
 
137
119
  class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
138
120
  """
139
- Base implementation of Chart of Accounts Model as an Abstract.
140
-
141
- 2. :func:`CreateUpdateMixIn <django_ledger.models.mixins.SlugMixIn>`
142
- 2. :func:`CreateUpdateMixIn <django_ledger.models.mixins.CreateUpdateMixIn>`
143
-
121
+ Abstract base class for the Chart of Account model.
122
+
144
123
  Attributes
145
124
  ----------
146
- uuid : UUID
147
- This is a unique primary key generated for the table. The default value of this field is uuid4().
148
-
149
- entity: EntityModel
150
- The EntityModel associated with this Chart of Accounts.
151
-
152
- active: bool
153
- This determines whether any changes can be done to the Chart of Accounts.
154
- Inactive Chart of Accounts will not be able to be used in new Transactions.
155
- Default value is set to False (inactive).
156
-
157
- description: str
158
- A user generated description for this Chart of Accounts.
125
+ uuid : UUIDField
126
+ UUID field for the chart of account model (primary key).
127
+ entity : ForeignKey
128
+ ForeignKey to the EntityModel.
129
+ active : BooleanField
130
+ BooleanField indicating whether the chart of account is active or not.
131
+ description : TextField
132
+ TextField storing the description of the chart of account.
133
+ objects : ChartOfAccountModelManager
134
+ Manager for the ChartOfAccountModel.
159
135
  """
160
136
 
161
137
  uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)
@@ -182,16 +158,58 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
182
158
  return self.slug
183
159
 
184
160
  def get_coa_root_accounts_qs(self) -> AccountModelQuerySet:
161
+ """
162
+ Retrieves the root accounts in the chart of accounts.
163
+
164
+ Returns:
165
+ AccountModelQuerySet: A queryset containing the root accounts in the chart of accounts.
166
+ """
185
167
  return self.accountmodel_set.all().is_coa_root()
186
168
 
187
- def get_coa_root_account(self) -> AccountModel:
169
+ def get_coa_root_node(self) -> AccountModel:
170
+ """
171
+ Retrieves the root node of the chart of accounts.
172
+
173
+ Returns:
174
+ AccountModel: The root node of the chart of accounts.
175
+
176
+ """
188
177
  qs = self.get_coa_root_accounts_qs()
189
178
  return qs.get(role__exact=ROOT_COA)
190
179
 
191
- def get_coa_l2_root(self,
192
- account_model: AccountModel,
193
- root_account_qs: Optional[AccountModelQuerySet] = None,
194
- as_queryset: bool = False) -> Union[AccountModelQuerySet, AccountModel]:
180
+ def get_account_root_node(self,
181
+ account_model: AccountModel,
182
+ root_account_qs: Optional[AccountModelQuerySet] = None,
183
+ as_queryset: bool = False) -> AccountModel:
184
+ """
185
+ Fetches the root node of the ChartOfAccountModel instance. The root node is the highest level of the CoA
186
+ hierarchy. It can be used to traverse the hierarchy of the CoA structure downstream.
187
+
188
+
189
+ Parameters
190
+ ----------
191
+ account_model : AccountModel
192
+ The account model for which to find the root node.
193
+ root_account_qs : Optional[AccountModelQuerySet], optional
194
+ The queryset of root accounts. If not provided, it will be retrieved using `get_coa_root_accounts_qs` method.
195
+ as_queryset : bool, optional
196
+ If True, return the root account queryset instead of a single root account. Default is False.
197
+
198
+ Returns
199
+ -------
200
+ Union[AccountModelQuerySet, AccountModel]
201
+ If `as_queryset` is True, returns the root account queryset. Otherwise, returns a single root account.
202
+
203
+ Raises
204
+ ------
205
+ ChartOfAccountsModelValidationError
206
+ If the account model is not part of the chart of accounts.
207
+ """
208
+
209
+ if account_model.coa_model_id != self.uuid:
210
+ raise ChartOfAccountsModelValidationError(
211
+ message=_(f'The account model {account_model} is not part of the chart of accounts {self.name}.'),
212
+ )
195
213
 
196
214
  if not account_model.is_root_account():
197
215
 
@@ -219,13 +237,72 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
219
237
  return qs.get()
220
238
 
221
239
  def get_non_root_coa_accounts_qs(self) -> AccountModelQuerySet:
240
+ """
241
+ Returns a query set of non-root accounts in the chart of accounts.
242
+
243
+ Returns
244
+ -------
245
+ AccountModelQuerySet
246
+ A query set of non-root accounts in the chart of accounts.
247
+ """
222
248
  return self.accountmodel_set.all().not_coa_root()
223
249
 
224
- def get_coa_account_tree(self):
225
- root_account = self.get_coa_root_account()
250
+ def get_coa_accounts(self, active_only: bool = True) -> AccountModelQuerySet:
251
+ """
252
+ Returns the AccountModelQuerySet associated with the ChartOfAccounts model instance.
253
+
254
+ Parameters
255
+ ----------
256
+ active_only : bool, optional
257
+ Flag to indicate whether to retrieve only active accounts or all accounts.
258
+ Default is True.
259
+
260
+ Returns
261
+ -------
262
+ AccountModelQuerySet
263
+ A queryset containing accounts from the chart of accounts.
264
+
265
+ """
266
+ qs = self.get_non_root_coa_accounts_qs()
267
+ if active_only:
268
+ return qs.active()
269
+ return qs
270
+
271
+ def get_coa_account_tree(self) -> Dict:
272
+ """
273
+ Performs a bulk dump of the ChartOfAccounts model instance accounts to a dictionary.
274
+ The method invokes the`dump_bulk` method on the ChartOfAccount model instance root node.
275
+ See Django Tree Beard documentation for more information.
276
+
277
+ Returns
278
+ -------
279
+ Dict
280
+ A dictionary containing all accounts from the chart of accounts in a nested structure.
281
+ """
282
+ root_account = self.get_coa_root_node()
226
283
  return AccountModel.dump_bulk(parent=root_account)
227
284
 
228
285
  def generate_slug(self, raise_exception: bool = False) -> str:
286
+ """
287
+ Generates and assigns a slug based on the ChartOfAccounts model instance EntityModel information.
288
+
289
+
290
+ Parameters
291
+ ----------
292
+ raise_exception : bool, optional
293
+ If set to True, it will raise a ChartOfAccountsModelValidationError if the `self.slug` is already set.
294
+
295
+ Returns
296
+ -------
297
+ str
298
+ The generated slug for the Chart of Accounts.
299
+
300
+ Raises
301
+ ------
302
+ ChartOfAccountsModelValidationError
303
+ If `raise_exception` is set to True and `self.slug` is already set.
304
+
305
+ """
229
306
  if self.slug:
230
307
  if raise_exception:
231
308
  raise ChartOfAccountsModelValidationError(
@@ -235,7 +312,17 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
235
312
  self.slug = f'coa-{self.entity.slug[-5:]}-' + ''.join(choices(SLUG_SUFFIX, k=15))
236
313
 
237
314
  def configure(self, raise_exception: bool = True):
315
+ """
316
+ A method that properly configures the ChartOfAccounts model and creates the appropriate hierarchy boilerplate
317
+ to support the insertion of new accounts into the chart of account model tree.
318
+ This method must be called every time the ChartOfAccounts model is created.
238
319
 
320
+ Parameters
321
+ ----------
322
+ raise_exception : bool, optional
323
+ Whether to raise an exception if root nodes already exist in the Chart of Accounts (default is True).
324
+ This indicates that the ChartOfAccountModel instance is already configured.
325
+ """
239
326
  self.generate_slug()
240
327
 
241
328
  root_accounts_qs = self.get_coa_root_accounts_qs()
@@ -284,12 +371,44 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
284
371
  ))
285
372
 
286
373
  def is_default(self) -> bool:
374
+ """
375
+ Check if the ChartOfAccountModel instance is set as the default for the EntityModel.
376
+
377
+ Returns
378
+ -------
379
+ bool
380
+ True if the ChartOfAccountModel instance is set as the default for the EntityModel. Else, False.
381
+ """
382
+ if not self.entity_id:
383
+ return False
384
+ if not self.entity.default_coa_id:
385
+ return False
287
386
  return self.entity.default_coa_id == self.uuid
288
387
 
289
388
  def is_active(self) -> bool:
389
+ """
390
+ Check if the ChartOfAccountModel instance is active.
391
+
392
+ Returns:
393
+ bool: True if the ChartOfAccountModel instance is active, False otherwise.
394
+ """
290
395
  return self.active is True
291
396
 
292
397
  def validate_account_model_qs(self, account_model_qs: AccountModelQuerySet):
398
+ """
399
+ Validates the given AccountModelQuerySet for the ChartOfAccountsModel.
400
+
401
+ Parameters
402
+ ----------
403
+ account_model_qs : AccountModelQuerySet
404
+ The AccountModelQuerySet to validate.
405
+
406
+ Raises
407
+ ------
408
+ ChartOfAccountsModelValidationError
409
+ If the account_model_qs is not an instance of AccountModelQuerySet or if it contains an account model with a different coa_model_id than the current CoA model.
410
+
411
+ """
293
412
  if not isinstance(account_model_qs, AccountModelQuerySet):
294
413
  raise ChartOfAccountsModelValidationError(
295
414
  message='Must pass an instance of AccountModelQuerySet'
@@ -300,24 +419,40 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
300
419
  message=f'Invalid root queryset for CoA {self.name}'
301
420
  )
302
421
 
303
- def allocate_account(self,
304
- account_model: AccountModel,
305
- root_account_qs: Optional[AccountModelQuerySet] = None):
422
+ def insert_account(self,
423
+ account_model: AccountModel,
424
+ root_account_qs: Optional[AccountModelQuerySet] = None):
306
425
  """
307
- Allocates a given account model to the appropriate root account depending on the Account Model Role.
426
+ This method inserts the given account model into the chart of accounts (COA) instance.
427
+ It first verifies if the account model's COA model ID matches the COA's UUID. If not, it
428
+ raises a `ChartOfAccountsModelValidationError`. If the `root_account_qs` is not provided, it retrieves the
429
+ root account query set using the `get_coa_root_accounts_qs` method. Providing a pre-fetched `root_account_qs`
430
+ avoids unnecessary retrieval of the root account query set every an account model is inserted into the CoA.
431
+
432
+ Next, it validates the provided `root_account_qs` if it is not None. Then, it obtains the root node for the
433
+ account model using the `get_account_root_node` method and assigns it to `account_root_node`.
434
+
435
+ Finally, it adds the account model as a child to the `account_root_node` and retrieves the updated COA accounts
436
+ query set using the `get_non_root_coa_accounts_qs` method. It returns the inserted account model found in the
437
+ COA accounts query set.
308
438
 
309
439
  Parameters
310
440
  ----------
311
- account_model: AccountModel
312
- The Account Model to Allocate
313
- root_account_qs:
314
- The Root Account QuerySet of the Chart Of Accounts to use.
315
- Will be validated against current CoA Model.
441
+ account_model : AccountModel
442
+ The account model to be inserted into the chart of accounts.
443
+ root_account_qs : Optional[AccountModelQuerySet], default=None
444
+ The root account query set. If not provided, it will be obtained using the `get_coa_root_accounts_qs`
445
+ method.
316
446
 
317
447
  Returns
318
448
  -------
319
449
  AccountModel
320
- The saved and allocated AccountModel.
450
+ The inserted account model.
451
+
452
+ Raises
453
+ ------
454
+ ChartOfAccountsModelValidationError
455
+ If the provided account model has an invalid COA model ID for the current COA.
321
456
  """
322
457
 
323
458
  if account_model.coa_model_id:
@@ -331,13 +466,12 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
331
466
  else:
332
467
  self.validate_account_model_qs(root_account_qs)
333
468
 
334
- l2_root_node: AccountModel = self.get_coa_l2_root(
469
+ account_root_node: AccountModel = self.get_account_root_node(
335
470
  account_model=account_model,
336
471
  root_account_qs=root_account_qs
337
472
  )
338
473
 
339
- account_model.coa_model = self
340
- l2_root_node.add_child(instance=account_model)
474
+ account_root_node.add_child(instance=account_model)
341
475
  coa_accounts_qs = self.get_non_root_coa_accounts_qs()
342
476
  return coa_accounts_qs.get(uuid__exact=account_model.uuid)
343
477
 
@@ -348,17 +482,41 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
348
482
  balance_type: str,
349
483
  active: bool,
350
484
  root_account_qs: Optional[AccountModelQuerySet] = None):
485
+ """
486
+ Proper method for inserting a new Account Model into a CoA.
487
+ Use this in liu of the direct instantiation of the AccountModel of using the django related manager.
351
488
 
489
+ Parameters
490
+ ----------
491
+ code : str
492
+ The code of the account to be created.
493
+ role : str
494
+ The role of the account. This can be a user-defined value.
495
+ name : str
496
+ The name of the account.
497
+ balance_type : str
498
+ The balance type of the account. This can be a user-defined value.
499
+ active : bool
500
+ Specifies whether the account is active or not.
501
+ root_account_qs : Optional[AccountModelQuerySet], optional
502
+ The query set of root accounts to which the created account should be linked. Defaults to None.
503
+
504
+ Returns
505
+ -------
506
+ AccountModel
507
+ The created account model instance.
508
+ """
352
509
  account_model = AccountModel(
353
510
  code=code,
354
511
  name=name,
355
512
  role=role,
356
513
  active=active,
357
- balance_type=balance_type
514
+ balance_type=balance_type,
515
+ coa_model=self
358
516
  )
359
517
  account_model.clean()
360
518
 
361
- account_model = self.allocate_account(
519
+ account_model = self.insert_account(
362
520
  account_model=account_model,
363
521
  root_account_qs=root_account_qs
364
522
  )
@@ -367,14 +525,14 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
367
525
  # ACTIONS -----
368
526
  # todo: use these methods once multi CoA features are enabled...
369
527
  def lock_all_accounts(self) -> AccountModelQuerySet:
370
- non_root_accounts_qs = self.get_non_root_coa_accounts_qs()
371
- non_root_accounts_qs.update(locked=True)
372
- return non_root_accounts_qs
528
+ account_qs = self.get_coa_accounts()
529
+ account_qs.update(locked=True)
530
+ return account_qs
373
531
 
374
532
  def unlock_all_accounts(self) -> AccountModelQuerySet:
375
- non_root_accounts_qs = self.get_non_root_coa_accounts_qs()
376
- non_root_accounts_qs.update(locked=False)
377
- return non_root_accounts_qs
533
+ account_qs = self.get_non_root_coa_accounts_qs()
534
+ account_qs.update(locked=False)
535
+ return account_qs
378
536
 
379
537
  def mark_as_default(self, commit: bool = False, raise_exception: bool = False, **kwargs):
380
538
  """
@@ -421,9 +579,23 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
421
579
  )
422
580
 
423
581
  def can_activate(self) -> bool:
582
+ """
583
+ Check if the ChartOffAccountModel instance can be activated.
584
+
585
+ Returns
586
+ -------
587
+ True if the object can be activated, False otherwise.
588
+ """
424
589
  return self.active is False
425
590
 
426
591
  def can_deactivate(self) -> bool:
592
+ """
593
+ Check if the ChartOffAccountModel instance can be deactivated.
594
+
595
+ Returns
596
+ -------
597
+ True if the object can be deactivated, False otherwise.
598
+ """
427
599
  return all([
428
600
  self.is_active(),
429
601
  not self.is_default()
@@ -517,6 +689,14 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
517
689
  }
518
690
  )
519
691
 
692
+ def get_coa_list_url(self):
693
+ return reverse(
694
+ viewname='django_ledger:coa-list',
695
+ kwargs={
696
+ 'entity_slug': self.entity.slug
697
+ }
698
+ )
699
+
520
700
  def get_absolute_url(self) -> str:
521
701
  return reverse(
522
702
  viewname='django_ledger:coa-detail',
@@ -546,7 +726,6 @@ class ChartOfAccountModelAbstract(SlugNameMixIn, CreateUpdateMixIn):
546
726
 
547
727
  def clean(self):
548
728
  self.generate_slug()
549
-
550
729
  if self.is_default() and not self.active:
551
730
  raise ChartOfAccountsModelValidationError(
552
731
  _('Default Chart of Accounts cannot be deactivated.')
@@ -1100,7 +1100,7 @@ class EntityModelAbstract(MP_Node,
1100
1100
  role=a['role'],
1101
1101
  balance_type=a['balance_type'],
1102
1102
  active=activate_accounts,
1103
- # coa_model=chart_of_accounts,
1103
+ coa_model=coa_model,
1104
1104
  ) for a in v] for k, v in CHART_OF_ACCOUNTS_ROOT_MAP.items()
1105
1105
  }
1106
1106
 
@@ -1115,7 +1115,7 @@ class EntityModelAbstract(MP_Node,
1115
1115
  pass
1116
1116
 
1117
1117
  account_model.clean()
1118
- coa_model.allocate_account(account_model, root_account_qs=root_account_qs)
1118
+ coa_model.insert_account(account_model, root_account_qs=root_account_qs)
1119
1119
 
1120
1120
  else:
1121
1121
  if not ignore_if_default_coa:
@@ -1124,6 +1124,25 @@ class EntityModelAbstract(MP_Node,
1124
1124
  'Use force=True to bypass this check'
1125
1125
  )
1126
1126
 
1127
+ def get_coa_model_qs(self, active: bool = True):
1128
+ """
1129
+ Fetches the current Entity Model instance Chart of Accounts Model Queryset.
1130
+
1131
+ Parameters
1132
+ ----------
1133
+ active: bool
1134
+ Returns only active Chart of Account Models. Defaults to True.
1135
+
1136
+ Returns
1137
+ -------
1138
+ ChartOfAccountModelQuerySet
1139
+ """
1140
+
1141
+ coa_model_qs = self.chartofaccountmodel_set.all()
1142
+ if active:
1143
+ return coa_model_qs.active()
1144
+ return coa_model_qs
1145
+
1127
1146
  # Model Validators....
1128
1147
  def validate_chart_of_accounts_for_entity(self,
1129
1148
  coa_model: ChartOfAccountModel,
@@ -743,7 +743,7 @@ class AccrualMixIn(models.Model):
743
743
 
744
744
  if commit:
745
745
  JournalEntryModel = lazy_loader.get_journal_entry_model()
746
- TransactionModel = lazy_loader.get_transaction_model()
746
+ TransactionModel = lazy_loader.get_txs_model()
747
747
 
748
748
  unit_uuids = list(set(k[1] for k in idx_keys))
749
749