feat: first version of go rewrite complete
This commit is contained in:
parent
b138ea7fdd
commit
555bf29551
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
test-data/
|
||||||
174
archived-python/faktura.py
Executable file
174
archived-python/faktura.py
Executable file
@ -0,0 +1,174 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
import csv
|
||||||
|
from functools import reduce
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
path2file = "konto.csv"
|
||||||
|
else:
|
||||||
|
path2file = sys.argv[1]
|
||||||
|
|
||||||
|
def float2(value, default=0.0):
|
||||||
|
if value is None:
|
||||||
|
return default
|
||||||
|
try:
|
||||||
|
return float(value)
|
||||||
|
except ValueError:
|
||||||
|
return default
|
||||||
|
|
||||||
|
print("Detta program rapporterar skillnader mellan verifikat som refererar till varandra genom beskrivningen av verifikatet. Den utgår från krediter och hittar matchande debeter")
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
result = []
|
||||||
|
with open(path2file, "r") as file:
|
||||||
|
lines = file.readlines()
|
||||||
|
|
||||||
|
startLine = 12
|
||||||
|
|
||||||
|
# READING
|
||||||
|
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if "Konto" in line and "Verifikation" in line:
|
||||||
|
startLine = i
|
||||||
|
break
|
||||||
|
|
||||||
|
with open(path2file, "w") as file:
|
||||||
|
file.writelines(lines[startLine:-1])
|
||||||
|
|
||||||
|
with open(path2file, "r") as file:
|
||||||
|
csvreader = csv.DictReader(file, delimiter=";")
|
||||||
|
for row in csvreader:
|
||||||
|
result.append(row)
|
||||||
|
with open(path2file, "w") as file:
|
||||||
|
file.writelines(lines)
|
||||||
|
|
||||||
|
# FILE RESTORED, PARSE DATA
|
||||||
|
|
||||||
|
krediter = {}
|
||||||
|
debeter = {}
|
||||||
|
verfs = {}
|
||||||
|
sums = {}
|
||||||
|
header = result.pop(0)
|
||||||
|
for line in result:
|
||||||
|
if not line['"Verifikation"']:
|
||||||
|
continue
|
||||||
|
verf = line['"Verifikation"'].replace(" ","")
|
||||||
|
if verf not in verfs.keys():
|
||||||
|
verfs[verf] = line
|
||||||
|
verfs[verf]["Corrections"] = []
|
||||||
|
verfs[verf]["Value"] = float2(line['"Debet"'].replace(",",".").replace(" ",""))
|
||||||
|
verfs[verf]["Value"] -= float2(line['"Kredit"'].replace(",",".").replace(" ",""))
|
||||||
|
else:
|
||||||
|
verfs[verf]["Value"] -= float2(line['"Kredit"'].replace(",",".").replace(" ",""))
|
||||||
|
verfs[verf]["Value"] += float2(line['"Debet"'].replace(",",".").replace(" ",""))
|
||||||
|
|
||||||
|
## ELIMINATE CORRECTIONS
|
||||||
|
|
||||||
|
verfs = dict(reversed(sorted(verfs.items(), key = lambda x: int(x[0][1:]))))
|
||||||
|
|
||||||
|
newVerfs = dict(verfs)
|
||||||
|
|
||||||
|
corrections = []
|
||||||
|
for verf, line in verfs.items():
|
||||||
|
if "Korrigering" in line['"Beskrivning"']:
|
||||||
|
corrections.append(verf)
|
||||||
|
toDelete = []
|
||||||
|
missingVerfs = []
|
||||||
|
for verf, line in verfs.items():
|
||||||
|
if verf in reduce(lambda x,y: x + y, list(map(lambda x: [x[0]] + x[1], toDelete)),[]):
|
||||||
|
continue
|
||||||
|
if "Korrigering" in line['"Beskrivning"']:
|
||||||
|
print(line['"Beskrivning"'])
|
||||||
|
toRemove = re.findall("(A\\d+)(?:\\D){0,1}",line['"Beskrivning"'])[0]
|
||||||
|
if toRemove not in verfs.keys():
|
||||||
|
missingVerfs.append(toRemove)
|
||||||
|
else:
|
||||||
|
newVerfs[toRemove]["Corrections"].append(verf)
|
||||||
|
newVerfs[toRemove]["Corrections"].extend(newVerfs[verf]["Corrections"])
|
||||||
|
newVerfs[toRemove]["Value"] += line["Value"]
|
||||||
|
### check if toRemove verf should be removed
|
||||||
|
if abs(newVerfs[toRemove]["Value"]) < 0.0001:
|
||||||
|
toDelete.append((toRemove, newVerfs[toRemove]["Corrections"]))
|
||||||
|
elif len(line["Corrections"]) > 0:
|
||||||
|
toDelete.append((verf, [None] + line["Corrections"]))
|
||||||
|
|
||||||
|
verfs = newVerfs
|
||||||
|
|
||||||
|
if len(missingVerfs) > 0:
|
||||||
|
print("")
|
||||||
|
print(f"VARNING! {missingVerfs} saknas i verifikat listan men refereras till av korrektioner!")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
if len(toDelete) > 0:
|
||||||
|
print(f"Tar bort {len(toDelete)} korrigeringspar...")
|
||||||
|
for v, ks in toDelete:
|
||||||
|
if ks[0] == None:
|
||||||
|
print(f" Verifikaten {ks[1:]} delkorrigerar {v}")
|
||||||
|
ks = ks[1:]
|
||||||
|
else:
|
||||||
|
print(f" Verifikaten {ks} korrigerar bort hela {v}")
|
||||||
|
del verfs[v]
|
||||||
|
for k in ks:
|
||||||
|
del verfs[k]
|
||||||
|
|
||||||
|
|
||||||
|
## FINISH PARSING DATA
|
||||||
|
for verf, line in verfs.items():
|
||||||
|
if line["Value"] < 0:
|
||||||
|
krediter[verf] = line
|
||||||
|
elif line["Value"] > 0:
|
||||||
|
debeter[verf] = line
|
||||||
|
|
||||||
|
# FIND DIFFS AND LOG INTO DATA STRUCTURE
|
||||||
|
print("Beräknar skillnader mellan utbetalningar och utlägg")
|
||||||
|
for kVerf, kLine in krediter.items():
|
||||||
|
sums[kVerf] = {"value": kLine["Value"], "debets": [], "date": kLine['"Datum"']}
|
||||||
|
toDel = []
|
||||||
|
for dVerf, dLine in debeter.items():
|
||||||
|
debetVal = dLine["Value"]
|
||||||
|
[company, invoiceNr] = re.findall("( \\w?\\d+)(?:\\D){0,1}",dLine['"Beskrivning"'])
|
||||||
|
kVerf = None
|
||||||
|
for v, kLine in krediter.items():
|
||||||
|
if invoiceNr in kLine['"Beskrivning"'] and company in kLine['"Beskrivning"']:
|
||||||
|
kVerf = v
|
||||||
|
if kVerf == None:
|
||||||
|
continue
|
||||||
|
if debetVal == 0:
|
||||||
|
continue
|
||||||
|
if kVerf not in sums:
|
||||||
|
if kVerf not in corrections:
|
||||||
|
print(f" I debeterande verifikat {dVerf} berörs {kVerf}, men {kVerf} hittas inte. (saknas kostnadsställe?)")
|
||||||
|
continue
|
||||||
|
sumVal = sums[kVerf]["value"]
|
||||||
|
if abs(sumVal + debetVal) < 0.0001:
|
||||||
|
sums[kVerf]["value"] = 0
|
||||||
|
debetVal = 0
|
||||||
|
toDel.append(dVerf)
|
||||||
|
elif sumVal + debetVal < 0:
|
||||||
|
sums[kVerf]["value"] += debetVal
|
||||||
|
debetVal = 0
|
||||||
|
toDel.append(dVerf)
|
||||||
|
else:
|
||||||
|
sums[kVerf]["value"] = 0
|
||||||
|
debetVal = debetVal + sumVal
|
||||||
|
sums[kVerf]["debets"] = sums[kVerf]["debets"] + [dVerf]
|
||||||
|
if abs(debetVal) > 0.0001:
|
||||||
|
debeter[dVerf]["Value"] = debetVal
|
||||||
|
for dVerf in list(set(toDel)):
|
||||||
|
del debeter[dVerf]
|
||||||
|
|
||||||
|
# PRINT!
|
||||||
|
|
||||||
|
print("---------------------")
|
||||||
|
print("Nedan är alla utlägg/kredit verifikat och dess tillhörande betalningar.")
|
||||||
|
for verf, a in sums.items():
|
||||||
|
if abs(a["value"]) > 0.0001:
|
||||||
|
print(" VARNING! Ofullständig matchning i ",verf, ": ", a)
|
||||||
|
else:
|
||||||
|
print(f"{verf}: {a}")
|
||||||
|
print("")
|
||||||
|
print("Här kommer omatchade debeter")
|
||||||
|
for debet, debetLine in debeter.items():
|
||||||
|
print(debet + ": " + debetLine['"Beskrivning"'] + " -- ", debetLine["Value"])
|
||||||
272
example.txt
272
example.txt
@ -1,272 +0,0 @@
|
|||||||
A214
|
|
||||||
Utlägg för överlämning - Filip Wallin (DAG)
|
|
||||||
-1 155,00
|
|
||||||
-1 155,00
|
|
||||||
A215
|
|
||||||
Utlägg för möteskostnader - Tobias Olsson (DAG)
|
|
||||||
-202,30
|
|
||||||
-1 357,30
|
|
||||||
A216
|
|
||||||
Utlägg för representationskläder - Viktor Käll (DAG)
|
|
||||||
-7 103,20
|
|
||||||
-8 460,50
|
|
||||||
A218
|
|
||||||
Utlägg för representationskläder, 1800kr ligger inom godkänd budget för representationskläder av Styret - Salam Hani (DAG)
|
|
||||||
-1 800,00
|
|
||||||
-10 260,50
|
|
||||||
A219
|
|
||||||
Utlägg för representationskläder, 1800kr ligger inom godkänd budget för representationskläder av Styret - Gustav Bruhn (DAG)
|
|
||||||
-1 800,00
|
|
||||||
-12 060,50
|
|
||||||
A217
|
|
||||||
Utlägg för representationskläder - Viktor Käll (DAG)
|
|
||||||
-1 749,30
|
|
||||||
-13 809,80
|
|
||||||
A300
|
|
||||||
utlägg A216
|
|
||||||
7 103,20
|
|
||||||
-6 706,60
|
|
||||||
A302
|
|
||||||
utlägg A215
|
|
||||||
202,30
|
|
||||||
-6 504,30
|
|
||||||
A304
|
|
||||||
utlägg A219
|
|
||||||
1 800,00
|
|
||||||
-4 704,30
|
|
||||||
A305
|
|
||||||
utlägg A218
|
|
||||||
1 800,00
|
|
||||||
-2 904,30
|
|
||||||
A306
|
|
||||||
utlägg A214
|
|
||||||
1 155,00
|
|
||||||
-1 749,30
|
|
||||||
A307
|
|
||||||
utlägg A217
|
|
||||||
1 749,30
|
|
||||||
0,00
|
|
||||||
A3777
|
|
||||||
Utlägg för aspning - Viktor Käll (DAG)
|
|
||||||
-1 183,30
|
|
||||||
-1 183,30
|
|
||||||
A1223
|
|
||||||
Utlägg för möteskostnader - Gustav Bruhn (DAG)
|
|
||||||
-271,14
|
|
||||||
-1 454,44
|
|
||||||
A1222
|
|
||||||
Utlägg för möteskostnader - Viktor Käll (DAG)
|
|
||||||
-265,18
|
|
||||||
-1 719,62
|
|
||||||
A1265
|
|
||||||
Betalning av utlägg A1222 (DAG)
|
|
||||||
265,18
|
|
||||||
-1 454,44
|
|
||||||
A1266
|
|
||||||
Betalning av utlägg A1223
|
|
||||||
271,14
|
|
||||||
-1 183,30
|
|
||||||
A1440
|
|
||||||
Utlägg för möteskostnader - Tobias Olsson (DAG)
|
|
||||||
-263,02
|
|
||||||
-1 446,32
|
|
||||||
A1844
|
|
||||||
Utlägg för teambuilding - Alfred Karlsson (DAG)
|
|
||||||
-564,00
|
|
||||||
-2 010,32
|
|
||||||
A1846
|
|
||||||
Utlägg för teambuilding - Viktor Käll (DAG)
|
|
||||||
-943,68
|
|
||||||
-2 954,00
|
|
||||||
A1848
|
|
||||||
Utlägg för matinköp afterschool - Alfred Karlsson (DAG)
|
|
||||||
-772,89
|
|
||||||
-3 726,89
|
|
||||||
A1847
|
|
||||||
Utlägg för matinköp afterschool - Filip Wallin (DAG)
|
|
||||||
-375,40
|
|
||||||
-4 102,29
|
|
||||||
A1845
|
|
||||||
Utlägg för möteskostnader - Tim Karlsson (DAG)
|
|
||||||
-236,33
|
|
||||||
-4 338,62
|
|
||||||
A1850
|
|
||||||
Betalning av utlägg A1846 (DAG)
|
|
||||||
943,68
|
|
||||||
-3 394,94
|
|
||||||
A1851
|
|
||||||
Betalning av utlägg A1847 (DAG)
|
|
||||||
375,40
|
|
||||||
-3 019,54
|
|
||||||
A1905
|
|
||||||
Betalning av utlägg A1848 (DAG)
|
|
||||||
772,89
|
|
||||||
-2 246,65
|
|
||||||
A1906
|
|
||||||
Betalning av utlägg A1844 (DAG)
|
|
||||||
564,00
|
|
||||||
-1 682,65
|
|
||||||
A1907
|
|
||||||
Betalning av utlägg A1440 (DAG)
|
|
||||||
263,02
|
|
||||||
-1 419,63
|
|
||||||
A1908
|
|
||||||
Betalning av utlägg A1845 (DAG)
|
|
||||||
236,33
|
|
||||||
-1 183,30
|
|
||||||
A1992
|
|
||||||
Utlägg för möteskostnader - Viktor Käll (DAG)
|
|
||||||
-261,92
|
|
||||||
-1 445,22
|
|
||||||
A1993
|
|
||||||
Utlägg för afterschool - Salam Hani (DAG)
|
|
||||||
-83,46
|
|
||||||
-1 528,68
|
|
||||||
A1994
|
|
||||||
A1993
|
|
||||||
83,46
|
|
||||||
-1 445,22
|
|
||||||
A1995
|
|
||||||
A1992
|
|
||||||
261,92
|
|
||||||
-1 183,30
|
|
||||||
A2525
|
|
||||||
Utlägg för möteskostnader - Viktor Käll (DAG)
|
|
||||||
-110,80
|
|
||||||
-1 294,10
|
|
||||||
A2479
|
|
||||||
Utlägg för alkoholinköp pub - Salam Hani (DAG)
|
|
||||||
-2 789,20
|
|
||||||
-4 083,30
|
|
||||||
A2526
|
|
||||||
Utlägg för möteskostnader - Tim Karlsson (DAG)
|
|
||||||
-213,85
|
|
||||||
-4 297,15
|
|
||||||
A2813
|
|
||||||
Utlägg för teambuilding - Viktor Käll (DAG)
|
|
||||||
-1 750,00
|
|
||||||
-6 047,15
|
|
||||||
A2527
|
|
||||||
Utlägg för möteskostnader - Alfred Karlsson (DAG)
|
|
||||||
-257,09
|
|
||||||
-6 304,24
|
|
||||||
A2833
|
|
||||||
Betalning av utlägg A2479 (Kommitté)
|
|
||||||
2 789,20
|
|
||||||
-3 515,04
|
|
||||||
A3774
|
|
||||||
Utlägg för möteskostnader - Tobias Olsson (DAG)
|
|
||||||
-196,44
|
|
||||||
-3 711,48
|
|
||||||
A3305
|
|
||||||
Utlägg för möteskostnader - Salam Hani (DAG)
|
|
||||||
-280,00
|
|
||||||
-3 991,48
|
|
||||||
A2830
|
|
||||||
Betalning av utlägg A2527 (DAG)
|
|
||||||
257,09
|
|
||||||
-3 734,39
|
|
||||||
A2831
|
|
||||||
Betalning av utlägg A2526 (Kommitté)
|
|
||||||
213,85
|
|
||||||
-3 520,54
|
|
||||||
A2832
|
|
||||||
Betalning av utlägg A2525 (Kommitté)
|
|
||||||
110,80
|
|
||||||
-3 409,74
|
|
||||||
A2829
|
|
||||||
Betalning av utlägg A2813 (DAG)
|
|
||||||
1 750,00
|
|
||||||
-1 659,74
|
|
||||||
A3306
|
|
||||||
Utlägg för möteskostnader - Viktor Käll (DAG)
|
|
||||||
-280,00
|
|
||||||
-1 939,74
|
|
||||||
A3307
|
|
||||||
Utlägg för aspning - Alfred Karlsson (DAG)
|
|
||||||
-1 063,78
|
|
||||||
-3 003,52
|
|
||||||
A3308
|
|
||||||
Utlägg för aspning - Filip Wallin (DAG)
|
|
||||||
-939,50
|
|
||||||
-3 943,02
|
|
||||||
A3309
|
|
||||||
Utlägg för möteskostnader - Tobias Olsson (DAG)
|
|
||||||
-280,00
|
|
||||||
-4 223,02
|
|
||||||
A3310
|
|
||||||
Utlägg för möteskostnader - Viktor Käll (DAG)
|
|
||||||
-239,14
|
|
||||||
-4 462,16
|
|
||||||
A3749
|
|
||||||
Utlägg för [matinköp t.ex. afterschool] - Filip Wallin (DAG)
|
|
||||||
-3 817,17
|
|
||||||
-8 279,33
|
|
||||||
A3746
|
|
||||||
Utlägg för [arrangemangskostnader] - Filip Wallin (DAG)
|
|
||||||
-4 370,00
|
|
||||||
-12 649,33
|
|
||||||
A3313
|
|
||||||
Betalning av utlägg A3308 (DAG)
|
|
||||||
939,50
|
|
||||||
-11 709,83
|
|
||||||
A3314
|
|
||||||
Betalning av utlägg A3306 (DAG)
|
|
||||||
280,00
|
|
||||||
-11 429,83
|
|
||||||
A3315
|
|
||||||
Betalning av utlägg A3310 (DAG)
|
|
||||||
239,19
|
|
||||||
-11 190,64
|
|
||||||
A3316
|
|
||||||
Betalning av utlägg A3305 (DAG)
|
|
||||||
280,00
|
|
||||||
-10 910,64
|
|
||||||
A3317
|
|
||||||
Betalning av utlägg A3309 (DAG)
|
|
||||||
280,00
|
|
||||||
-10 630,64
|
|
||||||
A3318
|
|
||||||
Betalning av utlägg A3307 (DAG)
|
|
||||||
1 063,78
|
|
||||||
-9 566,86
|
|
||||||
A3751
|
|
||||||
Utlägg för möteskostnader - Viktor Käll (DAG)
|
|
||||||
-202,95
|
|
||||||
-9 769,81
|
|
||||||
A3842
|
|
||||||
Utlägg för [arrangemangskostnader] - Alfred Karlsson (DAG)
|
|
||||||
-3 693,40
|
|
||||||
-13 463,21
|
|
||||||
A3745
|
|
||||||
Utlägg för möteskostnader - Salam Hani (DAG)
|
|
||||||
-222,25
|
|
||||||
-13 685,46
|
|
||||||
A3796
|
|
||||||
Betalning av utlägg A3751 (DAG)
|
|
||||||
202,95
|
|
||||||
-13 482,51
|
|
||||||
A3797
|
|
||||||
Betalning av utlägg A3777 (DAG)
|
|
||||||
1 183,30
|
|
||||||
-12 299,21
|
|
||||||
A3798
|
|
||||||
Betalning av utlägg A3749 (DAG)
|
|
||||||
3 817,17
|
|
||||||
-8 482,04
|
|
||||||
A3799
|
|
||||||
Betalning av utlägg A3745 (DAG)
|
|
||||||
222,25
|
|
||||||
-8 259,79
|
|
||||||
A3800
|
|
||||||
Betalning av utlägg A3746 (DAG)
|
|
||||||
4 370,00
|
|
||||||
-3 889,79
|
|
||||||
A3801
|
|
||||||
Betalning av utlägg A3774 (DAG)
|
|
||||||
196,44
|
|
||||||
-3 693,35
|
|
||||||
A3843
|
|
||||||
Betalning av utlägg A3842 (DAG)
|
|
||||||
3 693,40
|
|
||||||
0,05
|
|
||||||
BIN
go-rewrite/bookkeep
Executable file
BIN
go-rewrite/bookkeep
Executable file
Binary file not shown.
212
go-rewrite/bookkeep.zsh
Normal file
212
go-rewrite/bookkeep.zsh
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
#compdef bookkeep
|
||||||
|
compdef _bookkeep bookkeep
|
||||||
|
|
||||||
|
# zsh completion for bookkeep -*- shell-script -*-
|
||||||
|
|
||||||
|
__bookkeep_debug()
|
||||||
|
{
|
||||||
|
local file="$BASH_COMP_DEBUG_FILE"
|
||||||
|
if [[ -n ${file} ]]; then
|
||||||
|
echo "$*" >> "${file}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_bookkeep()
|
||||||
|
{
|
||||||
|
local shellCompDirectiveError=1
|
||||||
|
local shellCompDirectiveNoSpace=2
|
||||||
|
local shellCompDirectiveNoFileComp=4
|
||||||
|
local shellCompDirectiveFilterFileExt=8
|
||||||
|
local shellCompDirectiveFilterDirs=16
|
||||||
|
local shellCompDirectiveKeepOrder=32
|
||||||
|
|
||||||
|
local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace keepOrder
|
||||||
|
local -a completions
|
||||||
|
|
||||||
|
__bookkeep_debug "\n========= starting completion logic =========="
|
||||||
|
__bookkeep_debug "CURRENT: ${CURRENT}, words[*]: ${words[*]}"
|
||||||
|
|
||||||
|
# The user could have moved the cursor backwards on the command-line.
|
||||||
|
# We need to trigger completion from the $CURRENT location, so we need
|
||||||
|
# to truncate the command-line ($words) up to the $CURRENT location.
|
||||||
|
# (We cannot use $CURSOR as its value does not work when a command is an alias.)
|
||||||
|
words=("${=words[1,CURRENT]}")
|
||||||
|
__bookkeep_debug "Truncated words[*]: ${words[*]},"
|
||||||
|
|
||||||
|
lastParam=${words[-1]}
|
||||||
|
lastChar=${lastParam[-1]}
|
||||||
|
__bookkeep_debug "lastParam: ${lastParam}, lastChar: ${lastChar}"
|
||||||
|
|
||||||
|
# For zsh, when completing a flag with an = (e.g., bookkeep -n=<TAB>)
|
||||||
|
# completions must be prefixed with the flag
|
||||||
|
setopt local_options BASH_REMATCH
|
||||||
|
if [[ "${lastParam}" =~ '-.*=' ]]; then
|
||||||
|
# We are dealing with a flag with an =
|
||||||
|
flagPrefix="-P ${BASH_REMATCH}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prepare the command to obtain completions
|
||||||
|
requestComp="${words[1]} __complete ${words[2,-1]}"
|
||||||
|
if [ "${lastChar}" = "" ]; then
|
||||||
|
# If the last parameter is complete (there is a space following it)
|
||||||
|
# We add an extra empty parameter so we can indicate this to the go completion code.
|
||||||
|
__bookkeep_debug "Adding extra empty parameter"
|
||||||
|
requestComp="${requestComp} \"\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
__bookkeep_debug "About to call: eval ${requestComp}"
|
||||||
|
|
||||||
|
# Use eval to handle any environment variables and such
|
||||||
|
out=$(eval ${requestComp} 2>/dev/null)
|
||||||
|
__bookkeep_debug "completion output: ${out}"
|
||||||
|
|
||||||
|
# Extract the directive integer following a : from the last line
|
||||||
|
local lastLine
|
||||||
|
while IFS='\n' read -r line; do
|
||||||
|
lastLine=${line}
|
||||||
|
done < <(printf "%s\n" "${out[@]}")
|
||||||
|
__bookkeep_debug "last line: ${lastLine}"
|
||||||
|
|
||||||
|
if [ "${lastLine[1]}" = : ]; then
|
||||||
|
directive=${lastLine[2,-1]}
|
||||||
|
# Remove the directive including the : and the newline
|
||||||
|
local suffix
|
||||||
|
(( suffix=${#lastLine}+2))
|
||||||
|
out=${out[1,-$suffix]}
|
||||||
|
else
|
||||||
|
# There is no directive specified. Leave $out as is.
|
||||||
|
__bookkeep_debug "No directive found. Setting do default"
|
||||||
|
directive=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
__bookkeep_debug "directive: ${directive}"
|
||||||
|
__bookkeep_debug "completions: ${out}"
|
||||||
|
__bookkeep_debug "flagPrefix: ${flagPrefix}"
|
||||||
|
|
||||||
|
if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then
|
||||||
|
__bookkeep_debug "Completion received error. Ignoring completions."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local activeHelpMarker="_activeHelp_ "
|
||||||
|
local endIndex=${#activeHelpMarker}
|
||||||
|
local startIndex=$((${#activeHelpMarker}+1))
|
||||||
|
local hasActiveHelp=0
|
||||||
|
while IFS='\n' read -r comp; do
|
||||||
|
# Check if this is an activeHelp statement (i.e., prefixed with $activeHelpMarker)
|
||||||
|
if [ "${comp[1,$endIndex]}" = "$activeHelpMarker" ];then
|
||||||
|
__bookkeep_debug "ActiveHelp found: $comp"
|
||||||
|
comp="${comp[$startIndex,-1]}"
|
||||||
|
if [ -n "$comp" ]; then
|
||||||
|
compadd -x "${comp}"
|
||||||
|
__bookkeep_debug "ActiveHelp will need delimiter"
|
||||||
|
hasActiveHelp=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$comp" ]; then
|
||||||
|
# If requested, completions are returned with a description.
|
||||||
|
# The description is preceded by a TAB character.
|
||||||
|
# For zsh's _describe, we need to use a : instead of a TAB.
|
||||||
|
# We first need to escape any : as part of the completion itself.
|
||||||
|
comp=${comp//:/\\:}
|
||||||
|
|
||||||
|
local tab="$(printf '\t')"
|
||||||
|
comp=${comp//$tab/:}
|
||||||
|
|
||||||
|
__bookkeep_debug "Adding completion: ${comp}"
|
||||||
|
completions+=${comp}
|
||||||
|
lastComp=$comp
|
||||||
|
fi
|
||||||
|
done < <(printf "%s\n" "${out[@]}")
|
||||||
|
|
||||||
|
# Add a delimiter after the activeHelp statements, but only if:
|
||||||
|
# - there are completions following the activeHelp statements, or
|
||||||
|
# - file completion will be performed (so there will be choices after the activeHelp)
|
||||||
|
if [ $hasActiveHelp -eq 1 ]; then
|
||||||
|
if [ ${#completions} -ne 0 ] || [ $((directive & shellCompDirectiveNoFileComp)) -eq 0 ]; then
|
||||||
|
__bookkeep_debug "Adding activeHelp delimiter"
|
||||||
|
compadd -x "--"
|
||||||
|
hasActiveHelp=0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then
|
||||||
|
__bookkeep_debug "Activating nospace."
|
||||||
|
noSpace="-S ''"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $((directive & shellCompDirectiveKeepOrder)) -ne 0 ]; then
|
||||||
|
__bookkeep_debug "Activating keep order."
|
||||||
|
keepOrder="-V"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then
|
||||||
|
# File extension filtering
|
||||||
|
local filteringCmd
|
||||||
|
filteringCmd='_files'
|
||||||
|
for filter in ${completions[@]}; do
|
||||||
|
if [ ${filter[1]} != '*' ]; then
|
||||||
|
# zsh requires a glob pattern to do file filtering
|
||||||
|
filter="\*.$filter"
|
||||||
|
fi
|
||||||
|
filteringCmd+=" -g $filter"
|
||||||
|
done
|
||||||
|
filteringCmd+=" ${flagPrefix}"
|
||||||
|
|
||||||
|
__bookkeep_debug "File filtering command: $filteringCmd"
|
||||||
|
_arguments '*:filename:'"$filteringCmd"
|
||||||
|
elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then
|
||||||
|
# File completion for directories only
|
||||||
|
local subdir
|
||||||
|
subdir="${completions[1]}"
|
||||||
|
if [ -n "$subdir" ]; then
|
||||||
|
__bookkeep_debug "Listing directories in $subdir"
|
||||||
|
pushd "${subdir}" >/dev/null 2>&1
|
||||||
|
else
|
||||||
|
__bookkeep_debug "Listing directories in ."
|
||||||
|
fi
|
||||||
|
|
||||||
|
local result
|
||||||
|
_arguments '*:dirname:_files -/'" ${flagPrefix}"
|
||||||
|
result=$?
|
||||||
|
if [ -n "$subdir" ]; then
|
||||||
|
popd >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
return $result
|
||||||
|
else
|
||||||
|
__bookkeep_debug "Calling _describe"
|
||||||
|
if eval _describe $keepOrder "completions" completions $flagPrefix $noSpace; then
|
||||||
|
__bookkeep_debug "_describe found some completions"
|
||||||
|
|
||||||
|
# Return the success of having called _describe
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
__bookkeep_debug "_describe did not find completions."
|
||||||
|
__bookkeep_debug "Checking if we should do file completion."
|
||||||
|
if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then
|
||||||
|
__bookkeep_debug "deactivating file completion"
|
||||||
|
|
||||||
|
# We must return an error code here to let zsh know that there were no
|
||||||
|
# completions found by _describe; this is what will trigger other
|
||||||
|
# matching algorithms to attempt to find completions.
|
||||||
|
# For example zsh can match letters in the middle of words.
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
# Perform file completion
|
||||||
|
__bookkeep_debug "Activating file completion"
|
||||||
|
|
||||||
|
# We must return the result of this command, so it must be the
|
||||||
|
# last command, or else we must store its result to return it.
|
||||||
|
_arguments '*:filename:_files'" ${flagPrefix}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# don't run the completion function when being source-ed or eval-ed
|
||||||
|
if [ "$funcstack[1]" = "_bookkeep" ]; then
|
||||||
|
_bookkeep
|
||||||
|
fi
|
||||||
99
go-rewrite/cmd/outlays.go
Normal file
99
go-rewrite/cmd/outlays.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"math"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/rodaine/table"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"kryddan.xyz/bookkeep/process"
|
||||||
|
"kryddan.xyz/bookkeep/vismaparse"
|
||||||
|
)
|
||||||
|
|
||||||
|
func printFaults(faults []process.FaultyOutlay) {
|
||||||
|
headerFmt := color.New(color.FgGreen, color.Underline).SprintfFunc()
|
||||||
|
columnFmt := color.New(color.FgYellow).SprintfFunc()
|
||||||
|
|
||||||
|
tbl := table.New("Verification", "Description", "Related Verf.", "Error").
|
||||||
|
WithPadding(2).
|
||||||
|
WithHeaderFormatter(headerFmt).
|
||||||
|
WithFirstColumnFormatter(columnFmt)
|
||||||
|
|
||||||
|
totalChange := 0
|
||||||
|
for _, fault := range faults {
|
||||||
|
localChange := fault.Row.Debet - fault.Row.Kredit
|
||||||
|
totalChange += localChange
|
||||||
|
localDecimal := int(math.Max(float64(localChange%100), float64(-localChange%100)))
|
||||||
|
difference := fmt.Sprintf("%6d.%02d", localChange/100, localDecimal)
|
||||||
|
description := fault.Row.Description
|
||||||
|
if len(description) > 80 {
|
||||||
|
description = description[:76] + "..."
|
||||||
|
}
|
||||||
|
if len(fault.ContributingVerifications) == 0 {
|
||||||
|
tbl.AddRow(fault.Row.Verification, description, "", difference)
|
||||||
|
} else {
|
||||||
|
tbl.AddRow(fault.Row.Verification, description, fault.ContributingVerifications, difference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
totalDecimal := int(math.Max(float64(totalChange%100), float64(-totalChange%100)))
|
||||||
|
totalChangeString := fmt.Sprintf("%6d.%02d", totalChange/100, totalDecimal)
|
||||||
|
tbl.AddRow("Total", "", "", totalChangeString)
|
||||||
|
|
||||||
|
tbl.Print()
|
||||||
|
}
|
||||||
|
|
||||||
|
func OutlayCmd() *cobra.Command {
|
||||||
|
unitsFlagName := "unit"
|
||||||
|
accountNumberFlagName := "account"
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "outlays [file]",
|
||||||
|
Short: "This command analyses outlays in the provided file",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
filePath := args[0]
|
||||||
|
units, err := cmd.Flags().GetStringArray(unitsFlagName)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("get --units flag", slog.Any("err", err))
|
||||||
|
}
|
||||||
|
accountNumber, err := cmd.Flags().GetInt(accountNumberFlagName)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("get --units flag", slog.Any("err", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
org, err := vismaparse.LoadSheet(filePath)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("load sheet failed", slog.Any("err", err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, account := range org.Accounts {
|
||||||
|
if account.AccountNumber == accountNumber {
|
||||||
|
change := 0
|
||||||
|
for unit, perUnitAccount := range process.PerOrgUnit(account) {
|
||||||
|
if len(units) != 0 && !slices.Contains(units, unit) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
faults, ok := process.ValidateOutlays(perUnitAccount)
|
||||||
|
if ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Printf("\n----- Faulty verifications in %s -----\n", unit)
|
||||||
|
for _, fault := range faults {
|
||||||
|
change += fault.Row.Debet
|
||||||
|
change -= fault.Row.Kredit
|
||||||
|
}
|
||||||
|
printFaults(faults)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().StringArray(unitsFlagName, []string{""}, "The organizational units to analyse. Omit to include all")
|
||||||
|
cmd.Flags().Int(accountNumberFlagName, 2820, "The account number which tracks outlays")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
42
go-rewrite/cmd/root.go
Normal file
42
go-rewrite/cmd/root.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2026 NAME HERE <EMAIL ADDRESS>
|
||||||
|
*/
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// rootCmd represents the base command when called without any subcommands
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "bookkeep",
|
||||||
|
Short: "A book keeping helper to validate certain accounts",
|
||||||
|
Long: `bookkeep is a CLI library that validates book keeping accounts.
|
||||||
|
to quickly create a Cobra application.`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||||
|
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||||
|
func Execute() {
|
||||||
|
rootCmd.AddCommand()
|
||||||
|
err := rootCmd.Execute()
|
||||||
|
if err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Here you will define your flags and configuration settings.
|
||||||
|
// Cobra supports persistent flags, which, if defined here,
|
||||||
|
// will be global for your application.
|
||||||
|
|
||||||
|
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.vismascripts.yaml)")
|
||||||
|
|
||||||
|
// Cobra also supports local flags, which will only run
|
||||||
|
// when this action is called directly.
|
||||||
|
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||||
|
outlays := OutlayCmd()
|
||||||
|
rootCmd.AddCommand(outlays)
|
||||||
|
}
|
||||||
16
go-rewrite/go.mod
Normal file
16
go-rewrite/go.mod
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
module kryddan.xyz/bookkeep
|
||||||
|
|
||||||
|
go 1.27.1
|
||||||
|
|
||||||
|
require github.com/spf13/cobra v1.10.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/fatih/color v1.19.0 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/rodaine/table v1.4.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
|
)
|
||||||
24
go-rewrite/go.sum
Normal file
24
go-rewrite/go.sum
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||||
|
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/rodaine/table v1.4.0 h1:sbGLYXgrc15Db32nuFRamNXP3QdfTxTxXIvdM3fsjEA=
|
||||||
|
github.com/rodaine/table v1.4.0/go.mod h1:2XS7TM7slleLUM1km5Zsr9Ks+F2p7Y8F5ycDQq2Z7hk=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
11
go-rewrite/install-zsh.sh
Executable file
11
go-rewrite/install-zsh.sh
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
./bookkeep completion zsh > _bookkeep
|
||||||
|
sudo mkdir -p /usr/local/share/zsh/site-functions
|
||||||
|
sudo cp _bookkeep /usr/local/share/zsh/site-functions
|
||||||
|
mkdir -p ~/.zsh/completions
|
||||||
|
cp _bookkeep ~/.zsh/completions
|
||||||
|
|
||||||
|
echo 'fpath=(~/.zsh/completions $fpath)' >> ~/.zshrc
|
||||||
|
echo 'autoload -U compinit && compinit' >> ~/.zshrc
|
||||||
|
|
||||||
|
rm _bookkeep
|
||||||
10
go-rewrite/main.go
Normal file
10
go-rewrite/main.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/*
|
||||||
|
Copyright © 2026 NAME HERE <EMAIL ADDRESS>
|
||||||
|
*/
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "kryddan.xyz/bookkeep/cmd"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.Execute()
|
||||||
|
}
|
||||||
116
go-rewrite/process/process.go
Normal file
116
go-rewrite/process/process.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package process
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"kryddan.xyz/bookkeep/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PerOrgUnit(account types.BookkeepingAccount) map[string]types.BookkeepingAccount {
|
||||||
|
out := map[string]types.BookkeepingAccount{}
|
||||||
|
for _, row := range account.Rows {
|
||||||
|
unit := strings.ToLower(*cmp.Or(row.OrganizationUnit, new("")))
|
||||||
|
if _, ok := out[unit]; !ok {
|
||||||
|
out[unit] = types.BookkeepingAccount{
|
||||||
|
Name: account.Name,
|
||||||
|
AccountNumber: account.AccountNumber,
|
||||||
|
Rows: []types.BookkeepingAccountRow{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v := out[unit]
|
||||||
|
v.Rows = append(v.Rows, row)
|
||||||
|
out[unit] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
type FaultyOutlay struct {
|
||||||
|
Row types.BookkeepingAccountRow
|
||||||
|
ContributingVerifications []types.Verification
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateOutlays(account types.BookkeepingAccount) ([]FaultyOutlay, bool) {
|
||||||
|
rows := sortRows(account.Rows)
|
||||||
|
slices.Reverse(rows)
|
||||||
|
|
||||||
|
talliedVerifications := map[string]types.BookkeepingAccountRow{}
|
||||||
|
originalVerifications := map[string]types.BookkeepingAccountRow{}
|
||||||
|
for _, row := range rows {
|
||||||
|
if r, ok := talliedVerifications[row.Verification.String()]; ok {
|
||||||
|
r.Debet += row.Debet
|
||||||
|
r.Kredit += row.Kredit
|
||||||
|
talliedVerifications[row.Verification.String()] = r
|
||||||
|
originalVerifications[row.Verification.String()] = r
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
talliedVerifications[row.Verification.String()] = row
|
||||||
|
originalVerifications[row.Verification.String()] = row
|
||||||
|
}
|
||||||
|
|
||||||
|
backlinks := map[string][]string{}
|
||||||
|
for rowVerf, row := range originalVerifications {
|
||||||
|
if len(row.LinkedVerifications) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if row.Kredit == 0 && row.Debet == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
alteredRow := talliedVerifications[rowVerf]
|
||||||
|
if alteredRow.Kredit == 0 && alteredRow.Debet == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
alterValue := func(old int, adjustment int) (int, int) {
|
||||||
|
v := old + adjustment
|
||||||
|
if v > 0 {
|
||||||
|
return v, 0
|
||||||
|
} else {
|
||||||
|
return 0, -v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, linked := range row.LinkedVerifications {
|
||||||
|
linkedRow := talliedVerifications[linked.String()]
|
||||||
|
if linkedRow.Debet != 0 && row.Kredit != 0 {
|
||||||
|
linkedRow.Debet, row.Kredit = alterValue(linkedRow.Debet, -row.Kredit)
|
||||||
|
} else if linkedRow.Kredit != 0 && row.Debet != 0 {
|
||||||
|
linkedRow.Kredit, row.Debet = alterValue(linkedRow.Kredit, -row.Debet)
|
||||||
|
}
|
||||||
|
backlinks[linked.String()] = append(backlinks[linked.String()], row.Verification.String())
|
||||||
|
talliedVerifications[linked.String()] = linkedRow
|
||||||
|
}
|
||||||
|
|
||||||
|
talliedVerifications[row.Verification.String()] = row
|
||||||
|
}
|
||||||
|
|
||||||
|
faults := []FaultyOutlay{}
|
||||||
|
for verf, row := range talliedVerifications {
|
||||||
|
if row.Debet != 0 || row.Kredit != 0 {
|
||||||
|
contributingVerfs := []types.Verification{}
|
||||||
|
bls := backlinks[verf]
|
||||||
|
for _, v := range bls {
|
||||||
|
contributingVerfs = append(contributingVerfs, talliedVerifications[v].Verification)
|
||||||
|
}
|
||||||
|
faults = append(faults, FaultyOutlay{
|
||||||
|
Row: row,
|
||||||
|
ContributingVerifications: contributingVerfs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return faults, len(faults) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortRows(rows []types.BookkeepingAccountRow) []types.BookkeepingAccountRow {
|
||||||
|
slices.SortFunc(rows, func(a, b types.BookkeepingAccountRow) int {
|
||||||
|
if a.Verification.Series != b.Verification.Series {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return a.Verification.Number - b.Verification.Number
|
||||||
|
})
|
||||||
|
|
||||||
|
return rows
|
||||||
|
}
|
||||||
41
go-rewrite/types/types.go
Normal file
41
go-rewrite/types/types.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Verification struct {
|
||||||
|
Series string
|
||||||
|
Number int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Invoice struct {
|
||||||
|
Number int
|
||||||
|
}
|
||||||
|
|
||||||
|
type BookkeepingAccountRow struct {
|
||||||
|
Verification Verification
|
||||||
|
Date time.Time
|
||||||
|
Description string
|
||||||
|
Project string
|
||||||
|
OrganizationUnit *string
|
||||||
|
LinkedVerifications []Verification
|
||||||
|
LinkedInvoices []Invoice
|
||||||
|
Debet int
|
||||||
|
Kredit int
|
||||||
|
}
|
||||||
|
|
||||||
|
type BookkeepingAccount struct {
|
||||||
|
Name string
|
||||||
|
AccountNumber int
|
||||||
|
Rows []BookkeepingAccountRow
|
||||||
|
}
|
||||||
|
|
||||||
|
type Organization struct {
|
||||||
|
Accounts []BookkeepingAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Verification) String() string {
|
||||||
|
return fmt.Sprintf("%s%d", v.Series, v.Number)
|
||||||
|
}
|
||||||
261
go-rewrite/vismaparse/vismascripts.go
Normal file
261
go-rewrite/vismaparse/vismascripts.go
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
package vismaparse
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/csv"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"kryddan.xyz/bookkeep/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidRow error = errors.New("invalid input row")
|
||||||
|
ErrInvalidTable error = errors.New("invalid input table")
|
||||||
|
)
|
||||||
|
|
||||||
|
type accountRange struct {
|
||||||
|
High int
|
||||||
|
Low int
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadSheet(path string) (types.Organization, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return types.Organization{}, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
// Close the spreadsheet.
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
csvReader := csv.NewReader(f)
|
||||||
|
csvReader.Comma = ';'
|
||||||
|
csvReader.LazyQuotes = true
|
||||||
|
|
||||||
|
output, err := csvReader.ReadAll()
|
||||||
|
if err != nil {
|
||||||
|
return types.Organization{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
trimmed := trimHeaders(output)
|
||||||
|
|
||||||
|
ranges := accountRanges(trimmed)
|
||||||
|
accounts := []types.BookkeepingAccount{}
|
||||||
|
for _, r := range ranges {
|
||||||
|
account, err := constructBookkeepingAccount(trimmed[r.Low:r.High])
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("construct bookeeping account", slog.Any("err", err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
accounts = append(accounts, account)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.Organization{Accounts: accounts}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimHeaders(input [][]string) [][]string {
|
||||||
|
for i, line := range input {
|
||||||
|
if strings.Contains(line[0], "Konto") {
|
||||||
|
return input[i+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
func accountRanges(input [][]string) []accountRange {
|
||||||
|
var low int
|
||||||
|
ranges := []accountRange{}
|
||||||
|
for i, line := range input {
|
||||||
|
if line[0] != "" && low == 0 {
|
||||||
|
low = i
|
||||||
|
} else if line[0] != "" {
|
||||||
|
ranges = append(
|
||||||
|
ranges,
|
||||||
|
accountRange{
|
||||||
|
High: i - 1,
|
||||||
|
Low: low,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
low = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ranges
|
||||||
|
}
|
||||||
|
|
||||||
|
func constructBookkeepingAccount(input [][]string) (types.BookkeepingAccount, error) {
|
||||||
|
if len(input) < 2 || input[0][0] == "" {
|
||||||
|
return types.BookkeepingAccount{}, ErrInvalidTable
|
||||||
|
}
|
||||||
|
splitAccountString := strings.SplitN(input[0][0], " ", 2)
|
||||||
|
accountNumber, err := strconv.Atoi(splitAccountString[0])
|
||||||
|
if err != nil {
|
||||||
|
return types.BookkeepingAccount{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([]types.BookkeepingAccountRow, 0, len(input))
|
||||||
|
for _, line := range input[2:] {
|
||||||
|
row, err := cosntructBookeepingAccountRow(line)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrInvalidRow) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return types.BookkeepingAccount{}, err
|
||||||
|
}
|
||||||
|
rows = append(rows, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.BookkeepingAccount{
|
||||||
|
AccountNumber: accountNumber,
|
||||||
|
Name: splitAccountString[1],
|
||||||
|
Rows: rows,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ACCOUNT_INDEX int32 = 0
|
||||||
|
VERIFICATION_INDEX int32 = 1
|
||||||
|
DATE_INDEX int32 = 2
|
||||||
|
DESCRIPTION_INDEX int32 = 3
|
||||||
|
PROJECT_INDEX int32 = 7
|
||||||
|
KOSTNADSSTALLE_INDEX int32 = 8
|
||||||
|
DEBET_INDEX int32 = 10
|
||||||
|
KREDIT_INDEX int32 = 11
|
||||||
|
)
|
||||||
|
|
||||||
|
// index : value
|
||||||
|
// 0 : Account
|
||||||
|
// 1 : Verification
|
||||||
|
// 2 : Date
|
||||||
|
// 3 : Description
|
||||||
|
// 4 : Transaction text
|
||||||
|
// 5-6: VAT
|
||||||
|
// 7 : Project
|
||||||
|
// 8 : Kostnadsställe
|
||||||
|
// 9 : Motpart
|
||||||
|
// 10: Debet
|
||||||
|
// 11: Kredit
|
||||||
|
// 12: Beloppp
|
||||||
|
// 13: Saldo
|
||||||
|
func cosntructBookeepingAccountRow(input []string) (types.BookkeepingAccountRow, error) {
|
||||||
|
if len(input) <= 11 || input[VERIFICATION_INDEX] == "" {
|
||||||
|
return types.BookkeepingAccountRow{}, ErrInvalidRow
|
||||||
|
}
|
||||||
|
verification, err := parseVerification(input[VERIFICATION_INDEX])
|
||||||
|
if err != nil {
|
||||||
|
return types.BookkeepingAccountRow{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
debet, err := parseAmount(input[DEBET_INDEX])
|
||||||
|
if err != nil {
|
||||||
|
return types.BookkeepingAccountRow{}, err
|
||||||
|
}
|
||||||
|
kredit, err := parseAmount(input[KREDIT_INDEX])
|
||||||
|
if err != nil {
|
||||||
|
return types.BookkeepingAccountRow{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
date, err := time.Parse(time.DateOnly, input[DATE_INDEX])
|
||||||
|
if err != nil {
|
||||||
|
return types.BookkeepingAccountRow{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
linkedVerifications, err := parseLinkedVerifications(input[DESCRIPTION_INDEX])
|
||||||
|
if err != nil {
|
||||||
|
return types.BookkeepingAccountRow{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.BookkeepingAccountRow{
|
||||||
|
Verification: verification,
|
||||||
|
LinkedVerifications: linkedVerifications,
|
||||||
|
Project: input[PROJECT_INDEX],
|
||||||
|
Description: input[DESCRIPTION_INDEX],
|
||||||
|
OrganizationUnit: &input[KOSTNADSSTALLE_INDEX],
|
||||||
|
Debet: debet,
|
||||||
|
Kredit: kredit,
|
||||||
|
Date: date,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseVerification(input string) (types.Verification, error) {
|
||||||
|
r := regexp.MustCompile(`(?P<label>[a-zA-Z])\W*(?P<number>\d+)`)
|
||||||
|
|
||||||
|
verification := types.Verification{}
|
||||||
|
matches := r.FindStringSubmatch(input)
|
||||||
|
if len(matches) < 3 {
|
||||||
|
return types.Verification{}, errors.New("not enought matches")
|
||||||
|
}
|
||||||
|
for i, name := range r.SubexpNames() {
|
||||||
|
switch name {
|
||||||
|
case "label":
|
||||||
|
verification.Series = matches[i]
|
||||||
|
case "number":
|
||||||
|
verfNumber, err := strconv.Atoi(matches[i])
|
||||||
|
if err != nil {
|
||||||
|
return types.Verification{}, err
|
||||||
|
}
|
||||||
|
verification.Number = verfNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return verification, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAmount(input string) (int, error) {
|
||||||
|
parts := strings.Split(input, ",")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
// allow invalid parts
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
integer, err := strconv.Atoi(strings.ReplaceAll(parts[0], " ", ""))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
decimal, err := strconv.Atoi(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return integer*100 + decimal, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLinkedVerifications(input string) ([]types.Verification, error) {
|
||||||
|
r := regexp.MustCompile(`(?P<label>[a-zA-Z])\W*(?P<number>\d+)`)
|
||||||
|
|
||||||
|
allMatches := r.FindAllStringSubmatch(input, -1)
|
||||||
|
out := []types.Verification{}
|
||||||
|
for _, matches := range allMatches {
|
||||||
|
verification := types.Verification{}
|
||||||
|
|
||||||
|
if len(matches) < 3 {
|
||||||
|
return nil, errors.New("not enought matches")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, name := range r.SubexpNames() {
|
||||||
|
switch name {
|
||||||
|
case "label":
|
||||||
|
verification.Series = matches[i]
|
||||||
|
case "number":
|
||||||
|
verfNumber, err := strconv.Atoi(matches[i])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
verification.Number = verfNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = append(out, verification)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user