django-ledger 0.6.3__py3-none-any.whl → 0.7.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.

Files changed (115) hide show
  1. django_ledger/__init__.py +1 -4
  2. django_ledger/admin/coa.py +1 -2
  3. django_ledger/contrib/django_ledger_graphene/accounts/schema.py +1 -1
  4. django_ledger/forms/account.py +60 -44
  5. django_ledger/forms/bank_account.py +3 -2
  6. django_ledger/forms/bill.py +24 -36
  7. django_ledger/forms/customer.py +1 -1
  8. django_ledger/forms/data_import.py +3 -3
  9. django_ledger/forms/estimate.py +1 -1
  10. django_ledger/forms/invoice.py +5 -7
  11. django_ledger/forms/item.py +24 -15
  12. django_ledger/forms/transactions.py +3 -3
  13. django_ledger/io/io_core.py +4 -2
  14. django_ledger/io/io_middleware.py +5 -0
  15. django_ledger/io/roles.py +6 -0
  16. django_ledger/migrations/0017_alter_accountmodel_unique_together_and_more.py +31 -0
  17. django_ledger/models/accounts.py +629 -342
  18. django_ledger/models/bank_account.py +0 -4
  19. django_ledger/models/bill.py +0 -3
  20. django_ledger/models/closing_entry.py +0 -3
  21. django_ledger/models/coa.py +59 -48
  22. django_ledger/models/coa_default.py +9 -8
  23. django_ledger/models/customer.py +0 -4
  24. django_ledger/models/data_import.py +0 -3
  25. django_ledger/models/entity.py +80 -40
  26. django_ledger/models/estimate.py +0 -9
  27. django_ledger/models/invoice.py +0 -3
  28. django_ledger/models/items.py +4 -6
  29. django_ledger/models/journal_entry.py +2 -5
  30. django_ledger/models/ledger.py +0 -3
  31. django_ledger/models/mixins.py +0 -3
  32. django_ledger/models/purchase_order.py +0 -4
  33. django_ledger/models/signals.py +0 -3
  34. django_ledger/models/transactions.py +2 -5
  35. django_ledger/models/unit.py +0 -3
  36. django_ledger/models/utils.py +0 -3
  37. django_ledger/models/vendor.py +0 -3
  38. django_ledger/report/cash_flow_statement.py +1 -1
  39. django_ledger/static/.DS_Store +0 -0
  40. django_ledger/static/django_ledger/.DS_Store +0 -0
  41. django_ledger/static/django_ledger/logo_2/.DS_Store +0 -0
  42. django_ledger/static/django_ledger/logo_2/django_ledger_logo_dark.png +0 -0
  43. django_ledger/static/django_ledger/logo_2/django_ledger_logo_dark@0.5x.png +0 -0
  44. django_ledger/static/django_ledger/logo_2/django_ledger_logo_dark@2x.png +0 -0
  45. django_ledger/static/django_ledger/logo_2/django_ledger_logo_dark@3x.png +0 -0
  46. django_ledger/static/django_ledger/logo_2/djl-full-vert.png +0 -0
  47. django_ledger/static/django_ledger/logo_2/djl-full-vert@0.5x.png +0 -0
  48. django_ledger/static/django_ledger/logo_2/djl-full-vert@2x.png +0 -0
  49. django_ledger/static/django_ledger/logo_2/djl-full-vert@3x.png +0 -0
  50. django_ledger/static/django_ledger/logo_2/djl-logo-full-horiz.png +0 -0
  51. django_ledger/static/django_ledger/logo_2/djl-logo-full-horiz@0.5x.png +0 -0
  52. django_ledger/static/django_ledger/logo_2/djl-logo-full-horiz@2x.png +0 -0
  53. django_ledger/static/django_ledger/logo_2/djl-logo-full-horiz@3x.png +0 -0
  54. django_ledger/static/django_ledger/logo_2/djl-logo-full-vert.png +0 -0
  55. django_ledger/static/django_ledger/logo_2/djl-logo-full-vert@0.5x.png +0 -0
  56. django_ledger/static/django_ledger/logo_2/djl-logo-full-vert@2x.png +0 -0
  57. django_ledger/static/django_ledger/logo_2/djl-logo-full-vert@3x.png +0 -0
  58. django_ledger/static/django_ledger/logo_2/djl-logo.png +0 -0
  59. django_ledger/static/django_ledger/logo_2/djl-logo@0.5x.png +0 -0
  60. django_ledger/static/django_ledger/logo_2/djl-logo@2x.png +0 -0
  61. django_ledger/static/django_ledger/logo_2/djl-logo@3x.png +0 -0
  62. django_ledger/static/django_ledger/logo_2/djl-txt-full-horiz.png +0 -0
  63. django_ledger/static/django_ledger/logo_2/djl-txt-full-horiz@0.5x.png +0 -0
  64. django_ledger/static/django_ledger/logo_2/djl-txt-full-horiz@2x.png +0 -0
  65. django_ledger/static/django_ledger/logo_2/djl-txt-full-horiz@3x.png +0 -0
  66. django_ledger/static/django_ledger/logo_2/djl-txt-full-vert.png +0 -0
  67. django_ledger/static/django_ledger/logo_2/djl-txt-full-vert@0.5x.png +0 -0
  68. django_ledger/static/django_ledger/logo_2/djl-txt-full-vert@2x.png +0 -0
  69. django_ledger/static/django_ledger/logo_2/djl-txt-full-vert@3x.png +0 -0
  70. django_ledger/static/django_ledger/logo_2/djl-txt-horiz.png +0 -0
  71. django_ledger/static/django_ledger/logo_2/djl-txt-horiz@0.5x.png +0 -0
  72. django_ledger/static/django_ledger/logo_2/djl-txt-horiz@2x.png +0 -0
  73. django_ledger/static/django_ledger/logo_2/djl-txt-horiz@3x.png +0 -0
  74. django_ledger/templates/django_ledger/account/account_create.html +2 -2
  75. django_ledger/templates/django_ledger/account/account_update.html +1 -1
  76. django_ledger/templates/django_ledger/account/tags/account_txs_table.html +1 -0
  77. django_ledger/templates/django_ledger/account/tags/accounts_table.html +27 -18
  78. django_ledger/templates/django_ledger/bills/bill_detail.html +3 -3
  79. django_ledger/templates/django_ledger/expense/tags/expense_item_table.html +7 -0
  80. django_ledger/templates/django_ledger/invoice/invoice_detail.html +3 -3
  81. django_ledger/templatetags/django_ledger.py +7 -1
  82. django_ledger/tests/base.py +23 -7
  83. django_ledger/tests/test_accounts.py +145 -9
  84. django_ledger/urls/account.py +17 -24
  85. django_ledger/utils.py +8 -0
  86. django_ledger/views/__init__.py +1 -1
  87. django_ledger/views/account.py +80 -118
  88. django_ledger/views/auth.py +1 -1
  89. django_ledger/views/bank_account.py +9 -11
  90. django_ledger/views/bill.py +91 -80
  91. django_ledger/views/closing_entry.py +8 -0
  92. django_ledger/views/coa.py +2 -1
  93. django_ledger/views/customer.py +1 -1
  94. django_ledger/views/data_import.py +1 -1
  95. django_ledger/views/entity.py +1 -1
  96. django_ledger/views/estimate.py +13 -8
  97. django_ledger/views/feedback.py +1 -1
  98. django_ledger/views/financial_statement.py +1 -1
  99. django_ledger/views/home.py +1 -1
  100. django_ledger/views/inventory.py +9 -0
  101. django_ledger/views/invoice.py +5 -2
  102. django_ledger/views/item.py +58 -68
  103. django_ledger/views/journal_entry.py +1 -1
  104. django_ledger/views/ledger.py +3 -1
  105. django_ledger/views/mixins.py +9 -8
  106. django_ledger/views/purchase_order.py +1 -1
  107. django_ledger/views/transactions.py +1 -1
  108. django_ledger/views/unit.py +9 -0
  109. django_ledger/views/vendor.py +1 -1
  110. {django_ledger-0.6.3.dist-info → django_ledger-0.7.0.dist-info}/AUTHORS.md +8 -2
  111. {django_ledger-0.6.3.dist-info → django_ledger-0.7.0.dist-info}/METADATA +34 -43
  112. {django_ledger-0.6.3.dist-info → django_ledger-0.7.0.dist-info}/RECORD +115 -79
  113. {django_ledger-0.6.3.dist-info → django_ledger-0.7.0.dist-info}/WHEEL +1 -1
  114. {django_ledger-0.6.3.dist-info → django_ledger-0.7.0.dist-info}/top_level.txt +0 -1
  115. {django_ledger-0.6.3.dist-info → django_ledger-0.7.0.dist-info}/LICENSE +0 -0
django_ledger/__init__.py CHANGED
@@ -1,15 +1,12 @@
1
1
  """
2
2
  Django Ledger created by Miguel Sanda <msanda@arrobalytics.com>.
3
3
  Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
4
-
5
- Contributions to this module:
6
- * Miguel Sanda <msanda@arrobalytics.com>
7
4
  """
8
5
 
9
6
  default_app_config = 'django_ledger.apps.DjangoLedgerConfig'
10
7
 
11
8
  """Django Ledger"""
12
- __version__ = '0.6.3'
9
+ __version__ = '0.7.0'
13
10
  __license__ = 'GPLv3 License'
14
11
 
15
12
  __author__ = 'Miguel Sanda'
@@ -108,7 +108,6 @@ class ChartOfAccountsModelAdmin(ModelAdmin):
108
108
  list_display_links = ['name']
109
109
  fields = [
110
110
  'name',
111
- 'locked',
112
111
  'description',
113
112
  ]
114
113
  inlines = [
@@ -132,4 +131,4 @@ class ChartOfAccountsModelAdmin(ModelAdmin):
132
131
  def account_model_count(self, obj):
133
132
  return obj.accountmodel__count
134
133
 
135
- account_model_count.short_description = 'Accounts'
134
+ account_model_count.short_description = 'Accounts'
@@ -25,7 +25,7 @@ class Accountlist_Query(graphene.ObjectType):
25
25
  def resolve_all_accounts(self, info, slug_name, **kwargs):
26
26
  if info.context.user.is_authenticated:
27
27
  return AccountModel.objects.for_entity(
28
- entity_slug=slug_name,
28
+ entity_model=slug_name,
29
29
  user_model=info.context.user,
30
30
  ).select_related('parent').order_by('code')
31
31
  else:
@@ -1,43 +1,49 @@
1
+ """
2
+ Django Ledger created by Miguel Sanda <msanda@arrobalytics.com>.
3
+ Copyright© EDMA Group Inc licensed under the GPLv3 Agreement.
4
+
5
+ Contributions to this module:
6
+ * Miguel Sanda <msanda@arrobalytics.com>
7
+ """
8
+ from random import randint
1
9
  from typing import Optional
2
10
 
3
- from django.forms import TextInput, Select, ModelForm, ChoiceField, ValidationError, CheckboxInput
11
+ from django.forms import TextInput, Select, ModelForm, ChoiceField, ValidationError, CheckboxInput, HiddenInput
4
12
  from django.utils.translation import gettext_lazy as _
5
13
  from treebeard.forms import MoveNodeForm
6
14
 
7
15
  from django_ledger.io import ACCOUNT_CHOICES_NO_ROOT
16
+ from django_ledger.models import ChartOfAccountModel, EntityModel
8
17
  from django_ledger.models.accounts import AccountModel
9
18
  from django_ledger.settings import DJANGO_LEDGER_FORM_INPUT_CLASSES
10
19
 
11
- """
12
- The account Model has the below forms: All these form have Account Model as their base.
13
-
14
- CreateForm
15
- CreateChildForm
16
- Update Form
17
- """
18
-
19
20
 
20
21
  class AccountModelCreateForm(ModelForm):
21
22
  """
22
- Create Form:
23
- This Form is used for creation of a new account that does not exist in the default Chart of Accounts. It has some external as well as some internal field.
24
- The entity slug and the user model are the field which are internal and are predetermined in the lass itself
25
-
26
- Remaining fields which needs to be defined by user are :
27
-
28
- code: The code will be used to uniquely identify the particular account
29
- name: The name of the account. The name of the account should be resemblance of the nature of the transactions that will be in the account
30
- role: The role needs to be selected rom list of the options available. Choices are given under ACCOUNT ROLES. Refer the account model documentation for more info
31
- balance_type: Need to be selected from drop down as "Debit" or Credit"
23
+ A form for creating and managing account models within the system.
24
+
25
+ Attributes
26
+ ----------
27
+ ENTITY_MODEL : Model
28
+ The entity model being used in the form.
29
+ COA_MODEL : Model
30
+ The Chart of Account Model being used in the form.
32
31
  """
33
32
 
34
- def __init__(self, entity_model, coa_model, user_model, *args, **kwargs):
35
- self.ENTITY = entity_model
36
- self.COA_MODEL = coa_model
37
- self.USER_MODEL = user_model
33
+ FORM_ID_SEP = '___'
34
+
35
+ def __init__(self, coa_model: ChartOfAccountModel, *args, **kwargs):
36
+ self.COA_MODEL: ChartOfAccountModel = coa_model
38
37
  super().__init__(*args, **kwargs)
39
38
  self.fields['role'].choices = ACCOUNT_CHOICES_NO_ROOT
40
39
  self.fields['code'].required = False
40
+ self.fields['coa_model'].disabled = True
41
+ self.fields['coa_model'].required = False
42
+
43
+ self.form_id: str = self.get_form_id()
44
+
45
+ def get_form_id(self) -> str:
46
+ return f'account-model-create-form-{self.COA_MODEL.slug}{self.FORM_ID_SEP}{randint(100000, 999999)}'
41
47
 
42
48
  def clean_role_default(self):
43
49
  role_default = self.cleaned_data['role_default']
@@ -45,12 +51,8 @@ class AccountModelCreateForm(ModelForm):
45
51
  return None
46
52
  return role_default
47
53
 
48
- def clean_code(self):
49
- code = self.cleaned_data['code']
50
- is_code_valid = not self.COA_MODEL.accountmodel_set.filter(code=code).exists()
51
- if not is_code_valid:
52
- raise ValidationError(message=_('Code {} already exists for CoA {}').format(code, self.COA_MODEL.slug))
53
- return code
54
+ def clean_coa_model(self):
55
+ return self.COA_MODEL
54
56
 
55
57
  class Meta:
56
58
  model = AccountModel
@@ -61,7 +63,8 @@ class AccountModelCreateForm(ModelForm):
61
63
  'role_default',
62
64
  'balance_type',
63
65
  'active',
64
- 'active'
66
+ 'active',
67
+ 'coa_model'
65
68
  ]
66
69
  widgets = {
67
70
  'code': TextInput(attrs={
@@ -79,33 +82,41 @@ class AccountModelCreateForm(ModelForm):
79
82
  'balance_type': Select(attrs={
80
83
  'class': DJANGO_LEDGER_FORM_INPUT_CLASSES
81
84
  }),
85
+ 'coa_model': HiddenInput()
82
86
  }
83
87
 
84
88
 
85
89
  class AccountModelUpdateForm(MoveNodeForm):
86
90
  """
87
- Update Account Form:
88
- This form is for updating the account. This works for both the parent or the child Account .
89
- We can update the Parent , or The Code or even the Name of the Account.
91
+ AccountModelUpdateForm
92
+
93
+ A form for updating account model, inheriting from MoveNodeForm.
94
+
95
+ Attributes
96
+ ----------
97
+ _position : ChoiceField
98
+ A choice field for selecting the position.
99
+ _ref_node_id : ChoiceField
100
+ An optional choice field for selecting the relative node.
90
101
  """
91
102
 
92
103
  _position = ChoiceField(required=True,
93
- label=_("Position"),
104
+ label=_('Position'),
94
105
  widget=Select(attrs={
95
106
  'class': DJANGO_LEDGER_FORM_INPUT_CLASSES
96
107
  }))
97
108
  _ref_node_id = ChoiceField(required=False,
98
- label=_("Relative to"),
109
+ label=_('Relative to'),
99
110
  widget=Select(attrs={
100
111
  'class': DJANGO_LEDGER_FORM_INPUT_CLASSES
101
112
  }))
102
113
 
103
- def __init__(self, entity_model, coa_model, user_model, *args, **kwargs):
104
- self.ENTITY = entity_model
105
- self.COA_MODEL = coa_model
106
- self.USER_MODEL = user_model
114
+
115
+ def __init__(self, *args, **kwargs):
107
116
  super().__init__(*args, **kwargs)
108
- # self.fields['_ref_node_id'].choices = self.mk_dropdown_tree_choices()
117
+ self.fields['role'].disabled = True
118
+ self.fields['coa_model'].disabled = True
119
+
109
120
 
110
121
  @classmethod
111
122
  def mk_dropdown_tree(cls, model, for_node: Optional[AccountModel] = None):
@@ -114,15 +125,18 @@ class AccountModelUpdateForm(MoveNodeForm):
114
125
  if not for_node:
115
126
  raise ValidationError(message='Must provide for_node argument.')
116
127
 
117
- options = list()
118
128
  qs = for_node.get_account_move_choice_queryset()
119
129
 
120
- # for node in qs:
121
- # cls.add_subtree(for_node, node, options)
122
130
  return [
123
131
  (i.uuid, f'{"-" * (i.depth - 1)} {i}') for i in qs
124
132
  ]
125
133
 
134
+ def clean_role(self):
135
+ return self.instance.role
136
+
137
+ def coa_model(self):
138
+ return self.instance.coa_model
139
+
126
140
  def clean_role_default(self):
127
141
  role_default = self.cleaned_data['role_default']
128
142
  if not role_default:
@@ -131,8 +145,10 @@ class AccountModelUpdateForm(MoveNodeForm):
131
145
 
132
146
  class Meta:
133
147
  model = AccountModel
134
- exclude = ('depth', 'numchild', 'path', 'balance_type', 'role')
148
+ exclude = ('depth', 'numchild', 'path', 'balance_type')
135
149
  widgets = {
150
+ 'role': HiddenInput(),
151
+ 'coa_model': HiddenInput(),
136
152
  'parent': Select(attrs={
137
153
  'class': DJANGO_LEDGER_FORM_INPUT_CLASSES
138
154
  }),
@@ -13,9 +13,10 @@ class BankAccountCreateForm(ModelForm):
13
13
  super().__init__(*args, **kwargs)
14
14
  self.ENTITY_SLUG = entity_slug
15
15
  self.USER_MODEL = user_model
16
- account_qs = AccountModel.objects.for_entity_available(
16
+ account_qs = AccountModel.objects.for_entity(
17
17
  user_model=self.USER_MODEL,
18
- entity_slug=self.ENTITY_SLUG).filter(
18
+ entity_model=self.ENTITY_SLUG
19
+ ).available().filter(
19
20
  role__exact=ASSET_CA_CASH)
20
21
  self.fields['cash_account'].queryset = account_qs
21
22
 
@@ -6,25 +6,20 @@ from django.utils.translation import gettext_lazy as _
6
6
 
7
7
  from django_ledger.io.roles import ASSET_CA_CASH, ASSET_CA_PREPAID, LIABILITY_CL_ACC_PAYABLE
8
8
  from django_ledger.models import (ItemModel, AccountModel, BillModel, ItemTransactionModel,
9
- VendorModel, EntityUnitModel)
9
+ VendorModel, EntityUnitModel, EntityModel)
10
10
  from django_ledger.settings import DJANGO_LEDGER_FORM_INPUT_CLASSES
11
11
 
12
12
 
13
13
  class BillModelCreateForm(ModelForm):
14
- def __init__(self, *args, entity_slug, user_model, **kwargs):
14
+ def __init__(self, *args, entity_model: EntityModel, **kwargs):
15
15
  super().__init__(*args, **kwargs)
16
- self.ENTITY_SLUG = entity_slug
17
- self.USER_MODEL = user_model
18
- self.BILL_MODEL: BillModel = self.instance
16
+ self.ENTITY_MODEL = entity_model
19
17
  self.get_vendor_queryset()
20
18
  self.get_accounts_queryset()
21
19
 
22
20
  def get_vendor_queryset(self):
23
21
  if 'vendor' in self.fields:
24
- vendor_qs = VendorModel.objects.for_entity(
25
- user_model=self.USER_MODEL,
26
- entity_slug=self.ENTITY_SLUG
27
- )
22
+ vendor_qs = self.ENTITY_MODEL.vendormodel_set.active()
28
23
  self.fields['vendor'].queryset = vendor_qs
29
24
 
30
25
  def get_accounts_queryset(self):
@@ -34,14 +29,7 @@ class BillModelCreateForm(ModelForm):
34
29
  'prepaid_account' in self.fields,
35
30
  'unearned_account' in self.fields,
36
31
  ]):
37
- account_qs = AccountModel.objects.for_bill(
38
- user_model=self.USER_MODEL,
39
- entity_slug=self.ENTITY_SLUG
40
- )
41
-
42
- # forcing evaluation of qs to cache results for fields... (avoids multiple database queries)
43
- len(account_qs)
44
-
32
+ account_qs = self.ENTITY_MODEL.default_coa.accountmodel_set.all().for_bill()
45
33
  self.fields['cash_account'].queryset = account_qs.filter(role__exact=ASSET_CA_CASH)
46
34
  self.fields['prepaid_account'].queryset = account_qs.filter(role__exact=ASSET_CA_PREPAID)
47
35
  self.fields['unearned_account'].queryset = account_qs.filter(role__exact=LIABILITY_CL_ACC_PAYABLE)
@@ -107,11 +95,11 @@ class BaseBillModelUpdateForm(BillModelCreateForm):
107
95
 
108
96
  def __init__(self,
109
97
  *args,
110
- entity_slug,
98
+ entity_model,
111
99
  user_model,
112
100
  **kwargs):
113
- super().__init__(entity_slug=entity_slug, user_model=user_model, *args, **kwargs)
114
- self.ENTITY_SLUG = entity_slug
101
+ super().__init__(entity_model=entity_model, *args, **kwargs)
102
+ self.ENTITY_MODEL = entity_model
115
103
  self.USER_MODEL = user_model
116
104
  self.BILL_MODEL: BillModel = self.instance
117
105
 
@@ -120,7 +108,7 @@ class BaseBillModelUpdateForm(BillModelCreateForm):
120
108
  self.BILL_MODEL.update_state()
121
109
  self.instance.migrate_state(
122
110
  user_model=self.USER_MODEL,
123
- entity_slug=self.ENTITY_SLUG,
111
+ entity_slug=self.ENTITY_MODEL.slug,
124
112
  raise_exception=False
125
113
  )
126
114
  super().save(commit=commit)
@@ -226,6 +214,10 @@ class BillModelConfigureForm(BaseBillModelUpdateForm):
226
214
 
227
215
  class BillItemTransactionForm(ModelForm):
228
216
 
217
+ # def __init__(self, entity_unit_qs, *args, **kwargs):
218
+ # super().__init__(self, *args, **kwargs)
219
+ # self.fields['entity_unit'].queryset = entity_unit_qs
220
+
229
221
  def clean(self):
230
222
  cleaned_data = super(BillItemTransactionForm, self).clean()
231
223
  itemtxs_model: ItemTransactionModel = self.instance
@@ -262,28 +254,24 @@ class BillItemTransactionForm(ModelForm):
262
254
  class BaseBillItemTransactionFormset(BaseModelFormSet):
263
255
 
264
256
  def __init__(self, *args,
265
- entity_slug,
257
+ entity_model: EntityModel,
266
258
  bill_model: BillModel,
267
- user_model,
268
259
  **kwargs):
269
260
  super().__init__(*args, **kwargs)
270
- self.USER_MODEL = user_model
271
261
  self.BILL_MODEL = bill_model
272
- self.ENTITY_SLUG = entity_slug
273
-
274
- items_qs = ItemModel.objects.for_bill(
275
- entity_slug=self.ENTITY_SLUG,
276
- user_model=self.USER_MODEL
277
- )
262
+ self.ENTITY_MODEL = entity_model
263
+ self.queryset = self.BILL_MODEL.itemtransactionmodel_set.select_related(
264
+ 'item_model',
265
+ 'po_model',
266
+ 'bill_model'
267
+ ).order_by('-total_amount')
278
268
 
279
- unit_qs = EntityUnitModel.objects.for_entity(
280
- entity_slug=self.ENTITY_SLUG,
281
- user_model=self.USER_MODEL
282
- )
269
+ self.items_qs = self.ENTITY_MODEL.itemmodel_set.bills()
270
+ self.entity_unit_qs = self.ENTITY_MODEL.entityunitmodel_set.all()
283
271
 
284
272
  for form in self.forms:
285
- form.fields['item_model'].queryset = items_qs
286
- form.fields['entity_unit'].queryset = unit_qs
273
+ form.fields['item_model'].queryset = self.items_qs
274
+ form.fields['entity_unit'].queryset = self.entity_unit_qs
287
275
 
288
276
  if not self.BILL_MODEL.can_edit_items():
289
277
  form.fields['item_model'].disabled = True
@@ -37,7 +37,7 @@ class CustomerModelForm(ModelForm):
37
37
  'hidden'
38
38
  ]
39
39
  help_texts = {
40
- 'sales_tax_rate': _('Example: 3.50% should be entered as 0.0035')
40
+ 'sales_tax_rate': _('Example: 3.50% should be entered as 0.035')
41
41
  }
42
42
  widgets = {
43
43
  'customer_name': TextInput(attrs={
@@ -130,10 +130,10 @@ class BaseStagedTransactionModelFormSet(BaseModelFormSet):
130
130
  self.IMPORT_DISABLED = not exclude_account
131
131
  self.CASH_ACCOUNT = exclude_account
132
132
 
133
- account_model_qs = AccountModel.objects.for_entity_available(
133
+ account_model_qs = AccountModel.objects.for_entity(
134
134
  user_model=self.USER_MODEL,
135
- entity_slug=self.ENTITY_SLUG
136
- ).order_by('role', 'name')
135
+ entity_model=self.ENTITY_SLUG
136
+ ).available().order_by('role', 'name')
137
137
 
138
138
  unit_model_qs = EntityUnitModel.objects.for_entity(
139
139
  user_model=self.USER_MODEL,
@@ -27,7 +27,7 @@ class EstimateModelCreateForm(forms.ModelForm):
27
27
  return CustomerModel.objects.for_entity(
28
28
  entity_slug=self.ENTITY_SLUG,
29
29
  user_model=self.USER_MODEL
30
- )
30
+ ).active()
31
31
 
32
32
  class Meta:
33
33
  model = EstimateModel
@@ -31,7 +31,7 @@ class InvoiceModelCreateForEstimateForm(ModelForm):
31
31
  customer_qs = CustomerModel.objects.for_entity(
32
32
  user_model=self.USER_MODEL,
33
33
  entity_slug=self.ENTITY_SLUG
34
- )
34
+ ).active()
35
35
  self.fields['customer'].queryset = customer_qs
36
36
 
37
37
  def get_accounts_queryset(self):
@@ -41,13 +41,11 @@ class InvoiceModelCreateForEstimateForm(ModelForm):
41
41
  'prepaid_account' in self.fields,
42
42
  'unearned_account' in self.fields,
43
43
  ]):
44
- account_qs = AccountModel.objects.for_invoice(
45
- user_model=self.USER_MODEL,
46
- entity_slug=self.ENTITY_SLUG
47
- )
48
44
 
49
- # forcing evaluation of qs to cache results for fields... (avoids multiple database queries)
50
- len(account_qs)
45
+ account_qs = AccountModel.objects.for_entity(
46
+ user_model=self.USER_MODEL,
47
+ entity_model=self.ENTITY_SLUG
48
+ ).for_invoice()
51
49
 
52
50
  self.fields['cash_account'].queryset = account_qs.filter(role__exact=ASSET_CA_CASH)
53
51
  self.fields['prepaid_account'].queryset = account_qs.filter(role__exact=ASSET_CA_RECEIVABLES)
@@ -46,10 +46,12 @@ class ProductCreateForm(ModelForm):
46
46
  self.USER_MODEL = user_model
47
47
  super().__init__(*args, **kwargs)
48
48
 
49
- accounts_qs = AccountModel.objects.with_roles(
50
- roles=self.PRODUCT_OR_SERVICE_ROLES,
51
- entity_slug=self.ENTITY_SLUG,
52
- user_model=self.USER_MODEL).active()
49
+ accounts_qs = AccountModel.objects.for_entity(
50
+ entity_model=self.ENTITY_SLUG,
51
+ user_model=self.USER_MODEL
52
+ ).with_roles(
53
+ roles=self.PRODUCT_OR_SERVICE_ROLES
54
+ ).active()
53
55
 
54
56
  # caches the QS for filtering...
55
57
  len(accounts_qs)
@@ -139,10 +141,12 @@ class ServiceCreateForm(ModelForm):
139
141
  self.USER_MODEL = user_model
140
142
  super().__init__(*args, **kwargs)
141
143
 
142
- accounts_qs = AccountModel.objects.with_roles(
144
+ accounts_qs = AccountModel.objects.for_entity(
145
+ entity_model=self.ENTITY_SLUG,
146
+ user_model=self.USER_MODEL
147
+ ).with_roles(
143
148
  roles=self.SERVICE_ROLES,
144
- entity_slug=self.ENTITY_SLUG,
145
- user_model=self.USER_MODEL).active()
149
+ ).active()
146
150
 
147
151
  # caches the QS for filtering...
148
152
  len(accounts_qs)
@@ -224,10 +228,12 @@ class ExpenseItemCreateForm(ModelForm):
224
228
  self.USER_MODEL = user_model
225
229
  super().__init__(*args, **kwargs)
226
230
 
227
- accounts_qs = AccountModel.objects.with_roles(
228
- roles=GROUP_EXPENSES,
229
- entity_slug=self.ENTITY_SLUG,
230
- user_model=self.USER_MODEL).active()
231
+ accounts_qs = AccountModel.objects.for_entity(
232
+ entity_model=self.ENTITY_SLUG,
233
+ user_model=self.USER_MODEL
234
+ ).with_roles(
235
+ roles=GROUP_EXPENSES
236
+ ).active()
231
237
 
232
238
  self.fields['expense_account'].queryset = accounts_qs.filter(role__in=GROUP_EXPENSES)
233
239
 
@@ -295,6 +301,7 @@ class ExpenseItemUpdateForm(ExpenseItemCreateForm):
295
301
  'sku',
296
302
  'default_amount',
297
303
  'expense_account',
304
+ 'is_active'
298
305
  ]
299
306
 
300
307
 
@@ -306,10 +313,12 @@ class InventoryItemCreateForm(ModelForm):
306
313
  self.USER_MODEL = user_model
307
314
  super().__init__(*args, **kwargs)
308
315
 
309
- accounts_qs = AccountModel.objects.with_roles(
310
- roles=[ASSET_CA_INVENTORY],
311
- entity_slug=self.ENTITY_SLUG,
312
- user_model=self.USER_MODEL).active()
316
+ accounts_qs = AccountModel.objects.for_entity(
317
+ entity_model=self.ENTITY_SLUG,
318
+ user_model=self.USER_MODEL
319
+ ).with_roles(
320
+ roles=[ASSET_CA_INVENTORY]
321
+ ).active()
313
322
  self.fields['inventory_account'].queryset = accounts_qs
314
323
 
315
324
  if 'uom' in self.fields:
@@ -51,10 +51,10 @@ class TransactionModelFormSet(BaseModelFormSet):
51
51
  self.LEDGER_PK = ledger_pk
52
52
  self.ENTITY_SLUG = entity_slug
53
53
 
54
- account_qs = AccountModel.objects.for_entity_available(
54
+ account_qs = AccountModel.objects.for_entity(
55
55
  user_model=self.USER_MODEL,
56
- entity_slug=self.ENTITY_SLUG
57
- ).order_by('code')
56
+ entity_model=self.ENTITY_SLUG
57
+ ).available().order_by('code')
58
58
 
59
59
  for form in self.forms:
60
60
  form.fields['account'].queryset = account_qs
@@ -549,7 +549,7 @@ class IODatabaseMixIn:
549
549
  force_queryset_sorting: bool = False,
550
550
  **kwargs) -> IOResult:
551
551
  """
552
- Performs the appropriate transaction post-processing after DB aggregation..
552
+ Performs the appropriate transaction post-processing after DB aggregation.
553
553
 
554
554
 
555
555
  Parameters
@@ -757,7 +757,6 @@ class IODatabaseMixIn:
757
757
  by_unit=by_unit
758
758
  )
759
759
 
760
- # idea: change digest() name to something else? maybe aggregate, calculate?...
761
760
  io_state = roles_mgr.digest()
762
761
 
763
762
  if any([
@@ -938,6 +937,7 @@ class IOReportMixIn:
938
937
  balance_sheet_statement=True,
939
938
  txs_queryset=txs_queryset,
940
939
  as_io_digest=True,
940
+ signs=True,
941
941
  **kwargs
942
942
  )
943
943
 
@@ -991,6 +991,7 @@ class IOReportMixIn:
991
991
  income_statement=True,
992
992
  txs_queryset=txs_queryset,
993
993
  as_io_digest=True,
994
+ sings=True,
994
995
  **kwargs
995
996
  )
996
997
 
@@ -1046,6 +1047,7 @@ class IOReportMixIn:
1046
1047
  cash_flow_statement=True,
1047
1048
  txs_queryset=txs_queryset,
1048
1049
  as_io_digest=True,
1050
+ signs=True,
1049
1051
  **kwargs
1050
1052
  )
1051
1053
 
@@ -451,11 +451,16 @@ class CashFlowStatementIOMiddleware:
451
451
  bal for act, bal in self.IO_DATA[self.CFS_DIGEST_KEY]['net_cash_by_activity'].items()
452
452
  ])
453
453
 
454
+ def net_income(self):
455
+ group_balances = self.IO_DATA[AccountGroupIOMiddleware.GROUP_BALANCE_KEY]
456
+ self.IO_DATA[self.CFS_DIGEST_KEY]['net_income'] = group_balances['GROUP_CFS_NET_INCOME']
457
+
454
458
  def digest(self):
455
459
  self.IO_DATA[self.CFS_DIGEST_KEY] = dict()
456
460
  self.check_io_digest()
457
461
  self.operating()
458
462
  self.financing()
459
463
  self.investing()
464
+ self.net_income()
460
465
  self.net_cash()
461
466
  return self.IO_DATA
django_ledger/io/roles.py CHANGED
@@ -660,3 +660,9 @@ def validate_roles(roles: Union[str, List[str]], raise_exception: bool = True) -
660
660
  if raise_exception:
661
661
  raise InvalidRoleError('{rls}) is invalid. Choices are {ch}'.format(ch=', '.join(VALID_ROLES), rls=r))
662
662
  return set(roles)
663
+
664
+ VALID_PARENTS = {
665
+ ASSET_PPE_BUILDINGS_ACCUM_DEPRECIATION: [ASSET_PPE_BUILDINGS],
666
+ ASSET_PPE_EQUIPMENT_ACCUM_DEPRECIATION: [ASSET_PPE_EQUIPMENT],
667
+ ASSET_PPE_PLANT_ACCUM_DEPRECIATION: [ASSET_PPE_PLANT],
668
+ }
@@ -0,0 +1,31 @@
1
+ # Generated by Django 5.1.1 on 2024-10-09 19:40
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('django_ledger', '0016_remove_accountmodel_django_ledg_coa_mod_e19964_idx_and_more'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AlterUniqueTogether(
15
+ name='accountmodel',
16
+ unique_together=set(),
17
+ ),
18
+ migrations.AlterField(
19
+ model_name='accountmodel',
20
+ name='coa_model',
21
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='django_ledger.chartofaccountmodel', verbose_name='Chart of Accounts'),
22
+ ),
23
+ migrations.AddConstraint(
24
+ model_name='accountmodel',
25
+ constraint=models.UniqueConstraint(fields=('coa_model', 'code'), name='unique_code_for_coa_model', violation_error_message='Account codes must be unique for each Chart of Accounts Model.'),
26
+ ),
27
+ migrations.AddConstraint(
28
+ model_name='accountmodel',
29
+ constraint=models.UniqueConstraint(fields=('coa_model', 'role', 'role_default'), name='only_one_account_assigned_as_default_for_role', violation_error_message='Only one default account for role permitted.'),
30
+ ),
31
+ ]