Accountant: Inventar, where, Checkmk, Abrechnung

This commit is contained in:
br committed 2026-10-18 17:08:58 +02:00
1 parent 9f16148cde
commit dd93277750
59 files changed
+7522 -113

No files matched your search

+267
View File
@@ -0,0 +1,267 @@
package cli
import (
"errors"
"flag"
"fmt"
"slices"
"strings"
"text/tabwriter"
"sultan/pkg/accountant"
"sultan/pkg/ledger"
)
// parseCycle versteht m/q/j bzw. monthly/quarterly/yearly (und deutsche
// Varianten); "offen" bzw. "none" = noch nicht eingeordnet.
func parseCycle(s string) (string, error) {
switch strings.ToLower(strings.TrimSpace(s)) {
case "m", "monthly", "monatlich":
return ledger.CycleMonthly, nil
case "q", "quarterly", "quartal", "quartalsweise":
return ledger.CycleQuarterly, nil
case "j", "y", "yearly", "jährlich", "jaehrlich":
return ledger.CycleYearly, nil
case "offen", "none", "-":
return ledger.CycleUnclassified, nil
}
return "", fmt.Errorf("Zahlweise %q unbekannt (m, q, j oder offen)", s)
}
func cycleLabel(c string) string {
switch c {
case ledger.CycleMonthly:
return "monatlich"
case ledger.CycleQuarterly:
return "quartalsweise"
case ledger.CycleYearly:
return "jährlich"
}
return "nicht eingeordnet"
}
// customerFields sind die Felder, die add und set gemeinsam haben.
type customerFields struct {
name, group, note, cycle, flat *string
noFlat *bool
}
func addCustomerFlags(fs *flag.FlagSet) customerFields {
return customerFields{
name: fs.String("name", "", "Name"),
group: fs.String("group", "", "Kostenverursacher-Gruppe zum Clustern (z. B. extern, intern, familie)"),
note: fs.String("note", "", "Notiz, z. B. persönliche Nebenabreden"),
cycle: fs.String("cycle", "", "Zahlweise: m (monatlich), q (quartalsweise), j (jährlich), offen"),
flat: fs.String("flat-rate", "", "Pauschale je Monat in Euro, z. B. 89,00"),
noFlat: fs.Bool("no-flat-rate", false, "keine Pauschale"),
}
}
// apply übernimmt die auf der Befehlszeile gesetzten Felder in c.
func (f customerFields) apply(fs *flag.FlagSet, c *ledger.Customer) error {
set := map[string]bool{}
fs.Visit(func(fl *flag.Flag) { set[fl.Name] = true })
if set["flat-rate"] && *f.noFlat {
return usageError{errors.New("--flat-rate und --no-flat-rate schließen sich aus")}
}
if set["name"] {
c.Name = *f.name
}
if set["group"] {
c.Group = *f.group
}
if set["note"] {
c.Note = *f.note
}
if set["cycle"] {
cycle, err := parseCycle(*f.cycle)
if err != nil {
return usageError{err}
}
c.Cycle = cycle
}
if set["flat-rate"] {
cents, err := accountant.ParseMoney(*f.flat)
if err != nil {
return usageError{err}
}
c.FlatRate = &cents
}
if *f.noFlat {
c.FlatRate = nil
}
return nil
}
func customerAdd(env tokenEnv, args []string) error {
fs, dir := accountantFlags("customer add", env)
id := fs.String("id", "", "Kunden-ID (a–z, 0–9, - und _), z. B. Kundennummer")
fields := addCustomerFlags(fs)
if err := parse(fs, args, "id", "name"); err != nil {
return err
}
c := ledger.Customer{ID: *id}
if err := fields.apply(fs, &c); err != nil {
return err
}
l, err := openLedger(env, *dir)
if err != nil {
return err
}
defer l.Close()
if err := l.AddCustomer(env.ctx, c); err != nil {
return err
}
fmt.Fprintf(env.stdout, "Kunde %s angelegt\n", c.ID)
return nil
}
func customerSet(env tokenEnv, args []string) error {
fs, dir := accountantFlags("customer set", env)
id := fs.String("id", "", "Kunden-ID")
fields := addCustomerFlags(fs)
if err := parse(fs, args, "id"); err != nil {
return err
}
l, err := openLedger(env, *dir)
if err != nil {
return err
}
defer l.Close()
c, err := l.GetCustomer(env.ctx, *id)
if err != nil {
return err
}
if err := fields.apply(fs, &c); err != nil {
return err
}
if err := l.UpdateCustomer(env.ctx, c); err != nil {
return err
}
fmt.Fprintf(env.stdout, "Kunde %s geändert\n", c.ID)
return nil
}
func customerList(env tokenEnv, args []string) error {
fs, dir := accountantFlags("customer list", env)
group := fs.String("group", "", "nur Kunden dieser Gruppe")
unclassified := fs.Bool("unclassified", false, "nur Kunden ohne Zahlweise (Feedback der Buchhaltung fehlt)")
if err := parse(fs, args); err != nil {
return err
}
l, err := openLedger(env, *dir)
if err != nil {
return err
}
defer l.Close()
customers, err := l.ListCustomers(env.ctx)
if err != nil {
return err
}
maps, err := l.ListMappings(env.ctx)
if err != nil {
return err
}
tenants := map[string][]string{}
for _, c := range customers {
tenants[c.ID] = []string{c.ID} // Mandant mit gleicher ID (sofern nicht anders zugeordnet)
}
for _, m := range maps {
name := m.Tenant
if m.Payee != "" {
name = m.Payee + "/" + m.Tenant
}
if !slices.Contains(tenants[m.Customer], name) {
tenants[m.Customer] = append(tenants[m.Customer], name)
}
}
tw := tabwriter.NewWriter(env.stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(tw, "ID\tNAME\tGROUP\tCYCLE\tFLAT_RATE\tTENANTS\tNOTE")
for _, c := range customers {
if (*group != "" && c.Group != *group) || (*unclassified && c.Cycle != ledger.CycleUnclassified) {
continue
}
flat := "-"
if c.FlatRate != nil {
flat = accountant.FormatMoney(*c.FlatRate)
}
name := c.Name
if c.Auto {
name += " (automatisch)"
}
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", c.ID, name, cmpOrDash(c.Group), cycleLabel(c.Cycle), flat,
strings.Join(tenants[c.ID], ","), c.Note)
}
return tw.Flush()
}
func cmpOrDash(s string) string {
if s == "" {
return "-"
}
return s
}
func customerDelete(env tokenEnv, args []string) error {
fs, dir := accountantFlags("customer delete", env)
id := fs.String("id", "", "Kunden-ID")
if err := parse(fs, args, "id"); err != nil {
return err
}
l, err := openLedger(env, *dir)
if err != nil {
return err
}
defer l.Close()
if err := l.DeleteCustomer(env.ctx, *id); err != nil {
return err
}
fmt.Fprintf(env.stdout, "Kunde %s samt Zuordnungen gelöscht\n", *id)
return nil
}
func customerMap(env tokenEnv, args []string) error {
fs, dir := accountantFlags("customer map", env)
tenant := fs.String("tenant", "", "Mandanten-ID, wie sie der Sultan-Server meldet")
payee := fs.String("payee", "", "nur auf diesem Payee (Default: auf allen)")
customer := fs.String("customer", "", "Kunden-ID")
if err := parse(fs, args, "tenant", "customer"); err != nil {
return err
}
l, err := openLedger(env, *dir)
if err != nil {
return err
}
defer l.Close()
if err := l.MapTenant(env.ctx, *payee, *tenant, *customer); err != nil {
return err
}
where := "allen Payees"
if *payee != "" {
where = "Payee " + *payee
}
fmt.Fprintf(env.stdout, "Mandant %s auf %s → Kunde %s\n", *tenant, where, *customer)
if c, err := l.GetCustomer(env.ctx, *tenant); err == nil && c.Auto && c.ID != *customer {
fmt.Fprintf(env.stderr, "Hinweis: Kunde %s wurde automatisch angelegt und ist ggf. überflüssig: sultan accountant customer delete --id=%s\n", c.ID, c.ID)
}
return nil
}
func customerUnmap(env tokenEnv, args []string) error {
fs, dir := accountantFlags("customer unmap", env)
tenant := fs.String("tenant", "", "Mandanten-ID")
payee := fs.String("payee", "", "Payee der Zuordnung (leer = Zuordnung für alle Payees)")
if err := parse(fs, args, "tenant"); err != nil {
return err
}
l, err := openLedger(env, *dir)
if err != nil {
return err
}
defer l.Close()
if err := l.UnmapTenant(env.ctx, *payee, *tenant); err != nil {
return err
}
fmt.Fprintf(env.stdout, "Zuordnung von %s entfernt\n", *tenant)
return nil
}