From: Paul Hänsch Date: Tue, 21 Jan 2025 02:50:37 +0000 (+0100) Subject: template for invoices, billed by hour X-Git-Url: https://git.plutz.net/?a=commitdiff_plain;h=e9966b74a9bb80e87f0d87e602f634553d5ecbd8;p=invoices template for invoices, billed by hour --- diff --git a/tmpl_byhour/form.css b/tmpl_byhour/form.css new file mode 100644 index 0000000..b5432ce --- /dev/null +++ b/tmpl_byhour/form.css @@ -0,0 +1,130 @@ +form { + width: 480pt; + max-width: 100%; + margin: auto; +} + +input + label { margin-left: unset; } +label { + display: block; + font-weight: bold; + margin-top: 1em; +} +input, textarea { + display: block; + width: 100%; +} + +label[for=invnum], +input#invnum { + width: 48%; +} +label[for=invnum] { + margin-top: 2em; +} + +span.warning { + color: #F00; +} + +label[for=date], +input#date { + width: 48%; + text-align: right; + margin-left: 52%; +} +label[for=date] { + margin-top: -43pt; +} + +label[for=rate], +input#rate { + width: 48%; + text-align: right; + margin-left: 52%; +} + +table { + width: 100%; + margin-top: 2em; +} +table th, table td { + text-align: left; + padding: 0; +} + +table tr > :nth-child(1) { + width: 6em; + text-align: left; +} +table tr > :nth-child(1) textarea { + text-align: right; +} +table tr > :nth-child(2) { + text-align: left; +} +table tr > :nth-child(3) { + width: 4em; + text-align: right; +} +table tr > :nth-child(4) { + width: 6em; + text-align: right; +} + +table td > input { + position: absolute; + top: 0; bottom: 0; +} +table td > textarea { + width: 100%; +} + +table tr > th[colspan="3"] { + width: unset; + text-align: right; +} +table tr > th[colspan="3"] + td { + width: 6em; + text-align: right; +} + +textarea#freeformbottom { + min-height: 10em; +} + +input#taxfree { + margin-top: 2em; +} +input[name=taxtype], +input[name=taxtype] + label { + display: inline; + line-height: 2.5em; + width: unset; +} +input[name=taxfreetext] { + display: none; +} +input#taxfree:checked ~ input[name=taxfreetext] { + display: block; +} + +input#taxrate { width: 4em; } +label[for=taxrate]:before { + white-space: pre; + content: '\A'; +} +label[for=taxrate], input#taxrate { + display: none; + line-height: 1.375em; +} +input#taxnet:checked ~ label[for=taxrate], +input#taxnet:checked ~ input#taxrate, +input#taxgross:checked ~ label[for=taxrate], +input#taxgross:checked ~ input#taxrate { + display: inline; +} + +label[for=status] { + margin-top: 2em; +} diff --git a/tmpl_byhour/form.sh b/tmpl_byhour/form.sh new file mode 100755 index 0000000..9cc32ca --- /dev/null +++ b/tmpl_byhour/form.sh @@ -0,0 +1,175 @@ +#!/bin/sh + +. "$_EXEC/datetime.sh" + +sumtotal(){ + local taxrate="$(DB3 get taxrate || printf 19)" + local rate="$(DB3 get rate || printf 0)" + local n=0 time seq=1 total=0 + + [ $n -lt "$(DB3 count tb_time )" ] && n="$(DB3 count tb_time)" + + while [ $seq -le $n ]; do + time="$(DB3 get tb_time $seq |grep -m1 -xEe '-?[0-9]+(.00?|.25|.50?|.75)?' || printf 0)" + + total=$(awk "BEGIN { printf \"%.2f\", ${total} + ${time} * ${rate}; }") + seq=$((seq + 1)) + done + + case $(DB3 get taxtype) in + net) awk " + BEGIN { printf \"%.2f %.2f %.2f\", + $total, int($total * $taxrate + .5) / 100, + $total + int($total * $taxrate + .5) / 100 + }" + ;; + gross) awk " + BEGIN { printf \"%.2f %.2f %.2f\", + $total - int($total / (100 + $taxrate) * $taxrate * 100 + .5) / 100, + int($total / (100 + $taxrate) * $taxrate * 100 + .5) / 100, $total + }" + ;; + free|*) awk " + BEGIN { printf \"%.2f %.2f %.2f\", + $total, 0, $total + }" + ;; + esac +} + +DB3 get taxtype |grep -qxE 'free|net|gross' || DB3 set taxtype gross +DB3 get taxrate |grep -qxE '[-+]?[0-9]+(\.[0-9]+)?' || DB3 set taxrate 19 +isdate "$(DB3 get date)" >/dev/null || DB3 set date "$(date +%d.%m.%Y)" + +if ! DB3 get invnum |grep -qxE '.+'; then + invnum="$(date +%s) " + DB3 set invnum "$invnum" +fi + +DB3 get rate |grep -m1 -qxEe '-?(\.[0-9]+|[0-9]+\.?[0-9]*)' || DB3 set rate 0 +rate="$(printf '%.2f' "$(DB3 get rate)")" + +read -r net tax gross x <<-EOF +$(sumtotal) +EOF + +yield_form(){ + printf '%s\r\n' "Content-Type: text/html; charset=utf-8" "" + + "$_EXEC/cgilite/html-sh.sed" <<-EOF +[html [head + [meta name="viewport" content="width=device-width"] + [link rel="stylesheet" type="text/css" href="$_BASE/cgilite/common.css"] + [link rel="stylesheet" type="text/css" href="$_BASE/tmpl_byhour/form.css"] + [title $(_ Invoices)] +] [body + [datalist #senders + $(grep -hE '^sender ' "$_DATA"/*.kvd \ + | sort -u \ + | while read junk sender; do + printf '[option . %s]' "$(UNSTRING $sender |HTML)" + done) + ] + [datalist #clients + $(grep -hE '^rcpt ' "$_DATA"/*.kvd \ + | sort -u \ + | while read junk rcpt; do + printf '[option . %s]' "$(UNSTRING $rcpt |HTML)" + done) + ] + [datalist #taxfreetext + [option . Der Rechnungsbetrag ist nach §4 Abs. 21 UStG. umsatzsteuerfrei] + [option . Der Rechnungsbetrag ist nach §4 Abs. 25 UStG. umsatzsteuerfrei] + [option . Gemäß Kleinunternehmerregelung (§19 UStG.) wird keine Umsatzsteuer berechnet.] + ] + [form method="POST" + [label for=sender Absender:] + [input #sender list=senders name=sender value="$(DB3 get sender |HTML)" placeholder="Name; Straße; PLZ Ort"] + + [label for=rcpt Empfänger:] + [input #rcpt list=clients name=rcpt value="$(DB3 get rcpt |HTML)" placeholder="Name; Straße; PLZ Ort"] + + [label for=invnum Rechnungsnummer:] + [input #invnum name=invnum value="$(DB3 get invnum |HTML)"] + + [label for=date Datum:] + [input #date name=date value="$(DB3 get date)" placeholder="TT.MM.YYYY"] + + $([ "$(grep -xF "invnum $(DB3 get invnum |STRING)" "$_DATA"/*.kvd |wc -l)" -gt 1 ] \ + && printf '[span .warning Warnung: Rechnungsnummer doppelt vergeben]' + ) + + [label for=rate Stundensatz:] + [input type=number id=rate name=rate value="$rate" step=".01"] + + [table + [tr [th Datum] [th Leistung] [th Stunden] [th Betrag] ] + $(n=0; seq=0; + [ $n -lt "$(DB3 count tb_date)" ] && n="$(DB3 count tb_date)" + [ $n -lt "$(DB3 count tb_desc)" ] && n="$(DB3 count tb_desc)" + [ $n -lt "$(DB3 count tb_time)" ] && n="$(DB3 count tb_time)" + + while [ $seq -le $n ]; do + seq=$((seq + 1)) + date="$(DB3 get tb_date $seq |HTML)" + desc="$(DB3 get tb_desc $seq |HTML)" + time="$(DB3 get tb_time $seq |grep -m1 -xEe '-?[0-9]+(.00?|.25|.50?|.75)?' || printf 0)" + time="$(printf %.2f "$time")" + time="${time%0}" time="${time%.0}" + [ "$date" = "" -a "$desc" = "" -a "$time" = 0 ] && continue + + printf '[tr + [td [textarea name=tb_date . %s]] + [td [textarea name=tb_desc . %s]] + [td [input type=number name=tb_time value="%s" step=.25] ] + [td %s] + ]' "$date" "$desc" "${time}"\ + "$(awk "BEGIN { printf \"%.2f €\", ${time} * ${rate}; }")" + done + ) + [tr + [td [textarea name=tb_date]] + [td [textarea name=tb_desc]] + [td [input type=number name=tb_time value="0" step=.25] ] + [td] + ] + $(case $(DB3 get taxtype) in + (net) printf '[tr [th colspan=3 . Summe:][td . %7.2f €]] + [tr [th colspan=3 . + USt.:][td . %7.2f €]] + [tr [th colspan=3 . Gesamt:][td . %7.2f €]] + ' $net $tax $gross ;; + (gross) printf '[tr [th colspan=3 . Gesamt:][td . %7.2f €]] + [tr [th colspan=3 . incl. Netto:][td . %7.2f €]] + [tr [th colspan=3 . + USt.:][td . %7.2f €]] + ' $gross $net $tax ;; + (free|*) printf '[tr [th colspan=3 . Gesamt:][td . %7.2f €]]' "$net" + ;; + esac) + ] + + [radio "taxtype" "free" #taxfree $([ "$(DB3 get taxtype)" = free ] && printf checked)] + [label for=taxfree Umsatzsteuerfrei] + [radio "taxtype" "net" #taxnet $([ "$(DB3 get taxtype)" = net ] && printf checked)] + [label for=taxnet Netto] + [radio "taxtype" "gross" #taxgross $([ "$(DB3 get taxtype)" = gross ] && printf checked)] + [label for=taxgross Brutto] + [input list="taxfreetext" name="taxfreetext" value="$(DB3 get taxfreetext |HTML)" placeholder="Kommentar zur Umsatzsteuerbefreiung"] + [label for=taxrate USt. %:][input type=number #taxrate name="taxrate" value="$(DB3 get taxrate)"] + + [label for=status . $(_ Status):] + [select #status name=status + [option value=open $([ "$status" = open ] && printf selected=selected) . $(_ Offen)] + [option value=sent $([ "$status" = sent ] && printf selected=selected) . $(_ Versendet)] + [option value=resent $([ "$status" = resent ] && printf selected=selected) . $(_ Erinnert)] + [option value=paid $([ "$status" = paid ] && printf selected=selected) . $(_ Bezahlt)] + [option value=cancelled $([ "$status" = cancelled ] && printf selected=selected) . $(_ Storniert)] + ] + [input type=hidden name=id value="$id"] + [input type=hidden name=tid value="$tid"] + [input type=hidden name=session_key value="$SESSION_KEY"] + [button type=submit name=action value=update_invoice . $(_ Update)] + [button type=submit name=action value=update_invoice_return . ← Übersicht] + ] +] ] +EOF +} diff --git a/tmpl_byhour/print.sh b/tmpl_byhour/print.sh new file mode 100755 index 0000000..bb00246 --- /dev/null +++ b/tmpl_byhour/print.sh @@ -0,0 +1,182 @@ +#!/bin/zsh + +filenamestring="Rechung $(DB3 get sender |sed -E 's/ *;.*$//g') an $(DB3 get rcpt |sed -E 's/ *;.*$//g') $(isdate "$(DB3 get date)")" + +yield_html(){ + local sender rcpt date seq time desc taxtype hourly + + local net tax gross + read -r net tax gross <<-EOF + $(sumtotal) + EOF + taxtype="$(DB3 get taxtype)" + + date="$(isdate "$(DB3 get date)" || date +%F)" + date="$(date -d "$date" +%s)" + hourly="$(DB3 get rate |grep -m1 -xEe '-?(\.[0-9]+|[0-9]+\.?[0-9]*)' || printf 0)" + hourly="$(printf %.2f "$hourly")" + + sender="$(DB3 get sender |sed -E 's/ *; */\r\n/g' |HTML)" + rcpt="$(DB3 get rcpt |sed -E 's/ *; */\r\n/g' |HTML |sed -E ':A; /(.* ){6}/!{ s/$/\ \ /g; bA; }')" + [ ! "$rcpt" ] && rcpt=" " + + freeformtop="$(DB3 get freeformtop |sed -E 's/ *; */\r\n/g' |HTML)" + + cat <<-EOF + + + + + + + + + + + + +

+ ${rcpt}

+ +

+ $(date -d @${date} +%d.%m.%Y)

+ +

+ Rechnung $(DB3 get invnum |HTML)

+ +

+ ${freeformtop}

+ +

+ Stundensatz: ${hourly} €

+ + + + + + + + + + + + + + + + + + + $(n=0; seq=0; + [ $n -lt "$(DB3 count tb_date)" ] && n="$(DB3 count tb_date)" + [ $n -lt "$(DB3 count tb_time)" ] && n="$(DB3 count tb_time)" + [ $n -lt "$(DB3 count tb_desc)" ] && n="$(DB3 count tb_desc)" + + while [ $seq -le $n ]; do + seq=$((seq + 1)) + date="$(DB3 get tb_date $seq |HTML)" + desc="$(DB3 get tb_desc $seq |HTML)" + time="$(DB3 get tb_time $seq |grep -m1 -xEe '-?[0-9]+(.00?|.25|.50?|.75)?' || printf 0)" + time="$(printf %.2f "$time")" + time="${time%0}" time="${time%.0}" + + [ "$date" = "" -a "$desc" = "" -a "$time" = 0 ] && continue + + cat <<-ROW + + + + + + + + ROW + done + ) + $([ $taxtype != free ] && cat <<-ROW + + + + + + + + ROW + ) + + + + + + + + +

+ Datum

+

+

+

+ Leistung

+

+ Stunden

+

+ Betrag

+

+ ${date}

+

+

+

+ $(HTML "$desc")

+

+ + $(num "$time")

+

+ + $(awk "BEGIN { printf \"%.2f €\", ${time} * ${hourly}; }" |num)

+

+

+

+

+

+ + $([ $taxtype = gross ] && printf "incl."; [ $taxtype = net ] && printf "zzgl."; ) + $(DB3 get taxrate)% MwSt.:

+

+ + $([ $taxtype = gross ] && printf '%.2f €' $tax |num) +

+

+ + $([ $taxtype = net ] && printf '%.2f €' $tax |num) +

+

+

+

+

+

+ Gesamtsumme:

+

+

+

+ + $(printf '%.2f €' $gross |num)

+
+ + $([ $taxtype = free ] && cat <<-EOF +

+ $(DB3 get taxfreetext |HTML)

+ EOF + ) + +

+ $(DB3 get freeformbottom |HTML)

+ + + + EOF +}