Coverage for jbank/helpers.py : 70%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import logging
2from datetime import datetime, date
3from os.path import basename
4from typing import Any, Tuple, Optional, List
5import pytz
6from django.conf import settings
7from django.core.exceptions import ValidationError
8from django.db import transaction
9from django.utils.timezone import now
10from django.utils.translation import gettext_lazy as _
11from jacc.models import Account, AccountType, EntryType
12from jbank.models import Statement, StatementRecord, StatementRecordSepaInfo, ReferencePaymentBatch, \
13 ReferencePaymentRecord, StatementFile, ReferencePaymentBatchFile, Payout, PayoutStatus, PAYOUT_PAID
14from jbank.sepa import Pain002
15import re
16from lxml import etree, objectify # type: ignore # pytype: disable=import-error
17from jutil.parse import parse_datetime
18from jutil.format import strip_media_root
21ASSIGNABLE_STATEMENT_HEADER_FIELDS = (
22 'account_number',
23 'statement_number',
24 'begin_date',
25 'end_date',
26 'record_date',
27 'customer_identifier',
28 'begin_balance_date',
29 'begin_balance',
30 'record_count',
31 'currency_code',
32 'account_name',
33 'account_limit',
34 'owner_name',
35 'contact_info_1',
36 'contact_info_2',
37 'bank_specific_info_1',
38 'iban',
39 'bic',
40)
42ASSIGNABLE_STATEMENT_RECORD_FIELDS = (
43 'record_number',
44 'archive_identifier',
45 'record_date',
46 'value_date',
47 'paid_date',
48 'entry_type',
49 'record_code',
50 'record_description',
51 'amount',
52 'receipt_code',
53 'delivery_method',
54 'name',
55 'name_source',
56 'recipient_account_number',
57 'recipient_account_number_changed',
58 'remittance_info',
59)
61ASSIGNABLE_STATEMENT_RECORD_SEPA_INFO_FIELDS = (
62 'reference',
63 'iban_account_number',
64 'bic_code',
65 'recipient_name_detail',
66 'payer_name_detail',
67 'identifier',
68 'archive_identifier',
69)
71ASSIGNABLE_REFERENCE_PAYMENT_BATCH_HEADER_FIELDS = (
72 'record_date',
73 'institution_identifier',
74 'service_identifier',
75 'currency_identifier',
76)
78ASSIGNABLE_REFERENCE_PAYMENT_RECORD_FIELDS = (
79 'record_type',
80 'account_number',
81 'record_date',
82 'paid_date',
83 'archive_identifier',
84 'remittance_info',
85 'payer_name',
86 'currency_identifier',
87 'name_source',
88 'amount',
89 'correction_identifier',
90 'delivery_method',
91 'receipt_code',
92)
95logger = logging.getLogger(__name__)
98@transaction.atomic # noqa
99def create_statement(statement_data: dict, name: str, # noqa
100 file: StatementFile, **kw) -> Statement:
101 """
102 Creates Statement from statement data parsed by parse_tiliote_statements()
103 :param statement_data: See parse_tiliote_statements
104 :param name: File name of the account statement
105 :param file: Source statement file
106 :return: Statement
107 """
108 if 'header' not in statement_data or not statement_data['header']:
109 raise ValidationError('Invalid header field in statement data {}: {}'.format(name, statement_data.get('header')))
110 header = statement_data['header']
112 account_number = header['account_number']
113 if not account_number:
114 raise ValidationError('{name}: '.format(name=name) + _("account.not.found").format(account_number=''))
115 accounts = list(Account.objects.filter(name=account_number))
116 if len(accounts) != 1:
117 raise ValidationError('{name}: '.format(name=name) + _("account.not.found").format(account_number=account_number))
118 account = accounts[0]
119 assert isinstance(account, Account)
121 if Statement.objects.filter(name=name, account=account).first():
122 raise ValidationError('Bank account {} statement {} of processed already'.format(account_number, name))
123 stm = Statement(name=name, account=account, file=file)
124 for k in ASSIGNABLE_STATEMENT_HEADER_FIELDS:
125 if k in header:
126 setattr(stm, k, header[k])
127 # pprint(statement_data['header'])
128 for k, v in kw.items():
129 setattr(stm, k, v)
130 stm.full_clean()
131 stm.save()
133 if EntryType.objects.filter(code=settings.E_BANK_DEPOSIT).count() == 0:
134 raise ValidationError(_('entry.type.missing') + ' ({}): {}'.format('settings.E_BANK_DEPOSIT', settings.E_BANK_DEPOSIT))
135 if EntryType.objects.filter(code=settings.E_BANK_WITHDRAW).count() == 0:
136 raise ValidationError(_('entry.type.missing') + ' ({}): {}'.format('settings.E_BANK_WITHDRAW', settings.E_BANK_WITHDRAW))
137 entry_types = {
138 '1': EntryType.objects.get(code=settings.E_BANK_DEPOSIT),
139 '2': EntryType.objects.get(code=settings.E_BANK_WITHDRAW),
140 }
142 for rec_data in statement_data['records']:
143 line_number = rec_data['line_number']
144 e_type = entry_types.get(rec_data['entry_type'])
145 rec = StatementRecord(statement=stm, account=account, type=e_type, line_number=line_number)
146 for k in ASSIGNABLE_STATEMENT_RECORD_FIELDS:
147 if k in rec_data:
148 setattr(rec, k, rec_data[k])
149 for k in ('messages', 'client_messages', 'bank_messages'):
150 if k in rec_data:
151 setattr(rec, k, '\n'.join(rec_data[k]))
152 rec.full_clean()
153 rec.save()
155 if 'sepa' in rec_data:
156 sepa_info_data = rec_data['sepa']
157 sepa_info = StatementRecordSepaInfo(record=rec)
158 for k in ASSIGNABLE_STATEMENT_RECORD_SEPA_INFO_FIELDS:
159 if k in sepa_info_data:
160 setattr(sepa_info, k, sepa_info_data[k])
161 # pprint(rec_data['sepa'])
162 sepa_info.full_clean()
163 sepa_info.save()
165 return stm
168@transaction.atomic
169def create_reference_payment_batch(batch_data: dict, name: str, file: ReferencePaymentBatchFile, **kw) -> ReferencePaymentBatch:
170 """
171 Creates ReferencePaymentBatch from data parsed by parse_svm_batches()
172 :param batch_data: See parse_svm_batches
173 :param name: File name of the batch file
174 :return: ReferencePaymentBatch
175 """
176 if ReferencePaymentBatch.objects.exclude(file=file).filter(name=name).first():
177 raise ValidationError('Reference payment batch file {} already exists'.format(name))
179 if 'header' not in batch_data or not batch_data['header']:
180 raise ValidationError('Invalid header field in reference payment batch data {}: {}'.format(name, batch_data.get('header')))
181 header = batch_data['header']
183 batch = ReferencePaymentBatch(name=name, file=file)
184 for k in ASSIGNABLE_REFERENCE_PAYMENT_BATCH_HEADER_FIELDS:
185 if k in header:
186 setattr(batch, k, header[k])
187 # pprint(statement_data['header'])
188 for k, v in kw.items():
189 setattr(batch, k, v)
190 batch.full_clean()
191 batch.save()
192 e_type = EntryType.objects.get(code=settings.E_BANK_REFERENCE_PAYMENT)
194 for rec_data in batch_data['records']:
195 line_number = rec_data['line_number']
196 account_number = rec_data['account_number']
197 if not account_number:
198 raise ValidationError('{name}: '.format(name=name) + _("account.not.found").format(account_number=''))
199 accounts = list(Account.objects.filter(name=account_number))
200 if len(accounts) != 1:
201 raise ValidationError('{name}: '.format(name=name) + _("account.not.found").format(account_number=account_number))
202 account = accounts[0]
203 assert isinstance(account, Account)
205 rec = ReferencePaymentRecord(batch=batch, account=account, type=e_type, line_number=line_number)
206 for k in ASSIGNABLE_REFERENCE_PAYMENT_RECORD_FIELDS:
207 if k in rec_data:
208 setattr(rec, k, rec_data[k])
209 # pprint(rec_data)
210 rec.full_clean()
211 rec.save()
213 return batch
216def get_or_create_bank_account_entry_types() -> List[EntryType]:
217 e_type_codes = [
218 settings.E_BANK_DEPOSIT,
219 settings.E_BANK_WITHDRAW,
220 settings.E_BANK_REFERENCE_PAYMENT,
221 settings.E_BANK_REFUND,
222 settings.E_BANK_PAYOUT,
223 ]
224 e_types: List[EntryType] = []
225 for code in e_type_codes:
226 e_type = EntryType.objects.get_or_create(code=code, defaults={
227 'identifier': code,
228 'name': code,
229 'is_settlement': True,
230 'is_payment': code in [settings.E_BANK_DEPOSIT, settings.E_BANK_REFERENCE_PAYMENT],
231 })[0]
232 e_types.append(e_type)
233 return e_types
236def get_or_create_bank_account(account_number: str, currency: str = 'EUR') -> Account:
237 a_type = AccountType.objects.get_or_create(code=settings.ACCOUNT_BANK_ACCOUNT, is_asset=True, defaults={'name': _('bank account')})[0]
238 acc, created = Account.objects.get_or_create(name=account_number, type=a_type, currency=currency)
239 if created:
240 get_or_create_bank_account_entry_types()
241 return acc
244def process_pain002_file_content(bcontent: bytes, filename: str, created: Optional[datetime] = None):
245 if not created:
246 created = now()
247 s = Pain002(bcontent)
248 p = Payout.objects.filter(msg_id=s.original_msg_id).first()
250 ps = PayoutStatus(payout=p, file_name=basename(filename), file_path=strip_media_root(filename),
251 msg_id=s.msg_id, original_msg_id=s.original_msg_id,
252 group_status=s.group_status, status_reason=s.status_reason[:255], created=created)
253 ps.full_clean()
254 fields = (
255 'payout',
256 'file_name',
257 'response_code',
258 'response_text',
259 'msg_id',
260 'original_msg_id',
261 'group_status',
262 'status_reason',
263 )
264 params = {}
265 for k in fields:
266 params[k] = getattr(ps, k)
267 ps_old = PayoutStatus.objects.filter(**params).first()
268 if ps_old:
269 ps = ps_old
270 else:
271 ps.save()
272 logger.info('%s status updated %s', p, ps)
273 if p:
274 if ps.is_accepted:
275 p.state = PAYOUT_PAID
276 p.paid_date = s.credit_datetime
277 p.save(update_fields=['state', 'paid_date'])
278 logger.info('%s marked as paid %s', p, ps)
279 return ps
282def make_msg_id() -> str:
283 return re.sub(r'[^\d]', '', now().isoformat())[:-4]
286def validate_xml(content: bytes, xsd_file_name: str):
287 """
288 Validates XML using XSD
289 """
290 schema = etree.XMLSchema(file=xsd_file_name)
291 parser = objectify.makeparser(schema=schema)
292 objectify.fromstring(content, parser)
295def parse_start_and_end_date(tz: Any, **options) -> Tuple[Optional[date], Optional[date]]:
296 start_date = None
297 end_date = None
298 time_now = now().astimezone(tz if tz else pytz.utc)
299 if options['start_date']:
300 if options['start_date'] == 'today':
301 start_date = time_now.date()
302 else:
303 start_date = parse_datetime(options['start_date']).date() # type: ignore
304 end_date = start_date
305 if options['end_date']:
306 if options['end_date'] == 'today':
307 end_date = time_now.date()
308 else:
309 end_date = parse_datetime(options['end_date']).date() # type: ignore
310 return start_date, end_date