Skip to content

Commit d1e623a

Browse files
committed
Refactored PI-specific invoices
The PI-specific is now implemented in the `PIInvoice` class. This class takes in the entire billable invoice and will export the PI invoices to local storage and S3 Because the `upload_to_s3` function is no longer used, it is removed. Since the `export_s3` function in the Invoice class does not need to be concerned about file extensions, the test case for s3 exporting is somewhat simplified, only checking that the format of the output paths are correct.
1 parent 19ca40b commit d1e623a

File tree

4 files changed

+99
-55
lines changed

4 files changed

+99
-55
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import os
2+
from dataclasses import dataclass
3+
4+
import pandas
5+
6+
import process_report.invoices.invoice as invoice
7+
import process_report.util as util
8+
9+
10+
@dataclass
11+
class PIInvoice(invoice.Invoice):
12+
def _prepare(self):
13+
self.pi_list = self.data[invoice.PI_FIELD].unique()
14+
15+
def export(self):
16+
def _export_pi_invoice(pi):
17+
if pandas.isna(pi):
18+
return
19+
pi_projects = self.data[self.data[invoice.PI_FIELD] == pi]
20+
pi_instituition = pi_projects[invoice.INSTITUTION_FIELD].iat[0]
21+
pi_projects.to_csv(
22+
f"{self.name}/{pi_instituition}_{pi} {self.invoice_month}.csv"
23+
)
24+
25+
if not os.path.exists(
26+
self.name
27+
): # self.name is name of folder storing invoices
28+
os.mkdir(self.name)
29+
30+
for pi in self.pi_list:
31+
_export_pi_invoice(pi)
32+
33+
def export_s3(self, s3_bucket):
34+
def _export_s3_pi_invoice(pi_invoice):
35+
pi_invoice_path = os.path.join(self.name, pi_invoice)
36+
striped_invoice_path = os.path.splitext(pi_invoice_path)[0]
37+
output_s3_path = f"Invoices/{self.invoice_month}/{striped_invoice_path}.csv"
38+
output_s3_archive_path = f"Invoices/{self.invoice_month}/Archive/{striped_invoice_path} {util.get_iso8601_time()}.csv"
39+
s3_bucket.upload_file(pi_invoice_path, output_s3_path)
40+
s3_bucket.upload_file(pi_invoice_path, output_s3_archive_path)
41+
42+
for pi_invoice in os.listdir(self.name):
43+
_export_s3_pi_invoice(pi_invoice)

process_report/process_report.py

+5-38
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
billable_invoice,
1515
NERC_total_invoice,
1616
bu_internal_invoice,
17+
pi_specific_invoice,
1718
)
1819

1920

@@ -262,17 +263,11 @@ def main():
262263
subsidy_amount=args.BU_subsidy_amount,
263264
)
264265

265-
process_and_export_invoices([nerc_total_inv, bu_internal_inv], args.upload_to_s3)
266-
267-
export_pi_billables(billable_inv.data.copy(), args.output_folder, invoice_month)
268-
269-
if args.upload_to_s3:
270-
invoice_list = list()
271-
272-
for pi_invoice in os.listdir(args.output_folder):
273-
invoice_list.append(os.path.join(args.output_folder, pi_invoice))
266+
pi_inv = pi_specific_invoice.PIInvoice(
267+
name=args.output_folder, invoice_month=invoice_month, data=billable_inv.data
268+
)
274269

275-
upload_to_s3(invoice_list, invoice_month)
270+
process_and_export_invoices([nerc_total_inv, bu_internal_inv, pi_inv], args.upload_to_s3)
276271

277272

278273
def fetch_s3_invoices(invoice_month):
@@ -390,33 +385,5 @@ def export_billables(dataframe, output_file):
390385
dataframe.to_csv(output_file, index=False)
391386

392387

393-
def export_pi_billables(dataframe: pandas.DataFrame, output_folder, invoice_month):
394-
if not os.path.exists(output_folder):
395-
os.mkdir(output_folder)
396-
397-
pi_list = dataframe[PI_FIELD].unique()
398-
399-
for pi in pi_list:
400-
if pandas.isna(pi):
401-
continue
402-
pi_projects = dataframe[dataframe[PI_FIELD] == pi]
403-
pi_instituition = pi_projects[INSTITUTION_FIELD].iat[0]
404-
pi_projects.to_csv(
405-
output_folder + f"/{pi_instituition}_{pi}_{invoice_month}.csv", index=False
406-
)
407-
408-
409-
def upload_to_s3(invoice_list: list, invoice_month):
410-
invoice_bucket = get_invoice_bucket()
411-
for invoice_filename in invoice_list:
412-
striped_filename = os.path.splitext(invoice_filename)[0]
413-
invoice_s3_path = (
414-
f"Invoices/{invoice_month}/{striped_filename} {invoice_month}.csv"
415-
)
416-
invoice_s3_path_archive = f"Invoices/{invoice_month}/Archive/{striped_filename} {invoice_month} {get_iso8601_time()}.csv"
417-
invoice_bucket.upload_file(invoice_filename, invoice_s3_path)
418-
invoice_bucket.upload_file(invoice_filename, invoice_s3_path_archive)
419-
420-
421388
if __name__ == "__main__":
422389
main()

process_report/tests/unit_tests.py

+31-16
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,13 @@ def setUp(self):
182182

183183
def test_export_pi(self):
184184
output_dir = tempfile.TemporaryDirectory()
185-
process_report.export_pi_billables(
186-
self.dataframe, output_dir.name, self.invoice_month
185+
pi_inv = test_utils.new_pi_specific_invoice(
186+
output_dir.name, invoice_month=self.invoice_month, data=self.dataframe
187187
)
188-
189-
pi_csv_1 = f'{self.dataframe["Institution"][0]}_{self.dataframe["Manager (PI)"][0]}_{self.dataframe["Invoice Month"][0]}.csv'
190-
pi_csv_2 = f'{self.dataframe["Institution"][3]}_{self.dataframe["Manager (PI)"][3]}_{self.dataframe["Invoice Month"][3]}.csv'
188+
pi_inv.process()
189+
pi_inv.export()
190+
pi_csv_1 = f'{self.dataframe["Institution"][0]}_{self.dataframe["Manager (PI)"][0]} {self.dataframe["Invoice Month"][0]}.csv'
191+
pi_csv_2 = f'{self.dataframe["Institution"][3]}_{self.dataframe["Manager (PI)"][3]} {self.dataframe["Invoice Month"][3]}.csv'
191192
self.assertIn(pi_csv_1, os.listdir(output_dir.name))
192193
self.assertIn(pi_csv_2, os.listdir(output_dir.name))
193194
self.assertEqual(
@@ -789,32 +790,46 @@ def test_process_lenovo(self):
789790

790791
class TestUploadToS3(TestCase):
791792
@mock.patch("process_report.process_report.get_invoice_bucket")
792-
@mock.patch("process_report.process_report.get_iso8601_time")
793-
def test_remove_prefix(self, mock_get_time, mock_get_bucket):
793+
@mock.patch("process_report.util.get_iso8601_time")
794+
def test_upload_to_s3(self, mock_get_time, mock_get_bucket):
794795
mock_bucket = mock.MagicMock()
795796
mock_get_bucket.return_value = mock_bucket
796797
mock_get_time.return_value = "0"
797798

798799
invoice_month = "2024-03"
799-
filenames = ["test.csv", "test2.test.csv", "test3"]
800+
filenames = ["test-test", "test2.test", "test3"]
801+
sample_base_invoice = test_utils.new_base_invoice(invoice_month=invoice_month)
802+
800803
answers = [
801-
("test.csv", f"Invoices/{invoice_month}/test {invoice_month}.csv"),
802804
(
803-
"test.csv",
804-
f"Invoices/{invoice_month}/Archive/test {invoice_month} 0.csv",
805+
f"test-test {invoice_month}.csv",
806+
f"Invoices/{invoice_month}/test-test {invoice_month}.csv",
805807
),
806808
(
807-
"test2.test.csv",
809+
f"test-test {invoice_month}.csv",
810+
f"Invoices/{invoice_month}/Archive/test-test {invoice_month} 0.csv",
811+
),
812+
(
813+
f"test2.test {invoice_month}.csv",
808814
f"Invoices/{invoice_month}/test2.test {invoice_month}.csv",
809815
),
810816
(
811-
"test2.test.csv",
817+
f"test2.test {invoice_month}.csv",
812818
f"Invoices/{invoice_month}/Archive/test2.test {invoice_month} 0.csv",
813819
),
814-
("test3", f"Invoices/{invoice_month}/test3 {invoice_month}.csv"),
815-
("test3", f"Invoices/{invoice_month}/Archive/test3 {invoice_month} 0.csv"),
820+
(
821+
f"test3 {invoice_month}.csv",
822+
f"Invoices/{invoice_month}/test3 {invoice_month}.csv",
823+
),
824+
(
825+
f"test3 {invoice_month}.csv",
826+
f"Invoices/{invoice_month}/Archive/test3 {invoice_month} 0.csv",
827+
),
816828
]
817829

818-
process_report.upload_to_s3(filenames, invoice_month)
830+
for filename in filenames:
831+
sample_base_invoice.name = filename
832+
sample_base_invoice.export_s3(mock_bucket)
833+
819834
for i, call_args in enumerate(mock_bucket.upload_file.call_args_list):
820835
self.assertTrue(answers[i] in call_args)

process_report/tests/util.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import pandas
22

3-
from process_report.invoices import billable_invoice, bu_internal_invoice
3+
from process_report.invoices import invoice, billable_invoice, bu_internal_invoice, pi_specific_invoice
4+
5+
6+
def new_base_invoice(
7+
name="",
8+
invoice_month="0000-00",
9+
data=pandas.DataFrame(),
10+
):
11+
return invoice.Invoice(name, invoice_month, data)
412

513

614
def new_billable_invoice(
@@ -27,3 +35,14 @@ def new_bu_internal_invoice(
2735
return bu_internal_invoice.BUInternalInvoice(
2836
name, invoice_month, data, subsidy_amount
2937
)
38+
39+
def new_pi_specific_invoice(
40+
name="",
41+
invoice_month="0000-00",
42+
data=pandas.DataFrame(),
43+
):
44+
return pi_specific_invoice.PIInvoice(
45+
name,
46+
invoice_month,
47+
data,
48+
)

0 commit comments

Comments
 (0)