2022-09-03 09:17:48 +00:00
|
|
|
package services
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2022-09-24 19:33:38 +00:00
|
|
|
"errors"
|
2023-02-26 02:54:40 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"strings"
|
2022-09-03 09:17:48 +00:00
|
|
|
|
|
|
|
"github.com/google/uuid"
|
2023-02-26 02:54:40 +00:00
|
|
|
"github.com/hay-kot/homebox/backend/internal/core/services/reporting"
|
2022-10-30 04:05:38 +00:00
|
|
|
"github.com/hay-kot/homebox/backend/internal/data/repo"
|
2022-09-03 09:17:48 +00:00
|
|
|
)
|
|
|
|
|
2022-09-24 19:33:38 +00:00
|
|
|
var (
|
|
|
|
ErrNotFound = errors.New("not found")
|
|
|
|
ErrFileNotFound = errors.New("file not found")
|
|
|
|
)
|
|
|
|
|
2022-09-03 09:17:48 +00:00
|
|
|
type ItemService struct {
|
|
|
|
repo *repo.AllRepos
|
2022-09-12 22:47:27 +00:00
|
|
|
|
|
|
|
filepath string
|
2022-11-13 23:17:55 +00:00
|
|
|
|
|
|
|
autoIncrementAssetID bool
|
2022-09-03 09:17:48 +00:00
|
|
|
}
|
|
|
|
|
2022-11-13 23:17:55 +00:00
|
|
|
func (svc *ItemService) Create(ctx Context, item repo.ItemCreate) (repo.ItemOut, error) {
|
|
|
|
if svc.autoIncrementAssetID {
|
|
|
|
highest, err := svc.repo.Items.GetHighestAssetID(ctx, ctx.GID)
|
|
|
|
if err != nil {
|
|
|
|
return repo.ItemOut{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
item.AssetID = repo.AssetID(highest + 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
return svc.repo.Items.Create(ctx, ctx.GID, item)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *ItemService) EnsureAssetID(ctx context.Context, GID uuid.UUID) (int, error) {
|
|
|
|
items, err := svc.repo.Items.GetAllZeroAssetID(ctx, GID)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
highest, err := svc.repo.Items.GetHighestAssetID(ctx, GID)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
finished := 0
|
|
|
|
for _, item := range items {
|
|
|
|
highest++
|
|
|
|
|
|
|
|
err = svc.repo.Items.SetAssetID(ctx, GID, item.ID, repo.AssetID(highest))
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
finished++
|
|
|
|
}
|
|
|
|
|
|
|
|
return finished, nil
|
|
|
|
}
|
2022-09-06 19:15:07 +00:00
|
|
|
|
2023-02-26 02:54:40 +00:00
|
|
|
func (svc *ItemService) EnsureImportRef(ctx context.Context, GID uuid.UUID) (int, error) {
|
|
|
|
ids, err := svc.repo.Items.GetAllZeroImportRef(ctx, GID)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
finished := 0
|
|
|
|
for _, itemID := range ids {
|
|
|
|
ref := uuid.New().String()[0:8]
|
2022-09-13 04:54:30 +00:00
|
|
|
|
2023-02-26 02:54:40 +00:00
|
|
|
err = svc.repo.Items.Patch(ctx, GID, itemID, repo.ItemPatch{ImportRef: &ref})
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
2022-09-06 19:15:07 +00:00
|
|
|
}
|
|
|
|
|
2023-02-26 02:54:40 +00:00
|
|
|
finished++
|
2022-09-06 19:15:07 +00:00
|
|
|
}
|
|
|
|
|
2023-02-26 02:54:40 +00:00
|
|
|
return finished, nil
|
|
|
|
}
|
2022-10-16 01:46:57 +00:00
|
|
|
|
2023-02-26 02:54:40 +00:00
|
|
|
func serializeLocation[T ~[]string](location T) string {
|
|
|
|
return strings.Join(location, "/")
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
2022-10-16 01:46:57 +00:00
|
|
|
|
2023-02-26 02:54:40 +00:00
|
|
|
// ========================================
|
|
|
|
// Labels
|
2022-10-16 01:46:57 +00:00
|
|
|
|
2023-02-26 02:54:40 +00:00
|
|
|
labelMap := make(map[string]uuid.UUID)
|
|
|
|
{
|
|
|
|
labels, err := svc.repo.Labels.GetAll(ctx, GID)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
2022-10-16 01:46:57 +00:00
|
|
|
}
|
|
|
|
|
2023-02-26 02:54:40 +00:00
|
|
|
for _, label := range labels {
|
|
|
|
labelMap[label.Name] = label.ID
|
2022-10-16 01:46:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-26 02:54:40 +00:00
|
|
|
// ========================================
|
|
|
|
// Locations
|
2022-09-06 19:15:07 +00:00
|
|
|
|
2023-02-26 02:54:40 +00:00
|
|
|
locationMap := make(map[string]uuid.UUID)
|
|
|
|
{
|
|
|
|
locations, err := svc.repo.Locations.Tree(ctx, GID, repo.TreeQuery{WithItems: false})
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
2022-09-06 19:15:07 +00:00
|
|
|
|
2023-02-26 02:54:40 +00:00
|
|
|
// 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)
|
2022-09-06 19:15:07 +00:00
|
|
|
|
2023-02-26 02:54:40 +00:00
|
|
|
locationMap[serializeLocation(path)] = location.ID
|
|
|
|
|
|
|
|
for _, child := range location.Children {
|
|
|
|
traverse(child, path)
|
2022-10-16 01:46:57 +00:00
|
|
|
}
|
2022-09-06 19:15:07 +00:00
|
|
|
}
|
|
|
|
|
2023-02-26 02:54:40 +00:00
|
|
|
for _, location := range locations {
|
|
|
|
traverse(&location, []string{})
|
2022-09-06 19:15:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-26 02:54:40 +00:00
|
|
|
// ========================================
|
|
|
|
// Import items
|
|
|
|
|
|
|
|
// Asset ID Pre-Check
|
|
|
|
highestAID := repo.AssetID(-1)
|
2022-11-13 23:17:55 +00:00
|
|
|
if svc.autoIncrementAssetID {
|
2023-02-26 02:54:40 +00:00
|
|
|
highestAID, err = svc.repo.Items.GetHighestAssetID(ctx, GID)
|
2022-11-13 23:17:55 +00:00
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-26 02:54:40 +00:00
|
|
|
finished := 0
|
|
|
|
|
|
|
|
for i := range sheet.Rows {
|
|
|
|
row := sheet.Rows[i]
|
|
|
|
|
|
|
|
createRequired := true
|
|
|
|
|
|
|
|
// ========================================
|
|
|
|
// Preflight check for existing item
|
|
|
|
if row.ImportRef != "" {
|
|
|
|
exists, err := svc.repo.Items.CheckRef(ctx, GID, row.ImportRef)
|
2022-10-16 01:46:57 +00:00
|
|
|
if err != nil {
|
2023-02-26 02:54:40 +00:00
|
|
|
return 0, fmt.Errorf("error checking for existing item with ref %q: %w", row.ImportRef, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if exists {
|
|
|
|
createRequired = false
|
2022-10-16 01:46:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-26 02:54:40 +00:00
|
|
|
// ========================================
|
|
|
|
// 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
|
2022-09-06 19:15:07 +00:00
|
|
|
}
|
|
|
|
|
2023-02-26 02:54:40 +00:00
|
|
|
// ========================================
|
|
|
|
// 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
|
|
|
|
}
|
2022-09-06 19:15:07 +00:00
|
|
|
|
2023-02-26 02:54:40 +00:00
|
|
|
locationID, ok = locationMap[path]
|
|
|
|
if !ok {
|
|
|
|
return 0, errors.New("failed to create location")
|
|
|
|
}
|
2022-11-13 23:17:55 +00:00
|
|
|
}
|
|
|
|
|
2023-02-26 02:54:40 +00:00
|
|
|
var effAID repo.AssetID
|
|
|
|
if svc.autoIncrementAssetID && row.AssetID.Nil() {
|
|
|
|
effAID = highestAID + 1
|
|
|
|
highestAID++
|
|
|
|
} else {
|
|
|
|
effAID = row.AssetID
|
2022-11-13 23:17:55 +00:00
|
|
|
}
|
|
|
|
|
2023-02-26 02:54:40 +00:00
|
|
|
// ========================================
|
|
|
|
// Create Item
|
|
|
|
var item repo.ItemOut
|
|
|
|
switch {
|
|
|
|
case createRequired:
|
|
|
|
newItem := repo.ItemCreate{
|
|
|
|
ImportRef: row.ImportRef,
|
|
|
|
Name: row.Name,
|
|
|
|
Description: row.Description,
|
|
|
|
AssetID: effAID,
|
|
|
|
LocationID: locationID,
|
|
|
|
LabelIDs: labelIds,
|
|
|
|
}
|
2022-09-06 19:15:07 +00:00
|
|
|
|
2023-02-26 02:54:40 +00:00
|
|
|
item, err = svc.repo.Items.Create(ctx, GID, newItem)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
item, err = svc.repo.Items.GetByRef(ctx, GID, row.ImportRef)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if item.ID == uuid.Nil {
|
|
|
|
panic("item ID is nil on import - this should never happen")
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
|
|
|
}
|
2022-09-06 19:15:07 +00:00
|
|
|
}
|
|
|
|
|
2023-02-26 02:54:40 +00:00
|
|
|
updateItem := repo.ItemUpdate{
|
|
|
|
ID: item.ID,
|
|
|
|
LabelIDs: labelIds,
|
2022-09-13 04:54:30 +00:00
|
|
|
LocationID: locationID,
|
2022-09-06 19:15:07 +00:00
|
|
|
|
2023-02-26 02:54:40 +00:00
|
|
|
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)
|
2022-09-06 19:15:07 +00:00
|
|
|
if err != nil {
|
2023-02-26 02:54:40 +00:00
|
|
|
return 0, err
|
2022-09-06 19:15:07 +00:00
|
|
|
}
|
2022-10-16 01:46:57 +00:00
|
|
|
|
2023-02-26 02:54:40 +00:00
|
|
|
finished++
|
2022-09-06 19:15:07 +00:00
|
|
|
}
|
2023-02-26 02:54:40 +00:00
|
|
|
|
|
|
|
return finished, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *ItemService) ExportTSV(ctx context.Context, GID uuid.UUID) ([][]string, error) {
|
|
|
|
items, err := svc.repo.Items.GetAll(ctx, GID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
sheet := reporting.IOSheet{}
|
|
|
|
|
2023-04-01 23:10:27 +00:00
|
|
|
err = sheet.ReadItems(ctx, items, GID, svc.repo)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-02-26 02:54:40 +00:00
|
|
|
|
|
|
|
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)
|
2022-09-06 19:15:07 +00:00
|
|
|
}
|