Files
sultan/pkg/cli/customers.go
T

268 lines
7.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}