Hide keyboard shortcuts

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 

31 

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) 

52 

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

54 

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) 

73 

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) 

83 

84ASSIGNABLE_REFERENCE_PAYMENT_BATCH_HEADER_FIELDS = ( 

85 "record_date", 

86 "institution_identifier", 

87 "service_identifier", 

88 "currency_identifier", 

89) 

90 

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) 

106 

107 

108logger = logging.getLogger(__name__) 

109 

110 

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

125 

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) 

136 

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

148 

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 } 

161 

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

174 

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

184 

185 return stm 

186 

187 

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

200 

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

206 

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) 

217 

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) 

230 

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

238 

239 return batch 

240 

241 

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 

263 

264 

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 

273 

274 

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

280 

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 

318 

319 

320def make_msg_id() -> str: 

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

322 

323 

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) 

331 

332 

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 

349 

350 

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