def handle_method(self, method): pm = method.get_implementation() api = pm.get_api() for t in api.get_transactions(): # We will re-fetch most transactions, so only create them if they are not # already there. trans, created = TransferwiseTransaction.objects.get_or_create( paymentmethod=method, twreference=t['referenceNumber'], defaults={ 'datetime': api.parse_datetime(t['date']), 'amount': api.get_structured_amount(t['amount']), 'feeamount': api.get_structured_amount(t['totalFees']), 'transtype': t['details']['type'], 'paymentref': t['details']['paymentReference'][:200], 'fulldescription': t['details']['description'], }) if created: # Set optional fields trans.counterpart_name = t['details'].get('senderName', '') trans.counterpart_account = t['details'].get( 'senderAccount', '').replace(' ', '') # Weird stuff that sometimes shows up if trans.counterpart_account == 'Unknownbankaccount': trans.counterpart_account = '' if trans.counterpart_account: # If account is IBAN, then try to validate it! trans.counterpart_valid_iban = api.validate_iban( trans.counterpart_account) trans.save() # If this is a refund transaction, process it as such if trans.transtype == 'TRANSFER' and trans.paymentref.startswith( '{0} refund'.format(settings.ORG_SHORTNAME)): # Yes, this is one of our refunds. Can we find the corresponding transaction? m = re.match(r'^TRANSFER-(\d+)$', t['referenceNumber']) if not m: raise Exception( "Could not find TRANSFER info in transfer reference {0}" .format(t['referenceNumber'])) transferid = m.groups(1)[0] try: twrefund = TransferwiseRefund.objects.get( transferid=transferid) except TransferwiseRefund.DoesNotExist: print( "Could not find transferwise refund for id {0}, registering as manual bank transaction" .format(transferid)) register_bank_transaction(method, trans.id, trans.amount, trans.paymentref, trans.fulldescription, False) continue if twrefund.refundtransaction or twrefund.completedat: raise Exception( "Transferwise refund for id {0} has already been processed!" .format(transferid)) # Flag this one as done! twrefund.refundtransaction = trans twrefund.completedat = datetime.now() twrefund.save() invoicemanager = InvoiceManager() invoicemanager.complete_refund( twrefund.refundid, trans.amount + trans.feeamount, trans.feeamount, pm.config('bankaccount'), pm.config('feeaccount'), [], # urls method, ) elif trans.transtype == 'TRANSFER' and trans.paymentref.startswith( '{0} returned payment'.format(settings.ORG_SHORTNAME)): # Returned payment. Nothing much to do, but we create an accounting record # for it just to make things nice and clear. But first make sure we can # actually find the original transaction. try: po = TransferwisePayout.objects.get( reference=trans.paymentref) except TransferwisePayout.DoesNotExist: raise Exception( "Could not find transferwise payout object for {0}" .format(trans.paymentref)) po.completedat = datetime.now() po.completedtrans = trans po.save() m = re.match( r'^{0} returned payment (\d+)$'.format( settings.ORG_SHORTNAME), trans.paymentref) if not m: raise Exception( "Could not find returned transaction id in reference '{0}'" .format(trans.paymentref)) twtrans = TransferwiseTransaction.objects.get( pk=m.groups(1)[0]) if twtrans.amount != -trans.amount - trans.feeamount: raise Exception( "Original amount {0} does not match returned amount {1}" .format(twtrans.amount, -trans.amount - trans.feeamount)) accountingtxt = "TransferWise returned payment {0}".format( trans.twreference) accrows = [ (pm.config('bankaccount'), accountingtxt, trans.amount, None), (pm.config('feeaccount'), accountingtxt, trans.feeamount, None), (pm.config('bankaccount'), accountingtxt, -(trans.amount + trans.feeamount), None), ] create_accounting_entry(date.today(), accrows) elif trans.transtype == 'TRANSFER' and trans.paymentref.startswith( 'TW payout'): # Payout. Create an appropriate accounting record and a pending matcher. try: po = TransferwisePayout.objects.get( reference=trans.paymentref) except TransferwisePayout.DoesNotExist: raise Exception( "Could not find transferwise payout object for {0}" .format(trans.paymentref)) refno = int(trans.paymentref[len("TW payout "):]) if po.amount != -(trans.amount + trans.feeamount): raise Exception( "Transferwise payout {0} returned transaction with amount {1} instead of {2}" .format(refno, -(trans.amount + trans.feeamount), po.amount)) po.completedat = datetime.now() po.completedtrans = trans po.save() # Payout exists at TW, so proceed to generate records. If the receiving account # is a managed one, create a bank matcher. Otherwise just close the record # immediately. accrows = [ (pm.config('bankaccount'), trans.paymentref, trans.amount, None), (pm.config('feeaccount'), trans.paymentref, trans.feeamount, None), (pm.config('accounting_payout'), trans.paymentref, -(trans.amount + trans.feeamount), None), ] if is_managed_bank_account(pm.config('accounting_payout')): entry = create_accounting_entry( date.today(), accrows, True) register_pending_bank_matcher( pm.config('accounting_payout'), '.*TW.*payout.*{0}.*'.format(refno), -(trans.amount + trans.feeamount), entry) else: create_accounting_entry(date.today(), accrows) else: # Else register a pending bank transaction. This may immediately match an invoice # if it was an invoice payment, in which case the entire process will copmlete. register_bank_transaction(method, trans.id, trans.amount, trans.paymentref, trans.fulldescription, trans.counterpart_valid_iban)
def bankfiles(request): authenticate_backend_group(request, 'Invoice managers') if request.method == 'POST': # Uploading a file! method = get_object_or_404(InvoicePaymentMethod, active=True, config__has_key='file_upload_interval', id=get_int_or_error(request.POST, 'id')) impl = method.get_implementation() # Stage 1 upload has the file in request.FILES. Stage 2 has it in a hidden field in the form instead, # because we can't make it upload th file twice. if 'fc' in request.POST: # In stage 2 the file is included ase base64 txt = impl.convert_uploaded_file_to_utf8( BytesIO(base64.b64decode(request.POST['fc']))) try: rows = impl.parse_uploaded_file_to_rows(txt) (anyerror, extrakeys, hasvaluefor) = impl.process_loaded_rows(rows) numrows = len(rows) numtrans = 0 numpending = 0 numerrors = 0 with transaction.atomic(): # Store thef file itself bankfile = BankFileUpload( method=method, parsedrows=numrows, newtrans=0, newpending=0, errors=0, uploadby=request.user.username, name=request.POST['name'], textcontents=txt, ) bankfile.save() # To get an id we can use for r in rows: if r['row_already_exists']: continue if r.get('row_errors', []): numerrors += 1 continue # Insert the row b = BankStatementRow( method=method, fromfile=bankfile, uniqueid=r.get('uniqueid', None), date=r['date'], amount=r['amount'], description=r['text'], balance=r.get('balance', None), other=r['other'], ) b.save() numtrans += 1 if not register_bank_transaction( b.method, b.id, b.amount, b.description, ''): # This means the transaction wasn't directly matched and has been # registered as a pending transaction. numpending += 1 bankfile.newtrans = numtrans bankfile.newpending = numpending bankfile.errors = numerrors bankfile.save() except Exception as e: messages.error(request, "Error uploading file: {}".format(e)) return HttpResponseRedirect(".") if 'f' not in request.FILES: messages.error(request, "No file included in upload") elif request.FILES['f'].size < 1: messages.error(request, "Uploaded file is empty") else: f = request.FILES['f'] try: # Stage 1, mean we parse it and render a second form to confirm rows = impl.parse_uploaded_file_to_rows( impl.convert_uploaded_file_to_utf8(f)) (anyerror, extrakeys, hasvaluefor) = impl.process_loaded_rows(rows) f.seek(0) return render( request, 'invoices/bankfile_uploaded.html', { 'method': method, 'rows': rows, 'extrakeys': sorted(extrakeys), 'hasvaluefor': hasvaluefor, 'anyerror': anyerror, 'filename': f.name, 'fc': base64.b64encode(f.read()).decode('ascii'), 'topadmin': 'Invoices', 'helplink': 'payment', 'breadcrumbs': [ ('../../', 'Bank files'), ], }) except Exception as e: messages.error(request, "Error uploading file: {}".format(e)) methods = InvoicePaymentMethod.objects.filter( active=True, config__has_key='file_upload_interval').annotate( latest_file=Max('bankfileupload__created')) file_objects = BankFileUpload.objects.select_related( 'method').all().order_by('-created')[:1000] (files, paginator, page_range) = simple_pagination(request, file_objects, 50) return render( request, 'invoices/bankfiles.html', { 'files': files, 'page_range': page_range, 'methods': methods, 'topadmin': 'Invoices', 'helplink': 'payment', })