Work
This commit is contained in:
+71
-35
@@ -3,16 +3,32 @@ package invoicetxt
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"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:
|
// An invoice.txt entry looks like:
|
||||||
// <invoiceId> <hours> <rate> [retainerHours] [retainerRate] [retainerRollover] <contexts> <projects> <tags>
|
// <invoiceId> <date-sent> <hours> <rate> [retainerHours] [retainerRate] [retainerRollover] <contexts> <projects> <tags>
|
||||||
|
// And once it's paid, it looks like:
|
||||||
|
// x <invoiceId> <date-sent> <date-paid> <hours> <rate> [retainerHours] [retainerRate] [retainerRollover] <contexts> <projects> <tags>
|
||||||
type Invoice struct {
|
type Invoice struct {
|
||||||
ID int `json:"id"` // Invoice id
|
|
||||||
Original string `json:"original"` // original raw invoice text
|
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"`
|
Hours float64 `json:"hours"`
|
||||||
Rate float64 `json:"rate"`
|
Rate float64 `json:"rate"`
|
||||||
RetainerHours float64 `json:"retainerHours,omitempty"`
|
RetainerHours float64 `json:"retainerHours,omitempty"`
|
||||||
@@ -31,48 +47,56 @@ func ParseInvoice(text string) (*Invoice, error) {
|
|||||||
}
|
}
|
||||||
invoice.Original = strings.Trim(text, "\t\n\r")
|
invoice.Original = strings.Trim(text, "\t\n\r")
|
||||||
parts := getParts(invoice.Original)
|
parts := getParts(invoice.Original)
|
||||||
id, err := strconv.Atoi(parts[0])
|
if parts[0] == "x" {
|
||||||
|
invoice.Paid = true
|
||||||
|
parts = parts[1:]
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
parts = parts[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
hr, err := strconv.ParseFloat(parts[0], 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error parsing invoice id: %w", err)
|
return nil, fmt.Errorf("error parsing hours (%s): %w", parts[0], err)
|
||||||
}
|
}
|
||||||
invoice.ID = id
|
invoice.Hours, parts = hr, parts[1:]
|
||||||
hr, err := strconv.ParseFloat(parts[1], 64)
|
|
||||||
|
rt, err := strconv.ParseFloat(parts[0], 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error parsing hours: %w", err)
|
return nil, fmt.Errorf("error parsing rate (%s): %w", parts[0], err)
|
||||||
}
|
}
|
||||||
invoice.Hours = hr
|
invoice.Rate, parts = rt, parts[1:]
|
||||||
rt, err := strconv.ParseFloat(parts[2], 64)
|
|
||||||
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error parsing rate: %w", err)
|
return nil, fmt.Errorf("looks like we should have retainer info, but error parsing (%s): %w", parts[0], err)
|
||||||
}
|
}
|
||||||
invoice.Rate = rt
|
invoice.RetainerHours, parts = retHr, parts[1:]
|
||||||
lpI := 3
|
|
||||||
var hitS bool
|
retRt, err := strconv.ParseFloat(parts[0], 64)
|
||||||
for i := 3; i < len(parts); i++ {
|
|
||||||
if partIsSpecial(parts[i]) {
|
|
||||||
hitS = true
|
|
||||||
}
|
|
||||||
if !hitS {
|
|
||||||
wrk, err := strconv.ParseFloat(parts[i], 64)
|
|
||||||
switch i {
|
|
||||||
case 3: // retainer hours
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error parsing retainer hours: %w", err)
|
return nil, fmt.Errorf("error parsing retainer rate (%s): %w", parts[0], err)
|
||||||
}
|
}
|
||||||
invoice.RetainerHours = wrk
|
invoice.RetainerRate, parts = retRt, parts[1:]
|
||||||
case 4: // retainer rate
|
|
||||||
|
retRoll, err := strconv.ParseFloat(parts[0], 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error parsing retainer rate: %w", err)
|
return nil, fmt.Errorf("error parsing retainer rollover (%s): %w", parts[0], err)
|
||||||
}
|
}
|
||||||
invoice.RetainerRate = wrk
|
invoice.RetainerRollover, parts = retRoll, parts[1:]
|
||||||
case 5: // retainer rollover
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Error parsing retainer rollover: %w", err)
|
|
||||||
}
|
|
||||||
invoice.RetainerRollover = wrk
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(parts); i++ {
|
||||||
// We're in the 'special' parts
|
// We're in the 'special' parts
|
||||||
switch parts[i][0] {
|
switch parts[i][0] {
|
||||||
case '@': // context
|
case '@': // context
|
||||||
@@ -96,7 +120,7 @@ func partIsSpecial(pt string) bool {
|
|||||||
if len(pt) == 0 {
|
if len(pt) == 0 {
|
||||||
return false
|
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.
|
// 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)
|
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 {
|
func (invoice *Invoice) HasContext(context string) bool {
|
||||||
for _, v := range invoice.Contexts {
|
for _, v := range invoice.Contexts {
|
||||||
if v == context {
|
if v == context {
|
||||||
@@ -245,7 +281,7 @@ func (invoice *Invoice) GetTagsString() string {
|
|||||||
}
|
}
|
||||||
sort.Strings(keys)
|
sort.Strings(keys)
|
||||||
for _, key := range 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
|
return text
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-13
@@ -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
|
// 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.
|
// bool), and returns a new InvoiceList. The original InvoiceList is not modified.
|
||||||
func (invoicelist *InvoiceList) Filter(predicate func(*Invoice) bool) *InvoiceList {
|
func (invoicelist *InvoiceList) Filter(predicate func(*Invoice) bool) *InvoiceList {
|
||||||
var newList ProjectList
|
var newList InvoiceList
|
||||||
for _, t := range invoicelist.Invoices {
|
for _, t := range invoicelist.Invoices {
|
||||||
if predicate(t) {
|
if predicate(t) {
|
||||||
newList.AddInvoice(t)
|
newList.AddInvoice(t)
|
||||||
@@ -41,25 +41,27 @@ func (invoicelist *InvoiceList) Filter(predicate func(*Invoice) bool) *InvoiceLi
|
|||||||
return &newList
|
return &newList
|
||||||
}
|
}
|
||||||
|
|
||||||
func (invoicelist *InvoiceList) GetNextId() int {
|
// ForEach runs the given function passing it each invoice in the list
|
||||||
nextId := 0
|
func (invoicelist *InvoiceList) ForEach(run func(*Invoice)) {
|
||||||
for _, i := range invoicelist.Invoices {
|
for _, t := range invoicelist.Invoices {
|
||||||
if i.ID > nextId {
|
run(t)
|
||||||
nextId = i.ID
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return nextId + 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddInvoice prepe
|
// AddInvoice prepepends an Invoice to the current InvoiceList
|
||||||
func (invoicelist *InvoiceList) AddInvoice(invoice *Invoice) {
|
func (invoicelist *InvoiceList) AddInvoice(invoice *Invoice) {
|
||||||
invoicelist.Invoices = append(invoicelist.Invoices, 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) }
|
func (invoicelist *InvoiceList) Combine(other *InvoiceList) { invoicelist.AddInvoices(other.Invoices) }
|
||||||
|
|
||||||
// GetInvoice returns the Invoice with the given id from the invoice list
|
// GetInvoice returns the Invoice with the given id from the invoice list
|
||||||
// Returns an error if the invoice could not be found
|
// 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 {
|
for i := range invoicelist.Invoices {
|
||||||
if invoicelist.Invoices[i].ID == id {
|
if invoicelist.Invoices[i].ID == id {
|
||||||
return invoicelist.Invoices[i], nil
|
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.
|
// RemoveInvoice removes any Invoice with given Invoice id from the InvoiceList.
|
||||||
// Returns an error if no invoice was removed.
|
// 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 found bool
|
||||||
var remIdx int
|
var remIdx int
|
||||||
var i *Invoice
|
var i *Invoice
|
||||||
@@ -94,7 +96,7 @@ func (invoicelist *InvoiceList) ArchiveInvoiceToFile(invoice Invoice, filename s
|
|||||||
if err := invoicelist.RemoveInvoice(invoice.ID); err != nil {
|
if err := invoicelist.RemoveInvoice(invoice.ID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
f, err := os.Open(filename, os.O_APPEND|os.O_WRONLY, 0600)
|
f, err := os.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -139,7 +141,7 @@ func (invoicelist *InvoiceList) WriteToFile(file *os.File) error {
|
|||||||
|
|
||||||
// LoadFromFilename loads an InvoiceList from the filename
|
// LoadFromFilename loads an InvoiceList from the filename
|
||||||
// (it piggybacks on LoadFromFile)
|
// (it piggybacks on LoadFromFile)
|
||||||
func (invoicelist *InvoiceList) LoadFromFilename(filename strinng) error {
|
func (invoicelist *InvoiceList) LoadFromFilename(filename string) error {
|
||||||
file, err := os.Open(filename)
|
file, err := os.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
Reference in New Issue
Block a user