Skip to content

Commit c56d313

Browse files
committed
Allow invoice subclasses to explicitly declare their exported columns and column names
Two class attributes, `export_columns_list` and `exported_columns_map`, has been added to `Invoice`, along with a class function `_filter_columns()`. Subclasses of `Invoice` must now define `export_columns_list`, containing the ordered list of columns that must be exported in their respective invoices. Subclasses can optional define `exported_columns_map`, containing mappings between "internal" column names and what their name should be when exported. The field name `RATE_FIELD` has been added to `invoice.py`. It was previously forgotten.
1 parent 87b6b87 commit c56d313

9 files changed

+122
-33
lines changed

process_report/invoices/NERC_total_invoice.py

+18
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,24 @@ class NERCTotalInvoice(invoice.Invoice):
1212
"University of Rhode Island",
1313
]
1414

15+
export_columns_list = [
16+
invoice.INVOICE_DATE_FIELD,
17+
invoice.PROJECT_FIELD,
18+
invoice.PROJECT_ID_FIELD,
19+
invoice.PI_FIELD,
20+
invoice.INVOICE_EMAIL_FIELD,
21+
invoice.INVOICE_ADDRESS_FIELD,
22+
invoice.INSTITUTION_FIELD,
23+
invoice.INSTITUTION_ID_FIELD,
24+
invoice.SU_HOURS_FIELD,
25+
invoice.SU_TYPE_FIELD,
26+
invoice.RATE_FIELD,
27+
invoice.COST_FIELD,
28+
invoice.CREDIT_FIELD,
29+
invoice.CREDIT_CODE_FIELD,
30+
invoice.BALANCE_FIELD,
31+
]
32+
1533
@property
1634
def output_path(self) -> str:
1735
return f"NERC-{self.invoice_month}-Total-Invoice.csv"

process_report/invoices/billable_invoice.py

+18
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,24 @@
1414
class BillableInvoice(invoice.Invoice):
1515
PI_S3_FILEPATH = "PIs/PI.csv"
1616

17+
export_columns_list = [
18+
invoice.INVOICE_DATE_FIELD,
19+
invoice.PROJECT_FIELD,
20+
invoice.PROJECT_ID_FIELD,
21+
invoice.PI_FIELD,
22+
invoice.INVOICE_EMAIL_FIELD,
23+
invoice.INVOICE_ADDRESS_FIELD,
24+
invoice.INSTITUTION_FIELD,
25+
invoice.INSTITUTION_ID_FIELD,
26+
invoice.SU_HOURS_FIELD,
27+
invoice.SU_TYPE_FIELD,
28+
invoice.RATE_FIELD,
29+
invoice.COST_FIELD,
30+
invoice.CREDIT_FIELD,
31+
invoice.CREDIT_CODE_FIELD,
32+
invoice.BALANCE_FIELD,
33+
]
34+
1735
old_pi_filepath: str
1836
updated_old_pi_df: pandas.DataFrame
1937

process_report/invoices/bu_internal_invoice.py

+20-13
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,23 @@
55

66
@dataclass
77
class BUInternalInvoice(invoice.Invoice):
8+
export_columns_list = [
9+
invoice.INVOICE_DATE_FIELD,
10+
invoice.PI_FIELD,
11+
"Project",
12+
invoice.COST_FIELD,
13+
invoice.CREDIT_FIELD,
14+
invoice.BU_BALANCE_FIELD,
15+
invoice.PI_BALANCE_FIELD,
16+
]
17+
exported_columns_map = {
18+
invoice.BU_BALANCE_FIELD: "Subsidy",
19+
invoice.PI_BALANCE_FIELD: "Balance",
20+
}
21+
822
subsidy_amount: int
923

1024
def _prepare_export(self):
11-
self.data = self.data[
12-
[
13-
invoice.INVOICE_DATE_FIELD,
14-
invoice.PI_FIELD,
15-
"Project",
16-
invoice.COST_FIELD,
17-
invoice.CREDIT_FIELD,
18-
invoice.BU_BALANCE_FIELD,
19-
invoice.BALANCE_FIELD,
20-
]
21-
]
22-
2325
self.data = self._sum_project_allocations(self.data)
2426

2527
def _sum_project_allocations(self, dataframe):
@@ -28,7 +30,12 @@ def _sum_project_allocations(self, dataframe):
2830
each unique project, summing up its allocations' costs"""
2931
project_list = dataframe["Project"].unique()
3032
data_no_dup = dataframe.drop_duplicates("Project", inplace=False)
31-
sum_fields = [invoice.COST_FIELD, invoice.CREDIT_FIELD, invoice.BALANCE_FIELD]
33+
sum_fields = [
34+
invoice.COST_FIELD,
35+
invoice.CREDIT_FIELD,
36+
invoice.BU_BALANCE_FIELD,
37+
invoice.PI_BALANCE_FIELD,
38+
]
3239
for project in project_list:
3340
project_mask = dataframe["Project"] == project
3441
no_dup_project_mask = data_no_dup["Project"] == project

process_report/invoices/invoice.py

+11
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
INSTITUTION_ID_FIELD = "Institution - Specific Code"
2424
SU_HOURS_FIELD = "SU Hours (GBhr or SUhr)"
2525
SU_TYPE_FIELD = "SU Type"
26+
RATE_FIELD = "Rate"
2627
COST_FIELD = "Cost"
2728
CREDIT_FIELD = "Credit"
2829
CREDIT_CODE_FIELD = "Credit Code"
@@ -38,6 +39,9 @@
3839

3940
@dataclass
4041
class Invoice:
42+
export_columns_list = list()
43+
exported_columns_map = dict()
44+
4145
name: str
4246
invoice_month: str
4347
data: pandas.DataFrame
@@ -83,7 +87,14 @@ def _prepare_export(self):
8387
that should or should not be exported after processing."""
8488
pass
8589

90+
def _filter_columns(self):
91+
"""Filters and renames columns before exporting"""
92+
self.data = self.data[self.export_columns_list].rename(
93+
columns=self.exported_columns_map
94+
)
95+
8696
def export(self):
97+
self._filter_columns()
8798
self.data.to_csv(self.output_path, index=False)
8899

89100
def export_s3(self, s3_bucket):

process_report/invoices/lenovo_invoice.py

+11-11
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,17 @@
77
class LenovoInvoice(invoice.Invoice):
88
LENOVO_SU_TYPES = ["OpenShift GPUA100SXM4", "OpenStack GPUA100SXM4"]
99

10+
export_columns_list = [
11+
invoice.INVOICE_DATE_FIELD,
12+
invoice.PROJECT_FIELD,
13+
invoice.INSTITUTION_FIELD,
14+
invoice.SU_HOURS_FIELD,
15+
invoice.SU_TYPE_FIELD,
16+
"Charge"
17+
]
18+
exported_columns_map = {invoice.SU_HOURS_FIELD: "SU Hours"}
19+
1020
def _prepare_export(self):
1121
self.data = self.data[
1222
self.data[invoice.SU_TYPE_FIELD].isin(self.LENOVO_SU_TYPES)
13-
][
14-
[
15-
invoice.INVOICE_DATE_FIELD,
16-
invoice.PROJECT_FIELD,
17-
invoice.INSTITUTION_FIELD,
18-
invoice.SU_HOURS_FIELD,
19-
invoice.SU_TYPE_FIELD,
20-
]
21-
].copy()
22-
23-
self.data.rename(columns={invoice.SU_HOURS_FIELD: "SU Hours"}, inplace=True)
23+
]

process_report/invoices/nonbillable_invoice.py

+15
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,21 @@ class NonbillableInvoice(invoice.Invoice):
88
nonbillable_pis: list[str]
99
nonbillable_projects: list[str]
1010

11+
export_columns_list = [
12+
invoice.INVOICE_DATE_FIELD,
13+
invoice.PROJECT_FIELD,
14+
invoice.PROJECT_ID_FIELD,
15+
invoice.PI_FIELD,
16+
invoice.INVOICE_EMAIL_FIELD,
17+
invoice.INVOICE_ADDRESS_FIELD,
18+
invoice.INSTITUTION_FIELD,
19+
invoice.INSTITUTION_ID_FIELD,
20+
invoice.SU_HOURS_FIELD,
21+
invoice.SU_TYPE_FIELD,
22+
invoice.RATE_FIELD,
23+
invoice.COST_FIELD,
24+
]
25+
1126
def _prepare_export(self):
1227
self.data = self.data[
1328
self.data[invoice.PI_FIELD].isin(self.nonbillable_pis)

process_report/process_report.py

+8-9
Original file line numberDiff line numberDiff line change
@@ -232,15 +232,15 @@ def main():
232232
nonbillable_inv = nonbillable_invoice.NonbillableInvoice(
233233
name=args.nonbillable_file,
234234
invoice_month=invoice_month,
235-
data=add_institute_proc.data,
235+
data=lenovo_proc.data,
236236
nonbillable_pis=pi,
237237
nonbillable_projects=projects,
238238
)
239239

240240
### Perform main processing
241241

242242
remove_nonbillables_proc = remove_nonbillables_processor.RemoveNonbillables(
243-
"", invoice_month, add_institute_proc.data, pi, projects
243+
"", invoice_month, lenovo_proc.data, pi, projects
244244
)
245245
remove_nonbillables_proc.process()
246246

@@ -272,19 +272,15 @@ def main():
272272
billable_inv = billable_invoice.BillableInvoice(
273273
name=args.output_file,
274274
invoice_month=invoice_month,
275-
data=new_pi_credit_proc.data,
275+
data=bu_subsidy_proc.data,
276276
old_pi_filepath=old_pi_file,
277277
updated_old_pi_df=new_pi_credit_proc.updated_old_pi_df,
278278
)
279279

280-
process_and_export_invoices(
281-
[lenovo_inv, nonbillable_inv, billable_inv], args.upload_to_s3
282-
)
283-
284280
nerc_total_inv = NERC_total_invoice.NERCTotalInvoice(
285281
name=args.NERC_total_invoice_file,
286282
invoice_month=invoice_month,
287-
data=new_pi_credit_proc.data.copy(),
283+
data=bu_subsidy_proc.data.copy(),
288284
)
289285

290286
bu_internal_inv = bu_internal_invoice.BUInternalInvoice(
@@ -294,7 +290,10 @@ def main():
294290
subsidy_amount=args.BU_subsidy_amount,
295291
)
296292

297-
process_and_export_invoices([nerc_total_inv, bu_internal_inv], args.upload_to_s3)
293+
process_and_export_invoices(
294+
[lenovo_inv, nonbillable_inv, billable_inv, nerc_total_inv, bu_internal_inv],
295+
args.upload_to_s3,
296+
)
298297

299298
export_pi_billables(billable_inv.data.copy(), args.output_folder, invoice_month)
300299

process_report/tests/unit_tests.py

+12
Original file line numberDiff line numberDiff line change
@@ -907,3 +907,15 @@ def test_remove_prefix(self, mock_get_time, mock_get_bucket):
907907
process_report.upload_to_s3(filenames, invoice_month)
908908
for i, call_args in enumerate(mock_bucket.upload_file.call_args_list):
909909
self.assertTrue(answers[i] in call_args)
910+
911+
912+
class TestBaseInvoice(TestCase):
913+
def test_filter_exported_columns(self):
914+
test_invoice = pandas.DataFrame(columns=["C1", "C2", "C3", "C4", "C5"])
915+
answer_invoice = pandas.DataFrame(columns=["C1", "C3R", "C5R"])
916+
inv = test_utils.new_base_invoice(data=test_invoice)
917+
inv.export_columns_list = ["C1", "C3", "C5"]
918+
inv.exported_columns_map = {"C3": "C3R", "C5": "C5R"}
919+
inv._filter_columns()
920+
921+
self.assertTrue(inv.data.equals(answer_invoice))

process_report/tests/util.py

+9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
new_pi_credit_processor,
1010
bu_subsidy_processor,
1111
)
12+
from process_report.invoices import invoice
1213

1314

1415
def new_add_institution_processor(
@@ -71,3 +72,11 @@ def new_bu_subsidy_processor(
7172
return bu_subsidy_processor.BUSubsidyProcessor(
7273
name, invoice_month, data, subsidy_amount
7374
)
75+
76+
77+
def new_base_invoice(
78+
name="",
79+
invoice_month="0000-00",
80+
data=pandas.DataFrame(),
81+
):
82+
return invoice.Invoice(name, invoice_month, data)

0 commit comments

Comments
 (0)