Coverage for jbank/helpers.py: 70%

# Coverage for jbank/helpers.py : 70%

## 161 statements

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

1 import logging

2 from datetime import datetime, date

3 from os.path import basename

4 from typing import Any, Tuple, Optional, List

5 import pytz

6 from django.conf import settings

7 from django.core.exceptions import ValidationError

8 from django.db import transaction

9 from django.utils.timezone import now

10 from django.utils.translation import gettext_lazy as _

11 from jacc.models import Account, AccountType, EntryType

12 from jbank.models import Statement, StatementRecord, StatementRecordSepaInfo, ReferencePaymentBatch, \

13 ReferencePaymentRecord, StatementFile, ReferencePaymentBatchFile, Payout, PayoutStatus, PAYOUT_PAID

14 from jbank.sepa import Pain002

15 import re

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

17 from jutil.parse import parse_datetime

18 from jutil.format import strip_media_root

19

20

21 ASSIGNABLE_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)

41

42 ASSIGNABLE_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)

60

61 ASSIGNABLE_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)

70

71 ASSIGNABLE_REFERENCE_PAYMENT_BATCH_HEADER_FIELDS = (

72 'record_date',

73 'institution_identifier',

74 'service_identifier',

75 'currency_identifier',

76)

77

78 ASSIGNABLE_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)

93

94

95 logger = logging.getLogger(__name__)

96

97

98@transaction.atomic # noqa

99 def 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']

111

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)

120

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()

132

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 }

141

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()

154

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()

164

165 return stm

166

167

168@transaction.atomic

169 def 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))

178

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']

182

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)

193

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)

204

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()

212

213 return batch

214

215

216 def 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

234

235

236 def 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

242

243

244 def 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()

249

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

280

281

282 def make_msg_id() -> str:

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

284

285

286 def 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)

293

294

295 def 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