Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rebalance #350

Merged
merged 6 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading