ManagerPythonClient 0.0.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- manager_python_client/Client.py +461 -0
- manager_python_client/ExpectedEntry/ExpectedEntryBase.py +96 -0
- manager_python_client/ExpectedEntry/ExpectedEntryInterAccountTransfer.py +50 -0
- manager_python_client/ExpectedEntry/ExpectedEntryPurchaseInvoice.py +109 -0
- manager_python_client/ExpectedEntry/ExpectedEntryPurchaseInvoicePayment.py +75 -0
- manager_python_client/ExpectedEntry/ExpectedEntrySalesInvoice.py +88 -0
- manager_python_client/ExpectedEntry/ExpectedEntrySalesInvoiceReceipt.py +76 -0
- manager_python_client/ExpectedEntry/Pattern.py +48 -0
- manager_python_client/ExpectedEntry/__init__.py +6 -0
- manager_python_client/ManagerLoginSession.py +18 -0
- manager_python_client/PurchaseInvoiceGenerator.py +13 -0
- manager_python_client/SalesInvoiceGenerator.py +14 -0
- manager_python_client/TransactionReporter.py +48 -0
- manager_python_client/__init__.py +7 -0
- managerpythonclient-0.0.8.dist-info/METADATA +61 -0
- managerpythonclient-0.0.8.dist-info/RECORD +18 -0
- managerpythonclient-0.0.8.dist-info/WHEEL +4 -0
- managerpythonclient-0.0.8.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
from importlib.metadata import files
|
|
2
|
+
|
|
3
|
+
import PythonAPIClientBase
|
|
4
|
+
import json
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
import base64
|
|
8
|
+
import mimetypes
|
|
9
|
+
import os
|
|
10
|
+
import requests
|
|
11
|
+
import copy
|
|
12
|
+
from decimal import Decimal
|
|
13
|
+
|
|
14
|
+
from .SalesInvoiceGenerator import SalesInvoiceGenerator
|
|
15
|
+
from .PurchaseInvoiceGenerator import PurchaseInvoiceGenerator
|
|
16
|
+
from .TransactionReporter import TransactionReporter
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def json_dump_s_default_function(obj):
|
|
20
|
+
if isinstance(obj, Decimal):
|
|
21
|
+
return str(obj)
|
|
22
|
+
raise TypeError(
|
|
23
|
+
f"Object of type {type(obj).__name__} is not JSON serializable"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def nameMatchFunction(item, name):
|
|
27
|
+
return item["name"] == name
|
|
28
|
+
|
|
29
|
+
def _get_attachment_url(purchaseInvoiceKey, businessName):
|
|
30
|
+
name = businessName.encode()
|
|
31
|
+
|
|
32
|
+
protobuf = b"\xa2\x06" + bytes([len(name)]) + name
|
|
33
|
+
|
|
34
|
+
uuid = UUID(purchaseInvoiceKey)
|
|
35
|
+
|
|
36
|
+
protobuf += b"\x0a" + b"\x12"
|
|
37
|
+
protobuf += b"\x09" + uuid.bytes_le[:8]
|
|
38
|
+
protobuf += b"\x11" + uuid.bytes_le[8:]
|
|
39
|
+
|
|
40
|
+
encoded = base64.urlsafe_b64encode(protobuf).decode().rstrip("=")
|
|
41
|
+
|
|
42
|
+
return f"/new-attachment?{encoded}"
|
|
43
|
+
|
|
44
|
+
class ManagerClient(PythonAPIClientBase.APIClientBase):
|
|
45
|
+
debug = None
|
|
46
|
+
def __init__(self, baseUrl, mock=None, forceOneRequestAtATime=False, verboseLogging=PythonAPIClientBase.VerboseLoggingNullLogClass(), debug=False):
|
|
47
|
+
self.debug = debug
|
|
48
|
+
useBaseUrl = baseUrl
|
|
49
|
+
if useBaseUrl != "MOCK":
|
|
50
|
+
useBaseUrl = useBaseUrl
|
|
51
|
+
super().__init__(baseURL=useBaseUrl, mock=mock, forceOneRequestAtATime=forceOneRequestAtATime, verboseLogging=verboseLogging)
|
|
52
|
+
|
|
53
|
+
def is_online(self):
|
|
54
|
+
try:
|
|
55
|
+
response = self.sendGetRequest(
|
|
56
|
+
url="/",
|
|
57
|
+
loginSession=None
|
|
58
|
+
)
|
|
59
|
+
except PythonAPIClientBase.APIClientException as cli:
|
|
60
|
+
if cli.result.status_code == 401:
|
|
61
|
+
return True # running but not authed as we have no login session
|
|
62
|
+
return False
|
|
63
|
+
except Exception as exp:
|
|
64
|
+
print("Other exp", exp)
|
|
65
|
+
return False
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
def _get_generator(self, loginSession, apiObj, outputKey=None, enrichParams=None, skipFn=None):
|
|
69
|
+
if outputKey is None:
|
|
70
|
+
outputKey = apiObj
|
|
71
|
+
pageSize = 100
|
|
72
|
+
skip = 0
|
|
73
|
+
while True:
|
|
74
|
+
params = {
|
|
75
|
+
"pageSize": pageSize,
|
|
76
|
+
"skip": skip
|
|
77
|
+
}
|
|
78
|
+
if enrichParams is not None:
|
|
79
|
+
enrichParams(params)
|
|
80
|
+
response = self.sendGetRequest(
|
|
81
|
+
url=f"/{apiObj}",
|
|
82
|
+
loginSession=loginSession,
|
|
83
|
+
params=params
|
|
84
|
+
)
|
|
85
|
+
if response.status_code != 200:
|
|
86
|
+
print("status_code=", response.status_code)
|
|
87
|
+
raise Exception("Error getting " + apiObj)
|
|
88
|
+
data = json.loads(response.text)
|
|
89
|
+
# print("D", data)
|
|
90
|
+
if outputKey not in data:
|
|
91
|
+
print("Possible keys", data.keys())
|
|
92
|
+
raise Exception("given output key not possible - " + outputKey)
|
|
93
|
+
customers = data[outputKey]
|
|
94
|
+
for customer in customers:
|
|
95
|
+
skip = False
|
|
96
|
+
if skipFn is not None:
|
|
97
|
+
skip = skipFn(customer)
|
|
98
|
+
if not skip:
|
|
99
|
+
yield customer
|
|
100
|
+
if len(customers) < pageSize:
|
|
101
|
+
break
|
|
102
|
+
skip += pageSize
|
|
103
|
+
|
|
104
|
+
def _get_single(self, loginSession, name, apiObj, outputKey=None, matchFn=nameMatchFunction):
|
|
105
|
+
if outputKey is None:
|
|
106
|
+
outputKey = apiObj
|
|
107
|
+
response = self.sendGetRequest(
|
|
108
|
+
url=f"/{apiObj}?term={quote(name)}",
|
|
109
|
+
loginSession=loginSession
|
|
110
|
+
)
|
|
111
|
+
if response.status_code != 200:
|
|
112
|
+
raise Exception("Error getting " + apiObj)
|
|
113
|
+
data = json.loads(response.text)
|
|
114
|
+
if len(data[outputKey]) == 0:
|
|
115
|
+
return None
|
|
116
|
+
retItems = []
|
|
117
|
+
for item in data[outputKey]:
|
|
118
|
+
if matchFn(item, name):
|
|
119
|
+
retItems.append(item)
|
|
120
|
+
if len(retItems) == 0:
|
|
121
|
+
return None
|
|
122
|
+
if len(retItems) == 1:
|
|
123
|
+
return retItems[0]
|
|
124
|
+
return None # only return if we have a unique match
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def get_customers(self, loginSession):
|
|
128
|
+
return self._get_generator(loginSession, "customers")
|
|
129
|
+
def get_customer(self, loginSession, name):
|
|
130
|
+
return self._get_single(loginSession, name, "customers")
|
|
131
|
+
|
|
132
|
+
def get_suppliers(self, loginSession):
|
|
133
|
+
return self._get_generator(loginSession, "suppliers")
|
|
134
|
+
def get_supplier(self, loginSession, name):
|
|
135
|
+
return self._get_single(loginSession, name, "suppliers")
|
|
136
|
+
|
|
137
|
+
def get_bank_and_cash_accounts(self, loginSession):
|
|
138
|
+
return self._get_generator(loginSession, "bank-and-cash-accounts", "bankAndCashAccounts")
|
|
139
|
+
def get_bank_and_cash_account(self, loginSession, name):
|
|
140
|
+
return self._get_single(loginSession, name, "bank-and-cash-accounts", "bankAndCashAccounts")
|
|
141
|
+
|
|
142
|
+
def get_chart_of_accounts(self, loginSession):
|
|
143
|
+
return self._get_generator(loginSession, "chart-of-accounts", "chartOfAccounts")
|
|
144
|
+
def get_chart_of_account(self, loginSession, name):
|
|
145
|
+
return self._get_single(loginSession, name, "chart-of-accounts", "chartOfAccounts")
|
|
146
|
+
|
|
147
|
+
def get_sales_invoices(self, loginSession):
|
|
148
|
+
return self._get_generator(loginSession, "sales-invoices", "salesInvoices")
|
|
149
|
+
def get_sales_invoices_for_customer(self, loginSession, customerName):
|
|
150
|
+
def skipFn(sales_invoice):
|
|
151
|
+
if customerName != sales_invoice["customer"]:
|
|
152
|
+
return True
|
|
153
|
+
return False
|
|
154
|
+
return self._get_generator(loginSession, "sales-invoices", "salesInvoices", skipFn=skipFn)
|
|
155
|
+
def get_sales_invoice(self, loginSession, description, customerName):
|
|
156
|
+
def nameMatchFunction(item, name):
|
|
157
|
+
if item["customer"] != customerName:
|
|
158
|
+
return False
|
|
159
|
+
if item["description"] != description:
|
|
160
|
+
return False
|
|
161
|
+
return True
|
|
162
|
+
return self._get_single(loginSession, description, "sales-invoices", "salesInvoices", matchFn=nameMatchFunction)
|
|
163
|
+
|
|
164
|
+
def get_inter_account_transfer_for_description(self, loginSession, description):
|
|
165
|
+
def enrichParams(params):
|
|
166
|
+
params["term"] = description
|
|
167
|
+
# def skipFn(rec):
|
|
168
|
+
# if date == rec["date"]:
|
|
169
|
+
# return False
|
|
170
|
+
# return True
|
|
171
|
+
return self._get_generator(
|
|
172
|
+
loginSession,
|
|
173
|
+
"inter-account-transfers",
|
|
174
|
+
"interAccountTransfers",
|
|
175
|
+
enrichParams=enrichParams
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def get_purchase_invoices(self, loginSession):
|
|
180
|
+
return self._get_generator(loginSession, "purchase-invoices", "purchaseInvoices")
|
|
181
|
+
def get_purchase_invoices_for_supplier(self, loginSession, supplierName):
|
|
182
|
+
def skipFn(purchase_invoice):
|
|
183
|
+
if supplierName != purchase_invoice["supplier"]:
|
|
184
|
+
return True
|
|
185
|
+
return False
|
|
186
|
+
return self._get_generator(loginSession, "purchase-invoices", "purchaseInvoices", skipFn=skipFn)
|
|
187
|
+
def get_purchase_invoice(self, loginSession, description, supplierName):
|
|
188
|
+
def nameMatchFunction(item, name):
|
|
189
|
+
if item["supplier"] != supplierName:
|
|
190
|
+
return False
|
|
191
|
+
if item["description"] != description:
|
|
192
|
+
return False
|
|
193
|
+
return True
|
|
194
|
+
return self._get_single(loginSession, description, "purchase-invoices", "purchaseInvoices", matchFn=nameMatchFunction)
|
|
195
|
+
|
|
196
|
+
def get_payment_for_description(self, loginSession, description):
|
|
197
|
+
def enrichParams(params):
|
|
198
|
+
params["term"] = description
|
|
199
|
+
# def skipFn(rec):
|
|
200
|
+
# if date == rec["date"]:
|
|
201
|
+
# return False
|
|
202
|
+
# return True
|
|
203
|
+
return self._get_generator(
|
|
204
|
+
loginSession,
|
|
205
|
+
"payments",
|
|
206
|
+
"payments",
|
|
207
|
+
enrichParams=enrichParams
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def create_sales_invoice_generator(self, issueDate, reference, customerKey, billingAddress, description):
|
|
211
|
+
return SalesInvoiceGenerator(issueDate, reference, customerKey, billingAddress, description)
|
|
212
|
+
|
|
213
|
+
def create_sales_invoice(self, loginSession, generator):
|
|
214
|
+
if not isinstance(generator, SalesInvoiceGenerator):
|
|
215
|
+
raise Exception("Wrong type of generator")
|
|
216
|
+
hasLineDescription = False
|
|
217
|
+
for line in generator.lines:
|
|
218
|
+
accountKey, description, qty, unit_price = line
|
|
219
|
+
if description is not None and description != "":
|
|
220
|
+
hasLineDescription = True
|
|
221
|
+
postData = {
|
|
222
|
+
"IssueDate": generator.issueDate,
|
|
223
|
+
"Reference": generator.reference,
|
|
224
|
+
"Customer": generator.customerKey,
|
|
225
|
+
"BillingAddress": generator.billingAddress,
|
|
226
|
+
"Description": generator.description,
|
|
227
|
+
"HasLineDescription": hasLineDescription,
|
|
228
|
+
"Lines": []
|
|
229
|
+
}
|
|
230
|
+
if len(generator.lines) == 0:
|
|
231
|
+
raise Exception("No lines found")
|
|
232
|
+
|
|
233
|
+
for line in generator.lines:
|
|
234
|
+
accountKey, description, qty, unit_price = line
|
|
235
|
+
postData["Lines"].append({
|
|
236
|
+
"Account": accountKey,
|
|
237
|
+
"LineDescription": description,
|
|
238
|
+
"Qty": qty,
|
|
239
|
+
"SalesUnitPrice": unit_price
|
|
240
|
+
})
|
|
241
|
+
response = self.sendPostRequest(
|
|
242
|
+
url="/sales-invoice-form",
|
|
243
|
+
loginSession=loginSession,
|
|
244
|
+
data=json.dumps(postData, default=json_dump_s_default_function)
|
|
245
|
+
)
|
|
246
|
+
if response.status_code != 201:
|
|
247
|
+
raise Exception("Error creating sales invoice")
|
|
248
|
+
data = json.loads(response.text)
|
|
249
|
+
return data
|
|
250
|
+
|
|
251
|
+
def pay_sales_invoice(self, loginSession, date, recievedAccountKey, description, accountKey, accountRecivablesCustomer, salesInvoiceKey, amount):
|
|
252
|
+
postData = {
|
|
253
|
+
"Date": date,
|
|
254
|
+
"ReceivedIn": recievedAccountKey,
|
|
255
|
+
"Description": description,
|
|
256
|
+
"PaidBy": 1,
|
|
257
|
+
"Customer": accountRecivablesCustomer,
|
|
258
|
+
"Lines": [
|
|
259
|
+
{
|
|
260
|
+
"Account": accountKey,
|
|
261
|
+
"AccountsReceivableCustomer": accountRecivablesCustomer,
|
|
262
|
+
"AccountsReceivableSalesInvoice": salesInvoiceKey,
|
|
263
|
+
"Amount": amount
|
|
264
|
+
}
|
|
265
|
+
]
|
|
266
|
+
}
|
|
267
|
+
response = self.sendPostRequest(
|
|
268
|
+
url="/receipt-form",
|
|
269
|
+
loginSession=loginSession,
|
|
270
|
+
data=json.dumps(postData, default=json_dump_s_default_function)
|
|
271
|
+
)
|
|
272
|
+
if response.status_code != 201:
|
|
273
|
+
raise Exception("Error creating sales invoice payment")
|
|
274
|
+
data = json.loads(response.text)
|
|
275
|
+
return data
|
|
276
|
+
|
|
277
|
+
def get_receipt_for_description(self, loginSession, description):
|
|
278
|
+
def enrichParams(params):
|
|
279
|
+
params["term"] = description
|
|
280
|
+
# def skipFn(rec):
|
|
281
|
+
# if date == rec["date"]:
|
|
282
|
+
# return False
|
|
283
|
+
# return True
|
|
284
|
+
return self._get_generator(
|
|
285
|
+
loginSession,
|
|
286
|
+
"receipts",
|
|
287
|
+
"receipts",
|
|
288
|
+
enrichParams=enrichParams
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
def create_purchase_invoice_generator(self, issueDate, reference, supplierKey, description):
|
|
292
|
+
return PurchaseInvoiceGenerator(issueDate, reference, supplierKey, description)
|
|
293
|
+
|
|
294
|
+
def create_purchase_invoice(self, loginSession, generator):
|
|
295
|
+
if not isinstance(generator, PurchaseInvoiceGenerator):
|
|
296
|
+
raise Exception("Wrong type of generator")
|
|
297
|
+
hasLineDescription = False
|
|
298
|
+
for line in generator.lines:
|
|
299
|
+
accountKey, description, qty, unit_price = line
|
|
300
|
+
if description is not None and description != "":
|
|
301
|
+
hasLineDescription = True
|
|
302
|
+
postData = {
|
|
303
|
+
"IssueDate": generator.issueDate,
|
|
304
|
+
"Reference": generator.reference,
|
|
305
|
+
"Supplier": generator.supplierKey,
|
|
306
|
+
"Description": generator.description,
|
|
307
|
+
"HasLineDescription": hasLineDescription,
|
|
308
|
+
"Lines": []
|
|
309
|
+
}
|
|
310
|
+
if len(generator.lines) == 0:
|
|
311
|
+
raise Exception("No lines found")
|
|
312
|
+
|
|
313
|
+
for line in generator.lines:
|
|
314
|
+
accountKey, description, qty, unit_price = line
|
|
315
|
+
postData["Lines"].append({
|
|
316
|
+
"Account": accountKey,
|
|
317
|
+
"LineDescription": description,
|
|
318
|
+
"Qty": qty,
|
|
319
|
+
"PurchaseUnitPrice": unit_price
|
|
320
|
+
})
|
|
321
|
+
response = self.sendPostRequest(
|
|
322
|
+
url="/purchase-invoice-form",
|
|
323
|
+
loginSession=loginSession,
|
|
324
|
+
data=json.dumps(postData, default=json_dump_s_default_function)
|
|
325
|
+
)
|
|
326
|
+
if response.status_code != 201:
|
|
327
|
+
raise Exception("Error creating sales invoice")
|
|
328
|
+
data = json.loads(response.text)
|
|
329
|
+
return data
|
|
330
|
+
|
|
331
|
+
def pay_purchase_invoice(self, loginSession, date, paidFromAccountKey, description, accountKey, accountPayableSupplier, purchaseInvoiceKey, amount):
|
|
332
|
+
postData = {
|
|
333
|
+
"Date": date,
|
|
334
|
+
"PaidFrom": paidFromAccountKey,
|
|
335
|
+
"Description": description,
|
|
336
|
+
"Payee": 2,
|
|
337
|
+
"Supplier": accountPayableSupplier,
|
|
338
|
+
"Lines": [
|
|
339
|
+
{
|
|
340
|
+
"Account": accountKey,
|
|
341
|
+
"AccountsPayableSupplier": accountPayableSupplier,
|
|
342
|
+
"PurchaseInvoice": purchaseInvoiceKey,
|
|
343
|
+
"Amount": amount
|
|
344
|
+
}
|
|
345
|
+
]
|
|
346
|
+
}
|
|
347
|
+
response = self.sendPostRequest(
|
|
348
|
+
url="/payment-form",
|
|
349
|
+
loginSession=loginSession,
|
|
350
|
+
data=json.dumps(postData, default=json_dump_s_default_function)
|
|
351
|
+
)
|
|
352
|
+
if response.status_code != 201:
|
|
353
|
+
raise Exception("Error creating purchase invoice payment")
|
|
354
|
+
data = json.loads(response.text)
|
|
355
|
+
return data
|
|
356
|
+
|
|
357
|
+
def create_inter_account_transfer(
|
|
358
|
+
self,
|
|
359
|
+
loginSession,
|
|
360
|
+
date,
|
|
361
|
+
reference,
|
|
362
|
+
paidFromAccountKey,
|
|
363
|
+
receivedInAccountKey,
|
|
364
|
+
description,
|
|
365
|
+
amount
|
|
366
|
+
):
|
|
367
|
+
postData = {
|
|
368
|
+
"Date": date,
|
|
369
|
+
"Reference": reference,
|
|
370
|
+
"PaidFrom": paidFromAccountKey,
|
|
371
|
+
"ReceivedIn": receivedInAccountKey,
|
|
372
|
+
"Description": description,
|
|
373
|
+
"CreditAmount": amount,
|
|
374
|
+
"DebitAmount": amount
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
response = self.sendPostRequest(
|
|
378
|
+
url="/inter-account-transfer-form",
|
|
379
|
+
loginSession=loginSession,
|
|
380
|
+
data=json.dumps(postData, default=json_dump_s_default_function)
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
if response.status_code != 201:
|
|
384
|
+
raise Exception(
|
|
385
|
+
f"Error creating inter account transfer: "
|
|
386
|
+
f"{response.status_code} {response.text}"
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
return json.loads(response.text)
|
|
390
|
+
|
|
391
|
+
def get_business(self, loginSession):
|
|
392
|
+
response = self.sendGetRequest(
|
|
393
|
+
url="/access-tokens?pageSize=1",
|
|
394
|
+
loginSession=loginSession
|
|
395
|
+
)
|
|
396
|
+
if response.status_code != 200:
|
|
397
|
+
raise Exception(
|
|
398
|
+
f"Error getting business name: "
|
|
399
|
+
f"{response.status_code} {response.text}"
|
|
400
|
+
)
|
|
401
|
+
data = json.loads(response.text)
|
|
402
|
+
return data["business"]
|
|
403
|
+
|
|
404
|
+
def attach_to_purchase_invoice(
|
|
405
|
+
self,
|
|
406
|
+
loginSession,
|
|
407
|
+
purchaseInvoiceKey,
|
|
408
|
+
filename,
|
|
409
|
+
fileData,
|
|
410
|
+
fileType=None
|
|
411
|
+
):
|
|
412
|
+
import mimetypes
|
|
413
|
+
import os
|
|
414
|
+
fileNameToSend = os.path.basename(filename)
|
|
415
|
+
fileNameToSend = quote(fileNameToSend, safe="")
|
|
416
|
+
if fileType is None:
|
|
417
|
+
fileType, _ = mimetypes.guess_type(fileNameToSend)
|
|
418
|
+
if fileType is None:
|
|
419
|
+
fileType = "application/octet-stream"
|
|
420
|
+
business = self.get_business(loginSession)
|
|
421
|
+
url = (self.baseURL + _get_attachment_url(
|
|
422
|
+
purchaseInvoiceKey,
|
|
423
|
+
business["name"]
|
|
424
|
+
)).replace("/api2/", "/")
|
|
425
|
+
|
|
426
|
+
headers = {
|
|
427
|
+
"Content-Type": "application/octet-stream",
|
|
428
|
+
"X-File-Name": fileNameToSend,
|
|
429
|
+
"X-File-Type": fileType,
|
|
430
|
+
}
|
|
431
|
+
if loginSession is not None:
|
|
432
|
+
loginSession.injectHeaders(headers)
|
|
433
|
+
|
|
434
|
+
response = requests.post(
|
|
435
|
+
url=url,
|
|
436
|
+
data=fileData, # raw bytes, not files=
|
|
437
|
+
headers=headers
|
|
438
|
+
)
|
|
439
|
+
if response.status_code not in (200, 201):
|
|
440
|
+
raise Exception(
|
|
441
|
+
f"Error uploading attachment: "
|
|
442
|
+
f"{response.status_code} {response.text}"
|
|
443
|
+
)
|
|
444
|
+
data = json.loads(response.text)
|
|
445
|
+
return data
|
|
446
|
+
|
|
447
|
+
def get_transaction_reporter(self, loginSession):
|
|
448
|
+
def enrichParams(params):
|
|
449
|
+
params["fields"] = ["Date", "Transaction", "Reference", "BankOrCashAccount", "Customer",
|
|
450
|
+
"Supplier", "Employee",
|
|
451
|
+
"Description", "Item", "Account", "BalanceSheetAccount", "ProfitAndLossStatementAccount",
|
|
452
|
+
"LineDescription", "Qty", "UnitPrice", "Project", "Division", "TaxCode", "TaxAmount",
|
|
453
|
+
"Debit", "Credit", "Amount", "Timestamp"
|
|
454
|
+
]
|
|
455
|
+
gen = self._get_generator(
|
|
456
|
+
loginSession,
|
|
457
|
+
"transactions",
|
|
458
|
+
enrichParams=enrichParams
|
|
459
|
+
#,outputKey="balanceSheetTransactions"
|
|
460
|
+
)
|
|
461
|
+
return TransactionReporter(gen)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
class ExpectedEntryBase:
|
|
4
|
+
def __init__(self, companyObj, date, description):
|
|
5
|
+
self.companyObj = companyObj
|
|
6
|
+
self.date = date
|
|
7
|
+
self.description = description
|
|
8
|
+
self.checkRun = False
|
|
9
|
+
self.isPresent = False
|
|
10
|
+
self.correctionProcessWasRun = False
|
|
11
|
+
self.matched_item = None
|
|
12
|
+
|
|
13
|
+
def _get_start_output_line(self):
|
|
14
|
+
retVal = ""
|
|
15
|
+
if self.is_correctable():
|
|
16
|
+
retVal += "CANCOR "
|
|
17
|
+
else:
|
|
18
|
+
if self.isPresent:
|
|
19
|
+
if self.correctionProcessWasRun:
|
|
20
|
+
retVal += "FIXED "
|
|
21
|
+
else:
|
|
22
|
+
retVal += "OK "
|
|
23
|
+
else:
|
|
24
|
+
retVal += " "
|
|
25
|
+
retVal += (self.companyObj["shortName"] + " ")[:3] + " "
|
|
26
|
+
retVal += self.date + " "
|
|
27
|
+
retVal += self.__class__.__name__[13:] + " "
|
|
28
|
+
return retVal
|
|
29
|
+
|
|
30
|
+
def get_user_output_line(self):
|
|
31
|
+
retVal = self._get_start_output_line()
|
|
32
|
+
retVal += self.description + " "
|
|
33
|
+
retVal += str(self.amount) + " "
|
|
34
|
+
return retVal
|
|
35
|
+
|
|
36
|
+
def check(self, client, recheck=False):
|
|
37
|
+
if not recheck:
|
|
38
|
+
if self.checkRun:
|
|
39
|
+
return # Already checked
|
|
40
|
+
res = self._check(client)
|
|
41
|
+
if res is None:
|
|
42
|
+
return
|
|
43
|
+
self.isPresent = res
|
|
44
|
+
self.checkRun = True
|
|
45
|
+
|
|
46
|
+
def _checkOutcomeStr(self):
|
|
47
|
+
if not self.checkRun:
|
|
48
|
+
return ""
|
|
49
|
+
if self.isPresent:
|
|
50
|
+
return "✅ Found Entry"
|
|
51
|
+
return "❌ Missing"
|
|
52
|
+
# Checks the entry to see if it can be found
|
|
53
|
+
def _check(self, client):
|
|
54
|
+
raise Exception("__check Not Overridden in " + self.__class__.__name__)
|
|
55
|
+
|
|
56
|
+
def is_correctable(self):
|
|
57
|
+
if not self.checkRun:
|
|
58
|
+
return False
|
|
59
|
+
if self.isPresent:
|
|
60
|
+
return False # No need to correct
|
|
61
|
+
return self._is_correctable()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def correct(self, client):
|
|
65
|
+
if not self.is_correctable():
|
|
66
|
+
return 0
|
|
67
|
+
if self.isPresent:
|
|
68
|
+
raise Exception("It is correctable but element already present")
|
|
69
|
+
if self.correctionProcessWasRun:
|
|
70
|
+
raise Exception("Trying to correct twice")
|
|
71
|
+
num = self._correct(client)
|
|
72
|
+
self.check(client, recheck=True)
|
|
73
|
+
if not self.isPresent:
|
|
74
|
+
print("ERROR WHEN TRYING CORRECTION")
|
|
75
|
+
print(self.get_user_output_line())
|
|
76
|
+
print("")
|
|
77
|
+
raise Exception("Did not find created entry - bad error")
|
|
78
|
+
self.correctionProcessWasRun = True
|
|
79
|
+
return num
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# Override these both together
|
|
83
|
+
def _is_correctable(self):
|
|
84
|
+
return False
|
|
85
|
+
def _correct(self, client):
|
|
86
|
+
raise Exception("Not Overridden")
|
|
87
|
+
#return 1 #num records corrected
|
|
88
|
+
|
|
89
|
+
def _save_matched_item(self, item):
|
|
90
|
+
self.matched_item = item
|
|
91
|
+
|
|
92
|
+
def get_matched_item(self):
|
|
93
|
+
return self.matched_item
|
|
94
|
+
|
|
95
|
+
# def sorting_weight(self):
|
|
96
|
+
# return 100
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
|
|
2
|
+
from .ExpectedEntryBase import ExpectedEntryBase
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ExpectedEntryInterAccountTransfer(ExpectedEntryBase):
|
|
6
|
+
def __init__(self, companyObj, date, paidFromAccount, description, amount, receivedInAccount, reference):
|
|
7
|
+
# reference, billingAddress needed for creation
|
|
8
|
+
super().__init__(companyObj, date, description)
|
|
9
|
+
|
|
10
|
+
# Extra match on fields
|
|
11
|
+
self.paidFromAccount = paidFromAccount
|
|
12
|
+
self.receivedInAccount = receivedInAccount
|
|
13
|
+
self.amount = amount
|
|
14
|
+
|
|
15
|
+
# Other fields needed for creation
|
|
16
|
+
self.reference = reference
|
|
17
|
+
|
|
18
|
+
def get_user_output_line(self):
|
|
19
|
+
retVal = self._get_start_output_line()
|
|
20
|
+
retVal += "From " + self.paidFromAccount["name"]
|
|
21
|
+
retVal += " -> " + self.receivedInAccount["name"] + " "
|
|
22
|
+
retVal += str(self.amount) + " "
|
|
23
|
+
retVal += " " + self._checkOutcomeStr()
|
|
24
|
+
return retVal
|
|
25
|
+
|
|
26
|
+
def _check(self, client):
|
|
27
|
+
transfers = client.get_inter_account_transfer_for_description(self.companyObj["loginSession"], self.description)
|
|
28
|
+
for transfer in transfers:
|
|
29
|
+
# description already matched
|
|
30
|
+
# Has to match date amount paidFromAccount receivedInAccount
|
|
31
|
+
if transfer["date"] == self.date:
|
|
32
|
+
if transfer["amount"]["value"] == self.amount:
|
|
33
|
+
if transfer["paidFrom"] == self.paidFromAccount["name"]:
|
|
34
|
+
if transfer["receivedIn"] == self.receivedInAccount["name"]:
|
|
35
|
+
return True
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
def _is_correctable(self):
|
|
39
|
+
return True
|
|
40
|
+
def _correct(self, client):
|
|
41
|
+
transfer_result = client.create_inter_account_transfer(
|
|
42
|
+
loginSession=self.companyObj["loginSession"],
|
|
43
|
+
date=self.date,
|
|
44
|
+
reference=self.reference,
|
|
45
|
+
paidFromAccountKey=self.paidFromAccount["key"],
|
|
46
|
+
receivedInAccountKey=self.receivedInAccount["key"],
|
|
47
|
+
description=self.description,
|
|
48
|
+
amount=self.amount
|
|
49
|
+
)
|
|
50
|
+
return 1 #num records corrected
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from .ExpectedEntryBase import ExpectedEntryBase
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ExpectedEntryPurchaseInvoice(ExpectedEntryBase):
|
|
5
|
+
def __init__(self, companyObj, date, description, supplier, reference, account, lineDescription, qty, unitPrice, requiredAttachements=0, attachements=[]):
|
|
6
|
+
# reference, billingAddress needed for creation
|
|
7
|
+
super().__init__(companyObj, date, description)
|
|
8
|
+
|
|
9
|
+
# Extra match on fields
|
|
10
|
+
self.supplier = supplier
|
|
11
|
+
self.account = account
|
|
12
|
+
self.qty = qty
|
|
13
|
+
self.unitPrice = unitPrice
|
|
14
|
+
|
|
15
|
+
# Other fields needed for creation
|
|
16
|
+
self.reference = reference
|
|
17
|
+
self.lineDescription = lineDescription
|
|
18
|
+
|
|
19
|
+
self.requiredAttachements = requiredAttachements
|
|
20
|
+
self.attachements = attachements
|
|
21
|
+
if not isinstance(self.attachements, list):
|
|
22
|
+
raise Exception("Attachements should be a list")
|
|
23
|
+
for (filename, filedata) in self.attachements:
|
|
24
|
+
if not isinstance(filename, str):
|
|
25
|
+
raise Exception("Attachment Filename should be a string")
|
|
26
|
+
if not isinstance(filedata, bytes):
|
|
27
|
+
print("Got type", filedata.__class__)
|
|
28
|
+
raise Exception("Attachment filedata should be a x")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# purchaseInvoiceGenerator = client.create_purchase_invoice_generator(
|
|
32
|
+
# issueDate="2026-02-25",
|
|
33
|
+
# reference="REF" + random_string,
|
|
34
|
+
# supplierKey=supplier["key"],
|
|
35
|
+
# description=invoiceDescription
|
|
36
|
+
# )
|
|
37
|
+
# purchaseInvoiceGenerator.addLine(
|
|
38
|
+
# accountKey=expenseAccount["key"],
|
|
39
|
+
# description="Thing I really wanted",
|
|
40
|
+
# qty=1,
|
|
41
|
+
# unit_price=purchaseInvoiceAmount
|
|
42
|
+
# )
|
|
43
|
+
|
|
44
|
+
def get_user_output_line(self):
|
|
45
|
+
retVal = self._get_start_output_line()
|
|
46
|
+
retVal += self.supplier["name"] + " "
|
|
47
|
+
retVal += self.description + " "
|
|
48
|
+
retVal += str(self.qty) + "x" + str(self.unitPrice) + " "
|
|
49
|
+
retVal += "(" + self.account["name"] + ") "
|
|
50
|
+
retVal += " " + self._checkOutcomeStr()
|
|
51
|
+
return retVal
|
|
52
|
+
|
|
53
|
+
def _check(self, client):
|
|
54
|
+
# fields to match
|
|
55
|
+
# date
|
|
56
|
+
# description
|
|
57
|
+
# self.supplier = supplier
|
|
58
|
+
# self.account = account # Not going to be able to match on line account
|
|
59
|
+
# self.qty = qty
|
|
60
|
+
# self.unitPrice = unitPrice
|
|
61
|
+
invoices = client.get_purchase_invoices_for_supplier(self.companyObj["loginSession"], self.supplier["name"])
|
|
62
|
+
for invoice in invoices:
|
|
63
|
+
if invoice["issueDate"] == self.date:
|
|
64
|
+
if invoice["description"] == self.description:
|
|
65
|
+
if invoice["invoiceAmount"]["value"] == self.get_total_amount():
|
|
66
|
+
self._save_matched_item(invoice)
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
def get_name(self):
|
|
72
|
+
# Used in payment get_user_output_line display
|
|
73
|
+
return self.supplier["name"]
|
|
74
|
+
|
|
75
|
+
def get_total_amount(self):
|
|
76
|
+
return self.qty * self.unitPrice
|
|
77
|
+
|
|
78
|
+
def _is_correctable(self):
|
|
79
|
+
if len(self.attachements) != self.requiredAttachements:
|
|
80
|
+
return False
|
|
81
|
+
return True # False for now change later
|
|
82
|
+
def _correct(self, client):
|
|
83
|
+
purchaseInvoiceGenerator = client.create_purchase_invoice_generator(
|
|
84
|
+
issueDate=self.date,
|
|
85
|
+
reference=self.reference,
|
|
86
|
+
supplierKey=self.supplier["key"],
|
|
87
|
+
description=self.description
|
|
88
|
+
)
|
|
89
|
+
purchaseInvoiceGenerator.addLine(
|
|
90
|
+
accountKey=self.account["key"],
|
|
91
|
+
description=self.lineDescription,
|
|
92
|
+
qty=self.qty,
|
|
93
|
+
unit_price=self.unitPrice
|
|
94
|
+
)
|
|
95
|
+
purchase_result = client.create_purchase_invoice(self.companyObj["loginSession"], purchaseInvoiceGenerator)
|
|
96
|
+
self._save_matched_item(purchase_result)
|
|
97
|
+
|
|
98
|
+
for (filename, filedata) in self.attachements:
|
|
99
|
+
upload_attach_result = client.attach_to_purchase_invoice(
|
|
100
|
+
loginSession=self.companyObj["loginSession"],
|
|
101
|
+
purchaseInvoiceKey=purchase_result["Key"],
|
|
102
|
+
filename=filename,
|
|
103
|
+
fileData=filedata,
|
|
104
|
+
fileType=None # will guess if none is provided
|
|
105
|
+
)
|
|
106
|
+
return 1 #num records corrected
|
|
107
|
+
|
|
108
|
+
def sorting_weight(self):
|
|
109
|
+
return 50 # must before purchase invoice payments
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from .ExpectedEntryBase import ExpectedEntryBase
|
|
2
|
+
from .ExpectedEntryPurchaseInvoice import ExpectedEntryPurchaseInvoice
|
|
3
|
+
|
|
4
|
+
class ExpectedEntryPurchaseInvoicePayment(ExpectedEntryBase):
|
|
5
|
+
def __init__(self, companyObj, date, description, paidFromAccount, accountPayableSupplier, account, amount, purchaseInvoice):
|
|
6
|
+
# reference, billingAddress needed for creation
|
|
7
|
+
super().__init__(companyObj, date, description)
|
|
8
|
+
|
|
9
|
+
# Extra match on fields
|
|
10
|
+
self.paidFromAccount = paidFromAccount
|
|
11
|
+
self.accountPayableSupplier = accountPayableSupplier
|
|
12
|
+
self.amount = amount
|
|
13
|
+
|
|
14
|
+
# Other fields needed for creation
|
|
15
|
+
self.account = account # e.g. accounts payable
|
|
16
|
+
self.purchaseInvoice = purchaseInvoice
|
|
17
|
+
if not isinstance(purchaseInvoice, ExpectedEntryPurchaseInvoice):
|
|
18
|
+
raise Exception("Must have a purchase invoice to relate to")
|
|
19
|
+
if self.companyObj["name"] != self.purchaseInvoice.companyObj["name"]:
|
|
20
|
+
raise Exception("Purchase Invoice does not belong to the same company")
|
|
21
|
+
if self.amount != self.purchaseInvoice.get_total_amount():
|
|
22
|
+
raise Exception ("Purchase invoice NOT being paid in full")
|
|
23
|
+
|
|
24
|
+
# purchaseInvoicePaymentResult = client.pay_purchase_invoice(
|
|
25
|
+
# loginSession=testBusinessLoginSesison,
|
|
26
|
+
# date="2026-02-25",
|
|
27
|
+
# paidFromAccountKey=rebloutCashAccount["key"],
|
|
28
|
+
# description="pay Desc",
|
|
29
|
+
# accountKey=accountsPayable["key"],
|
|
30
|
+
# accountPayableSupplier=supplier["key"],
|
|
31
|
+
# purchaseInvoiceKey=purchase_result["Key"],
|
|
32
|
+
# amount=purchaseInvoiceAmount
|
|
33
|
+
# )
|
|
34
|
+
|
|
35
|
+
def get_user_output_line(self):
|
|
36
|
+
retVal = self._get_start_output_line()
|
|
37
|
+
retVal += "Paying " + self.purchaseInvoice.get_name() + " "
|
|
38
|
+
retVal += self.paidFromAccount["name"] + " "
|
|
39
|
+
retVal += self.description + " "
|
|
40
|
+
retVal += str(self.amount) + " "
|
|
41
|
+
retVal += "(" + self.account["name"] + ") "
|
|
42
|
+
retVal += " " + self._checkOutcomeStr()
|
|
43
|
+
return retVal
|
|
44
|
+
|
|
45
|
+
def _check(self, client):
|
|
46
|
+
# fields to match
|
|
47
|
+
# date
|
|
48
|
+
# description
|
|
49
|
+
# self.paidFromAccount = paidFromAccount
|
|
50
|
+
# self.accountPayableSupplier = accountPayableSupplier
|
|
51
|
+
# self.amount = amount
|
|
52
|
+
payments = client.get_payment_for_description(self.companyObj["loginSession"], self.description)
|
|
53
|
+
for payment in payments:
|
|
54
|
+
#print("purchase invoice Payment Match Debug", payment)
|
|
55
|
+
if payment["date"] == self.date:
|
|
56
|
+
if payment["paidFrom"] == self.paidFromAccount["name"]:
|
|
57
|
+
if payment["payee"] == self.accountPayableSupplier["name"]:
|
|
58
|
+
if payment["amount"]["value"] == self.amount:
|
|
59
|
+
return True
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
def _is_correctable(self):
|
|
63
|
+
return True
|
|
64
|
+
def _correct(self, client):
|
|
65
|
+
purchaseInvoicePaymentResult = client.pay_purchase_invoice(
|
|
66
|
+
loginSession=self.companyObj["loginSession"],
|
|
67
|
+
date=self.date,
|
|
68
|
+
paidFromAccountKey=self.paidFromAccount["key"],
|
|
69
|
+
description=self.description,
|
|
70
|
+
accountKey=self.account["key"],
|
|
71
|
+
accountPayableSupplier=self.accountPayableSupplier["key"],
|
|
72
|
+
purchaseInvoiceKey=self.purchaseInvoice.get_matched_item()["key"],
|
|
73
|
+
amount=self.amount
|
|
74
|
+
)
|
|
75
|
+
return 1 #num records corrected
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from .ExpectedEntryBase import ExpectedEntryBase
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ExpectedEntrySalesInvoice(ExpectedEntryBase):
|
|
5
|
+
def __init__(self, companyObj, issueDate, description, customer, reference, billingAddress, account, lineDescription, qty, unitPrice):
|
|
6
|
+
# reference, billingAddress needed for creation
|
|
7
|
+
super().__init__(companyObj, date=issueDate, description=description)
|
|
8
|
+
|
|
9
|
+
# Extra match on fields
|
|
10
|
+
self.customer = customer
|
|
11
|
+
self.account = account
|
|
12
|
+
self.qty = qty
|
|
13
|
+
self.unitPrice = unitPrice
|
|
14
|
+
|
|
15
|
+
# Other fields needed for creation
|
|
16
|
+
self.reference = reference
|
|
17
|
+
self.lineDescription = lineDescription
|
|
18
|
+
self.billingAddress = billingAddress
|
|
19
|
+
|
|
20
|
+
# salesInvoiceGenerator = client.create_sales_invoice_generator(
|
|
21
|
+
# issueDate="2026-01-31",
|
|
22
|
+
# reference="REF",
|
|
23
|
+
# customerKey=customer["key"],
|
|
24
|
+
# billingAddress="One the road\nthe street\nth towh, DF3 F45",
|
|
25
|
+
# description=invoiceDescription
|
|
26
|
+
# )
|
|
27
|
+
# salesInvoiceGenerator.addLine(
|
|
28
|
+
# accountKey=account["key"],
|
|
29
|
+
# description="TODO",
|
|
30
|
+
# qty=2,
|
|
31
|
+
# unit_price=12.34
|
|
32
|
+
# )
|
|
33
|
+
|
|
34
|
+
def get_user_output_line(self):
|
|
35
|
+
retVal = self._get_start_output_line()
|
|
36
|
+
retVal += self.customer["name"] + " "
|
|
37
|
+
retVal += self.description + " "
|
|
38
|
+
retVal += str(self.qty) + "x" + str(self.unitPrice) + " "
|
|
39
|
+
retVal += "(" + self.account["name"] + ") "
|
|
40
|
+
retVal += " " + self._checkOutcomeStr()
|
|
41
|
+
return retVal
|
|
42
|
+
|
|
43
|
+
def _check(self, client):
|
|
44
|
+
# fields to match
|
|
45
|
+
# date
|
|
46
|
+
# description
|
|
47
|
+
# self.customer = customer
|
|
48
|
+
# self.account = account # Not going to be able to match on line account
|
|
49
|
+
# self.qty = qty
|
|
50
|
+
# self.unitPrice = unitPrice
|
|
51
|
+
invoices = client.get_sales_invoices_for_customer(self.companyObj["loginSession"], self.customer["name"])
|
|
52
|
+
for invoice in invoices:
|
|
53
|
+
if invoice["issueDate"] == self.date:
|
|
54
|
+
if invoice["description"] == self.description:
|
|
55
|
+
if invoice["invoiceAmount"]["value"] == self.get_total_amount():
|
|
56
|
+
self._save_matched_item(invoice)
|
|
57
|
+
return True
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
def get_name(self):
|
|
61
|
+
# Used in payment get_user_output_line display
|
|
62
|
+
return self.customer["name"]
|
|
63
|
+
|
|
64
|
+
def get_total_amount(self):
|
|
65
|
+
return self.qty * self.unitPrice
|
|
66
|
+
|
|
67
|
+
def _is_correctable(self):
|
|
68
|
+
return True
|
|
69
|
+
def _correct(self, client):
|
|
70
|
+
salesInvoiceGenerator = client.create_sales_invoice_generator(
|
|
71
|
+
issueDate=self.date,
|
|
72
|
+
reference=self.reference,
|
|
73
|
+
customerKey=self.customer["key"],
|
|
74
|
+
billingAddress=self.billingAddress,
|
|
75
|
+
description=self.description
|
|
76
|
+
)
|
|
77
|
+
salesInvoiceGenerator.addLine(
|
|
78
|
+
accountKey=self.account["key"],
|
|
79
|
+
description=self.lineDescription,
|
|
80
|
+
qty=self.qty,
|
|
81
|
+
unit_price=self.unitPrice
|
|
82
|
+
)
|
|
83
|
+
sales_invoice_info = client.create_sales_invoice(self.companyObj["loginSession"], salesInvoiceGenerator)
|
|
84
|
+
self._save_matched_item(sales_invoice_info)
|
|
85
|
+
return 1
|
|
86
|
+
|
|
87
|
+
def sorting_weight(self):
|
|
88
|
+
return 50 # must before purchase invoice patments
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from .ExpectedEntryBase import ExpectedEntryBase
|
|
2
|
+
from .ExpectedEntrySalesInvoice import ExpectedEntrySalesInvoice
|
|
3
|
+
|
|
4
|
+
class ExpectedEntrySalesInvoiceReceipt(ExpectedEntryBase):
|
|
5
|
+
def __init__(self, companyObj, date, description, receivedAccount, accountRecivablesCustomer, account, amount, salesInvoice):
|
|
6
|
+
super().__init__(companyObj, date, description)
|
|
7
|
+
|
|
8
|
+
# Extra match on fields
|
|
9
|
+
self.receivedAccount = receivedAccount
|
|
10
|
+
self.accountReceivablesCustomer = accountRecivablesCustomer
|
|
11
|
+
self.amount = amount
|
|
12
|
+
|
|
13
|
+
# Other fields needed for creation
|
|
14
|
+
self.account = account # e.g. accounts payable
|
|
15
|
+
self.salesInvoice = salesInvoice
|
|
16
|
+
if not isinstance(salesInvoice, ExpectedEntrySalesInvoice):
|
|
17
|
+
raise Exception("Must have a sales invoice to relate to")
|
|
18
|
+
if self.companyObj["name"] != self.salesInvoice.companyObj["name"]:
|
|
19
|
+
raise Exception("Sales Invoice does not belong to the same company")
|
|
20
|
+
if self.amount != self.salesInvoice.get_total_amount():
|
|
21
|
+
raise Exception ("Sales invoice NOT being paid in full")
|
|
22
|
+
|
|
23
|
+
# result = client.pay_sales_invoice(
|
|
24
|
+
# loginSession=testBusinessLoginSesison,
|
|
25
|
+
# date="2026-11-30",
|
|
26
|
+
# recievedAccountKey=rebloutCashAccount["key"],
|
|
27
|
+
# description="pay Desc",
|
|
28
|
+
# accountKey=accountsRecievable["key"],
|
|
29
|
+
# accountReceivablesCustomer=customer["key"],
|
|
30
|
+
# salesInvoiceKey=looked_up_sales_invoice["key"],
|
|
31
|
+
# amount=1.01
|
|
32
|
+
# )
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_user_output_line(self):
|
|
36
|
+
retVal = self._get_start_output_line()
|
|
37
|
+
retVal += "Paying " + self.salesInvoice.get_name() + " "
|
|
38
|
+
retVal += self.receivedAccount["name"] + " "
|
|
39
|
+
retVal += self.description + " "
|
|
40
|
+
retVal += str(self.amount) + " "
|
|
41
|
+
retVal += "(" + self.account["name"] + ") "
|
|
42
|
+
retVal += " " + self._checkOutcomeStr()
|
|
43
|
+
return retVal
|
|
44
|
+
|
|
45
|
+
def _check(self, client):
|
|
46
|
+
# fields to match
|
|
47
|
+
# date
|
|
48
|
+
# description
|
|
49
|
+
# self.receivedAccount = receivedAccount
|
|
50
|
+
# self.accountReceivablesCustomer = accountReceivablesCustomer
|
|
51
|
+
# self.amount = amount
|
|
52
|
+
# Paid By is always going to be None
|
|
53
|
+
payments = client.get_receipt_for_description(self.companyObj["loginSession"], self.description)
|
|
54
|
+
for payment in payments:
|
|
55
|
+
##print("sales invoice receipt Match Debug", payment)
|
|
56
|
+
if payment["date"] == self.date:
|
|
57
|
+
if payment["paidBy"] == self.accountReceivablesCustomer["name"]:
|
|
58
|
+
if payment["receivedIn"]["key"] == self.receivedAccount["key"]:
|
|
59
|
+
if payment["amount"]["value"] == self.amount:
|
|
60
|
+
return True
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
def _is_correctable(self):
|
|
64
|
+
return True
|
|
65
|
+
def _correct(self, client):
|
|
66
|
+
result = client.pay_sales_invoice(
|
|
67
|
+
loginSession=self.companyObj["loginSession"],
|
|
68
|
+
date=self.date,
|
|
69
|
+
recievedAccountKey=self.receivedAccount["key"],
|
|
70
|
+
description=self.description,
|
|
71
|
+
accountKey=self.account["key"],
|
|
72
|
+
accountRecivablesCustomer=self.accountReceivablesCustomer["key"],
|
|
73
|
+
salesInvoiceKey=self.salesInvoice.get_matched_item()["key"],
|
|
74
|
+
amount=self.amount
|
|
75
|
+
)
|
|
76
|
+
return 1 #num records corrected
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Pattern:
|
|
5
|
+
entries = None
|
|
6
|
+
key = None
|
|
7
|
+
def __init__(self,key):
|
|
8
|
+
self.entries = []
|
|
9
|
+
self.key = key
|
|
10
|
+
def add_entry(self, expected_entry):
|
|
11
|
+
self.entries.append(expected_entry)
|
|
12
|
+
|
|
13
|
+
def get_key(self):
|
|
14
|
+
return self.key
|
|
15
|
+
|
|
16
|
+
def get_user_output(self):
|
|
17
|
+
retVal = ""
|
|
18
|
+
|
|
19
|
+
for entry in self.entries:
|
|
20
|
+
retVal += entry.get_user_output_line() + "\n"
|
|
21
|
+
|
|
22
|
+
return retVal
|
|
23
|
+
|
|
24
|
+
def check_all_enteries(self, client):
|
|
25
|
+
for entry in self.entries:
|
|
26
|
+
entry.check(client)
|
|
27
|
+
|
|
28
|
+
def num_correctable_entries(self, client):
|
|
29
|
+
self.check_all_enteries(client)
|
|
30
|
+
retVal =0
|
|
31
|
+
for entry in self.entries:
|
|
32
|
+
if entry.is_correctable():
|
|
33
|
+
retVal += 1
|
|
34
|
+
return retVal
|
|
35
|
+
|
|
36
|
+
def is_correctable(self, client):
|
|
37
|
+
self.check_all_enteries(client)
|
|
38
|
+
for entry in self.entries:
|
|
39
|
+
if not entry.is_correctable():
|
|
40
|
+
return False
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
def run_corrections(self, client):
|
|
44
|
+
number_corrected = 0
|
|
45
|
+
##self.entries.sort(key=lambda entry: entry.sorting_weight())
|
|
46
|
+
for entry in self.entries:
|
|
47
|
+
number_corrected += entry.correct(client)
|
|
48
|
+
return number_corrected
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from .Pattern import Pattern
|
|
2
|
+
from .ExpectedEntrySalesInvoice import ExpectedEntrySalesInvoice
|
|
3
|
+
from .ExpectedEntryInterAccountTransfer import ExpectedEntryInterAccountTransfer
|
|
4
|
+
from .ExpectedEntryPurchaseInvoice import ExpectedEntryPurchaseInvoice
|
|
5
|
+
from .ExpectedEntryPurchaseInvoicePayment import ExpectedEntryPurchaseInvoicePayment
|
|
6
|
+
from .ExpectedEntrySalesInvoiceReceipt import ExpectedEntrySalesInvoiceReceipt
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from PythonAPIClientBase import LoginSession
|
|
2
|
+
|
|
3
|
+
class ManagerLoginSession(LoginSession):
|
|
4
|
+
token = None
|
|
5
|
+
def __init__(self, client, token):
|
|
6
|
+
self.client = client
|
|
7
|
+
self.token = token
|
|
8
|
+
|
|
9
|
+
def injectHeaders(self, headers):
|
|
10
|
+
headers["X-API-KEY"] = self.token
|
|
11
|
+
|
|
12
|
+
def refresh(self):
|
|
13
|
+
return False
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ManagerLoginSessionFromAuthToken(ManagerLoginSession):
|
|
17
|
+
def __init__(self, client, access_token):
|
|
18
|
+
super().__init__(client=client, token=access_token)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
class PurchaseInvoiceGenerator:
|
|
4
|
+
lines = None
|
|
5
|
+
def __init__(self, issueDate, reference, supplierKey, description):
|
|
6
|
+
self.issueDate = issueDate
|
|
7
|
+
self.reference = reference
|
|
8
|
+
self.supplierKey = supplierKey
|
|
9
|
+
self.description = description
|
|
10
|
+
self.lines = []
|
|
11
|
+
|
|
12
|
+
def addLine(self, accountKey, description, qty, unit_price):
|
|
13
|
+
self.lines.append((accountKey, description, qty, unit_price))
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
class SalesInvoiceGenerator:
|
|
4
|
+
lines = None
|
|
5
|
+
def __init__(self, issueDate, reference, customerKey, billingAddress, description):
|
|
6
|
+
self.issueDate = issueDate
|
|
7
|
+
self.reference = reference
|
|
8
|
+
self.customerKey = customerKey
|
|
9
|
+
self.billingAddress = billingAddress
|
|
10
|
+
self.description = description
|
|
11
|
+
self.lines = []
|
|
12
|
+
|
|
13
|
+
def addLine(self, accountKey, description, qty, unit_price):
|
|
14
|
+
self.lines.append((accountKey, description, qty, unit_price))
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
CASH_ACCOUNT_NAME = "Cash & cash equivalents"
|
|
4
|
+
|
|
5
|
+
class TransactionReporter:
|
|
6
|
+
mainData = None
|
|
7
|
+
# Dict, first key is the name of the bankOrCashAccount
|
|
8
|
+
def __init__(self, generator):
|
|
9
|
+
self.mainData = {}
|
|
10
|
+
for transaction in generator:
|
|
11
|
+
transaction["amount"] = Decimal(str(transaction["amount"]))
|
|
12
|
+
if transaction["bankOrCashAccount"] not in self.mainData:
|
|
13
|
+
self.mainData[transaction["bankOrCashAccount"]] = []
|
|
14
|
+
self.mainData[transaction["bankOrCashAccount"]].append(transaction)
|
|
15
|
+
|
|
16
|
+
def get_bank_or_cash_accounts(self):
|
|
17
|
+
return [account for account in self.mainData if account is not None]
|
|
18
|
+
|
|
19
|
+
def get_bank_or_cash_account_entries(self, bankOrCashAccountName):
|
|
20
|
+
if bankOrCashAccountName not in self.mainData:
|
|
21
|
+
raise Exception("bankOrCashAccountName not found")
|
|
22
|
+
retVal = []
|
|
23
|
+
for entry in self.mainData[bankOrCashAccountName]:
|
|
24
|
+
if entry["account"] == CASH_ACCOUNT_NAME:
|
|
25
|
+
retVal.append(entry)
|
|
26
|
+
return sorted(retVal, key=lambda entry: entry["date"])
|
|
27
|
+
|
|
28
|
+
def get_customer_receipts(self, customerName):
|
|
29
|
+
retVal = []
|
|
30
|
+
for account in self.mainData.keys():
|
|
31
|
+
if account is not None:
|
|
32
|
+
for entry in self.mainData[account]:
|
|
33
|
+
if entry["transaction"] == "Receipt":
|
|
34
|
+
if entry["customer"] == customerName:
|
|
35
|
+
if entry["account"] == CASH_ACCOUNT_NAME:
|
|
36
|
+
retVal.append(entry)
|
|
37
|
+
return sorted(retVal, key=lambda entry: entry["date"])
|
|
38
|
+
|
|
39
|
+
def get_supplier_payments(self, supplierName):
|
|
40
|
+
retVal = []
|
|
41
|
+
for account in self.mainData.keys():
|
|
42
|
+
if account is not None:
|
|
43
|
+
for entry in self.mainData[account]:
|
|
44
|
+
if entry["transaction"] == "Payment":
|
|
45
|
+
if entry["supplier"] == supplierName:
|
|
46
|
+
if entry["account"] == CASH_ACCOUNT_NAME:
|
|
47
|
+
retVal.append(entry)
|
|
48
|
+
return sorted(retVal, key=lambda entry: entry["date"])
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from .ManagerLoginSession import ManagerLoginSessionFromAuthToken
|
|
2
|
+
from .Client import ManagerClient
|
|
3
|
+
from .ExpectedEntry import Pattern, ExpectedEntrySalesInvoice, ExpectedEntryInterAccountTransfer, ExpectedEntryPurchaseInvoice, ExpectedEntryPurchaseInvoicePayment, ExpectedEntrySalesInvoiceReceipt
|
|
4
|
+
|
|
5
|
+
def hello_world() -> str:
|
|
6
|
+
"""Return the library's initial greeting."""
|
|
7
|
+
return "Hello, world!"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ManagerPythonClient
|
|
3
|
+
Version: 0.0.8
|
|
4
|
+
Summary: Manager python client for accessing API.
|
|
5
|
+
Project-URL: Homepage, https://github.com/rmetcalf9/ManagerPythonClient
|
|
6
|
+
Project-URL: Repository, https://github.com/rmetcalf9/ManagerPythonClient
|
|
7
|
+
Author: Robert Metcalf
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 Robert Metcalf
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Classifier: Development Status :: 3 - Alpha
|
|
31
|
+
Classifier: Intended Audience :: Developers
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Programming Language :: Python :: 3
|
|
34
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
38
|
+
Requires-Python: >=3.11
|
|
39
|
+
Requires-Dist: inquirerpy==0.3.4
|
|
40
|
+
Requires-Dist: pythonapiclientbase==0.0.19
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# Python client for Manager API
|
|
44
|
+
|
|
45
|
+
## Development
|
|
46
|
+
|
|
47
|
+
Run the tests with:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uv run --with pytest pytest
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Build the distribution with:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
uv build
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Release
|
|
60
|
+
|
|
61
|
+
use release.sh
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
manager_python_client/Client.py,sha256=RkQ5JxRj9kcQPgHM2ZAeDvqqhUL9j9Y2HrKZWXKaGLo,17323
|
|
2
|
+
manager_python_client/ManagerLoginSession.py,sha256=2Yh7sg1rOz5Vl7yreJOg2rs75Ips6DvwflPGFu5I6zc,494
|
|
3
|
+
manager_python_client/PurchaseInvoiceGenerator.py,sha256=tqLfDH2-8cyn5Wdb0meUFkpd7F-m5ZIri0r7eyLsDhs,431
|
|
4
|
+
manager_python_client/SalesInvoiceGenerator.py,sha256=haDXdKEYYFX887CAtSqdQiPBWURIA6X-Gd9Vb94_NI8,489
|
|
5
|
+
manager_python_client/TransactionReporter.py,sha256=G0E6NjnYbkKD-bUZMZN25xoKq5xvQg3QSoajVnmtUjI,2138
|
|
6
|
+
manager_python_client/__init__.py,sha256=1HV81Wo_THzZoRHubyu4muB2j2Qca3hYo-I1F81FIuI,401
|
|
7
|
+
manager_python_client/ExpectedEntry/ExpectedEntryBase.py,sha256=FxQbuvTdQXaO8CvgcRMg5KvnNtG8AWq4oCnzEtfFmos,2921
|
|
8
|
+
manager_python_client/ExpectedEntry/ExpectedEntryInterAccountTransfer.py,sha256=LJte0MT85nFIEh7LSevwehz0QJQGWFNYTHcB_wYesYY,2044
|
|
9
|
+
manager_python_client/ExpectedEntry/ExpectedEntryPurchaseInvoice.py,sha256=T9NPeErOYdo3-ZPbFIO2x9U9FndvBY8FtWKhI54Zizo,4284
|
|
10
|
+
manager_python_client/ExpectedEntry/ExpectedEntryPurchaseInvoicePayment.py,sha256=RizfiVohV5BJ3WWHXW0W1ZQx9IhByCKigw-WKVCzyEs,3416
|
|
11
|
+
manager_python_client/ExpectedEntry/ExpectedEntrySalesInvoice.py,sha256=tun6gMS4hcVg3LZN3qbcxbKLq_WrSfu0uW0QAUwvorg,3276
|
|
12
|
+
manager_python_client/ExpectedEntry/ExpectedEntrySalesInvoiceReceipt.py,sha256=WnsqHKtIQAp3WlyIbJEdfFhfwFL56onxMFLZKisPnGA,3340
|
|
13
|
+
manager_python_client/ExpectedEntry/Pattern.py,sha256=YL3EiCOD9b_IARnDDFtc3UJ1z0DaT-9LmM1P-C3EuGE,1237
|
|
14
|
+
manager_python_client/ExpectedEntry/__init__.py,sha256=ji1JulYKLCLkumsqQG4dZ0v08LwTuREKXcVBF7NiGds,410
|
|
15
|
+
managerpythonclient-0.0.8.dist-info/METADATA,sha256=S2ToKeFgFEbSI9togGQZWvALC8w9GqX72IwTHdudYQ0,2270
|
|
16
|
+
managerpythonclient-0.0.8.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
17
|
+
managerpythonclient-0.0.8.dist-info/licenses/LICENSE,sha256=ODtATG_0rj1J6X6amg9J_EU-Re2PnJX88IE_vyH6GSA,1071
|
|
18
|
+
managerpythonclient-0.0.8.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Robert Metcalf
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|