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