def banktransfer(request): if any(k not in request.GET for k in ('invoice', 'key', 'prv')): return HttpResponse("Required parameter missing") invoice = get_object_or_404(Invoice, pk=get_int_or_error(request.GET, 'invoice'), recipient_secret=request.GET['key']) method = get_object_or_404(InvoicePaymentMethod, pk=get_int_or_error(request.GET, 'prv')) wrapper = PaymentMethodWrapper(method, invoice) return HttpResponse(wrapper.implementation.render_page(request, invoice))
def wikipage_history(request, confurl, wikiurl): conference = get_object_or_404(Conference, urlname=confurl) page = get_object_or_404(Wikipage, conference=conference, url=wikiurl) reg = _check_wiki_permissions(request, page) if not page.history: raise PermissionDenied() fromid = toid = None if request.method == 'POST': # View a diff if not ('from' in request.POST and 'to' in request.POST): messages.warning(request, "Must specify both source and target version") return HttpResponseRedirect('.') page_from = get_object_or_404(WikipageHistory, page=page, pk=get_int_or_error( request.POST, 'from')) fromid = page_from.id if request.POST['to'] != '-1': page_to = get_object_or_404(WikipageHistory, page=page, pk=get_int_or_error( request.POST, 'to')) toid = page_to.id else: page_to = page toid = None diff = "\n".join( difflib.unified_diff( page_from.contents.split('\r\n'), page_to.contents.split('\r\n'), fromfile='{0}'.format(page_from.publishedat), tofile='{0}'.format(page_to.publishedat), lineterm='', )) else: diff = '' return render_conference_response(request, conference, 'wiki', 'confwiki/wikipage_history.html', { 'page': page, 'diff': diff, 'fromid': fromid, 'toid': toid, })
def multiregs(request, urlname): conference = get_authenticated_conference(request, urlname) return render(request, 'confreg/admin_multireg_list.html', { 'conference': conference, 'bulkpays': BulkPayment.objects.select_related('user', 'invoice__paidusing').prefetch_related('conferenceregistration_set').filter(conference=conference).order_by('-paidat', '-createdat'), 'highlight': get_int_or_error(request.GET, 'b', -1), 'helplink': 'registrations', })
def get_query(self, request, term): q = super(RegistrationLookup, self).get_query(request, term) if 'conference' in request.GET: return q.filter(conference_id=get_int_or_error( request.GET, 'conference'), payconfirmedat__isnull=False) else: # Don't return anything if parameter not present return None
def api(request, urlname, regtoken, what): (conference, user, is_admin) = _get_checkin(request, urlname, regtoken) if what == 'status': return _json_response({ 'user': user.attendee.username, 'name': user.fullname, 'active': conference.checkinactive, 'confname': conference.conferencename, 'admin': is_admin, }) # Only the stats API call is allowed when check-in is not open if not conference.checkinactive and what != 'stats': return HttpResponse("Check-in not open", status=412) if what == 'lookup': token = request.GET.get('lookup') if not (token.startswith('ID$') and token.endswith('$ID')): raise Http404() token = token[3:-3] r = get_object_or_404(ConferenceRegistration, conference=conference, payconfirmedat__isnull=False, idtoken=token) return _json_response({'reg': _get_reg_json(r)}) elif what == 'search': s = request.GET.get('search').strip() return _json_response({ 'regs': [ _get_reg_json(r) for r in ConferenceRegistration.objects.filter( Q(firstname__icontains=s) | Q(lastname__icontains=s), conference=conference, payconfirmedat__isnull=False) ], }) elif is_admin and what == 'stats': return _json_response(_get_statistics(conference)) elif request.method == 'POST' and what == 'checkin': if not conference.checkinactive: return HttpResponse("Check-in not open", status=412) reg = get_object_or_404(ConferenceRegistration, conference=conference, payconfirmedat__isnull=False, pk=get_int_or_error(request.POST, 'reg')) if reg.checkedinat: return HttpResponse("Already checked in.", status=412) reg.checkedinat = datetime.datetime.now() reg.checkedinby = user reg.save() return _json_response({ 'reg': _get_reg_json(reg), }) else: raise Http404()
def bankfile_transactions(request, methodid): authenticate_backend_group(request, 'Invoice managers') method = get_object_or_404(InvoicePaymentMethod, pk=methodid) # Needed for backlinks methodcount = InvoicePaymentMethod.objects.filter( config__has_key='file_upload_interval').count() backbutton = "../" breadlabel = "Bank transactions" if methodcount == 1: # If there is only one method, we have to return all the way back to the index page, or we'll # just get redirected back to ourselves. backbutton = "/admin/" q = Q(method=method) if 'file' in request.GET: q = q & Q(fromfile=get_int_or_error(request.GET, 'file')) backbutton = "../../" breadlabel = "Bankfiles" allrows = BankStatementRow.objects.filter(q).order_by('-date', 'id') (rows, paginator, page_range) = simple_pagination(request, allrows, 50) extrakeys = set() hasvaluefor = { 'uniqueid': False, 'balance': False, } for r in rows: extrakeys.update(r.other.keys()) for k in hasvaluefor.keys(): if getattr(r, k, None): hasvaluefor[k] = True params = request.GET.copy() if 'page' in params: del params['page'] return render( request, 'invoices/bankfile_transactions.html', { 'rows': rows, 'extrakeys': extrakeys, 'hasvaluefor': hasvaluefor, 'page_range': page_range, 'topadmin': 'Invoices', 'helplink': 'payment', 'requestparams': params.urlencode(), 'breadcrumbs': [ (backbutton, breadlabel), ], 'backbutton': backbutton, })
def banktransactions_match_invoice(request, transid, invoiceid): authenticate_backend_group(request, 'Invoice managers') trans = get_object_or_404(PendingBankTransaction, pk=transid) invoice = get_object_or_404(Invoice, pk=invoiceid) pm = trans.method.get_implementation() if request.method == 'POST': if pm.config('feeaccount'): fee_account = Account.objects.get(num=pm.config('feeaccount')) else: fee_account = get_object_or_404(Account, num=get_int_or_error(request.POST, 'account')) r = _flag_invoices(request, trans, [invoice, ], pm, fee_account) if r: return HttpResponseRedirect("/admin/invoices/banktransactions/") else: return HttpResponseRedirect(".") # Generate the form if pm.config('feeaccount'): fee_account = Account.objects.get(num=pm.config('feeaccount')) accounts = [] else: fee_account = None accounts = get_account_choices() return render(request, 'invoices/banktransactions_match_invoice.html', { 'transaction': trans, 'invoices': [invoice, ], 'topadmin': 'Invoices', 'fee_account': fee_account, 'accounts': accounts, 'match': { 'amountdiff': invoice.total_amount - trans.amount, 'absdiff': abs(invoice.total_amount - trans.amount), 'percentdiff': (abs(invoice.total_amount - trans.amount) / invoice.total_amount) * 100, 'found_ref': invoice.payment_reference in trans.transtext, 'found_id': str(invoice.id) in trans.transtext, 'highlight_ref': re.sub('({0})'.format(invoice.payment_reference), r'<strong>\1</strong>', escape(trans.transtext)), 'highlight_id': re.sub('({0})'.format(invoice.id), r'<strong>\1</strong>', escape(trans.transtext)), }, 'breadcrumbs': [ ('/admin/invoices/banktransactions/', 'Pending bank transactions'), ('/admin/invoices/banktransactions/{0}/'.format(trans.id), 'Transaction'), ], 'helplink': 'payment', })
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')) 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'] impl = method.get_implementation() try: (contents, numrows, numtrans, numpending) = impl.parse_uploaded_file(f) BankFileUpload( method=method, uploadby=request.user.username, name=f.name, textcontents=contents, parsedrows=numrows, newtrans=numtrans, newpending=numpending, ).save() messages.info( request, "File uploaded. {} rows parsed, {} transactions stored, resulting in {} pending transactions." .format(numrows, numtrans, numpending)) return HttpResponseRedirect('.') 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', })
def feedback_sessions(request, confname): conference = get_authenticated_conference(request, confname) # Get all sessions that have actual comments on them cursor = connection.cursor() cursor.execute( "SELECT concat(s.title, ' (' || (SELECT string_agg(fullname, ', ') FROM confreg_speaker spk INNER JOIN confreg_conferencesession_speaker css ON css.speaker_id=spk.id WHERE css.conferencesession_id=s.id) || ')'), conference_feedback FROM confreg_conferencesessionfeedback fb INNER JOIN confreg_conferencesession s ON fb.session_id=s.id WHERE s.conference_id=%s AND NOT conference_feedback='' ORDER BY 1,2" % (conference.id, )) commented_sessions = cursor.fetchall() # Now for all of our fancy toplists # The django ORM just can't do this... minvotes = 10 if request.method == 'POST': minvotes = get_int_or_error(request.POST, 'minvotes') toplists = [] # Start with top sessions toplists.extend( build_toplists( 'Sessions', "SELECT s.title || ' (' || (SELECT string_agg(fullname, ', ') FROM confreg_speaker spk INNER JOIN confreg_conferencesession_speaker css ON css.speaker_id=spk.id WHERE css.conferencesession_id=s.id) || ')', avg(fb.{{key}}), count(*), stddev(fb.{{key}}) FROM confreg_conferencesessionfeedback fb INNER JOIN confreg_conferencesession s ON fb.session_id=s.id WHERE s.conference_id=%s AND fb.{{key}}>0 GROUP BY s.id HAVING count(*)>=%s ORDER BY 2 DESC" % (conference.id, minvotes))) # Now let's do the speakers toplists.extend( build_toplists( 'Speakers', "SELECT (SELECT string_agg(fullname, ', ') FROM confreg_speaker spk INNER JOIN confreg_conferencesession_speaker css ON css.speaker_id=spk.id WHERE css.conferencesession_id=s.id) AS speakername, avg(fb.{{key}}), count(*), stddev(fb.{{key}}) FROM confreg_conferencesessionfeedback fb INNER JOIN confreg_conferencesession s ON fb.session_id=s.id WHERE s.conference_id=%s AND fb.{{key}}>0 GROUP BY speakername HAVING count(*)>=%s ORDER BY 2 DESC" % (conference.id, minvotes))) return render( request, 'confreg/admin_session_feedback.html', { 'conference': conference, 'toplists': toplists, 'minvotes': minvotes, 'commented_sessions': commented_sessions, 'breadcrumbs': (('/events/admin/{0}/reports/feedback/'.format( conference.urlname), 'Feedback'), ), 'helplink': 'feedback', })
def year(request, year): authenticate_backend_group(request, 'Accounting managers') try: year = Year.objects.get(year=int(year)) except Year.DoesNotExist: # Year does not exist, but what do we do about it? if int(year) == date.today().year: # For current year, we automatically create the year and move on year = Year(year=int(year), isopen=True) year.save() messages.info( request, "Year {} did not exist in the system, but since it's the current year it has now been created." .format(year.year)) else: return HttpResponse( "Year {} does not exist in the system, and is not current year." .format(int(year))) if 'search' in request.GET: _setup_search(request, request.GET['search']) return HttpResponseRedirect('/accounting/%s/' % year.year) (searchterm, entries) = _perform_search(request, year) paginator = EntryPaginator(entries) currpage = get_int_or_error(request.GET, 'p', 1) return render( request, 'accounting/main.html', { 'entries': paginator.page(currpage), 'page': currpage, 'pages': paginator.get_pages(currpage), 'numpages': paginator.num_pages, 'hasopen': any([not e.closed for e in entries]), 'year': year, 'years': Year.objects.all(), 'reportable_objects': _get_reportable_objects(year), 'searchterm': searchterm, })
def index(request): if not request.user.is_superuser: raise PermissionDenied("Access denied") config = get_config() if request.method == 'POST': if request.POST.get('submit') == 'Hold all jobs': config.hold_all_jobs = True config.save() notify_job_change() return HttpResponseRedirect('.') elif request.POST.get('submit') == 'Re-enable job execution': what = get_int_or_error(request.POST, 'pending') if what == 0: messages.error(request, "Must decide what to do with pending jobs") return HttpResponseRedirect(".") elif what not in (1, 2): messages.error(request, "Invalid choice for pending jobs") return HttpResponseRedirect(".") if what == 2: # Re-schedule all pending jobs for job in ScheduledJob.objects.filter( enabled=True, nextrun__lte=timezone.now()): reschedule_job(job, save=True) config.hold_all_jobs = False config.save() notify_job_change() messages.info(request, "Job execution re-enabled") return HttpResponseRedirect(".") raise Http404("Unknown button") jobs = ScheduledJob.objects.all().order_by( F('nextrun').asc(nulls_last=True)) try: lastjob = ScheduledJob.objects.only('lastrun').filter( lastrun__isnull=False).order_by('-lastrun')[0] lastjobtime = lastjob.lastrun lastjob_recent = (timezone.now() - lastjobtime) < timedelta(hours=6) except IndexError: # No job has run yet, can happen on brand new installation lastjobtime = None lastjob_recent = False history = JobHistory.objects.only( 'time', 'job__description', 'success', 'runtime').select_related('job').order_by('-time')[:20] with connection.cursor() as curs: curs.execute( "SELECT count(1) FROM pg_stat_activity WHERE application_name='pgeu scheduled job runner' AND datname=current_database()" ) n, = curs.fetchone() runner_active = (n > 0) return render( request, 'scheduler/index.html', { 'jobs': jobs, 'lastjob': lastjobtime, 'lastjob_recent': lastjob_recent, 'runner_active': runner_active, 'holdall': config.hold_all_jobs, 'history': history, 'apps': {a.name: a.verbose_name for a in apps.get_app_configs()}, 'helplink': 'jobs', })
def volunteerschedule_api(request, urlname, adm=False): try: (conference, can_admin, reg) = _get_conference_and_reg(request, urlname) except ConferenceRegistration.DoesNotExist: raise PermissionDenied() is_admin = can_admin and adm if request.method == 'GET': # GET just always returns the complete volunteer schedule slots = VolunteerSlot.objects.prefetch_related( 'volunteerassignment_set', 'volunteerassignment_set__reg').filter(conference=conference) return HttpResponse(json.dumps({ 'slots': [_slot_return_data(slot) for slot in slots], 'volunteers': [{ 'id': vol.id, 'name': vol.fullname, } for vol in conference.volunteers.all().order_by( 'firstname', 'lastname')], 'meta': { 'isadmin': is_admin, 'regid': reg.id, }, 'stats': _get_volunteer_stats(conference), }), content_type='application/json') if request.method != 'POST': raise Http404() if 'op' not in request.POST: raise Http404() slotid = get_int_or_error(request.POST, 'slotid') volid = get_int_or_error(request.POST, 'volid') # We should always have a valid slot slot = get_object_or_404(VolunteerSlot, conference=conference, pk=slotid) err = None if request.POST['op'] == 'signup': if volid != 0: raise PermissionDenied("Invalid post data") err = _signup(request, conference, reg, is_admin, slot) elif request.POST['op'] == 'remove': err = _remove(request, conference, reg, is_admin, slot, volid) elif request.POST['op'] == 'confirm': err = _confirm(request, conference, reg, is_admin, slot, volid) elif request.POST['op'] == 'add': err = _add(request, conference, reg, is_admin, slot, volid) else: raise Http404() if err: return HttpResponse( json.dumps({'err': err}), content_type='application/json', status=500, ) # Req-query the database to pick up any changes, and return the complete object slot = VolunteerSlot.objects.prefetch_related( 'volunteerassignment_set', 'volunteerassignment_set__reg').filter(conference=conference, pk=slot.pk)[0] return HttpResponse(json.dumps({ 'err': None, 'slot': _slot_return_data(slot), 'stats': _get_volunteer_stats(conference), }), content_type='application/json')
def volunteer_twitter(request, urlname, token): try: conference = Conference.objects.select_related('series').get( urlname=urlname) except Conference.DoesNotExist: raise Http404() if not conference.has_social_broadcast: raise Http404() reg = get_object_or_404(ConferenceRegistration, conference=conference, regtoken=token) if conference.administrators.filter(pk=reg.attendee_id).exists( ) or conference.series.administrators.filter(pk=reg.attendee_id): is_admin = True canpost = conference.twitter_postpolicy != 0 canpostdirect = conference.twitter_postpolicy != 0 canmoderate = conference.twitter_postpolicy in (2, 3) elif not conference.volunteers.filter(pk=reg.pk).exists(): raise Http404() else: is_admin = False canpost = conference.twitter_postpolicy >= 2 canpostdirect = conference.twitter_postpolicy == 4 canmoderate = conference.twitter_postpolicy == 3 providers = ProviderCache() if request.method == 'POST': if request.POST.get('op', '') == 'post': approved = False approvedby = None if is_admin: if conference.twitter_postpolicy == 0: raise PermissionDenied() # Admins can use the bypass parameter to, well, bypass if request.POST.get('bypass', '0') == '1': approved = True approvedby = reg.attendee else: if conference.twitter_postpolicy in (0, 1): raise PermissionDenied() if conference.twitter_postpolicy == 4: # Post without approval for volunteers approved = True approvedby = reg.attendee # Check if we have *exactly the same tweet* in the queue already, in the past 5 minutes. # in which case it's most likely a clicked-too-many-times. if ConferenceTweetQueue.objects.filter( conference=conference, contents=request.POST['txt'], author=reg.attendee, datetime__gt=timezone.now() - datetime.timedelta(minutes=5)): return _json_response({'error': 'Duplicate post detected'}) # Now insert it in the queue, bypassing time validation since it's not an automatically # generated tweet. t = ConferenceTweetQueue( conference=conference, contents=request.POST['txt'][:280], approved=approved, approvedby=approvedby, author=reg.attendee, replytotweetid=request.POST.get('replyid', None), ) if 'image' in request.FILES: t.image = request.FILES['image'].read() # Actually validate that it loads as PNG or JPG try: p = ImageFile.Parser() p.feed(t.image) p.close() image = p.image if image.format not in ('PNG', 'JPEG'): return _json_response({ 'error': 'Image must be PNG or JPEG, not {}'.format( image.format) }) except Exception as e: return _json_response({'error': 'Failed to parse image'}) MAXIMAGESIZE = 1 * 1024 * 1024 if len(t.image) > MAXIMAGESIZE: # Image is bigger than 4Mb, but it is a valid image, so try to rescale it # We can't know exactly how to resize it to get it to the right size, but most # likely if we cut the resolution by n% the filesize goes down by > n% (usually # an order of magnitude), so we base it on that and just fail again if that didn't # work. rescalefactor = MAXIMAGESIZE / len(t.image) newimg = image.resize((int(image.size[0] * rescalefactor), int(image.size[1] * rescalefactor)), Image.ANTIALIAS) b = io.BytesIO() newimg.save(b, image.format) t.image = b.getvalue() if len(t.image) > MAXIMAGESIZE: return _json_response({ 'error': 'Image file too big and automatic resize failed' }) t.save() if request.POST.get('replyid', None): orig = ConferenceIncomingTweet.objects.select_related( 'provider').get(conference=conference, statusid=get_int_or_error( request.POST, 'replyid')) orig.processedat = timezone.now() orig.processedby = reg.attendee orig.save() # When when replying to a tweet, it goes to the original provider *only* t.remainingtosend.set([orig.provider]) return _json_response({}) elif request.POST.get('op', None) in ('approve', 'discard'): if not is_admin: # Admins can always approve, but volunteers only if policy allows if conference.twitter_postpolicy != 3: raise PermissionDenied() try: t = ConferenceTweetQueue.objects.get(conference=conference, approved=False, pk=get_int_or_error( request.POST, 'id')) except ConferenceTweetQueue.DoesNotExist: return _json_response({'error': 'Tweet already discarded'}) if t.approved: return _json_response( {'error': 'Tweet has already been approved'}) if request.POST.get('op') == 'approve': if t.author == reg.attendee: return _json_response( {'error': "Can't approve your own tweets"}) t.approved = True t.approvedby = reg.attendee t.save() trigger_immediate_job_run('twitter_post') else: t.delete() return _json_response({}) elif request.POST.get('op', None) in ('dismissincoming', 'retweet'): if not is_admin: # Admins can always approve, but volunteers only if policy allows if conference.twitter_postpolicy != 3: raise PermissionDenied() try: t = ConferenceIncomingTweet.objects.get( conference=conference, statusid=get_int_or_error(request.POST, 'id')) except ConferenceIncomingTweet.DoesNotExist: return _json_response({'error': 'Tweet does not exist'}) if request.POST.get('op', None) == 'dismissincoming': if t.processedat: return _json_response( {'error': 'Tweet is already dismissed or replied'}) t.processedby = reg.attendee t.processedat = timezone.now() t.save(update_fields=['processedby', 'processedat']) else: if t.retweetstate > 0: return _json_response({'error': 'Tweet '}) t.retweetstate = 1 t.save(update_fields=['retweetstate']) trigger_immediate_job_run('twitter_post') return _json_response({}) else: # Unknown op raise Http404() # GET request here if request.GET.get('op', None) == 'queue': # We show the queue to everybody, but non-moderators don't get to approve # Return the approval queue queue = ConferenceTweetQueue.objects.defer( 'image', 'imagethumb').filter(conference=conference, approved=False).extra( select={ 'hasimage': "image is not null and image != ''" }).order_by('datetime') # Return the latest ones approved latest = ConferenceTweetQueue.objects.defer( 'image', 'imagethumb').filter(conference=conference, approved=True).extra( select={ 'hasimage': "image is not null and image != ''" }).order_by('-datetime')[:5] def _postdata(objs): return [{ 'id': t.id, 'txt': t.contents, 'author': t.author and t.author.username or '', 'time': t.datetime, 'hasimage': t.hasimage, 'delivered': t.sent, } for t in objs] return _json_response({ 'queue': _postdata(queue), 'latest': _postdata(latest), }) elif request.GET.get('op', None) == 'incoming': incoming = ConferenceIncomingTweet.objects.select_related( 'provider').filter(conference=conference, processedat__isnull=True).order_by('created') latest = ConferenceIncomingTweet.objects.select_related( 'provider').filter( conference=conference, processedat__isnull=False).order_by('-processedat')[:5] def _postdata(objs): return [{ 'id': str(t.statusid), 'txt': t.text, 'author': t.author_screenname, 'authorfullname': t.author_name, 'time': t.created, 'rt': t.retweetstate, 'provider': t.provider.publicname, 'media': [m for m in t.media if m is not None], 'url': providers.get(t.provider).get_public_url(t), 'replymaxlength': providers.get(t.provider).max_post_length, } for t in objs.annotate( media=ArrayAgg('conferenceincomingtweetmedia__mediaurl'))] return _json_response({ 'incoming': _postdata(incoming), 'incominglatest': _postdata(latest), }) elif request.GET.get('op', None) == 'hasqueue': return _json_response({ 'hasqueue': ConferenceTweetQueue.objects.filter( conference=conference, approved=False).exclude(author=reg.attendee_id).exists(), 'hasincoming': ConferenceIncomingTweet.objects.filter( conference=conference, processedat__isnull=True).exists(), }) elif request.GET.get('op', None) == 'thumb': # Get a thumbnail -- or make one if it's not there t = get_object_or_404(ConferenceTweetQueue, conference=conference, pk=get_int_or_error(request.GET, 'id')) if not t.imagethumb: # Need to generate a thumbnail here. Thumbnails are always made in PNG! p = ImageFile.Parser() p.feed(bytes(t.image)) p.close() im = p.image im.thumbnail((256, 256)) b = io.BytesIO() im.save(b, "png") t.imagethumb = b.getvalue() t.save() resp = HttpResponse(content_type='image/png') resp.write(bytes(t.imagethumb)) return resp # Maximum length from any of the configured providers providermaxlength = { m.provider.publicname: providers.get(m.provider).max_post_length for m in ConferenceMessaging.objects.select_related('provider').filter( conference=conference, broadcast=True, provider__active=True) } return render( request, 'confreg/twitter.html', { 'conference': conference, 'reg': reg, 'poster': canpost and 1 or 0, 'directposter': canpostdirect and 1 or 0, 'moderator': canmoderate and 1 or 0, 'providerlengths': ", ".join( ["{}: {}".format(k, v) for k, v in providermaxlength.items()]), 'maxlength': max((v for k, v in providermaxlength.items())), })
def report(request, year, reporttype): authenticate_backend_group(request, 'Accounting managers') years = list(Year.objects.all()) year = get_object_or_404(Year, year=year) mindate = maxdate = None suppress_years = False if request.GET.get('obj', None): object = get_object_or_404(Object, pk=get_int_or_error(request.GET, 'obj')) objstr = "AND ji.object_id=%s" % object.id else: object = None objstr = '' hasopenentries = JournalEntry.objects.filter(year=year, closed=False).exists() if request.GET.get('acc', None): account = get_object_or_404(Account, num=get_int_or_error(request.GET, 'acc')) else: account = None if request.GET.get('ed', None) and request.GET['ed'] != 'undefined': enddate = datetime.strptime(request.GET['ed'], '%Y-%m-%d').date() if year and enddate.year != year.year: enddate = date(year.year, 12, 31) else: enddate = date(year.year, 12, 31) if request.GET.get('io', 0) == '1': includeopen = True else: includeopen = False filtered_objects = _get_reportable_objects(year) if reporttype == 'ledger': # This is a special report, so we don't use the collate functionality # XXX: consider perhaps including the in and out balance as well. # Yup, the django ORM fails again - no window aggregates sql = "SELECT a.num as accountnum, a.name as accountname, sum(i.amount) FILTER (WHERE i.amount > 0) over w1 as totaldebit, sum(-i.amount) FILTER (WHERE i.amount < 0) over w1 as totalcredit, e.seq as entryseq, e.date, i.description, case when i.amount > 0 then i.amount else 0 end as debit, case when i.amount < 0 then -i.amount else 0 end as credit, o.name as object, e.closed FROM accounting_journalitem i INNER JOIN accounting_account a ON i.account_id=a.num INNER JOIN accounting_journalentry e ON i.journal_id=e.id LEFT JOIN accounting_object o ON i.object_id=o.id WHERE e.year_id=%(year)s AND (e.closed OR %(includeopen)s) AND e.date<=%(enddate)s" params = { 'year': year.year, 'enddate': enddate, 'includeopen': includeopen, } if request.GET.get('obj', None): sql += " AND o.id=%(objectid)s" params['objectid'] = get_int_or_error(request.GET, 'obj') if request.GET.get('acc', None): sql += " AND a.num=%(account)s" params['account'] = get_int_or_error(request.GET, 'acc') sql += " WINDOW w1 AS (PARTITION BY a.num) ORDER BY a.num, e.date, e.seq" curs = connection.cursor() curs.execute(sql, params) # Django templates are also too stupid to be able to produce # a section-summary value, so we need to build them up as # a two stage array. items = [] lastaccount = 0 for row in curs.fetchall(): if row[0] != lastaccount: items.append({ 'accountnum': row[0], 'accountname': row[1], 'totaldebit': row[2], 'totalcredit': row[3], 'entries': [] }) lastaccount = row[0] items[-1]['entries'].append( dict( list(zip([col[0] for col in curs.description[4:]], row[4:])))) return render( request, 'accounting/ledgerreport.html', { 'year': year, 'years': years, 'reportable_objects': filtered_objects, 'currentobj': object, 'accounts': Account.objects.all(), 'currentaccount': account, 'reporttype': 'ledger', 'title': 'Ledger', 'items': items, 'enddate': enddate, 'hasopenentries': hasopenentries, 'includeopen': includeopen, 'yearsuffix': 'report/ledger/', 'isreport': True, }) elif reporttype == 'results': # The results report is the easiest one, since we can assume that # all accounts enter the year with a value 0. Therefor, we only # care about summing the data for this year. # We only show accounts that have had some transactions on them. # Note! Probably sync up much of this query with the object report! (results, totalresult) = _collate_results( """WITH t AS ( SELECT ac.name as acname, ag.name as agname, ag.foldable, a.num as anum, a.name, sum(-ji.amount) as amount FROM accounting_accountclass ac INNER JOIN accounting_accountgroup ag ON ac.id=ag.accountclass_id INNER JOIN accounting_account a ON ag.id=a.group_id INNER JOIN accounting_journalitem ji ON ji.account_id=a.num INNER JOIN accounting_journalentry je ON je.id=ji.journal_id WHERE je.year_id=%(year)s AND je.date <= %(enddate)s AND (je.closed or %(includeopen)s) AND NOT ac.inbalance {0} GROUP BY ac.name, ag.name, ag.foldable, a.id, a.name ) SELECT acname, agname, anum, name, count(*) OVER (PARTITION BY agname) = 1 AND foldable as agfold, sum(amount) OVER (PARTITION by acname) AS acamount, sum(amount) OVER (PARTITION by agname) AS agamount, amount, sum(amount) OVER () FROM t ORDER BY anum""".format(objstr), { 'year': year.year, 'enddate': enddate, 'includeopen': includeopen, }, 1) title = 'Results report' totalname = 'Final result' valheaders = ['Amount'] elif reporttype == 'balance': # Balance report. # We always assume we have an incoming balance and that the previous # year has been closed. If it's not closed, we just show a warning # about that. try: prevyear = Year.objects.get(year=year.year - 1) if prevyear and prevyear.isopen: messages.warning( request, 'Previous year (%s) is still open. Incoming balance will be incorrect!' % prevyear.year) except Year.DoesNotExist: pass if includeopen: valheaders = [ 'Incoming', 'Period', 'Open', 'Period+Open', 'Outgoing' ] else: valheaders = ['Incoming', 'Period', 'Outgoing'] (results, totalresult) = _collate_results( _get_balance_query(objstr, includeopen), { 'year': year.year, 'enddate': enddate, }, len(valheaders)) title = 'Balance report' totalname = 'Final balance' elif reporttype == 'object': if not object: raise Http404("Object report requires object") # Hasopenentries now need to reflect if this *object* has any open entries hasopenentries = JournalEntry.objects.filter( journalitem__object=object, closed=False).exists() # Note! Probably sync up much of this query with the results report! (results, totalresult) = _collate_results( """WITH t AS ( SELECT ac.name as acname, ag.name as agname, ag.foldable, a.num as anum, a.name, sum(-ji.amount) as amount FROM accounting_accountclass ac INNER JOIN accounting_accountgroup ag ON ac.id=ag.accountclass_id INNER JOIN accounting_account a ON ag.id=a.group_id INNER JOIN accounting_journalitem ji ON ji.account_id=a.num INNER JOIN accounting_journalentry je ON je.id=ji.journal_id WHERE (je.closed or %(includeopen)s) AND NOT ac.inbalance {0} GROUP BY ac.name, ag.name, ag.foldable, a.id, a.name ) SELECT acname, agname, anum, name, count(*) OVER (PARTITION BY agname) = 1 AND foldable as agfold, sum(amount) OVER (PARTITION by acname) AS acamount, sum(amount) OVER (PARTITION by agname) AS agamount, amount, sum(amount) OVER () FROM t ORDER BY anum""".format(objstr), { 'year': year.year, 'enddate': enddate, 'includeopen': includeopen, }, 1) title = '{0} result report'.format(object.name) totalname = 'Final result for {0}'.format(object.name) valheaders = ['Amount'] curs = connection.cursor() curs.execute( "SELECT min(date), max(date) FROM accounting_journalentry e WHERE EXISTS (SELECT 1 FROM accounting_journalitem i WHERE i.object_id=%(object)s AND i.journal_id=e.id)", { 'object': object.id, }) (mindate, maxdate) = curs.fetchone() suppress_years = True else: raise Http404("Unknown report") # XXX: PDF maybe? return render( request, 'accounting/yearreports.html', { 'reporttype': reporttype, 'title': title, 'year': year and year or -1, 'years': years, 'reportable_objects': filtered_objects, 'hasopenentries': hasopenentries, 'results': results, 'totalresult': totalresult, 'valheaders': valheaders, 'totalname': totalname, 'currentobj': object, 'enddate': enddate, 'mindate': mindate, 'maxdate': maxdate, 'includeopen': includeopen, 'yearsuffix': 'report/{}/'.format(reporttype), 'isreport': True, 'suppress_years': suppress_years, })
def entry(request, entryid): authenticate_backend_group(request, 'Accounting managers') entry = get_object_or_404(JournalEntry, pk=entryid) if 'search' in request.GET: _setup_search(request, request.GET['search']) return HttpResponseRedirect('/accounting/e/%s/' % entryid) (searchterm, entries) = _perform_search(request, entry.year) paginator = EntryPaginator(entries) currpage = get_int_or_error(request.GET, 'p', 1) extra = max(2, 6 - entry.journalitem_set.count()) inlineformset = inlineformset_factory(JournalEntry, JournalItem, JournalItemForm, JournalItemFormset, can_delete=True, extra=extra) inlineurlformset = inlineformset_factory(JournalEntry, JournalUrl, JournalUrlForm, can_delete=True, extra=2, exclude=[]) if request.method == 'POST': if request.POST['submit'] == 'Delete': year = entry.year entry.delete() return HttpResponseRedirect("/accounting/%s/" % year.year) form = JournalEntryForm(data=request.POST, instance=entry) formset = inlineformset(data=request.POST, instance=entry) urlformset = inlineurlformset(data=request.POST, instance=entry) if form.is_valid(): if formset.is_valid() and urlformset.is_valid(): instance = form.save() formset.save() urlformset.save() if request.POST['submit'] == 'Close': instance.closed = True instance.save() return HttpResponseRedirect(".") # Else fall through else: form = JournalEntryForm(instance=entry) formset = inlineformset(instance=entry) urlformset = inlineurlformset(instance=entry) items = list(entry.journalitem_set.all()) totals = (sum([i.amount for i in items if i.amount > 0]), -sum([i.amount for i in items if i.amount < 0])) urls = list(entry.journalurl_set.all()) return render( request, 'accounting/main.html', { 'entries': paginator.page(currpage), 'page': currpage, 'pages': paginator.get_pages(currpage), 'numpages': paginator.num_pages, 'hasopen': any([not e.closed for e in entries]), 'year': entry.year, 'entry': entry, 'has_pending_banktrans': entry.pendingbankmatcher_set.exists(), 'items': items, 'urls': urls, 'totals': totals, 'form': form, 'formset': formset, 'urlformset': urlformset, 'years': Year.objects.all(), 'reportable_objects': _get_reportable_objects(entry.year), 'searchterm': searchterm, })
def banktransactions(request): authenticate_backend_group(request, 'Invoice managers') if request.method == 'POST': if 'submit' not in request.POST: return HttpResponseRedirect(".") if 'transid' in request.POST: trans = get_object_or_404(PendingBankTransaction, id=get_int_or_error( request.POST, 'transid')) if request.POST['submit'] == 'Discard': InvoiceLog( message="Discarded bank transaction of {0}{1} with text {2}" .format(trans.amount, settings.CURRENCY_ABBREV, trans.transtext)).save() trans.delete() messages.info(request, "Transaction discarded") return HttpResponseRedirect(".") elif request.POST['submit'] == 'Create accounting record': pm = trans.method.get_implementation() accrows = [ (pm.config('bankaccount'), trans.transtext, trans.amount, None), ] entry = create_accounting_entry(date.today(), accrows, True) InvoiceLog( message= "Created manual accounting entry for transaction of {0}{1} with text {2}" .format(trans.amount, settings.CURRENCY_ABBREV, trans.transtext)).save() trans.delete() return HttpResponseRedirect("/accounting/e/{0}/".format( entry.id)) elif request.POST['submit'] == 'Return to sender': pm = trans.method.get_implementation() pm.return_payment(trans) InvoiceLog( message= "Scheduled transaction '{0}' ({1}{2}) for return to sender using {3}" .format(trans.transtext, trans.amount, settings.CURRENCY_ABBREV, trans.method.internaldescription)).save() trans.delete() return HttpResponseRedirect(".") else: raise Http404("Invalid request") elif 'matcherid' in request.POST: matcher = get_object_or_404(PendingBankMatcher, pk=get_int_or_error( request.POST, 'matcherid')) if request.POST['submit'] == 'Discard': InvoiceLog( message="Discarded pending bank matcher {0} for {1} {2}". format(matcher.pattern, matcher.amount, settings.CURRENCY_ABBREV)).save() matcher.delete() messages.info(request, "Matcher discarded") return HttpResponseRedirect(".") else: raise Http404("Invalid request") else: raise Http404("Invalid request") pendingtransactions = PendingBankTransaction.objects.order_by('created') pendingmatchers = PendingBankMatcher.objects.order_by('created') return render( request, 'invoices/banktransactions.html', { 'transactions': pendingtransactions, 'matchers': pendingmatchers, 'topadmin': 'Invoices', 'helplink': 'payment', })
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', })
def payment_post(request): nonce = request.POST['payment_method_nonce'] invoice = get_object_or_404(Invoice, pk=get_int_or_error(request.POST, 'invoice'), deleted=False, finalized=True) method = get_object_or_404(InvoicePaymentMethod, pk=get_int_or_error(request.POST, 'method'), active=True) pm = method.get_implementation() if invoice.processor: manager = InvoiceManager() processor = manager.get_invoice_processor(invoice) returnurl = processor.get_return_url(invoice) else: if invoice.recipient_user: returnurl = "%s/invoices/%s/" % (settings.SITEBASE, invoice.pk) else: returnurl = "%s/invoices/%s/%s/" % (settings.SITEBASE, invoice.pk, invoice.recipient_secret) # Generate the transaction result = pm.braintree_sale({ 'amount': '{0}'.format(invoice.total_amount), 'order_id': '#{0}'.format(invoice.pk), 'payment_method_nonce': nonce, 'merchant_account_id': pm.config('merchantacctid'), 'options': { 'submit_for_settlement': True, } }) trans = result.transaction if result.is_success: # Successful transaction. Store it for later processing. At authorization, we proceed to # flag the payment as done. BraintreeLog(transid=trans.id, message='Received successful result for {0}'.format(trans.id), paymentmethod=method).save() if trans.currency_iso_code != settings.CURRENCY_ISO: BraintreeLog(transid=trans.id, error=True, message='Invalid currency {0}, should be {1}'.format(trans.currency_iso_code, settings.CURRENCY_ISO), paymentmethod=method).save() send_simple_mail(settings.INVOICE_SENDER_EMAIL, pm.config('notification_receiver'), 'Invalid currency received in Braintree payment', 'Transaction {0} paid in {1}, should be {2}.'.format(trans.id, trans.currency_iso_code, settings.CURRENCY_ISO)) # We'll just throw the "processing error" page, and have # the operator deal with the complaints as this is a # should-never-happen scenario. return render(request, 'braintreepayment/processing_error.html') with transaction.atomic(): # Flag the invoice as paid manager = InvoiceManager() try: def invoice_logger(msg): raise BraintreeProcessingException('Invoice processing failed: %s'.format(msg)) manager.process_incoming_payment_for_invoice(invoice, trans.amount, 'Braintree id {0}'.format(trans.id), 0, pm.config('accounting_authorized'), 0, [], invoice_logger, method, ) except BraintreeProcessingException as ex: send_simple_mail(settings.INVOICE_SENDER_EMAIL, pm.config('notification_receiver'), 'Exception occurred processing Braintree result', "An exception occured processing the payment result for {0}:\n\n{1}\n".format(trans.id, ex)) return render(request, 'braintreepayment/processing_error.html') # Create a braintree transaction - so we can update it later when the transaction settles bt = BraintreeTransaction(transid=trans.id, authorizedat=timezone.now(), amount=trans.amount, method=trans.credit_card['card_type'], paymentmethod=method) if invoice.accounting_object: bt.accounting_object = invoice.accounting_object bt.save() send_simple_mail(settings.INVOICE_SENDER_EMAIL, pm.config('notification_receiver'), 'Braintree payment authorized', "A payment of %s%s with reference %s was authorized on the Braintree platform for %s.\nInvoice: %s\nRecipient name: %s\nRecipient user: %s\nBraintree reference: %s\n" % ( settings.CURRENCY_ABBREV, trans.amount, trans.id, method.internaldescription, invoice.title, invoice.recipient_name, invoice.recipient_email, trans.id)) return HttpResponseRedirect(returnurl) else: if not trans: reason = "Internal error" elif trans.status == 'processor_declined': reason = "Processor declined: {0}/{1}".format(trans.processor_response_code, trans.processor_response_text) elif trans.status == 'gateway_rejected': reason = "Gateway rejected: {0}".format(trans.gateway_rejection_reason) else: reason = "unknown" BraintreeLog(transid=trans and trans.id or "UNKNOWN", message='Received FAILED result for {0}'.format(trans and trans.id or "UNKNOWN"), error=True, paymentmethod=method).save() return render(request, 'braintreepayment/payment_failed.html', { 'invoice': invoice, 'reason': reason, 'url': returnurl, })