-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathapp.py
173 lines (146 loc) · 5.24 KB
/
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import configparser
import csv
import json
import requests
import sys
import time
from requests.auth import HTTPBasicAuth
# URL used to obtain tokens from Xero
XERO_TOKEN_URL = "https://identity.xero.com/connect/token"
# Read in the config.ini file
config = configparser.ConfigParser()
config.read('config.ini')
try:
VOID_TYPE = str(config["DEFAULT"]["VOID_TYPE"])
DRY_RUN = str(config['DEFAULT']['DRY_RUN'])
except KeyError:
print("Please check your file is named config.ini - we couldn't find it")
sys.exit(1)
def check_config():
"""
Check the config entries are valid
"""
# Immediately exit if someone hasn't set DRY_RUN properly
if DRY_RUN not in ("Enabled", "Disabled"):
print("Dry run needs to be set to Enabled or Disabled. Exiting...")
sys.exit(1)
# Check void type is supported, otherwise exit immediately
if VOID_TYPE not in ("Invoices", "CreditNotes"):
print("Void type needs to be Invoices or CreditNotes")
sys.exit(1)
def get_token():
"""
Obtains a token from Xero, lasts 30 minutes
"""
headers = {
'Content-Type': 'application/x-www-form-urlencoded'
}
data = {
'grant_type': "client_credentials",
'scopes': ['accounting.transactions']
}
token_res = post_xero_api_call(XERO_TOKEN_URL, headers, data, auth=True)
if token_res.status_code == 200:
print(f"Obtained token, it will expire in 30 minutes")
return token_res.json()['access_token']
else:
print("Couldn't fetch a token, have you set up the App at developer.xero.com?")
print(f"Status Code: {token_res.status_code}")
def open_csv_file(column_name="InvoiceNumber"):
"""
Opens a .csv file and prints out invoice IDs
"""
try:
with open(config['DEFAULT']['CSV_FILENAME'], newline='') as csvfile:
reader = csv.DictReader(csvfile)
return [row[column_name] for row in reader]
except Exception:
print("Error: Please check your .csv file is named correctly and contains an InvoiceNumber column")
raise
def post_xero_api_call(url, headers, data, auth=False):
"""
Send a post request to Xero
1) Auth true will pass client id and secret into BasicAuth
2) Auth false expects you to have added the Bearer token header
"""
if auth:
xero_res = requests.post(
url,
headers=headers,
auth=HTTPBasicAuth(config['DEFAULT']['CLIENT_ID'], config['DEFAULT']['CLIENT_SECRET']),
data=data
)
else:
xero_res = requests.post(
url,
headers=headers,
data=json.dumps(data)
)
return xero_res
def process_void_job(token, invoice_ids, all_at_once):
"""
We either void instantly or wait 1 second inbetween API calls using all_at_once
"""
if not all_at_once:
for idx in invoice_ids:
# Sleep 1.5 seconds so it's impossible to hit the rate limit
# of 60 API calls max per minute
time.sleep(1.5)
void_invoice(token, idx)
return
for idx in invoice_ids:
void_invoice(token, idx)
return
def void_invoice(token, invoice_number):
"""
Voids a given invoice number
"""
print(f"Asking Xero to void {invoice_number}")
url = f"https://api.xero.com/api.xro/2.0/{VOID_TYPE}/{invoice_number}"
headers = {
'Accept': 'application/json',
'Authorization': f"Bearer {token}",
'Content-Type': 'application/json'
}
data = {
"InvoiceNumber": invoice_number,
"Status": "VOIDED"
}
void_res = post_xero_api_call(url, headers, data)
if void_res.status_code == 200:
print(f"Voided {invoice_number} successfully!")
else:
print(f"Couldn't void {invoice_number}, please check there's no payments applied to it and that it still exists in Xero")
print(f"Status Code: {void_res.status_code}")
print(void_res.json())
def main():
"""
Main execution loop
"""
try:
# Request access token from Xero
print("Asking Xero for an Access Token...")
token = get_token()
# Read invoice numbers into program
# Note: You MUST use a file named invoices.csv
# The file MUST have a column named InvoiceNumber
# Refer to README.md for examples
invoice_ids = set(open_csv_file())
# Safety mechanism for those wanting to check before committing
if DRY_RUN == "Enabled":
print("Dry run is enabled, not voiding anything")
print(f"Without Dry run we will void: \n{invoice_ids}")
else:
if len(invoice_ids) > 60:
print("Warning: The Xero API limit is 60 calls per minute. We will void one per second.")
process_void_job(token, invoice_ids, all_at_once=False)
else:
print("Warning: The Xero API limit is 60 calls per minute. You are voiding less than 60 so we will blast through them.")
process_void_job(token, invoice_ids, all_at_once=True)
except Exception as err:
print(f"Encountered an error: {str(err)}")
if __name__ == "__main__":
print("Running bulk void tool...")
check_config()
main()
print("Exiting...")