268 lines
7.2 KiB
Go
268 lines
7.2 KiB
Go
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 = ¢s
|
||
}
|
||
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
|
||
}
|