refactoring

This commit is contained in:
Hayden 2023-02-24 18:24:06 -09:00
parent 4fbd2c7542
commit 79566a7346
No known key found for this signature in database
GPG key ID: 17CF79474E257545
11 changed files with 252 additions and 61 deletions

View file

@ -19,7 +19,7 @@ func (ctrl *V1Controller) HandleBillOfMaterialsExport() server.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) error {
actor := services.UseUserCtx(r.Context())
csv, err := ctrl.svc.Reporting.BillOfMaterialsTSV(r.Context(), actor.GroupID)
csv, err := ctrl.svc.Items.ExportBillOfMaterialsTSV(r.Context(), actor.GroupID)
if err != nil {
return err
}

View file

@ -1,16 +1,13 @@
package services
import (
"github.com/hay-kot/homebox/backend/internal/core/services/reporting"
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/rs/zerolog/log"
)
type AllServices struct {
User *UserService
Group *GroupService
Items *ItemService
Reporting *reporting.ReportingService
User *UserService
Group *GroupService
Items *ItemService
}
type OptionsFunc func(*options)
@ -45,7 +42,5 @@ func New(repos *repo.AllRepos, opts ...OptionsFunc) *AllServices {
repo: repos,
autoIncrementAssetID: options.autoIncrementAssetID,
},
// TODO: don't use global logger
Reporting: reporting.NewReportingService(repos, &log.Logger),
}
}

View file

@ -1,10 +1,8 @@
package reporting
import (
"context"
"github.com/gocarina/gocsv"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/data/types"
)
@ -24,13 +22,7 @@ type BillOfMaterialsEntry struct {
// BillOfMaterialsTSV returns a byte slice of the Bill of Materials for a given GID in TSV format
// See BillOfMaterialsEntry for the format of the output
func (rs *ReportingService) BillOfMaterialsTSV(ctx context.Context, GID uuid.UUID) ([]byte, error) {
entities, err := rs.repos.Items.GetAll(ctx, GID)
if err != nil {
rs.l.Debug().Err(err).Msg("failed to get all items for BOM Csv Reporting")
return nil, err
}
func BillOfMaterialsTSV(entities []repo.ItemOut) ([]byte, error) {
bomEntries := make([]BillOfMaterialsEntry, len(entities))
for i, entity := range entities {
bomEntries[i] = BillOfMaterialsEntry{

View file

@ -17,6 +17,7 @@ type ExportTSVRow struct {
LabelStr LabelString `csv:"HB.labels"`
ImportRef string `csv:"HB.import_ref"`
AssetID repo.AssetID `csv:"HB.asset_id"`
Archived bool `csv:"HB.archived"`
Name string `csv:"HB.name"`
Quantity int `csv:"HB.quantity"`

View file

@ -10,6 +10,7 @@ import (
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/hay-kot/homebox/backend/internal/data/types"
"github.com/rs/zerolog/log"
)
// IOSheet is the representation of a CSV/TSV sheet that is used for importing/exporting
@ -115,7 +116,11 @@ func (s *IOSheet) Read(data io.Reader) error {
v = parseLabelString(val)
}
fmt.Printf("%s: %v (%T)\n", tag, v, v)
log.Debug().
Str("tag", tag).
Interface("val", v).
Str("type", fmt.Sprintf("%T", v)).
Msg("parsed value")
// Nil values are not allowed at the moment. This may change.
if v == nil {
@ -185,6 +190,7 @@ func (s *IOSheet) ReadItems(items []repo.ItemOut) {
Quantity: item.Quantity,
Description: item.Description,
Insured: item.Insured,
Archived: item.Archived,
PurchasePrice: item.PurchasePrice,
PurchaseFrom: item.PurchaseFrom,

View file

@ -1,28 +0,0 @@
package reporting
import (
"encoding/csv"
"io"
"github.com/gocarina/gocsv"
"github.com/hay-kot/homebox/backend/internal/data/repo"
"github.com/rs/zerolog"
)
type ReportingService struct {
repos *repo.AllRepos
l *zerolog.Logger
}
func NewReportingService(repos *repo.AllRepos, l *zerolog.Logger) *ReportingService {
gocsv.SetCSVWriter(func(out io.Writer) *gocsv.SafeCSVWriter {
writer := csv.NewWriter(out)
writer.Comma = '\t'
return gocsv.NewSafeCSVWriter(writer)
})
return &ReportingService{
repos: repos,
l: l,
}
}

View file

@ -3,7 +3,9 @@ package services
import (
"context"
"errors"
"fmt"
"io"
"strings"
"github.com/google/uuid"
"github.com/hay-kot/homebox/backend/internal/core/services/reporting"
@ -62,13 +64,234 @@ func (svc *ItemService) EnsureAssetID(ctx context.Context, GID uuid.UUID) (int,
return finished, nil
}
func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data io.Reader) (int, error) {
// loaded, err := reporting.ReadCSV(data)
// if err != nil {
// return 0, err
// }
func serializeLocation[T ~[]string](location T) string {
return strings.Join(location, "/")
}
return 0, nil
// CsvImport imports items from a CSV file. using the standard defined format.
//
// CsvImport applies the following rules/operations
//
// 1. If the item does not exist, it is created.
// 2. If the item has a ImportRef and it exists it is skipped
// 3. Locations and Labels are created if they do not exist.
func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data io.Reader) (int, error) {
sheet := reporting.IOSheet{}
err := sheet.Read(data)
if err != nil {
return 0, err
}
// ========================================
// Labels
labelMap := make(map[string]uuid.UUID)
{
labels, err := svc.repo.Labels.GetAll(ctx, GID)
if err != nil {
return 0, err
}
for _, label := range labels {
labelMap[label.Name] = label.ID
}
}
// ========================================
// Locations
locationMap := make(map[string]uuid.UUID)
{
locations, err := svc.repo.Locations.Tree(ctx, GID, repo.TreeQuery{WithItems: false})
if err != nil {
return 0, err
}
// Traverse the tree and build a map of location full paths to IDs
// where the full path is the location name joined by slashes.
var traverse func(location *repo.TreeItem, path []string)
traverse = func(location *repo.TreeItem, path []string) {
path = append(path, location.Name)
locationMap[serializeLocation(path)] = location.ID
for _, child := range location.Children {
traverse(child, path)
}
}
for _, location := range locations {
traverse(&location, []string{})
}
}
// ========================================
// Import items
// Asset ID Pre-Check
highestAID := repo.AssetID(-1)
if svc.autoIncrementAssetID {
highestAID, err = svc.repo.Items.GetHighestAssetID(ctx, GID)
if err != nil {
return 0, err
}
}
finished := 0
for i := range sheet.Rows {
row := sheet.Rows[i]
// ========================================
// Preflight check for existing item
// TODO: Allow updates to existing items by matching on ImportRef
if row.ImportRef != "" {
exists, err := svc.repo.Items.CheckRef(ctx, GID, row.ImportRef)
if err != nil {
return 0, fmt.Errorf("error checking for existing item with ref %q: %w", row.ImportRef, err)
}
if exists {
continue
}
}
// ========================================
// Pre-Create Labels as necessary
labelIds := make([]uuid.UUID, len(row.LabelStr))
for j := range row.LabelStr {
label := row.LabelStr[j]
id, ok := labelMap[label]
if !ok {
newLabel, err := svc.repo.Labels.Create(ctx, GID, repo.LabelCreate{Name: label})
if err != nil {
return 0, err
}
id = newLabel.ID
}
labelIds[j] = id
labelMap[label] = id
}
// ========================================
// Pre-Create Locations as necessary
path := serializeLocation(row.Location)
locationID, ok := locationMap[path]
if !ok { // Traverse the path of LocationStr and check each path element to see if it exists already, if not create it.
paths := []string{}
for i, pathElement := range row.Location {
paths = append(paths, pathElement)
path := serializeLocation(paths)
locationID, ok = locationMap[path]
if !ok {
parentID := uuid.Nil
// Get the parent ID
if i > 0 {
parentPath := serializeLocation(row.Location[:i])
parentID = locationMap[parentPath]
}
newLocation, err := svc.repo.Locations.Create(ctx, GID, repo.LocationCreate{
ParentID: parentID,
Name: pathElement,
})
if err != nil {
return 0, err
}
locationID = newLocation.ID
}
locationMap[path] = locationID
}
locationID, ok = locationMap[path]
if !ok {
return 0, errors.New("failed to create location")
}
}
var effAID repo.AssetID
if svc.autoIncrementAssetID && row.AssetID.Nil() {
effAID = highestAID + 1
highestAID++
} else {
effAID = row.AssetID
}
// ========================================
// Create Item
newItem := repo.ItemCreate{
ImportRef: row.ImportRef,
Name: row.Name,
Description: row.Description,
AssetID: effAID,
LocationID: locationID,
LabelIDs: labelIds,
}
item, err := svc.repo.Items.Create(ctx, GID, newItem)
if err != nil {
return 0, err
}
fields := make([]repo.ItemField, len(row.Fields))
for i := range row.Fields {
fields[i] = repo.ItemField{
Name: row.Fields[i].Name,
Type: "text",
TextValue: row.Fields[i].Value,
}
}
updateItem := repo.ItemUpdate{
ID: item.ID,
LabelIDs: labelIds,
LocationID: locationID,
Name: row.Name,
Description: row.Description,
AssetID: effAID,
Insured: row.Insured,
Quantity: row.Quantity,
Archived: row.Archived,
PurchasePrice: row.PurchasePrice,
PurchaseFrom: row.PurchaseFrom,
PurchaseTime: row.PurchaseTime,
Manufacturer: row.Manufacturer,
ModelNumber: row.ModelNumber,
SerialNumber: row.SerialNumber,
LifetimeWarranty: row.LifetimeWarranty,
WarrantyExpires: row.WarrantyExpires,
WarrantyDetails: row.WarrantyDetails,
SoldTo: row.SoldTo,
SoldTime: row.SoldTime,
SoldPrice: row.SoldPrice,
SoldNotes: row.SoldNotes,
Notes: row.Notes,
Fields: fields,
}
item, err = svc.repo.Items.UpdateByGroup(ctx, GID, updateItem)
if err != nil {
return 0, err
}
finished++
}
return finished, nil
}
func (svc *ItemService) ExportTSV(ctx context.Context, GID uuid.UUID) ([][]string, error) {
@ -83,3 +306,12 @@ func (svc *ItemService) ExportTSV(ctx context.Context, GID uuid.UUID) ([][]strin
return sheet.TSV()
}
func (svc *ItemService) ExportBillOfMaterialsTSV(ctx context.Context, GID uuid.UUID) ([]byte, error) {
items, err := svc.repo.Items.GetAll(ctx, GID)
if err != nil {
return nil, err
}
return reporting.BillOfMaterialsTSV(items)
}

View file

@ -16,9 +16,7 @@ func getPrevMonth(now time.Time) time.Time {
// avoid infinite loop
max := 15
for t.Month() == now.Month() {
println("month is the same")
t = t.AddDate(0, 0, -1)
println(t.String())
max--
if max == 0 {

View file

@ -2,7 +2,6 @@ package repo
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
@ -81,7 +80,6 @@ func TestUserRepo_GetAll(t *testing.T) {
assert.Equal(t, len(created), len(allUsers))
for _, usr := range created {
fmt.Printf("%+v\n", usr)
for _, usr2 := range allUsers {
if usr.ID == usr2.ID {
assert.Equal(t, usr.Email, usr2.Email)

View file

@ -2,7 +2,6 @@ package types
import (
"errors"
"fmt"
"strings"
"time"
)
@ -74,9 +73,7 @@ func (d Date) MarshalJSON() ([]byte, error) {
func (d *Date) UnmarshalJSON(data []byte) (err error) {
// unescape the string if necessary `\"` -> `"`
str := strings.Trim(string(data), "\"")
fmt.Printf("str: %q\n", str)
if str == "" || str == "null" || str == `""` {
println("empty date")
*d = Date{}
return nil
}