diff --git a/src/main/java/no/nibio/vips/logic/service/ObservationService.java b/src/main/java/no/nibio/vips/logic/service/ObservationService.java
index c23dd322845d1f9a574eed3192676edd64ea4a7d..128a5bf9c241cb2103a9bc04d34486a1d313def7 100755
--- a/src/main/java/no/nibio/vips/logic/service/ObservationService.java
+++ b/src/main/java/no/nibio/vips/logic/service/ObservationService.java
@@ -188,25 +188,26 @@ public class ObservationService {
             @QueryParam("locale") String localeStr,
             @QueryParam("isPositive") Boolean isPositive
     ) {
-        LOGGER.info("Generate xlsx file for observations");
         VipsLogicUser user = getVipsLogicUser(userUUID);
         ULocale locale = new ULocale(localeStr != null ? localeStr :
                 user != null ? user.getOrganizationId().getDefaultLocale() :
                         userBean.getOrganization(organizationId).getDefaultLocale());
-
+        LOGGER.info("Generate xlsx file for observations for user {}", user != null ? user.getUserId() : "unregistered");
 
         LocalDateTime now = LocalDateTime.now();
         DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
         String filenameTimestamp = now.format(formatter);
 
         try {
-            byte[] excelFile = ExcelFileGenerator.generateExcel(getFilteredObservationListItems(organizationId, observationTimeSeriesId, pestId, cropId, cropCategoryId, fromStr, toStr, userUUID, localeStr, isPositive), locale);
+            List<ObservationListItem> observations = getFilteredObservationListItems(organizationId, observationTimeSeriesId, pestId, cropId, cropCategoryId, fromStr, toStr, userUUID, localeStr, isPositive);
+            byte[] excelFile = ExcelFileGenerator.generateExcel(observations, locale);
 
             return Response
                     .ok(excelFile)
-                    .header("Content-Disposition", "attachment; filename=\"" +filenameTimestamp+ "-observations.xlsx\"")
+                    .header("Content-Disposition", "attachment; filename=\"" + filenameTimestamp + "-observations.xlsx\"")
                     .build();
         } catch (IOException e) {
+            LOGGER.error(e.getMessage());
             return Response.serverError().entity("Error generating Excel file: " + e.getMessage()).build();
         }
     }
diff --git a/src/main/java/no/nibio/vips/logic/util/ExcelFileGenerator.java b/src/main/java/no/nibio/vips/logic/util/ExcelFileGenerator.java
index 4380680bd9a820e78107e340796ee2a4256e8206..93918b0187535e3322a9515a25eb3fc4acdafea6 100644
--- a/src/main/java/no/nibio/vips/logic/util/ExcelFileGenerator.java
+++ b/src/main/java/no/nibio/vips/logic/util/ExcelFileGenerator.java
@@ -1,7 +1,11 @@
 package no.nibio.vips.logic.util;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.ibm.icu.util.ULocale;
 import no.nibio.vips.logic.entity.rest.ObservationListItem;
+import no.nibio.vips.observationdata.ObservationDataSchema;
 import org.apache.poi.xssf.usermodel.XSSFWorkbook;
 import org.apache.poi.ss.usermodel.*;
 
@@ -10,45 +14,186 @@ import java.io.IOException;
 import java.time.LocalDate;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
-import java.util.List;
-import java.util.ResourceBundle;
+import java.util.*;
+
+public final class ExcelFileGenerator {
+
+    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+    private static final ObjectMapper objectMapper = new ObjectMapper();
+
+    private static final int COL_INDEX_DATE = 0;
+    private static final int COL_INDEX_LOCATION = 1;
+    private static final int COL_INDEX_ORGANISM = 2;
+    private static final int COL_INDEX_CROP_ORGANISM = 3;
+    private static final int COL_INDEX_HEADING = 4;
+    private static final int COL_START_INDEX_DATA = 5;
+
+    public static byte[] generateExcel(List<ObservationListItem> observations, ULocale locale) throws IOException {
+        ResourceBundle rb = ResourceBundle.getBundle("no.nibio.vips.logic.i18n.vipslogictexts", locale.toLocale());
 
-public class ExcelFileGenerator {
-    public static byte[] generateExcel(List<ObservationListItem> filteredObservationListItems, ULocale locale) throws IOException {
         try (XSSFWorkbook workbook = new XSSFWorkbook();
              ByteArrayOutputStream out = new ByteArrayOutputStream()) {
 
-            ResourceBundle rb = ResourceBundle.getBundle("no.nibio.vips.logic.i18n.vipslogictexts", locale.toLocale());
-            // Prøvde å bruke i18n, fikk Invalid char (/) found at index (13) in sheet name 'Observasjoner/f?rstefunn'
-            Sheet sheet = workbook.createSheet("Observations");
+            // Create main mainSheet for all observations, with header row
+            Sheet mainSheet = workbook.createSheet(rb.getString("allObservations"));
+            createHeaderRow(mainSheet, rb);
 
-            Row headerRow = sheet.createRow(0);
-            headerRow.createCell(0).setCellValue(rb.getString("timeOfObservation"));
-            headerRow.createCell(1).setCellValue(rb.getString("organism"));
-            headerRow.createCell(2).setCellValue(rb.getString("cropOrganismId"));
-            headerRow.createCell(3).setCellValue(rb.getString("observationHeading"));
+            int mainSheetRowIndex = 1;
+            // Add one row for each observation in list of all observations
+            for (ObservationListItem item : observations) {
+                createItemRow(mainSheet, mainSheetRowIndex++, item);
+            }
+            autoSizeColumns(mainSheet, 0, COL_INDEX_HEADING);
 
-            int rowNum = 1;
-            for (ObservationListItem item : filteredObservationListItems) {
-                // TODO Better way of getting current timezone?
-                LocalDate localDateOfObservation = item.getTimeOfObservation().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
-                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+            // Prepare list of observations for each type of pest
+            Map<Integer, List<ObservationListItem>> pestObservations = getObservationsForEachPest(observations);
 
-                Row row = sheet.createRow(rowNum++);
-                row.createCell(0).setCellValue(localDateOfObservation.format(formatter));
-                row.createCell(1).setCellValue(item.getOrganismName());
-                row.createCell(2).setCellValue(item.getCropOrganismName());
-                row.createCell(3).setCellValue(item.getObservationHeading());
+            // Create sheets for each individual pest type
+            for (Integer pestId : pestObservations.keySet()) {
+                List<ObservationListItem> observationsForPest = pestObservations.get(pestId);
+                ObservationListItem firstObservationForPest = observationsForPest.get(0);
+                String pestName = firstObservationForPest.getOrganismName();
+                Sheet pestSheet = workbook.createSheet(pestName);
+                Row headerRow = createHeaderRow(pestSheet, rb);
 
-            }
+                // Add column titles for observation data
+                Map<String, String> dataColumnTitles = getObservationDataColumnTitles(firstObservationForPest.getObservationDataSchema());
+                int pestSheetColIndex = COL_START_INDEX_DATA;
+                for (String key : dataColumnTitles.keySet()) {
+                    headerRow.createCell(pestSheetColIndex++).setCellValue(dataColumnTitles.get(key));
+                }
+
+                int pestSheetRowIndex = 1;
+                for (ObservationListItem item : observationsForPest) {
+                    Row row = createItemRow(pestSheet, pestSheetRowIndex++, item);
 
-            // Auto-size columns to fit content
-            for (int i = 0; i <= 3; i++) {
-                sheet.autoSizeColumn(i);
+                    if (item.getObservationData() != null) {
+                        Map<String, Object> observationDataMap = objectMapper.readValue(item.getObservationData(), HashMap.class);
+                        pestSheetColIndex = COL_START_INDEX_DATA;
+                        for (String key : dataColumnTitles.keySet()) {
+                            Object value = observationDataMap.get(key);
+                            if (value instanceof Number) {
+                                row.createCell(pestSheetColIndex++).setCellValue(((Number) value).intValue());
+                            } else {
+                                row.createCell(pestSheetColIndex++).setCellValue(value != null ? value.toString() : "");
+                            }
+                        }
+                    }
+                }
+                autoSizeColumns(pestSheet, COL_INDEX_DATE, COL_START_INDEX_DATA + dataColumnTitles.size());
             }
 
             workbook.write(out);
             return out.toByteArray();
         }
     }
+
+    /**
+     * Auto-size columns of given sheet, from startIndex to endIndex, to ensure that content fits
+     *
+     * @param sheet      The sheet of which to auto-size columns
+     * @param startIndex The index of the first column to auto-size
+     * @param endIndex   The index of the last column to auto-size
+     */
+    private static void autoSizeColumns(Sheet sheet, int startIndex, int endIndex) {
+        for (int i = startIndex; i <= endIndex; i++) {
+            sheet.autoSizeColumn(i);
+        }
+    }
+
+    /**
+     * Create map with data property name as key, and corresponding localized name as value
+     *
+     * @param observationDataSchema The observation data schema which contains localized names and other info
+     * @return result map
+     */
+    private static Map<String, String> getObservationDataColumnTitles(ObservationDataSchema observationDataSchema) throws JsonProcessingException {
+        JsonNode rootNode = objectMapper.readTree(observationDataSchema.getDataSchema());
+        JsonNode propertiesNode = rootNode.path("properties");
+        Map<String, String> resultMap = new HashMap<>();
+        Iterator<Map.Entry<String, JsonNode>> fields = propertiesNode.fields();
+        while (fields.hasNext()) {
+            Map.Entry<String, JsonNode> field = fields.next();
+            String key = field.getKey();
+            String value = field.getValue().path("title").asText();
+            resultMap.put(key, value);
+        }
+        return resultMap;
+    }
+
+    /**
+     * Find the name of the point of interest in given geoInfo string
+     *
+     * @param geoInfo The geoInfo which might contain the name of the point of interest
+     * @return the point of interest name, or empty string
+     */
+    private static String getPointOfInterestName(String geoInfo) throws JsonProcessingException {
+        JsonNode rootNode = objectMapper.readTree(geoInfo);
+        JsonNode featuresNode = rootNode.path("features");
+        if (featuresNode.isArray() && !featuresNode.isEmpty()) {
+            JsonNode firstFeature = featuresNode.get(0);
+            JsonNode propertiesNode = firstFeature.path("properties");
+            return propertiesNode.path("pointOfInterestName").asText();
+        }
+        return "";
+    }
+
+    /**
+     * Create map with pestId as key, and list of corresponding observations as value
+     *
+     * @param observations The original list of observations
+     * @return result map
+     */
+    private static Map<Integer, List<ObservationListItem>> getObservationsForEachPest(List<ObservationListItem> observations) {
+        Map<Integer, List<ObservationListItem>> pestObservations = new HashMap<>();
+        for (ObservationListItem observation : observations) {
+            Integer pestId = observation.getOrganismId();
+            if (!pestObservations.containsKey(pestId)) {
+                List<ObservationListItem> observationList = new ArrayList<>();
+                observationList.add(observation);
+                pestObservations.put(pestId, observationList);
+            } else {
+                pestObservations.get(pestId).add(observation);
+            }
+        }
+        return pestObservations;
+    }
+
+    /**
+     * Create row with given index, for given observation list item
+     *
+     * @param sheet    The sheet to which a row will be added
+     * @param rowIndex The index of the row
+     * @param item     The item of which to add data
+     * @return the newly created row
+     */
+    private static Row createItemRow(Sheet sheet, int rowIndex, ObservationListItem item) throws JsonProcessingException {
+        LocalDate localDateOfObservation = item.getTimeOfObservation().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+        String pointOfInterestName = getPointOfInterestName(item.getGeoInfo());
+
+        Row row = sheet.createRow(rowIndex);
+        row.createCell(COL_INDEX_DATE).setCellValue(localDateOfObservation.format(DATE_FORMATTER));
+        row.createCell(COL_INDEX_LOCATION).setCellValue(pointOfInterestName);
+        row.createCell(COL_INDEX_ORGANISM).setCellValue(item.getOrganismName());
+        row.createCell(COL_INDEX_CROP_ORGANISM).setCellValue(item.getCropOrganismName());
+        row.createCell(COL_INDEX_HEADING).setCellValue(item.getObservationHeading());
+        return row;
+    }
+
+    /**
+     * Create first row of given sheet, with standard set of column titles
+     *
+     * @param sheet The sheet to which a row will be added
+     * @param rb    A resource bundle enabling localized messages
+     * @return the newly created header row
+     */
+    public static Row createHeaderRow(Sheet sheet, ResourceBundle rb) {
+        Row headerRow = sheet.createRow(0);
+        headerRow.createCell(COL_INDEX_DATE).setCellValue(rb.getString("timeOfObservation"));
+        headerRow.createCell(COL_INDEX_LOCATION).setCellValue(rb.getString("location"));
+        headerRow.createCell(COL_INDEX_ORGANISM).setCellValue(rb.getString("organism"));
+        headerRow.createCell(COL_INDEX_CROP_ORGANISM).setCellValue(rb.getString("cropOrganismId"));
+        headerRow.createCell(COL_INDEX_HEADING).setCellValue(rb.getString("observationHeading"));
+        return headerRow;
+    }
 }
diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties
index 80a357d25a61df2d96e5e3f7fbbfb66433f71d15..ac9819608aa1a1c0d740535f4016846db737bb89 100755
--- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties
+++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties
@@ -556,6 +556,8 @@ observationDataField_trapCountCropInside = Number of trap counts inside the fiel
 
 observationDataField_unit = Measuring unit
 
+allObservations = All observations
+
 observationDeleted = Observation was deleted
 
 observationHeading = Observation heading
diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties
index db00fc266365213c9c50b7885c415baa578a339a..ccc99bf289cefcae94c4472346ae762d02977cb3 100755
--- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties
+++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties
@@ -556,6 +556,8 @@ observationDataField_trapCountCropInside = Antall insekter, fellefangst inne i f
 
 observationDataField_unit = M\u00e5leenhet
 
+allObservations = Alle observasjoner
+
 observationDeleted = Observasjonen ble slettet
 
 observationHeading = Observasjonstittel