Skip to content

Commit

Permalink
Add initial version
Browse files Browse the repository at this point in the history
  • Loading branch information
mvach committed Oct 21, 2024
1 parent ff7adb8 commit d774109
Show file tree
Hide file tree
Showing 10 changed files with 602 additions and 2 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ go.work.sum

# env file
.env

local/
bin/
59 changes: 57 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,57 @@
# thwInventurMerge
Dieses kleine Toole ermöglicht es erfasstes Equipment mir den THW Inventurdaten aus THWin zu mergen.
# thwInventoryMerge
Dieses kleine Tool ermöglicht es, erfasstes Equipment mit den THW-Inventurdaten aus THWin zu mergen. Das Equipment wird dabei mittels Barcode-Scannern erfasst, die die gescannten Codes als CSV-Dateien speichern.

## Installation
Das Tool lädt man am einfachsten aus der [Releases-Sektion](https://github.com/mvach/thwInventoryMerge/releases) herunter und legt es in ein beliebiges leeres Verzeichnis (das `working_dir`).

## Konfiguration
Die CSV-Dateien mit den erfassten Barcodes der Scanner sowie die (aus THWin exportierte) Inventur-Excel-Datei legt man am besten ebenfalls in das `working_dir`.

Zudem erstellt man eine Konfigurationsdatei (`config.json`), die man am einfachsten auch in das `working_dir` legt.

## Hier ist eine Beispielkonfiguration:

```
// config.json
{
"excel_file_name": "20240101_Bestand.xlsx",
"excel_config": {
"worksheet_name": "N",
"equipment_id_column_name": "Inventar Nr",
"equipment_available_column_name": "Verfügbar"
}
}
```

### Verzeichnisstruktur

```
C:/
└── Users/
└── DeinUser/
└── MeinArbeitsverzeichnis/
├── config.json
├── 20240101_Bestand.xlsx
├── scanner1.csv
├── scanner2.csv
├── scanner3.csv
└── thwInventoryMerge.exe
```

### CSV Beispiel
```csv
// scanner1.csv
0001-S001304
0509-002494
0509-002494
0591-S002360
0591-002781
0591-002781
0591-S002318
...
```

## Ausführung

Liegen alle Dateien gemeinsam im `working_dir`, kann thwInventoryMerge.exe einfach per Doppelklick ausgeführt werden.
145 changes: 145 additions & 0 deletions app/update_excel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package app

import (
"encoding/csv"
"fmt"
"os"
"strings"
"thwInventoryMerge/config"

"github.com/xuri/excelize/v2"
)

type UpdateExcel interface {
Update() error
}

type updateExcel struct {
config config.Config
}

func NewUpdateExcel(config config.Config) UpdateExcel {
return &updateExcel{
config: config,
}
}

func (u *updateExcel) Update() error {
recordedInventory, err := u.getRecordedInventory()
if err != nil {
return fmt.Errorf("failed to get recorded inventory: %w", err)
}

excelFile, err := excelize.OpenFile(u.config.GetAbsoluteExcelFileName())
if err != nil {
return fmt.Errorf("failed to open the Excel file: %w", err)
}

rows, err := excelFile.GetRows(u.config.ExcelConfig.WorksheetName)
if err != nil {
return fmt.Errorf("failed to get rows from Excel file: %w", err)
}

for rowIndex, row := range rows {
if rowIndex == 0 {
err := u.getHeaderIndices(row)
if err != nil {
return fmt.Errorf("failed to get headers from Excel file: %w", err)
}
continue
}

// Check if the equipment ID exists in the inventory numbers
equipmentID := row[*u.config.ExcelConfig.EquipmentIDColumnIndex]
if count, exists := recordedInventory[strings.ToLower(equipmentID)]; exists {

// Set the value in the specified cell
cell, err := excelize.CoordinatesToCellName(*u.config.ExcelConfig.EquipmentAvailableColumnIndex+1, rowIndex+1)
if err != nil {
return fmt.Errorf("failed to get cell from coordinates: %w", err)
}
excelFile.SetCellValue(
u.config.ExcelConfig.WorksheetName,
cell,
count,
)
}
}

if err := excelFile.Save(); err != nil {
return fmt.Errorf("failed to save the updated Excel file: %w", err)
} else {
fmt.Println("\nUpdated Excel file successfully.")
}

return nil
}

func (u *updateExcel) getHeaderIndices(row []string) error {
for j, col := range row {
if strings.EqualFold(col, u.config.ExcelConfig.EquipmentIDColumnName) {
u.config.ExcelConfig.EquipmentIDColumnIndex = &j
} else if strings.EqualFold(col, u.config.ExcelConfig.EquipmentAvailableColumnName) {
u.config.ExcelConfig.EquipmentAvailableColumnIndex = &j
}
}

if u.config.ExcelConfig.EquipmentIDColumnIndex == nil {
return fmt.Errorf(
"failed to find header %s in first row of worksheet %s",
u.config.ExcelConfig.EquipmentIDColumnName,
u.config.ExcelConfig.WorksheetName,
)
}
if u.config.ExcelConfig.EquipmentAvailableColumnIndex == nil {
return fmt.Errorf(
"failed to find header %s in first row of worksheet %s",
u.config.ExcelConfig.EquipmentAvailableColumnName,
u.config.ExcelConfig.WorksheetName,
)
}

return nil
}

func (u *updateExcel) getRecordedInventory() (map[string]int, error) {
inventoryNumbers := make(map[string]int)

csvFiles, err := u.config.GetCSVFiles()
if err != nil {
return nil, fmt.Errorf("failed to get CSV files: %w", err)
}

for _, file := range csvFiles {
f, err := os.Open(file)
if err != nil {
return nil, fmt.Errorf("failed to open CSV file: %w", err)
}
defer f.Close()

reader := csv.NewReader(f)
records, err := reader.ReadAll()
if err != nil {
return nil, fmt.Errorf("failed to read CSV file: %w", err)
}

for _, record := range records {
if len(record) > 0 {
inventoryNumbers[strings.ToLower(record[0])]++
}
}
}

u.printInventoryNumbers(inventoryNumbers)

return inventoryNumbers, nil
}

func (u *updateExcel) printInventoryNumbers(inventoryNumbers map[string]int) {
fmt.Printf("%-20s %s\n", "Inventory Number", "Quantity")
fmt.Println(strings.Repeat("-", 30)) // Print a separator line

for number, quantity := range inventoryNumbers {
fmt.Printf("%-20s %d\n", number, quantity)
}
}
79 changes: 79 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package config

import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
)

type Config struct {
WorkingDir string `json:"working_dir"`
ExcelFileName string `json:"excel_file_name"`
ExcelConfig struct {
WorksheetName string `json:"worksheet_name"`
EquipmentIDColumnName string `json:"equipment_id_column_name"`
EquipmentIDColumnIndex *int
EquipmentAvailableColumnName string `json:"equipment_available_column_name"`
EquipmentAvailableColumnIndex *int
} `json:"excel_config"`
}

func (c *Config) GetCSVFiles() ([]string, error) {
var csvFiles []string

files, err := os.ReadDir(c.WorkingDir)
if err != nil {
return nil, err
}

for _, file := range files {
if !file.IsDir() && filepath.Ext(file.Name()) == ".csv" {
csvFiles = append(csvFiles, filepath.Join(c.WorkingDir, file.Name()))
}
}

return csvFiles, nil
}

func (c *Config) GetAbsoluteExcelFileName() string {
return filepath.Join(c.WorkingDir, c.ExcelFileName)
}

func LoadConfig(filePath string) (*Config, error) {
data, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}

var config Config

err = json.Unmarshal(data, &config)
if err != nil {
return nil, fmt.Errorf("failed to load invalid config file, %w", err)
}

err = config.validate()
if err != nil {
return nil, fmt.Errorf("failed to validate the config file, %w", err)
}

return &config, nil
}

func (c Config) validate() error {
if c.ExcelFileName == "" {
return errors.New("property excel_file_name is required")
}
if c.ExcelConfig.WorksheetName == "" {
return errors.New("property excel_config.worksheet_name is required")
}
if c.ExcelConfig.EquipmentIDColumnName == "" {
return errors.New("property excel_config.equipment_id_column_name is required")
}
if c.ExcelConfig.EquipmentAvailableColumnName == "" {
return errors.New("property excel_config.equipment_available_column_name is required")
}
return nil
}
13 changes: 13 additions & 0 deletions config/config_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package config_test

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestConfig(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Config Suite")
}
Loading

0 comments on commit d774109

Please sign in to comment.