diff --git a/pom.xml b/pom.xml
index b942c53a5dd01548fee6ae7d81bd110e56e68088..89120e15b262d8dcf44c39784557233bbb7774fa 100755
--- a/pom.xml
+++ b/pom.xml
@@ -266,6 +266,21 @@
             <version>2.0.11</version>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi</artifactId>
+            <version>5.3.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi-ooxml</artifactId>
+            <version>5.3.0</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.17.0</version>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/src/main/java/no/nibio/vips/logic/controller/session/ObservationBean.java b/src/main/java/no/nibio/vips/logic/controller/session/ObservationBean.java
index e205348f4a9e392b952760fb525d084c284033a6..99a5f3c304414d0c40f218fd7a03323e694c7631 100755
--- a/src/main/java/no/nibio/vips/logic/controller/session/ObservationBean.java
+++ b/src/main/java/no/nibio/vips/logic/controller/session/ObservationBean.java
@@ -61,7 +61,7 @@ import org.wololo.geojson.GeoJSONFactory;
  */
 @Stateless
 public class ObservationBean {
-    private static Logger LOGGER = LoggerFactory.getLogger(ObservationBean.class);
+    private static final Logger LOGGER = LoggerFactory.getLogger(ObservationBean.class);
     @PersistenceContext(unitName = "VIPSLogic-PU")
     EntityManager em;
     @EJB
@@ -520,10 +520,6 @@ public class ObservationBean {
                 .filter(o -> o.getObservationTimeSeriesId() != null)
                 .forEach(o -> o.setObservationTimeSeries(timeSeriesMap.get(o.getObservationTimeSeriesId())));
 
-        for (Observation o : observations) {
-            LOGGER.info("{}", o);
-        }
-
         return observations;
     }
     private List<Observation> getObservationsWithObservers(List<Observation> observations) {
diff --git a/src/main/java/no/nibio/vips/logic/entity/Observation.java b/src/main/java/no/nibio/vips/logic/entity/Observation.java
index 241e0c14ad16f334804c251a2f47dc698315c2f1..0b244dcfbb9170f91e9fe3c0eb296c30cd90d419 100755
--- a/src/main/java/no/nibio/vips/logic/entity/Observation.java
+++ b/src/main/java/no/nibio/vips/logic/entity/Observation.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2014 NIBIO <http://www.nibio.no/>. 
+ * Copyright (c) 2014 NIBIO <http://www.nibio.no/>.
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as published by
@@ -95,15 +95,15 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse
     private Boolean broadcastMessage;
     private Boolean locationIsPrivate;
     private PolygonService polygonService;
-    
+
     private ObservationDataSchema observationDataSchema;
-    
+
     private VipsLogicUser user; // Transient
     private VipsLogicUser lastEditedByUser; //
     private PointOfInterest location; // Transient
-    
+
     private Set<ObservationIllustration> observationIllustrationSet;
-    
+
     private GISEntityUtil GISEntityUtil;
     private GISUtil GISUtil;
 
@@ -156,7 +156,7 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse
         this.timeOfObservation = timeOfObservation;
     }
 
-    
+
     // Using PostGIS + Hibernate-spatial + Java Topology Suite to make this work
     /*@JsonIgnore
     @Type(type = "org.hibernate.spatial.GeometryType")
@@ -168,12 +168,12 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse
     public void setLocation(Point location) {
         this.location = location;
     }*/
-    
+
     public void setGeoinfos(List<Gis> geoinfo)
     {
         this.geoinfo = geoinfo;
     }
-    
+
     public void addGeoInfo(Gis geoinfo)
     {
         if(this.geoinfo == null)
@@ -182,20 +182,20 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse
         }
         this.geoinfo.add(geoinfo);
     }
-    
+
     @JsonIgnore
     @Transient
     public List<Gis> getGeoinfos()
     {
         return this.geoinfo;
     }
-    
-    
+
+
     public void setGeoinfo(String json)
     {
         this.setGeoinfos(this.GISEntityUtil.getGisFromGeoJSON(json));
     }
-    
+
     @Transient
     @Override
     public String getGeoinfo()
@@ -269,37 +269,37 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse
     @Override
     public String toString() {
         return "Observation{" +
-                "observationId=" + observationId +
-                ", timeOfObservation=" + timeOfObservation +
-                ", organism=" + organism +
-                ", cropOrganism=" + cropOrganism +
-                ", userId=" + userId +
-                ", lastEditedBy=" + lastEditedBy +
-                ", geoinfo=" + geoinfo +
-                ", observationTimeSeries=" + observationTimeSeries +
-                ", locationPointOfInterestId=" + locationPointOfInterestId +
-                ", observationHeading='" + observationHeading + '\'' +
-                ", observationText='" + observationText + '\'' +
-                ", statusTypeId=" + statusTypeId +
-                ", statusChangedByUserId=" + statusChangedByUserId +
-                ", statusChangedTime=" + statusChangedTime +
-                ", lastEditedTime=" + lastEditedTime +
-                ", statusRemarks='" + statusRemarks + '\'' +
-                ", observationData='" + observationData + '\'' +
-                ", isQuantified=" + isQuantified +
-                ", isPositive=" + isPositive +
-                ", broadcastMessage=" + broadcastMessage +
-                ", locationIsPrivate=" + locationIsPrivate +
-                ", polygonService=" + polygonService +
-                ", observationDataSchema=" + observationDataSchema +
-                ", user=" + user +
-                ", lastEditedByUser=" + lastEditedByUser +
-                ", location=" + location +
-                ", observationIllustrationSet=" + observationIllustrationSet +
-                ", GISEntityUtil=" + GISEntityUtil +
-                ", GISUtil=" + GISUtil +
-                ", source=" + source +
-                '}';
+            "observationId=" + observationId +
+            ", timeOfObservation=" + timeOfObservation +
+            ", organism=" + organism +
+            ", cropOrganism=" + cropOrganism +
+            ", userId=" + userId +
+            ", lastEditedBy=" + lastEditedBy +
+            ", geoinfo=" + geoinfo +
+            ", observationTimeSeries=" + observationTimeSeries +
+            ", locationPointOfInterestId=" + locationPointOfInterestId +
+            ", observationHeading='" + observationHeading + '\'' +
+            ", observationText='" + observationText + '\'' +
+            ", statusTypeId=" + statusTypeId +
+            ", statusChangedByUserId=" + statusChangedByUserId +
+            ", statusChangedTime=" + statusChangedTime +
+            ", lastEditedTime=" + lastEditedTime +
+            ", statusRemarks='" + statusRemarks + '\'' +
+            ", observationData='" + observationData + '\'' +
+            ", isQuantified=" + isQuantified +
+            ", isPositive=" + isPositive +
+            ", broadcastMessage=" + broadcastMessage +
+            ", locationIsPrivate=" + locationIsPrivate +
+            ", polygonService=" + polygonService +
+            ", observationDataSchema=" + observationDataSchema +
+            ", user=" + user +
+            ", lastEditedByUser=" + lastEditedByUser +
+            ", location=" + location +
+            ", observationIllustrationSet=" + observationIllustrationSet +
+            ", GISEntityUtil=" + GISEntityUtil +
+            ", GISUtil=" + GISUtil +
+            ", source=" + source +
+            '}';
     }
 
     /**
@@ -338,13 +338,13 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse
     public void setOrganism(Organism organism) {
         this.organism = organism;
     }
-    
+
     @JoinColumn(name = "polygon_service_id", referencedColumnName = "polygon_service_id")
     @ManyToOne
     public PolygonService getPolygonService(){
         return this.polygonService;
     }
-    
+
     public void setPolygonService(PolygonService polygonService)
     {
         this.polygonService = polygonService;
@@ -363,7 +363,7 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse
         return this.getLocationCoordinate().x;
     }
     */
-    
+
     @Override
     @Transient
     public String getName() {
@@ -480,13 +480,13 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse
     public void setObservationData(String observationData) {
         this.observationData = observationData;
     }
-    
+
     @Transient
     public ObservationDataSchema getObservationDataSchema()
     {
         return this.observationDataSchema != null ? this.observationDataSchema : null;
     }
-    
+
     public void setObservationDataSchema(ObservationDataSchema observationDataSchema)
     {
         this.observationDataSchema = observationDataSchema;
@@ -522,7 +522,7 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse
     public void setCropOrganism(Organism cropOrganism) {
         this.cropOrganism = cropOrganism;
     }
-    
+
     @Transient
     public Integer getCropOrganismId() {
         return this.getCropOrganism() != null ? this.getCropOrganism().getOrganismId() : null;
@@ -564,7 +564,7 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse
     }
 
     /**
-      * @return the observation time series
+     * @return the observation time series
      */
     @JoinColumn(name = "observation_time_series_id", referencedColumnName = "observation_time_series_id")
     @ManyToOne
@@ -678,7 +678,7 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse
     /**
      * Simplifies the public JSON object
      * @param locale
-     * @return 
+     * @return
      */
     public ObservationListItem getListItem(String locale, ObservationDataSchema observationDataSchema)
     {
@@ -690,36 +690,41 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse
             this.location.addProperty("timestamp", this.getTimeOfObservation().getTime());
         }
         return new ObservationListItem(
-                this.getObservationId(),
-                this.getObservationTimeSeriesId(),
-                this.getTimeOfObservation(),
-                this.getOrganismId(),
-                ! this.getOrganism().getLocalName(locale).trim().isBlank() ? this.getOrganism().getLocalName(locale) : this.getOrganism().getLatinName(),
-                this.getCropOrganismId(),
-                ! this.getCropOrganism().getLocalName(locale).trim().isBlank() ? this.getCropOrganism().getLocalName(locale) : this.getCropOrganism().getLatinName(),
-                this.observationTimeSeries != null ? this.observationTimeSeries.getLabel() : null,
-                // Specific geoInfo trumps location. This is to be interpreted 
-                // as that the observation has been geographically masked by
-                // choice of the observer
-                this.location != null && this.geoinfo == null ? this.location.getGeoJSON() : this.getGeoinfo(),
-                this.getObservationHeading(),
-                this.getBroadcastMessage(),
-                this.getLocationIsPrivate(),
-                this.getIsPositive(),
-                this.getObservationData(),
-                observationDataSchema
+            this.getObservationId(),
+            this.userId,
+            this.user != null ? this.user.getFullName() : null,
+            this.getObservationTimeSeriesId(),
+            this.getTimeOfObservation(),
+            this.getOrganismId(),
+            ! this.getOrganism().getLocalName(locale).trim().isBlank() ? this.getOrganism().getLocalName(locale) : this.getOrganism().getLatinName(),
+            this.getCropOrganismId(),
+            ! this.getCropOrganism().getLocalName(locale).trim().isBlank() ? this.getCropOrganism().getLocalName(locale) : this.getCropOrganism().getLatinName(),
+            this.observationTimeSeries != null ? this.observationTimeSeries.getLabel() : null,
+            // Specific geoInfo trumps location. This is to be interpreted
+            // as that the observation has been geographically masked by
+            // choice of the observer
+            this.location != null ? this.location.getPointOfInterestId() : null,
+            this.location != null ? this.location.getName() : null,
+            this.location != null && this.geoinfo == null ? this.location.getGeoJSON() : this.getGeoinfo(),
+            this.getObservationHeading(),
+            this.getObservationText(),
+            this.getBroadcastMessage(),
+            this.getLocationIsPrivate(),
+            this.getIsPositive(),
+            this.getObservationData(),
+            observationDataSchema
         );
     }
 
     @Temporal(TemporalType.TIMESTAMP)
     @Column(name = "last_edited_time")
-	public Date getLastEditedTime() {
-		return lastEditedTime; 
-	}
+    public Date getLastEditedTime() {
+        return lastEditedTime;
+    }
 
-	public void setLastEditedTime(Date lastEditedTime) {
-		this.lastEditedTime = lastEditedTime;
-	}
+    public void setLastEditedTime(Date lastEditedTime) {
+        this.lastEditedTime = lastEditedTime;
+    }
 
     @Column(name = "is_positive")
     public Boolean getIsPositive() {
diff --git a/src/main/java/no/nibio/vips/logic/entity/VipsLogicUser.java b/src/main/java/no/nibio/vips/logic/entity/VipsLogicUser.java
index c8d96e9477c78f3cac0cb482c7168e210f084eb1..27102b4dd8893a1bc2857c365a4cec3b6a7f6446 100755
--- a/src/main/java/no/nibio/vips/logic/entity/VipsLogicUser.java
+++ b/src/main/java/no/nibio/vips/logic/entity/VipsLogicUser.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015-2019 NIBIO <http://www.nibio.no/>. 
+ * Copyright (c) 2015-2019 NIBIO <http://www.nibio.no/>.
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as published by
@@ -63,7 +63,7 @@ import java.util.UUID;
     @NamedQuery(name = "VipsLogicUser.findByCompletePhoneNumber", query = "SELECT v FROM VipsLogicUser v WHERE v.phoneCountryCode || v.phone = :completePhoneNumber")
 })
 public class VipsLogicUser implements Serializable, Comparable{
-    
+
     private static final long serialVersionUID = 1L;
     private Integer userId;
     //if the field contains email address consider using this annotation to enforce field validation
@@ -93,7 +93,7 @@ public class VipsLogicUser implements Serializable, Comparable{
     private Integer vipsCoreUserId;
     private boolean approvesSmsBilling;
     private boolean freeSms;
-    
+
     private UUID userUuid;
 
     public VipsLogicUser() {
@@ -143,6 +143,12 @@ public class VipsLogicUser implements Serializable, Comparable{
         this.lastName = lastName;
     }
 
+    @JsonIgnore
+    @Transient
+    public String getFullName() {
+        return firstName + " " + lastName;
+    }
+
     @OneToMany(cascade = CascadeType.ALL, mappedBy = "vipsLogicUser", fetch=FetchType.EAGER)
     @XmlTransient
     @JsonIgnore
@@ -160,7 +166,7 @@ public class VipsLogicUser implements Serializable, Comparable{
     public Organization getOrganizationId() {
         return organizationId;
     }
-    
+
     @Transient
     public Integer getOrganization_id(){
         return organizationId.getOrganizationId();
@@ -249,11 +255,11 @@ public class VipsLogicUser implements Serializable, Comparable{
     @ManyToMany(fetch = FetchType.EAGER)
     @JsonIgnore
     @JoinTable(
-            name = "user_vips_logic_role",
-            joinColumns = {
-                @JoinColumn(name = "user_id")},
-            inverseJoinColumns = {
-                @JoinColumn(name = "vips_logic_role_id")}
+        name = "user_vips_logic_role",
+        joinColumns = {
+            @JoinColumn(name = "user_id")},
+        inverseJoinColumns = {
+            @JoinColumn(name = "vips_logic_role_id")}
     )
     public Set<VipsLogicRole> getVipsLogicRoles() {
         return vipsLogicRoles;
@@ -287,7 +293,7 @@ public class VipsLogicUser implements Serializable, Comparable{
         }
         return false;
     }
-    
+
     @JsonIgnore
     @Transient
     public boolean isObservationAuthority(){
@@ -298,7 +304,7 @@ public class VipsLogicUser implements Serializable, Comparable{
         }
         return false;
     }
-    
+
     @JsonIgnore
     @Transient
     public boolean isOrganismEditor() {
@@ -309,7 +315,7 @@ public class VipsLogicUser implements Serializable, Comparable{
         }
         return false;
     }
-    
+
     @JsonIgnore
     @Transient
     public boolean isAppleFruitMothAdministrator(){
@@ -320,7 +326,7 @@ public class VipsLogicUser implements Serializable, Comparable{
         }
         return false;
     }
-    
+
     @JsonIgnore
     @Transient
     public boolean isMessageAuthor(){
@@ -499,7 +505,7 @@ public class VipsLogicUser implements Serializable, Comparable{
     public boolean isFreeSms() {
         return freeSms;
     }
-    
+
 
     /**
      * @param freeSms the freeSms to set
@@ -513,6 +519,6 @@ public class VipsLogicUser implements Serializable, Comparable{
         VipsLogicUser other = (VipsLogicUser)o;
         return (this.getLastName() + ", " + this.getFirstName()).compareTo(other.getLastName() + ", " + other.getFirstName());
     }
-    
-    
+
+
 }
diff --git a/src/main/java/no/nibio/vips/logic/entity/rest/ObservationListItem.java b/src/main/java/no/nibio/vips/logic/entity/rest/ObservationListItem.java
index a4dd12f901526bfda95fb6c5edadc1c793218772..95d83c07dfcc4494d8fbc4022afebdff8fbb58aa 100644
--- a/src/main/java/no/nibio/vips/logic/entity/rest/ObservationListItem.java
+++ b/src/main/java/no/nibio/vips/logic/entity/rest/ObservationListItem.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2018 NIBIO <http://www.nibio.no/>. 
+ * Copyright (c) 2018 NIBIO <http://www.nibio.no/>.
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as published by
@@ -28,11 +28,16 @@ import no.nibio.vips.observationdata.ObservationDataSchema;
  */
 public class ObservationListItem implements Comparable{
     private Integer observationId, observationTimeSeriesId, organismId, cropOrganismId;
+    private Integer observerId;
+    private String observerName;
     private Date timeOfObservation;
     private String organismName, cropOrganismName;
     private String observationTimeSeriesLabel;
+    private Integer locationPointOfInterestId;
+    private String locationPointOfInterestName;
     private String geoInfo;
     private String observationHeading;
+    private String observationText;
     private String observationData;
     private ObservationDataSchema observationDataSchema;
     private Boolean broadcastMessage;
@@ -40,23 +45,29 @@ public class ObservationListItem implements Comparable{
     private Boolean isPositive;
 
     public ObservationListItem(
-            Integer observationId,
-            Integer observationTimeSeriesId,
-            Date timeOfObservation,
-            Integer organismId,
-            String organismName,
-            Integer cropOrganismId,
-            String cropOrganismName,
-            String observationTimeSeriesLabel,
-            String geoinfo,
-            String observationHeading,
-            Boolean broadcastMessage,
-            Boolean locationIsPrivate,
-            Boolean isPositive,
-            String observationData,
-            ObservationDataSchema observationDataSchema
-    ){
+        Integer observationId,
+        Integer observerId,
+        String observerName,
+        Integer observationTimeSeriesId,
+        Date timeOfObservation,
+        Integer organismId,
+        String organismName,
+        Integer cropOrganismId,
+        String cropOrganismName,
+        String observationTimeSeriesLabel,
+        Integer poiId,
+        String poiName,
+        String geoinfo,
+        String observationHeading,
+        String observationText,
+        Boolean broadcastMessage,
+        Boolean locationIsPrivate,
+        Boolean isPositive,
+        String observationData,
+        ObservationDataSchema observationDataSchema){
         this.observationId = observationId;
+        this.observerId = observerId;
+        this.observerName = observerName;
         this.observationTimeSeriesId = observationTimeSeriesId;
         this.timeOfObservation = timeOfObservation;
         this.organismId = organismId;
@@ -64,15 +75,18 @@ public class ObservationListItem implements Comparable{
         this.cropOrganismId = cropOrganismId;
         this.cropOrganismName = cropOrganismName;
         this.observationTimeSeriesLabel = observationTimeSeriesLabel;
+        this.locationPointOfInterestId = poiId;
+        this.locationPointOfInterestName = poiName;
         this.geoInfo = geoinfo;
         this.observationHeading = observationHeading;
+        this.observationText = observationText;
         this.broadcastMessage = broadcastMessage;
         this.locationIsPrivate = locationIsPrivate;
         this.isPositive = isPositive;
         this.observationData = observationData;
         this.observationDataSchema = observationDataSchema;
     }
-    
+
 
     @Override
     public int compareTo(Object otherOne)
@@ -83,7 +97,7 @@ public class ObservationListItem implements Comparable{
         }
         return this.getTimeOfObservation().compareTo(((ObservationListItem) otherOne).getTimeOfObservation());
     }
-    
+
     /**
      * @return the observationId
      */
@@ -98,6 +112,34 @@ public class ObservationListItem implements Comparable{
         this.observationId = observationId;
     }
 
+    /**
+     * @return The ID of the observer
+     */
+    public Integer getObserverId() {
+        return observerId;
+    }
+
+    /**
+     * @param observerId The ID to set
+     */
+    public void setObserverId(Integer observerId) {
+        this.observerId = observerId;
+    }
+
+    /**
+     * @return The full name of the observer
+     */
+    public String getObserverName() {
+        return observerName;
+    }
+
+    /**
+     * @param observerName The observer name to set
+     */
+    public void setObserverName(String observerName) {
+        this.observerName = observerName;
+    }
+
     /**
      * @return the observationTimeSeriesId
      */
@@ -162,6 +204,22 @@ public class ObservationListItem implements Comparable{
         this.observationTimeSeriesLabel = observationTimeSeriesLabel;
     }
 
+    public Integer getLocationPointOfInterestId() {
+        return locationPointOfInterestId;
+    }
+
+    public void setLocationPointOfInterestId(Integer locationPointOfInterestId) {
+        this.locationPointOfInterestId = locationPointOfInterestId;
+    }
+
+    public String getLocationPointOfInterestName() {
+        return locationPointOfInterestName;
+    }
+
+    public void setLocationPointOfInterestName(String locationPointOfInterestName) {
+        this.locationPointOfInterestName = locationPointOfInterestName;
+    }
+
     /**
      * @return the geoInfo
      */
@@ -190,6 +248,20 @@ public class ObservationListItem implements Comparable{
         this.observationHeading = observationHeading;
     }
 
+    /**
+     * @return the observation text
+     */
+    public String getObservationText() {
+        return observationText;
+    }
+
+    /**
+     * @param observationText The observation text to set
+     */
+    public void setObservationText(String observationText) {
+        this.observationText = observationText;
+    }
+
     /**
      * @return the organismId
      */
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 daf359258690e26b9c4a7393a5b2ce1e945554c3..75d4ac8ba5def7edab317ebfe851561a08c28b84 100755
--- a/src/main/java/no/nibio/vips/logic/service/ObservationService.java
+++ b/src/main/java/no/nibio/vips/logic/service/ObservationService.java
@@ -32,6 +32,7 @@ import no.nibio.vips.logic.entity.rest.ObservationListItem;
 import no.nibio.vips.logic.entity.rest.PointMappingResponse;
 import no.nibio.vips.logic.entity.rest.ReferencedPoint;
 import no.nibio.vips.logic.messaging.MessagingBean;
+import no.nibio.vips.logic.util.ExcelFileGenerator;
 import no.nibio.vips.logic.util.GISEntityUtil;
 import no.nibio.vips.logic.util.Globals;
 import org.jboss.resteasy.annotations.GZIP;
@@ -59,6 +60,8 @@ import java.net.URI;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
 import java.util.stream.Collectors;
 
@@ -108,34 +111,35 @@ public class ObservationService {
     @Produces("application/json;charset=UTF-8")
     @TypeHint(Observation[].class)
     public Response getFilteredObservations(
-            @PathParam("organizationId") Integer organizationId,
-            @QueryParam("observationTimeSeriesId") Integer observationTimeSeriesId,
-            @QueryParam("pestId") Integer pestId,
-            @QueryParam("cropId") Integer cropId,
-            @QueryParam("cropCategoryId") List<Integer> cropCategoryId,
-            @QueryParam("from") String fromStr,
-            @QueryParam("to") String toStr,
-            @QueryParam("isPositive") Boolean isPositive
+        @PathParam("organizationId") Integer organizationId,
+        @QueryParam("observationTimeSeriesId") Integer observationTimeSeriesId,
+        @QueryParam("pestId") Integer pestId,
+        @QueryParam("cropId") Integer cropId,
+        @QueryParam("cropCategoryId") List<Integer> cropCategoryId,
+        @QueryParam("from") String fromStr,
+        @QueryParam("to") String toStr,
+        @QueryParam("isPositive") Boolean isPositive
     ) {
         return Response.ok().entity(getFilteredObservationsFromBackend(
-                organizationId,
-                observationTimeSeriesId,
-                pestId,
-                cropId,
-                cropCategoryId,
-                fromStr,
-                toStr,
-                isPositive
+            organizationId,
+            observationTimeSeriesId,
+            pestId,
+            cropId,
+            cropCategoryId,
+            fromStr,
+            toStr,
+            isPositive
         )).build();
     }
 
     /**
-     * @param organizationId Database ID of the organization
-     * @param pestId         Database ID of the pest
-     * @param cropId         Database ID of the crop
-     * @param cropCategoryId cropCategoryId Database IDs of the crop category/categories
-     * @param fromStr        format "yyyy-MM-dd"
-     * @param toStr          format "yyyy-MM-dd"
+     * @param organizationId          Database ID of the organization
+     * @param observationTimeSeriesId Database ID of the observation time series
+     * @param pestId                  Database ID of the pest
+     * @param cropId                  Database ID of the crop
+     * @param cropCategoryId          cropCategoryId Database IDs of the crop category/categories
+     * @param fromStr                 format "yyyy-MM-dd"
+     * @param toStr                   format "yyyy-MM-dd"
      * @return Observation objects for which the user is authorized to observe with properties relevant for lists
      */
     @GET
@@ -144,59 +148,105 @@ public class ObservationService {
     @Produces("application/json;charset=UTF-8")
     @TypeHint(ObservationListItem.class)
     public Response getFilteredObservationListItemsAsJson(
-            @PathParam("organizationId") Integer organizationId,
-            @QueryParam("observationTimeSeriesId") Integer observationTimeSeriesId,
-            @QueryParam("pestId") Integer pestId,
-            @QueryParam("cropId") Integer cropId,
-            @QueryParam("cropCategoryId") List<Integer> cropCategoryId,
-            @QueryParam("from") String fromStr,
-            @QueryParam("to") String toStr,
-            @QueryParam("userUUID") String userUUID,
-            @QueryParam("locale") String localeStr,
-            @QueryParam("isPositive") Boolean isPositive
+        @PathParam("organizationId") Integer organizationId,
+        @QueryParam("observationTimeSeriesId") Integer observationTimeSeriesId,
+        @QueryParam("pestId") Integer pestId,
+        @QueryParam("cropId") Integer cropId,
+        @QueryParam("cropCategoryId") List<Integer> cropCategoryId,
+        @QueryParam("from") String fromStr,
+        @QueryParam("to") String toStr,
+        @QueryParam("userUUID") String userUUID,
+        @QueryParam("locale") String localeStr,
+        @QueryParam("isPositive") Boolean isPositive
     ) {
         return Response.ok().entity(this.getFilteredObservationListItems(organizationId, observationTimeSeriesId, pestId, cropId, cropCategoryId, fromStr, toStr, userUUID, localeStr, isPositive)).build();
     }
 
-    private List<ObservationListItem> getFilteredObservationListItems(
-            Integer organizationId,
-            Integer observationTimeSeriesId,
-            Integer pestId,
-            Integer cropId,
-            List<Integer> cropCategoryId,
-            String fromStr,
-            String toStr,
-            String userUUID,
-            String localeStr,
-            Boolean isPositive) {
-        VipsLogicUser user = (VipsLogicUser) httpServletRequest.getSession().getAttribute("user");
+    /**
+     * @param organizationId          Database ID of the organization
+     * @param observationTimeSeriesId Database ID of the observation time series
+     * @param pestId                  Database ID of the pest
+     * @param cropId                  Database ID of the crop
+     * @param cropCategoryId          cropCategoryId Database IDs of the crop category/categories
+     * @param fromStr                 format "yyyy-MM-dd"
+     * @param toStr                   format "yyyy-MM-dd"
+     * @return Observation objects for which the user is authorized to observe with properties relevant for lists
+     */
+    @GET
+    @Path("list/filter/{organizationId}/xlsx")
+    @GZIP
+    @Produces("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
+    public Response getFilteredObservationListItemsAsXlsx(
+        @PathParam("organizationId") Integer organizationId,
+        @QueryParam("observationTimeSeriesId") Integer observationTimeSeriesId,
+        @QueryParam("pestId") Integer pestId,
+        @QueryParam("cropId") Integer cropId,
+        @QueryParam("cropCategoryId") List<Integer> cropCategoryId,
+        @QueryParam("from") String fromStr,
+        @QueryParam("to") String toStr,
+        @QueryParam("userUUID") String userUUID,
+        @QueryParam("locale") String localeStr,
+        @QueryParam("isPositive") Boolean isPositive
+    ) {
+        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 {} from {} to {}", user != null ? user.getUserId() : "unregistered", fromStr, toStr);
 
-        if (user == null && userUUID != null) {
-            user = userBean.findVipsLogicUser(UUID.fromString(userUUID));
+        LocalDateTime now = LocalDateTime.now();
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
+        String filenameTimestamp = now.format(formatter);
+
+        try {
+            List<ObservationListItem> observations = getFilteredObservationListItems(organizationId, observationTimeSeriesId, pestId, cropId, cropCategoryId, fromStr, toStr, userUUID, localeStr, isPositive);
+            byte[] excelFile = ExcelFileGenerator.generateExcel(user, locale, now, fromStr, toStr, observations);
+
+            return Response
+                .ok(excelFile)
+                .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();
         }
+    }
+
+    private List<ObservationListItem> getFilteredObservationListItems(
+        Integer organizationId,
+        Integer observationTimeSeriesId,
+        Integer pestId,
+        Integer cropId,
+        List<Integer> cropCategoryId,
+        String fromStr,
+        String toStr,
+        String userUUID,
+        String localeStr,
+        Boolean isPositive) {
+        VipsLogicUser user = getVipsLogicUser(userUUID);
         ULocale locale = new ULocale(localeStr != null ? localeStr :
-                user != null ? user.getOrganizationId().getDefaultLocale() :
-                        userBean.getOrganization(organizationId).getDefaultLocale());
+            user != null ? user.getOrganizationId().getDefaultLocale() :
+                userBean.getOrganization(organizationId).getDefaultLocale());
 
         LOGGER.info("Get filtered observations for user {}", user != null ? user.getUserId() : "<no user>");
         List<ObservationListItem> observations = getFilteredObservationsFromBackend(
-                organizationId,
-                observationTimeSeriesId,
-                pestId,
-                cropId,
-                cropCategoryId,
-                fromStr,
-                toStr,
-                isPositive,
-                user
+            organizationId,
+            observationTimeSeriesId,
+            pestId,
+            cropId,
+            cropCategoryId,
+            fromStr,
+            toStr,
+            isPositive,
+            user
         ).stream().map(obs -> {
             try {
                 return obs.getListItem(locale.getLanguage(),
-                        observationBean.getLocalizedObservationDataSchema(
-                                observationBean.getObservationDataSchema(organizationId, obs.getOrganismId()),
-                                httpServletRequest,
-                                locale
-                        )
+                    observationBean.getLocalizedObservationDataSchema(
+                        observationBean.getObservationDataSchema(organizationId, obs.getOrganismId()),
+                        httpServletRequest,
+                        locale
+                    )
                 );
             } catch (IOException e) {
                 LOGGER.error("Exception when getting localized observation data schema for observation " + obs.getObservationId(), e);
@@ -222,16 +272,16 @@ public class ObservationService {
     @Produces("text/csv;charset=UTF-8")
     @TypeHint(ObservationListItem.class)
     public Response getFilteredObservationListItemsAsCSV(
-            @PathParam("organizationId") Integer organizationId,
-            @QueryParam("observationTimeSeriesId") Integer observationTimeSeriesId,
-            @QueryParam("pestId") Integer pestId,
-            @QueryParam("cropId") Integer cropId,
-            @QueryParam("cropCategoryId") List<Integer> cropCategoryId,
-            @QueryParam("from") String fromStr,
-            @QueryParam("to") String toStr,
-            @QueryParam("userUUID") String userUUID,
-            @QueryParam("locale") String localeStr,
-            @QueryParam("isPositive") Boolean isPositive
+        @PathParam("organizationId") Integer organizationId,
+        @QueryParam("observationTimeSeriesId") Integer observationTimeSeriesId,
+        @QueryParam("pestId") Integer pestId,
+        @QueryParam("cropId") Integer cropId,
+        @QueryParam("cropCategoryId") List<Integer> cropCategoryId,
+        @QueryParam("from") String fromStr,
+        @QueryParam("to") String toStr,
+        @QueryParam("userUUID") String userUUID,
+        @QueryParam("locale") String localeStr,
+        @QueryParam("isPositive") Boolean isPositive
     ) {
         List<ObservationListItem> observations = this.getFilteredObservationListItems(organizationId, observationTimeSeriesId, pestId, cropId, cropCategoryId, fromStr, toStr, userUUID, localeStr, isPositive);
         Collections.sort(observations);
@@ -245,12 +295,12 @@ public class ObservationService {
                 c = ((Point) geometries.get(0)).getCoordinate();
             }
             retVal += "\n" + obs.getObservationId()
-                    + ";" + obs.getOrganismName()
-                    + ";" + obs.getCropOrganismName()
-                    + ";" + obs.getTimeOfObservation()
-                    + ";" + (c != null ? c.getY() + "," + c.getX() : "")
-                    + ";" + obs.getObservationHeading()
-                    + ";" + obs.getObservationData();
+                + ";" + obs.getOrganismName()
+                + ";" + obs.getCropOrganismName()
+                + ";" + obs.getTimeOfObservation()
+                + ";" + (c != null ? c.getY() + "," + c.getX() : "")
+                + ";" + obs.getObservationHeading()
+                + ";" + obs.getObservationData();
         }
         return Response.ok().entity(retVal).build();
     }
@@ -266,14 +316,14 @@ public class ObservationService {
      * @return Observation objects for which the user is authorized to observe with properties relevant for lists
      */
     private List<Observation> getFilteredObservationsFromBackend(
-            Integer organizationId,
-            Integer observationTimeSeriesId,
-            Integer pestId,
-            Integer cropId,
-            List<Integer> cropCategoryId,
-            String fromStr,
-            String toStr,
-            Boolean isPositive) {
+        Integer organizationId,
+        Integer observationTimeSeriesId,
+        Integer pestId,
+        Integer cropId,
+        List<Integer> cropCategoryId,
+        String fromStr,
+        String toStr,
+        Boolean isPositive) {
         SimpleDateFormat format = new SimpleDateFormat(Globals.defaultDateFormat);
         //TODO Set correct timeZone!!!
         Date from = null;
@@ -286,14 +336,14 @@ public class ObservationService {
         }
 
         return observationBean.getFilteredObservations(
-                organizationId,
-                observationTimeSeriesId,
-                pestId,
-                cropId,
-                cropCategoryId,
-                from,
-                to,
-                isPositive
+            organizationId,
+            observationTimeSeriesId,
+            pestId,
+            cropId,
+            cropCategoryId,
+            from,
+            to,
+            isPositive
         );
 
     }
@@ -411,14 +461,14 @@ public class ObservationService {
     @Produces("application/json;charset=UTF-8")
     @TypeHint(GeoJSON.class)
     public Response getFilteredObservationsAsGeoJSON(
-            @PathParam("organizationId") Integer organizationId,
-            @QueryParam("observationTimeSeriesId") Integer observationTimeSeriesId,
-            @QueryParam("pestId") Integer pestId,
-            @QueryParam("cropId") Integer cropId,
-            @QueryParam("cropCategoryId") List<Integer> cropCategoryId,
-            @QueryParam("from") String fromStr,
-            @QueryParam("to") String toStr,
-            @QueryParam("isPositive") Boolean isPositive
+        @PathParam("organizationId") Integer organizationId,
+        @QueryParam("observationTimeSeriesId") Integer observationTimeSeriesId,
+        @QueryParam("pestId") Integer pestId,
+        @QueryParam("cropId") Integer cropId,
+        @QueryParam("cropCategoryId") List<Integer> cropCategoryId,
+        @QueryParam("from") String fromStr,
+        @QueryParam("to") String toStr,
+        @QueryParam("isPositive") Boolean isPositive
 
     ) {
         SimpleDateFormat format = new SimpleDateFormat(Globals.defaultDateFormat);
@@ -433,14 +483,14 @@ public class ObservationService {
         }
 
         List<Observation> filteredObservations = this.getFilteredObservationsFromBackend(
-                organizationId,
-                observationTimeSeriesId,
-                pestId,
-                cropId,
-                cropCategoryId,
-                fromStr,
-                toStr,
-                isPositive
+            organizationId,
+            observationTimeSeriesId,
+            pestId,
+            cropId,
+            cropCategoryId,
+            fromStr,
+            toStr,
+            isPositive
         );
 
         GISEntityUtil gisUtil = new GISEntityUtil();
@@ -505,7 +555,7 @@ public class ObservationService {
     @Produces("application/json;charset=UTF-8")
     @TypeHint(Observation[].class)
     public Response getObservationsForUser(
-            @QueryParam("observationIds") String observationIds
+        @QueryParam("observationIds") String observationIds
     ) {
         LOGGER.info("getObservationsForUser for observationIds={}", observationIds);
         try {
@@ -516,14 +566,14 @@ public class ObservationService {
                 LOGGER.info("Found {} observations for user {}", allObs.size(), user.getUserId());
                 if (observationIds != null) {
                     Set<Integer> observationIdSet = Arrays.asList(observationIds.split(",")).stream()
-                            .map(s -> Integer.valueOf(s))
-                            .collect(Collectors.toSet());
+                        .map(s -> Integer.valueOf(s))
+                        .collect(Collectors.toSet());
                     return Response.ok().entity(
-                                    allObs.stream()
-                                            .filter(obs -> observationIdSet.contains(obs.getObservationId()))
-                                            .collect(Collectors.toList())
-                            )
-                            .build();
+                            allObs.stream()
+                                .filter(obs -> observationIdSet.contains(obs.getObservationId()))
+                                .collect(Collectors.toList())
+                        )
+                        .build();
                 }
                 return Response.ok().entity(allObs).build();
             } else {
@@ -550,7 +600,7 @@ public class ObservationService {
         VipsLogicUser user = userBean.getUserFromUUID(httpServletRequest);
         if (user != null) {
             return Response.ok().entity(observationBean.getObservationsForUser(user).stream()
-                    .map(obs -> new ObservationSyncInfo(obs)).collect(Collectors.toList())).build();
+                .map(obs -> new ObservationSyncInfo(obs)).collect(Collectors.toList())).build();
         } else {
             return Response.status(Status.UNAUTHORIZED).build();
         }
@@ -568,13 +618,13 @@ public class ObservationService {
     @Produces("application/json;charset=UTF-8")
     @TypeHint(Observation[].class)
     public Response getBroadcastObservations(
-            @PathParam("organizationId") Integer organizationId,
-            @QueryParam("season") Integer season,
-            @QueryParam("timeOfObservationFrom") String timeOfObservationFrom,
-            @QueryParam("timeOfObservationTo") String timeOfObservationTo
+        @PathParam("organizationId") Integer organizationId,
+        @QueryParam("season") Integer season,
+        @QueryParam("timeOfObservationFrom") String timeOfObservationFrom,
+        @QueryParam("timeOfObservationTo") String timeOfObservationTo
     ) {
         if ((timeOfObservationFrom != null && !timeOfObservationFrom.isEmpty())
-                || (timeOfObservationTo != null && !timeOfObservationTo.isEmpty())) {
+            || (timeOfObservationTo != null && !timeOfObservationTo.isEmpty())) {
             Date from = null;
             Date to = null;
             try {
@@ -606,8 +656,8 @@ public class ObservationService {
     @Produces("application/json;charset=UTF-8")
     @TypeHint(Observation.class)
     public Response getObservation(
-            @PathParam("observationId") Integer observationId,
-            @QueryParam("userUUID") String userUUID
+        @PathParam("observationId") Integer observationId,
+        @QueryParam("userUUID") String userUUID
     ) {
         // Observation needs to be masked here as well, or does it create trouble for VIPSLogic observation admin?
         Observation o = observationBean.getObservation(observationId);
@@ -636,9 +686,9 @@ public class ObservationService {
                 List<Observation> intermediary = new ArrayList<>();
                 intermediary.add(o);
                 intermediary = this.maskObservations(o.getPolygonService(),
-                        observationBean.getObservationsWithLocations(
-                                observationBean.getObservationsWithGeoInfo(intermediary)
-                        )
+                    observationBean.getObservationsWithLocations(
+                        observationBean.getObservationsWithGeoInfo(intermediary)
+                    )
                 );
                 o = intermediary.get(0);
             }
@@ -659,7 +709,7 @@ public class ObservationService {
     @Produces("application/json;charset=UTF-8")
     @TypeHint(PolygonService[].class)
     public Response getPolygonServicesForOrganization(
-            @PathParam("organizationId") Integer organizationId
+        @PathParam("organizationId") Integer organizationId
     ) {
         return Response.ok().entity(observationBean.getPolygonServicesForOrganization(organizationId)).build();
     }
@@ -679,10 +729,10 @@ public class ObservationService {
             return Response.status(Response.Status.UNAUTHORIZED).build();
         }
         if (!userBean.authorizeUser(user,
-                VipsLogicRole.OBSERVER,
-                VipsLogicRole.OBSERVATION_AUTHORITY,
-                VipsLogicRole.ORGANIZATION_ADMINISTRATOR,
-                VipsLogicRole.SUPERUSER
+            VipsLogicRole.OBSERVER,
+            VipsLogicRole.OBSERVATION_AUTHORITY,
+            VipsLogicRole.ORGANIZATION_ADMINISTRATOR,
+            VipsLogicRole.SUPERUSER
         )
         ) {
             return Response.status(Response.Status.FORBIDDEN).build();
@@ -712,10 +762,10 @@ public class ObservationService {
                 return Response.status(Response.Status.UNAUTHORIZED).build();
             }
             if (!userBean.authorizeUser(user,
-                    VipsLogicRole.OBSERVER,
-                    VipsLogicRole.OBSERVATION_AUTHORITY,
-                    VipsLogicRole.ORGANIZATION_ADMINISTRATOR,
-                    VipsLogicRole.SUPERUSER
+                VipsLogicRole.OBSERVER,
+                VipsLogicRole.OBSERVATION_AUTHORITY,
+                VipsLogicRole.ORGANIZATION_ADMINISTRATOR,
+                VipsLogicRole.SUPERUSER
             )
             ) {
                 return Response.status(Response.Status.FORBIDDEN).build();
@@ -744,7 +794,7 @@ public class ObservationService {
     public Response getFirstObservation(@PathParam("organismId") Integer organismId) {
         Date firstObsTime = observationBean.getFirstObservationTime(organismId);
         return firstObsTime != null ? Response.ok().entity(firstObsTime).build()
-                : Response.status(404).entity("No observations of organism with id=" + organismId).build();
+            : Response.status(404).entity("No observations of organism with id=" + organismId).build();
     }
 
     /**
@@ -766,26 +816,26 @@ public class ObservationService {
     }
 
     /**
-     * @param organizationId Database id of the organization
+     * @param organizationId          Database id of the organization
      * @param observationTimeSeriesId Database id of the observation time series
-     * @param pestId         Database id of the pest
-     * @param cropId         Database id of the crop
-     * @param cropCategoryId Database ids of the crop categories
-     * @param fromStr        format "yyyy-MM-dd"
-     * @param toStr          format "yyyy-MM-dd"
-     * @param user           The user that requests this (used for authorization)
+     * @param pestId                  Database id of the pest
+     * @param cropId                  Database id of the crop
+     * @param cropCategoryId          Database ids of the crop categories
+     * @param fromStr                 format "yyyy-MM-dd"
+     * @param toStr                   format "yyyy-MM-dd"
+     * @param user                    The user that requests this (used for authorization)
      * @return A list of observations that meets the filter criteria
      */
     private List<Observation> getFilteredObservationsFromBackend(
-            Integer organizationId,
-            Integer observationTimeSeriesId,
-            Integer pestId,
-            Integer cropId,
-            List<Integer> cropCategoryId,
-            String fromStr,
-            String toStr,
-            Boolean isPositive,
-            VipsLogicUser user
+        Integer organizationId,
+        Integer observationTimeSeriesId,
+        Integer pestId,
+        Integer cropId,
+        List<Integer> cropCategoryId,
+        String fromStr,
+        String toStr,
+        Boolean isPositive,
+        VipsLogicUser user
     ) {
         List<Observation> filteredObservations = this.getFilteredObservationsFromBackend(organizationId, observationTimeSeriesId, pestId, cropId, cropCategoryId, fromStr, toStr, isPositive);
 
@@ -803,10 +853,10 @@ public class ObservationService {
             LOGGER.info("Return {} masked public observations for unregistered user", retVal.size());
             return sortObservationsByDateAndId(retVal);
         }
-         // Else: This is a registered user without special privileges. Show public observations + user's own
+        // Else: This is a registered user without special privileges. Show public observations + user's own
         // Making sure we don't add duplicates
-        Set<Integer> obsIds = retVal.stream().map(o->o.getObservationId()).collect(Collectors.toSet());
-        retVal.addAll(observationBean.getObservationsForUser(user).stream().filter(o->!obsIds.contains(o.getObservationId())).collect(Collectors.toList()));
+        Set<Integer> obsIds = retVal.stream().map(o -> o.getObservationId()).collect(Collectors.toSet());
+        retVal.addAll(observationBean.getObservationsForUser(user).stream().filter(o -> !obsIds.contains(o.getObservationId())).collect(Collectors.toList()));
         LOGGER.info("Return {} masked public observations and user's own observations for registered user {}", retVal.size(), user.getUserId());
         return sortObservationsByDateAndId(retVal);
     }
@@ -839,7 +889,7 @@ public class ObservationService {
             Map<Integer, Observation> maskedObservations = new HashMap<>();
             registeredPolygonServicesInObservationList.keySet().forEach((pService) -> {
                 this.maskObservations(pService, registeredPolygonServicesInObservationList.get(pService))
-                        .forEach(o -> maskedObservations.put(o.getObservationId(), o));
+                    .forEach(o -> maskedObservations.put(o.getObservationId(), o));
             });
 
             // Adding the rest of the observations (the ones that don't need masking)
@@ -858,19 +908,19 @@ public class ObservationService {
         Client client = ClientBuilder.newClient();
         WebTarget target = client.target(polygonService.getGisSearchUrlTemplate());
         List<ReferencedPoint> points = observations.stream()
-                .filter(obs -> (obs.getGeoinfos() != null && !obs.getGeoinfos().isEmpty()) || obs.getLocation() != null)
-                .map(obs -> {
-                    ReferencedPoint rp = new ReferencedPoint();
-                    rp.setId(String.valueOf(obs.getObservationId()));
-                    if (obs.getGeoinfos() != null) {
-                        rp.setLon(obs.getGeoinfos().get(0).getGisGeom().getCoordinate().x);
-                        rp.setLat(obs.getGeoinfos().get(0).getGisGeom().getCoordinate().y);
-                    } else {
-                        rp.setLon(obs.getLocation().getLongitude());
-                        rp.setLat(obs.getLocation().getLatitude());
-                    }
-                    return rp;
-                }).collect(Collectors.toList());
+            .filter(obs -> (obs.getGeoinfos() != null && !obs.getGeoinfos().isEmpty()) || obs.getLocation() != null)
+            .map(obs -> {
+                ReferencedPoint rp = new ReferencedPoint();
+                rp.setId(String.valueOf(obs.getObservationId()));
+                if (obs.getGeoinfos() != null) {
+                    rp.setLon(obs.getGeoinfos().get(0).getGisGeom().getCoordinate().x);
+                    rp.setLat(obs.getGeoinfos().get(0).getGisGeom().getCoordinate().y);
+                } else {
+                    rp.setLon(obs.getLocation().getLongitude());
+                    rp.setLat(obs.getLocation().getLatitude());
+                }
+                return rp;
+            }).collect(Collectors.toList());
         /*System.out.println("maskobservations - target.request() about to be called");
         ObjectMapper oMapper = new ObjectMapper();
         try {
@@ -879,7 +929,7 @@ public class ObservationService {
             Logger.getLogger(ObservationService.class.getName()).log(Level.SEVERE, null, ex);
         }*/
         PointMappingResponse response = target.request(MediaType.APPLICATION_JSON)
-                .post(Entity.entity(points.toArray(new ReferencedPoint[points.size()]), MediaType.APPLICATION_JSON), PointMappingResponse.class);
+            .post(Entity.entity(points.toArray(new ReferencedPoint[points.size()]), MediaType.APPLICATION_JSON), PointMappingResponse.class);
         // We need to loop through the observations and find corresponding featurecollections and replace those
         Map<Integer, Feature> indexedPolygons = new HashMap<>();
         for (Feature feature : response.getFeatureCollection().getFeatures()) {
@@ -915,7 +965,7 @@ public class ObservationService {
     @Produces("application/json;charset=UTF-8")
     @TypeHint(Observation.class)
     public Response syncObservationFromApp(
-            String observationJson
+        String observationJson
     ) {
         LOGGER.info("In syncObservationFromApp");
 
@@ -1074,17 +1124,32 @@ public class ObservationService {
      */
     private List<Observation> sortObservationsByDateAndId(List<Observation> observations) {
         return observations.stream()
-                .sorted((o1, o2) -> {
-                    int timeCompare = o2.getTimeOfObservation().compareTo(o1.getTimeOfObservation());
-                    if (timeCompare != 0) {
-                        return timeCompare;
-                    } else {
-                        return Integer.compare(o2.getObservationId(), o1.getObservationId());
-                    }
-                })
-                .collect(Collectors.toList());
+            .sorted((o1, o2) -> {
+                int timeCompare = o2.getTimeOfObservation().compareTo(o1.getTimeOfObservation());
+                if (timeCompare != 0) {
+                    return timeCompare;
+                } else {
+                    return Integer.compare(o2.getObservationId(), o1.getObservationId());
+                }
+            })
+            .collect(Collectors.toList());
     }
 
+    /**
+     * Find VipsLogic user from session or given userUUID
+     *
+     * @param userUUID the UUID of the user
+     * @return the corresponding VipsLogicUser
+     */
+    private VipsLogicUser getVipsLogicUser(String userUUID) {
+        VipsLogicUser user = (VipsLogicUser) httpServletRequest.getSession().getAttribute("user");
+        if (user == null && userUUID != null) {
+            user = userBean.findVipsLogicUser(UUID.fromString(userUUID));
+        }
+        return user;
+    }
+
+
     /**
      * Utility method for getting the string value of a given property in a given map.
      *
diff --git a/src/main/java/no/nibio/vips/logic/util/ExcelFileGenerator.java b/src/main/java/no/nibio/vips/logic/util/ExcelFileGenerator.java
new file mode 100644
index 0000000000000000000000000000000000000000..242306b53b1674404bb49a9f6503cc17e18e8cf0
--- /dev/null
+++ b/src/main/java/no/nibio/vips/logic/util/ExcelFileGenerator.java
@@ -0,0 +1,441 @@
+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.VipsLogicUser;
+import no.nibio.vips.logic.entity.rest.ObservationListItem;
+import no.nibio.vips.observationdata.ObservationDataSchema;
+import org.apache.poi.common.usermodel.HyperlinkType;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.apache.poi.ss.usermodel.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+public final class ExcelFileGenerator {
+
+  private static final Logger LOGGER = LoggerFactory.getLogger(ExcelFileGenerator.class);
+  private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+  private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+  private static final ObjectMapper objectMapper = new ObjectMapper();
+
+  private static final String VIPSWEB = "https://www.vips-landbruk.no";
+  private static final String VIPSLOGIC = "https://logic.vips.nibio.no";
+
+  private enum ColumnIndex {
+    ID(false, 0, 0, "observationId"),
+    DATE(false, 1, 1, "timeOfObservation"),
+    POI_NAME(false, 2, 2, "location"),
+    OBSERVER_NAME(true, null, 3, "observer"),
+    OBSERVATION_TIME_SERIES_LABEL(false, 3, 4, "observationTimeSeriesLabel"),
+    ORGANISM(false, 4, 5, "organism"),
+    CROP_ORGANISM(false, 5, 6, "cropOrganismId"),
+    HEADING(false, 6, 7, "observationHeading"),
+    DESCRIPTION(false, 7, 8, "observationText"),
+    BROADCAST(false, 8, 9, "isBroadcast"),
+    POSITIVE(false, 9, 10, "isPositiveRegistration"),
+    INDEX_DATA(false, 10, 11, null);
+
+    private final boolean isSensitive;
+    private final Integer openIndex;
+    private final Integer adminIndex;
+    private final String rbKey;
+
+    ColumnIndex(boolean isSensitive, Integer openIndex, Integer adminIndex, String rbKey) {
+      this.isSensitive = isSensitive;
+      this.openIndex = openIndex;
+      this.adminIndex = adminIndex;
+      this.rbKey = rbKey;
+    }
+
+    public static List<ColumnIndex> forUser(boolean isAdmin) {
+      if (!isAdmin) {
+        return Arrays.stream(ColumnIndex.values()).filter(columnIndex -> !columnIndex.isSensitive).toList();
+      }
+      return Arrays.stream(ColumnIndex.values()).toList();
+    }
+
+    public String getColumnHeading(ResourceBundle rb) {
+      return rbKey != null && !rbKey.isBlank() ? rb.getString(rbKey) : "";
+    }
+
+    public Integer getIndex(boolean admin) {
+      if (admin) {
+        return adminIndex;
+      }
+      return openIndex;
+    }
+  }
+
+  public static byte[] generateExcel(VipsLogicUser user, ULocale locale, LocalDateTime now, String fromStr, String toStr, List<ObservationListItem> observations) throws IOException {
+    ResourceBundle rb = ResourceBundle.getBundle("no.nibio.vips.logic.i18n.vipslogictexts", locale.toLocale());
+    boolean isAdmin = user != null && (user.isSuperUser() || user.isOrganizationAdmin());
+    LOGGER.info("Create Excel file containing {} observations for {} user", observations.size(), isAdmin ? "admin" : "regular");
+    try (XSSFWorkbook workbook = new XSSFWorkbook();
+        ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+
+      Font font = workbook.createFont();
+      font.setBold(true);
+      CellStyle headerStyle = workbook.createCellStyle();
+      headerStyle.setFont(font);
+
+      // Create main sheet for all observations, with header row
+      Sheet mainSheet = workbook.createSheet(rb.getString("allObservations"));
+      createHeaderRow(isAdmin, mainSheet, headerStyle, rb);
+
+      int mainSheetRowIndex = 1;
+      // Add one row for each observation in list of all observations
+      for (ObservationListItem item : observations) {
+        createItemRow(isAdmin, mainSheet, mainSheetRowIndex++, item, rb);
+      }
+      autoSizeColumns(mainSheet, 0, ColumnIndex.INDEX_DATA.getIndex(isAdmin) - 1);
+
+      // Create meta sheet for information about the download
+      Sheet metaSheet = workbook.createSheet(rb.getString("downloadInfo"));
+      addMetaInfo(metaSheet, user, now, fromStr, toStr, observations, headerStyle, rb);
+
+      // Prepare list of observations for each type of pest
+      Map<Integer, List<ObservationListItem>> pestObservations = getObservationsForEachPest(observations);
+
+      // 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(sanitizeSheetName(pestId, pestName));
+        Row headerRow = createHeaderRow(isAdmin, pestSheet, headerStyle, rb);
+
+        // Add column titles for observation data
+        Map<String, String> dataColumnTitles = getObservationDataColumnTitles(firstObservationForPest.getObservationDataSchema());
+        int pestSheetColIndex = ColumnIndex.INDEX_DATA.getIndex(isAdmin);
+        for (String key : dataColumnTitles.keySet()) {
+          Cell cell = headerRow.createCell(pestSheetColIndex++);
+          cell.setCellStyle(headerStyle);
+          cell.setCellValue(dataColumnTitles.get(key));
+        }
+
+        int pestSheetRowIndex = 1;
+        for (ObservationListItem item : observationsForPest) {
+          Row row = createItemRow(isAdmin, pestSheet, pestSheetRowIndex++, item, rb);
+
+          if (item.getObservationData() != null) {
+            Map<String, Object> observationDataMap = objectMapper.readValue(item.getObservationData(), HashMap.class);
+            if (observationDataMap != null) {
+              pestSheetColIndex = ColumnIndex.INDEX_DATA.getIndex(isAdmin);
+              for (String key : dataColumnTitles.keySet()) {
+                pestSheetColIndex = addValueToCell(row, pestSheetColIndex, observationDataMap.get(key));
+              }
+            }
+          }
+        }
+        autoSizeColumns(pestSheet, ColumnIndex.ID.getIndex(isAdmin), ColumnIndex.INDEX_DATA.getIndex(isAdmin) + dataColumnTitles.size());
+      }
+
+      workbook.write(out);
+      return out.toByteArray();
+    }
+  }
+
+  /**
+   * Add meta information to given sheet
+   *
+   * @param metaSheet    The sheet in which to add content
+   * @param user         The current user
+   * @param now          The current timestamp
+   * @param fromStr      The start of the period for which we have observations
+   * @param toStr        The end of the period for which we have observations
+   * @param observations The list of observations
+   * @param headerStyle  How to style the title cells
+   * @param rb           Resource bundle with translations
+   */
+  private static void addMetaInfo(Sheet metaSheet, VipsLogicUser user, LocalDateTime now, String fromStr, String toStr, List<ObservationListItem> observations, CellStyle headerStyle, ResourceBundle rb) {
+    Row userRow = metaSheet.createRow(0);
+    Cell downloadedByCell = userRow.createCell(0);
+    downloadedByCell.setCellStyle(headerStyle);
+    downloadedByCell.setCellValue(rb.getString("downloadedBy"));
+    userRow.createCell(1).setCellValue(user != null ? user.getFullName() : rb.getString("unregisteredUser"));
+    Row timeRow = metaSheet.createRow(1);
+    Cell downloadedTimeCell = timeRow.createCell(0);
+    downloadedTimeCell.setCellStyle(headerStyle);
+    downloadedTimeCell.setCellValue(rb.getString("downloadedTime"));
+    timeRow.createCell(1).setCellValue(DATE_TIME_FORMATTER.format(now));
+    Row fromRow = metaSheet.createRow(2);
+    Cell dateFromCell = fromRow.createCell(0);
+    dateFromCell.setCellStyle(headerStyle);
+    dateFromCell.setCellValue(rb.getString("dateStart"));
+    fromRow.createCell(1).setCellValue(fromStr);
+    Row toRow = metaSheet.createRow(3);
+    Cell dateToCell = toRow.createCell(0);
+    dateToCell.setCellStyle(headerStyle);
+    dateToCell.setCellValue(rb.getString("dateEnd"));
+    toRow.createCell(1).setCellValue(toStr);
+    Row countRow = metaSheet.createRow(4);
+    Cell countCell = countRow.createCell(0);
+    countCell.setCellStyle(headerStyle);
+    countCell.setCellValue(rb.getString("observationCount"));
+    countRow.createCell(1).setCellValue(observations.size());
+    autoSizeColumns(metaSheet, 0, 1);
+  }
+
+  /**
+   * Create sheet name without invalid characters and within size limits
+   *
+   * @param pestId   The id of the pest
+   * @param pestName The name of the pest
+   * @return a sanitized string to be used as sheet name
+   */
+  private static String sanitizeSheetName(Integer pestId, String pestName) {
+    if (pestName == null || pestName.isBlank()) {
+      return "Id=" + pestId;
+    }
+    return pestName.replaceAll("[\\[\\]\\*/:?\\\\]", "_").substring(0, Math.min(pestName.length(), 31));
+  }
+
+
+  /**
+   * 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;
+  }
+
+  /**
+   * 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 first row of given sheet, with column titles dependent on user privileges
+   *
+   * @param isAdmin     Whether the user is a logged in admin
+   * @param sheet       The sheet to which a row will be added
+   * @param headerStyle The style to be applied to each cell in the header row
+   * @param rb          A resource bundle enabling localized messages
+   * @return the newly created header row
+   */
+  public static Row createHeaderRow(boolean isAdmin, Sheet sheet, CellStyle headerStyle, ResourceBundle rb) {
+    Row headerRow = sheet.createRow(0);
+    for (ColumnIndex columnIndex : ColumnIndex.forUser(isAdmin)) {
+      Cell cell = headerRow.createCell(columnIndex.getIndex(isAdmin));
+      cell.setCellStyle(headerStyle);
+      cell.setCellValue(columnIndex.getColumnHeading(rb));
+    }
+    return headerRow;
+  }
+
+  /**
+   * Create row with given index, for given observation list item
+   *
+   * @param isAdmin  Whether the user is a logged in admin
+   * @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(boolean isAdmin, Sheet sheet, int rowIndex, ObservationListItem item, ResourceBundle rb) throws JsonProcessingException {
+    LocalDate localDateOfObservation = item.getTimeOfObservation().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+    Row row = sheet.createRow(rowIndex);
+    addObservationLink(row, ColumnIndex.ID.getIndex(isAdmin), item.getObservationId());
+    addValueToCell(row, ColumnIndex.DATE.getIndex(isAdmin), localDateOfObservation.format(DATE_FORMATTER));
+
+    if (item.getLocationPointOfInterestId() != null) {
+      Integer poiNameIndex = ColumnIndex.POI_NAME.getIndex(isAdmin);
+      if(isAdmin) {
+        addPoiLink(row, poiNameIndex, item.getLocationPointOfInterestId(), item.getLocationPointOfInterestName());
+      } else {
+        addValueToCell(row, poiNameIndex, item.getLocationPointOfInterestName());
+      }
+    }
+
+    if (isAdmin) {
+      addUserLink(row, ColumnIndex.OBSERVER_NAME.getIndex(isAdmin), item.getObserverId(), item.getObserverName());
+    }
+    if (item.getObservationTimeSeriesId() != null) {
+      addTimeSeriesLink(row, ColumnIndex.OBSERVATION_TIME_SERIES_LABEL.getIndex(isAdmin), item.getObservationTimeSeriesId(), item.getObservationTimeSeriesLabel());
+    }
+    addValueToCell(row, ColumnIndex.ORGANISM.getIndex(isAdmin), item.getOrganismName());
+    addValueToCell(row, ColumnIndex.CROP_ORGANISM.getIndex(isAdmin), item.getCropOrganismName());
+    addValueToCell(row, ColumnIndex.HEADING.getIndex(isAdmin), item.getObservationHeading());
+    addValueToCell(row, ColumnIndex.DESCRIPTION.getIndex(isAdmin), item.getObservationText());
+
+    addValueToCell(row, ColumnIndex.BROADCAST.getIndex(isAdmin), getBooleanStringValue(rb, item.getBroadcastMessage()));
+    addValueToCell(row, ColumnIndex.POSITIVE.getIndex(isAdmin), getBooleanStringValue(rb, item.getIsPositive()));
+    return row;
+  }
+
+  /**
+   * Add value to cell
+   *
+   * @param row      The row to which the value should be added
+   * @param colIndex The index of the column to add the value to
+   * @param value    The value
+   * @return The next index value
+   */
+  private static Integer addValueToCell(Row row, Integer colIndex, Object value) {
+    Integer index = colIndex;
+    if (value == null) {
+      row.createCell(index++).setCellValue("");
+    } else if (value instanceof Number) {
+      row.createCell(index++).setCellValue(((Number) value).intValue());
+    } else {
+      try {
+        int intValue = Integer.parseInt(value.toString());
+        row.createCell(index++).setCellValue(intValue);
+      } catch (NumberFormatException e) {
+        row.createCell(index++).setCellValue(value.toString());
+      }
+    }
+    return index;
+  }
+
+  /**
+   * Get a localized String representing either true or false
+   *
+   * @param rb    The resource bundle to get the localized string from
+   * @param value Either true or false
+   * @return A localized String
+   */
+  private static String getBooleanStringValue(ResourceBundle rb, Boolean value) {
+    if (value == null) {
+      return null;
+    } else if (value) {
+      return rb.getString("yes");
+    }
+    return rb.getString("no");
+  }
+
+  /**
+   * Add link to observation details in column containing the observation Id
+   *
+   * @param row           A reference to the current row
+   * @param colIndex      The index of the column in which the link should be added
+   * @param observationId The id of the observation
+   */
+  private static void addObservationLink(Row row, Integer colIndex, Integer observationId) {
+    Cell cell = row.createCell(colIndex);
+    cell.setCellValue(observationId);
+
+    Workbook workbook = row.getSheet().getWorkbook();
+    cell.setHyperlink(createHyperlink(workbook, VIPSWEB + "/observations/" + observationId));
+    cell.setCellStyle(hyperlinkCellStyle(workbook));
+  }
+
+  /**
+   * Add link to timeseries details in column with given index
+   *
+   * @param row             A reference to the current row
+   * @param colIndex        The index of the column in which the link should be added
+   * @param timeSeriesId    The id of the timeseries
+   * @param timeSeriesLabel The text which should be displayed in the cell
+   */
+  private static void addTimeSeriesLink(Row row, Integer colIndex, Integer timeSeriesId, String timeSeriesLabel) {
+    Cell cell = row.createCell(colIndex);
+    cell.setCellValue(timeSeriesLabel);
+
+    Workbook workbook = row.getSheet().getWorkbook();
+    cell.setHyperlink(createHyperlink(workbook, VIPSWEB + "/observations/timeseries/" + timeSeriesId));
+    cell.setCellStyle(hyperlinkCellStyle(workbook));
+  }
+
+  /**
+   * Add link to poi details in column with given index
+   *
+   * @param row      A reference to the current row
+   * @param colIndex The index of the column in which the link should be added
+   * @param poiId    The id of the poi
+   * @param poiName  The text which should be displayed in the cell
+   */
+  private static void addPoiLink(Row row, Integer colIndex, Integer poiId, String poiName) {
+    Cell cell = row.createCell(colIndex);
+    cell.setCellValue(poiName);
+
+    Workbook workbook = row.getSheet().getWorkbook();
+    cell.setHyperlink(createHyperlink(workbook, VIPSLOGIC + "/weatherStation?pointOfInterestId=" + poiId));
+    cell.setCellStyle(hyperlinkCellStyle(workbook));
+  }
+
+  /**
+   * Add link to user details in column with given index
+   *
+   * @param row      A reference to the current row
+   * @param colIndex The index of the column in which the link should be added
+   * @param userId   The id of the user
+   * @param userName The text which should be displayed in the cell
+   */
+  private static void addUserLink(Row row, Integer colIndex, Integer userId, String userName) {
+    Cell cell = row.createCell(colIndex);
+    cell.setCellValue(userName);
+
+    Workbook workbook = row.getSheet().getWorkbook();
+    cell.setHyperlink(createHyperlink(workbook, VIPSLOGIC + "/user?action=viewUser&userId=" + userId));
+    cell.setCellStyle(hyperlinkCellStyle(workbook));
+  }
+
+  private static Hyperlink createHyperlink(Workbook workbook, String url) {
+    CreationHelper creationHelper = workbook.getCreationHelper();
+    Hyperlink hyperlink = creationHelper.createHyperlink(HyperlinkType.URL);
+    hyperlink.setAddress(url);
+    return hyperlink;
+  }
+
+  private static CellStyle hyperlinkCellStyle(Workbook workbook) {
+    CellStyle hyperlinkStyle = workbook.createCellStyle();
+    Font hlinkFont = workbook.createFont();
+    hlinkFont.setUnderline(Font.U_SINGLE);
+    hlinkFont.setColor(IndexedColors.BLUE.getIndex());
+    hyperlinkStyle.setFont(hlinkFont);
+    return hyperlinkStyle;
+  }
+
+}
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..532895e14fbd20b5fa9b871d174e0ab5b463168f 100755
--- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties
+++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties
@@ -1,21 +1,21 @@
 #Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
 #
- # Copyright (c) 2014 NIBIO <http://www.nibio.no/>. 
- # 
- # This file is part of VIPSLogic.
- # This program is free software: you can redistribute it and/or modify
- # it under the terms of the GNU Affero General Public License as published by
- # the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- # GNU Affero General Public License for more details.
- #
- # You should have received a copy of the GNU Affero General Public License
- # along with this program.  If not, see <https://www.gnu.org/licenses/>.
- # 
+# Copyright (c) 2014 NIBIO <http://www.nibio.no/>.
+#
+# This file is part of VIPSLogic.
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+#
 ALTERNARIA = Alternaria Model
 
 APPLESCABM = Apple scab model
@@ -391,7 +391,9 @@ isPest = Is pest
 isPrivate = Is private
 
 isQuantified = Is quantified
+
 isPositiveRegistration=Pest presence confirmed
+
 isRequiredField = Required field
 
 language = Language
@@ -556,6 +558,8 @@ observationDataField_trapCountCropInside = Number of trap counts inside the fiel
 
 observationDataField_unit = Measuring unit
 
+allObservations = All observations
+
 observationDeleted = Observation was deleted
 
 observationHeading = Observation heading
@@ -592,7 +596,7 @@ observationSiteStored = Observation site was successfully updated
 
 observationStored = Observation was stored
 
-observationText = Observation text
+observationText = Description
 
 observations = Observations
 
@@ -600,6 +604,8 @@ observedDateOfFirstCatch = Observed date of first catch
 
 observedValue = Observed value
 
+observerId = Observer ID
+
 observer = Observer
 
 older = Older
@@ -1052,3 +1058,15 @@ privacyStatement=Privacy statement
 privacyStatementFileName=Privacy_statement_NIBIO-VIPS.pdf
 thresholdDSVMax=DSV threshold for high infection risk
 thresholdDSVTempMin=Minimum temperature for DSV calculation
+observationTimeSeriesId=Timeseries
+observationTimeSeriesLabel=Timeseries label
+observationId=Observation
+isBroadcast=Is broadcast
+yes=Yes
+no=No
+downloadInfo=About download
+downloadedBy=Downloaded by
+unregisteredUser=Unregistered user
+downloadedTime=Time of download
+observationCount=Observation count
+
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 1e03844cc22e2c12fe721b2eb34d16f56e097f44..24728fee46f5f10c94ffa13944a6227f467095b6 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
@@ -1,20 +1,20 @@
 #
- # Copyright (c) 2014 NIBIO <http://www.nibio.no/>. 
- # 
- # This file is part of VIPSLogic.
- # This program is free software: you can redistribute it and/or modify
- # it under the terms of the GNU Affero General Public License as published by
- # the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- # GNU Affero General Public License for more details.
- #
- # You should have received a copy of the GNU Affero General Public License
- # along with this program.  If not, see <https://www.gnu.org/licenses/>.
- # 
+# Copyright (c) 2014 NIBIO <http://www.nibio.no/>.
+#
+# This file is part of VIPSLogic.
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+#
 isPositiveRegistration=Skadegj\u00f8rer p\u00e5vist
 ALTERNARIA = Alternariamodell
 
@@ -354,7 +354,7 @@ greeting = Velkommen til
 
 groupMembers = Gruppemedlemmer
 
-heading = Overskrift
+heading = Tittel
 
 help = Hjelp
 
@@ -420,13 +420,13 @@ listSelectedCropCategoryOnTop = List kulturer fra valgt gruppe \u00f8verst
 
 localName = Lokalt navn
 
-location = Plassering
+location=Sted
 
-locationIsPrivate = Lokalitet skal ikke offentliggj\u00f8res
+locationIsPrivate = Sted skal ikke offentliggj\u00f8res
 
-locationIsPublic = Lokaliteten kan vises offentlig
+locationIsPublic = Sted kan vises offentlig
 
-locationPointOfInterestId = Lokalitet
+locationPointOfInterestId=Sted
 
 logInterval = M\u00e5leintervall
 
@@ -556,9 +556,11 @@ observationDataField_trapCountCropInside = Antall insekter, fellefangst inne i f
 
 observationDataField_unit = M\u00e5leenhet
 
+allObservations = Alle observasjoner
+
 observationDeleted = Observasjonen ble slettet
 
-observationHeading = Observasjons-overskrift
+observationHeading = Tittel
 
 observationMap = Observasjonskart
 
@@ -592,7 +594,7 @@ observationSiteStored = Rogneb\u00e6rm\u00f8llstasjonen ble oppdatert
 
 observationStored = Observasjonen ble lagret
 
-observationText = Observasjonstekst
+observationText = Beskrivelse
 
 observations = Observasjoner/f\u00f8rstefunn
 
@@ -600,6 +602,8 @@ observedDateOfFirstCatch = Observert dato for f\u00f8rste fellefangst
 
 observedValue = Observert verdi
 
+observerId = Observat\u00f8r-Id
+
 observer = Observat\u00f8r
 
 older = Eldre
@@ -894,7 +898,7 @@ thresholdRelativeHumidity = Terskelverdi relativ luftfuktighet (%)
 
 tillageMethod = Jordarbeiding
 
-timeOfObservation = Observasjonstidspunkt
+timeOfObservation = Observasjonsdato
 
 timeZone = Tidssone
 
@@ -1030,6 +1034,7 @@ weatherStations = M\u00e5lestasjoner
 
 xIsNotAfterY = {0} er ikke etter {1}
 
+
 xIsNotEqualToY = {0} er ikke lik {1}
 
 your = Din
@@ -1052,3 +1057,15 @@ privacyStatement=Personvernerkl\u00e6ring
 privacyStatementFileName=Personvernerklaering_NIBIO-VIPS.pdf
 thresholdDSVMax=DSV-terskel for h\u00f8y infeksjonsrisiko
 thresholdDSVTempMin=Minimumstemperatur for beregning av DSV
+observationTimeSeriesId=Tidsserie-Id
+observationTimeSeriesLabel=Tidsserie
+observationId=Observasjon-Id
+isBroadcast=Er kringkastet
+yes=Ja
+no=Nei
+downloadInfo=Om nedlastingen
+downloadedBy=Lastet ned av
+unregisteredUser=Uregistrert bruker
+downloadedTime=Tidspunkt for nedlasting
+observationCount=Antall observasjoner
+
diff --git a/src/test/resources/HTMLFormGeneratorTest/forecastConfigurationForm_result.html b/src/test/resources/HTMLFormGeneratorTest/forecastConfigurationForm_result.html
index 92f82d74c599925b4fa6869a9f780a92c10e9558..4b2e97c64a691fd9ffe8d896aba1cf56feb0c8cb 100755
--- a/src/test/resources/HTMLFormGeneratorTest/forecastConfigurationForm_result.html
+++ b/src/test/resources/HTMLFormGeneratorTest/forecastConfigurationForm_result.html
@@ -17,7 +17,7 @@
 <span class="help-block" id="forecastConfigurationForm_modelId_validation"></span>
 </div>
 <div class="form-group">
-<label for="locationPointOfInterestId">Lokalitet</label>
+<label for="locationPointOfInterestId">Sted</label>
 <select class="form-control" name="locationPointOfInterestId" onblur="validateField(this);">
 <option value="-1">Vennligst velg målestasjon</option>
 <option value="38">Ullensvang</option>