Skip to content

Commit

Permalink
Merge pull request #256 from micromata/Release-8.1-SNAPSHOT
Browse files Browse the repository at this point in the history
Release 8.1 snapshot
GlobalErrorHandling improved, Forecast supports now snapshots, ScriptingLogger -> ThreadLocal
  • Loading branch information
kreinhard authored Jan 15, 2025
2 parents b3dfe80 + 946458b commit 44e8025
Show file tree
Hide file tree
Showing 23 changed files with 682 additions and 318 deletions.
21 changes: 20 additions & 1 deletion ToDo.adoc
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
Aktuell:
- KI-Anteil in Zeitberichten
- Viewpage für user für non-admins.
- Scripting: Ergebnis Unresolved reference 'memo', 'todo'.: line 94 to 94 (add only activated plugins)
- Can't get value type from 'org.projectforge.business.timesheet.TimesheetPrefData'. Class not found (old incompatible ProjectForge version)?
- Summen in DB-Exporten eintragen und serverseitig evaluieren.
Expand Down Expand Up @@ -60,12 +62,27 @@ docker volume rm <volume-name>

Postgresql-Dump-Imports bechleunigen:

docker run --name projectforge-postgres -p 127.0.0.1:5432:5432 -e POSTGRES_PASSWORD=$PGPASSWORD -e POSTGRES_USER=projectforge -d postgres:13.18
docker run -e PGPASSWORD=$PGPASSWORD -it --rm --link projectforge-postgres:postgres postgres:13.18 psql -h postgres -U projectforge

ALTER SYSTEM SET fsync = off;
ALTER SYSTEM SET synchronous_commit = off;
SET maintenance_work_mem = '512MB';

gunzip projectforge-*.sql.gz
docker run -v ~/ProjectForgeBackup/pf.sql:/mnt/pf.sql -e PGPASSWORD=$PGPASSWORD -it --rm --link projectforge-postgres:postgres postgres:13.18 psql -h postgres -U projectforge -q -f /mnt/pf.sql
drop view v_t_pf_user;
update t_pf_user_password SET password_hash='SHA{BC871652288E56E306CFA093BEFC3FFCD0ED8872}', password_salt=null;
update t_pf_user SET password='SHA{BC871652288E56E306CFA093BEFC3FFCD0ED8872}', password_salt=null, email='m.developer@localhost';
update t_calendar set ext_subscription=false;
insert into t_pf_user_password (pk,deleted,user_id,password_hash) values(2,false,2,'SHA{BC871652288E56E306CFA093BEFC3FFCD0ED8872}');
Orderbook-Export über die GUI Auftragsbuch -> Dev: export order book
\c postgres;
DROP DATABASE projectforge;
CREATE DATABASE projectforge;
Orderbooks importieren:
Expand All @@ -75,5 +92,7 @@ docker cp ~/ProjectForgeBackup/ProjectForge-Orderbook_*.gz projectforge-postgres
INSERT INTO t_fibu_orderbook_snapshots (date, created, serialized_orderbook, size) VALUES ('2023-11-01', NOW(), pg_read_binary_file(:'file_path')::bytea, (pg_stat_file(:'file_path')).size);

docker run -e PGPASSWORD=$PGPASSWORD -it --rm --link projectforge-postgres:postgres postgres:13.18 pg_dump -h postgres -U projectforge --data-only --column-inserts --table=t_fibu_orderbook_snapshots

remove duplicates
psql -f export.sql


4 changes: 3 additions & 1 deletion projectforge-application/src/main/resources/i18nKeys.json
Original file line number Diff line number Diff line change
Expand Up @@ -818,7 +818,7 @@
{"i18nKey":"fibu.auftrag.filter.type.vollstaendigFakturiert","bundleName":"I18nResources","translation":"completely invoiced","translationDE":"vollständig fakturiert","usedInClasses":["org.projectforge.business.fibu.AuftragFakturiertFilterStatus"],"usedInFiles":[]},
{"i18nKey":"fibu.auftrag.filter.type.zuFakturieren","bundleName":"I18nResources","translation":"to be invoiced","translationDE":"zu fakturieren","usedInClasses":["org.projectforge.business.fibu.AuftragFakturiertFilterStatus"],"usedInFiles":[]},
{"i18nKey":"fibu.auftrag.forecast","bundleName":"I18nResources","translation":"Forecast","translationDE":"Forecast","usedInClasses":["org.projectforge.business.fibu.ForecastOrderAnalysis"],"usedInFiles":[]},
{"i18nKey":"fibu.auftrag.forecast.lostBudgetWarning","bundleName":"I18nResources","translation":"Budget Loss Warning: The budget is expected to be undershot by more than {0}%: {1}. For analysis details see the order editing page.","translationDE":"Budget-Verlust-Warnung: Das Budget wird um voraussichtlich mehr als {0} % unterschritten: {1}. Für Analysedetails siehe Auftrageditierseite.","usedInClasses":["org.projectforge.business.fibu.ForecastExport"],"usedInFiles":[]},
{"i18nKey":"fibu.auftrag.forecast.lostBudgetWarning","bundleName":"I18nResources","translation":"{1} Budget Loss Warning: The budget is expected to be undershot by more than {0}%. For analysis details see the order editing page.","translationDE":"{1} Budget-Verlust-Warnung: Das Budget wird um voraussichtlich mehr als {0} % unterschritten. Für Analysedetails siehe Auftrageditierseite.","usedInClasses":["org.projectforge.business.fibu.ForecastExport"],"usedInFiles":[]},
{"i18nKey":"fibu.auftrag.forecastExportAsXls","bundleName":"I18nResources","translation":"Forecast","translationDE":"Forecast","usedInClasses":["org.projectforge.web.fibu.AuftragListPage"],"usedInFiles":[]},
{"i18nKey":"fibu.auftrag.forecastExportAsXls.tooltip","bundleName":"I18nResources","translation":"Forecast export for actual orderbook list. The start date will be taken from the period of performance begin. If not present it uses 01/01/[actual year].","translationDE":"Export des Forecasts für die aktuelle Auftragsliste. Das Startdatum wird aus dem Leistungszeitraum Start Filter verwendet. Wenn nicht angegeben wird der 01.01.[aktuelles Jahr] verwendet.","usedInClasses":["org.projectforge.web.fibu.AuftragListPage"],"usedInFiles":[]},
{"i18nKey":"fibu.auftrag.hint.kannVonProjektKundenAbweichen","bundleName":"I18nResources","translation":"Customer can be different to the customer of the project.","translationDE":"Kunde kann vom Projektkunden abweichen.","usedInClasses":["org.projectforge.web.fibu.AuftragEditForm"],"usedInFiles":[]},
Expand Down Expand Up @@ -2402,6 +2402,8 @@
{"i18nKey":"timeleft.years","bundleName":"I18nResources","translation":"in {0} years","translationDE":"in {0} Jahren","usedInClasses":[],"usedInFiles":[]},
{"i18nKey":"timeleft.years.one","bundleName":"I18nResources","translation":"in a year","translationDE":"in einem Jahr","usedInClasses":[],"usedInFiles":[]},
{"i18nKey":"timesheet","bundleName":"I18nResources","translation":"Time-sheet","translationDE":"Zeitbericht","usedInClasses":["org.projectforge.Constants","org.projectforge.business.timesheet.TimesheetFavoritesService","org.projectforge.framework.persistence.DaoConst","org.projectforge.registry.Registry","org.projectforge.rest.calendar.CalendarServicesRest","org.projectforge.rest.calendar.FullCalendarEvent","org.projectforge.rest.calendar.TimesheetEventsProvider","org.projectforge.web.calendar.TimesheetEventsProvider","org.projectforge.web.timesheet.TimesheetEditPage","org.projectforge.web.timesheet.TimesheetListPage"],"usedInFiles":[]},
{"i18nKey":"timesheet.ai.minutesSavedByAI","bundleName":"I18nResources","translation":"Minutes saved by AI","translationDE":"Gesparte Minuten durch KI","usedInClasses":["org.projectforge.business.timesheet.TimesheetDO"],"usedInFiles":[]},
{"i18nKey":"timesheet.ai.minutesSavedByAI.info","bundleName":"I18nResources","translation":"Estimated minutes saved by using AI for this time sheet.","translationDE":"Geschätze Zeitersparnis in Minuten durch den Einsatz von KI in diesem Zeitbericht.","usedInClasses":["org.projectforge.business.timesheet.TimesheetDO"],"usedInFiles":[]},
{"i18nKey":"timesheet.break","bundleName":"I18nResources","translation":"Break","translationDE":"leer","usedInClasses":["org.projectforge.rest.calendar.TimesheetEventsProvider","org.projectforge.web.calendar.TimesheetEventsProvider"],"usedInFiles":[]},
{"i18nKey":"timesheet.description","bundleName":"I18nResources","translation":"Activity report","translationDE":"Tätigkeitsbericht","usedInClasses":["org.projectforge.business.humanresources.HRPlanningExport","org.projectforge.business.timesheet.TimesheetDO","org.projectforge.rest.TimesheetPagesRest","org.projectforge.web.timesheet.TimesheetEditForm","org.projectforge.web.timesheet.TimesheetEditSelectRecentDialogPanel"],"usedInFiles":[]},
{"i18nKey":"timesheet.duration","bundleName":"I18nResources","translation":"Duration","translationDE":"Dauer","usedInClasses":["org.projectforge.business.timesheet.TimesheetExport","org.projectforge.renderer.custom.MicromataFormatter","org.projectforge.rest.TimesheetPagesRest","org.projectforge.web.calendar.CalendarForm","org.projectforge.web.teamcal.event.MyWicketEvent","org.projectforge.web.timesheet.TimesheetListPage"],"usedInFiles":[]},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ object ExceptionStackTracePrinter {
*/
@JvmStatic
@JvmOverloads
fun toString(ex: Exception, showExceptionMessage: Boolean = true, stopBeforeForeignPackages: Boolean = true, depth: Int = 10, vararg showPackagesOnly: String): String {
fun toString(ex: Throwable, showExceptionMessage: Boolean = true, stopBeforeForeignPackages: Boolean = true, depth: Int = 10, vararg showPackagesOnly: String): String {
val sb = StringBuilder()
if (showExceptionMessage) {
sb.append(ex::class.java.name).append(":").append(ex.message).append("\n")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,16 @@

package org.projectforge.business.fibu

import de.micromata.merlin.I18n
import de.micromata.merlin.excel.ExcelSheet
import de.micromata.merlin.excel.ExcelWorkbook
import de.micromata.merlin.excel.ExcelWriterContext
import mu.KotlinLogging
import org.apache.poi.ss.usermodel.IndexedColors
import org.projectforge.Constants
import org.projectforge.business.excel.ExcelDateFormats
import org.projectforge.business.excel.XlsContentProvider
import org.projectforge.business.fibu.kost.ProjektCache
import org.projectforge.business.fibu.orderbooksnapshots.OrderbookSnapshotsService
import org.projectforge.business.scripting.ScriptLogger
import org.projectforge.business.scripting.ThreadLocalScriptingContext
import org.projectforge.business.task.TaskTree
import org.projectforge.business.user.ProjectForgeGroup
import org.projectforge.common.DateFormatType
Expand Down Expand Up @@ -144,14 +143,42 @@ open class ForecastExport { // open needed by Wicket.
val percentageCellStyle = workbook.createOrGetCellStyle("DataFormat.percentage")
val boldRedFont = ExcelUtils.createFont(
workbook,
"boldRed",
"boldRedFont",
bold = true,
heightInPoints = 10,
color = IndexedColors.RED.index,
color = IndexedColors.DARK_RED.index,
)
val boldRedLargeFont = ExcelUtils.cloneFont(workbook, "boldRedLargeFont", boldRedFont, heightInPoints = 12)
val boldRedHugeFont = ExcelUtils.cloneFont(workbook, "boldRedLargeFont", boldRedFont, heightInPoints = 14)
val errorCellStyle = ExcelUtils.createCellStyle(workbook, "error", font = boldRedFont)
val writerContext =
ExcelWriterContext(I18n(Constants.RESOURCE_BUNDLE_NAME, ThreadLocalUserContext.locale), workbook)
val largeErrorCellStyle = ExcelUtils.cloneStyle(
workbook,
"largeError",
errorCellStyle,
fillForegroundColor = IndexedColors.LIGHT_YELLOW,
font = boldRedLargeFont
)
val hugeErrorCellStyle = ExcelUtils.cloneStyle(
workbook,
"hugeError",
errorCellStyle,
fillForegroundColor = IndexedColors.LIGHT_ORANGE,
font = boldRedHugeFont
)
init {
currencyCellStyle.dataFormat = workbook.getDataFormat(XlsContentProvider.FORMAT_CURRENCY)
percentageCellStyle.dataFormat = workbook.getDataFormat("0%")
boldRedFont.fontName = "Arial"
}

val errorCurrencyCellStyle = // currencyCellStyle of errorCellStyle must be set before.
ExcelUtils.cloneStyle(
workbook,
"errorCurrency",
currencyCellStyle,
font = boldRedFont,
fillForegroundColor = IndexedColors.LIGHT_YELLOW,
)
val orderMap = mutableMapOf<Long, OrderInfo>()

// All projects of the user used in the orders to show also invoices without order, but with assigned project:
Expand All @@ -160,12 +187,6 @@ open class ForecastExport { // open needed by Wicket.
false // showAll is true, if no filter is given and for financial and controlling staff only.
val orderPositionMap = mutableMapOf<Long, OrderPositionInfo>()
val orderMapByPositionId = mutableMapOf<Long, OrderInfo>()

init {
currencyCellStyle.dataFormat = workbook.getDataFormat(XlsContentProvider.FORMAT_CURRENCY)
percentageCellStyle.dataFormat = workbook.getDataFormat("0%")
boldRedFont.fontName = "Arial"
}
}

@JvmOverloads
Expand All @@ -181,12 +202,32 @@ open class ForecastExport { // open needed by Wicket.
filter.periodOfPerformanceStartDate =
startDate.plusYears(-2).localDate // Go 2 years back for getting all orders referred by invoices of prior year.
filter.user = origFilter.user
val orderList = if (snapshotDate != null) {
log.info { "Exporting forecast script for date ${startDate.isoString} with snapshotDate ${snapshotDate}, projects=${filter.projectList?.joinToString { it.name ?: "???" }}" }
orderbookSnapshotsService.readSnapshot(snapshotDate)?.filter { filter.match(it) }
val scriptLogger = ThreadLocalScriptingContext.getLogger()
var closesSnapshotDate = snapshotDate
if (snapshotDate != null) {
orderbookSnapshotsService.selectClosestSnapshotDate(snapshotDate)?.let {
closesSnapshotDate = it
}
if (closesSnapshotDate != snapshotDate) {
val msg = "No snapshot found for date $snapshotDate. Using closest snapshot date $closesSnapshotDate."
scriptLogger?.warn { msg } ?: log.warn { msg }
}
}
val msgSB = StringBuilder("Exporting forecast script for date ${startDate.isoString}")
if (closesSnapshotDate != null) {
msgSB.append(" with snapshotDate ${closesSnapshotDate}")
} else if (!filter.searchString.isNullOrBlank()) {
msgSB.append(" with filter: str='${filter.searchString}'")
}
if (!filter.projectList.isNullOrEmpty()) {
msgSB.append(", projects=${filter.projectList?.joinToString { it.name ?: "???" }}")
}
val orderList = if (closesSnapshotDate != null) {
scriptLogger?.info { msgSB } ?: log.info { msgSB }
orderbookSnapshotsService.readSnapshot(closesSnapshotDate!!)?.filter { filter.match(it) }
?.sortedByDescending { it.nummer } ?: emptyList()
} else {
log.info { "Exporting forecast script for date ${startDate.isoString} with filter: str='${filter.searchString ?: ""}', projects=${filter.projectList?.joinToString { it.name ?: "???" }}" }
scriptLogger?.info { msgSB } ?: log.info { msgSB }
orderDao.select(filter)
}
val showAll = accessChecker.isLoggedInUserMemberOfGroup(
Expand All @@ -195,7 +236,13 @@ open class ForecastExport { // open needed by Wicket.
) &&
filter.searchString.isNullOrBlank() &&
filter.projectList.isNullOrEmpty()
return xlsExport(orderList, startDate = startDate, snapshotDate = snapshotDate, showAll = showAll)
return xlsExport(
orderList,
startDate = startDate,
snapshotDate = closesSnapshotDate,
showAll = showAll,
scriptLogger = scriptLogger
)
}

private fun getStartDate(origFilter: AuftragFilter): PFDay {
Expand All @@ -222,7 +269,8 @@ open class ForecastExport { // open needed by Wicket.
): String {
val startDateString = "-start_${startDate.year}-${startDate.monthValue.format2Digits()}"
val partString = if (part.isNullOrBlank()) "" else "-$part"
val snapshotString = if (snapshot != null) "-snapshot_${snapshot}" else ""
val useSnapshot = orderbookSnapshotsService.selectClosestSnapshotDate(snapshot)
val snapshotString = if (useSnapshot != null) "-snapshot_${useSnapshot}" else ""
return "Forecast$partString$snapshotString${startDateString}-created_${DateHelper.getDateAsFilenameSuffix(Date())}${extension ?: ""}"
}

Expand All @@ -237,14 +285,16 @@ open class ForecastExport { // open needed by Wicket.
* @return The byte array of the Excel file.
*/
@Throws(IOException::class)
open fun xlsExport(
private fun xlsExport(
orderList: Collection<AuftragDO>,
startDate: PFDay,
showAll: Boolean,
snapshotDate: LocalDate? = null,
snapshotDate: LocalDate?,
scriptLogger: ScriptLogger?,
): ByteArray? {
if (orderList.isEmpty()) {
log.info { "No orders found for export." }
val msg = "No orders found for export."
scriptLogger?.info { msg } ?: log.info { msg } // scriptLogger does also log.info
// No orders found, so we don't need the forecast sheet.
return null
}
Expand Down Expand Up @@ -344,7 +394,8 @@ open class ForecastExport { // open needed by Wicket.
}
}
if (!orderPositionFound) {
log.info { "No orders positions found for export." }
val msg = "No orders positions found for export."
scriptLogger?.info { msg } ?: log.info { msg } // scriptLogger does also log.info
// No order positions found, so we don't need the forecast sheet.
return null
}
Expand Down Expand Up @@ -372,7 +423,7 @@ open class ForecastExport { // open needed by Wicket.
cell.evaluateFormularCell()
}
}

workbook.pOIWorkbook.creationHelper.createFormulaEvaluator().evaluateAll()
return workbook.asByteArrayOutputStream.toByteArray()
}
}
Expand Down Expand Up @@ -623,32 +674,26 @@ open class ForecastExport { // open needed by Wicket.
)
cell.cellStyle = ctx.currencyCellStyle
if (monthEntry.lostBudgetWarning) {
val errorStyle = when {
monthEntry.lostBudget > NumberHelper.HUNDRED_THOUSAND -> ctx.hugeErrorCellStyle
monthEntry.lostBudget > NumberHelper.TEN_THOUSAND -> ctx.largeErrorCellStyle
else -> ctx.errorCellStyle
}
sheet.setStringValue(
row,
ForecastCol.WARNING.header,
translateMsg(
"fibu.auftrag.forecast.lostBudgetWarning",
monthEntry.lostBudget.formatCurrency(true, scale = 0),
monthEntry.lostBudgetPercent,
ForecastOrderPosInfo.PERCENTAGE_OF_LOST_BUDGET_WARNING,
monthEntry.lostBudget.formatCurrency()
)
).cellStyle = ctx.errorCellStyle
highlightErrorCell(ctx, row, columnDef.columnNumber)
).cellStyle = errorStyle
sheet.getCell(row, columnDef.columnNumber)?.cellStyle = ctx.errorCurrencyCellStyle
}
}
}

private fun highlightErrorCell(ctx: Context, rowNumber: Int, colNumber: Int) {
val excelRow = ctx.forecastSheet.getRow(rowNumber)
val excelCell = excelRow.getCell(colNumber)
ctx.writerContext.cellHighlighter.highlightErrorCell(
excelCell,
ctx.writerContext,
ctx.forecastSheet,
ctx.forecastSheet.getColumnDef(0),
excelRow
)
}

private fun getMonthIndex(ctx: Context, date: PFDay): Int {
val monthDate = date.year * 12 + date.monthValue
val monthBaseDate = ctx.startDate.year * 12 + ctx.startDate.monthValue
Expand Down
Loading

0 comments on commit 44e8025

Please sign in to comment.