From b309e9be9cd1ca8ee663b31f7e5ac3cfc69bb284 Mon Sep 17 00:00:00 2001 From: Kai Reinhard Date: Wed, 1 Jan 2025 15:04:18 +0100 Subject: [PATCH 1/7] MailAttachments improved (effected BirthdayButler, Polls). E-Mail is sent automatically, if nightly sanity check has errors. --- .../birthdaybutler/BirthdayButlerService.kt | 10 +---- .../business/jobs/CronSanityCheckJob.kt | 30 ++++++++++++++ .../event/model/TeamEventAttachmentDO.kt | 41 ++++--------------- .../teamcal/event/model/TeamEventDO.kt | 4 +- .../org/projectforge/mail/IMailAttachment.kt} | 16 +++----- .../org/projectforge/mail/MailAttachment.kt | 31 ++++++++++++++ .../kotlin/org/projectforge/mail/SendMail.kt | 8 ++-- .../projectforge/rest/poll/PollCronJobs.kt | 10 +---- .../projectforge/rest/poll/PollPageRest.kt | 18 +++----- .../org/projectforge/web/admin/AdminPage.java | 3 +- 10 files changed, 90 insertions(+), 81 deletions(-) rename projectforge-business/src/main/{java/org/projectforge/mail/MailAttachment.java => kotlin/org/projectforge/mail/IMailAttachment.kt} (86%) create mode 100644 projectforge-business/src/main/kotlin/org/projectforge/mail/MailAttachment.kt diff --git a/projectforge-business/src/main/kotlin/org/projectforge/birthdaybutler/BirthdayButlerService.kt b/projectforge-business/src/main/kotlin/org/projectforge/birthdaybutler/BirthdayButlerService.kt index ec206a4c86..e6441cc60d 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/birthdaybutler/BirthdayButlerService.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/birthdaybutler/BirthdayButlerService.kt @@ -111,15 +111,7 @@ class BirthdayButlerService { } val word = response.wordDocument if (word != null) { - val attachment = object : MailAttachment { - override fun getFilename(): String { - return createFilename(month, locale) - } - - override fun getContent(): ByteArray { - return word - } - } + val attachment = MailAttachment(createFilename(month, locale), word) val list = mutableListOf() list.add(attachment) sendMail(month, content = "birthdayButler.email.content", mailAttachments = list, locale = locale) diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/jobs/CronSanityCheckJob.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/jobs/CronSanityCheckJob.kt index 21b4bb44d5..2d0f814653 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/business/jobs/CronSanityCheckJob.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/business/jobs/CronSanityCheckJob.kt @@ -31,12 +31,20 @@ import kotlinx.coroutines.launch import mu.KotlinLogging import org.projectforge.business.task.TaskDao import org.projectforge.common.extensions.formatMillis +import org.projectforge.framework.configuration.Configuration +import org.projectforge.framework.configuration.ConfigurationParam +import org.projectforge.framework.time.DateHelper import org.projectforge.jcr.JCRCheckSanityJob import org.projectforge.jobs.AbstractJob +import org.projectforge.jobs.JobExecutionContext import org.projectforge.jobs.JobListExecutionContext +import org.projectforge.mail.Mail +import org.projectforge.mail.MailAttachment +import org.projectforge.mail.SendMail import org.springframework.beans.factory.annotation.Autowired import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component +import java.util.* private val log = KotlinLogging.logger {} @@ -54,6 +62,9 @@ class CronSanityCheckJob { @Autowired private lateinit var jcrCheckSanityJob: JCRCheckSanityJob + @Autowired + private lateinit var sendMail: SendMail + @Autowired private lateinit var taskDao: TaskDao @@ -63,6 +74,7 @@ class CronSanityCheckJob { registerJob(jcrCheckSanityJob) // JCRCheckSanityJob is a plugin job, and it is registered here, because CronSanityCheckJob is not known by JCR. } + // For testing: @Scheduled(fixedDelay = 3600 * 1000, initialDelay = 10 * 1000) @Scheduled(cron = "\${projectforge.cron.sanityChecks}") fun cron() { log.info("Cronjob for executing sanity checks started...") @@ -71,6 +83,19 @@ class CronSanityCheckJob { val start = System.currentTimeMillis() try { val contextList = execute() + if (contextList.status == JobExecutionContext.Status.ERRORS) { + val recipients = Configuration.instance.getStringValue(ConfigurationParam.SYSTEM_ADMIN_E_MAIL) + if (!recipients.isNullOrBlank()) { + val msg = Mail() + msg.addTo(recipients) + msg.setProjectForgeSubject("Errors occurred on sanity check job.") + msg.content = + "Please refer the attached log file for more information or simply\nre-run system check on page Administration -> System -> check system integrity.\n\nYour ProjectForge system" + msg.contentType = Mail.CONTENTTYPE_TEXT + val attachments = listOf(MailAttachment(FILENAME, contextList.getReportAsText().toByteArray())) + sendMail.send(msg, null, attachments) + } + } } finally { log.info("Cronjob for executing sanity checks finished after ${(System.currentTimeMillis() - start).formatMillis()}") } @@ -96,4 +121,9 @@ class CronSanityCheckJob { log.info { "Registering sanity check job: ${job::class.simpleName}" } jobs.add(job) } + + companion object { + @JvmStatic + val FILENAME = "projectforge_sanity-check${DateHelper.getDateAsFilenameSuffix(Date())}.txt" + } } diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/teamcal/event/model/TeamEventAttachmentDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/teamcal/event/model/TeamEventAttachmentDO.kt index 3f1843081e..587d252d68 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/business/teamcal/event/model/TeamEventAttachmentDO.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/business/teamcal/event/model/TeamEventAttachmentDO.kt @@ -23,17 +23,13 @@ package org.projectforge.business.teamcal.event.model +import jakarta.persistence.Entity +import jakarta.persistence.Table import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.builder.HashCodeBuilder -import org.hibernate.annotations.Type import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed import org.projectforge.framework.persistence.entities.DefaultBaseDO -import org.projectforge.mail.MailAttachment -import jakarta.persistence.Column -import jakarta.persistence.Entity -import jakarta.persistence.Table -import org.hibernate.annotations.JdbcTypeCode -import org.hibernate.type.SqlTypes +import org.projectforge.mail.IMailAttachment /** * @author Kai Reinhard (k.reinhard@micromata.de) @@ -42,32 +38,11 @@ import org.hibernate.type.SqlTypes @Entity @Indexed @Table(name = "T_PLUGIN_CALENDAR_EVENT_ATTACHMENT") -open class TeamEventAttachmentDO : DefaultBaseDO(), Comparable, MailAttachment { - - private var filename: String? = null - - private var content: ByteArray? = null +open class TeamEventAttachmentDO : DefaultBaseDO(), Comparable, IMailAttachment { - @Column - override fun getFilename(): String? { - return filename - } - - open fun setFilename(filename: String): TeamEventAttachmentDO { - this.filename = filename - return this - } + override var filename: String? = null - @Column - @JdbcTypeCode(SqlTypes.BLOB) - override fun getContent(): ByteArray? { - return content - } - - open fun setContent(content: ByteArray): TeamEventAttachmentDO { - this.content = content - return this - } + override var content: ByteArray? = null /** * @see java.lang.Comparable.compareTo @@ -103,10 +78,10 @@ open class TeamEventAttachmentDO : DefaultBaseDO(), Comparable? + attachments: Collection? ): Boolean { return send(composedMessage, null, attachments, true) } @@ -144,7 +144,7 @@ open class SendMail { fun send( composedMessage: Mail?, icalContent: String? = null, - attachments: Collection? = null, + attachments: Collection? = null, async: Boolean = true ): Boolean { if (composedMessage == null) { @@ -208,7 +208,7 @@ open class SendMail { private fun sendIt( composedMessage: Mail, icalContent: String?, - attachments: Collection? + attachments: Collection? ) { log.info("Start sending e-mail message: " + StringUtils.join(composedMessage.to, ", ")) try { @@ -270,7 +270,7 @@ open class SendMail { @Throws(MessagingException::class) private fun createMailAttachmentContent( message: MimeMessage, composedMessage: Mail, icalContent: String?, - attachments: Collection?, + attachments: Collection?, charset: String ): MimeMultipart { // create and fill the first message part diff --git a/projectforge-rest/src/main/kotlin/org/projectforge/rest/poll/PollCronJobs.kt b/projectforge-rest/src/main/kotlin/org/projectforge/rest/poll/PollCronJobs.kt index aeb4fee98b..8ea46ef013 100644 --- a/projectforge-rest/src/main/kotlin/org/projectforge/rest/poll/PollCronJobs.kt +++ b/projectforge-rest/src/main/kotlin/org/projectforge/rest/poll/PollCronJobs.kt @@ -90,15 +90,7 @@ class PollCronJobs { val excel = exporter.getExcel(poll) - val mailAttachment = object : MailAttachment { - override fun getFilename(): String { - return "${pollDO.title}_${LocalDateTime.now().year}_Result.xlsx" - } - - override fun getContent(): ByteArray? { - return excel - } - } + val mailAttachment = MailAttachment("${pollDO.title}_${LocalDateTime.now().year}_Result.xlsx", excel) val owner = userService.getUser(poll.owner?.id) val mailTo = pollMailService.getAllAttendeesEmails(poll) val mailFrom = pollDO.owner?.email.toString() diff --git a/projectforge-rest/src/main/kotlin/org/projectforge/rest/poll/PollPageRest.kt b/projectforge-rest/src/main/kotlin/org/projectforge/rest/poll/PollPageRest.kt index 965e4ba2f1..a0de337f04 100644 --- a/projectforge-rest/src/main/kotlin/org/projectforge/rest/poll/PollPageRest.kt +++ b/projectforge-rest/src/main/kotlin/org/projectforge/rest/poll/PollPageRest.kt @@ -216,7 +216,7 @@ class PollPageRest : AbstractDTOPagesRest(PollDao::class. val colWidth = UILength(xs = 12, sm = 12, md = 6, lg = 6) fieldset - .add(UISpacer(width = 100)) + .add(UISpacer(width = 100)) .add(UISpacer(width = 100)) .add(UILabel("poll.headline.userConfiguration")) .add( @@ -452,15 +452,7 @@ class PollPageRest : AbstractDTOPagesRest(PollDao::class. val excel = excelExport.getExcel(poll) val filename = ("${postData.data.title}_${LocalDateTime.now().year}_Result.xlsx") - val mailAttachment = object : MailAttachment { - override fun getFilename(): String { - return filename - } - - override fun getContent(): ByteArray? { - return excel - } - } + val mailAttachment = MailAttachment(filename, excel) if (fuemailSent != true) { @@ -488,7 +480,7 @@ class PollPageRest : AbstractDTOPagesRest(PollDao::class. if (obj.inputFields.isNullOrEmpty() || obj.inputFields.equals("[]")) { throw AccessException("poll.error.oneQuestionRequired") } - + if(obj.owner == null) { throw AccessException("poll.error.ownerRequired") } @@ -895,9 +887,9 @@ class PollPageRest : AbstractDTOPagesRest(PollDao::class. UICol(colWidth) .add(lc, "location") ) - + ) - + } else { fieldset .add(UILabel("poll.headline.generalConfiguration")) diff --git a/projectforge-wicket/src/main/java/org/projectforge/web/admin/AdminPage.java b/projectforge-wicket/src/main/java/org/projectforge/web/admin/AdminPage.java index e205ff626b..586a429067 100644 --- a/projectforge-wicket/src/main/java/org/projectforge/web/admin/AdminPage.java +++ b/projectforge-wicket/src/main/java/org/projectforge/web/admin/AdminPage.java @@ -30,6 +30,7 @@ import org.projectforge.business.book.BookDO; import org.projectforge.business.book.BookDao; import org.projectforge.business.book.BookStatus; +import org.projectforge.business.jobs.CronSanityCheckJob; import org.projectforge.business.system.SystemService; import org.projectforge.business.user.UserXmlPreferencesCache; import org.projectforge.business.user.UserXmlPreferencesMigrationDao; @@ -278,7 +279,7 @@ protected void checkSystemIntegrity() { log.info("Administration: check integrity of tasks."); checkAccess(); final String result = WicketSupport.get(SystemService.class).checkSystemIntegrity(); - final String filename = "projectforge_sanity-check" + DateHelper.getDateAsFilenameSuffix(new Date()) + ".txt"; + final String filename = CronSanityCheckJob.getFILENAME(); DownloadUtils.setDownloadTarget(result.getBytes(), filename); } From 1a77630dcc4d545c2d063f78b46eb16df5b945bf Mon Sep 17 00:00:00 2001 From: Kai Reinhard Date: Wed, 1 Jan 2025 16:04:24 +0100 Subject: [PATCH 2/7] Kotlin.Coroutine -> Thread in Cronjobs and SystemService --- .../projectforge/business/jobs/CronNightlyJob.kt | 10 ++-------- .../business/jobs/CronSanityCheckJob.kt | 9 ++------- .../business/system/SystemService.kt | 16 ++++------------ .../org/projectforge/jcr/JCRCheckSanityJob.kt | 11 ++--------- 4 files changed, 10 insertions(+), 36 deletions(-) diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/jobs/CronNightlyJob.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/jobs/CronNightlyJob.kt index 482920ba41..bf7ae327b9 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/business/jobs/CronNightlyJob.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/business/jobs/CronNightlyJob.kt @@ -23,10 +23,6 @@ package org.projectforge.business.jobs -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch import mu.KotlinLogging import org.projectforge.common.extensions.formatMillis import org.projectforge.framework.persistence.search.HibernateSearchReindexer @@ -47,14 +43,12 @@ class CronNightlyJob { @Autowired private lateinit var hibernateSearchReindexer: HibernateSearchReindexer - private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - //@Scheduled(cron = "0 30 2 * * *") @Scheduled(cron = "\${projectforge.cron.nightly}") fun execute() { val started = System.currentTimeMillis() log.info("Nightly job started.") - coroutineScope.launch { + Thread { try { hibernateSearchReindexer.execute() } catch (ex: Throwable) { @@ -62,6 +56,6 @@ class CronNightlyJob { } finally { log.info("Nightly job job finished after ${(System.currentTimeMillis() - started).formatMillis()}.") } - } + }.start() } } diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/jobs/CronSanityCheckJob.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/jobs/CronSanityCheckJob.kt index 2d0f814653..2f9fc9973b 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/business/jobs/CronSanityCheckJob.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/business/jobs/CronSanityCheckJob.kt @@ -24,10 +24,6 @@ package org.projectforge.business.jobs import jakarta.annotation.PostConstruct -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch import mu.KotlinLogging import org.projectforge.business.task.TaskDao import org.projectforge.common.extensions.formatMillis @@ -57,7 +53,6 @@ private val log = KotlinLogging.logger {} @Component class CronSanityCheckJob { private val jobs = mutableListOf() - private val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) @Autowired private lateinit var jcrCheckSanityJob: JCRCheckSanityJob @@ -79,7 +74,7 @@ class CronSanityCheckJob { fun cron() { log.info("Cronjob for executing sanity checks started...") - coroutineScope.launch { + Thread { val start = System.currentTimeMillis() try { val contextList = execute() @@ -99,7 +94,7 @@ class CronSanityCheckJob { } finally { log.info("Cronjob for executing sanity checks finished after ${(System.currentTimeMillis() - start).formatMillis()}") } - } + }.start() } fun execute(): JobListExecutionContext { diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/system/SystemService.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/system/SystemService.kt index 70cfd77a04..93ce5b9b15 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/business/system/SystemService.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/business/system/SystemService.kt @@ -23,10 +23,6 @@ package org.projectforge.business.system -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch import mu.KotlinLogging import org.apache.commons.io.FileUtils import org.projectforge.business.address.BirthdayCache.Companion.instance @@ -59,8 +55,6 @@ private val log = KotlinLogging.logger {} */ @Service class SystemService { - private val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) - @Autowired private lateinit var accessChecker: AccessChecker @@ -124,13 +118,11 @@ class SystemService { return internalCheckSystemIntegrity() } val user = requiredLoggedInUser - val userContext = - ThreadLocalUserContext.userContextAsContextElement // Must get thread-local user outside the coroutine! - coroutineScope.launch(userContext) { + Thread { // Can't run it in a Kotlin coroutine, because JCR code has something like runInSession. val start = System.currentTimeMillis() log.info { "Check system integrity started..." } try { - //ThreadLocalUserContext.setUser(user) + ThreadLocalUserContext.setUser(user) val content = internalCheckSystemIntegrity() val filename = "projectforge_sanity-check${DateHelper.getTimestampAsFilenameSuffix(Date())}.txt" val description = "System integrity check result" @@ -141,10 +133,10 @@ class SystemService { receiver = user, ) } finally { - //ThreadLocalUserContext.clear() + ThreadLocalUserContext.clear() log.info("Checking of system integrity finished after ${(System.currentTimeMillis() - start).formatMillis()}") } - } + }.start() return "Checking of system integrity started.\n\nThe results will be in Your personal data transfer box in a few minutes (dependant on your ProjectForge installation)..." } diff --git a/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/JCRCheckSanityJob.kt b/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/JCRCheckSanityJob.kt index a4ed2ec8bb..f3c2a888bf 100644 --- a/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/JCRCheckSanityJob.kt +++ b/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/JCRCheckSanityJob.kt @@ -23,14 +23,9 @@ package org.projectforge.jcr -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch import mu.KotlinLogging import org.projectforge.common.FormatterUtils import org.projectforge.common.extensions.format -import org.projectforge.common.extensions.formatBytes import org.projectforge.common.extensions.formatMillis import org.projectforge.jobs.AbstractJob import org.projectforge.jobs.JobExecutionContext @@ -46,8 +41,6 @@ open class JCRCheckSanityJob : AbstractJob("JCR Check Sanity") { @Autowired internal lateinit var repoService: RepoService - private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - class CheckResult( val errors: List, val warnings: List, @@ -62,7 +55,7 @@ open class JCRCheckSanityJob : AbstractJob("JCR Check Sanity") { val started = System.currentTimeMillis() log.info("JCR sanity check job started.") val job = this - coroutineScope.launch { + Thread { try { val jobContext = JobExecutionContext(job) execute(jobContext) @@ -80,7 +73,7 @@ open class JCRCheckSanityJob : AbstractJob("JCR Check Sanity") { } catch (ex: Throwable) { log.error("While executing hibernate search re-index job: " + ex.message, ex) } - } + }.start() } override fun execute(jobContext: JobExecutionContext) { From 76920e51f86c89ed8a0a1ac2b20c0f83b1f2ff92 Mon Sep 17 00:00:00 2001 From: Kai Reinhard Date: Wed, 1 Jan 2025 16:43:40 +0100 Subject: [PATCH 3/7] cosmetic corrections in SystemService and SanityCheckJobs --- .../datatransfer/DataTransferSanityCheckJob.kt | 13 ++++++++++--- .../java/org/projectforge/web/WicketSupport.java | 4 ++-- .../business/jobs/CronSanityCheckJob.kt | 4 ++-- .../projectforge/business/system/SystemService.kt | 8 +++++++- .../main/kotlin/org/projectforge/jcr/BackupMain.kt | 2 +- ...RCheckSanityJob.kt => JCRCheckSanityCheckJob.kt} | 8 ++++++-- .../org/projectforge/jcr/RepoBackupService.kt | 4 ++-- .../kotlin/org/projectforge/jcr/SanityCheckMain.kt | 2 +- .../kotlin/org/projectforge/jcr/RepoBackupTest.kt | 4 ++-- 9 files changed, 33 insertions(+), 16 deletions(-) rename projectforge-jcr/src/main/kotlin/org/projectforge/jcr/{JCRCheckSanityJob.kt => JCRCheckSanityCheckJob.kt} (96%) diff --git a/plugins/org.projectforge.plugins.datatransfer/src/main/kotlin/org/projectforge/plugins/datatransfer/DataTransferSanityCheckJob.kt b/plugins/org.projectforge.plugins.datatransfer/src/main/kotlin/org/projectforge/plugins/datatransfer/DataTransferSanityCheckJob.kt index d24124f26e..f7a6081ba2 100644 --- a/plugins/org.projectforge.plugins.datatransfer/src/main/kotlin/org/projectforge/plugins/datatransfer/DataTransferSanityCheckJob.kt +++ b/plugins/org.projectforge.plugins.datatransfer/src/main/kotlin/org/projectforge/plugins/datatransfer/DataTransferSanityCheckJob.kt @@ -25,6 +25,7 @@ package org.projectforge.plugins.datatransfer import jakarta.annotation.PostConstruct import org.projectforge.business.jobs.CronSanityCheckJob +import org.projectforge.common.extensions.format import org.projectforge.framework.jcr.AttachmentsService import org.projectforge.jobs.AbstractJob import org.projectforge.jobs.JobExecutionContext @@ -55,6 +56,7 @@ class DataTransferSanityCheckJob : AbstractJob("Check data transfer files.") { var areaCounter = 0 var missingInJcrCounter = 0 var orphanedCounter = 0 + var fileCounter = 0 dataTransferAreaDao.selectAll(checkAccess = false).forEach { area -> ++areaCounter val areaName = "'${area.displayName}'" @@ -66,6 +68,7 @@ class DataTransferSanityCheckJob : AbstractJob("Check data transfer files.") { checkAccess = false, ) val attachmentsCounter = area.attachmentsCounter ?: 0 + fileCounter += attachmentsCounter if (attachmentsCounter == 0 && attachments.isNullOrEmpty()) { // No attachments given/expected, nothing to check. return@forEach @@ -95,9 +98,13 @@ class DataTransferSanityCheckJob : AbstractJob("Check data transfer files.") { jobContext.addWarning("Error while checking data transfer area $areaName: ${ex.message}") } } - if (missingInJcrCounter > 0 || orphanedCounter > 0) { - jobContext.addError("Checked $areaCounter data transfer areas: $missingInJcrCounter missed, $orphanedCounter orphaned attachments.") + val baseMsg = "Checked ${fileCounter.format()} files in ${areaCounter.format()} data transfer areas" + if (missingInJcrCounter > 0 ) { + jobContext.addError("$baseMsg: $missingInJcrCounter missed, $orphanedCounter orphaned attachments.") + } else if (orphanedCounter > 0) { + jobContext.addWarning("$baseMsg: $orphanedCounter orphaned attachments.") + } else { + jobContext.addMessage("$baseMsg.") } - jobContext.addMessage("Checked $areaCounter data transfer areas.") } } diff --git a/projectforge-business/src/main/java/org/projectforge/web/WicketSupport.java b/projectforge-business/src/main/java/org/projectforge/web/WicketSupport.java index dce0d89a19..fc0e1bae13 100644 --- a/projectforge-business/src/main/java/org/projectforge/web/WicketSupport.java +++ b/projectforge-business/src/main/java/org/projectforge/web/WicketSupport.java @@ -46,7 +46,7 @@ import org.projectforge.framework.persistence.database.DatabaseDao; import org.projectforge.framework.persistence.database.DatabaseInitTestDataService; import org.projectforge.framework.persistence.search.HibernateSearchReindexer; -import org.projectforge.jcr.JCRCheckSanityJob; +import org.projectforge.jcr.JCRCheckSanityCheckJob; import org.projectforge.menu.builder.MenuCreator; import org.projectforge.plugins.core.PluginAdminService; import org.projectforge.sms.SmsSenderConfig; @@ -186,7 +186,7 @@ private void registerBeans(ApplicationContext applicationContext) { registerBean(applicationContext.getBean(EmployeeSalaryDao.class)); registerBean(applicationContext.getBean(GroupDao.class)); registerBean(applicationContext.getBean(HibernateSearchReindexer.class)); - registerBean(applicationContext.getBean(JCRCheckSanityJob.class)); + registerBean(applicationContext.getBean(JCRCheckSanityCheckJob.class)); registerBean(applicationContext.getBean(Kost1Dao.class)); registerBean(applicationContext.getBean(Kost2Dao.class)); registerBean(applicationContext.getBean(KostCache.class)); diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/jobs/CronSanityCheckJob.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/jobs/CronSanityCheckJob.kt index 2f9fc9973b..fba0081626 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/business/jobs/CronSanityCheckJob.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/business/jobs/CronSanityCheckJob.kt @@ -30,7 +30,7 @@ import org.projectforge.common.extensions.formatMillis import org.projectforge.framework.configuration.Configuration import org.projectforge.framework.configuration.ConfigurationParam import org.projectforge.framework.time.DateHelper -import org.projectforge.jcr.JCRCheckSanityJob +import org.projectforge.jcr.JCRCheckSanityCheckJob import org.projectforge.jobs.AbstractJob import org.projectforge.jobs.JobExecutionContext import org.projectforge.jobs.JobListExecutionContext @@ -55,7 +55,7 @@ class CronSanityCheckJob { private val jobs = mutableListOf() @Autowired - private lateinit var jcrCheckSanityJob: JCRCheckSanityJob + private lateinit var jcrCheckSanityJob: JCRCheckSanityCheckJob @Autowired private lateinit var sendMail: SendMail diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/system/SystemService.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/system/SystemService.kt index 93ce5b9b15..13088a7517 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/business/system/SystemService.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/business/system/SystemService.kt @@ -137,7 +137,13 @@ class SystemService { log.info("Checking of system integrity finished after ${(System.currentTimeMillis() - start).formatMillis()}") } }.start() - return "Checking of system integrity started.\n\nThe results will be in Your personal data transfer box in a few minutes (dependant on your ProjectForge installation)..." + return """ + |Checking of system integrity started. + | + |The results will be in Your personal data transfer box in a few minutes (dependant on your ProjectForge installation)... + | + |For large files in the data transfer boxes, it may take much more time (all checksums will be calculated)." + """.trimMargin() } private fun internalCheckSystemIntegrity(): String { diff --git a/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/BackupMain.kt b/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/BackupMain.kt index e98616151e..d6e6358525 100644 --- a/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/BackupMain.kt +++ b/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/BackupMain.kt @@ -62,7 +62,7 @@ class BackupMain { val repoBackupService = RepoBackupService() repoService.init(repositoryLocation) repoBackupService.repoService = repoService - val jcrCheckSanityJob = JCRCheckSanityJob() + val jcrCheckSanityJob = JCRCheckSanityCheckJob() jcrCheckSanityJob.repoService = repoService repoBackupService.jcrCheckSanityJob = jcrCheckSanityJob return repoBackupService diff --git a/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/JCRCheckSanityJob.kt b/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/JCRCheckSanityCheckJob.kt similarity index 96% rename from projectforge-jcr/src/main/kotlin/org/projectforge/jcr/JCRCheckSanityJob.kt rename to projectforge-jcr/src/main/kotlin/org/projectforge/jcr/JCRCheckSanityCheckJob.kt index f3c2a888bf..32d89881e4 100644 --- a/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/JCRCheckSanityJob.kt +++ b/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/JCRCheckSanityCheckJob.kt @@ -26,6 +26,7 @@ package org.projectforge.jcr import mu.KotlinLogging import org.projectforge.common.FormatterUtils import org.projectforge.common.extensions.format +import org.projectforge.common.extensions.formatBytes import org.projectforge.common.extensions.formatMillis import org.projectforge.jobs.AbstractJob import org.projectforge.jobs.JobExecutionContext @@ -37,7 +38,7 @@ import javax.jcr.Node private val log = KotlinLogging.logger {} @Component -open class JCRCheckSanityJob : AbstractJob("JCR Check Sanity") { +open class JCRCheckSanityCheckJob : AbstractJob("JCR Check Sanity") { @Autowired internal lateinit var repoService: RepoService @@ -78,8 +79,10 @@ open class JCRCheckSanityJob : AbstractJob("JCR Check Sanity") { override fun execute(jobContext: JobExecutionContext) { var failedChecks = 0 + var totalSizeOfFiles = 0L val walker = object : RepoTreeWalker(repoService) { override fun visitFile(fileNode: Node, fileObject: FileObject) { + totalSizeOfFiles += fileObject.size ?: 0 fileObject.checksum.let { repoChecksum -> if (repoChecksum != null && repoChecksum.length > 10) { val checksum = @@ -144,7 +147,7 @@ open class JCRCheckSanityJob : AbstractJob("JCR Check Sanity") { ) if (failedChecks > 0) { jobContext.addError( - "Checksums of $failedChecks/${walker.numberOfVisitedFiles.format()} files (${walker.numberOfVisitedNodes.format()} nodes) failed." + "Checksums of $failedChecks/${walker.numberOfVisitedFiles.format()} files (${totalSizeOfFiles.formatBytes()}, ${walker.numberOfVisitedNodes.format()} nodes) failed." ) } } @@ -181,5 +184,6 @@ open class JCRCheckSanityJob : AbstractJob("JCR Check Sanity") { companion object { const val NUMBER_OF_VISITED_NODES = "numberOfVisitedNodes" const val NUMBER_OF_VISITED_FILES = "numberOfVisitedFiles" + } } diff --git a/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/RepoBackupService.kt b/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/RepoBackupService.kt index fcfe139d99..81a26b987a 100644 --- a/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/RepoBackupService.kt +++ b/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/RepoBackupService.kt @@ -53,7 +53,7 @@ open class RepoBackupService { internal lateinit var repoService: RepoService @Autowired - internal lateinit var jcrCheckSanityJob: JCRCheckSanityJob + internal lateinit var jcrCheckSanityJob: JCRCheckSanityCheckJob internal val listOfIgnoredNodePaths = mutableListOf() @@ -183,7 +183,7 @@ open class RepoBackupService { zipIn: ZipInputStream, securityConfirmation: String, absPath: String = "/${repoService.mainNodeName}" - ): JCRCheckSanityJob.CheckResult { + ): JCRCheckSanityCheckJob.CheckResult { if (securityConfirmation != RESTORE_SECURITY_CONFIRMATION__I_KNOW_WHAT_I_M_DOING__REPO_MAY_BE_DESTROYED) { throw IllegalArgumentException("You must use the correct security confirmation if you know what you're doing. The repo content may be lost after restoring!") } diff --git a/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/SanityCheckMain.kt b/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/SanityCheckMain.kt index 82e2866a0e..880e1a7680 100644 --- a/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/SanityCheckMain.kt +++ b/projectforge-jcr/src/main/kotlin/org/projectforge/jcr/SanityCheckMain.kt @@ -36,7 +36,7 @@ class SanityCheckMain { val repositoryLocation = checkRepoDir(args[0]) ?: return val repoService = RepoService() repoService.init(repositoryLocation) - val jcrCheckSanityJob = JCRCheckSanityJob() + val jcrCheckSanityJob = JCRCheckSanityCheckJob() jcrCheckSanityJob.repoService = repoService jcrCheckSanityJob.execute() } diff --git a/projectforge-jcr/src/test/kotlin/org/projectforge/jcr/RepoBackupTest.kt b/projectforge-jcr/src/test/kotlin/org/projectforge/jcr/RepoBackupTest.kt index ba2fd95352..fdde55fc80 100644 --- a/projectforge-jcr/src/test/kotlin/org/projectforge/jcr/RepoBackupTest.kt +++ b/projectforge-jcr/src/test/kotlin/org/projectforge/jcr/RepoBackupTest.kt @@ -41,7 +41,7 @@ class RepoBackupTest { val repoDir = testUtils.deleteAndCreateTestFile("testBackupRepo") repoService.init(repoDir) repoBackupService.repoService = repoService - repoBackupService.jcrCheckSanityJob = JCRCheckSanityJob() + repoBackupService.jcrCheckSanityJob = JCRCheckSanityCheckJob() repoBackupService.jcrCheckSanityJob.repoService = repoService } @@ -85,7 +85,7 @@ class RepoBackupTest { val repo2Dir = testUtils.deleteAndCreateTestFile("testBackupRepo2") repo2Service.init(repo2Dir) repo2BackupService.repoService = repo2Service - repo2BackupService.jcrCheckSanityJob = JCRCheckSanityJob() + repo2BackupService.jcrCheckSanityJob = JCRCheckSanityCheckJob() repo2BackupService.jcrCheckSanityJob.repoService = repo2Service ZipInputStream(FileInputStream(zipFile)).use { From fc0d7f60e5c5bed5cc4006570c649dd9774a6977 Mon Sep 17 00:00:00 2001 From: Kai Reinhard Date: Thu, 2 Jan 2025 00:04:23 +0100 Subject: [PATCH 4/7] DBFullTextResultIterator: block size 100 -> 10_000 for much better performance., QueryFilter limit is now 50_000 instead of 10_000. --- ToDo.adoc | 2 -- .../projectforge/framework/persistence/api/QueryFilter.kt | 2 +- .../persistence/api/impl/DBFullTextResultIterator.kt | 5 ++++- .../framework/persistence/api/impl/DBHistoryQuery.kt | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/ToDo.adoc b/ToDo.adoc index 70c182d0fd..a69b8a033e 100644 --- a/ToDo.adoc +++ b/ToDo.adoc @@ -1,7 +1,5 @@ Aktuell: - Scripting: Ergebnis Unresolved reference 'memo', 'todo'.: line 94 to 94 (add only activated plugins) -- Zeitberichte: kost2.nummer:4.* dauert sehr lang, auch für kurze Zeiträume. -- Healthcheck (daily) with mail notification on errors. Mit Registrierung von Services (jcr, orderbook snaphosts etc.) - Groovy-scripts: remove or fix. - AG-Grid: setColumnStates wird nicht in den UserPrefs gespeichert. - Wicket: Auftragsbuch: org.apache.wicket.core.request.mapper.StalePageException: A request to page '[Page class = org.projectforge.web.fibu.AuftragEditPage, id = 9, render count = 3]' has been made with stale 'renderCount'. The page will be re-rendered. diff --git a/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/api/QueryFilter.kt b/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/api/QueryFilter.kt index ca4f7f3daf..7acd275089 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/api/QueryFilter.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/api/QueryFilter.kt @@ -261,7 +261,7 @@ class QueryFilter @JvmOverloads constructor(filter: BaseSearchFilter? = null) { /** * If no maximum number of results is defined, MAX_ROWS is used as max value. */ - const val QUERY_FILTER_MAX_ROWS: Int = 10000 + const val QUERY_FILTER_MAX_ROWS: Int = 50_000 @JvmStatic fun isNull(field: String): DBPredicate.IsNull { diff --git a/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/api/impl/DBFullTextResultIterator.kt b/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/api/impl/DBFullTextResultIterator.kt index 2af35e3989..d3bfd545c6 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/api/impl/DBFullTextResultIterator.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/api/impl/DBFullTextResultIterator.kt @@ -39,7 +39,10 @@ import java.text.Collator private val log = KotlinLogging.logger {} -private const val MAX_RESULTS = 100 +/** + * Block wise search result iterator. (100 was much too low: 100 caused a lot of queries) + */ +private const val MAX_RESULTS = 10_000 internal class DBFullTextResultIterator>( val baseDao: BaseDao, diff --git a/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/api/impl/DBHistoryQuery.kt b/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/api/impl/DBHistoryQuery.kt index 4f85abd107..16792f05ad 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/api/impl/DBHistoryQuery.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/framework/persistence/api/impl/DBHistoryQuery.kt @@ -34,7 +34,7 @@ import java.util.* private val log = KotlinLogging.logger {} internal object DBHistoryQuery { - private const val MAX_RESULT_SIZE = 100000 // Limit result list to 100000 + private const val MAX_RESULT_SIZE = 100_000 // Limit result list to 100_000 fun searchHistoryEntryByCriteria( entityManager: EntityManager, From c4e0a8a2df0f7532b9b6c99277bdae5358f3f02a Mon Sep 17 00:00:00 2001 From: Kai Reinhard Date: Thu, 2 Jan 2025 00:12:47 +0100 Subject: [PATCH 5/7] Doc --- .../fibu/orderbooksnapshots/OrderbookSnapshotDO.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/orderbooksnapshots/OrderbookSnapshotDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/orderbooksnapshots/OrderbookSnapshotDO.kt index 7f4aa9fae9..d822633947 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/orderbooksnapshots/OrderbookSnapshotDO.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/orderbooksnapshots/OrderbookSnapshotDO.kt @@ -30,7 +30,15 @@ import java.time.LocalDate import java.util.* /** + * Order book snapshot stores the orderbook as a serialized byte array including current state of invoices (paid, to-be-invoiced etc.). + * + * The order book is stored as full backup or as incremental backup. + * + * The purpose of this table is to compare old order books with new ones to compare forecasts with reality. + * + * ``` * SELECT date, created, incremental_based_on, octet_length(serialized_orderbook) AS byte_count, size FROM t_fibu_orderbook_snapshots; + * ``` * @author Kai Reinhard (k.reinhard@micromata.de) */ @Entity From 1bd5072f02e6d0bc73a69bcd070857a66fa1e31f Mon Sep 17 00:00:00 2001 From: Kai Reinhard Date: Thu, 2 Jan 2025 00:58:27 +0100 Subject: [PATCH 6/7] Minor improvements --- .../OrderbookSnapshotsService.kt | 2 +- .../business/jobs/CronSanityCheckJob.kt | 14 ++++++++++++-- .../org/projectforge/jobs/JobExecutionContext.kt | 4 ++-- .../projectforge/jobs/JobListExecutionContext.kt | 4 ++-- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/orderbooksnapshots/OrderbookSnapshotsService.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/orderbooksnapshots/OrderbookSnapshotsService.kt index 21eb775f80..f257a95f4a 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/orderbooksnapshots/OrderbookSnapshotsService.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/orderbooksnapshots/OrderbookSnapshotsService.kt @@ -84,7 +84,7 @@ class OrderbookSnapshotsService { val today = LocalDate.now() val entry = findEntry(today) if (entry != null) { - log.info { "Order book for today already exists. OK, nothing to do." } + log.info { "Order book snapshot for today ($today UTC) already exists. OK, nothing to do." } return@runInNewTransaction } var incrementalBasedOn: LocalDate? = null diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/jobs/CronSanityCheckJob.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/jobs/CronSanityCheckJob.kt index fba0081626..b69b54de35 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/business/jobs/CronSanityCheckJob.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/business/jobs/CronSanityCheckJob.kt @@ -84,8 +84,18 @@ class CronSanityCheckJob { val msg = Mail() msg.addTo(recipients) msg.setProjectForgeSubject("Errors occurred on sanity check job.") - msg.content = - "Please refer the attached log file for more information or simply\nre-run system check on page Administration -> System -> check system integrity.\n\nYour ProjectForge system" + val sb = StringBuilder() + sb.appendLine( + """ + |Please refer the attached log file for more information or simply + |re-run system check on page Administration -> System -> check system integrity. + | + |Your ProjectForge system + | + """.trimMargin() + ) + sb.appendLine(contextList.getReportAsText(showAllMessages = false)) + msg.content = sb.toString() msg.contentType = Mail.CONTENTTYPE_TEXT val attachments = listOf(MailAttachment(FILENAME, contextList.getReportAsText().toByteArray())) sendMail.send(msg, null, attachments) diff --git a/projectforge-common/src/main/kotlin/org/projectforge/jobs/JobExecutionContext.kt b/projectforge-common/src/main/kotlin/org/projectforge/jobs/JobExecutionContext.kt index cdefa4a23a..9351cc7018 100644 --- a/projectforge-common/src/main/kotlin/org/projectforge/jobs/JobExecutionContext.kt +++ b/projectforge-common/src/main/kotlin/org/projectforge/jobs/JobExecutionContext.kt @@ -97,14 +97,14 @@ class JobExecutionContext(val producer: AbstractJob) { } } - fun addReportAsText(sb: StringBuilder) { + fun addReportAsText(sb: StringBuilder, showAllMessages: Boolean = true) { addIntro(sb) if (errors.isNotEmpty()) { sb.appendLine("*** Errors:") errors.forEach { sb.appendLine("*** ERROR: ${it.date}: ${it.message}") } sb.appendLine() } - if (allMessages.isNotEmpty()) { + if (showAllMessages && allMessages.isNotEmpty()) { sb.appendLine("Messages:") allMessages.forEach { msg -> val marker = when (msg.status) { diff --git a/projectforge-common/src/main/kotlin/org/projectforge/jobs/JobListExecutionContext.kt b/projectforge-common/src/main/kotlin/org/projectforge/jobs/JobListExecutionContext.kt index c4f77fb362..f4d1b1bf6d 100644 --- a/projectforge-common/src/main/kotlin/org/projectforge/jobs/JobListExecutionContext.kt +++ b/projectforge-common/src/main/kotlin/org/projectforge/jobs/JobListExecutionContext.kt @@ -42,7 +42,7 @@ class JobListExecutionContext { return JobExecutionContext(job).also { jobs.add(it) } } - fun getReportAsText(): String { + fun getReportAsText(showAllMessages: Boolean = true): String { val sb = StringBuilder() addSeparatorLine(sb) addSeparatorLine(sb) @@ -60,7 +60,7 @@ class JobListExecutionContext { addSeparatorLine(sb) sb.appendLine() sortedJobs.forEach { job -> - job.addReportAsText(sb) + job.addReportAsText(sb, showAllMessages) } sb.appendLine() return sb.toString() From 4550ccc8ae033136a73e22e7c0525c9eb80f4e18 Mon Sep 17 00:00:00 2001 From: Kai Reinhard Date: Thu, 2 Jan 2025 22:39:05 +0100 Subject: [PATCH 7/7] MonthlyEmployeeReport: show weekly net and gross hours. Fix select in AuftragDao.positionen.status Scripting: Add PFCaches as variable. GlobalDefaultExceptionHandling: Exception handling improved. --- .../business/fibu/MonthlyEmployeeReport.java | 2 +- .../fibu/MonthlyEmployeeReportEntry.java | 106 ------------------ .../fibu/MonthlyEmployeeReportWeek.java | 15 +-- .../projectforge/business/fibu/AuftragDO.kt | 1 - .../projectforge/business/fibu/AuftragDao.kt | 39 +------ .../fibu/MonthlyEmployeeReportEntry.kt | 85 ++++++++++++++ .../business/scripting/ScriptExecutor.kt | 2 + .../projectforge/rest/TimesheetPagesRest.kt | 1 - .../core/GlobalDefaultExceptionHandling.kt | 50 ++++++--- .../web/fibu/MonthlyEmployeeReportPage.java | 4 +- 10 files changed, 139 insertions(+), 166 deletions(-) delete mode 100644 projectforge-business/src/main/java/org/projectforge/business/fibu/MonthlyEmployeeReportEntry.java create mode 100644 projectforge-business/src/main/kotlin/org/projectforge/business/fibu/MonthlyEmployeeReportEntry.kt diff --git a/projectforge-business/src/main/java/org/projectforge/business/fibu/MonthlyEmployeeReport.java b/projectforge-business/src/main/java/org/projectforge/business/fibu/MonthlyEmployeeReport.java index c39ef8d1f2..2d59fbebdb 100644 --- a/projectforge-business/src/main/java/org/projectforge/business/fibu/MonthlyEmployeeReport.java +++ b/projectforge-business/src/main/java/org/projectforge/business/fibu/MonthlyEmployeeReport.java @@ -322,7 +322,7 @@ public void calculate() { taskTotal.addMillis(entry.getMillis()); } totalGrossDuration += entry.getMillis(); - totalNetDuration += entry.getMillis(); + totalNetDuration += entry.getWorkFractionMillis(); } } } diff --git a/projectforge-business/src/main/java/org/projectforge/business/fibu/MonthlyEmployeeReportEntry.java b/projectforge-business/src/main/java/org/projectforge/business/fibu/MonthlyEmployeeReportEntry.java deleted file mode 100644 index 09c527d6ff..0000000000 --- a/projectforge-business/src/main/java/org/projectforge/business/fibu/MonthlyEmployeeReportEntry.java +++ /dev/null @@ -1,106 +0,0 @@ -///////////////////////////////////////////////////////////////////////////// -// -// Project ProjectForge Community Edition -// www.projectforge.org -// -// Copyright (C) 2001-2025 Micromata GmbH, Germany (www.micromata.com) -// -// ProjectForge is dual-licensed. -// -// This community edition is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License as published -// by the Free Software Foundation; version 3 of the License. -// -// This community edition 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 General -// Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, see http://www.gnu.org/licenses/. -// -///////////////////////////////////////////////////////////////////////////// - -package org.projectforge.business.fibu; - -import org.projectforge.business.PfCaches; -import org.projectforge.business.fibu.kost.Kost2ArtDO; -import org.projectforge.business.fibu.kost.Kost2DO; -import org.projectforge.business.task.TaskDO; - -import java.io.Serializable; - -/** - * Repräsentiert einen Eintrag innerhalb eines Wochenberichts eines Mitarbeiters zu einem Kostenträger (Anzahl Stunden). - * @author Kai Reinhard (k.reinhard@micromata.de) - */ -public class MonthlyEmployeeReportEntry implements Serializable -{ - private static final long serialVersionUID = 7290000602224467755L; - - private Kost2DO kost2; - - private TaskDO task; - - private long millis; - - public MonthlyEmployeeReportEntry(Kost2DO kost2) - { - this.kost2 = kost2; - } - - public MonthlyEmployeeReportEntry(TaskDO task) - { - this.task = task; - } - - public void addMillis(long millis) - { - this.millis += millis; - } - - /** - * Only given, if task is not given and vice versa. - */ - public Kost2DO getKost2() - { - return kost2; - } - - /** - * Only given, if kost2 is not given and vice versa. - */ - public TaskDO getTask() - { - return task; - } - - /** - * If this entry has a kost2 with a working time fraction set or a kost2art with a working time fraction set then the fraction of millis - * will be returned. - */ - public long getWorkFractionMillis() - { - Kost2DO useKost2 = PfCaches.getInstance().getKost2IfNotInitialized(kost2); - if (useKost2 != null) { - if (useKost2.getWorkFraction() != null) { - return (long) (useKost2.getWorkFraction().doubleValue() * millis); - } - final Kost2ArtDO kost2Art = PfCaches.getInstance().getKost2ArtIfNotInitialized(useKost2.getKost2Art()); - if (kost2Art.getWorkFraction() != null) { - return (long) (kost2Art.getWorkFraction().doubleValue() * millis); - } - } - return this.millis; - } - - public long getMillis() - { - return millis; - } - - public String getFormattedDuration() - { - return MonthlyEmployeeReport.getFormattedDuration(millis); - } -} diff --git a/projectforge-business/src/main/java/org/projectforge/business/fibu/MonthlyEmployeeReportWeek.java b/projectforge-business/src/main/java/org/projectforge/business/fibu/MonthlyEmployeeReportWeek.java index a8a0a6f8ae..081070f3de 100644 --- a/projectforge-business/src/main/java/org/projectforge/business/fibu/MonthlyEmployeeReportWeek.java +++ b/projectforge-business/src/main/java/org/projectforge/business/fibu/MonthlyEmployeeReportWeek.java @@ -47,6 +47,8 @@ public class MonthlyEmployeeReportWeek implements Serializable { private long totalDuration = 0; + private long totalGrossDuration = 0; + /** * Key is kost2 id. */ @@ -110,19 +112,14 @@ void addEntry(TimesheetDO sheet, final boolean hasSelectAccess) { } long duration = sheet.getDuration(); entry.addMillis(duration); - totalDuration += duration; + totalDuration += entry.getWorkFractionMillis(); + totalGrossDuration += duration; } - /** - * @see StringHelper#format2DigitNumber(int) - */ public String getFormattedFromDayOfMonth() { return StringHelper.format2DigitNumber(fromDate.getDayOfMonth()); } - /** - * @see StringHelper#format2DigitNumber(int) - */ public String getFormattedToDayOfMonth() { return StringHelper.format2DigitNumber(toDate.getDayOfMonth()); } @@ -138,6 +135,10 @@ public String getFormattedTotalDuration() { return MonthlyEmployeeReport.getFormattedDuration(totalDuration); } + public String getFormattedGrossDuration() { + return MonthlyEmployeeReport.getFormattedDuration(totalGrossDuration); + } + /** * Return the hours assigned to the different Kost2's. The key of the map is the kost2 id. * diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/AuftragDO.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/AuftragDO.kt index 41e2fe411f..b734768f60 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/AuftragDO.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/AuftragDO.kt @@ -36,7 +36,6 @@ import org.projectforge.framework.DisplayNameCapable import org.projectforge.framework.i18n.I18nHelper import org.projectforge.framework.jcr.AttachmentsInfo import org.projectforge.framework.json.IdOnlySerializer -import org.projectforge.framework.json.IdsOnlySerializer import org.projectforge.framework.persistence.candh.CandHIgnore import org.projectforge.framework.persistence.entities.DefaultBaseDO import org.projectforge.framework.persistence.history.NoHistory diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/AuftragDao.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/AuftragDao.kt index 0a2378c890..050e634179 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/AuftragDao.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/AuftragDao.kt @@ -236,9 +236,6 @@ open class AuftragDao : BaseDao(AuftragDO::class.java) { eq("paymentSchedules.reached", true) ) ) - } else { - addCriterionForAuftragsStatuses(myFilter, queryFilter) - positionStatusAlreadyFilterd = true } if (myFilter.user != null) { @@ -271,30 +268,13 @@ open class AuftragDao : BaseDao(AuftragDO::class.java) { // Don't use filter for orders to be invoiced. list = list.toMutableList() // Make mutable list of Kotlin's immutable list. filterPositionsArten(myFilter, list) - if (!positionStatusAlreadyFilterd) { // Don't filter position status' again. - filterPositionsStatus(myFilter, list) - } + list = filterPositionsStatus(myFilter, list) filterPositionsPaymentTypes(myFilter, list) } return list } - private fun addCriterionForAuftragsStatuses(myFilter: AuftragFilter, queryFilter: QueryFilter) { - val auftragsStatuses = myFilter.auftragsStatuses - if (auftragsStatuses.isNotEmpty()) { - val orCriterions: MutableList = ArrayList() - orCriterions.add(isIn("status", auftragsStatuses)) - orCriterions.add(isIn("positionen.status", myFilter.auftragsStatuses)) - queryFilter.add(or(*orCriterions.toTypedArray())) - } - - // check deleted - if (!myFilter.ignoreDeleted) { - queryFilter.add(eq("positionen.deleted", myFilter.deleted)) - } - } - private fun createCriterionForErfassungsDatum(myFilter: AuftragFilter): Optional { val startDate = myFilter.startDate val endDate = myFilter.endDate @@ -335,19 +315,12 @@ open class AuftragDao : BaseDao(AuftragDO::class.java) { } } - private fun filterPositionsStatus(myFilter: AuftragFilter, list: List) { - if (CollectionUtils.isEmpty(myFilter.auftragsStatuses)) { - return - } - val statusFilter = AuftragsPositionsStatusFilter(myFilter.auftragsStatuses) - CollectionUtils.filter( - list - ) { `object`: AuftragDO? -> - statusFilter.match( - list.toMutableList(), - `object`!! - ) + private fun filterPositionsStatus(myFilter: AuftragFilter, list: List): List { + val auftragsStatuses: List = myFilter.auftragsStatuses + if (auftragsStatuses.isEmpty()) { + return list } + return list.filter { auftrag -> auftrag.positionenExcludingDeleted.any { pos -> auftragsStatuses.contains(pos.status) } } } private fun filterPositionsPaymentTypes(myFilter: AuftragFilter, list: List) { diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/MonthlyEmployeeReportEntry.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/MonthlyEmployeeReportEntry.kt new file mode 100644 index 0000000000..69e51835b4 --- /dev/null +++ b/projectforge-business/src/main/kotlin/org/projectforge/business/fibu/MonthlyEmployeeReportEntry.kt @@ -0,0 +1,85 @@ +//////////////////////////////////////////////////////////////////////////// +// Project ProjectForge Community Edition +// www.projectforge.org +// +// Copyright (C) 2001-2025 Micromata GmbH, Germany (www.micromata.com) +// +// ProjectForge is dual-licensed. +// +// This community edition is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License as published +// by the Free Software Foundation; version 3 of the License. +// +// This community edition 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 General +// Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, see http://www.gnu.org/licenses/. +// +////////////////////////////////////////////////////////////////////////// + +package org.projectforge.business.fibu + +import org.projectforge.business.PfCaches.Companion.instance +import org.projectforge.business.fibu.kost.Kost2DO +import org.projectforge.business.task.TaskDO +import java.io.Serializable +import java.math.BigDecimal + +/** + * Repräsentiert einen Eintrag innerhalb eines Wochenberichts eines Mitarbeiters zu einem Kostenträger (Anzahl Stunden). + * @author Kai Reinhard (k.reinhard@micromata.de) + */ +class MonthlyEmployeeReportEntry : Serializable { + /** + * Only given, if task is not given and vice versa. + */ + var kost2: Kost2DO? = null + private set + + /** + * Only given, if kost2 is not given and vice versa. + */ + var task: TaskDO? = null + private set + + var millis: Long = 0 + private set + + constructor(kost2: Kost2DO?) { + this.kost2 = kost2 + } + + constructor(task: TaskDO?) { + this.task = task + } + + fun addMillis(millis: Long) { + this.millis += millis + } + + /** + * If this entry has a kost2 with a working time fraction set or a kost2art with a working time fraction set then the fraction of millis + * will be returned. + */ + val workFractionMillis: Long + get() = workFraction.multiply(millis.toBigDecimal()).toLong() + + val workFraction: BigDecimal + get() { + val useKost2 = instance.getKost2IfNotInitialized(kost2) ?: return BigDecimal.ONE + useKost2.workFraction?.let { + return it + } + return instance.getKost2ArtIfNotInitialized(useKost2.kost2Art)?.workFraction ?: BigDecimal.ONE + } + + val formattedDuration: String + get() = MonthlyEmployeeReport.getFormattedDuration(millis) + + companion object { + private const val serialVersionUID = 7290000602224467755L + } +} diff --git a/projectforge-business/src/main/kotlin/org/projectforge/business/scripting/ScriptExecutor.kt b/projectforge-business/src/main/kotlin/org/projectforge/business/scripting/ScriptExecutor.kt index 1805fa3bfc..a3a26a20cb 100644 --- a/projectforge-business/src/main/kotlin/org/projectforge/business/scripting/ScriptExecutor.kt +++ b/projectforge-business/src/main/kotlin/org/projectforge/business/scripting/ScriptExecutor.kt @@ -26,6 +26,7 @@ package org.projectforge.business.scripting import mu.KotlinLogging import org.apache.commons.lang3.StringUtils import org.projectforge.ProjectForgeVersion +import org.projectforge.business.PfCaches import org.projectforge.business.fibu.ForecastExport import org.projectforge.business.fibu.kost.reporting.ReportGeneratorList import org.projectforge.business.task.ScriptingTaskTree @@ -94,6 +95,7 @@ abstract class ScriptExecutor( variables["log"] = scriptLogger variables["reportList"] = ReportGeneratorList() variables["i18n"] = I18n() + variables["caches"] = PfCaches.instance variables["forecastExport"] = ApplicationContextProvider.getApplicationContext().getBean(ForecastExport::class.java) for (entry in Registry.getInstance().orderedList) { diff --git a/projectforge-rest/src/main/kotlin/org/projectforge/rest/TimesheetPagesRest.kt b/projectforge-rest/src/main/kotlin/org/projectforge/rest/TimesheetPagesRest.kt index b56a7a5062..c63ca7f79f 100644 --- a/projectforge-rest/src/main/kotlin/org/projectforge/rest/TimesheetPagesRest.kt +++ b/projectforge-rest/src/main/kotlin/org/projectforge/rest/TimesheetPagesRest.kt @@ -300,7 +300,6 @@ class TimesheetPagesRest : AbstractDTOPagesRest log.info(msg) + else -> log.error(msg) + } + return ResponseEntity(msg, exInfo.status) + } } diff --git a/projectforge-wicket/src/main/java/org/projectforge/web/fibu/MonthlyEmployeeReportPage.java b/projectforge-wicket/src/main/java/org/projectforge/web/fibu/MonthlyEmployeeReportPage.java index 09f4e35bc3..580b6df04b 100644 --- a/projectforge-wicket/src/main/java/org/projectforge/web/fibu/MonthlyEmployeeReportPage.java +++ b/projectforge-wicket/src/main/java/org/projectforge/web/fibu/MonthlyEmployeeReportPage.java @@ -339,7 +339,7 @@ private void addReport() { "font-weight: bold; color:red; text-align: right;"))); } if (report.getTotalGrossDuration() != report.getTotalNetDuration()) { - // Net sum row. + // Gross sum row. final WebMarkupContainer row = new WebMarkupContainer(rowRepeater.newChildId()); rowRepeater.add(row); if (rowCounter++ % 2 == 0) { @@ -359,7 +359,7 @@ private void addReport() { final RepeatingView colWeekRepeater = new RepeatingView("colWeekRepeater"); row.add(colWeekRepeater); for (@SuppressWarnings("unused") final MonthlyEmployeeReportWeek week : report.getWeeks()) { - colWeekRepeater.add(new Label(colWeekRepeater.newChildId(), "")); + colWeekRepeater.add(new Label(colWeekRepeater.newChildId(), week.getFormattedGrossDuration())); } row.add(new Label("sum", report.getFormattedTotalGrossDuration()).add(AttributeModifier.replace("style", "font-weight: bold; text-align: right;")));