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.
- nemo_billing-4.0.1/LICENSE +21 -0
- nemo_billing-4.0.1/NEMO_billing/__init__.py +0 -0
- nemo_billing-4.0.1/NEMO_billing/admin.py +404 -0
- nemo_billing-4.0.1/NEMO_billing/api.py +94 -0
- nemo_billing-4.0.1/NEMO_billing/app_settings.py +10 -0
- nemo_billing-4.0.1/NEMO_billing/apps.py +20 -0
- nemo_billing-4.0.1/NEMO_billing/cap_discount/__init__.py +0 -0
- nemo_billing-4.0.1/NEMO_billing/cap_discount/admin.py +145 -0
- nemo_billing-4.0.1/NEMO_billing/cap_discount/app_settings.py +1 -0
- nemo_billing-4.0.1/NEMO_billing/cap_discount/apps.py +15 -0
- nemo_billing-4.0.1/NEMO_billing/cap_discount/customization.py +61 -0
- nemo_billing-4.0.1/NEMO_billing/cap_discount/exceptions.py +21 -0
- nemo_billing-4.0.1/NEMO_billing/cap_discount/migrations/0001_initial.py +345 -0
- nemo_billing-4.0.1/NEMO_billing/cap_discount/migrations/0002_version_2_0_0.py +54 -0
- nemo_billing-4.0.1/NEMO_billing/cap_discount/migrations/0003_multi_tier_discount.py +161 -0
- nemo_billing-4.0.1/NEMO_billing/cap_discount/migrations/0004_shortened_unique_constraints.py +61 -0
- nemo_billing-4.0.1/NEMO_billing/cap_discount/migrations/0005_cap_multi_core_facilities.py +97 -0
- nemo_billing-4.0.1/NEMO_billing/cap_discount/migrations/__init__.py +0 -0
- nemo_billing-4.0.1/NEMO_billing/cap_discount/models.py +446 -0
- nemo_billing-4.0.1/NEMO_billing/cap_discount/processors.py +426 -0
- nemo_billing-4.0.1/NEMO_billing/cap_discount/templates/cap_discount/cap_discount_status.html +58 -0
- nemo_billing-4.0.1/NEMO_billing/cap_discount/templates/cap_discount/usage_cap_discounts.html +28 -0
- nemo_billing-4.0.1/NEMO_billing/cap_discount/templates/customizations/customizations_cap_discount.html +96 -0
- nemo_billing-4.0.1/NEMO_billing/cap_discount/urls.py +19 -0
- nemo_billing-4.0.1/NEMO_billing/cap_discount/views.py +98 -0
- nemo_billing-4.0.1/NEMO_billing/exceptions.py +33 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/__init__.py +0 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/admin.py +328 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/api.py +234 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/app_settings.py +5 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/apps.py +15 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/customization.py +100 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/exceptions.py +67 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/management/__init__.py +0 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/management/commands/__init__.py +0 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/management/commands/deactivate_expired_projects.py +10 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/management/commands/send_invoice_payment_reminder.py +10 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0001_initial.py +305 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0002_version_1_2_0.py +95 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0003_version_1_4_0.py +37 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0004_version_1_6_0.py +18 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0005_version_1_6_1.py +19 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0006_version_1_7_1.py +25 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0007_version_1_8_0.py +18 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0008_version_1_10_0.py +18 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0009_version_1_23_0.py +36 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0010_version_2_0_0.py +18 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0011_version_2_4_0.py +50 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0012_invoiceconfiguration_invoice_title.py +22 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0013_alter_invoiceconfiguration_name_and_more.py +38 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/migrations/0014_invoicedetailitem_waived.py +18 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/migrations/__init__.py +0 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/models.py +598 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/pdf_utilities.py +310 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/processors.py +752 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/renderers.py +595 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/static/invoices/invoices.css +50 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/templates/customizations/customizations_billing.html +109 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/templates/customizations/customizations_invoices.html +178 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/base.html +6 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/email/billing_project_expiration_reminder_email_message.html +53 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/email/billing_project_expiration_reminder_email_subject.txt +5 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/email/email_send_invoice_message.html +36 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/email/email_send_invoice_reminder_message.html +36 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/email/email_send_invoice_reminder_subject.txt +1 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/email/email_send_invoice_subject.txt +1 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/invoice.html +351 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/invoice_details.html +93 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/invoices.html +367 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/project/edit_project.html +401 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/project/view_project_additional_info.html +98 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/templates/invoices/usage.html +404 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/urls.py +75 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/utilities.py +107 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/views/__init__.py +0 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/views/invoices.py +530 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/views/project.py +148 -0
- nemo_billing-4.0.1/NEMO_billing/invoices/views/usage.py +353 -0
- nemo_billing-4.0.1/NEMO_billing/migrations/0001_initial.py +126 -0
- nemo_billing-4.0.1/NEMO_billing/migrations/0002_version_1_2_0.py +26 -0
- nemo_billing-4.0.1/NEMO_billing/migrations/0003_version_2_0_0.py +18 -0
- nemo_billing-4.0.1/NEMO_billing/migrations/0004_version_2_4_0.py +329 -0
- nemo_billing-4.0.1/NEMO_billing/migrations/0005_version_2_5_6.py +17 -0
- nemo_billing-4.0.1/NEMO_billing/migrations/0006_projectbillinghardcap.py +56 -0
- nemo_billing-4.0.1/NEMO_billing/migrations/0007_alter_projectbillinghardcap_end_date_and_more.py +23 -0
- nemo_billing-4.0.1/NEMO_billing/migrations/0008_customcharge_validated_customcharge_validated_by_and_more.py +68 -0
- nemo_billing-4.0.1/NEMO_billing/migrations/__init__.py +0 -0
- nemo_billing-4.0.1/NEMO_billing/models.py +478 -0
- nemo_billing-4.0.1/NEMO_billing/policy.py +145 -0
- nemo_billing-4.0.1/NEMO_billing/prepayments/__init__.py +0 -0
- nemo_billing-4.0.1/NEMO_billing/prepayments/admin.py +81 -0
- nemo_billing-4.0.1/NEMO_billing/prepayments/api.py +57 -0
- nemo_billing-4.0.1/NEMO_billing/prepayments/app_settings.py +0 -0
- nemo_billing-4.0.1/NEMO_billing/prepayments/apps.py +21 -0
- nemo_billing-4.0.1/NEMO_billing/prepayments/exceptions.py +34 -0
- nemo_billing-4.0.1/NEMO_billing/prepayments/migrations/0001_initial.py +180 -0
- nemo_billing-4.0.1/NEMO_billing/prepayments/migrations/0002_projectprepaymentdetail_overdraft_amount_allowed.py +22 -0
- nemo_billing-4.0.1/NEMO_billing/prepayments/migrations/0003_alter_fundtype_name.py +18 -0
- nemo_billing-4.0.1/NEMO_billing/prepayments/migrations/__init__.py +0 -0
- nemo_billing-4.0.1/NEMO_billing/prepayments/models.py +363 -0
- nemo_billing-4.0.1/NEMO_billing/prepayments/policy.py +76 -0
- nemo_billing-4.0.1/NEMO_billing/prepayments/templates/prepayments/prepaid_project_status.html +23 -0
- nemo_billing-4.0.1/NEMO_billing/prepayments/templates/prepayments/prepaid_project_status_table.html +109 -0
- nemo_billing-4.0.1/NEMO_billing/prepayments/urls.py +16 -0
- nemo_billing-4.0.1/NEMO_billing/prepayments/views/__init__.py +0 -0
- nemo_billing-4.0.1/NEMO_billing/prepayments/views/prepayments.py +94 -0
- nemo_billing-4.0.1/NEMO_billing/rates/__init__.py +1 -0
- nemo_billing-4.0.1/NEMO_billing/rates/admin.py +256 -0
- nemo_billing-4.0.1/NEMO_billing/rates/api.py +69 -0
- nemo_billing-4.0.1/NEMO_billing/rates/app_settings.py +4 -0
- nemo_billing-4.0.1/NEMO_billing/rates/apps.py +22 -0
- nemo_billing-4.0.1/NEMO_billing/rates/customization.py +66 -0
- nemo_billing-4.0.1/NEMO_billing/rates/migrations/0001_initial.py +156 -0
- nemo_billing-4.0.1/NEMO_billing/rates/migrations/0002_version_1_15_0.py +21 -0
- nemo_billing-4.0.1/NEMO_billing/rates/migrations/0003_version_1_17_0.py +63 -0
- nemo_billing-4.0.1/NEMO_billing/rates/migrations/0004_version_2_0_0.py +17 -0
- nemo_billing-4.0.1/NEMO_billing/rates/migrations/0005_version_2_3_0.py +20 -0
- nemo_billing-4.0.1/NEMO_billing/rates/migrations/0006_rate_daily_split_multi_day_charges.py +30 -0
- nemo_billing-4.0.1/NEMO_billing/rates/migrations/0007_areahighestdailyrategroup.py +30 -0
- nemo_billing-4.0.1/NEMO_billing/rates/migrations/0008_alter_ratecategory_name.py +18 -0
- nemo_billing-4.0.1/NEMO_billing/rates/migrations/__init__.py +0 -0
- nemo_billing-4.0.1/NEMO_billing/rates/model_diff.py +56 -0
- nemo_billing-4.0.1/NEMO_billing/rates/models.py +505 -0
- nemo_billing-4.0.1/NEMO_billing/rates/rates_class.py +103 -0
- nemo_billing-4.0.1/NEMO_billing/rates/static/rates/rates.css +5 -0
- nemo_billing-4.0.1/NEMO_billing/rates/templates/customizations/customizations_billing_rates.html +213 -0
- nemo_billing-4.0.1/NEMO_billing/rates/templates/rates/base.html +5 -0
- nemo_billing-4.0.1/NEMO_billing/rates/templates/rates/rate.html +172 -0
- nemo_billing-4.0.1/NEMO_billing/rates/templates/rates/rate_form_details.html +165 -0
- nemo_billing-4.0.1/NEMO_billing/rates/templates/rates/rate_list.html +149 -0
- nemo_billing-4.0.1/NEMO_billing/rates/templates/rates/rates.html +123 -0
- nemo_billing-4.0.1/NEMO_billing/rates/urls.py +23 -0
- nemo_billing-4.0.1/NEMO_billing/rates/views.py +506 -0
- nemo_billing-4.0.1/NEMO_billing/templates/base/navbar.html +7 -0
- nemo_billing-4.0.1/NEMO_billing/templates/base/navbar_billing.html +14 -0
- nemo_billing-4.0.1/NEMO_billing/templates/billing/cap_and_prepaid_base.html +36 -0
- nemo_billing-4.0.1/NEMO_billing/templates/billing/custom_charge.html +263 -0
- nemo_billing-4.0.1/NEMO_billing/templates/billing/custom_charges.html +46 -0
- nemo_billing-4.0.1/NEMO_billing/templates/billing/email_broadcast.html +152 -0
- nemo_billing-4.0.1/NEMO_billing/templates/staff_charges/choose_project.html +35 -0
- nemo_billing-4.0.1/NEMO_billing/templates/staff_charges/new_custom_staff_charge.html +53 -0
- nemo_billing-4.0.1/NEMO_billing/templatetags/__init__.py +0 -0
- nemo_billing-4.0.1/NEMO_billing/templatetags/billing_tags.py +30 -0
- nemo_billing-4.0.1/NEMO_billing/tests/__init__.py +0 -0
- nemo_billing-4.0.1/NEMO_billing/tests/cap/__init__.py +0 -0
- nemo_billing-4.0.1/NEMO_billing/tests/cap/test_cap_discount.py +409 -0
- nemo_billing-4.0.1/NEMO_billing/tests/cap/test_cap_invoicing.py +560 -0
- nemo_billing-4.0.1/NEMO_billing/tests/cap/test_cap_utilities.py +116 -0
- nemo_billing-4.0.1/NEMO_billing/tests/prepayments/__init__.py +0 -0
- nemo_billing-4.0.1/NEMO_billing/tests/prepayments/test_prepayments.py +325 -0
- nemo_billing-4.0.1/NEMO_billing/tests/rates/__init__.py +0 -0
- nemo_billing-4.0.1/NEMO_billing/tests/rates/test_rate_amount.py +670 -0
- nemo_billing-4.0.1/NEMO_billing/tests/rates/test_rate_time.py +366 -0
- nemo_billing-4.0.1/NEMO_billing/tests/rates/test_rate_uniqueness.py +112 -0
- nemo_billing-4.0.1/NEMO_billing/tests/rates/test_split_billables.py +516 -0
- nemo_billing-4.0.1/NEMO_billing/tests/test_hard_cap.py +338 -0
- nemo_billing-4.0.1/NEMO_billing/tests/test_project_expiration.py +53 -0
- nemo_billing-4.0.1/NEMO_billing/tests/test_settings.py +93 -0
- nemo_billing-4.0.1/NEMO_billing/tests/test_urls.py +36 -0
- nemo_billing-4.0.1/NEMO_billing/tests/test_utilities.py +23 -0
- nemo_billing-4.0.1/NEMO_billing/urls.py +38 -0
- nemo_billing-4.0.1/NEMO_billing/utilities.py +165 -0
- nemo_billing-4.0.1/NEMO_billing/views.py +280 -0
- nemo_billing-4.0.1/NEMO_billing.egg-info/PKG-INFO +245 -0
- nemo_billing-4.0.1/NEMO_billing.egg-info/SOURCES.txt +170 -0
- nemo_billing-4.0.1/NEMO_billing.egg-info/dependency_links.txt +1 -0
- nemo_billing-4.0.1/NEMO_billing.egg-info/requires.txt +16 -0
- nemo_billing-4.0.1/NEMO_billing.egg-info/top_level.txt +1 -0
- nemo_billing-4.0.1/PKG-INFO +245 -0
- nemo_billing-4.0.1/README.md +187 -0
- nemo_billing-4.0.1/pyproject.toml +58 -0
- 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"])
|
|
File without changes
|