Skip to content

Commit 19ca40b

Browse files
authored
Merge pull request CCI-MOC#71 from QuanMPhm/52.4/refactor_bu_internal
Refactored BU Internal Invoice
2 parents fb0be7c + eb2d471 commit 19ca40b

File tree

7 files changed

+229
-127
lines changed

7 files changed

+229
-127
lines changed

process_report/invoices/billable_invoice.py

+18-25
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
from dataclasses import dataclass
2-
from decimal import Decimal
32
import logging
43
import sys
54

65
import pandas
76
import pyarrow
87

9-
import process_report.invoices.invoice as invoice
10-
import process_report.util as util
8+
from process_report.invoices import invoice, discount_invoice
9+
from process_report import util
1110

1211

1312
logger = logging.getLogger(__name__)
1413
logging.basicConfig(level=logging.INFO)
1514

1615

1716
@dataclass
18-
class BillableInvoice(invoice.Invoice):
17+
class BillableInvoice(discount_invoice.DiscountInvoice):
1918
NEW_PI_CREDIT_CODE = "0002"
2019
INITIAL_CREDIT_AMOUNT = 1000
2120
EXCLUDE_SU_TYPES = ["OpenShift GPUA100SXM4", "OpenStack GPUA100SXM4"]
@@ -89,7 +88,7 @@ def _prepare(self):
8988
self.data = self._validate_pi_names(self.data)
9089
self.data[invoice.CREDIT_FIELD] = None
9190
self.data[invoice.CREDIT_CODE_FIELD] = None
92-
self.data[invoice.BALANCE_FIELD] = Decimal(0)
91+
self.data[invoice.BALANCE_FIELD] = self.data[invoice.COST_FIELD]
9392
self.old_pi_df = self._load_old_pis(self.old_pi_filepath)
9493

9594
def _process(self):
@@ -143,14 +142,17 @@ def get_initial_credit_amount(
143142

144143
current_pi_set = set(data[invoice.PI_FIELD])
145144
for pi in current_pi_set:
146-
pi_projects = data[data[invoice.PI_FIELD] == pi]
145+
credit_eligible_projects = data[
146+
(data[invoice.PI_FIELD] == pi)
147+
& ~(data[invoice.SU_TYPE_FIELD].isin(self.EXCLUDE_SU_TYPES))
148+
]
147149
pi_age = self._get_pi_age(old_pi_df, pi, self.invoice_month)
148150
pi_old_pi_entry = old_pi_df.loc[
149151
old_pi_df[invoice.PI_PI_FIELD] == pi
150152
].squeeze()
151153

152154
if pi_age > 1:
153-
for i, row in pi_projects.iterrows():
155+
for i, row in credit_eligible_projects.iterrows():
154156
data.at[i, invoice.BALANCE_FIELD] = row[invoice.COST_FIELD]
155157
else:
156158
if pi_age == 0:
@@ -176,25 +178,16 @@ def get_initial_credit_amount(
176178
)
177179
credit_used_field = invoice.PI_2ND_USED
178180

179-
initial_credit = remaining_credit
180-
for i, row in pi_projects.iterrows():
181-
if (
182-
remaining_credit == 0
183-
or row[invoice.SU_TYPE_FIELD] in self.EXCLUDE_SU_TYPES
184-
):
185-
data.at[i, invoice.BALANCE_FIELD] = row[invoice.COST_FIELD]
186-
else:
187-
project_cost = row[invoice.COST_FIELD]
188-
applied_credit = min(project_cost, remaining_credit)
189-
190-
data.at[i, invoice.CREDIT_FIELD] = applied_credit
191-
data.at[i, invoice.CREDIT_CODE_FIELD] = self.NEW_PI_CREDIT_CODE
192-
data.at[i, invoice.BALANCE_FIELD] = (
193-
row[invoice.COST_FIELD] - applied_credit
194-
)
195-
remaining_credit -= applied_credit
181+
credits_used = self.apply_flat_discount(
182+
data,
183+
credit_eligible_projects,
184+
remaining_credit,
185+
invoice.CREDIT_FIELD,
186+
invoice.BALANCE_FIELD,
187+
invoice.CREDIT_CODE_FIELD,
188+
self.NEW_PI_CREDIT_CODE,
189+
)
196190

197-
credits_used = initial_credit - remaining_credit
198191
if (pi_old_pi_entry[credit_used_field] != 0) and (
199192
credits_used != pi_old_pi_entry[credit_used_field]
200193
):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from dataclasses import dataclass
2+
from decimal import Decimal
3+
4+
import process_report.invoices.invoice as invoice
5+
import process_report.invoices.discount_invoice as discount_invoice
6+
7+
8+
@dataclass
9+
class BUInternalInvoice(discount_invoice.DiscountInvoice):
10+
subsidy_amount: int
11+
12+
def _prepare(self):
13+
def get_project(row):
14+
project_alloc = row[invoice.PROJECT_FIELD]
15+
if project_alloc.rfind("-") == -1:
16+
return project_alloc
17+
else:
18+
return project_alloc[: project_alloc.rfind("-")]
19+
20+
self.data = self.data[
21+
self.data[invoice.INSTITUTION_FIELD] == "Boston University"
22+
].copy()
23+
self.data["Project"] = self.data.apply(get_project, axis=1)
24+
self.data[invoice.SUBSIDY_FIELD] = Decimal(0)
25+
self.data = self.data[
26+
[
27+
invoice.INVOICE_DATE_FIELD,
28+
invoice.PI_FIELD,
29+
"Project",
30+
invoice.COST_FIELD,
31+
invoice.CREDIT_FIELD,
32+
invoice.SUBSIDY_FIELD,
33+
invoice.BALANCE_FIELD,
34+
]
35+
]
36+
37+
def _process(self):
38+
data_summed_projects = self._sum_project_allocations(self.data)
39+
self.data = self._apply_subsidy(data_summed_projects, self.subsidy_amount)
40+
41+
def _sum_project_allocations(self, dataframe):
42+
"""A project may have multiple allocations, and therefore multiple rows
43+
in the raw invoices. For BU-Internal invoice, we only want 1 row for
44+
each unique project, summing up its allocations' costs"""
45+
project_list = dataframe["Project"].unique()
46+
data_no_dup = dataframe.drop_duplicates("Project", inplace=False)
47+
sum_fields = [invoice.COST_FIELD, invoice.CREDIT_FIELD, invoice.BALANCE_FIELD]
48+
for project in project_list:
49+
project_mask = dataframe["Project"] == project
50+
no_dup_project_mask = data_no_dup["Project"] == project
51+
52+
sum_fields_sums = dataframe[project_mask][sum_fields].sum().values
53+
data_no_dup.loc[no_dup_project_mask, sum_fields] = sum_fields_sums
54+
55+
return data_no_dup
56+
57+
def _apply_subsidy(self, dataframe, subsidy_amount):
58+
pi_list = dataframe[invoice.PI_FIELD].unique()
59+
60+
for pi in pi_list:
61+
pi_projects = dataframe[dataframe[invoice.PI_FIELD] == pi]
62+
self.apply_flat_discount(
63+
dataframe,
64+
pi_projects,
65+
subsidy_amount,
66+
invoice.SUBSIDY_FIELD,
67+
invoice.BALANCE_FIELD,
68+
)
69+
70+
return dataframe
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from dataclasses import dataclass
2+
3+
import pandas
4+
5+
import process_report.invoices.invoice as invoice
6+
7+
8+
@dataclass
9+
class DiscountInvoice(invoice.Invoice):
10+
"""
11+
Invoice class containing functions useful for applying discounts
12+
on dataframes
13+
"""
14+
15+
@staticmethod
16+
def apply_flat_discount(
17+
invoice: pandas.DataFrame,
18+
pi_projects: pandas.DataFrame,
19+
discount_amount: int,
20+
discount_field: str,
21+
balance_field: str,
22+
code_field: str = None,
23+
discount_code: str = None,
24+
):
25+
"""
26+
Takes in an invoice and a list of PI projects that are a subset of it,
27+
and applies a flat discount to those PI projects. Note that this function
28+
will change the provided `invoice` Dataframe directly. Therefore, it does
29+
not return the changed invoice.
30+
31+
This function assumes that the balance field shows the remaining cost of the project,
32+
or what the PI would pay before the flat discount is applied.
33+
34+
If the optional parameters `code_field` and `discount_code` are passed in,
35+
`discount_code` will be comma-APPENDED to the `code_field` of projects where
36+
the discount is applied
37+
38+
Returns the amount of discount used.
39+
40+
:param invoice: Dataframe containing all projects
41+
:param pi_projects: A subset of `invoice`, containing all projects for a PI you want to apply the discount
42+
:param discount_amount: The discount given to the PI
43+
:param discount_field: Name of the field to put the discount amount applied to each project
44+
:param balance_field: Name of the balance field
45+
:param code_field: Name of the discount code field
46+
:param discount_code: Code of the discount
47+
"""
48+
49+
def apply_discount_on_project(remaining_discount_amount, project_i, project):
50+
remaining_project_balance = project[balance_field]
51+
applied_discount = min(remaining_project_balance, remaining_discount_amount)
52+
invoice.at[project_i, discount_field] = applied_discount
53+
invoice.at[project_i, balance_field] = (
54+
project[balance_field] - applied_discount
55+
)
56+
remaining_discount_amount -= applied_discount
57+
return remaining_discount_amount
58+
59+
def apply_credit_code_on_project(project_i):
60+
if code_field and discount_code:
61+
if pandas.isna(invoice.at[project_i, code_field]):
62+
invoice.at[project_i, code_field] = discount_code
63+
else:
64+
invoice.at[project_i, code_field] = (
65+
invoice.at[project_i, code_field] + "," + discount_code
66+
)
67+
68+
remaining_discount_amount = discount_amount
69+
for i, row in pi_projects.iterrows():
70+
if remaining_discount_amount == 0:
71+
break
72+
else:
73+
remaining_discount_amount = apply_discount_on_project(
74+
remaining_discount_amount, i, row
75+
)
76+
apply_credit_code_on_project(i)
77+
78+
discount_used = discount_amount - remaining_discount_amount
79+
return discount_used

0 commit comments

Comments
 (0)