diff --git a/server/controllers/import.go b/server/controllers/import.go
index 38a897d..3ea9a9d 100644
--- a/server/controllers/import.go
+++ b/server/controllers/import.go
@@ -9,6 +9,7 @@ import (
func RegisteImportController(router *gin.RouterGroup) {
router.POST("/import/fuelly", fuellyImport)
+ router.POST("/import/drivvo", drivvoImport)
}
func fuellyImport(c *gin.Context) {
@@ -24,3 +25,17 @@ func fuellyImport(c *gin.Context) {
}
c.JSON(http.StatusOK, gin.H{})
}
+
+func drivvoImport(c *gin.Context) {
+ bytes, err := getFileBytes(c, "file")
+ if err != nil {
+ c.JSON(http.StatusUnprocessableEntity, err)
+ return
+ }
+ errors := service.DrivvoImport(bytes, c.MustGet("userId").(string))
+ if len(errors) > 0 {
+ c.JSON(http.StatusUnprocessableEntity, gin.H{"errors": errors})
+ return
+ }
+ c.JSON(http.StatusOK, gin.H{})
+}
diff --git a/server/service/importService.go b/server/service/importService.go
index dbd5eab..2c97d09 100644
--- a/server/service/importService.go
+++ b/server/service/importService.go
@@ -11,6 +11,188 @@ import (
"github.com/leekchan/accounting"
)
+// TODO: Move drivvo stuff to separate file
+
+func DrivvoParseExpenses(content []byte, user *db.User) ([]db.Expense, []string) {
+ expenseReader := csv.NewReader(bytes.NewReader(content))
+ expenseReader.Comment = '#'
+ // Read headers (there is a trailing comma at the end, that's why we have to read the first line)
+ expenseReader.Read()
+ expenseReader.FieldsPerRecord = 6
+ expenseRecords, err := expenseReader.ReadAll()
+
+ var errors []string
+ if err != nil {
+ errors = append(errors, err.Error())
+ println(err.Error())
+ return nil, errors
+ }
+
+ var expenses []db.Expense
+ for index, record := range expenseRecords {
+ expense := db.Expense{}
+
+ date, err := time.Parse("2006-01-02 15:04:05", record[1])
+ if err != nil {
+ errors = append(errors, "Found an invalid date/time at service/expense row "+strconv.Itoa(index+1))
+ }
+ expense.Date = date
+
+ totalCost, err := strconv.ParseFloat(record[2], 32)
+ if err != nil {
+ errors = append(errors, "Found and invalid total cost at service/expense row "+strconv.Itoa(index+1))
+ }
+ expense.Amount = float32(totalCost)
+
+ odometer, err := strconv.Atoi(record[0])
+ if err != nil {
+ errors = append(errors, "Found an invalid odometer reading at service/expense row "+strconv.Itoa(index+1))
+ }
+ expense.OdoReading = odometer
+
+ notes := fmt.Sprintf("Location: %s\nNotes: %s\n", record[4], record[5])
+ expense.Comments = notes
+
+ expense.ExpenseType = record[3]
+ expense.UserID = user.ID
+ expense.Currency = user.Currency
+ expense.DistanceUnit = user.DistanceUnit
+ expense.Source = "Drivvo"
+
+ expenses = append(expenses, expense)
+ }
+
+ return expenses, errors
+}
+
+func DrivvoParseRefuelings(content []byte, user *db.User) ([]db.Fillup, []string) {
+ refuelingReader := csv.NewReader(bytes.NewReader(content))
+ refuelingReader.Comment = '#'
+ refuelingRecords, err := refuelingReader.ReadAll()
+
+ var errors []string
+ if err != nil {
+ errors = append(errors, err.Error())
+ println(err.Error())
+ return nil, errors
+ }
+
+ var fillups []db.Fillup
+ for index, record := range refuelingRecords {
+ // Skip column titles
+ if index == 0 {
+ continue
+ }
+
+ fillup := db.Fillup{}
+
+ date, err := time.Parse("2006-01-02 15:04:05", record[1])
+ if err != nil {
+ errors = append(errors, "Found an invalid date/time at refuel row "+strconv.Itoa(index+1))
+ }
+ fillup.Date = date
+
+ totalCost, err := strconv.ParseFloat(record[4], 32)
+ if err != nil {
+ errors = append(errors, "Found and invalid total cost at refuel row "+strconv.Itoa(index+1))
+ }
+ fillup.TotalAmount = float32(totalCost)
+
+ odometer, err := strconv.Atoi(record[0])
+ if err != nil {
+ errors = append(errors, "Found an invalid odometer reading at refuel row "+strconv.Itoa(index+1))
+ }
+ fillup.OdoReading = odometer
+
+ // TODO: Make optional
+ location := record[17]
+ fillup.FillingStation = location
+
+ pricePerUnit, err := strconv.ParseFloat(record[3], 32)
+ if err != nil {
+ // TODO: Add unit type to error message
+ errors = append(errors, "Found an invalid cost per unit at refuel row "+strconv.Itoa(index+1))
+ }
+ fillup.PerUnitPrice = float32(pricePerUnit)
+
+ quantity, err := strconv.ParseFloat(record[5], 32)
+ if err != nil {
+ errors = append(errors, "Found an invalid quantity at refuel row "+strconv.Itoa(index+1))
+ }
+ fillup.FuelQuantity = float32(quantity)
+
+ isTankFull := record[6] == "Yes"
+ fillup.IsTankFull = &isTankFull
+
+ // Unfortunatly, drivvo doesn't expose this info in their export
+ fal := false
+ fillup.HasMissedFillup = &fal
+
+ notes := fmt.Sprintf("Reason: %s\nNotes: %s\nFuel: %s\n", record[18], record[19], record[2])
+ fillup.Comments = notes
+
+ fillup.UserID = user.ID
+ fillup.Currency = user.Currency
+ fillup.DistanceUnit = user.DistanceUnit
+ fillup.Source = "Drivvo"
+
+ fillups = append(fillups, fillup)
+ }
+ return fillups, errors
+}
+
+func DrivvoImport(content []byte, userId string) []string {
+ var errors []string
+ user, err := GetUserById(userId)
+ if err != nil {
+ errors = append(errors, err.Error())
+ return errors
+ }
+
+ serviceSectionIndex := bytes.Index(content, []byte("#Service"))
+
+ endParseIndex := bytes.Index(content, []byte("#Income"))
+ if endParseIndex == -1 {
+ endParseIndex = bytes.Index(content, []byte("#Route"))
+ if endParseIndex == -1 {
+ endParseIndex = len(content)
+ }
+
+ }
+
+ expenseSectionIndex := bytes.Index(content, []byte("#Expense"))
+ if expenseSectionIndex == -1 {
+ expenseSectionIndex = endParseIndex
+ }
+
+ fillups, errors := DrivvoParseRefuelings(content[:serviceSectionIndex], user)
+ _ = fillups
+
+ var allExpenses []db.Expense
+ if serviceSectionIndex != -1 {
+ services, parseErrors := DrivvoParseExpenses(content[serviceSectionIndex:expenseSectionIndex], user)
+ if parseErrors != nil {
+ errors = append(errors, parseErrors...)
+ }
+ allExpenses = append(allExpenses, services...)
+ }
+
+ if expenseSectionIndex != endParseIndex {
+ expenses, parseErrors := DrivvoParseExpenses(content[expenseSectionIndex:endParseIndex], user)
+ if parseErrors != nil {
+ errors = append(errors, parseErrors...)
+ }
+ allExpenses = append(allExpenses, expenses...)
+ }
+
+ if len(errors) != 0 {
+ return errors
+ }
+
+ errors = append(errors, "Not implemented")
+ return errors
+}
+
func FuellyImport(content []byte, userId string) []string {
stream := bytes.NewReader(content)
reader := csv.NewReader(stream)
diff --git a/ui/src/router/routes.js b/ui/src/router/routes.js
index 82495f7..011de78 100644
--- a/ui/src/router/routes.js
+++ b/ui/src/router/routes.js
@@ -410,6 +410,15 @@ export default [
},
props: (route) => ({ user: store.state.auth.currentUser || {} }),
},
+ {
+ path: '/import/drivvo',
+ name: 'import-drivvo',
+ component: () => lazyLoadView(import('@views/import-drivvo.vue')),
+ meta: {
+ authRequired: true,
+ },
+ props: (route) => ({ user: store.state.auth.currentUser || {} }),
+ },
{
path: '/logout',
name: 'logout',
diff --git a/ui/src/router/views/import-drivvo.vue b/ui/src/router/views/import-drivvo.vue
new file mode 100644
index 0000000..f30b377
--- /dev/null
+++ b/ui/src/router/views/import-drivvo.vue
@@ -0,0 +1,146 @@
+
+
+
+ Steps to import data from Drivvo Choose the Drivvo CSV and press the import button.Import from Drivvo
+
+
+
+
+
+
If you have been using Fuelly to store your vehicle data, export the CSV file from Fuelly and click here to import.
-If you have been using Fuelly to store your vehicle data, export the CSV file from Fuelly and click here to import.
+Import data stored in Drivvo to Hammond by exporting the CSV file from Drivvo and uploading it here.
+