mirror of
https://github.com/telavivmakers/boring-admin.git
synced 2024-05-25 11:56:55 +03:00
better comments and docstrings
This commit is contained in:
parent
57fe74e75d
commit
5abf19798f
|
@ -1,21 +1,20 @@
|
||||||
#!/usr/bin/python
|
#!/usr/bin/python
|
||||||
"""
|
"""
|
||||||
TAMI invoice bank-to-invoice connection
|
Creates receipts in TAMI's accounting provider, from bank-reports.
|
||||||
receive: (XXXXX) bank's transactions list, in Excel format
|
|
||||||
purpose:
|
input: (XXXXX) bank's transactions list, in Excel format [currently: TSV from google-sheets]
|
||||||
issue invoices
|
output: a new donation-recepit (type#405) at GreenInvoice.co.il
|
||||||
(maybe) send them to whoever.
|
|
||||||
|
|
||||||
usage:
|
usage:
|
||||||
bank2invoice.py real <excel-file-1> [<excel-file-2> ...]
|
bank2invoice.py real <excel-file-1> [<excel-file-2> ...]
|
||||||
bank2invoice.py search
|
bank2invoice.py search # get latest existing receipts
|
||||||
bank2invoice.py test
|
bank2invoice.py test #
|
||||||
|
|
||||||
by default, running in TEST mode (inside a sandbox)
|
*REAL vs TEST modes:*
|
||||||
if you want to get things into the real accounting system,
|
by default, running in TEST mode (inside a sandbox).
|
||||||
add the command line argument: real
|
if you want to get things into the real accounting system, add the command line argument: real
|
||||||
|
|
||||||
test is using built-in data file, which is embedded in this file
|
The "test" command is using built-in data file, which is embedded in this module.
|
||||||
|
|
||||||
started by shaharr.info,
|
started by shaharr.info,
|
||||||
on request of yair, 2023-02-15
|
on request of yair, 2023-02-15
|
||||||
|
@ -55,7 +54,8 @@ class AttrDict(dict):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
""" # ----- copied from green_invoice/resources/resource.py, merely for documentation! -----
|
||||||
|
|
||||||
class DocumentType(enum.IntEnum):
|
class DocumentType(enum.IntEnum):
|
||||||
PRICE_QUOTE = 10
|
PRICE_QUOTE = 10
|
||||||
ORDER = 100
|
ORDER = 100
|
||||||
|
@ -71,7 +71,6 @@ class DocumentType(enum.IntEnum):
|
||||||
RECEIPT_OF_A_DEPOSIT = 600
|
RECEIPT_OF_A_DEPOSIT = 600
|
||||||
WITHDRAWAL_OF_DEPOSIT = 610
|
WITHDRAWAL_OF_DEPOSIT = 610
|
||||||
|
|
||||||
|
|
||||||
class DocumentStatus(enum.IntEnum):
|
class DocumentStatus(enum.IntEnum):
|
||||||
OPENED_DOCUMENT = 0
|
OPENED_DOCUMENT = 0
|
||||||
CLOSED_DOCUMENT = 1
|
CLOSED_DOCUMENT = 1
|
||||||
|
@ -79,19 +78,16 @@ class DocumentStatus(enum.IntEnum):
|
||||||
CANCELING_OTHER_DOCUMENT = 3
|
CANCELING_OTHER_DOCUMENT = 3
|
||||||
CANCELED_DOCUMENT = 4
|
CANCELED_DOCUMENT = 4
|
||||||
|
|
||||||
|
|
||||||
class DocumentLanguage(str, enum.Enum):
|
class DocumentLanguage(str, enum.Enum):
|
||||||
HEBREW = "he"
|
HEBREW = "he"
|
||||||
ENGLISH = "en"
|
ENGLISH = "en"
|
||||||
|
|
||||||
|
|
||||||
class Currency(str, enum.Enum):
|
class Currency(str, enum.Enum):
|
||||||
ILS = "ILS"
|
ILS = "ILS"
|
||||||
USD = "USD"
|
USD = "USD"
|
||||||
EUR = "EUR"
|
EUR = "EUR"
|
||||||
GBP = "GBP"
|
GBP = "GBP"
|
||||||
|
|
||||||
|
|
||||||
class PaymentType(enum.IntEnum):
|
class PaymentType(enum.IntEnum):
|
||||||
UNPAID = -1
|
UNPAID = -1
|
||||||
DEDUCTION_AT_SOURCE = 0
|
DEDUCTION_AT_SOURCE = 0
|
||||||
|
@ -104,7 +100,7 @@ class PaymentType(enum.IntEnum):
|
||||||
OTHER = 11
|
OTHER = 11
|
||||||
|
|
||||||
|
|
||||||
"""
|
# ----- end of documentation ---- """
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -112,7 +108,7 @@ class PaymentType(enum.IntEnum):
|
||||||
flags = re.M+re.I+re.S
|
flags = re.M+re.I+re.S
|
||||||
conffile = expanduser('~/.bank2invoice.ini')
|
conffile = expanduser('~/.bank2invoice.ini')
|
||||||
#conf = Config(json_file=expanduser('~/.config/someapp.json'))
|
#conf = Config(json_file=expanduser('~/.config/someapp.json'))
|
||||||
payment_type_map = {
|
payment_type_map = { # see docstring in guess_payment_type()
|
||||||
'^bit העברת כספים$': 10,
|
'^bit העברת כספים$': 10,
|
||||||
'.*מזומן.*': 1,
|
'.*מזומן.*': 1,
|
||||||
}
|
}
|
||||||
|
@ -176,6 +172,7 @@ def err(msg, fatal=False):
|
||||||
|
|
||||||
|
|
||||||
def guess_payment_type(payment, conf):
|
def guess_payment_type(payment, conf):
|
||||||
|
""" for guessing trx classification based on payment's comments. """
|
||||||
for key in payment_type_map.keys():
|
for key in payment_type_map.keys():
|
||||||
if re.match(key, payment.comments):
|
if re.match(key, payment.comments):
|
||||||
return payment_type_map[key]
|
return payment_type_map[key]
|
||||||
|
@ -193,6 +190,7 @@ def read_conf():
|
||||||
return conf
|
return conf
|
||||||
|
|
||||||
def normalize_dates(s):
|
def normalize_dates(s):
|
||||||
|
""" convert dd/mm/yyyy (eu dates) to yyyy-mm-dd (ISO dates) """
|
||||||
return re.sub(r'([0-3]\d)/([01]\d)/(20[23]\d)', r'\3-\2-\1', s, flags=flags)
|
return re.sub(r'([0-3]\d)/([01]\d)/(20[23]\d)', r'\3-\2-\1', s, flags=flags)
|
||||||
|
|
||||||
|
|
||||||
|
@ -200,18 +198,17 @@ def get_existing_documents(dtype):
|
||||||
""" purpose: to prevent duplicates,
|
""" purpose: to prevent duplicates,
|
||||||
and to get last receipt date (cant add earlier reciept)"""
|
and to get last receipt date (cant add earlier reciept)"""
|
||||||
|
|
||||||
# search documents since 100 days ago
|
# search documents since 100 days ago. earlier dates aren't relevant. I hope.
|
||||||
from_date = (datetime.today() - timedelta(days=100)).strftime('%Y-%m-%d')
|
from_date = (datetime.today() - timedelta(days=100)).strftime('%Y-%m-%d')
|
||||||
to_date = iso_date()
|
to_date = iso_date()
|
||||||
documentResource = DocumentResource()
|
documentResource = DocumentResource()
|
||||||
Payment.latest = INITIAL_LATEST_PAYMENT
|
Payment.latest = INITIAL_LATEST_PAYMENT
|
||||||
results = []
|
results = []
|
||||||
docs = set()
|
docs = set()
|
||||||
PAGES = 6
|
PAGES = 6 # just a random number; green-invoice's paging system is beyond logic. 6 sounds nice. 10 was taking too long. 1 page with pagesize 300 was giving only 25 recs per page anyway.
|
||||||
|
|
||||||
for page in range(PAGES):
|
for page in range(PAGES):
|
||||||
print(f'downloading existing docs, page {page} / {PAGES}')
|
print(f'downloading existing docs, page {page} / {PAGES}')
|
||||||
|
|
||||||
params = dumps({
|
params = dumps({
|
||||||
"page": page, "pageSize": 50,
|
"page": page, "pageSize": 50,
|
||||||
"type": [ dtype ], "sort": "documentDate",
|
"type": [ dtype ], "sort": "documentDate",
|
||||||
|
@ -225,7 +222,7 @@ def get_existing_documents(dtype):
|
||||||
|
|
||||||
|
|
||||||
for ret in results:
|
for ret in results:
|
||||||
#print(ret)
|
# convert green-invoices entity to our Payment() object. We need that to compare -> prevent duplicates.
|
||||||
doc = Payment( **dict(
|
doc = Payment( **dict(
|
||||||
type=ret['type'],
|
type=ret['type'],
|
||||||
pay_date=ret['payment'][0]["date"],
|
pay_date=ret['payment'][0]["date"],
|
||||||
|
@ -234,7 +231,7 @@ def get_existing_documents(dtype):
|
||||||
client_name=ret['client']['name'],
|
client_name=ret['client']['name'],
|
||||||
comments=ret['remarks']
|
comments=ret['remarks']
|
||||||
) )
|
) )
|
||||||
docs.add(doc)
|
docs.add(doc) # unique items only
|
||||||
Payment.latest = max(Payment.latest, doc.pay_date)
|
Payment.latest = max(Payment.latest, doc.pay_date)
|
||||||
print(f'{Payment.latest=} vs. {doc.pay_date=}')
|
print(f'{Payment.latest=} vs. {doc.pay_date=}')
|
||||||
return docs
|
return docs
|
||||||
|
@ -251,30 +248,30 @@ def payment_exists(payment, existing):
|
||||||
|
|
||||||
def main(xl_file, conf):
|
def main(xl_file, conf):
|
||||||
green_invoice.client.configure(
|
green_invoice.client.configure(
|
||||||
env="sandbox",
|
env="sandbox", # actually i didn't test it yet in the real system. donno whats this.
|
||||||
api_key_id = conf.api_key_id,
|
api_key_id = conf.api_key_id,
|
||||||
api_key_secret = conf.api_key_secret,
|
api_key_secret = conf.api_key_secret,
|
||||||
logger=logging.root,
|
logger=logging.root,
|
||||||
)
|
)
|
||||||
|
|
||||||
existing = get_existing_documents(default_doctype) # needed to prevent duplicates
|
existing = get_existing_documents(default_doctype) # for preventing duplicates, later
|
||||||
|
|
||||||
s = open(xl_file).read()
|
s = open(xl_file).read()
|
||||||
s = normalize_dates(s) # convert israeli (european) dates to ISO dates
|
s = normalize_dates(s) # convert israeli (european) dates to ISO dates:
|
||||||
# green-invoice requires them,
|
# green-invoice requires them,
|
||||||
# plus we must sort by date.
|
# plus we must sort by date.
|
||||||
lines = s.splitlines()
|
lines = s.splitlines()
|
||||||
lines.sort()
|
lines.sort()
|
||||||
for line in lines:
|
for line in lines:
|
||||||
if not '\t' in line: continue # empty lines
|
if not '\t' in line: continue # empty lines
|
||||||
if line[:2] !='20': continue # should be year; header lines
|
if line[:2] !='20': continue # must start with the year; otherwise it's a header line
|
||||||
payment = Payment(line=line)
|
payment = Payment(line=line)
|
||||||
if not payment: continue
|
if not payment: continue
|
||||||
if payment_exists(payment, existing):
|
if payment_exists(payment, existing):
|
||||||
print('payment already documented')
|
print('payment already in the system. skip')
|
||||||
continue
|
continue
|
||||||
id, url = create_receipt(payment, conf)
|
id, url = create_receipt(payment, conf)
|
||||||
#.
|
# @todo: send url to donnor. but we dont have their contact here.
|
||||||
|
|
||||||
|
|
||||||
def create_receipt(payment, conf):
|
def create_receipt(payment, conf):
|
||||||
|
@ -298,17 +295,16 @@ def create_receipt(payment, conf):
|
||||||
"signed": True,
|
"signed": True,
|
||||||
"rounding": False,
|
"rounding": False,
|
||||||
"remarks": payment.comments,
|
"remarks": payment.comments,
|
||||||
#"income": [
|
#"income": [ # ------- according to support, this is for חשבונית מס which we don't use in our non-profit org.
|
||||||
# {
|
# {
|
||||||
# "price": payment.amount,
|
# "price": payment.amount,
|
||||||
# "currency": payment.currency,
|
# "currency": payment.currency,
|
||||||
# "quantity": 1,
|
# "quantity": 1,
|
||||||
# "description": DEFAULT_TXN_DESCRIPTION,
|
# "description": DEFAULT_TXN_DESCRIPTION,
|
||||||
# "vatType": IncomeVatType.DEFAULT, # based on the business type
|
# "vatType": IncomeVatType.DEFAULT, # based on the business type
|
||||||
#
|
|
||||||
# }
|
# }
|
||||||
#],
|
#],
|
||||||
"payment": [
|
"payment": [ # ---- according to support, this is for קבלה, which is the specific accounting status we use here.
|
||||||
{
|
{
|
||||||
"type": guess_payment_type(payment, conf),
|
"type": guess_payment_type(payment, conf),
|
||||||
"date": payment.pay_date,
|
"date": payment.pay_date,
|
||||||
|
@ -326,26 +322,25 @@ def create_receipt(payment, conf):
|
||||||
)
|
)
|
||||||
id = doc['id']
|
id = doc['id']
|
||||||
url = doc['id']
|
url = doc['id']
|
||||||
#get_document_download_link(id)
|
|
||||||
return id, url
|
return id, url
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Payment:
|
class Payment:
|
||||||
latest = INITIAL_LATEST_PAYMENT
|
""" represents a transaction's data """
|
||||||
|
|
||||||
|
latest = INITIAL_LATEST_PAYMENT # global scope
|
||||||
|
|
||||||
def __init__(self, **kw):
|
def __init__(self, **kw):
|
||||||
"""parse a line from the bank report;
|
"""parse a line from the bank report;
|
||||||
right now i've downloaded a tab-separated file from google-spreadsheet;
|
right now i've downloaded a tab-separated file from google-spreadsheet;
|
||||||
|
|
||||||
overloaded usage:
|
overloaded usage:
|
||||||
obj = Payment(line) <- string, tab-separated, from excel
|
obj = Payment(line) <- string, tab-separated, from excel
|
||||||
obj = Payment({date:x, name:y, ...}) <- all object properties
|
obj = Payment({date:x, name:y, ...}) <- all object properties
|
||||||
"""
|
"""
|
||||||
kw = AttrDict(kw)
|
|
||||||
self.type = default_doctype
|
self.type = default_doctype
|
||||||
if 'line' in kw:
|
if 'line' in kw:
|
||||||
parts = kw.line.split('\t')
|
parts = kw['line'].split('\t')
|
||||||
if len(parts) != 7:
|
if len(parts) != 7:
|
||||||
print('shit, bank data record must have 7 columns exactly')
|
print('shit, bank data record must have 7 columns exactly')
|
||||||
raise Exception # either a bug, or format change, must re-adapt the code !
|
raise Exception # either a bug, or format change, must re-adapt the code !
|
||||||
|
@ -360,7 +355,11 @@ class Payment:
|
||||||
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
#self.pay_date, self.client_name, self.bank, self.snif, self.account, self.amount, self.comments
|
""" represents the unique signature for transactions;
|
||||||
|
used for comparing against existing records/transactions: (pay1 == pay2)
|
||||||
|
we get from the bank these items:
|
||||||
|
pay_date, client_name, bank, snif, account#, amount, comments """
|
||||||
|
|
||||||
return (self.type == other.type
|
return (self.type == other.type
|
||||||
and self.pay_date == other.pay_date
|
and self.pay_date == other.pay_date
|
||||||
and self.amount == other.amount
|
and self.amount == other.amount
|
||||||
|
@ -368,12 +367,15 @@ class Payment:
|
||||||
and self.comments == other.comments) # this is tricky, @todo
|
and self.comments == other.comments) # this is tricky, @todo
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
|
""" makes this class hashable; i'm using it to use within set() """
|
||||||
return hash(f'{self.pay_date}{self.amount}{self.client_name}')
|
return hash(f'{self.pay_date}{self.amount}{self.client_name}')
|
||||||
|
|
||||||
|
|
||||||
def iso_date():
|
def iso_date():
|
||||||
|
""" todays date as ISO """
|
||||||
return datetime.today().strftime('%Y-%m-%d')
|
return datetime.today().strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
|
||||||
def self_test():
|
def self_test():
|
||||||
conf = read_conf()
|
conf = read_conf()
|
||||||
conf.test = True
|
conf.test = True
|
||||||
|
@ -394,14 +396,14 @@ def self_test():
|
||||||
'''
|
'''
|
||||||
open(f,'w').write(test_data)
|
open(f,'w').write(test_data)
|
||||||
main(f, conf)
|
main(f, conf)
|
||||||
|
assert True # @todo; needs much better testing;
|
||||||
|
|
||||||
|
|
||||||
conf = read_conf()
|
conf = read_conf()
|
||||||
if f'{sys.version_info.major:02d}{sys.version_info.minor:02d}' < '0310':
|
if f'{sys.version_info.major:02d}{sys.version_info.minor:02d}' < '0308':
|
||||||
msg = f'WARNING !!! '*5 + f'\n "bank2invoice.py" was tested on python 3.10, not on {sys.version_info.major}.{sys.version_info.minor}.'
|
msg = f'WARNING !!! '*5 + f"\n 'bank2invoice.py' was tested on python 3.10, not on {sys.version_info.major}.{sys.version_info.minor}. OTOH yair wants to run it on py3.8, Let's see if it really does!"
|
||||||
print(msg)
|
print(msg)
|
||||||
err(msg)
|
err(msg) # just a non blocking, non-fatal warning
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -416,15 +418,15 @@ if __name__ == "__main__":
|
||||||
print('test mode')
|
print('test mode')
|
||||||
|
|
||||||
files = set()
|
files = set()
|
||||||
for arg in args:
|
for arg in args: # parsing command line without external tricks
|
||||||
if 'search' in args:
|
if 'search' in args:
|
||||||
pass
|
pass # @todo; make it print existing transactions; No use for that now.
|
||||||
|
|
||||||
elif 'test' in args:
|
elif 'test' in args:
|
||||||
self_test()
|
self_test()
|
||||||
exit()
|
exit()
|
||||||
|
|
||||||
elif arg[:6]=='--date':
|
elif arg[:6]=='--date': # in case we want to register recepits as a certain date; not implemented(?)
|
||||||
d = arg[7:]
|
d = arg[7:]
|
||||||
today = iso_date()
|
today = iso_date()
|
||||||
if not re.match(r'^20[23]\d-[01]\d-[0-3]\d$', d) or arg <= today:
|
if not re.match(r'^20[23]\d-[01]\d-[0-3]\d$', d) or arg <= today:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user