Coverage for jbank/helpers.py: 78%

1# pylint: disable=c-extension-no-member

2 import logging

3 import os

4 from datetime import date, timedelta

5 from typing import Any, Tuple, Optional, List

6 import pytz

7 from django.conf import settings

8 from django.core.files import File

9 from django.db import models

10 from django.utils.timezone import now

11 from django.utils.translation import gettext_lazy as _

12 from jacc.models import Account, AccountType, EntryType

13 import re

14 from lxml import etree, objectify # type: ignore # pytype: disable=import-error

15 from jutil.command import get_date_range_by_name

16 from jutil.parse import parse_datetime

17 from jutil.format import strip_media_root, is_media_full_path

18

19 MESSAGE_STATEMENT_RECORD_FIELDS = ("messages", "client_messages", "bank_messages")

20

21 logger = logging.getLogger(__name__)

22

23

24 def get_or_create_bank_account_entry_types() -> List[EntryType]:

25 e_type_codes = [

26 settings.E_BANK_DEPOSIT,

27 settings.E_BANK_WITHDRAW,

28 settings.E_BANK_REFERENCE_PAYMENT,

29 settings.E_BANK_REFUND,

30 settings.E_BANK_PAYOUT,

31 ]

32 e_types: List[EntryType] = []

33 for code in e_type_codes:

34 e_type = EntryType.objects.get_or_create(

35 code=code,

36 defaults={

37 "identifier": code,

38 "name": code,

39 "is_settlement": True,

40 "is_payment": code in [settings.E_BANK_DEPOSIT, settings.E_BANK_REFERENCE_PAYMENT],

41 },

42 )[0]

43 e_types.append(e_type)

44 return e_types

45

46

47 def get_or_create_bank_account(account_number: str, currency: str = "EUR") -> Account:

48 a_type = AccountType.objects.get_or_create(code=settings.ACCOUNT_BANK_ACCOUNT, is_asset=True, defaults={"name": _("bank account")})[0]

49 acc, created = Account.objects.get_or_create(name=account_number, type=a_type, currency=currency)

50 if created:

51 get_or_create_bank_account_entry_types()

52 return acc

53

54

55 def make_msg_id() -> str:

56 return re.sub(r"[^\d]", "", now().isoformat())[:-4]

57

58

59 def validate_xml(content: bytes, xsd_file_name: str):

60 """

61 Validates XML using XSD

62 """

63 schema = etree.XMLSchema(file=xsd_file_name)

64 parser = objectify.makeparser(schema=schema)

65 objectify.fromstring(content, parser)

66

67

68 def parse_date_or_relative_date(value: str, tz: Any = None) -> Optional[date]:

69 try:

70 return parse_datetime(value, tz=tz).date()

71 except Exception:

72 return get_date_range_by_name(value.replace("-", "_"), tz=tz)[0].date()

73

74

75 def parse_start_and_end_date(tz: Any, **options) -> Tuple[Optional[date], Optional[date]]:

76 start_date = None

77 end_date = None

78 time_now = now().astimezone(tz if tz else pytz.utc)

79 if options["start_date"]:

80 start_date = parse_date_or_relative_date(options["start_date"], tz=tz)

81 end_date = time_now.astimezone(tz).date() + timedelta(days=1)

82 if options["end_date"]:

83 end_date = parse_date_or_relative_date(options["end_date"], tz=tz)

84 return start_date, end_date

85

86

87 def save_or_store_media(file: models.FileField, filename: str):

88 """

89 Saves FileField filename as relative path if it's under MEDIA_ROOT.

90 Otherwise writes file under media root.

91 """

92 if is_media_full_path(filename):

93 file.name = strip_media_root(filename) # type: ignore

94 else:

95 with open(filename, "rb") as fp:

96 plain_filename = os.path.basename(filename)

97 file.save(plain_filename, File(fp)) # type: ignore # noqa