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
134 changes: 134 additions & 0 deletions process_report/invoices/MOCA_group_specific_invoice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import os
from dataclasses import dataclass
import tempfile

import pandas

from process_report.invoices import invoice, pdf_invoice


@dataclass
class MOCAGroupInvoice(pdf_invoice.PDFInvoice):
CREDIT_COLUMN_COPY_LIST = [
invoice.INVOICE_DATE_FIELD,
invoice.INVOICE_EMAIL_FIELD,
invoice.GROUP_NAME_FIELD,
invoice.GROUP_INSTITUTION_FIELD,
]
TOTAL_COLUMN_LIST = [
invoice.COST_FIELD,
invoice.GROUP_BALANCE_USED_FIELD,
invoice.CREDIT_FIELD,
invoice.BALANCE_FIELD,
]

DOLLAR_COLUMN_LIST = [
invoice.RATE_FIELD,
invoice.GROUP_BALANCE_FIELD,
invoice.COST_FIELD,
invoice.GROUP_BALANCE_USED_FIELD,
invoice.CREDIT_FIELD,
invoice.BALANCE_FIELD,
]

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.GROUP_NAME_FIELD,
invoice.GROUP_INSTITUTION_FIELD,
invoice.GROUP_BALANCE_FIELD,
invoice.COST_FIELD,
invoice.GROUP_BALANCE_USED_FIELD,
invoice.CREDIT_FIELD,
invoice.CREDIT_CODE_FIELD,
invoice.BALANCE_FIELD,
]

prepay_credits: pandas.DataFrame

def _prepare(self):
self.export_data = self.data[
self.data[invoice.IS_BILLABLE_FIELD] & ~self.data[invoice.MISSING_PI_FIELD]
]
self.export_data = self.export_data[
~self.export_data[invoice.GROUP_NAME_FIELD].isna()
]
self.group_list = self.export_data[invoice.GROUP_NAME_FIELD].unique()

def _get_group_dataframe(self, data, group):
group_projects = (
data[data[invoice.GROUP_NAME_FIELD] == group].copy().reset_index(drop=True)
)

# Add row for each prepay credit for the group in the invoice month
group_credit_mask = (
self.prepay_credits[invoice.PREPAY_MONTH_FIELD] == self.invoice_month
) & (self.prepay_credits[invoice.PREPAY_GROUP_NAME_FIELD] == group)
group_credit_info = self.prepay_credits[group_credit_mask]
for _, credit_info in group_credit_info.iterrows():
group_credit = credit_info[invoice.PREPAY_CREDIT_FIELD]
group_projects.loc[len(group_projects)] = None

# In this "credit row", certain values should be
# the same for every columns (i.e Invoice Month, Group Name, etc.)
for column_name in self.CREDIT_COLUMN_COPY_LIST:
if column_name in group_projects.columns:
group_projects.loc[group_projects.index[-1], column_name] = (
group_projects.loc[0, column_name]
)

# Group is billed for the credit amount
group_projects.loc[
group_projects.index[-1], [invoice.COST_FIELD, invoice.BALANCE_FIELD]
] = [group_credit] * 2

# Add sum row
column_sums = []
sum_columns_list = []
for column_name in self.TOTAL_COLUMN_LIST:
if column_name in group_projects.columns:
column_sums.append(group_projects[column_name].sum())
sum_columns_list.append(column_name)
group_projects.loc[len(group_projects)] = (
None # Adds a new row to end of dataframe initialized with None
)
group_projects.loc[group_projects.index[-1], invoice.INVOICE_DATE_FIELD] = (
"Total"
)
group_projects.loc[group_projects.index[-1], sum_columns_list] = column_sums

# Add dollar signs
for column_name in self.DOLLAR_COLUMN_LIST:
if column_name in group_projects.columns:
group_projects[column_name] = group_projects[column_name].apply(
lambda data: data if pandas.isna(data) else f"${data}"
)

group_projects.fillna("", inplace=True)

return group_projects

def export(self):
self._filter_columns()

if not os.path.exists(self.name):
os.mkdir(self.name)

for group in self.group_list:
group_dataframe = self._get_group_dataframe(self.export_data, group)
group_instituition = group_dataframe[invoice.GROUP_INSTITUTION_FIELD].iat[0]
group_contact_email = group_dataframe[invoice.INVOICE_EMAIL_FIELD].iat[0]
group_invoice_path = f"{self.name}/{group_instituition}_{group_contact_email}_{self.invoice_month}.pdf"

with tempfile.NamedTemporaryFile(mode="w", suffix=".html") as temp_fd:
self._create_html_invoice(temp_fd, group_dataframe, "pi_invoice.html")
self._create_pdf_invoice(temp_fd.name, group_invoice_path)
59 changes: 59 additions & 0 deletions process_report/invoices/pdf_invoice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import os
import sys
from dataclasses import dataclass
import subprocess

import pandas
from jinja2 import Environment, FileSystemLoader

import process_report.invoices.invoice as invoice
import process_report.util as util


TEMPLATE_DIR_PATH = "process_report/templates"


@dataclass
class PDFInvoice(invoice.Invoice):
@staticmethod
def _create_html_invoice(temp_fd, data: pandas.DataFrame, template_filename: str):
environment = Environment(loader=FileSystemLoader(TEMPLATE_DIR_PATH))
template = environment.get_template(template_filename)
content = template.render(
data=data,
)
temp_fd.write(content)
temp_fd.flush()

@staticmethod
def _create_pdf_invoice(html_filepath: str, output_pdf_path: str):
chrome_binary_location = os.environ.get("CHROME_BIN_PATH", "/usr/bin/chromium")
if not os.path.exists(chrome_binary_location):
sys.exit(
f"Chrome binary does not exist at {chrome_binary_location}. Make sure the env var CHROME_BIN_PATH is set correctly or that Google Chrome is installed"
)

subprocess.run(
[
chrome_binary_location,
"--headless",
"--no-sandbox",
f"--print-to-pdf={output_pdf_path}",
"--no-pdf-header-footer",
"file://" + html_filepath,
],
capture_output=True,
)

def export_s3(self, s3_bucket):
def _export_s3_group_invoice(invoice):
invoice_path = os.path.join(self.name, invoice)
striped_invoice_path = os.path.splitext(invoice_path)[0]
output_s3_path = f"Invoices/{self.invoice_month}/{striped_invoice_path}.pdf"
output_s3_archive_path = f"Invoices/{self.invoice_month}/Archive/{striped_invoice_path} {util.get_iso8601_time()}.pdf"
s3_bucket.upload_file(invoice_path, output_s3_path)
s3_bucket.upload_file(invoice_path, output_s3_archive_path)

# self.name is name of folder storing PDF invoices
for invoice_filename in os.listdir(self.name):
_export_s3_group_invoice(invoice_filename)
60 changes: 7 additions & 53 deletions process_report/invoices/pi_specific_invoice.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import os
import sys
from dataclasses import dataclass
import subprocess
import tempfile
import logging

import pandas
from jinja2 import Environment, FileSystemLoader

import process_report.invoices.invoice as invoice
import process_report.util as util
from process_report.invoices import invoice, pdf_invoice


TEMPLATE_DIR_PATH = "process_report/templates"
Expand All @@ -20,7 +16,7 @@


@dataclass
class PIInvoice(invoice.Invoice):
class PIInvoice(pdf_invoice.PDFInvoice):
"""
This invoice operates on data processed by these Processors:
- ValidateBillablePIsProcessor
Expand Down Expand Up @@ -110,39 +106,6 @@ def _get_pi_dataframe(self, data, pi):
return pi_projects

def export(self):
def _create_html_invoice(temp_fd):
environment = Environment(loader=FileSystemLoader(TEMPLATE_DIR_PATH))
template = environment.get_template("pi_invoice.html")
content = template.render(
data=pi_dataframe,
)
temp_fd.write(content)
temp_fd.flush()

def _create_pdf_invoice(temp_fd_name):
chrome_binary_location = os.environ.get(
"CHROME_BIN_PATH", "/usr/bin/chromium"
)
if not os.path.exists(chrome_binary_location):
sys.exit(
f"Chrome binary does not exist at {chrome_binary_location}. Make sure the env var CHROME_BIN_PATH is set correctly and that Google Chrome is installed"
)

invoice_pdf_path = (
f"{self.name}/{pi_instituition}_{pi}_{self.invoice_month}.pdf"
)
subprocess.run(
[
chrome_binary_location,
"--headless",
"--no-sandbox",
f"--print-to-pdf={invoice_pdf_path}",
"--no-pdf-header-footer",
f"file://{temp_fd_name}",
],
capture_output=True,
)

self._filter_columns()

# self.name is name of folder storing invoices
Expand All @@ -154,19 +117,10 @@ def _create_pdf_invoice(temp_fd_name):

pi_dataframe = self._get_pi_dataframe(self.export_data, pi)
pi_instituition = pi_dataframe[invoice.INSTITUTION_FIELD].iat[0]
invoice_pdf_path = (
f"{self.name}/{pi_instituition}_{pi}_{self.invoice_month}.pdf"
)

with tempfile.NamedTemporaryFile(mode="w", suffix=".html") as temp_fd:
_create_html_invoice(temp_fd)
_create_pdf_invoice(temp_fd.name)

def export_s3(self, s3_bucket):
def _export_s3_pi_invoice(pi_invoice):
pi_invoice_path = os.path.join(self.name, pi_invoice)
striped_invoice_path = os.path.splitext(pi_invoice_path)[0]
output_s3_path = f"Invoices/{self.invoice_month}/{striped_invoice_path}.pdf"
output_s3_archive_path = f"Invoices/{self.invoice_month}/Archive/{striped_invoice_path} {util.get_iso8601_time()}.pdf"
s3_bucket.upload_file(pi_invoice_path, output_s3_path)
s3_bucket.upload_file(pi_invoice_path, output_s3_archive_path)

for pi_invoice in os.listdir(self.name):
_export_s3_pi_invoice(pi_invoice)
self._create_html_invoice(temp_fd, pi_dataframe, "pi_invoice.html")
self._create_pdf_invoice(temp_fd.name, invoice_pdf_path)
15 changes: 15 additions & 0 deletions process_report/process_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
MOCA_prepaid_invoice,
prepay_credits_snapshot,
ocp_test_invoice,
MOCA_group_specific_invoice,
)
from process_report.processors import (
coldfront_fetch_processor,
Expand Down Expand Up @@ -184,6 +185,12 @@ def main():
default="pi_invoices",
help="Name of output folder containing pi-specific invoice csvs",
)
parser.add_argument(
"--prepay-groups-output-folder",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not make this configurable but provide default locations for invoices moving forward. Name should be hardcoded in the invoice.

required=False,
default="group_invoices",
help="Name of output folder containing prepay-group-specific invoice PDFs",
)
parser.add_argument(
"--BU-invoice-file",
required=False,
Expand Down Expand Up @@ -391,6 +398,13 @@ def main():
prepay_contacts=prepay_info,
)

moca_group_inv = MOCA_group_specific_invoice.MOCAGroupInvoice(
name=args.prepay_groups_output_folder,
invoice_month=invoice_month,
data=processed_data,
prepay_credits=prepay_credits,
)

ocp_test_inv = ocp_test_invoice.OcpTestInvoice(
name="", invoice_month=invoice_month, data=processed_data.copy()
)
Expand All @@ -406,6 +420,7 @@ def main():
moca_prepaid_inv,
prepay_credits_snap,
ocp_test_inv,
moca_group_inv,
],
args.upload_to_s3,
)
Expand Down
Loading
Loading