Skip to content

Commit

Permalink
Merge pull request #350 from fasrc/ajk_rebalance
Browse files Browse the repository at this point in the history
Rebalance
  • Loading branch information
aaronk authored Dec 6, 2024
2 parents 1cdd9c2 + fef4c94 commit 8be3100
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 18 deletions.
7 changes: 7 additions & 0 deletions coldfront/config/plugins/ifx.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ class GROUPS():
class RATES():
INTERNAL_RATE_NAME = 'Harvard Internal Rate'

class EMAILS():
DEFAULT_EMAIL_FROM_ADDRESS = '[email protected]'

# Ignore billing models in the django-author pre-save so that values are set directly
AUTHOR_IGNORE_MODELS = [
'ifxbilling.BillingRecord',
Expand All @@ -38,3 +41,7 @@ class RATES():

IFXREPORT_FILE_ROOT = os.path.join(MEDIA_ROOT, 'reports')
IFXREPORT_URL_ROOT = f'{MEDIA_URL}reports'

# Class to be used for rebalancing
REBALANCER_CLASS = 'coldfront.plugins.ifx.calculator.ColdfrontRebalance'

91 changes: 86 additions & 5 deletions coldfront/plugins/ifx/calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from django.utils import timezone
from django.contrib.auth import get_user_model
from django.conf import settings
from ifxbilling.calculator import BasicBillingCalculator, NewBillingCalculator
from ifxbilling.calculator import BasicBillingCalculator, NewBillingCalculator, Rebalance
from ifxbilling.models import Account, Product, ProductUsage, Rate, BillingRecord
from ifxuser.models import Organization
from coldfront.core.allocation.models import Allocation, AllocationStatusChoice
Expand All @@ -29,7 +29,7 @@ class NewColdfrontBillingCalculator(NewBillingCalculator):
STORAGE_QUOTA_ATTRIBUTE = 'Storage Quota (TB)'
STORAGE_RESOURCE_TYPE = 'Storage'

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

results = {}
for organization in organizations_to_process:
result = self.generate_billing_records_for_organization(year, month, organization, recalculate)
result = self.generate_billing_records_for_organization(year, month, organization, user, recalculate)
results[organization.name] = result

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

return Resultinator(results)

def generate_billing_records_for_organization(self, year, month, organization, recalculate, **kwargs):
def generate_billing_records_for_organization(self, year, month, organization, user, recalculate, **kwargs):
'''
Create and save all of the :class:`~ifxbilling.models.BillingRecord` objects for the month for an organization.
Expand Down Expand Up @@ -102,6 +105,9 @@ def generate_billing_records_for_organization(self, year, month, organization, r
:param organization: The organization whose :class:`~ifxbilling.models.BillingRecord` objects should be generated
:type organization: list
:param user: Limit billing to this user. If not set, all users will be processed.
:type user: :class:`~ifxuser.models.IfxUser`
:param recalculate: If True, will delete existing :class:`~ifxbilling.models.BillingRecord` objects if possible
:type recalculate: bool
Expand Down Expand Up @@ -587,7 +593,8 @@ def generate_billing_records_for_allocation_user(self, year, month, user, organi

if BillingRecord.objects.filter(product_usage=product_usage).exists():
if recalculate:
BillingRecord.objects.filter(product_usage=product_usage).delete()
for br in BillingRecord.objects.filter(product_usage=product_usage):
br.delete()
else:
msg = f'Billing record already exists for usage {product_usage}'
raise Exception(msg)
Expand Down Expand Up @@ -733,6 +740,24 @@ def get_errors_by_organization(self, organization_name=None):
errors_by_lab[lab] = output[1]
return errors_by_lab

def get_other_errors_by_organization(self, organization_name=None):
'''
Return dict of all of the "Other" errors keyed by lab
'''
errors_by_lab = {}
for lab, output in self.results.items():
if output[1] and 'No project' not in output[1][0]:
if organization_name is None or lab == organization_name:
for error in output[1]:
for error_type, regex in self.error_types.items():
if error_type == 'Other' and re.search(regex, error):
if lab not in errors_by_lab:
errors_by_lab[lab] = []
errors_by_lab[lab].append(error)
elif re.search(regex, error):
break
return errors_by_lab

def get_successes_by_organization(self, organization_name=None):
'''
Return dict of successes keyed by lab
Expand Down Expand Up @@ -771,3 +796,59 @@ def get_organizations_by_error_type(self):
errors_by_type[error_type].append(lab)
break
return errors_by_type


class ColdfrontRebalance(Rebalance):
'''
Coldfront Rebalance. Does not do a user-specific rebalance, but rather the entire organization so that offer letter reprocessing is done.
'''

def get_recalculate_body(self, user, account_data):
'''
Get the body of the recalculate POST
'''
if not account_data or not len(account_data):
raise Exception('No account data provided')

# Figure out the organization that needs to be rebalanced from the account_data
organization = None
try:
account = Account.objects.filter(ifxacct=account_data[0]['account']).first()
organization = account.organization
except Account.DoesNotExist:
raise Exception(f'Account {account_data[0]["account"]} not found')

return {
'recalculate': False,
'user_ifxorg': organization.ifxorg,
}

def remove_billing_records(self, user, account_data):
'''
Remove the billing records for the given facility, year, month, and organization (as determined by the account_data)
Need to clear out the whole org so that offer letter allocations can be properly credited
'''
if not account_data or not len(account_data):
raise Exception('No account data provided')

# Figure out the organization that needs to be rebalanced from the account_data
organization = None
try:
account = Account.objects.filter(ifxacct=account_data[0]['account']).first()
organization = account.organization
except Account.DoesNotExist:
raise Exception(f'Account {account_data[0]["account"]} not found')

if not organization:
raise Exception(f'Organization not found for account {account_data[0]["account"]}')

# Remove the billing records for the organization
billing_records = BillingRecord.objects.filter(
product_usage__product__facility=self.facility,
account__organization=organization,
year=self.year,
month=self.month,
).exclude(current_state='FINAL')

for br in billing_records:
br.delete()
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@

$.ajax({
contentType: 'application/json',
url: `/ifx/api/calculate-billing-month/${year}/${month}/`,
url: `/ifx/api/billing/calculate-billing-month/RC/${year}/${month}/`,
method: 'POST',
headers: {'X-CSRFToken': '{{ csrf_token }}'},
data: data,
Expand All @@ -161,9 +161,10 @@
error: function (jqXHR, status, error) {
alert(status + ' ' + error)
},
}).success(
alert('Update started. You will receive an email when the update is complete.')
)
success: function () {
alert('Update started. You will receive an email when the update is complete.')
}
})
})
})
})(jQuery)
Expand Down
2 changes: 1 addition & 1 deletion coldfront/plugins/ifx/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
path('api/billing/get-summary-by-account/', ifxbilling_views.get_summary_by_account),
path('api/billing/get-pending-year-month/<str:invoice_prefix>/', ifxbilling_views.get_pending_year_month),
path('api/billing/rebalance/', ifxbilling_views.rebalance),
path('api/calculate-billing-month/<int:year>/<int:month>/', calculate_billing_month, name='calculate-billing-month'),
path('api/billing/calculate-billing-month/<str:invoice_prefix>/<int:year>/<int:month>/', calculate_billing_month, name='calculate-billing-month'),
path('api/run-report/', run_report),
path('api/get-org-names/', get_org_names, name='get-org-names'),
path('api/get-product-usages/', get_product_usages, name='get-product-usages'),
Expand Down
24 changes: 19 additions & 5 deletions coldfront/plugins/ifx/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,31 +94,45 @@ def get_billing_record_list(request):
raise PermissionDenied
return ifxbilling_get_billing_record_list(request._request)

@login_required
@api_view(['POST',])
@permission_classes([AdminPermissions,])
def calculate_billing_month(request, year, month):
def calculate_billing_month(request, invoice_prefix, year, month):
'''
Calculate billing month view
'''
logger.error('Calculating billing records for month %d of year %d', month, year)
recalculate = False
user_ifxorg = None
try:
data = request.data
logger.error('Request data: %s', data)
recalculate = data.get('recalculate') and data['recalculate'].lower() == 'true'
if data and 'user_ifxorg' in data:
user_ifxorg = data['user_ifxorg']
except Exception as e:
logger.exception(e)
return Response(data={'error': 'Cannot parse request body'}, status=status.HTTP_400_BAD_REQUEST)

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

try:
organizations = ifxuser_models.Organization.objects.filter(org_tree='Harvard')
if user_ifxorg:
organizations = [ifxuser_models.Organization.objects.get(ifxorg=user_ifxorg)]

if recalculate:
ifxbilling_models.BillingRecord.objects.filter(year=year, month=month).delete()
for br in ifxbilling_models.BillingRecord.objects.filter(year=year, month=month):
br.delete()
ifxbilling_models.ProductUsageProcessing.objects.filter(product_usage__year=year, product_usage__month=month).delete()
calculator = NewColdfrontBillingCalculator()
calculator.calculate_billing_month(year, month, recalculate=recalculate)
resultinator = calculator.calculate_billing_month(year, month, organizations=organizations, recalculate=recalculate)
successes = 0
errors = []
for org, result in resultinator.results.items():
if len(result[0]):
successes += len(result[0])
errors = [v[0] for v in resultinator.get_other_errors_by_organization().values()]

return Response(data={ 'successes': successes, 'errors': errors }, status=status.HTTP_200_OK)
return Response('OK', status=status.HTTP_200_OK)
# pylint: disable=broad-exception-caught
except Exception as e:
Expand Down
2 changes: 1 addition & 1 deletion ifxbilling
2 changes: 1 addition & 1 deletion ifxurls
Submodule ifxurls updated 1 files
+62 −3 ifxurls/urls.py
2 changes: 1 addition & 1 deletion ifxuser

0 comments on commit 8be3100

Please sign in to comment.