1+ from datadir import DataDir
2+ from typing import List
3+ import requests , re , datetime , dateutil
4+ from itertools import chain
5+ from functools import reduce
6+ from operator import add
7+
8+ from dateutil .relativedelta import relativedelta
9+ from dateutil .parser import parse
10+ from decimal import Decimal
11+
12+ from openpyxl import Workbook
13+
14+ class ApiBase (object ):
15+ QUARTER_FIRST_DATE = ['01-01' , '04-01' , '07-01' , '10-01' ] # MM.DD
16+
17+ def __init__ (self , d :DataDir ):
18+ self .quarter_first_date = []
19+ self ._period_str = None
20+ self .period = self ._period (d .get_config_v ('statements.period.value' ), d .get_config_v ('statements.period.start' ), d .get_config_v ('statements.period.end' ))
21+ self .ignore_accounts = d .get_config_v ('statements.ignore_accounts' )
22+ self .accounts :List [str ] = []
23+ self ._statements = {}
24+
25+ def _period (self , value , start , end ):
26+ first_date = None
27+ last_date = None
28+
29+ if start and end :
30+ (st_year , st_month , st_day ) = self ._parse_date (start )
31+ (end_year , end_month , end_day ) = self ._parse_date (end )
32+ if st_year :
33+ first_date = datetime .date (st_year , st_month , st_day )
34+ if end_year :
35+ last_date = datetime .date (end_year , end_month , end_day )
36+ if not first_date :
37+ first_date = dateutil .parser .parse (start , fuzzy = True )
38+ if not last_date :
39+ last_date = dateutil .parser .parse (end , fuzzy = True )
40+ self ._period_str = f'{ first_date } -- { last_date } '
41+ elif value :
42+ if value .lower () == 'prev' :
43+ first_date , last_date = self .prev_quarter (datetime .date .today ())
44+ self ._period_str = f'{ first_date } -- { last_date } '
45+ else :
46+ (year , quarter , _ ) = self ._parse_date (value )
47+ self ._period_str = f'{ year } -{ quarter .lower ()} '
48+ if not quarter .isdigit ():
49+ quarter = quarter .lower ()
50+ if quarter == 'i' : quarter = 1
51+ if quarter == 'ii' : quarter = 2
52+ if quarter == 'iii' : quarter = 3
53+ if quarter == 'iv' : quarter = 4
54+ else : quarter = int (quarter )
55+ quarter_start = f'{ year } -{ ApiBase .QUARTER_FIRST_DATE [quarter - 1 ]} '
56+ first_date = dateutil .parser .parse (quarter_start )
57+ last_date = first_date + relativedelta (months = 3 , days = - 1 )
58+ return first_date , last_date
59+
60+ @property
61+ def first_date (self ): return self .period [0 ]
62+
63+ @property
64+ def last_date (self ): return self .period [1 ]
65+
66+ @property
67+ def period_str (self ): return self ._period_str
68+
69+ def prev_quarter (self , ref ):
70+ first_month_of_quarter = ((ref .month - 1 ) // 3 ) * 3 + 1
71+ last_date = ref .replace (month = first_month_of_quarter , day = 1 ) - relativedelta (days = 1 )
72+ first_date = last_date - relativedelta (months = 3 , days = - 1 )
73+ return first_date , last_date
74+
75+ def _parse_date (self , q ):
76+ if not q : return q
77+ exps = [r'(?P<day>\d{1,2}).(?P<month>\d{1,2}).(?P<year>\d{4})' , #20.1.2020, 20-1-2020
78+ r'(?P<year>\d{4}).(?P<month>\d{1,2}).(?P<day>\d{1,2})' , # 2020.1.20, 2020-1-20
79+ r'(?P<year>\d{4}).(?P<quarter>\d)' , # 2020.4, 2020-4
80+ r'(?P<year>\d{4}).(?P<quarter>[iIvV]{1,2})' , # 2020.iv, 2020-iv
81+ r'(?P<quarter>\d).(?P<year>\d{4})' , # 4.2020, 4-2020
82+ r'(?P<quarter>[iIvV]{1,2}).(?P<year>\d{4})' , # iv.2020, iv-2020
83+ ]
84+ for r in exps :
85+ m = re .match (r , q )
86+ if m :
87+ values = m .groupdict ()
88+ return (int (values ['year' ]), values ['quarter' ], None ) if 'quarter' in values else (int (values ['year' ]), int (values ['month' ]), int (values ['day' ]) if "day" in values else None )
89+ return (None , None , None )
90+
91+ def ignored (self , account ):
92+ for ap in self .ignore_accounts :
93+ if re .match (ap , account ) is not None :
94+ return True
95+ return False
96+
97+ class PbApi (ApiBase ):
98+ def __init__ (self , d :DataDir ):
99+ ApiBase .__init__ (self , d )
100+
101+ self .pb_id :str = d .get_config_str ('statements.pb_id' )
102+ self .pb_token :str = d .get_config_str ('statements.pb_token' )
103+
104+ def fetch_statements (self ):
105+ self .accounts :List [str ] = []
106+
107+ params = {'startDate' : self .first_date .strftime ('%d-%m-%Y' ), 'endDate' : self .last_date .strftime ('%d-%m-%Y' )}
108+ headers = {'id' : self .pb_id
109+ , 'token' : self .pb_token
110+ , 'Content-type' : 'application/json;charset=utf8' }
111+ r = requests .get ("https://acp.privatbank.ua/api/proxy/transactions" , params = params , headers = headers )
112+ r .raise_for_status ()
113+ stmts = r .json ()['StatementsResponse' ]['statements' ]
114+ self .accounts = list (chain .from_iterable (stmts ))
115+ for account_item in stmts :
116+ self ._statements .update (account_item .items ())
117+
118+ def balance (self ):
119+
120+ params = {'startDate' : self .first_date .strftime ('%d-%m-%Y' ), 'endDate' : self .last_date .strftime ('%d-%m-%Y' )}
121+ headers = {'id' : self .pb_id
122+ , 'token' : self .pb_token
123+ , 'Content-type' : 'application/json;charset=utf8' }
124+ r = requests .get ("https://acp.privatbank.ua/api/statements/balance/final" , headers = headers )
125+ r .raise_for_status ()
126+ return r .json ()['balances' ]
127+
128+
129+ def statements (self , accountNo :str ): return list (chain .from_iterable ([x .values () for x in self ._statements [accountNo ]]))
130+
131+ def datetime_pb (self , s ) -> datetime : return datetime .datetime .strptime (s , '%d.%m.%Y %H:%M:%S' )
132+
133+ def store (self , wb_filename , start_row :int ):
134+ all_statements = []
135+ for accountNo in self .accounts :
136+ if self .ignored (accountNo ): continue
137+ all_statements .extend (self .statements (accountNo ))
138+ all_statements .sort (key = lambda x : self .datetime_pb (x ['DATE_TIME_DAT_OD_TIM_P' ]))
139+ wb = Workbook ()
140+ ws = wb .active
141+
142+ ws .cell (row = 2 , column = 1 , value = f'Виписка по декількох рахунках з { self .first_date } по { self .last_date } ' )
143+
144+ #ws.title = f'{self.first_date}-{self.last_date}'
145+ ws .cell (row = 4 , column = 1 , value = '№' )
146+ ws .cell (row = 4 , column = 2 , value = 'Дата проводки' )
147+ ws .cell (row = 4 , column = 3 , value = 'Час проводки' )
148+ ws .cell (row = 4 , column = 4 , value = 'Сума' )
149+ ws .cell (row = 4 , column = 5 , value = 'Валюта' )
150+ ws .cell (row = 4 , column = 6 , value = 'Призначення платежу' )
151+ ws .cell (row = 4 , column = 7 , value = 'ЄДРПОУ' )
152+ ws .cell (row = 4 , column = 8 , value = 'Назва контрагента' )
153+ ws .cell (row = 4 , column = 9 , value = 'Рахунок контрагента' )
154+ ws .cell (row = 4 , column = 10 , value = 'МФО контрагента' )
155+ ws .cell (row = 4 , column = 11 , value = 'Ваш МФО' )
156+ ws .cell (row = 4 , column = 12 , value = 'Ваш ЄДРПОУ' )
157+ ws .cell (row = 4 , column = 13 , value = 'Ваш рахунок' )
158+ ws .cell (row = 4 , column = 14 , value = 'Назва вашого рахунку' )
159+ ws .cell (row = 4 , column = 15 , value = 'Референс' )
160+
161+
162+ row = start_row
163+ for s in all_statements :
164+ ws .cell (row = row , column = 1 , value = s ['BPL_NUM_DOC' ])
165+ date = self .datetime_pb (s ['DATE_TIME_DAT_OD_TIM_P' ])
166+ amount = Decimal (s ['BPL_SUM' ])
167+ ws .cell (row = row , column = 2 , value = date .strftime ('%d.%m.%Y' ))
168+ ws .cell (row = row , column = 3 , value = date .strftime ('%H:%M:%S' ))
169+ ws .cell (row = row , column = 4 , value = - amount if s ['TRANTYPE' ] == 'D' else amount )
170+ ws .cell (row = row , column = 5 , value = s ['BPL_CCY' ])
171+ ws .cell (row = row , column = 6 , value = s ['BPL_OSND' ])
172+ ws .cell (row = row , column = 7 , value = s ['AUT_CNTR_CRF' ])
173+ ws .cell (row = row , column = 8 , value = s ['AUT_CNTR_NAM' ])
174+ ws .cell (row = row , column = 9 , value = s ['AUT_CNTR_ACC' ])
175+ ws .cell (row = row , column = 10 , value = s ['AUT_CNTR_MFO' ])
176+ ws .cell (row = row , column = 11 , value = s ['AUT_MY_MFO' ])
177+ ws .cell (row = row , column = 12 , value = s ['AUT_MY_CRF' ])
178+ ws .cell (row = row , column = 13 , value = s ['AUT_MY_ACC' ])
179+ ws .cell (row = row , column = 14 , value = s ['AUT_MY_NAM' ])
180+ ws .cell (row = row , column = 15 , value = s ['BPL_REF' ])
181+ row = row + 1
182+ wb .save (wb_filename )
183+ return len (all_statements )
0 commit comments