mirror of
https://github.com/hay-kot/homebox.git
synced 2025-08-06 01:20:31 +00:00
refactoring
This commit is contained in:
parent
4fbd2c7542
commit
79566a7346
11 changed files with 252 additions and 61 deletions
|
@ -19,7 +19,7 @@ func (ctrl *V1Controller) HandleBillOfMaterialsExport() server.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) error {
|
return func(w http.ResponseWriter, r *http.Request) error {
|
||||||
actor := services.UseUserCtx(r.Context())
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/hay-kot/homebox/backend/internal/core/services/reporting"
|
|
||||||
"github.com/hay-kot/homebox/backend/internal/data/repo"
|
"github.com/hay-kot/homebox/backend/internal/data/repo"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AllServices struct {
|
type AllServices struct {
|
||||||
User *UserService
|
User *UserService
|
||||||
Group *GroupService
|
Group *GroupService
|
||||||
Items *ItemService
|
Items *ItemService
|
||||||
Reporting *reporting.ReportingService
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type OptionsFunc func(*options)
|
type OptionsFunc func(*options)
|
||||||
|
@ -45,7 +42,5 @@ func New(repos *repo.AllRepos, opts ...OptionsFunc) *AllServices {
|
||||||
repo: repos,
|
repo: repos,
|
||||||
autoIncrementAssetID: options.autoIncrementAssetID,
|
autoIncrementAssetID: options.autoIncrementAssetID,
|
||||||
},
|
},
|
||||||
// TODO: don't use global logger
|
|
||||||
Reporting: reporting.NewReportingService(repos, &log.Logger),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
package reporting
|
package reporting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/gocarina/gocsv"
|
"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"
|
"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
|
// 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
|
// See BillOfMaterialsEntry for the format of the output
|
||||||
func (rs *ReportingService) BillOfMaterialsTSV(ctx context.Context, GID uuid.UUID) ([]byte, error) {
|
func BillOfMaterialsTSV(entities []repo.ItemOut) ([]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
|
|
||||||
}
|
|
||||||
|
|
||||||
bomEntries := make([]BillOfMaterialsEntry, len(entities))
|
bomEntries := make([]BillOfMaterialsEntry, len(entities))
|
||||||
for i, entity := range entities {
|
for i, entity := range entities {
|
||||||
bomEntries[i] = BillOfMaterialsEntry{
|
bomEntries[i] = BillOfMaterialsEntry{
|
||||||
|
|
|
@ -17,6 +17,7 @@ type ExportTSVRow struct {
|
||||||
LabelStr LabelString `csv:"HB.labels"`
|
LabelStr LabelString `csv:"HB.labels"`
|
||||||
ImportRef string `csv:"HB.import_ref"`
|
ImportRef string `csv:"HB.import_ref"`
|
||||||
AssetID repo.AssetID `csv:"HB.asset_id"`
|
AssetID repo.AssetID `csv:"HB.asset_id"`
|
||||||
|
Archived bool `csv:"HB.archived"`
|
||||||
|
|
||||||
Name string `csv:"HB.name"`
|
Name string `csv:"HB.name"`
|
||||||
Quantity int `csv:"HB.quantity"`
|
Quantity int `csv:"HB.quantity"`
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
"github.com/hay-kot/homebox/backend/internal/data/repo"
|
"github.com/hay-kot/homebox/backend/internal/data/repo"
|
||||||
"github.com/hay-kot/homebox/backend/internal/data/types"
|
"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
|
// 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)
|
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.
|
// Nil values are not allowed at the moment. This may change.
|
||||||
if v == nil {
|
if v == nil {
|
||||||
|
@ -185,6 +190,7 @@ func (s *IOSheet) ReadItems(items []repo.ItemOut) {
|
||||||
Quantity: item.Quantity,
|
Quantity: item.Quantity,
|
||||||
Description: item.Description,
|
Description: item.Description,
|
||||||
Insured: item.Insured,
|
Insured: item.Insured,
|
||||||
|
Archived: item.Archived,
|
||||||
|
|
||||||
PurchasePrice: item.PurchasePrice,
|
PurchasePrice: item.PurchasePrice,
|
||||||
PurchaseFrom: item.PurchaseFrom,
|
PurchaseFrom: item.PurchaseFrom,
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,7 +3,9 @@ package services
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/hay-kot/homebox/backend/internal/core/services/reporting"
|
"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
|
return finished, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *ItemService) CsvImport(ctx context.Context, GID uuid.UUID, data io.Reader) (int, error) {
|
func serializeLocation[T ~[]string](location T) string {
|
||||||
// loaded, err := reporting.ReadCSV(data)
|
return strings.Join(location, "/")
|
||||||
// if err != nil {
|
}
|
||||||
// return 0, err
|
|
||||||
// }
|
|
||||||
|
|
||||||
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) {
|
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()
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -16,9 +16,7 @@ func getPrevMonth(now time.Time) time.Time {
|
||||||
// avoid infinite loop
|
// avoid infinite loop
|
||||||
max := 15
|
max := 15
|
||||||
for t.Month() == now.Month() {
|
for t.Month() == now.Month() {
|
||||||
println("month is the same")
|
|
||||||
t = t.AddDate(0, 0, -1)
|
t = t.AddDate(0, 0, -1)
|
||||||
println(t.String())
|
|
||||||
|
|
||||||
max--
|
max--
|
||||||
if max == 0 {
|
if max == 0 {
|
||||||
|
|
|
@ -2,7 +2,6 @@ package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -81,7 +80,6 @@ func TestUserRepo_GetAll(t *testing.T) {
|
||||||
assert.Equal(t, len(created), len(allUsers))
|
assert.Equal(t, len(created), len(allUsers))
|
||||||
|
|
||||||
for _, usr := range created {
|
for _, usr := range created {
|
||||||
fmt.Printf("%+v\n", usr)
|
|
||||||
for _, usr2 := range allUsers {
|
for _, usr2 := range allUsers {
|
||||||
if usr.ID == usr2.ID {
|
if usr.ID == usr2.ID {
|
||||||
assert.Equal(t, usr.Email, usr2.Email)
|
assert.Equal(t, usr.Email, usr2.Email)
|
||||||
|
|
|
@ -2,7 +2,6 @@ package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -74,9 +73,7 @@ func (d Date) MarshalJSON() ([]byte, error) {
|
||||||
func (d *Date) UnmarshalJSON(data []byte) (err error) {
|
func (d *Date) UnmarshalJSON(data []byte) (err error) {
|
||||||
// unescape the string if necessary `\"` -> `"`
|
// unescape the string if necessary `\"` -> `"`
|
||||||
str := strings.Trim(string(data), "\"")
|
str := strings.Trim(string(data), "\"")
|
||||||
fmt.Printf("str: %q\n", str)
|
|
||||||
if str == "" || str == "null" || str == `""` {
|
if str == "" || str == "null" || str == `""` {
|
||||||
println("empty date")
|
|
||||||
*d = Date{}
|
*d = Date{}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue