1
1
import os
2
+ import sys
2
3
from dataclasses import dataclass
4
+ import subprocess
5
+ import tempfile
6
+ import logging
3
7
4
8
import pandas
9
+ from jinja2 import Environment , FileSystemLoader
5
10
6
11
import process_report .invoices .invoice as invoice
7
12
import process_report .util as util
8
13
9
14
15
+ TEMPLATE_DIR_PATH = "process_report/templates"
16
+
17
+
18
+ logger = logging .getLogger (__name__ )
19
+ logging .basicConfig (level = logging .INFO )
20
+
21
+
10
22
@dataclass
11
23
class PIInvoice (invoice .Invoice ):
12
24
"""
@@ -15,6 +27,21 @@ class PIInvoice(invoice.Invoice):
15
27
- NewPICreditProcessor
16
28
"""
17
29
30
+ TOTAL_COLUMN_LIST = [
31
+ invoice .COST_FIELD ,
32
+ invoice .CREDIT_FIELD ,
33
+ invoice .BALANCE_FIELD ,
34
+ ]
35
+
36
+ DOLLAR_COLUMN_LIST = [
37
+ invoice .RATE_FIELD ,
38
+ invoice .GROUP_BALANCE_FIELD ,
39
+ invoice .COST_FIELD ,
40
+ invoice .GROUP_BALANCE_USED_FIELD ,
41
+ invoice .CREDIT_FIELD ,
42
+ invoice .BALANCE_FIELD ,
43
+ ]
44
+
18
45
export_columns_list = [
19
46
invoice .INVOICE_DATE_FIELD ,
20
47
invoice .PROJECT_FIELD ,
@@ -43,31 +70,101 @@ def _prepare(self):
43
70
]
44
71
self .pi_list = self .export_data [invoice .PI_FIELD ].unique ()
45
72
73
+ def _get_pi_dataframe (self , data , pi ):
74
+ pi_projects = data [data [invoice .PI_FIELD ] == pi ].copy ().reset_index (drop = True )
75
+
76
+ # Remove prepay group data if it's empty
77
+ if pandas .isna (pi_projects [invoice .GROUP_NAME_FIELD ]).all ():
78
+ pi_projects = pi_projects .drop (
79
+ [
80
+ invoice .GROUP_NAME_FIELD ,
81
+ invoice .GROUP_INSTITUTION_FIELD ,
82
+ invoice .GROUP_BALANCE_FIELD ,
83
+ invoice .GROUP_BALANCE_USED_FIELD ,
84
+ ],
85
+ axis = 1 ,
86
+ )
87
+
88
+ # Add a row containing sums for certain columns
89
+ column_sums = []
90
+ sum_columns_list = []
91
+ for column_name in self .TOTAL_COLUMN_LIST :
92
+ if column_name in pi_projects .columns :
93
+ column_sums .append (pi_projects [column_name ].sum ())
94
+ sum_columns_list .append (column_name )
95
+ pi_projects .loc [
96
+ len (pi_projects )
97
+ ] = None # Adds a new row to end of dataframe initialized with None
98
+ pi_projects .loc [pi_projects .index [- 1 ], invoice .INVOICE_DATE_FIELD ] = "Total"
99
+ pi_projects .loc [pi_projects .index [- 1 ], sum_columns_list ] = column_sums
100
+
101
+ # Add dollar sign to certain columns
102
+ for column_name in self .DOLLAR_COLUMN_LIST :
103
+ if column_name in pi_projects .columns :
104
+ pi_projects [column_name ] = pi_projects [column_name ].apply (
105
+ lambda data : data if pandas .isna (data ) else f"${ data } "
106
+ )
107
+
108
+ pi_projects .fillna ("" , inplace = True )
109
+
110
+ return pi_projects
111
+
46
112
def export (self ):
47
- def _export_pi_invoice (pi ):
48
- if pandas .isna (pi ):
49
- return
50
- pi_projects = self .export_data [self .export_data [invoice .PI_FIELD ] == pi ]
51
- pi_instituition = pi_projects [invoice .INSTITUTION_FIELD ].iat [0 ]
52
- pi_projects .to_csv (
53
- f"{ self .name } /{ pi_instituition } _{ pi } { self .invoice_month } .csv"
113
+ def _create_html_invoice (temp_fd ):
114
+ environment = Environment (loader = FileSystemLoader (TEMPLATE_DIR_PATH ))
115
+ template = environment .get_template ("pi_invoice.html" )
116
+ content = template .render (
117
+ data = pi_dataframe ,
118
+ )
119
+ temp_fd .write (content )
120
+ temp_fd .flush ()
121
+
122
+ def _create_pdf_invoice (temp_fd_name ):
123
+ chrome_binary_location = os .environ .get (
124
+ "CHROME_BIN_PATH" , "/usr/bin/chromium"
125
+ )
126
+ if not os .path .exists (chrome_binary_location ):
127
+ sys .exit (
128
+ 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"
129
+ )
130
+
131
+ invoice_pdf_path = (
132
+ f"{ self .name } /{ pi_instituition } _{ pi } _{ self .invoice_month } .pdf"
133
+ )
134
+ subprocess .run (
135
+ [
136
+ chrome_binary_location ,
137
+ "--headless" ,
138
+ "--no-sandbox" ,
139
+ f"--print-to-pdf={ invoice_pdf_path } " ,
140
+ "--no-pdf-header-footer" ,
141
+ f"file://{ temp_fd_name } " ,
142
+ ],
143
+ capture_output = True ,
54
144
)
55
145
56
146
self ._filter_columns ()
57
- if not os .path .exists (
58
- self .name
59
- ): # self.name is name of folder storing invoices
60
- os .mkdir (self .name )
147
+
148
+ # self.name is name of folder storing invoices
149
+ os .makedirs (self .name , exist_ok = True )
61
150
62
151
for pi in self .pi_list :
63
- _export_pi_invoice (pi )
152
+ if pandas .isna (pi ):
153
+ continue
154
+
155
+ pi_dataframe = self ._get_pi_dataframe (self .export_data , pi )
156
+ pi_instituition = pi_dataframe [invoice .INSTITUTION_FIELD ].iat [0 ]
157
+
158
+ with tempfile .NamedTemporaryFile (mode = "w" , suffix = ".html" ) as temp_fd :
159
+ _create_html_invoice (temp_fd )
160
+ _create_pdf_invoice (temp_fd .name )
64
161
65
162
def export_s3 (self , s3_bucket ):
66
163
def _export_s3_pi_invoice (pi_invoice ):
67
164
pi_invoice_path = os .path .join (self .name , pi_invoice )
68
165
striped_invoice_path = os .path .splitext (pi_invoice_path )[0 ]
69
- output_s3_path = f"Invoices/{ self .invoice_month } /{ striped_invoice_path } .csv "
70
- output_s3_archive_path = f"Invoices/{ self .invoice_month } /Archive/{ striped_invoice_path } { util .get_iso8601_time ()} .csv "
166
+ output_s3_path = f"Invoices/{ self .invoice_month } /{ striped_invoice_path } .pdf "
167
+ output_s3_archive_path = f"Invoices/{ self .invoice_month } /Archive/{ striped_invoice_path } { util .get_iso8601_time ()} .pdf "
71
168
s3_bucket .upload_file (pi_invoice_path , output_s3_path )
72
169
s3_bucket .upload_file (pi_invoice_path , output_s3_archive_path )
73
170
0 commit comments