NEMO-billing 4.0.1__tar.gz

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.
Files changed (172) hide show
  1. nemo_billing-4.0.1/LICENSE +21 -0
  2. nemo_billing-4.0.1/NEMO_billing/__init__.py +0 -0
  3. nemo_billing-4.0.1/NEMO_billing/admin.py +404 -0
  4. nemo_billing-4.0.1/NEMO_billing/api.py +94 -0
  5. nemo_billing-4.0.1/NEMO_billing/app_settings.py +10 -0
  6. nemo_billing-4.0.1/NEMO_billing/apps.py +20 -0
  7. nemo_billing-4.0.1/NEMO_billing/cap_discount/__init__.py +0 -0
  8. nemo_billing-4.0.1/NEMO_billing/cap_discount/admin.py +145 -0
  9. nemo_billing-4.0.1/NEMO_billing/cap_discount/app_settings.py +1 -0
  10. nemo_billing-4.0.1/NEMO_billing/cap_discount/apps.py +15 -0
  11. nemo_billing-4.0.1/NEMO_billing/cap_discount/customization.py +61 -0
  12. nemo_billing-4.0.1/NEMO_billing/cap_discount/exceptions.py +21 -0
  13. nemo_billing-4.0.1/NEMO_billing/cap_discount/migrations/0001_initial.py +345 -0
  14. nemo_billing-4.0.1/NEMO_billing/cap_discount/migrations/0002_version_2_0_0.py +54 -0
  15. nemo_billing-4.0.1/NEMO_billing/cap_discount/migrations/0003_multi_tier_discount.py +161 -0
  16. nemo_billing-4.0.1/NEMO_billing/cap_discount/migrations/0004_shortened_unique_constraints.py +61 -0
  17. nemo_billing-4.0.1/NEMO_billing/cap_discount/migrations/0005_cap_multi_core_facilities.py +97 -0
  18. nemo_billing-4.0.1/NEMO_billing/cap_discount/migrations/__init__.py +0 -0
  19. nemo_billing-4.0.1/NEMO_billing/cap_discount/models.py +446 -0
  20. nemo_billing-4.0.1/NEMO_billing/cap_discount/processors.py +426 -0
  21. nemo_billing-4.0.1/NEMO_billing/cap_discount/templates/cap_discount/cap_discount_status.html +58 -0
  22. nemo_billing-4.0.1/NEMO_billing/cap_discount/templates/cap_discount/usage_cap_discounts.html +28 -0
  23. nemo_billing-4.0.1/NEMO_billing/cap_discount/templates/customizations/customizations_cap_discount.html +96 -0
  24. nemo_billing-4.0.1/NEMO_billing/cap_discount/urls.py +19 -0
  25. nemo_billing-4.0.1/NEMO_billing/cap_discount/views.py +98 -0
  26. nemo_billing-4.0.1/NEMO_billing/exceptions.py +33 -0
  27. nemo_billing-4.0.1/NEMO_billing/invoices/__init__.py +0 -0
  28. nemo_billing-4.0.1/NEMO_billing/invoices/admin.py +328 -0
  29. nemo_billing-4.0.1/NEMO_billing/invoices/api.py +234 -0
  30. nemo_billing-4.0.1/NEMO_billing/invoices/app_settings.py +5 -0
  31. nemo_billing-4.0.1/NEMO_billing/invoices/apps.py +15 -0
  32. nemo_billing-4.0.1/NEMO_billing/invoices/customization.py +100 -0
  33. nemo_billing-4.0.1/NEMO_billing/invoices/exceptions.py +67 -0
  34. nemo_billing-4.0.1/NEMO_billing/invoices/management/__init__.py +0 -0
  35. nemo_billing-4.0.1/NEMO_billing/invoices/management/commands/__init__.py +0 -0
  36. nemo_billing-4.0.1/NEMO_billing/invoices/management/commands/deactivate_expired_projects.py +10 -0
  37. nemo_billing-4.0.1/NEMO_billing/invoices/management/commands/send_invoice_payment_reminder.py +10 -0
  38. nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0001_initial.py +305 -0
  39. nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0002_version_1_2_0.py +95 -0
  40. nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0003_version_1_4_0.py +37 -0
  41. nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0004_version_1_6_0.py +18 -0
  42. nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0005_version_1_6_1.py +19 -0
  43. nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0006_version_1_7_1.py +25 -0
  44. nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0007_version_1_8_0.py +18 -0
  45. nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0008_version_1_10_0.py +18 -0
  46. nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0009_version_1_23_0.py +36 -0
  47. nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0010_version_2_0_0.py +18 -0
  48. nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0011_version_2_4_0.py +50 -0
  49. nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0012_invoiceconfiguration_invoice_title.py +22 -0
  50. nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0013_alter_invoiceconfiguration_name_and_more.py +38 -0
  51. nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0014_invoicedetailitem_waived.py +18 -0
  52. nemo_billing-4.0.1/NEMO_billing/invoices/migrations/__init__.py +0 -0
  53. nemo_billing-4.0.1/NEMO_billing/invoices/models.py +598 -0
  54. nemo_billing-4.0.1/NEMO_billing/invoices/pdf_utilities.py +310 -0
  55. nemo_billing-4.0.1/NEMO_billing/invoices/processors.py +752 -0
  56. nemo_billing-4.0.1/NEMO_billing/invoices/renderers.py +595 -0
  57. nemo_billing-4.0.1/NEMO_billing/invoices/static/invoices/invoices.css +50 -0
  58. nemo_billing-4.0.1/NEMO_billing/invoices/templates/customizations/customizations_billing.html +109 -0
  59. nemo_billing-4.0.1/NEMO_billing/invoices/templates/customizations/customizations_invoices.html +178 -0
  60. nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/base.html +6 -0
  61. nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/email/billing_project_expiration_reminder_email_message.html +53 -0
  62. nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/email/billing_project_expiration_reminder_email_subject.txt +5 -0
  63. nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/email/email_send_invoice_message.html +36 -0
  64. nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/email/email_send_invoice_reminder_message.html +36 -0
  65. nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/email/email_send_invoice_reminder_subject.txt +1 -0
  66. nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/email/email_send_invoice_subject.txt +1 -0
  67. nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/invoice.html +351 -0
  68. nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/invoice_details.html +93 -0
  69. nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/invoices.html +367 -0
  70. nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/project/edit_project.html +401 -0
  71. nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/project/view_project_additional_info.html +98 -0
  72. nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/usage.html +404 -0
  73. nemo_billing-4.0.1/NEMO_billing/invoices/urls.py +75 -0
  74. nemo_billing-4.0.1/NEMO_billing/invoices/utilities.py +107 -0
  75. nemo_billing-4.0.1/NEMO_billing/invoices/views/__init__.py +0 -0
  76. nemo_billing-4.0.1/NEMO_billing/invoices/views/invoices.py +530 -0
  77. nemo_billing-4.0.1/NEMO_billing/invoices/views/project.py +148 -0
  78. nemo_billing-4.0.1/NEMO_billing/invoices/views/usage.py +353 -0
  79. nemo_billing-4.0.1/NEMO_billing/migrations/0001_initial.py +126 -0
  80. nemo_billing-4.0.1/NEMO_billing/migrations/0002_version_1_2_0.py +26 -0
  81. nemo_billing-4.0.1/NEMO_billing/migrations/0003_version_2_0_0.py +18 -0
  82. nemo_billing-4.0.1/NEMO_billing/migrations/0004_version_2_4_0.py +329 -0
  83. nemo_billing-4.0.1/NEMO_billing/migrations/0005_version_2_5_6.py +17 -0
  84. nemo_billing-4.0.1/NEMO_billing/migrations/0006_projectbillinghardcap.py +56 -0
  85. nemo_billing-4.0.1/NEMO_billing/migrations/0007_alter_projectbillinghardcap_end_date_and_more.py +23 -0
  86. nemo_billing-4.0.1/NEMO_billing/migrations/0008_customcharge_validated_customcharge_validated_by_and_more.py +68 -0
  87. nemo_billing-4.0.1/NEMO_billing/migrations/__init__.py +0 -0
  88. nemo_billing-4.0.1/NEMO_billing/models.py +478 -0
  89. nemo_billing-4.0.1/NEMO_billing/policy.py +145 -0
  90. nemo_billing-4.0.1/NEMO_billing/prepayments/__init__.py +0 -0
  91. nemo_billing-4.0.1/NEMO_billing/prepayments/admin.py +81 -0
  92. nemo_billing-4.0.1/NEMO_billing/prepayments/api.py +57 -0
  93. nemo_billing-4.0.1/NEMO_billing/prepayments/app_settings.py +0 -0
  94. nemo_billing-4.0.1/NEMO_billing/prepayments/apps.py +21 -0
  95. nemo_billing-4.0.1/NEMO_billing/prepayments/exceptions.py +34 -0
  96. nemo_billing-4.0.1/NEMO_billing/prepayments/migrations/0001_initial.py +180 -0
  97. nemo_billing-4.0.1/NEMO_billing/prepayments/migrations/0002_projectprepaymentdetail_overdraft_amount_allowed.py +22 -0
  98. nemo_billing-4.0.1/NEMO_billing/prepayments/migrations/0003_alter_fundtype_name.py +18 -0
  99. nemo_billing-4.0.1/NEMO_billing/prepayments/migrations/__init__.py +0 -0
  100. nemo_billing-4.0.1/NEMO_billing/prepayments/models.py +363 -0
  101. nemo_billing-4.0.1/NEMO_billing/prepayments/policy.py +76 -0
  102. nemo_billing-4.0.1/NEMO_billing/prepayments/templates/prepayments/prepaid_project_status.html +23 -0
  103. nemo_billing-4.0.1/NEMO_billing/prepayments/templates/prepayments/prepaid_project_status_table.html +109 -0
  104. nemo_billing-4.0.1/NEMO_billing/prepayments/urls.py +16 -0
  105. nemo_billing-4.0.1/NEMO_billing/prepayments/views/__init__.py +0 -0
  106. nemo_billing-4.0.1/NEMO_billing/prepayments/views/prepayments.py +94 -0
  107. nemo_billing-4.0.1/NEMO_billing/rates/__init__.py +1 -0
  108. nemo_billing-4.0.1/NEMO_billing/rates/admin.py +256 -0
  109. nemo_billing-4.0.1/NEMO_billing/rates/api.py +69 -0
  110. nemo_billing-4.0.1/NEMO_billing/rates/app_settings.py +4 -0
  111. nemo_billing-4.0.1/NEMO_billing/rates/apps.py +22 -0
  112. nemo_billing-4.0.1/NEMO_billing/rates/customization.py +66 -0
  113. nemo_billing-4.0.1/NEMO_billing/rates/migrations/0001_initial.py +156 -0
  114. nemo_billing-4.0.1/NEMO_billing/rates/migrations/0002_version_1_15_0.py +21 -0
  115. nemo_billing-4.0.1/NEMO_billing/rates/migrations/0003_version_1_17_0.py +63 -0
  116. nemo_billing-4.0.1/NEMO_billing/rates/migrations/0004_version_2_0_0.py +17 -0
  117. nemo_billing-4.0.1/NEMO_billing/rates/migrations/0005_version_2_3_0.py +20 -0
  118. nemo_billing-4.0.1/NEMO_billing/rates/migrations/0006_rate_daily_split_multi_day_charges.py +30 -0
  119. nemo_billing-4.0.1/NEMO_billing/rates/migrations/0007_areahighestdailyrategroup.py +30 -0
  120. nemo_billing-4.0.1/NEMO_billing/rates/migrations/0008_alter_ratecategory_name.py +18 -0
  121. nemo_billing-4.0.1/NEMO_billing/rates/migrations/__init__.py +0 -0
  122. nemo_billing-4.0.1/NEMO_billing/rates/model_diff.py +56 -0
  123. nemo_billing-4.0.1/NEMO_billing/rates/models.py +505 -0
  124. nemo_billing-4.0.1/NEMO_billing/rates/rates_class.py +103 -0
  125. nemo_billing-4.0.1/NEMO_billing/rates/static/rates/rates.css +5 -0
  126. nemo_billing-4.0.1/NEMO_billing/rates/templates/customizations/customizations_billing_rates.html +213 -0
  127. nemo_billing-4.0.1/NEMO_billing/rates/templates/rates/base.html +5 -0
  128. nemo_billing-4.0.1/NEMO_billing/rates/templates/rates/rate.html +172 -0
  129. nemo_billing-4.0.1/NEMO_billing/rates/templates/rates/rate_form_details.html +165 -0
  130. nemo_billing-4.0.1/NEMO_billing/rates/templates/rates/rate_list.html +149 -0
  131. nemo_billing-4.0.1/NEMO_billing/rates/templates/rates/rates.html +123 -0
  132. nemo_billing-4.0.1/NEMO_billing/rates/urls.py +23 -0
  133. nemo_billing-4.0.1/NEMO_billing/rates/views.py +506 -0
  134. nemo_billing-4.0.1/NEMO_billing/templates/base/navbar.html +7 -0
  135. nemo_billing-4.0.1/NEMO_billing/templates/base/navbar_billing.html +14 -0
  136. nemo_billing-4.0.1/NEMO_billing/templates/billing/cap_and_prepaid_base.html +36 -0
  137. nemo_billing-4.0.1/NEMO_billing/templates/billing/custom_charge.html +263 -0
  138. nemo_billing-4.0.1/NEMO_billing/templates/billing/custom_charges.html +46 -0
  139. nemo_billing-4.0.1/NEMO_billing/templates/billing/email_broadcast.html +152 -0
  140. nemo_billing-4.0.1/NEMO_billing/templates/staff_charges/choose_project.html +35 -0
  141. nemo_billing-4.0.1/NEMO_billing/templates/staff_charges/new_custom_staff_charge.html +53 -0
  142. nemo_billing-4.0.1/NEMO_billing/templatetags/__init__.py +0 -0
  143. nemo_billing-4.0.1/NEMO_billing/templatetags/billing_tags.py +30 -0
  144. nemo_billing-4.0.1/NEMO_billing/tests/__init__.py +0 -0
  145. nemo_billing-4.0.1/NEMO_billing/tests/cap/__init__.py +0 -0
  146. nemo_billing-4.0.1/NEMO_billing/tests/cap/test_cap_discount.py +409 -0
  147. nemo_billing-4.0.1/NEMO_billing/tests/cap/test_cap_invoicing.py +560 -0
  148. nemo_billing-4.0.1/NEMO_billing/tests/cap/test_cap_utilities.py +116 -0
  149. nemo_billing-4.0.1/NEMO_billing/tests/prepayments/__init__.py +0 -0
  150. nemo_billing-4.0.1/NEMO_billing/tests/prepayments/test_prepayments.py +325 -0
  151. nemo_billing-4.0.1/NEMO_billing/tests/rates/__init__.py +0 -0
  152. nemo_billing-4.0.1/NEMO_billing/tests/rates/test_rate_amount.py +670 -0
  153. nemo_billing-4.0.1/NEMO_billing/tests/rates/test_rate_time.py +366 -0
  154. nemo_billing-4.0.1/NEMO_billing/tests/rates/test_rate_uniqueness.py +112 -0
  155. nemo_billing-4.0.1/NEMO_billing/tests/rates/test_split_billables.py +516 -0
  156. nemo_billing-4.0.1/NEMO_billing/tests/test_hard_cap.py +338 -0
  157. nemo_billing-4.0.1/NEMO_billing/tests/test_project_expiration.py +53 -0
  158. nemo_billing-4.0.1/NEMO_billing/tests/test_settings.py +93 -0
  159. nemo_billing-4.0.1/NEMO_billing/tests/test_urls.py +36 -0
  160. nemo_billing-4.0.1/NEMO_billing/tests/test_utilities.py +23 -0
  161. nemo_billing-4.0.1/NEMO_billing/urls.py +38 -0
  162. nemo_billing-4.0.1/NEMO_billing/utilities.py +165 -0
  163. nemo_billing-4.0.1/NEMO_billing/views.py +280 -0
  164. nemo_billing-4.0.1/NEMO_billing.egg-info/PKG-INFO +245 -0
  165. nemo_billing-4.0.1/NEMO_billing.egg-info/SOURCES.txt +170 -0
  166. nemo_billing-4.0.1/NEMO_billing.egg-info/dependency_links.txt +1 -0
  167. nemo_billing-4.0.1/NEMO_billing.egg-info/requires.txt +16 -0
  168. nemo_billing-4.0.1/NEMO_billing.egg-info/top_level.txt +1 -0
  169. nemo_billing-4.0.1/PKG-INFO +245 -0
  170. nemo_billing-4.0.1/README.md +187 -0
  171. nemo_billing-4.0.1/pyproject.toml +58 -0
  172. nemo_billing-4.0.1/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Atlantis Labs LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
File without changes
@@ -0,0 +1,404 @@
1
+ from copy import deepcopy
2
+ from typing import Optional
3
+
4
+ from NEMO.admin import AreaAdmin, AreaAdminForm, ConsumableAdmin, StaffChargeAdmin, ToolAdmin, ToolAdminForm
5
+ from NEMO.models import Area, Consumable, StaffCharge, Tool
6
+ from django import forms
7
+ from django.conf import settings
8
+ from django.contrib import admin
9
+ from django.contrib.admin import register, widgets
10
+ from django.contrib.admin.widgets import FilteredSelectMultiple
11
+ from django.core.exceptions import ValidationError
12
+ from django.db.models import Q
13
+
14
+ from NEMO_billing.invoices.models import BillableItemType
15
+ from NEMO_billing.models import (
16
+ CoreFacility,
17
+ CoreRelationship,
18
+ CustomCharge,
19
+ Department,
20
+ Institution,
21
+ InstitutionType,
22
+ ProjectBillingHardCap,
23
+ )
24
+ from NEMO_billing.templatetags.billing_tags import cap_discount_installed
25
+ from NEMO_billing.utilities import IntMultipleChoiceField, hide_form_field
26
+
27
+ STATES = [
28
+ ("AL", "Alabama"),
29
+ ("AK", "Alaska"),
30
+ ("AS", "American Samoa"),
31
+ ("AZ", "Arizona"),
32
+ ("AR", "Arkansas"),
33
+ ("CA", "California"),
34
+ ("CO", "Colorado"),
35
+ ("CT", "Connecticut"),
36
+ ("DE", "Delaware"),
37
+ ("DC", "District of Columbia"),
38
+ ("FL", "Florida"),
39
+ ("GA", "Georgia"),
40
+ ("GU", "Guam"),
41
+ ("HI", "Hawaii"),
42
+ ("ID", "Idaho"),
43
+ ("IL", "Illinois"),
44
+ ("IN", "Indiana"),
45
+ ("IA", "Iowa"),
46
+ ("KS", "Kansas"),
47
+ ("KY", "Kentucky"),
48
+ ("LA", "Louisiana"),
49
+ ("ME", "Maine"),
50
+ ("MD", "Maryland"),
51
+ ("MA", "Massachusetts"),
52
+ ("MI", "Michigan"),
53
+ ("MN", "Minnesota"),
54
+ ("MS", "Mississippi"),
55
+ ("MO", "Missouri"),
56
+ ("MT", "Montana"),
57
+ ("NE", "Nebraska"),
58
+ ("NV", "Nevada"),
59
+ ("NH", "New Hampshire"),
60
+ ("NJ", "New Jersey"),
61
+ ("NM", "New Mexico"),
62
+ ("NY", "New York"),
63
+ ("NC", "North Carolina"),
64
+ ("ND", "North Dakota"),
65
+ ("MP", "Northern Mariana Islands"),
66
+ ("OH", "Ohio"),
67
+ ("OK", "Oklahoma"),
68
+ ("OR", "Oregon"),
69
+ ("PA", "Pennsylvania"),
70
+ ("PR", "Puerto Rico"),
71
+ ("RI", "Rhode Island"),
72
+ ("SC", "South Carolina"),
73
+ ("SD", "South Dakota"),
74
+ ("TN", "Tennessee"),
75
+ ("TX", "Texas"),
76
+ ("UT", "Utah"),
77
+ ("VT", "Vermont"),
78
+ ("VI", "Virgin Islands"),
79
+ ("VA", "Virginia"),
80
+ ("WA", "Washington"),
81
+ ("WV", "West Virginia"),
82
+ ("WI", "Wisconsin"),
83
+ ("WY", "Wyoming"),
84
+ ]
85
+
86
+
87
+ def changed_or_added(change, original_set, current_set):
88
+ # If the model object is being changed then we can get the list of previous members.
89
+ if change:
90
+ original_members = set(original_set)
91
+ else: # The model object is being created (instead of changed) so we can assume there are no members (initially).
92
+ original_members = set()
93
+ current_members = set(current_set)
94
+ added_members = []
95
+ removed_members = []
96
+
97
+ # Log membership changes if they occurred.
98
+ symmetric_difference = original_members ^ current_members
99
+ if symmetric_difference:
100
+ if change: # the members have changed, so find out what was added and removed...
101
+ # We can see the previous members of the object model by looking it up
102
+ # in the database because the member list hasn't been committed yet.
103
+ added_members = set(current_members) - set(original_members)
104
+ removed_members = set(original_members) - set(current_members)
105
+
106
+ else: # a model object is being created (instead of changed) so we can assume all the members are new...
107
+ added_members = current_set
108
+ return added_members, removed_members
109
+
110
+
111
+ def save_all_core_facility_relationships(
112
+ current_items, original_items, core_facility: CoreFacility, field: str, change
113
+ ):
114
+ added_items, removed_items = changed_or_added(change, original_items, current_items)
115
+ for item in added_items:
116
+ save_or_delete_core_facility(item, core_facility, field)
117
+ for item in removed_items:
118
+ save_or_delete_core_facility(item, None, field)
119
+
120
+
121
+ def save_or_delete_core_facility(obj, core_facility: Optional[CoreFacility], field):
122
+ has_core_relationship = hasattr(obj, "core_rel")
123
+ if core_facility:
124
+ if not has_core_relationship:
125
+ obj.core_rel = CoreRelationship()
126
+ obj.core_rel.core_facility = core_facility
127
+ setattr(obj.core_rel, field, obj)
128
+ obj.core_rel.save()
129
+ elif not core_facility and has_core_relationship:
130
+ obj.core_rel.delete()
131
+
132
+
133
+ class ListTextWidget(forms.TextInput):
134
+ def __init__(self, data_list, name, values_only=False, *args, **kwargs):
135
+ super(ListTextWidget, self).__init__(*args, **kwargs)
136
+ self._name = name
137
+ self._list = data_list
138
+ self._values_only = values_only
139
+ self.attrs.update({"list": "list__{}".format(self._name)})
140
+
141
+ def render(self, name, value, attrs=None, renderer=None):
142
+ text_html = super(ListTextWidget, self).render(name, value, attrs=attrs)
143
+ data_list = '<datalist id="list__{}">'.format(self._name)
144
+ for item in self._list:
145
+ value = item[0] if not self._values_only else item[1]
146
+ data_list += '<option value="{}">{}</option>'.format(value, item[1])
147
+ data_list += "</datalist>"
148
+
149
+ return text_html + data_list
150
+
151
+
152
+ class InstitutionForm(forms.ModelForm):
153
+ class Meta:
154
+ model = Institution
155
+ fields = "__all__"
156
+ widgets = {"state": ListTextWidget(data_list=STATES, name="state_list", values_only=True)}
157
+
158
+
159
+ @register(Institution)
160
+ class InstitutionAdmin(admin.ModelAdmin):
161
+ list_display = ["name", "institution_type", "state", "get_country_display", "zip_code"]
162
+ form = InstitutionForm
163
+
164
+
165
+ class CoreFacilityAdminForm(forms.ModelForm):
166
+ class Meta:
167
+ model = CoreFacility
168
+ fields = "__all__"
169
+
170
+ core_facility_tools = forms.ModelMultipleChoiceField(
171
+ queryset=Tool.objects.all(),
172
+ required=False,
173
+ widget=FilteredSelectMultiple(verbose_name="Core facility tools", is_stacked=False),
174
+ )
175
+ core_facility_areas = forms.ModelMultipleChoiceField(
176
+ queryset=Area.objects.all(),
177
+ required=False,
178
+ widget=FilteredSelectMultiple(verbose_name="Core facility areas", is_stacked=False),
179
+ )
180
+ core_facility_consumables = forms.ModelMultipleChoiceField(
181
+ queryset=Consumable.objects.all(),
182
+ required=False,
183
+ widget=FilteredSelectMultiple(verbose_name="Core facility consumable", is_stacked=False),
184
+ )
185
+
186
+ def __init__(self, *args, **kwargs):
187
+ super().__init__(*args, **kwargs)
188
+ # We are filtering out already set tools, areas and consumables
189
+ no_facility_filter = Q(core_rel__isnull=True)
190
+ # Exclude children tools since their core facility is their parent's
191
+ if "core_facility_tools" in self.fields:
192
+ tool_filter = Q()
193
+ if self.instance.pk:
194
+ tool_filter = Q(core_rel__in=self.instance.corerelationship_set.filter(tool__isnull=False))
195
+ self.fields["core_facility_tools"].queryset = Tool.objects.filter(tool_filter | no_facility_filter).exclude(
196
+ parent_tool__isnull=False
197
+ )
198
+ if self.instance.pk:
199
+ self.fields["core_facility_tools"].initial = Tool.objects.filter(tool_filter)
200
+ if "core_facility_areas" in self.fields:
201
+ area_filter = Q()
202
+ if self.instance.pk:
203
+ area_filter = Q(core_rel__in=self.instance.corerelationship_set.filter(area__isnull=False))
204
+ self.fields["core_facility_areas"].queryset = Area.objects.filter(area_filter | no_facility_filter)
205
+ if self.instance.pk:
206
+ self.fields["core_facility_areas"].initial = Area.objects.filter(area_filter)
207
+ if "core_facility_consumables" in self.fields:
208
+ consumable_filter = Q()
209
+ if self.instance.pk:
210
+ consumable_filter = Q(core_rel__in=self.instance.corerelationship_set.filter(consumable__isnull=False))
211
+ self.fields["core_facility_consumables"].queryset = Consumable.objects.filter(
212
+ consumable_filter | no_facility_filter
213
+ )
214
+ if self.instance.pk:
215
+ self.fields["core_facility_consumables"].initial = Consumable.objects.filter(consumable_filter)
216
+
217
+
218
+ @register(CoreFacility)
219
+ class CoreFacilityAdmin(admin.ModelAdmin):
220
+ form = CoreFacilityAdminForm
221
+
222
+ def save_model(self, request, obj, form, change):
223
+ super().save_model(request, obj, form, change)
224
+ if "core_facility_tools" in form.changed_data:
225
+ original_items = Tool.objects.filter(core_rel__in=obj.corerelationship_set.filter(tool__isnull=False))
226
+ save_all_core_facility_relationships(
227
+ form.cleaned_data["core_facility_tools"], original_items, obj, "tool", change
228
+ )
229
+ if "core_facility_areas" in form.changed_data:
230
+ original_items = Area.objects.filter(core_rel__in=obj.corerelationship_set.filter(area__isnull=False))
231
+ save_all_core_facility_relationships(
232
+ form.cleaned_data["core_facility_areas"], original_items, obj, "area", change
233
+ )
234
+ if "core_facility_consumables" in form.changed_data:
235
+ original_items = Consumable.objects.filter(
236
+ core_rel__in=obj.corerelationship_set.filter(consumable__isnull=False)
237
+ )
238
+ save_all_core_facility_relationships(
239
+ form.cleaned_data["core_facility_consumables"], original_items, obj, "consumable", change
240
+ )
241
+
242
+
243
+ class NewToolAdminForm(ToolAdminForm):
244
+ core_facility = forms.ModelChoiceField(
245
+ queryset=CoreFacility.objects.all(),
246
+ required=False,
247
+ help_text="The core facility this tool belongs to. Used for billing purposes.",
248
+ )
249
+
250
+ def __init__(self, *args, **kwargs):
251
+ super().__init__(*args, **kwargs)
252
+ if self.instance.pk and "core_facility" in self.fields:
253
+ self.fields["core_facility"].initial = self.instance.core_facility
254
+
255
+ def clean_core_facility(self):
256
+ parent_tool = self.cleaned_data.get("parent_tool")
257
+ core_facility = self.cleaned_data.get("core_facility")
258
+ if not parent_tool and not core_facility and settings.TOOL_CORE_FACILITY_REQUIRED:
259
+ raise ValidationError("This field is required.")
260
+ return core_facility
261
+
262
+
263
+ class NewToolAdmin(ToolAdmin):
264
+ form = NewToolAdminForm
265
+
266
+ def save_model(self, request, obj: Tool, form, change):
267
+ super().save_model(request, obj, form, change)
268
+ save_or_delete_core_facility(obj, form.cleaned_data.get("core_facility"), "tool")
269
+
270
+ def get_fieldsets(self, request, obj: Area = None):
271
+ # Add core_facility field
272
+ fieldsets = deepcopy(super().get_fieldsets(request, obj))
273
+ fieldsets[0][1]["fields"] = fieldsets[0][1]["fields"] + ("core_facility",)
274
+ return fieldsets
275
+
276
+
277
+ class NewAreaAdminForm(AreaAdminForm):
278
+ core_facility = forms.ModelChoiceField(
279
+ queryset=CoreFacility.objects.all(),
280
+ required=settings.AREA_CORE_FACILITY_REQUIRED,
281
+ help_text="The core facility this area belongs to. Used for billing purposes.",
282
+ )
283
+
284
+ def __init__(self, *args, **kwargs):
285
+ super().__init__(*args, **kwargs)
286
+ if self.instance.pk and "core_facility" in self.fields:
287
+ self.fields["core_facility"].initial = self.instance.core_facility
288
+
289
+
290
+ class NewAreaAdmin(AreaAdmin):
291
+ form = NewAreaAdminForm
292
+
293
+ def get_fieldsets(self, request, obj: Area = None):
294
+ # Add core_facility field
295
+ fieldsets = deepcopy(super().get_fieldsets(request, obj))
296
+ fieldsets[0][1]["fields"] = fieldsets[0][1]["fields"] + ("core_facility",)
297
+ return fieldsets
298
+
299
+ def save_model(self, request, obj: Area, form, change):
300
+ super().save_model(request, obj, form, change)
301
+ save_or_delete_core_facility(obj, form.cleaned_data["core_facility"], "area")
302
+
303
+
304
+ class NewConsumableAdminForm(forms.ModelForm):
305
+ core_facility = forms.ModelChoiceField(
306
+ queryset=CoreFacility.objects.all(),
307
+ required=settings.CONSUMABLE_CORE_FACILITY_REQUIRED,
308
+ help_text="The core facility this consumable belongs to. Used for billing purposes.",
309
+ )
310
+
311
+ def __init__(self, *args, **kwargs):
312
+ super().__init__(*args, **kwargs)
313
+ if self.instance.pk and "core_facility" in self.fields:
314
+ self.fields["core_facility"].initial = self.instance.core_facility
315
+
316
+
317
+ class NewConsumableAdmin(ConsumableAdmin):
318
+ form = NewConsumableAdminForm
319
+
320
+ def save_model(self, request, obj: Consumable, form, change):
321
+ super().save_model(request, obj, form, change)
322
+ save_or_delete_core_facility(obj, form.cleaned_data["core_facility"], "consumable")
323
+
324
+
325
+ class NewStaffChargeAdminForm(forms.ModelForm):
326
+ core_facility = forms.ModelChoiceField(
327
+ queryset=CoreFacility.objects.all(),
328
+ required=settings.STAFF_CHARGE_CORE_FACILITY_REQUIRED,
329
+ help_text="The core facility this staff charge belongs to. Used for billing purposes.",
330
+ )
331
+
332
+ def __init__(self, *args, **kwargs):
333
+ super().__init__(*args, **kwargs)
334
+ if self.instance.pk and "core_facility" in self.fields:
335
+ self.fields["core_facility"].initial = self.instance.core_facility
336
+
337
+
338
+ class NewStaffChargeAdmin(StaffChargeAdmin):
339
+ form = NewStaffChargeAdminForm
340
+
341
+ def save_model(self, request, obj: Consumable, form, change):
342
+ super().save_model(request, obj, form, change)
343
+ save_or_delete_core_facility(obj, form.cleaned_data["core_facility"], "staff_charge")
344
+
345
+
346
+ class CustomChargeAdminForm(forms.ModelForm):
347
+ class Meta:
348
+ model = CustomCharge
349
+ fields = "__all__"
350
+
351
+ core_facility = forms.ModelChoiceField(
352
+ queryset=CoreFacility.objects.all(),
353
+ required=settings.CUSTOM_CHARGE_CORE_FACILITY_REQUIRED,
354
+ help_text="The core facility this tool belongs to. Used for billing purposes.",
355
+ )
356
+
357
+ def __init__(self, *args, **kwargs):
358
+ super().__init__(*args, **kwargs)
359
+ if not cap_discount_installed():
360
+ hide_form_field(self, "cap_eligible")
361
+
362
+
363
+ @register(CustomCharge)
364
+ class CustomChargeAdmin(admin.ModelAdmin):
365
+ list_display = ("name", "date", "amount", "customer", "project", "creator", "core_facility", "waived")
366
+ search_fields = ("name", "customer__first_name", "customer__last_name", "customer__username", "project__name")
367
+ list_filter = ("date", "project", "core_facility", "waived")
368
+ form = CustomChargeAdminForm
369
+
370
+
371
+ class ProjectBillingHardCapAdminForm(forms.ModelForm):
372
+ charge_types = IntMultipleChoiceField(
373
+ choices=BillableItemType.choices(),
374
+ required=True,
375
+ widget=widgets.FilteredSelectMultiple(verbose_name="Types", is_stacked=False),
376
+ )
377
+
378
+ class Meta:
379
+ model = ProjectBillingHardCap
380
+ fields = "__all__"
381
+
382
+
383
+ @admin.register(ProjectBillingHardCap)
384
+ class ProjectBillingHardCapAdmin(admin.ModelAdmin):
385
+ list_display = ["project", "enabled", "start_date", "end_date", "amount", "get_charge_types"]
386
+ list_filter = ["enabled", "project"]
387
+ form = ProjectBillingHardCapAdminForm
388
+
389
+ @admin.display(description="Charge types")
390
+ def get_charge_types(self, instance: ProjectBillingHardCap):
391
+ return instance.get_charge_types_display()
392
+
393
+
394
+ # Re-register ToolAdmin, AreaAdmin & ConsumableAdmin
395
+ admin.site.unregister(Tool)
396
+ admin.site.register(Tool, NewToolAdmin)
397
+ admin.site.unregister(Area)
398
+ admin.site.register(Area, NewAreaAdmin)
399
+ admin.site.unregister(Consumable)
400
+ admin.site.register(Consumable, NewConsumableAdmin)
401
+ admin.site.unregister(StaffCharge)
402
+ admin.site.register(StaffCharge, NewStaffChargeAdmin)
403
+ admin.site.register(InstitutionType)
404
+ admin.site.register(Department)
@@ -0,0 +1,94 @@
1
+ from NEMO.serializers import ModelSerializer
2
+ from NEMO.views.api import ModelViewSet, boolean_filters, datetime_filters, key_filters, number_filters, string_filters
3
+ from rest_flex_fields import FlexFieldsModelSerializer
4
+ from rest_framework.fields import CharField
5
+
6
+ from NEMO_billing.models import CustomCharge, Department, Institution, InstitutionType
7
+
8
+
9
+ class CustomChargeSerializer(FlexFieldsModelSerializer, ModelSerializer):
10
+ class Meta:
11
+ model = CustomCharge
12
+ fields = "__all__"
13
+ expandable_fields = {
14
+ "customer": "NEMO.serializers.UserSerializer",
15
+ "creator": "NEMO.serializers.UserSerializer",
16
+ "project": "NEMO.serializers.ProjectSerializer",
17
+ "validated_by": "NEMO.serializers.UserSerializer",
18
+ "waived_by": "NEMO.serializers.UserSerializer",
19
+ }
20
+
21
+ customer_name = CharField(source="customer.get_name", read_only=True)
22
+ creator_name = CharField(source="creator.get_name", read_only=True)
23
+
24
+
25
+ class CustomChargeViewSet(ModelViewSet):
26
+ filename = "custom_charges"
27
+ queryset = CustomCharge.objects.all()
28
+ serializer_class = CustomChargeSerializer
29
+ filterset_fields = {
30
+ "name": string_filters,
31
+ "customer": key_filters,
32
+ "creator": key_filters,
33
+ "project": key_filters,
34
+ "date": datetime_filters,
35
+ "amount": number_filters,
36
+ "core_facility": key_filters,
37
+ "validated": boolean_filters,
38
+ "validated_by": key_filters,
39
+ "waived": boolean_filters,
40
+ "waived_on": datetime_filters,
41
+ "waived_by": key_filters,
42
+ }
43
+
44
+
45
+ class DepartmentSerializer(ModelSerializer):
46
+ class Meta:
47
+ model = Department
48
+ fields = "__all__"
49
+
50
+
51
+ class DepartmentViewSet(ModelViewSet):
52
+ filename = "departments"
53
+ queryset = Department.objects.all()
54
+ serializer_class = DepartmentSerializer
55
+ filterset_fields = {
56
+ "name": string_filters,
57
+ }
58
+
59
+
60
+ class InstitutionTypeSerializer(ModelSerializer):
61
+ class Meta:
62
+ model = InstitutionType
63
+ fields = "__all__"
64
+
65
+
66
+ class InstitutionTypeViewSet(ModelViewSet):
67
+ filename = "institution_types"
68
+ queryset = InstitutionType.objects.all()
69
+ serializer_class = InstitutionTypeSerializer
70
+ filterset_fields = {
71
+ "name": string_filters,
72
+ }
73
+
74
+
75
+ class InstitutionSerializer(FlexFieldsModelSerializer, ModelSerializer):
76
+ class Meta:
77
+ model = Institution
78
+ fields = "__all__"
79
+ expandable_fields = {
80
+ "institution_type": "NEMO_billing.api.InstitutionTypeSerializer",
81
+ }
82
+
83
+
84
+ class InstitutionViewSet(ModelViewSet):
85
+ filename = "institutions"
86
+ queryset = Institution.objects.all()
87
+ serializer_class = InstitutionSerializer
88
+ filterset_fields = {
89
+ "name": string_filters,
90
+ "institution_type_id": key_filters,
91
+ "state": string_filters,
92
+ "country": string_filters,
93
+ "zip_code": string_filters,
94
+ }
@@ -0,0 +1,10 @@
1
+ TOOL_CORE_FACILITY_REQUIRED = False
2
+ AREA_CORE_FACILITY_REQUIRED = False
3
+ CONSUMABLE_CORE_FACILITY_REQUIRED = False
4
+ STAFF_CHARGE_CORE_FACILITY_REQUIRED = False
5
+ CUSTOM_CHARGE_CORE_FACILITY_REQUIRED = False
6
+
7
+ STAFF_TOOL_USAGE_AS_STAFF_CHARGE = True
8
+ STAFF_AREA_ACCESS_AS_STAFF_CHARGE = True
9
+
10
+ NEMO_POLICY_CLASS = "NEMO_billing.policy.BillingPolicy"
@@ -0,0 +1,20 @@
1
+ from django.apps import AppConfig
2
+ from django.conf import settings
3
+
4
+ from . import app_settings as defaults
5
+
6
+ # Set some app default settings
7
+ for name in dir(defaults):
8
+ if name.isupper() and not hasattr(settings, name):
9
+ setattr(settings, name, getattr(defaults, name))
10
+
11
+
12
+ class NEMOBillingConfig(AppConfig):
13
+ name = "NEMO_billing"
14
+ verbose_name = "Billing"
15
+ default_auto_field = "django.db.models.AutoField"
16
+
17
+ def ready(self):
18
+ from NEMO.plugins.utils import check_extra_dependencies
19
+
20
+ check_extra_dependencies(self.name, ["NEMO", "NEMO-CE"])