Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions process_report/invoices/bm_invoice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from dataclasses import dataclass


from process_report.invoices import invoice


@dataclass
class BMInvoice(invoice.Invoice):
name: str = "bm_projects"
export_columns_list = [
invoice.INVOICE_DATE_FIELD,
invoice.PROJECT_FIELD,
invoice.PROJECT_ID_FIELD,
invoice.PI_FIELD,
invoice.INVOICE_EMAIL_FIELD,
invoice.INVOICE_ADDRESS_FIELD,
invoice.INSTITUTION_FIELD,
invoice.INSTITUTION_ID_FIELD,
invoice.SU_HOURS_FIELD,
invoice.SU_TYPE_FIELD,
invoice.RATE_FIELD,
invoice.COST_FIELD,
invoice.CREDIT_FIELD,
invoice.CREDIT_CODE_FIELD,
invoice.BALANCE_FIELD,
]

def _prepare_export(self):
self.export_data = self.data[self.data[invoice.CLUSTER_NAME_FIELD] == "bm"]
4 changes: 4 additions & 0 deletions process_report/process_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from process_report import util
from process_report.invoices import (
invoice,
bm_invoice,
lenovo_invoice,
nonbillable_invoice,
billable_invoice,
Expand All @@ -30,6 +31,7 @@
bu_subsidy_processor,
prepayment_processor,
validate_cluster_name_processor,
bm_usage_processor,
)


Expand Down Expand Up @@ -71,6 +73,7 @@ def main():
validate_cluster_name_processor.ValidateClusterNameProcessor,
coldfront_fetch_processor.ColdfrontFetchProcessor,
validate_pi_alias_processor.ValidatePIAliasProcessor,
bm_usage_processor.BMUsageProcessor,
add_institution_processor.AddInstitutionProcessor,
lenovo_processor.LenovoProcessor,
validate_billable_pi_processor.ValidateBillablePIsProcessor,
Expand All @@ -94,6 +97,7 @@ def main():
MOCA_prepaid_invoice.MOCAPrepaidInvoice,
prepay_credits_snapshot.PrepayCreditsSnapshot,
ocp_test_invoice.OcpTestInvoice,
bm_invoice.BMInvoice,
],
invoice_settings.upload_to_s3,
)
Expand Down
18 changes: 18 additions & 0 deletions process_report/processors/bm_usage_processor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from dataclasses import dataclass

from process_report.invoices import invoice
from process_report.processors import processor


@dataclass
class BMUsageProcessor(processor.Processor):
def _get_bm_project_mask(self):
return self.data[invoice.CLUSTER_NAME_FIELD] == "bm"

def _process(self):
bm_projects_mask = self._get_bm_project_mask()
self.data.loc[bm_projects_mask, invoice.PROJECT_FIELD] = self.data.loc[
bm_projects_mask, invoice.PROJECT_FIELD
].apply(lambda v: v + " BM Usage")
self.data.loc[bm_projects_mask, invoice.PROJECT_ID_FIELD] = "ESI Bare Metal"
self.data.loc[bm_projects_mask, invoice.INVOICE_EMAIL_FIELD] = "nclinton@bu.edu"
5 changes: 4 additions & 1 deletion process_report/processors/bu_subsidy_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ def _get_subsidy_eligible_projects(data):
]
filtered_data = filtered_data[
filtered_data[invoice.INSTITUTION_FIELD] == "Boston University"
].copy()
]
filtered_data = filtered_data[
~(filtered_data[invoice.CLUSTER_NAME_FIELD] == "bm")
]

return filtered_data

Expand Down
4 changes: 4 additions & 0 deletions process_report/processors/new_pi_credit_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,14 @@ def _filter_nonbillables(self, data):
def _filter_missing_pis(self, data):
return data[~data["Missing PI"]]

def _filter_bm_projects(self, data):
return data[~(data[invoice.CLUSTER_NAME_FIELD] == "bm")]

def _get_credit_eligible_projects(self, data: pandas.DataFrame):
filtered_data = self._filter_nonbillables(data)
filtered_data = self._filter_missing_pis(filtered_data)
filtered_data = self._filter_excluded_su_types(filtered_data)
filtered_data = self._filter_bm_projects(filtered_data)
if self.limit_new_pi_credit_to_partners:
filtered_data = self._filter_partners(filtered_data)

Expand Down
1 change: 1 addition & 0 deletions process_report/tests/e2e/test_e2e_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"MOCA-A_Prepaid_Groups-2025-06-Invoice.csv",
"NERC_Prepaid_Group-Credits-2025-06.csv",
"OCP_TEST 2025-06.csv",
"bm_projects 2025-06.csv",
]

EXPECTED_DIRECTORIES = ["pi_invoices"]
Expand Down
34 changes: 34 additions & 0 deletions process_report/tests/unit/processors/test_bm_usage_processor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from unittest import TestCase

import pandas

from process_report.tests import util as test_utils


class TestBUSubsidyProcessor(TestCase):
def test_process_bm_usage(self):
test_invoice = pandas.DataFrame(
{
"Project - Allocation": ["test", "test bm-bm", "not-bm"],
"Project - Allocation ID": [None] * 3,
"Invoice Email": [None] * 3,
"Cluster Name": ["bm", "bm", "ocp"],
}
)

answer_invoice = pandas.DataFrame(
{
"Project - Allocation": [
"test BM Usage",
"test bm-bm BM Usage",
"not-bm",
],
"Project - Allocation ID": ["ESI Bare Metal"] * 2 + [None],
"Invoice Email": ["nclinton@bu.edu"] * 2 + [None],
"Cluster Name": ["bm", "bm", "ocp"],
}
)

bm_usage_proc = test_utils.new_bm_usage_processor(data=test_invoice)
bm_usage_proc.process()
self.assertTrue(bm_usage_proc.data.equals(answer_invoice))
23 changes: 23 additions & 0 deletions process_report/tests/unit/processors/test_bu_subsidy_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def _get_test_invoice(
institution=None,
is_billable=None,
missing_pi=None,
clusters=None,
):
if not balances:
balances = pi_balances
Expand All @@ -48,6 +49,9 @@ def _get_test_invoice(
if not missing_pi:
missing_pi = [False for _ in range(len(pi))]

if not clusters:
clusters = ["" for _ in range(len(pi))]

return pandas.DataFrame(
{
"Manager (PI)": pi,
Expand All @@ -57,6 +61,7 @@ def _get_test_invoice(
"Institution": institution,
"Is Billable": is_billable,
"Missing PI": missing_pi,
"Cluster Name": clusters,
}
)

Expand Down Expand Up @@ -175,3 +180,21 @@ def test_two_pi(self):
answer_invoice["PI Balance"] = [0, 60, 0, 0]

self._assert_result_invoice(subsidy_amount, test_invoice, answer_invoice)

def test_exclude_bm_cluster(self):
"""Projects in the 'bm' cluster should be excluded from BU subsidy calculation."""
subsidy_amount = 100
test_invoice = self._get_test_invoice(
["PI"] * 2, # single PI (will be broadcast to two rows by lengths below)
pi_balances=[60, 60],
project_names=["P1", "P2"],
clusters=["bm", "ocp"],
)

answer_invoice = test_invoice.copy()
answer_invoice["Project"] = answer_invoice["Project - Allocation"]
# bm allocation gets no subsidy, non-bm allocation gets up to its PI balance (60)
answer_invoice["Subsidy"] = [0, 60]
answer_invoice["PI Balance"] = [60, 0]

self._assert_result_invoice(subsidy_amount, test_invoice, answer_invoice)
Loading