diff --git a/index.py b/index.py index 00dc317..05405cb 100755 --- a/index.py +++ b/index.py @@ -2,9 +2,10 @@ # coding=utf-8 from bottle import Bottle, static_file, request, template, run -import time +import datetime import json import sys +import re import kontomodel as konto @@ -14,12 +15,63 @@ @app.route('/byCategory/') def indexFile(byCategory='month'): lists = konto.getLists() - return template('index.tpl', title='Ausgaben', site=byCategory, options=lists, byCategory=byCategory, tracesJSON=json.dumps(['traces', 'profit'])) + accounts = konto.getAccounts() + return template('index.tpl', title='Ausgaben', site=byCategory, options=lists, byCategory=byCategory, tracesJSON=json.dumps(['traces', 'profit']), accounts=accounts) + +@app.route('/addNewItem', method='POST') +def addNewItem(): + thedate = request.json.get('newItemDate') + theamount = request.json.get('newItemAmount') + thename = request.json.get('newItemName') + thetitle = request.json.get('newItemTitle') + thedescription = request.json.get('newItemDescription') + thecategory = request.json.get('newItemCategory') + + if thedate is None or thedate == '': + raise Exception("invalid date") + if theamount is None or theamount == '': + raise Exception("invalid amount") + if thename is None or thename == '': + raise Exception("invalid name") + if thetitle is None or thetitle == '': + raise Exception("invalid title") + thedateobj = datetime.datetime.strptime(thedate, '%Y-%m-%d') + + theamount = float(theamount) + thefilename = 'Bargeld-' + datetime.datetime.now().strftime('%Y-%m') + theaccount = 'Bargeld' + + newItem = konto.appendItem(thefilename=thefilename, + thedate=thedate, + theaccount=theaccount, + theamount=theamount, + thecurrency='€', + thename=thename, + thetitle=thetitle, + thedescription=thedescription, + thecategory=thecategory) + + amountcurrency = str(newItem['amount']) + newItem['currency'] + thecategory = '' if newItem['category'] == 'nicht kategorisiert' else newItem['category'] + htmlentry = template('categoryItem.tpl', + date=newItem['date'], + name=newItem['name'], + shortname=newItem['name'][0:20], + title=newItem['title'], + account=newItem['account'], + theid=newItem['id'], + shorttitle=newItem['title'][0:40], + amountcurrency=amountcurrency, + thecategory=thecategory, + description=newItem['description']) + + return json.dumps({'htmlentry': htmlentry, 'errormsg': ''}) @app.route('/profit') def profit(): lists = konto.getLists() - return template('index.tpl', title='Gewinn', site='profit', options=lists, byCategory='month', tracesJSON=json.dumps(['profit', 'totalprofit'])) + accounts = konto.getAccounts() + return template('index.tpl', title='Gewinn', site='profit', options=lists, byCategory='month', tracesJSON=json.dumps(['profit', 'totalprofit']), accounts=accounts) @app.route('/static/') def server_static(thefilename): @@ -33,7 +85,11 @@ def editCategories(thefilename='categories'): @app.route('/editCategories', method='POST') def submitCategories(): - konto.writeCategories(categoriesString=request.forms.getunicode('categories')) + try: + konto.writeCategories(categoriesString=request.forms.getunicode('categories')) + except re.error as e: + return 'invalid regex: ' + str(e) + return editCategories() @app.route('/getConsolidated', method="POST") @@ -45,23 +101,31 @@ def getConsolidated(): fromDateJSON = request.json.get('fromDate', None) fromDate = None if fromDateJSON is not None: - fromDate = time.strptime(fromDateJSON, '%Y-%m-%d') + fromDate = datetime.datetime.strptime(fromDateJSON, '%Y-%m-%d') toDateJSON = request.json.get('toDate', None) toDate = None if toDateJSON is not None: - toDate = time.strptime(toDateJSON, '%Y-%m-%d') + toDate = datetime.datetime.strptime(toDateJSON, '%Y-%m-%d') + + accounts = request.json.get('accounts') + patternInput = request.json.get('patternInput') + thepattern = None if len(patternInput) == 0 else patternInput - consolidated = konto.getConsolidated(byCategory=byCategory, traceNames=traces, fromDate=fromDate, categories=categories, toDate=toDate) + consolidated = konto.getConsolidated(byCategory=byCategory, traceNames=traces, fromDate=fromDate, categories=categories, toDate=toDate, accounts=accounts, thepattern=thepattern) # return json.dumps(request.json.get('items')) return json.dumps({'traces': consolidated['traces'], 'foundDuplicates': consolidated['foundDuplicates']}) -@app.route('/categorize', method="POST") -def categorize(): +@app.route('/updateItem', method="POST") +def updateItem(): itemId = request.json.get('itemId') + thename = request.json.get('thename') + thetitle = request.json.get('thetitle') + theamount = request.json.get('theamount') thecategory = request.json.get('thecategory') - konto.updateCategory(itemId=itemId, thecategory=thecategory) + thedescription = request.json.get('thedescription') + konto.updateItem(itemId=itemId, thename=thename, thetitle=thetitle, theamount=theamount, thecategory=thecategory, thedescription=thedescription) @app.route('/getDetails', method="POST") def getDetails(): @@ -72,15 +136,39 @@ def getDetails(): fromDateJSON = request.json.get('fromDate', None) fromDate = None if fromDateJSON is not None: - fromDate = time.strptime(fromDateJSON, '%Y-%m-%d') + fromDate = datetime.datetime.strptime(fromDateJSON, '%Y-%m-%d') toDateJSON = request.json.get('toDate', None) toDate = None if toDateJSON is not None: - toDate = time.strptime(toDateJSON, '%Y-%m-%d') + toDate = datetime.datetime.strptime(toDateJSON, '%Y-%m-%d') + + accounts = request.json.get('accounts') + patternInput = request.json.get('patternInput') + thepattern = None if patternInput is None or len(patternInput) == 0 else patternInput + categorySelection = request.json.get('categorySelection') + + sortScatterBy = request.json.get('sortScatterBy') + sortScatterByReverse = request.json.get('sortScatterByReverse') + + theaction = request.json.get('action') + theactionParam = request.json.get('actionParam') + + if theaction == 'deleteItem': + konto.deleteEntry(theactionParam) + + title = 'Umsätze' + if categorySelection is not None: + if len(categorySelection) == 1: + title = title + template(' der Kategorie "{{x}}"', x=categorySelection[0]) + elif len(categorySelection) > 1: + title = title + template(' der Kategorien {{x}}', x=(", ".join(categorySelection))) + + if theX is not None: + title = title + template(' für {{theX}}', theX=theX) - consolidated = konto.getConsolidated(byCategory=byCategory, traceNames=['scatter'], fromDate=fromDate, categories=categories, toDate=toDate) - return template('categorize.tpl', title=template("Umsätze für {{theX}}", theX=theX), theX=theX, allcategoriesNames=consolidated['allcategoriesNames'], scatter=consolidated['scatter']) + consolidated = konto.getConsolidated(byCategory=byCategory, traceNames=['scatter'], fromDate=fromDate, categories=categories, toDate=toDate, accounts=accounts, thepattern=thepattern, categorySelection=categorySelection, sortScatterBy=sortScatterBy) + return template('categorize.tpl', title=title, theX=theX, allcategoriesNames=consolidated['allcategoriesNames'], scatter=consolidated['scatter'], reverseSort=sortScatterByReverse) if __name__ == "__main__": if len(sys.argv) > 1 and sys.argv[1] == 'dev': diff --git a/kontomodel.py b/kontomodel.py index d5c8eb0..3ffed6d 100644 --- a/kontomodel.py +++ b/kontomodel.py @@ -3,6 +3,7 @@ import os import time +import datetime import re import html @@ -18,28 +19,36 @@ def doUnlock(thefilename, thepath='lists/'): def findCategory(theitem, categories, unknownCategory='nicht kategorisiert'): for category in categories: - if re.search(category['pattern'], theitem[category['field']]): + if category['compiledPattern'].search(theitem[category['field']]): return category['category'] return unknownCategory def writeCategories(categoriesString, thepath='lists/', thefilename='categories'): + _parseCategories(categoriesString.split('\n')) + doLock(thefilename=thefilename, thepath=thepath) with open(thepath + thefilename + '.txt', 'w') as f: f.write(categoriesString) doUnlock(thefilename=thefilename, thepath=thepath) def parseCategories(thefilename='categories', thepath='lists/'): - result = [] with open(thepath + thefilename + '.txt', 'r') as f: - for line in f: - line = line.strip() - if len(line) == 0 or line[0] == '#': - continue - categoryEnd = line.find(';') - fieldEnd = line.find(';', categoryEnd+1) - result.append({'pattern': line[fieldEnd+1:], - 'field': line[categoryEnd+1:fieldEnd], - 'category': line[0:categoryEnd]}) + result = _parseCategories(f) + return result + +def _parseCategories(f): + result = [] + for line in f: + line = line.strip() + if len(line) == 0 or line[0] == '#': + continue + categoryEnd = line.find(';') + fieldEnd = line.find(';', categoryEnd+1) + thepattern = line[fieldEnd+1:] + result.append({'pattern': thepattern, + 'compiledPattern': re.compile(thepattern), + 'field': line[categoryEnd+1:fieldEnd], + 'category': line[0:categoryEnd]}) return result def readCategoriesFile(thefilename='categories', thepath='lists/'): @@ -54,7 +63,7 @@ def parseFiles(thefilenames): result.extend(parseFile(thefilename)) return result -def parseListLine(line): +def parseListLine(line, account): tmp = line.strip() if len(tmp) == 0 or tmp[0] == '#': return None @@ -65,23 +74,28 @@ def parseListLine(line): currency = linesplit[2].strip() if currency != '€': raise Exception('unknown currency ' + currency) - thecategory = ';'.join(linesplit[5:]).strip() + thedescription = "" + if len(linesplit) > 6: + thedescription = ';'.join(linesplit[6:]).strip() return {'date': linesplit[0].strip().replace('/', '-'), + 'account': account, 'amount': float(linesplit[1].strip()), 'currency': currency, 'name': linesplit[3].strip(), 'title': linesplit[4].strip(), - 'category': thecategory} + 'category': linesplit[5].strip(), + 'description': thedescription} def parseFile(thefilename, thepath='lists/'): result = [] linepos = 0 + account = _getAccountName(thefilename) with open(thepath + thefilename + '.list', 'r') as f: for line in f: - parsedLine = parseListLine(line) + parsedLine = parseListLine(line=line, account=account) if parsedLine is not None: - # thehash = str(hashlib.md5((parsedLine['date'] + str(parsedLine['amount']) + parsedLine['currency'] + parsedLine['name'] + parsedLine['title']).encode()).hexdigest()) + # thehash = str(hashlib.md5((parsedLine['date'] + str(parsedLine['amount']) + parsedLine['currency'] + parsedLine['name'] + parsedLine['title'] + parsedLine['description']).encode()).hexdigest()) parsedLine['id'] = thefilename + '_' + str(linepos) linepos = linepos + 1 result.append(parsedLine) @@ -90,19 +104,41 @@ def parseFile(thefilename, thepath='lists/'): def writeFile(thefilename, filecontent, thepath='lists/'): with open(thepath + thefilename + '.list', 'w') as thefile: for line in filecontent: - thefile.write(line['date'] + ';' + str(line['amount']) + ';' + line['currency'] + ';' + line['name'] + ';' + line['title'] + ';' + line['category'] + '\n') + thefile.write(line['date'] + ';' + str(line['amount']) + ';' + line['currency'] + ';' + line['name'] + ';' + line['title'] + ';' + line['category'] + ';' + line['description'] + '\n') + +def appendItem(thefilename, thedate, theaccount, theamount, thecurrency, thename, thetitle, thedescription, thecategory, thepath='lists/'): + with open(thepath + thefilename + '.list', 'a') as thefile: + thefile.write(thedate + ';' + str(theamount) + ';' + thecurrency + ';' + thename + ';' + thetitle + ';' + thecategory + ';' + thedescription + '\n') -def getLists(thepath='lists'): + parsedFile = parseFile(thefilename=thefilename, thepath=thepath) + return parsedFile[-1] + +def getLists(accounts=None, thepath='lists'): result = [] for (dirpath, dirnames, filenames) in os.walk(thepath): for filename in filenames: if filename.endswith('.list'): - result.append(filename[0:-5]) + accountName = _getAccountName(filename) + if accounts is None or accountName in accounts: + result.append(filename[0:-5]) break return sorted(result) -def getConsolidated(byCategory, traceNames, fromDate, toDate, categories, thepath='lists/'): - filecontent = parseFiles(getLists(thepath=thepath)) +def _getAccountName(filename): + pos = filename.find('-') + accountName = filename if pos == -1 else filename[0:pos] + return accountName + +def getAccounts(thepath='lists'): + lists = getLists(accounts=None, thepath=thepath) + accounts = {} + for l in lists: + accountName = _getAccountName(l) + accounts[accountName] = True + return sorted(accounts.keys()) + +def getConsolidated(byCategory, traceNames, fromDate, toDate, categories, accounts, thepattern=None, categorySelection=None, sortScatterBy='date', thepath='lists/'): + filecontent = parseFiles(getLists(accounts=accounts, thepath=thepath)) duplicatesMap = {} foundDuplicates = [] @@ -116,6 +152,10 @@ def getConsolidated(byCategory, traceNames, fromDate, toDate, categories, thepat sumdict = {} incomedict = {} + compiledPattern = None + if thepattern is not None: + compiledPattern = re.compile(thepattern) + for f in filecontent: if len(f['category']) == 0: dupLine = f['date'] + ' ' + str(f['amount']) + ' ' + f['currency'] + ' ' + f['name'] + ' ' + f['title'] @@ -128,14 +168,17 @@ def getConsolidated(byCategory, traceNames, fromDate, toDate, categories, thepat if f['category'] not in allcategoriesNames: allcategoriesNames.append(f['category']) - thedate = time.strptime(f['date'], '%Y-%m-%d') + thedate = datetime.datetime.strptime(f['date'], '%Y-%m-%d') theX = f['date'] - if (fromDate is None or thedate >= fromDate) and (toDate is None or thedate <= toDate): + if (categorySelection is None or f['category'] in categorySelection)\ + and (fromDate is None or thedate >= fromDate)\ + and (toDate is None or thedate <= toDate)\ + and (compiledPattern is None or compiledPattern.search('|'.join(map(str, f.values())))): if byCategory == 'month' or byCategory == 'profit': theX = f['date'][0:7] elif byCategory == 'year': theX = f['date'][0:4] - elif byCategory == 'day': + else: theX = f['date'] f['theX'] = theX @@ -188,6 +231,8 @@ def getConsolidated(byCategory, traceNames, fromDate, toDate, categories, thepat 'y': [], 'name': key, 'type': 'bar'} + if key == "Umbuchung": + thetrace["visible"] = "legendonly" for tracekey in sorted(allcategories[key].keys()): thetrace['x'].append(tracekey) thetrace['y'].append(allcategories[key][tracekey]) @@ -195,11 +240,12 @@ def getConsolidated(byCategory, traceNames, fromDate, toDate, categories, thepat result = {'traces': traces, 'foundDuplicates': foundDuplicates} if 'scatter' in traceNames: - scatter = {'date': [], 'id': [], 'amount': [], 'category': [], 'currency': [], 'name': [], 'title': [], 'theX': []} + scatter = {'date': [], 'account': [], 'id': [], 'amount': [], 'category': [], 'currency': [], 'name': [], 'title': [], 'description': [], 'theX': []} # 'mode': 'markers', 'type': 'scatter', 'visible': 'legendonly', 'name': 'Einzelumsätze', 'text': [], 'marker': {'size': 5, 'opacity': 0.5} - scatterlist = sorted(scatterlist, key=lambda x: x['date']) + scatterlist = sorted(scatterlist, key=lambda x: x[sortScatterBy]) for x in scatterlist: scatter['date'].append(x['date']) + scatter['account'].append(x['account']) scatter['id'].append(x['id']) scatter['amount'].append(x['amount']) scatter['currency'].append(x['currency']) @@ -207,14 +253,16 @@ def getConsolidated(byCategory, traceNames, fromDate, toDate, categories, thepat # scatter['text'].append(x['name'] + ' ' + x['title']) scatter['name'].append(x['name']) scatter['title'].append(x['title']) + scatter['description'].append(x['description']) scatter['theX'].append(x['theX']) result['scatter'] = scatter result['allcategoriesNames'] = sorted(allcategoriesNames, key=lambda x: x.lower()) return result -def updateCategory(itemId, thecategory, thepath='lists/'): - thefilenames = getLists(thepath) +def updateItem(itemId, thename=None, thetitle=None, theamount=None, thecurrency=None, thecategory=None, thedescription=None, thepath='lists/'): + thecategory = thecategory.replace(';', '-') + thefilenames = getLists(accounts=None, thepath=thepath) fileIdPos = itemId.rfind('_') if fileIdPos == -1: raise Exception('invalid id') @@ -227,7 +275,18 @@ def updateCategory(itemId, thecategory, thepath='lists/'): content = parseFile(thefilename, thepath=thepath) for c in content: if c['id'] == itemId: - c['category'] = thecategory + if thename is not None: + c['name'] = thename + if thetitle is not None: + c['title'] = thetitle + if theamount is not None: + c['amount'] = theamount + if thecurrency is not None: + c['currency'] = thecurrency + if thecategory is not None: + c['category'] = thecategory + if thedescription is not None: + c['description'] = thedescription doLock(thefilename=thefilename, thepath=thepath) writeFile(thefilename=thefilename, filecontent=content, thepath=thepath) @@ -235,34 +294,66 @@ def updateCategory(itemId, thecategory, thepath='lists/'): return True return False +def deleteEntry(itemId, thepath='lists/'): + thefilenames = getLists(accounts=None, thepath=thepath) + fileIdPos = itemId.rfind('_') + if fileIdPos == -1: + raise Exception('invalid id') + fileId = itemId[0:fileIdPos] + + for thefilename in thefilenames: + if not thefilename.startswith(fileId): + continue + + content = parseFile(thefilename, thepath=thepath) + filteredContent = [] + found = False + for c in content: + if not found and c['id'] == itemId: + found = True + continue + else: + filteredContent.append(c) + + if found: + doLock(thefilename=thefilename, thepath=thepath) + writeFile(thefilename=thefilename, filecontent=filteredContent, thepath=thepath) + doUnlock(thefilename=thefilename, thepath=thepath) + return True + return False + def convertToItem(scatter, pos): return { 'date': scatter['date'][i], + 'account': scatter['account'][i], 'amount': scatter['amount'][i], 'currency': scatter['currency'][i], 'name': scatter['name'][i], 'title': scatter['title'][i], + 'description': scatter['description'][i], 'category': scatter['category'][i], 'id': scatter['id'][i] } -def getUncategorizedItems(): +def getUncategorizedItems(accounts=None): categories = parseCategories() - consolidated = getConsolidated(byCategory='month', traceNames=['scatter'], fromDate=None, categories=categories, toDate=None) + consolidated = getConsolidated(byCategory='month', traceNames=['scatter'], fromDate=None, categories=categories, toDate=None, accounts=accounts) scatter = consolidated['scatter'] result = {} result['allcategoriesNames'] = consolidated['allcategoriesNames'] - resultItems = {'date': [], 'id': [], 'amount': [], 'category': [], 'currency': [], 'name': [], 'title': [], 'theX': []} + resultItems = {'date': [], 'account': [], 'id': [], 'amount': [], 'category': [], 'currency': [], 'name': [], 'title': [], 'description': [], 'theX': []} for i in range(0, len(scatter['date'])): if scatter['category'][i] == '' or scatter['category'][i] == 'nicht kategorisiert': resultItems['date'].append(scatter['date'][i]) + resultItems['account'].append(scatter['account'][i]) resultItems['id'].append(scatter['id'][i]) resultItems['amount'].append(scatter['amount'][i]) resultItems['currency'].append(scatter['currency'][i]) resultItems['category'].append(scatter['category'][i]) resultItems['name'].append(scatter['name'][i]) resultItems['title'].append(scatter['title'][i]) + resultItems['description'].append(scatter['description'][i]) resultItems['theX'].append(scatter['theX'][i]) result['items'] = resultItems return result diff --git a/static/add.png b/static/add.png new file mode 100644 index 0000000..13ebf09 Binary files /dev/null and b/static/add.png differ diff --git a/static/categorize.js b/static/categorize.js index 6c4ca34..ba60ee6 100644 --- a/static/categorize.js +++ b/static/categorize.js @@ -10,12 +10,25 @@ function insertCategories() { } } -function categorize(theid, successFunction) { +function updateItem(theid) { + var thename = null; + if ($('#name-' + theid).length == 1) { + thename = $('#name-' + theid).val(); + } + var thetitle = null; + if ($('#title-' + theid).length == 1) { + thename = $('#title-' + theid).val(); + } + var theamount = null; + if ($('#amount-' + theid).length == 1) { + thename = $('#amount-' + theid).val(); + } var theValue = $("#item-" + theid).val(); + var thedescription = $("#description-" + theid).val(); if (allcategoriesNames.indexOf(theValue) == -1) { allcategoriesNames.push(theValue); - catlistValues = allcategoriesNames.sort(function (a, b) { + allcategoriesNames = allcategoriesNames.sort(function (a, b) { return a.toLowerCase().localeCompare(b.toLowerCase()); }); // thats not very efficient. @@ -25,10 +38,12 @@ function categorize(theid, successFunction) { $("#button-" + theid).addClass("dontshow"); $.ajax({ type: "POST", - url: "/categorize", - data: JSON.stringify({itemId: theid, thecategory: theValue}), + url: "/updateItem", + data: JSON.stringify({itemId: theid, thecategory: theValue, thedescription: thedescription}), success: function(data) { - onItemCategorized(theid); + if (theValue.trim() != "") { + onItemCategorized(theid); + } }, contentType: "application/json; charset=utf-8" }); @@ -36,10 +51,117 @@ function categorize(theid, successFunction) { function isConfirmed(event, theid) { if (event.keyCode == 13) { - categorize(theid); + updateItem(theid); + event.target.blur(); return false; } $("#button-" + theid).removeClass("dontshow"); return true; } + +function isConfirmedNewItem(event) { + if (event.keyCode == 13) { + submitNewItem(); + event.target.blur(); + return false; + } + + $("#button-newItemInput").removeClass("dontshow"); + return true; +} + +function markEmptyFields(fieldIds) { + var result = false; + for (var i = 0; i < fieldIds.length; i++) { + if ($("#" + fieldIds[i]).val() == null || $("#" + fieldIds[i]).val().trim() == "") { + $("#" + fieldIds[i]).addClass("wrongInput"); + result = true; + } else { + $("#" + fieldIds[i]).removeClass("wrongInput"); + } + } + return result; +} + +function submitNewItem() { + var hasWrongInput = markEmptyFields(["newItemDate", "newItemName", "newItemTitle", "newItemAmount"]); + if (isNaN($("#newItemAmount").val())) { + $("#newItemAmount").addClass("wrongInput"); + hasWrongInput = true; + } + + if (!hasWrongInput) { + $.ajax({ + type: "POST", + url: "/addNewItem", + data: JSON.stringify({newItemDate: $("#newItemDate").val(), + newItemName: $("#newItemName").val(), + newItemTitle: $("#newItemTitle").val(), + newItemAmount: $("#newItemAmount").val(), + newItemCategory: $("#newItemCategory").val(), + newItemDescription: $("#newItemDescription").val() + }), + success: function(data) { + if (data.errormsg == "") { + $("#addRevenueInput").before(data.htmlentry); + $("#newItemName").val(""); + $("#newItemTitle").val(""); + $("#newItemAmount").val(""); + $("#newItemCategory").val(""); + $("#newItemDescription").val(""); + } else { + window.alert(data.errormsg); + } + }, + dataType: "json", + contentType: "application/json; charset=utf-8" + }); + } +} + + +var detailsParams = {theX: null, + byCategory: "month", + fromDate: null, + toDate: null, + accounts: null, + patternInput: null, + categorySelection: null, + sortScatterBy: 'date', + sortScatterByReverse: true + }; + +function doSortScatterBy(sortBy) { + if (detailsParams["sortScatterBy"] == sortBy) { + detailsParams["sortScatterByReverse"] = !detailsParams["sortScatterByReverse"]; + } else { + detailsParams["sortScatterBy"] = sortBy; + detailsParams["sortScatterByReverse"] = false; + } + showDetails(detailsParams); +} + +function showDetails(params, action=null, actionParam=null) { + $("#details").html("

Lade Details ...

"); + $.ajax({ + type: "POST", + url: "/getDetails", + data: JSON.stringify({theX: params["theX"], + byCategory: params["byCategory"], + fromDate: params["fromDate"], + toDate: params["toDate"], + accounts: params["accounts"], + patternInput: params["patternInput"], + categorySelection: params["categorySelection"], + sortScatterBy: params["sortScatterBy"], + sortScatterByReverse: params["sortScatterByReverse"], + action: action, + actionParam: actionParam}), + success: function(thedata) { + $("#details").html(thedata); + insertCategories(); + }, + contentType: "application/json; charset=utf-8" + }); +} diff --git a/static/categorize.png b/static/categorize.png new file mode 100644 index 0000000..8adc5ba Binary files /dev/null and b/static/categorize.png differ diff --git a/static/delete.png b/static/delete.png new file mode 100644 index 0000000..98838da Binary files /dev/null and b/static/delete.png differ diff --git a/static/new.png b/static/new.png new file mode 100644 index 0000000..fe48030 Binary files /dev/null and b/static/new.png differ diff --git a/static/note.png b/static/note.png new file mode 100644 index 0000000..d12dba9 Binary files /dev/null and b/static/note.png differ diff --git a/static/ok.png b/static/ok.png index 82658b1..af0cf26 100644 Binary files a/static/ok.png and b/static/ok.png differ diff --git a/static/style.css b/static/style.css index c284159..2719251 100644 --- a/static/style.css +++ b/static/style.css @@ -45,6 +45,23 @@ tr:hover { background-color: #ddd; } +td img { + vertical-align: middle; +} + +input[type=text] { + border: 1px solid #555; +} + +input[type=text]:focus { + border: 3px solid #555; + background-color: lightgreen; +} + +input.wrongInput { + background-color: #FB7373; +} + .dontshow { visibility: hidden; } diff --git a/views/categorize.tpl b/views/categorize.tpl index 10d60d2..c019a35 100644 --- a/views/categorize.tpl +++ b/views/categorize.tpl @@ -9,30 +9,49 @@ allcategoriesNames = {{! json.dumps(allcategoriesNames)}}; +
- + + + + + + + + + + + + + + + + + + + + + + -% for i in range(len(scatter['date']) - 1, -1, -1): +% therange = range(len(scatter['date']) - 1, -1, -1) if reverseSort else range(0, len(scatter['date'])) +% for i in therange: % if theX is None or scatter['theX'][i] == theX: % date = scatter['date'][i] % name = scatter['name'][i] - % shortname = scatter['name'][i][0:15] + % shortname = scatter['name'][i][0:20] % title = scatter['title'][i] + % account = scatter['account'][i] % theid = scatter['id'][i] - % shorttitle = scatter['title'][i][0:30] + % shorttitle = scatter['title'][i][0:40] % amountcurrency = str(scatter['amount'][i]) + scatter['currency'][i] % thecategory = '' if scatter['category'][i] == 'nicht kategorisiert' else scatter['category'][i] - - - - - - - + % description = scatter['description'][i] + % + % include('categoryItem.tpl', date=date, name=name, shortname=shortname, title=title, account=account, theid=theid, shorttitle=shorttitle, amountcurrency=amountcurrency, thecategory=thecategory, description=description) %end %end - +
DatumNameBeschreibungBetragKategorie
DatumNameBeschreibungBetragKategorieNotiz
{{date}}{{shortname}}{{shorttitle}}{{amountcurrency}} - - -
+ +
diff --git a/views/categoryItem.tpl b/views/categoryItem.tpl new file mode 100644 index 0000000..8bfc9c5 --- /dev/null +++ b/views/categoryItem.tpl @@ -0,0 +1,18 @@ + + {{date}} + {{shortname}} + {{shorttitle}} + {{amountcurrency}} + + + + + + + + + %if account == 'Bargeld': + + %end + + diff --git a/views/editCategories.tpl b/views/editCategories.tpl index ea9f8b9..8872fbc 100644 --- a/views/editCategories.tpl +++ b/views/editCategories.tpl @@ -3,6 +3,7 @@ + konto - Kategorien bearbeiten @@ -13,7 +14,8 @@ } $(document).ready(function() { - insertCategories(); + detailsParams["categorySelection"] = ["nicht kategorisiert"] + showDetails(detailsParams); }); @@ -28,7 +30,7 @@ -% include('categorize.tpl', scatter=uncategorized, allcategoriesNames=allcategoriesNames, theX=None, title="Umsätze ohne Kategorie:") +

konto — ©Michael Till Beck, 2018

diff --git a/views/index.tpl b/views/index.tpl index 8e46a36..7be540c 100644 --- a/views/index.tpl +++ b/views/index.tpl @@ -7,6 +7,7 @@ + konto - Übersicht @@ -17,16 +18,22 @@ @@ -160,15 +160,30 @@ $(document).ready(function() { % include('menu.tpl', site=site) -% if showDateSelector: -% fromDate = (datetime.datetime.today() - datetime.timedelta(days=5*30)).replace(day=1).strftime('%Y-%m-%d') -% toDate = datetime.datetime.today().strftime('%Y-%m-%d') -von: -bis: - -% end +
+ +
+ Einstellungen + + % for a in accounts: + {{a}} + % end + + + % if showDateSelector: + % fromDate = (datetime.datetime.today() - datetime.timedelta(days=5*30)).replace(day=1).strftime('%Y-%m-%d') + % toDate = datetime.datetime.today().strftime('%Y-%m-%d') + von: + bis: + % end + + Suche: + + + Umsätze aktualisieren +
+
-Umsätze aktualisieren