diff --git a/src/main/java/org/olat/core/gui/components/form/flexible/impl/elements/table/StaticFlexiCellRenderer.java b/src/main/java/org/olat/core/gui/components/form/flexible/impl/elements/table/StaticFlexiCellRenderer.java
index f161fe9851753fa2406bb31b40b19a223e6c5e96..cd312f11f7187c3b781ecd13f58ac92c131098e5 100644
--- a/src/main/java/org/olat/core/gui/components/form/flexible/impl/elements/table/StaticFlexiCellRenderer.java
+++ b/src/main/java/org/olat/core/gui/components/form/flexible/impl/elements/table/StaticFlexiCellRenderer.java
@@ -51,6 +51,7 @@ public class StaticFlexiCellRenderer implements FlexiCellRenderer, ActionDelegat
 	private String iconRightCSS;
 	private String linkCSS;
 	private String linkTitle;
+	private boolean push = false;
 	private boolean newWindow = false;
 	private boolean dirtyCheck = true;
 	private FlexiCellRenderer labelDelegate;
@@ -129,6 +130,14 @@ public class StaticFlexiCellRenderer implements FlexiCellRenderer, ActionDelegat
 		this.action = action;
 	}
 	
+	public boolean isPush() {
+		return push;
+	}
+
+	public void setPush(boolean push) {
+		this.push = push;
+	}
+
 	@Override
 	public List<String> getActions() {
 		if(StringHelper.containsNonWhitespace(action)) {
@@ -190,7 +199,7 @@ public class StaticFlexiCellRenderer implements FlexiCellRenderer, ActionDelegat
 				if(!StringHelper.containsNonWhitespace(href)) {
 					href = "javascript:;";
 				}
-				String jsCode = FormJSHelper.getXHRFnCallFor(rootForm, id, 1, dirtyCheck, true, false, pair);
+				String jsCode = FormJSHelper.getXHRFnCallFor(rootForm, id, 1, dirtyCheck, true, push, pair);
 				target.append("<a href=\"").append(href).append("\" onclick=\"").append(jsCode).append("; return false;\"");
 			}
 			
diff --git a/src/main/java/org/olat/course/certificate/_spring/certificateContext.xml b/src/main/java/org/olat/course/certificate/_spring/certificateContext.xml
index 23558cb5126cb6c8c09e29f85e7bc3454c98e812..cc5d67c80d586523f94c6884214026eaee64bf4d 100644
--- a/src/main/java/org/olat/course/certificate/_spring/certificateContext.xml
+++ b/src/main/java/org/olat/course/certificate/_spring/certificateContext.xml
@@ -1,9 +1,33 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <beans xmlns="http://www.springframework.org/schema/beans"
 	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-	xsi:schemaLocation="http://www.springframework.org/schema/beans 
-                        http://www.springframework.org/schema/beans/spring-beans.xsd">
+	xmlns:context="http://www.springframework.org/schema/context" 
+	xsi:schemaLocation="
+  http://www.springframework.org/schema/beans 
+  http://www.springframework.org/schema/beans/spring-beans.xsd
+  http://www.springframework.org/schema/context 
+  http://www.springframework.org/schema/context/spring-context.xsd">
                         
 	<import resource="classpath:org/olat/course/certificate/_spring/certificateJms_${jms.provider}.xml" />
+	
+	<!-- Certificates report panel -->
+	<bean class="org.olat.core.extensions.action.GenericActionExtension" init-method="initExtensionPoints">
+		<property name="order" value="8211" />
+		<property name="actionController">	
+			<bean class="org.olat.core.gui.control.creator.AutoCreator" scope="prototype">
+				<property name="className" value="org.olat.course.certificate.ui.report.CertificatesReportController"/>
+			</bean>
+		</property>
+		<property name="navigationKey" value="reportCertificates" />
+		<property name="i18nActionKey" value="admin.menu.report.certificates.title"/>
+		<property name="i18nDescriptionKey" value="admin.menu.report.certificates.title.alt"/>
+		<property name="translationPackage" value="org.olat.course.certificate.ui.report"/>
+		<property name="parentTreeNodeIdentifier" value="reportsParent" /> 
+		<property name="extensionPoints">
+			<list>	
+				<value>org.olat.admin.SystemAdminMainController</value>		
+			</list>
+		</property>
+	</bean>
                         
 </beans>
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/certificate/model/AbstractCertificate.java b/src/main/java/org/olat/course/certificate/model/AbstractCertificate.java
index 7a27e75093bb274c7969020d85b340ff482922f8..a98644e1ab945e632baf5eaa966503e6e3b5a792 100644
--- a/src/main/java/org/olat/course/certificate/model/AbstractCertificate.java
+++ b/src/main/java/org/olat/course/certificate/model/AbstractCertificate.java
@@ -182,10 +182,12 @@ public abstract class AbstractCertificate implements Certificate, Persistable {
 		this.last = last;
 	}
 
+	@Override
 	public Date getNextRecertificationDate() {
 		return nextRecertificationDate;
 	}
 
+	@Override
 	public void setNextRecertificationDate(Date nextRecertificationDate) {
 		this.nextRecertificationDate = nextRecertificationDate;
 	}
diff --git a/src/main/java/org/olat/course/certificate/ui/UploadCertificateController.java b/src/main/java/org/olat/course/certificate/ui/UploadCertificateController.java
index 05054d12c7c4a2de6c2e30f32b4fea14906e1ffb..94f86630edf7cafe0073b0864f9e4a8ca0f71643 100644
--- a/src/main/java/org/olat/course/certificate/ui/UploadCertificateController.java
+++ b/src/main/java/org/olat/course/certificate/ui/UploadCertificateController.java
@@ -30,7 +30,6 @@ import java.nio.file.SimpleFileVisitor;
 import java.nio.file.attribute.BasicFileAttributes;
 import java.util.List;
 
-import org.apache.commons.io.IOUtils;
 import org.apache.pdfbox.pdmodel.PDDocument;
 import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
 import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
@@ -210,9 +209,9 @@ public class UploadCertificateController extends FormBasicController {
 	private boolean validatePdf(File template) {
 		boolean allOk = true;
 		
-		PDDocument document = null;
-		try (InputStream in = Files.newInputStream(template.toPath())) {		
-			document = PDDocument.load(in);
+		try (InputStream in = Files.newInputStream(template.toPath());
+				PDDocument document = PDDocument.load(in)) {		
+			
 			if (document.isEncrypted()) {
 				fileEl.setErrorKey("upload.error.encrypted", null);
 				allOk &= false;
@@ -221,7 +220,6 @@ public class UploadCertificateController extends FormBasicController {
 				PDDocumentCatalog docCatalog = document.getDocumentCatalog();
 				PDAcroForm acroForm = docCatalog.getAcroForm();
 				if (acroForm != null) {
-					@SuppressWarnings("unchecked")
 					List<PDField> fields = acroForm.getFields();
 					for(PDField field:fields) {
 						field.setValue("test");
@@ -241,8 +239,6 @@ public class UploadCertificateController extends FormBasicController {
 			logError("", ex);
 			fileEl.setErrorKey("upload.unkown.error", null);
 			allOk &= false;
-		} finally {
-			IOUtils.closeQuietly(document);
 		}
 		
 		return allOk;
diff --git a/src/main/java/org/olat/course/certificate/ui/report/CertificatesReportController.java b/src/main/java/org/olat/course/certificate/ui/report/CertificatesReportController.java
new file mode 100644
index 0000000000000000000000000000000000000000..c66326e3197b8a0c1ad069cff4afe977ceec01cf
--- /dev/null
+++ b/src/main/java/org/olat/course/certificate/ui/report/CertificatesReportController.java
@@ -0,0 +1,239 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.course.certificate.ui.report;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+
+import org.olat.NewControllerFactory;
+import org.olat.commons.calendar.CalendarUtils;
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.form.flexible.FormItem;
+import org.olat.core.gui.components.form.flexible.FormItemContainer;
+import org.olat.core.gui.components.form.flexible.elements.DateChooser;
+import org.olat.core.gui.components.form.flexible.elements.FlexiTableElement;
+import org.olat.core.gui.components.form.flexible.elements.FormLink;
+import org.olat.core.gui.components.form.flexible.elements.MultipleSelectionElement;
+import org.olat.core.gui.components.form.flexible.elements.TextElement;
+import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
+import org.olat.core.gui.components.form.flexible.impl.FormEvent;
+import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.DefaultFlexiColumnModel;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiColumnModel;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableColumnModel;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.FlexiTableDataModelFactory;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.SelectionEvent;
+import org.olat.core.gui.components.form.flexible.impl.elements.table.StaticFlexiCellRenderer;
+import org.olat.core.gui.components.link.Link;
+import org.olat.core.gui.control.Controller;
+import org.olat.core.gui.control.WindowControl;
+import org.olat.core.gui.media.MediaResource;
+import org.olat.core.util.Formatter;
+import org.olat.core.util.StringHelper;
+import org.olat.core.util.Util;
+import org.olat.course.certificate.ui.report.CertificatesReportParameters.With;
+import org.olat.ims.qti21.ui.AssessmentTestDisplayController;
+import org.olat.repository.RepositoryEntry;
+import org.olat.repository.RepositoryManager;
+import org.olat.repository.RepositoryService;
+import org.olat.repository.model.SearchRepositoryEntryParameters;
+import org.olat.repository.ui.RepositoryEntryACColumnDescriptor;
+import org.olat.repository.ui.RepositoryFlexiTableModel;
+import org.olat.repository.ui.RepositoryFlexiTableModel.RepoCols;
+import org.olat.repository.ui.author.TypeRenderer;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * 
+ * Initial date: 24 juin 2020<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class CertificatesReportController extends FormBasicController {
+	
+	private static final String[] searchWithKeys = new String[] {
+			With.withoutCertificate.name(), With.validCertificate.name(), With.expiredCertificate.name()
+		};
+	private static final String[] passedKeys = new String[] { "passed" };
+	
+	private TextElement searchStringEl;
+	private FormLink generateReportButton;
+	private DateChooser certificatesDateEl;
+	private MultipleSelectionElement withEl;
+	private MultipleSelectionElement passedEl;
+	private FlexiTableElement tableEl;
+	private RepositoryFlexiTableModel tableModel;
+	
+	@Autowired
+	private RepositoryManager repositoryManager;
+	
+	public CertificatesReportController(UserRequest ureq, WindowControl wControl) {
+		super(ureq, wControl, "reports", Util.createPackageTranslator(AssessmentTestDisplayController.class, ureq.getLocale(),
+				Util.createPackageTranslator(RepositoryService.class, ureq.getLocale())));
+		
+		initForm(ureq);
+	}
+
+	@Override
+	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
+		FormLayoutContainer searchCont = FormLayoutContainer.createDefaultFormLayout("search.cont", getTranslator());
+		formLayout.add("search.cont", searchCont);
+
+		searchStringEl = uifactory.addTextElement("search.text", 2000, null, searchCont);
+		certificatesDateEl = uifactory.addDateChooser("search.dates", "search.dates", null, searchCont);
+		certificatesDateEl.setSecondDate(true);
+		certificatesDateEl.setSeparator("search.dates.separator");
+		
+		String[] searchWithValues = new String[] {
+			translate("search.without"), translate("search.with.valid"), translate("search.with.expired")
+		};
+		withEl = uifactory.addCheckboxesVertical("search.with", "search.with", searchCont, searchWithKeys, searchWithValues, 1);
+		withEl.select(searchWithKeys[1], true);
+		
+		String[] passedValues = new String[] { translate("search.course.passed") };
+		passedEl = uifactory.addCheckboxesVertical("search.passed", "search.passed", searchCont, passedKeys, passedValues, 1);
+		passedEl.select(passedKeys[0], true);
+		uifactory.addFormSubmitButton("search", searchCont);
+		
+		FlexiTableColumnModel columnsModel = FlexiTableDataModelFactory.createFlexiTableColumnModel();
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(RepoCols.ac, new RepositoryEntryACColumnDescriptor()));
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(RepoCols.repoEntry, new TypeRenderer()));
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(false, RepoCols.externalId));// visible if managed
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(RepoCols.externalRef));
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(RepoCols.displayname, "select"));
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(RepoCols.author));
+		StaticFlexiCellRenderer reportRenderer = new StaticFlexiCellRenderer(translate("report"), "report");
+		reportRenderer.setPush(true);
+		reportRenderer.setDirtyCheck(false);
+		columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(true, true, "report", null, -1, "report", false, null, FlexiColumnModel.ALIGNMENT_LEFT, reportRenderer));
+
+		tableModel = new RepositoryFlexiTableModel(columnsModel, getLocale()); 
+		tableEl = uifactory.addTableElement(getWindowControl(), "table", tableModel, 20, false, getTranslator(), formLayout);
+		tableEl.setExportEnabled(true);
+		tableEl.setSelectAllEnable(true);
+		tableEl.setMultiSelect(true);
+		tableEl.setAndLoadPersistedPreferences(ureq, "certificates-reports-courses-list");
+		tableEl.setEmtpyTableMessageKey("search.empty");
+		
+		generateReportButton = uifactory.addFormLink("report.certificates", "report.certificates", null, formLayout, Link.BUTTON);
+		
+		tableEl.addBatchButton(generateReportButton);
+	}
+
+	@Override
+	protected void doDispose() {
+		//
+	}
+	
+	protected void loadModel(UserRequest ureq, String searchString) {
+		SearchRepositoryEntryParameters params = new SearchRepositoryEntryParameters();
+		params.addResourceTypes("CourseModule");
+		params.setIdentity(getIdentity());
+		params.setRoles(ureq.getUserSession().getRoles());
+		params.setIdRefsAndTitle(searchString);
+		
+		List<RepositoryEntry> entries = repositoryManager.genericANDQueryWithRolesRestriction(params, 0, -1, true);
+		tableModel.setObjects(entries);
+		tableEl.reset(true, true, true);
+	}
+
+	@Override
+	protected void formOK(UserRequest ureq) {
+		doSearch(ureq);
+	}
+
+	@Override
+	protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) {
+		if(generateReportButton == source) {
+			doReport(ureq);
+		} else if(tableEl == source) {
+			if(event instanceof SelectionEvent) {
+				SelectionEvent se = (SelectionEvent)event;
+				if("select".equals(se.getCommand())) {
+					doSelect(ureq, tableModel.getObject(se.getIndex()));
+				} else if("report".equals(se.getCommand())) {
+					doReport(ureq, tableModel.getObject(se.getIndex()));
+				}
+			}
+		}
+		super.formInnerEvent(ureq, source, event);
+	}
+	
+	private void doSearch(UserRequest ureq) {
+		String searchString = searchStringEl.getValue();
+		loadModel(ureq, searchString);
+	}
+	
+	private void doSelect(UserRequest ureq, RepositoryEntry re) {
+		String businessPath = "[RepositoryEntry:" + re.getKey() + "]";
+		NewControllerFactory.getInstance().launch(businessPath, ureq, getWindowControl());
+	}
+	
+	private void doReport(UserRequest ureq, RepositoryEntry re) {
+		String filename = re.getDisplayname() + "_Certificates_" + Formatter.formatDatetimeWithMinutes(ureq.getRequestTimestamp());
+		filename = StringHelper.transformDisplayNameToFileSystemName(filename);
+		
+		List<RepositoryEntry> entries = Collections.singletonList(re);
+		CertificatesReportParameters reportParams = getReportParameters();
+		MediaResource report = new CertificatesReportMediaResource(filename, entries, reportParams, getTranslator());
+		ureq.getDispatchResult().setResultingMediaResource(report);
+	}
+	
+	private void doReport(UserRequest ureq) {
+		List<RepositoryEntry> entries = getSelectedEntries();
+		if(entries.isEmpty()) {
+			showWarning("warning.at.least.one.test");
+		} else {
+			String filename = "Certificates_" + Formatter.formatDatetimeWithMinutes(ureq.getRequestTimestamp());
+			filename = StringHelper.transformDisplayNameToFileSystemName(filename);
+			
+			CertificatesReportParameters reportParams = getReportParameters();
+			MediaResource report = new CertificatesReportMediaResource(filename, entries, reportParams, getTranslator());
+			ureq.getDispatchResult().setResultingMediaResource(report);
+		}
+	}
+
+	private CertificatesReportParameters getReportParameters() {
+		List<With> with = With.values(withEl.getSelectedKeys());
+		boolean onlyPassed = passedEl.isAtLeastSelected(1);
+		Date certificatesStart = certificatesDateEl.getDate();
+		if(certificatesStart != null) {
+			certificatesStart = CalendarUtils.startOfDay(certificatesStart);
+		}
+		Date certificatesEnd = certificatesDateEl.getSecondDate();
+		if(certificatesEnd != null) {
+			certificatesEnd = CalendarUtils.endOfDay(certificatesEnd);
+		}
+		return new CertificatesReportParameters(certificatesStart, certificatesEnd, with, onlyPassed);
+	}
+	
+	private List<RepositoryEntry> getSelectedEntries() {
+		Set<Integer> selectedIndexes = tableEl.getMultiSelectedIndex();
+		List<RepositoryEntry> selectedEntries = new ArrayList<>(selectedIndexes.size());
+		for(Integer selectedIndex:selectedIndexes) {
+			RepositoryEntry entry = tableModel.getObject(selectedIndex.intValue());
+			selectedEntries.add(entry);
+		}
+		return selectedEntries;
+	}
+}
diff --git a/src/main/java/org/olat/course/certificate/ui/report/CertificatesReportMediaResource.java b/src/main/java/org/olat/course/certificate/ui/report/CertificatesReportMediaResource.java
new file mode 100644
index 0000000000000000000000000000000000000000..ed3ad0ea344f75c83f71e449c073303eac2de761
--- /dev/null
+++ b/src/main/java/org/olat/course/certificate/ui/report/CertificatesReportMediaResource.java
@@ -0,0 +1,257 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.course.certificate.ui.report;
+
+import java.io.OutputStream;
+import java.text.Collator;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.apache.logging.log4j.Logger;
+import org.olat.core.CoreSpringFactory;
+import org.olat.core.commons.persistence.DB;
+import org.olat.core.gui.translator.Translator;
+import org.olat.core.id.Identity;
+import org.olat.core.logging.Tracing;
+import org.olat.core.util.openxml.OpenXMLWorkbook;
+import org.olat.core.util.openxml.OpenXMLWorkbookResource;
+import org.olat.core.util.openxml.OpenXMLWorksheet;
+import org.olat.core.util.openxml.OpenXMLWorksheet.Row;
+import org.olat.course.CourseFactory;
+import org.olat.course.assessment.AssessmentToolManager;
+import org.olat.course.assessment.manager.UserCourseInformationsManager;
+import org.olat.course.assessment.model.SearchAssessedIdentityParams;
+import org.olat.course.certificate.CertificateLight;
+import org.olat.course.certificate.CertificatesManager;
+import org.olat.course.certificate.ui.report.CertificatesReportParameters.With;
+import org.olat.modules.assessment.AssessmentEntry;
+import org.olat.modules.assessment.ui.AssessmentToolSecurityCallback;
+import org.olat.repository.RepositoryEntry;
+import org.olat.resource.OLATResource;
+import org.olat.user.UserManager;
+import org.olat.user.propertyhandlers.UserPropertyHandler;
+import org.springframework.beans.factory.annotation.Autowired;
+
+
+/**
+ * 
+ * Initial date: 11 juin 2020<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class CertificatesReportMediaResource extends OpenXMLWorkbookResource {
+	
+	private static final Logger log = Tracing.createLoggerFor(CertificatesReportMediaResource.class);
+	
+
+	private static final String usageIdentifyer = CertificatesReportMediaResource.class.getName();
+	
+	private final Date now = new Date();
+	private final Translator translator;
+	private final List<RepositoryEntry> entries;
+	private List<UserPropertyHandler> userPropertyHandlers;
+	private final CertificatesReportParameters reportParams;
+	private final AssessmentToolSecurityCallback secCallback = new AssessmentToolSecurityCallback(true, false, true, true, true, null);
+	
+	@Autowired
+	private DB dbInstance;
+	@Autowired
+	private UserManager userManager;
+	@Autowired
+	private CertificatesManager certificatesManager;
+	@Autowired
+	private UserCourseInformationsManager userInfosMgr;
+	@Autowired
+	private AssessmentToolManager assessmentToolManager;
+	
+	public CertificatesReportMediaResource(String label, List<RepositoryEntry> entries,
+			CertificatesReportParameters reportParams, Translator translator) {
+		super(label);
+		CoreSpringFactory.autowireObject(this);
+		this.entries = entries;
+		this.reportParams = reportParams;
+		this.translator = userManager.getPropertyHandlerTranslator(translator);
+		userPropertyHandlers = userManager.getUserPropertyHandlersFor(usageIdentifyer, true);
+	}
+	
+	@Override
+	protected void generate(OutputStream out) {
+		try(OpenXMLWorkbook workbook = new OpenXMLWorkbook(out, 1)) {
+			OpenXMLWorksheet sheet = workbook.nextWorksheet();
+			sheet.setHeaderRows(1);
+			generateHeaders(sheet);
+			generateData(sheet, workbook);
+		} catch (Exception e) {
+			log.error("", e);
+		}
+	}
+	
+	protected void generateHeaders(OpenXMLWorksheet sheet) {
+		Row headerRow = sheet.newRow();
+		int col = 0;
+		
+		// course
+		headerRow.addCell(col++, translator.translate("report.course.displayname"));
+		
+		// user properties
+		for(UserPropertyHandler userPropertyHandler:userPropertyHandlers) {
+			if(userPropertyHandler == null) continue;
+			headerRow.addCell(col++, translator.translate(userPropertyHandler.i18nColumnDescriptorLabelKey()));
+		}
+		
+		//passed
+		headerRow.addCell(col++, translator.translate("report.course.passed"));
+		
+		// initial launch date
+		headerRow.addCell(col++, translator.translate("report.initialLaunchDate"));
+		
+		// certificate
+		headerRow.addCell(col, translator.translate("report.certificate"));
+	}
+	
+	protected void generateData(OpenXMLWorksheet sheet, OpenXMLWorkbook workbook) {
+		Collections.sort(entries, new RepositoryEntryComparator(translator.getLocale()));
+		
+		for(RepositoryEntry entry:entries) {
+			try {
+				generateData(entry, sheet, workbook);
+			} catch (Exception e) {
+				log.error("", e);
+			} finally {
+				dbInstance.commitAndCloseSession();
+			}
+		}	
+	}
+	
+	protected void generateData(RepositoryEntry entry, OpenXMLWorksheet sheet, OpenXMLWorkbook workbook) {	
+		OLATResource resource = entry.getOlatResource();
+		List<CertificateLight> certificates = certificatesManager.getLastCertificates(resource);
+		Map<Long, CertificateLight> certificateMap = certificates.stream()
+				.collect(Collectors.toMap(CertificateLight::getIdentityKey, Function.identity()));
+		
+		Map<Long, Date> initialLaunchDates = userInfosMgr
+				.getInitialLaunchDates(resource);
+		
+		String rootIdent = CourseFactory.loadCourse(entry).getRunStructure().getRootNode().getIdent();
+		SearchAssessedIdentityParams params = new SearchAssessedIdentityParams(entry, rootIdent, null, secCallback);
+		List<Identity> assessedIdentities = assessmentToolManager.getAssessedIdentities(null, params);
+		List<AssessmentEntry> assessmentEntries = assessmentToolManager.getAssessmentEntries(null, params, null);
+		Map<Long,AssessmentEntry> entryMap = new HashMap<>();
+		assessmentEntries.stream()
+			.filter(assessmentEntry -> assessmentEntry.getIdentity() != null)
+			.forEach(assessmentEntry -> entryMap.put(assessmentEntry.getIdentity().getKey(), assessmentEntry));
+		
+		dbInstance.commitAndCloseSession();
+		
+		for(Identity participant:assessedIdentities) {
+			AssessmentEntry assessmentEntry = entryMap.get(participant.getKey());
+			CertificateLight certificate = certificateMap.get(participant.getKey());
+			if(!accept(assessmentEntry, certificate)) {
+				continue;
+			}
+			
+			Date launchDate = initialLaunchDates.get(participant.getKey());
+			
+			int col = 0;
+			Row row = sheet.newRow();
+			row.addCell(col++, entry.getDisplayname());
+			
+			for(UserPropertyHandler userPropertyHandler:userPropertyHandlers) {
+				if(userPropertyHandler == null) continue;
+
+				String val = participant.getUser().getProperty(userPropertyHandler.getName(), translator.getLocale());
+				row.addCell(col++, val);
+			}
+			
+			//passed
+			if(assessmentEntry == null || assessmentEntry.getPassed() == null) {
+				col++;
+			} else {
+				String val = assessmentEntry.getPassed().booleanValue()
+						? translator.translate("report.course.passed.passed") : translator.translate("report.course.passed.failed");
+				row.addCell(col++, val);
+			}
+			
+			row.addCell(col++, launchDate, workbook.getStyles().getDateStyle());
+			
+			if(certificate != null) {
+				row.addCell(col, certificate.getCreationDate(), workbook.getStyles().getDateStyle());
+			}
+		}
+	}
+	
+	private boolean accept(AssessmentEntry assessmentEntry, CertificateLight certificate) {
+		if(reportParams.getCertificateStart() != null && (certificate == null || certificate.getCreationDate().before(reportParams.getCertificateStart()))) {
+			return false;	
+		}
+		if(reportParams.getCertificateEnd() != null && (certificate == null || certificate.getCreationDate().after(reportParams.getCertificateEnd()))) {
+			return false;	
+		}
+		if(reportParams.isOnlyPassed() && (assessmentEntry == null || assessmentEntry.getPassed() == null || !assessmentEntry.getPassed().booleanValue())) {
+			return false;
+		}
+		
+		List<With> withList = reportParams.getWith();
+		if(withList == null || withList.isEmpty()) {
+			return true;
+		}
+		
+		for(With with:withList) {
+			if(with == With.withoutCertificate && certificate == null) {
+				return true;
+			}
+			if(with == With.validCertificate && (certificate != null && (certificate.getNextRecertificationDate() == null || certificate.getNextRecertificationDate().after(now)))) {
+				return true;
+			}
+			if(with == With.expiredCertificate && (certificate != null && (certificate.getNextRecertificationDate() != null || certificate.getNextRecertificationDate().before(now)))) {
+				return true;
+			}
+		}
+		
+		return false;
+	}
+	
+	private static class RepositoryEntryComparator implements Comparator<RepositoryEntry> {
+		
+		private final Collator collator;
+		
+		public RepositoryEntryComparator(Locale locale) {
+			collator = Collator.getInstance(locale);
+		}
+		
+		@Override
+		public int compare(RepositoryEntry o1, RepositoryEntry o2) {
+			String d1 = o1.getDisplayname();
+			String d2 = o2.getDisplayname();
+			int c = collator.compare(d1, d2);
+			if(c == 0) {
+				c = o1.getKey().compareTo(o2.getKey());
+			}
+			return c;
+		}
+	}
+}
diff --git a/src/main/java/org/olat/course/certificate/ui/report/CertificatesReportParameters.java b/src/main/java/org/olat/course/certificate/ui/report/CertificatesReportParameters.java
new file mode 100644
index 0000000000000000000000000000000000000000..95cf11c31ed375a4031ff908346d1dbcd7353917
--- /dev/null
+++ b/src/main/java/org/olat/course/certificate/ui/report/CertificatesReportParameters.java
@@ -0,0 +1,74 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.course.certificate.ui.report;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 
+ * Initial date: 26 juin 2020<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+public class CertificatesReportParameters {
+	
+	private final List<With> with;
+	private final boolean onlyPassed;
+	private final Date certificateStart;
+	private final Date certificateEnd;
+	
+	public CertificatesReportParameters(Date certificateStart, Date certificateEnd, List<With> with, boolean onlyPassed) {
+		this.with = with;
+		this.onlyPassed = onlyPassed;
+		this.certificateEnd = certificateEnd;
+		this.certificateStart = certificateStart;
+	}
+
+	public List<With> getWith() {
+		return with;
+	}
+	
+	public boolean isOnlyPassed() {
+		return onlyPassed;
+	}
+
+	public Date getCertificateStart() {
+		return certificateStart;
+	}
+
+	public Date getCertificateEnd() {
+		return certificateEnd;
+	}
+	
+	public enum With {
+		withoutCertificate,
+		validCertificate,
+		expiredCertificate;
+		
+		public static List<With> values(Collection<String> keys) {
+			return keys.stream()
+					.map(With::valueOf)
+					.collect(Collectors.toList());
+		}
+	}
+}
diff --git a/src/main/java/org/olat/course/certificate/ui/report/_content/reports.html b/src/main/java/org/olat/course/certificate/ui/report/_content/reports.html
new file mode 100644
index 0000000000000000000000000000000000000000..2182de2f0f4e283fda31f874cf329698fbd6d476
--- /dev/null
+++ b/src/main/java/org/olat/course/certificate/ui/report/_content/reports.html
@@ -0,0 +1,3 @@
+<div class="o_info">$r.translate("report.explain")</div>
+$r.render("search.cont")
+$r.render("table")
\ No newline at end of file
diff --git a/src/main/java/org/olat/course/certificate/ui/report/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/course/certificate/ui/report/_i18n/LocalStrings_de.properties
new file mode 100644
index 0000000000000000000000000000000000000000..85e5106b9f9bda504dbd05e90a2350069071e758
--- /dev/null
+++ b/src/main/java/org/olat/course/certificate/ui/report/_i18n/LocalStrings_de.properties
@@ -0,0 +1,22 @@
+#Thu Sep 03 11:09:03 CEST 2015
+admin.menu.report.certificates.title=Zertifikaten
+admin.menu.report.certificates.title.alt=Zertifikaten
+report=Report
+report.certificates=Report Zertifikaten
+report.explain=Liste von Zertifikaten
+report.course.displayname=Module
+report.course.passed=$org.olat.course.assessment\:table.header.passed
+report.course.passed.failed=Nicht bestanden
+report.course.passed.passed=Bestanden
+report.initialLaunchDate=$org.olat.course.assessment\:table.header.initialLaunchDate
+report.certificate=$org.olat.course.assessment\:table.header.certificate
+search.course.passed=Kurs bestanden
+search.dates=Zertifikat Datum
+search.dates.separator=bis
+search.empty=Es wurden keine Kurse gefunden, die ihren Kriterien entsprechen.
+search.passed=Benutzer haben
+search.text=Titel
+search.with=Benutzer
+search.without=ohne Zertifikat
+search.with.valid=mit g\u00FCltigem Zertifikat
+search.with.expired=mit abgelaufenem Zertifikat
diff --git a/src/main/java/org/olat/course/certificate/ui/report/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/course/certificate/ui/report/_i18n/LocalStrings_en.properties
new file mode 100644
index 0000000000000000000000000000000000000000..9edf7abde9840bb8c37e5ead93ee660e26c7ca41
--- /dev/null
+++ b/src/main/java/org/olat/course/certificate/ui/report/_i18n/LocalStrings_en.properties
@@ -0,0 +1,23 @@
+#Thu Sep 03 11:09:03 CEST 2015
+admin.menu.report.certificates.title=Certificates
+admin.menu.report.certificates.title.alt=Certificates
+report=Report
+report.certificates=Report certificates
+report.explain=List of certificates
+report.course.displayname=Module
+report.course.passed=$org.olat.course.assessment\:table.header.passed
+report.course.passed.failed=Failed
+report.course.passed.passed=Passed
+report.initialLaunchDate=$org.olat.course.assessment\:table.header.initialLaunchDate
+report.certificate=$org.olat.course.assessment\:table.header.certificate
+search.course.passed=passed the course
+search.dates=Certificate date
+search.dates.separator=until
+search.empty=No courses were found that met your criteria.
+search.passed=Users have
+search.text=Title
+search.with=Users
+search.without=without certificate
+search.with.valid=with valid certificate
+search.with.expired=with expired certificate
+
diff --git a/src/main/java/org/olat/user/propertyhandlers/_spring/userPropertiesContext.xml b/src/main/java/org/olat/user/propertyhandlers/_spring/userPropertiesContext.xml
index f9a838cadc7383c6a38528a8958f4e7b91e4848b..83208014f973e706ccdfd11d7a80f9a49ea0b9cc 100644
--- a/src/main/java/org/olat/user/propertyhandlers/_spring/userPropertiesContext.xml
+++ b/src/main/java/org/olat/user/propertyhandlers/_spring/userPropertiesContext.xml
@@ -1698,6 +1698,22 @@
 					</bean>
 				</entry>
 				
+				<!-- User properties for certificates report -->
+				<entry key="org.olat.course.certificate.ui.report.CertificatesReportMediaResource">
+					<bean class="org.olat.user.propertyhandlers.UserPropertyUsageContext">
+						<property name="description" value="Properties for the certificates report" />
+						<property name="propertyHandlers">
+							<list>
+								<ref bean="userPropertyGenericText" />
+								<ref bean="userPropertyLastName" />
+								<ref bean="userPropertyFirstName" />
+								<ref bean="userPropertyEmail" />
+								<ref bean="userPropertyCountry" />
+							</list>
+						</property>
+					</bean>
+				</entry>
+				
 
 				<!--
 					Default configuration in case nothing else matches.