10
10
from django .utils import timezone
11
11
from django .contrib .auth import get_user_model
12
12
from django .conf import settings
13
- from ifxbilling .calculator import BasicBillingCalculator , NewBillingCalculator
13
+ from ifxbilling .calculator import BasicBillingCalculator , NewBillingCalculator , Rebalance
14
14
from ifxbilling .models import Account , Product , ProductUsage , Rate , BillingRecord
15
15
from ifxuser .models import Organization
16
16
from coldfront .core .allocation .models import Allocation , AllocationStatusChoice
@@ -29,7 +29,7 @@ class NewColdfrontBillingCalculator(NewBillingCalculator):
29
29
STORAGE_QUOTA_ATTRIBUTE = 'Storage Quota (TB)'
30
30
STORAGE_RESOURCE_TYPE = 'Storage'
31
31
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 ):
33
33
'''
34
34
Calculate a month of billing for the given year and month
35
35
@@ -47,6 +47,9 @@ def calculate_billing_month(self, year, month, organizations=None, recalculate=F
47
47
:param organizations: List of specific organizations to process. If not set, all Harvard org_tree organizations will be processed.
48
48
:type organizations: list, optional
49
49
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
+
50
53
:param recalculate: If set to True, will delete existing :class:`~ifxbilling.models.BillingRecord` objects
51
54
:type recalculate: bool, optional
52
55
@@ -66,15 +69,15 @@ def calculate_billing_month(self, year, month, organizations=None, recalculate=F
66
69
67
70
results = {}
68
71
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 )
70
73
results [organization .name ] = result
71
74
72
75
if year == 2023 and (month == 3 or month == 4 ):
73
76
adjust .march_april_2023_dr ()
74
77
75
78
return Resultinator (results )
76
79
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 ):
78
81
'''
79
82
Create and save all of the :class:`~ifxbilling.models.BillingRecord` objects for the month for an organization.
80
83
@@ -102,6 +105,9 @@ def generate_billing_records_for_organization(self, year, month, organization, r
102
105
:param organization: The organization whose :class:`~ifxbilling.models.BillingRecord` objects should be generated
103
106
:type organization: list
104
107
108
+ :param user: Limit billing to this user. If not set, all users will be processed.
109
+ :type user: :class:`~ifxuser.models.IfxUser`
110
+
105
111
:param recalculate: If True, will delete existing :class:`~ifxbilling.models.BillingRecord` objects if possible
106
112
:type recalculate: bool
107
113
@@ -587,7 +593,8 @@ def generate_billing_records_for_allocation_user(self, year, month, user, organi
587
593
588
594
if BillingRecord .objects .filter (product_usage = product_usage ).exists ():
589
595
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 ()
591
598
else :
592
599
msg = f'Billing record already exists for usage { product_usage } '
593
600
raise Exception (msg )
@@ -733,6 +740,24 @@ def get_errors_by_organization(self, organization_name=None):
733
740
errors_by_lab [lab ] = output [1 ]
734
741
return errors_by_lab
735
742
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
+
736
761
def get_successes_by_organization (self , organization_name = None ):
737
762
'''
738
763
Return dict of successes keyed by lab
@@ -771,3 +796,59 @@ def get_organizations_by_error_type(self):
771
796
errors_by_type [error_type ].append (lab )
772
797
break
773
798
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 ()
0 commit comments