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
2import os
3from datetime import datetime, date
4from os.path import basename
5from typing import Any, Tuple, Optional, List
6import pytz
7from django.conf import settings
8from django.core.exceptions import ValidationError
9from django.core.files import File
10from django.db import transaction, models
11from django.utils.timezone import now
12from django.utils.translation import gettext_lazy as _
13from jacc.models import Account, AccountType, EntryType
14from jbank.models import (
15 Statement,
16 StatementRecord,
17 StatementRecordSepaInfo,
18 ReferencePaymentBatch,
19 ReferencePaymentRecord,
20 StatementFile,
21 ReferencePaymentBatchFile,
22 Payout,
23 PayoutStatus,
24 PAYOUT_PAID,
25)
26from jbank.sepa import Pain002
27import re
28from lxml import etree, objectify # type: ignore # pytype: disable=import-error
29from jutil.parse import parse_datetime
30from jutil.format import strip_media_root, is_media_full_path
32ASSIGNABLE_STATEMENT_HEADER_FIELDS = (
33 "account_number",
34 "statement_number",
35 "begin_date",
36 "end_date",
37 "record_date",
38 "customer_identifier",
39 "begin_balance_date",
40 "begin_balance",
41 "record_count",
42 "currency_code",
43 "account_name",
44 "account_limit",
45 "owner_name",
46 "contact_info_1",
47 "contact_info_2",
48 "bank_specific_info_1",
49 "iban",
50 "bic",
51)
53MESSAGE_STATEMENT_RECORD_FIELDS = ("messages", "client_messages", "bank_messages")
55ASSIGNABLE_STATEMENT_RECORD_FIELDS = (
56 "record_date",
57 "value_date",
58 "paid_date",
59 "record_number",
60 "archive_identifier",
61 "entry_type",
62 "record_code",
63 "record_description",
64 "amount",
65 "receipt_code",
66 "delivery_method",
67 "name",
68 "name_source",
69 "recipient_account_number",
70 "recipient_account_number_changed",
71 "remittance_info",
72)
74ASSIGNABLE_STATEMENT_RECORD_SEPA_INFO_FIELDS = (
75 "reference",
76 "iban_account_number",
77 "bic_code",
78 "recipient_name_detail",
79 "payer_name_detail",
80 "identifier",
81 "archive_identifier",
82)
84ASSIGNABLE_REFERENCE_PAYMENT_BATCH_HEADER_FIELDS = (
85 "record_date",
86 "institution_identifier",
87 "service_identifier",
88 "currency_identifier",
89)
91ASSIGNABLE_REFERENCE_PAYMENT_RECORD_FIELDS = (
92 "record_type",
93 "account_number",
94 "record_date",
95 "paid_date",
96 "archive_identifier",
97 "remittance_info",
98 "payer_name",
99 "currency_identifier",
100 "name_source",
101 "amount",
102 "correction_identifier",
103 "delivery_method",
104 "receipt_code",
105)
108logger = logging.getLogger(__name__)
111@transaction.atomic # noqa
112def create_statement(statement_data: dict, name: str, file: StatementFile, **kw) -> Statement: # noqa
113 """
114 Creates Statement from statement data parsed by parse_tiliote_statements()
115 :param statement_data: See parse_tiliote_statements
116 :param name: File name of the account statement
117 :param file: Source statement file
118 :return: Statement
119 """
120 if "header" not in statement_data or not statement_data["header"]:
121 raise ValidationError(
122 "Invalid header field in statement data {}: {}".format(name, statement_data.get("header"))
123 )
124 header = statement_data["header"]
126 account_number = header["account_number"]
127 if not account_number:
128 raise ValidationError("{name}: ".format(name=name) + _("account.not.found").format(account_number=""))
129 accounts = list(Account.objects.filter(name=account_number))
130 if len(accounts) != 1:
131 raise ValidationError(
132 "{name}: ".format(name=name) + _("account.not.found").format(account_number=account_number)
133 )
134 account = accounts[0]
135 assert isinstance(account, Account)
137 if Statement.objects.filter(name=name, account=account).first():
138 raise ValidationError("Bank account {} statement {} of processed already".format(account_number, name))
139 stm = Statement(name=name, account=account, file=file)
140 for k in ASSIGNABLE_STATEMENT_HEADER_FIELDS:
141 if k in header:
142 setattr(stm, k, header[k])
143 # pprint(statement_data['header'])
144 for k, v in kw.items():
145 setattr(stm, k, v)
146 stm.full_clean()
147 stm.save()
149 if EntryType.objects.filter(code=settings.E_BANK_DEPOSIT).count() == 0:
150 raise ValidationError(
151 _("entry.type.missing") + " ({}): {}".format("settings.E_BANK_DEPOSIT", settings.E_BANK_DEPOSIT)
152 )
153 if EntryType.objects.filter(code=settings.E_BANK_WITHDRAW).count() == 0:
154 raise ValidationError(
155 _("entry.type.missing") + " ({}): {}".format("settings.E_BANK_WITHDRAW", settings.E_BANK_WITHDRAW)
156 )
157 entry_types = {
158 "1": EntryType.objects.get(code=settings.E_BANK_DEPOSIT),
159 "2": EntryType.objects.get(code=settings.E_BANK_WITHDRAW),
160 }
162 for rec_data in statement_data["records"]:
163 line_number = rec_data["line_number"]
164 e_type = entry_types.get(rec_data["entry_type"])
165 rec = StatementRecord(statement=stm, account=account, type=e_type, line_number=line_number)
166 for k in ASSIGNABLE_STATEMENT_RECORD_FIELDS:
167 if k in rec_data:
168 setattr(rec, k, rec_data[k])
169 for k in MESSAGE_STATEMENT_RECORD_FIELDS:
170 if k in rec_data:
171 setattr(rec, k, "\n".join(rec_data[k]))
172 rec.full_clean()
173 rec.save()
175 if "sepa" in rec_data:
176 sepa_info_data = rec_data["sepa"]
177 sepa_info = StatementRecordSepaInfo(record=rec)
178 for k in ASSIGNABLE_STATEMENT_RECORD_SEPA_INFO_FIELDS:
179 if k in sepa_info_data:
180 setattr(sepa_info, k, sepa_info_data[k])
181 # pprint(rec_data['sepa'])
182 sepa_info.full_clean()
183 sepa_info.save()
185 return stm
188@transaction.atomic
189def create_reference_payment_batch(
190 batch_data: dict, name: str, file: ReferencePaymentBatchFile, **kw
191) -> ReferencePaymentBatch:
192 """
193 Creates ReferencePaymentBatch from data parsed by parse_svm_batches()
194 :param batch_data: See parse_svm_batches
195 :param name: File name of the batch file
196 :return: ReferencePaymentBatch
197 """
198 if ReferencePaymentBatch.objects.exclude(file=file).filter(name=name).first():
199 raise ValidationError("Reference payment batch file {} already exists".format(name))
201 if "header" not in batch_data or not batch_data["header"]:
202 raise ValidationError(
203 "Invalid header field in reference payment batch data {}: {}".format(name, batch_data.get("header"))
204 )
205 header = batch_data["header"]
207 batch = ReferencePaymentBatch(name=name, file=file)
208 for k in ASSIGNABLE_REFERENCE_PAYMENT_BATCH_HEADER_FIELDS:
209 if k in header:
210 setattr(batch, k, header[k])
211 # pprint(statement_data['header'])
212 for k, v in kw.items():
213 setattr(batch, k, v)
214 batch.full_clean()
215 batch.save()
216 e_type = EntryType.objects.get(code=settings.E_BANK_REFERENCE_PAYMENT)
218 for rec_data in batch_data["records"]:
219 line_number = rec_data["line_number"]
220 account_number = rec_data["account_number"]
221 if not account_number:
222 raise ValidationError("{name}: ".format(name=name) + _("account.not.found").format(account_number=""))
223 accounts = list(Account.objects.filter(name=account_number))
224 if len(accounts) != 1:
225 raise ValidationError(
226 "{name}: ".format(name=name) + _("account.not.found").format(account_number=account_number)
227 )
228 account = accounts[0]
229 assert isinstance(account, Account)
231 rec = ReferencePaymentRecord(batch=batch, account=account, type=e_type, line_number=line_number)
232 for k in ASSIGNABLE_REFERENCE_PAYMENT_RECORD_FIELDS:
233 if k in rec_data:
234 setattr(rec, k, rec_data[k])
235 # pprint(rec_data)
236 rec.full_clean()
237 rec.save()
239 return batch
242def get_or_create_bank_account_entry_types() -> List[EntryType]:
243 e_type_codes = [
244 settings.E_BANK_DEPOSIT,
245 settings.E_BANK_WITHDRAW,
246 settings.E_BANK_REFERENCE_PAYMENT,
247 settings.E_BANK_REFUND,
248 settings.E_BANK_PAYOUT,
249 ]
250 e_types: List[EntryType] = []
251 for code in e_type_codes:
252 e_type = EntryType.objects.get_or_create(
253 code=code,
254 defaults={
255 "identifier": code,
256 "name": code,
257 "is_settlement": True,
258 "is_payment": code in [settings.E_BANK_DEPOSIT, settings.E_BANK_REFERENCE_PAYMENT],
259 },
260 )[0]
261 e_types.append(e_type)
262 return e_types
265def get_or_create_bank_account(account_number: str, currency: str = "EUR") -> Account:
266 a_type = AccountType.objects.get_or_create(
267 code=settings.ACCOUNT_BANK_ACCOUNT, is_asset=True, defaults={"name": _("bank account")}
268 )[0]
269 acc, created = Account.objects.get_or_create(name=account_number, type=a_type, currency=currency)
270 if created:
271 get_or_create_bank_account_entry_types()
272 return acc
275def process_pain002_file_content(bcontent: bytes, filename: str, created: Optional[datetime] = None):
276 if not created:
277 created = now()
278 s = Pain002(bcontent)
279 p = Payout.objects.filter(msg_id=s.original_msg_id).first()
281 ps = PayoutStatus(
282 payout=p,
283 file_name=basename(filename),
284 file_path=strip_media_root(filename),
285 msg_id=s.msg_id,
286 original_msg_id=s.original_msg_id,
287 group_status=s.group_status,
288 status_reason=s.status_reason[:255],
289 created=created,
290 )
291 ps.full_clean()
292 fields = (
293 "payout",
294 "file_name",
295 "response_code",
296 "response_text",
297 "msg_id",
298 "original_msg_id",
299 "group_status",
300 "status_reason",
301 )
302 params = {}
303 for k in fields:
304 params[k] = getattr(ps, k)
305 ps_old = PayoutStatus.objects.filter(**params).first()
306 if ps_old:
307 ps = ps_old
308 else:
309 ps.save()
310 logger.info("%s status updated %s", p, ps)
311 if p:
312 if ps.is_accepted:
313 p.state = PAYOUT_PAID
314 p.paid_date = s.credit_datetime
315 p.save(update_fields=["state", "paid_date"])
316 logger.info("%s marked as paid %s", p, ps)
317 return ps
320def make_msg_id() -> str:
321 return re.sub(r"[^\d]", "", now().isoformat())[:-4]
324def validate_xml(content: bytes, xsd_file_name: str):
325 """
326 Validates XML using XSD
327 """
328 schema = etree.XMLSchema(file=xsd_file_name)
329 parser = objectify.makeparser(schema=schema)
330 objectify.fromstring(content, parser)
333def parse_start_and_end_date(tz: Any, **options) -> Tuple[Optional[date], Optional[date]]:
334 start_date = None
335 end_date = None
336 time_now = now().astimezone(tz if tz else pytz.utc)
337 if options["start_date"]:
338 if options["start_date"] == "today":
339 start_date = time_now.date()
340 else:
341 start_date = parse_datetime(options["start_date"]).date() # type: ignore
342 end_date = start_date
343 if options["end_date"]:
344 if options["end_date"] == "today":
345 end_date = time_now.date()
346 else:
347 end_date = parse_datetime(options["end_date"]).date() # type: ignore
348 return start_date, end_date
351def save_or_store_media(file: models.FileField, filename: str):
352 """
353 Saves FileField filename as relative path if it's under MEDIA_ROOT.
354 Otherwise writes file under media root.
355 """
356 if is_media_full_path(filename):
357 file.name = strip_media_root(filename) # type: ignore
358 else:
359 with open(filename, "rb") as fp:
360 plain_filename = os.path.basename(filename)
361 file.save(plain_filename, File(fp)) # type: ignore # noqa