From 763616df26e627aa0b72d5f877efec9ac8140c90 Mon Sep 17 00:00:00 2001 From: Brian Buller Date: Thu, 10 Sep 2026 06:37:23 -0500 Subject: [PATCH] Work --- invoice.go | 120 +++++++++++++++++++++++++++++++----------------- invoice_list.go | 28 +++++------ 2 files changed, 93 insertions(+), 55 deletions(-) diff --git a/invoice.go b/invoice.go index 2617331..1239657 100644 --- a/invoice.go +++ b/invoice.go @@ -3,16 +3,32 @@ package invoicetxt import ( "fmt" + "regexp" "sort" "strconv" "strings" + "time" +) + +var ( + // DateLayout is used for formatting time.Time into invoice.txt date format and vice-versa. + DateLayout = time.DateOnly + + addonTagRx = regexp.MustCompile(`(^|\s+)([\w-]+):(\S+)`) // Match additional tags date: '... due:2012-12-12 ...' + contextRx = regexp.MustCompile(`(^|\s+)@(\S+)`) // Match contexts: '@Context ...' or '... @Context ...' + projectRx = regexp.MustCompile(`(^|\s+)\+(\S+)`) // Match projects: '+Project...' or '... +Project ...') ) // An invoice.txt entry looks like: -// [retainerHours] [retainerRate] [retainerRollover] +// [retainerHours] [retainerRate] [retainerRollover] +// And once it's paid, it looks like: +// x [retainerHours] [retainerRate] [retainerRollover] type Invoice struct { - ID int `json:"id"` // Invoice id Original string `json:"original"` // original raw invoice text + Paid bool `json:"paid"` + ID string `json:"id"` // Invoice id + DateSent time.Time `json:"sentDate"` + DatePaid time.Time `json:"paidDate"` Hours float64 `json:"hours"` Rate float64 `json:"rate"` RetainerHours float64 `json:"retainerHours,omitempty"` @@ -31,48 +47,56 @@ func ParseInvoice(text string) (*Invoice, error) { } invoice.Original = strings.Trim(text, "\t\n\r") parts := getParts(invoice.Original) - id, err := strconv.Atoi(parts[0]) - if err != nil { - return nil, fmt.Errorf("Error parsing invoice id: %w", err) + if parts[0] == "x" { + invoice.Paid = true + parts = parts[1:] } - invoice.ID = id - hr, err := strconv.ParseFloat(parts[1], 64) - if err != nil { - return nil, fmt.Errorf("Error parsing hours: %w", err) + invoice.ID, parts = parts[0], parts[1:] + if invoice.DateSent, err = time.Parse(DateLayout, parts[0]); err != nil { + return nil, fmt.Errorf("unable to parse datesent: %w", err) } - invoice.Hours = hr - rt, err := strconv.ParseFloat(parts[2], 64) - if err != nil { - return nil, fmt.Errorf("Error parsing rate: %w", err) - } - invoice.Rate = rt - lpI := 3 - var hitS bool - for i := 3; i < len(parts); i++ { - if partIsSpecial(parts[i]) { - hitS = true + parts = parts[1:] + if invoice.Paid { + if invoice.DatePaid, err = time.Parse(DateLayout, parts[0]); err != nil { + return nil, fmt.Errorf("invoice marked paid, but unable to parse datepaid: %w", err) } - if !hitS { - wrk, err := strconv.ParseFloat(parts[i], 64) - switch i { - case 3: // retainer hours - if err != nil { - return nil, fmt.Errorf("Error parsing retainer hours: %w", err) - } - invoice.RetainerHours = wrk - case 4: // retainer rate - if err != nil { - return nil, fmt.Errorf("Error parsing retainer rate: %w", err) - } - invoice.RetainerRate = wrk - case 5: // retainer rollover - if err != nil { - return nil, fmt.Errorf("Error parsing retainer rollover: %w", err) - } - invoice.RetainerRollover = wrk - } - continue + parts = parts[1:] + } + + hr, err := strconv.ParseFloat(parts[0], 64) + if err != nil { + return nil, fmt.Errorf("error parsing hours (%s): %w", parts[0], err) + } + invoice.Hours, parts = hr, parts[1:] + + rt, err := strconv.ParseFloat(parts[0], 64) + if err != nil { + return nil, fmt.Errorf("error parsing rate (%s): %w", parts[0], err) + } + invoice.Rate, parts = rt, parts[1:] + + if !partIsSpecial(parts[0]) { + // If we're not in the 'specials' yet, we must have retainer info + retHr, err := strconv.ParseFloat(parts[0], 64) + if err != nil { + return nil, fmt.Errorf("looks like we should have retainer info, but error parsing (%s): %w", parts[0], err) } + invoice.RetainerHours, parts = retHr, parts[1:] + + retRt, err := strconv.ParseFloat(parts[0], 64) + if err != nil { + return nil, fmt.Errorf("error parsing retainer rate (%s): %w", parts[0], err) + } + invoice.RetainerRate, parts = retRt, parts[1:] + + retRoll, err := strconv.ParseFloat(parts[0], 64) + if err != nil { + return nil, fmt.Errorf("error parsing retainer rollover (%s): %w", parts[0], err) + } + invoice.RetainerRollover, parts = retRoll, parts[1:] + } + + for i := 0; i < len(parts); i++ { // We're in the 'special' parts switch parts[i][0] { case '@': // context @@ -96,7 +120,7 @@ func partIsSpecial(pt string) bool { if len(pt) == 0 { return false } - return pt[0] == '#' || pt[0] == '@' || strings.Contains(pts, ":") + return pt[0] == '#' || pt[0] == '@' || strings.Contains(pt, ":") } // getParts parses the text from 'text' pulling out each part. @@ -188,6 +212,18 @@ func (invoice Invoice) String() string { return fmt.Sprintf("%s %s", text, sTxt) } +func (invoice *Invoice) HasRetainer() bool { + return invoice.RetainerRate > 0 +} + +func (invoice *Invoice) GetAmount() float64 { + return (invoice.GetOverageHours() * invoice.Rate) + invoice.RetainerRate +} + +func (invoice *Invoice) GetOverageHours() float64 { + return invoice.Hours - invoice.RetainerHours - invoice.RetainerRollover +} + func (invoice *Invoice) HasContext(context string) bool { for _, v := range invoice.Contexts { if v == context { @@ -245,7 +281,7 @@ func (invoice *Invoice) GetTagsString() string { } sort.Strings(keys) for _, key := range keys { - text = fmt.Sprintf("%s %s:%s", text, key, invoice.AdditionnalTags[key]) + text = fmt.Sprintf("%s %s:%s", text, key, invoice.AdditionalTags[key]) } return text } diff --git a/invoice_list.go b/invoice_list.go index b7e7ef2..4a6933a 100644 --- a/invoice_list.go +++ b/invoice_list.go @@ -32,7 +32,7 @@ func (invoicelist *InvoiceList) GetInvoicesWithProject(project string) *InvoiceL // Filter filters the current InvoiceList for the given predicate (a function that takes an invoice as input and returns a // bool), and returns a new InvoiceList. The original InvoiceList is not modified. func (invoicelist *InvoiceList) Filter(predicate func(*Invoice) bool) *InvoiceList { - var newList ProjectList + var newList InvoiceList for _, t := range invoicelist.Invoices { if predicate(t) { newList.AddInvoice(t) @@ -41,25 +41,27 @@ func (invoicelist *InvoiceList) Filter(predicate func(*Invoice) bool) *InvoiceLi return &newList } -func (invoicelist *InvoiceList) GetNextId() int { - nextId := 0 - for _, i := range invoicelist.Invoices { - if i.ID > nextId { - nextId = i.ID - } +// ForEach runs the given function passing it each invoice in the list +func (invoicelist *InvoiceList) ForEach(run func(*Invoice)) { + for _, t := range invoicelist.Invoices { + run(t) } - return nextId + 1 } -// AddInvoice prepe +// AddInvoice prepepends an Invoice to the current InvoiceList func (invoicelist *InvoiceList) AddInvoice(invoice *Invoice) { invoicelist.Invoices = append(invoicelist.Invoices, invoice) } +func (invoicelist *InvoiceList) AddInvoices(invoices []*Invoice) { + for _, v := range invoices { + invoicelist.AddInvoice(v) + } +} func (invoicelist *InvoiceList) Combine(other *InvoiceList) { invoicelist.AddInvoices(other.Invoices) } // GetInvoice returns the Invoice with the given id from the invoice list // Returns an error if the invoice could not be found -func (invoicelist *InvoiceList) GetInvoice(id int) (*Invoice, error) { +func (invoicelist *InvoiceList) GetInvoice(id string) (*Invoice, error) { for i := range invoicelist.Invoices { if invoicelist.Invoices[i].ID == id { return invoicelist.Invoices[i], nil @@ -70,7 +72,7 @@ func (invoicelist *InvoiceList) GetInvoice(id int) (*Invoice, error) { // RemoveInvoice removes any Invoice with given Invoice id from the InvoiceList. // Returns an error if no invoice was removed. -func (invoicelist *InvoiceList) RemoveInvoice(id int) error { +func (invoicelist *InvoiceList) RemoveInvoice(id string) error { var found bool var remIdx int var i *Invoice @@ -94,7 +96,7 @@ func (invoicelist *InvoiceList) ArchiveInvoiceToFile(invoice Invoice, filename s if err := invoicelist.RemoveInvoice(invoice.ID); err != nil { return err } - f, err := os.Open(filename, os.O_APPEND|os.O_WRONLY, 0600) + f, err := os.Open(filename) if err != nil { return err } @@ -139,7 +141,7 @@ func (invoicelist *InvoiceList) WriteToFile(file *os.File) error { // LoadFromFilename loads an InvoiceList from the filename // (it piggybacks on LoadFromFile) -func (invoicelist *InvoiceList) LoadFromFilename(filename strinng) error { +func (invoicelist *InvoiceList) LoadFromFilename(filename string) error { file, err := os.Open(filename) if err != nil { return err