Skip to content

Commit b5ca684

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 5d3638b commit b5ca684

File tree

7 files changed

+107
-28
lines changed

7 files changed

+107
-28
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

+19
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,25 @@ class BillableInvoice(discount_invoice.DiscountInvoice):
2222

2323
nonbillable_pis: list[str]
2424
nonbillable_projects: list[str]
25+
26+
export_columns_list = [
27+
invoice.INVOICE_DATE_FIELD,
28+
invoice.PROJECT_FIELD,
29+
invoice.PROJECT_ID_FIELD,
30+
invoice.PI_FIELD,
31+
invoice.INVOICE_EMAIL_FIELD,
32+
invoice.INVOICE_ADDRESS_FIELD,
33+
invoice.INSTITUTION_FIELD,
34+
invoice.INSTITUTION_ID_FIELD,
35+
invoice.SU_HOURS_FIELD,
36+
invoice.SU_TYPE_FIELD,
37+
invoice.RATE_FIELD,
38+
invoice.COST_FIELD,
39+
invoice.CREDIT_FIELD,
40+
invoice.CREDIT_CODE_FIELD,
41+
invoice.BALANCE_FIELD,
42+
]
43+
2544
old_pi_filepath: str
2645

2746
@staticmethod

process_report/invoices/bu_internal_invoice.py

+10-11
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@
77

88
@dataclass
99
class BUInternalInvoice(discount_invoice.DiscountInvoice):
10+
export_columns_list = [
11+
invoice.INVOICE_DATE_FIELD,
12+
invoice.PI_FIELD,
13+
"Project",
14+
invoice.COST_FIELD,
15+
invoice.CREDIT_FIELD,
16+
invoice.SUBSIDY_FIELD,
17+
invoice.BALANCE_FIELD,
18+
]
19+
1020
subsidy_amount: int
1121

1222
def _prepare(self):
@@ -22,17 +32,6 @@ def get_project(row):
2232
].copy()
2333
self.data["Project"] = self.data.apply(get_project, axis=1)
2434
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-
]
3635

3736
def _process(self):
3837
data_summed_projects = self._sum_project_allocations(self.data)

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"
@@ -33,6 +34,9 @@
3334

3435
@dataclass
3536
class Invoice:
37+
export_columns_list = list()
38+
exported_columns_map = dict()
39+
3640
name: str
3741
invoice_month: str
3842
data: pandas.DataFrame
@@ -78,7 +82,14 @@ def _prepare_export(self):
7882
that should or should not be exported after processing."""
7983
pass
8084

85+
def _filter_columns(self):
86+
"""Filters and renames columns before exporting"""
87+
self.data = self.data[self.export_columns_list].rename(
88+
columns=self.exported_columns_map
89+
)
90+
8191
def export(self):
92+
self._filter_columns()
8293
self.data.to_csv(self.output_path, index=False)
8394

8495
def export_s3(self, s3_bucket):

process_report/invoices/lenovo_invoice.py

+18-15
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,24 @@ class LenovoInvoice(invoice.Invoice):
88
LENOVO_SU_TYPES = ["OpenShift GPUA100SXM4", "OpenStack GPUA100SXM4"]
99
SU_CHARGE_MULTIPLIER = 1
1010

11-
def _prepare(self):
12-
self.data = self.data[
13-
self.data[invoice.SU_TYPE_FIELD].isin(self.LENOVO_SU_TYPES)
14-
][
15-
[
16-
invoice.INVOICE_DATE_FIELD,
17-
invoice.PROJECT_FIELD,
18-
invoice.INSTITUTION_FIELD,
19-
invoice.SU_HOURS_FIELD,
20-
invoice.SU_TYPE_FIELD,
21-
]
22-
].copy()
11+
export_columns_list = [
12+
invoice.INVOICE_DATE_FIELD,
13+
invoice.PROJECT_FIELD,
14+
invoice.INSTITUTION_FIELD,
15+
invoice.SU_HOURS_FIELD,
16+
invoice.SU_TYPE_FIELD,
17+
"SU Charge",
18+
"Charge",
19+
]
20+
exported_columns_map = {invoice.SU_HOURS_FIELD: "SU Hours"}
2321

24-
self.data.rename(columns={invoice.SU_HOURS_FIELD: "SU Hours"}, inplace=True)
25-
self.data.insert(len(self.data.columns), "SU Charge", self.SU_CHARGE_MULTIPLIER)
22+
def _prepare(self):
23+
self.data["SU Charge"] = self.SU_CHARGE_MULTIPLIER
2624

2725
def _process(self):
28-
self.data["Charge"] = self.data["SU Hours"] * self.data["SU Charge"]
26+
self.data["Charge"] = self.data[invoice.SU_HOURS_FIELD] * self.data["SU Charge"]
27+
28+
def _prepare_export(self):
29+
self.data = self.data[
30+
self.data[invoice.SU_TYPE_FIELD].isin(self.LENOVO_SU_TYPES)
31+
]

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/tests/unit_tests.py

+16-2
Original file line numberDiff line numberDiff line change
@@ -773,7 +773,7 @@ def test_process_lenovo(self):
773773
process_report.PROJECT_FIELD,
774774
process_report.INSTITUTION_FIELD,
775775
process_report.SU_TYPE_FIELD,
776-
"SU Hours",
776+
process_report.SU_HOURS_FIELD,
777777
"SU Charge",
778778
"Charge",
779779
]
@@ -785,7 +785,9 @@ def test_process_lenovo(self):
785785
row[process_report.SU_TYPE_FIELD],
786786
["OpenShift GPUA100SXM4", "OpenStack GPUA100SXM4"],
787787
)
788-
self.assertEqual(row["Charge"], row["SU Charge"] * row["SU Hours"])
788+
self.assertEqual(
789+
row["Charge"], row["SU Charge"] * row["SU Hours (GBhr or SUhr)"]
790+
)
789791

790792

791793
class TestUploadToS3(TestCase):
@@ -833,3 +835,15 @@ def test_upload_to_s3(self, mock_get_time, mock_get_bucket):
833835

834836
for i, call_args in enumerate(mock_bucket.upload_file.call_args_list):
835837
self.assertTrue(answers[i] in call_args)
838+
839+
840+
class TestBaseInvoice(TestCase):
841+
def test_filter_exported_columns(self):
842+
test_invoice = pandas.DataFrame(columns=["C1", "C2", "C3", "C4", "C5"])
843+
answer_invoice = pandas.DataFrame(columns=["C1", "C3R", "C5R"])
844+
inv = test_utils.new_base_invoice(data=test_invoice)
845+
inv.export_columns_list = ["C1", "C3", "C5"]
846+
inv.exported_columns_map = {"C3": "C3R", "C5": "C5R"}
847+
inv._filter_columns()
848+
849+
self.assertTrue(inv.data.equals(answer_invoice))

0 commit comments

Comments
 (0)