Skip to content

Commit fa3ccc9

Browse files
committed
Merge branch 'master' into development
2 parents c459147 + 8be3100 commit fa3ccc9

File tree

8 files changed

+121
-18
lines changed

8 files changed

+121
-18
lines changed

coldfront/config/plugins/ifx.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ class GROUPS():
2525
class RATES():
2626
INTERNAL_RATE_NAME = 'Harvard Internal Rate'
2727

28+
class EMAILS():
29+
DEFAULT_EMAIL_FROM_ADDRESS = 'rchelp@rc.fas.harvard.edu'
30+
2831
# Ignore billing models in the django-author pre-save so that values are set directly
2932
AUTHOR_IGNORE_MODELS = [
3033
'ifxbilling.BillingRecord',
@@ -38,3 +41,7 @@ class RATES():
3841

3942
IFXREPORT_FILE_ROOT = os.path.join(MEDIA_ROOT, 'reports')
4043
IFXREPORT_URL_ROOT = f'{MEDIA_URL}reports'
44+
45+
# Class to be used for rebalancing
46+
REBALANCER_CLASS = 'coldfront.plugins.ifx.calculator.ColdfrontRebalance'
47+

coldfront/plugins/ifx/calculator.py

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from django.utils import timezone
1111
from django.contrib.auth import get_user_model
1212
from django.conf import settings
13-
from ifxbilling.calculator import BasicBillingCalculator, NewBillingCalculator
13+
from ifxbilling.calculator import BasicBillingCalculator, NewBillingCalculator, Rebalance
1414
from ifxbilling.models import Account, Product, ProductUsage, Rate, BillingRecord
1515
from ifxuser.models import Organization
1616
from coldfront.core.allocation.models import Allocation, AllocationStatusChoice
@@ -29,7 +29,7 @@ class NewColdfrontBillingCalculator(NewBillingCalculator):
2929
STORAGE_QUOTA_ATTRIBUTE = 'Storage Quota (TB)'
3030
STORAGE_RESOURCE_TYPE = 'Storage'
3131

32-
def calculate_billing_month(self, year, month, organizations=None, recalculate=False, verbosity=0):
32+
def calculate_billing_month(self, year, month, organizations=None, user=None, recalculate=False, verbosity=0):
3333
'''
3434
Calculate a month of billing for the given year and month
3535
@@ -47,6 +47,9 @@ def calculate_billing_month(self, year, month, organizations=None, recalculate=F
4747
:param organizations: List of specific organizations to process. If not set, all Harvard org_tree organizations will be processed.
4848
:type organizations: list, optional
4949
50+
:param user: Limit billing to this year. If not set, all users will be processed.
51+
:type user: :class:`~ifxuser.models.IfxUser`, optional
52+
5053
:param recalculate: If set to True, will delete existing :class:`~ifxbilling.models.BillingRecord` objects
5154
:type recalculate: bool, optional
5255
@@ -66,15 +69,15 @@ def calculate_billing_month(self, year, month, organizations=None, recalculate=F
6669

6770
results = {}
6871
for organization in organizations_to_process:
69-
result = self.generate_billing_records_for_organization(year, month, organization, recalculate)
72+
result = self.generate_billing_records_for_organization(year, month, organization, user, recalculate)
7073
results[organization.name] = result
7174

7275
if year == 2023 and (month == 3 or month == 4):
7376
adjust.march_april_2023_dr()
7477

7578
return Resultinator(results)
7679

77-
def generate_billing_records_for_organization(self, year, month, organization, recalculate, **kwargs):
80+
def generate_billing_records_for_organization(self, year, month, organization, user, recalculate, **kwargs):
7881
'''
7982
Create and save all of the :class:`~ifxbilling.models.BillingRecord` objects for the month for an organization.
8083
@@ -102,6 +105,9 @@ def generate_billing_records_for_organization(self, year, month, organization, r
102105
:param organization: The organization whose :class:`~ifxbilling.models.BillingRecord` objects should be generated
103106
:type organization: list
104107
108+
:param user: Limit billing to this user. If not set, all users will be processed.
109+
:type user: :class:`~ifxuser.models.IfxUser`
110+
105111
:param recalculate: If True, will delete existing :class:`~ifxbilling.models.BillingRecord` objects if possible
106112
:type recalculate: bool
107113
@@ -587,7 +593,8 @@ def generate_billing_records_for_allocation_user(self, year, month, user, organi
587593

588594
if BillingRecord.objects.filter(product_usage=product_usage).exists():
589595
if recalculate:
590-
BillingRecord.objects.filter(product_usage=product_usage).delete()
596+
for br in BillingRecord.objects.filter(product_usage=product_usage):
597+
br.delete()
591598
else:
592599
msg = f'Billing record already exists for usage {product_usage}'
593600
raise Exception(msg)
@@ -733,6 +740,24 @@ def get_errors_by_organization(self, organization_name=None):
733740
errors_by_lab[lab] = output[1]
734741
return errors_by_lab
735742

743+
def get_other_errors_by_organization(self, organization_name=None):
744+
'''
745+
Return dict of all of the "Other" errors keyed by lab
746+
'''
747+
errors_by_lab = {}
748+
for lab, output in self.results.items():
749+
if output[1] and 'No project' not in output[1][0]:
750+
if organization_name is None or lab == organization_name:
751+
for error in output[1]:
752+
for error_type, regex in self.error_types.items():
753+
if error_type == 'Other' and re.search(regex, error):
754+
if lab not in errors_by_lab:
755+
errors_by_lab[lab] = []
756+
errors_by_lab[lab].append(error)
757+
elif re.search(regex, error):
758+
break
759+
return errors_by_lab
760+
736761
def get_successes_by_organization(self, organization_name=None):
737762
'''
738763
Return dict of successes keyed by lab
@@ -771,3 +796,59 @@ def get_organizations_by_error_type(self):
771796
errors_by_type[error_type].append(lab)
772797
break
773798
return errors_by_type
799+
800+
801+
class ColdfrontRebalance(Rebalance):
802+
'''
803+
Coldfront Rebalance. Does not do a user-specific rebalance, but rather the entire organization so that offer letter reprocessing is done.
804+
'''
805+
806+
def get_recalculate_body(self, user, account_data):
807+
'''
808+
Get the body of the recalculate POST
809+
'''
810+
if not account_data or not len(account_data):
811+
raise Exception('No account data provided')
812+
813+
# Figure out the organization that needs to be rebalanced from the account_data
814+
organization = None
815+
try:
816+
account = Account.objects.filter(ifxacct=account_data[0]['account']).first()
817+
organization = account.organization
818+
except Account.DoesNotExist:
819+
raise Exception(f'Account {account_data[0]["account"]} not found')
820+
821+
return {
822+
'recalculate': False,
823+
'user_ifxorg': organization.ifxorg,
824+
}
825+
826+
def remove_billing_records(self, user, account_data):
827+
'''
828+
Remove the billing records for the given facility, year, month, and organization (as determined by the account_data)
829+
Need to clear out the whole org so that offer letter allocations can be properly credited
830+
'''
831+
if not account_data or not len(account_data):
832+
raise Exception('No account data provided')
833+
834+
# Figure out the organization that needs to be rebalanced from the account_data
835+
organization = None
836+
try:
837+
account = Account.objects.filter(ifxacct=account_data[0]['account']).first()
838+
organization = account.organization
839+
except Account.DoesNotExist:
840+
raise Exception(f'Account {account_data[0]["account"]} not found')
841+
842+
if not organization:
843+
raise Exception(f'Organization not found for account {account_data[0]["account"]}')
844+
845+
# Remove the billing records for the organization
846+
billing_records = BillingRecord.objects.filter(
847+
product_usage__product__facility=self.facility,
848+
account__organization=organization,
849+
year=self.year,
850+
month=self.month,
851+
).exclude(current_state='FINAL')
852+
853+
for br in billing_records:
854+
br.delete()

coldfront/plugins/ifx/templates/plugins/ifx/calculate_billing_month.html

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@
138138

139139
$.ajax({
140140
contentType: 'application/json',
141-
url: `/ifx/api/calculate-billing-month/${year}/${month}/`,
141+
url: `/ifx/api/billing/calculate-billing-month/RC/${year}/${month}/`,
142142
method: 'POST',
143143
headers: {'X-CSRFToken': '{{ csrf_token }}'},
144144
data: data,
@@ -161,9 +161,10 @@
161161
error: function (jqXHR, status, error) {
162162
alert(status + ' ' + error)
163163
},
164-
}).success(
165-
alert('Update started. You will receive an email when the update is complete.')
166-
)
164+
success: function () {
165+
alert('Update started. You will receive an email when the update is complete.')
166+
}
167+
})
167168
})
168169
})
169170
})(jQuery)

coldfront/plugins/ifx/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
path('api/billing/get-summary-by-account/', ifxbilling_views.get_summary_by_account),
2222
path('api/billing/get-pending-year-month/<str:invoice_prefix>/', ifxbilling_views.get_pending_year_month),
2323
path('api/billing/rebalance/', ifxbilling_views.rebalance),
24-
path('api/calculate-billing-month/<int:year>/<int:month>/', calculate_billing_month, name='calculate-billing-month'),
24+
path('api/billing/calculate-billing-month/<str:invoice_prefix>/<int:year>/<int:month>/', calculate_billing_month, name='calculate-billing-month'),
2525
path('api/run-report/', run_report),
2626
path('api/get-org-names/', get_org_names, name='get-org-names'),
2727
path('api/get-product-usages/', get_product_usages, name='get-product-usages'),

coldfront/plugins/ifx/views.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,31 +94,45 @@ def get_billing_record_list(request):
9494
raise PermissionDenied
9595
return ifxbilling_get_billing_record_list(request._request)
9696

97-
@login_required
9897
@api_view(['POST',])
9998
@permission_classes([AdminPermissions,])
100-
def calculate_billing_month(request, year, month):
99+
def calculate_billing_month(request, invoice_prefix, year, month):
101100
'''
102101
Calculate billing month view
103102
'''
104103
logger.error('Calculating billing records for month %d of year %d', month, year)
105104
recalculate = False
105+
user_ifxorg = None
106106
try:
107107
data = request.data
108-
logger.error('Request data: %s', data)
109108
recalculate = data.get('recalculate') and data['recalculate'].lower() == 'true'
109+
if data and 'user_ifxorg' in data:
110+
user_ifxorg = data['user_ifxorg']
110111
except Exception as e:
111112
logger.exception(e)
112113
return Response(data={'error': 'Cannot parse request body'}, status=status.HTTP_400_BAD_REQUEST)
113114

114115
logger.debug('Calculating billing records for month %d of year %d, with recalculate flag %s', month, year, str(recalculate))
115116

116117
try:
118+
organizations = ifxuser_models.Organization.objects.filter(org_tree='Harvard')
119+
if user_ifxorg:
120+
organizations = [ifxuser_models.Organization.objects.get(ifxorg=user_ifxorg)]
121+
117122
if recalculate:
118-
ifxbilling_models.BillingRecord.objects.filter(year=year, month=month).delete()
123+
for br in ifxbilling_models.BillingRecord.objects.filter(year=year, month=month):
124+
br.delete()
119125
ifxbilling_models.ProductUsageProcessing.objects.filter(product_usage__year=year, product_usage__month=month).delete()
120126
calculator = NewColdfrontBillingCalculator()
121-
calculator.calculate_billing_month(year, month, recalculate=recalculate)
127+
resultinator = calculator.calculate_billing_month(year, month, organizations=organizations, recalculate=recalculate)
128+
successes = 0
129+
errors = []
130+
for org, result in resultinator.results.items():
131+
if len(result[0]):
132+
successes += len(result[0])
133+
errors = [v[0] for v in resultinator.get_other_errors_by_organization().values()]
134+
135+
return Response(data={ 'successes': successes, 'errors': errors }, status=status.HTTP_200_OK)
122136
return Response('OK', status=status.HTTP_200_OK)
123137
# pylint: disable=broad-exception-caught
124138
except Exception as e:

ifxbilling

ifxurls

ifxuser

0 commit comments

Comments
 (0)