From 8fcd0d84d34852509288b170e57da838bc8522cb Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Mon, 13 Feb 2023 09:37:55 -0900 Subject: [PATCH] new reporting service --- .../{testdata => .testdata}/import.csv | 0 .../{testdata => .testdata}/import.tsv | 0 backend/internal/core/services/all.go | 15 +++- .../core/services/reporting/reporting.go | 85 +++++++++++++++++++ .../core/services/service_items_csv_test.go | 4 +- backend/internal/data/repo/repo_items.go | 7 +- 6 files changed, 102 insertions(+), 9 deletions(-) rename backend/internal/core/services/{testdata => .testdata}/import.csv (100%) rename backend/internal/core/services/{testdata => .testdata}/import.tsv (100%) create mode 100644 backend/internal/core/services/reporting/reporting.go diff --git a/backend/internal/core/services/testdata/import.csv b/backend/internal/core/services/.testdata/import.csv similarity index 100% rename from backend/internal/core/services/testdata/import.csv rename to backend/internal/core/services/.testdata/import.csv diff --git a/backend/internal/core/services/testdata/import.tsv b/backend/internal/core/services/.testdata/import.tsv similarity index 100% rename from backend/internal/core/services/testdata/import.tsv rename to backend/internal/core/services/.testdata/import.tsv diff --git a/backend/internal/core/services/all.go b/backend/internal/core/services/all.go index 43deb52..2997095 100644 --- a/backend/internal/core/services/all.go +++ b/backend/internal/core/services/all.go @@ -1,11 +1,16 @@ package services -import "github.com/hay-kot/homebox/backend/internal/data/repo" +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 + User *UserService + Group *GroupService + Items *ItemService + Reporting *reporting.ReportingService } type OptionsFunc func(*options) @@ -40,5 +45,7 @@ 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), } } diff --git a/backend/internal/core/services/reporting/reporting.go b/backend/internal/core/services/reporting/reporting.go new file mode 100644 index 0000000..4ba408b --- /dev/null +++ b/backend/internal/core/services/reporting/reporting.go @@ -0,0 +1,85 @@ +package reporting + +import ( + "context" + "encoding/csv" + "io" + "time" + + "github.com/gocarina/gocsv" + "github.com/google/uuid" + "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, + } +} + +// ================================================================================================= + +// NullableTime is a custom type that implements the MarshalCSV interface +// to allow for nullable time.Time fields in the CSV output to be empty +// and not "0001-01-01". It also overrides the default CSV output format +type NullableTime time.Time + +func (t NullableTime) MarshalCSV() (string, error) { + if time.Time(t).IsZero() { + return "", nil + } + // YYYY-MM-DD + return time.Time(t).Format("2006-01-02"), nil +} + +type BillOfMaterialsEntry struct { + PurchaseDate NullableTime `csv:"Purchase Date"` + Name string `csv:"Name"` + Description string `csv:"Description"` + Manufacturer string `csv:"Manufacturer"` + SerialNumber string `csv:"Serial Number"` + ModelNumber string `csv:"Model Number"` + Quantity int `csv:"Quantity"` + Price float64 `csv:"Price"` + TotalPrice float64 `csv:"Total Price"` +} + +// 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 + } + + bomEntries := make([]BillOfMaterialsEntry, len(entities)) + for i, entity := range entities { + bomEntries[i] = BillOfMaterialsEntry{ + PurchaseDate: NullableTime(entity.PurchaseTime), + Name: entity.Name, + Description: entity.Description, + Manufacturer: entity.Manufacturer, + SerialNumber: entity.SerialNumber, + ModelNumber: entity.ModelNumber, + Quantity: entity.Quantity, + Price: entity.PurchasePrice, + TotalPrice: entity.PurchasePrice * float64(entity.Quantity), + } + } + + return gocsv.MarshalBytes(&bomEntries) +} diff --git a/backend/internal/core/services/service_items_csv_test.go b/backend/internal/core/services/service_items_csv_test.go index 675a1a9..5338979 100644 --- a/backend/internal/core/services/service_items_csv_test.go +++ b/backend/internal/core/services/service_items_csv_test.go @@ -12,10 +12,10 @@ import ( "github.com/stretchr/testify/assert" ) -//go:embed testdata/import.csv +//go:embed .testdata/import.csv var CSVData_Comma []byte -//go:embed testdata/import.tsv +//go:embed .testdata/import.tsv var CSVData_Tab []byte func loadcsv() [][]string { diff --git a/backend/internal/data/repo/repo_items.go b/backend/internal/data/repo/repo_items.go index 91dd2b3..0f644f0 100644 --- a/backend/internal/data/repo/repo_items.go +++ b/backend/internal/data/repo/repo_items.go @@ -185,7 +185,8 @@ func mapItemSummary(item *ent.Item) ItemSummary { } var ( - mapItemOutErr = mapTErrFunc(mapItemOut) + mapItemOutErr = mapTErrFunc(mapItemOut) + mapItemsOutErr = mapTEachErrFunc(mapItemOut) ) func mapFields(fields []*ent.ItemField) []ItemField { @@ -434,8 +435,8 @@ func (e *ItemsRepository) QueryByAssetID(ctx context.Context, gid uuid.UUID, ass } // GetAll returns all the items in the database with the Labels and Locations eager loaded. -func (e *ItemsRepository) GetAll(ctx context.Context, gid uuid.UUID) ([]ItemSummary, error) { - return mapItemsSummaryErr(e.db.Item.Query(). +func (e *ItemsRepository) GetAll(ctx context.Context, gid uuid.UUID) ([]ItemOut, error) { + return mapItemsOutErr(e.db.Item.Query(). Where(item.HasGroupWith(group.ID(gid))). WithLabel(). WithLocation().