Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 8.1 snapshot #282

Merged
merged 4 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion ToDo.adoc
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
==== Aktuell:
- History User/Gruppen z. B. ScriptDO: DisplayName anstelle von Ids.
- Änderungskommentar bei Usern/Gruppen (JIRA-Nummern)
- AI timesavings: Zeit bei Eingabe ausrechnen, recent ai texte als vorlage.
- JCR: Tool for removing or recovering orphaned nodes.
Expand Down
10 changes: 5 additions & 5 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,11 @@ org-apache-httpcomponents-core5 = "5.3.1" # Needed by httpclient5
org-apache-jackrabbit-oak = "1.74.0"
org-apache-poi = "5.3.0"
org-apache-poi-ooxml = "5.3.0"
org-apache-wicket-myextensions = "10.3.0"
org-apache-wicket-spring = "10.3.0"
org-apache-wicket-tester = "10.3.0"
org-apache-wicket-myextensions = "10.4.0"
org-apache-wicket-spring = "10.4.0"
org-apache-wicket-tester = "10.4.0"
org-wicketstuff-html5 = "10.4.0"
org-wicketstuff-select2 = "10.4.0"
org-apache-xmlgraphics-batik = "1.17"
org-apache-xmlgraphics-fop = "2.10"
org-apache-xmlgraphics-commons = "2.9"
Expand Down Expand Up @@ -76,8 +78,6 @@ org-springframework-boot = "3.3.8"
org-apache-tomcat-embed = "10.1.33" # must match spring-boot
org-springframework-spring = "6.1.16"
org-springframework-security = "6.4.1"
org-wicketstuff-html5 = "10.2.0"
org-wicketstuff-select2 = "10.2.0"
se-sawano-java-alphanumeric-comparator = "1.4.1"

# logging
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,15 +135,20 @@ open class ForecastExport { // open needed by Wicket.
) &&
filter.searchString.isNullOrBlank() &&
filter.projectList.isNullOrEmpty()
return xlsExport(
orderList,
startDate = startDate,
planningDate = closestPlanningDate,
snapshotDate = closestSnapshotDate,
showAll = showAll,
auftragFilter = filter,
scriptLogger = scriptLogger
)
try {
return xlsExport(
orderList,
startDate = startDate,
planningDate = closestPlanningDate,
snapshotDate = closestSnapshotDate,
showAll = showAll,
auftragFilter = filter,
scriptLogger = scriptLogger
)
} catch (ex: Exception) {
log.error(ex) { "Error exporting forecast: $ex" }
throw ex
}
}

private fun getStartDate(origFilter: AuftragFilter): PFDay {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@ import org.projectforge.business.fibu.orderbooksnapshots.OrderbookSnapshotsServi
import org.projectforge.common.extensions.formatCurrency
import org.projectforge.common.extensions.formatForUser
import org.projectforge.common.extensions.formatFractionAsPercent
import org.projectforge.common.html.CssClass
import org.projectforge.common.html.Html
import org.projectforge.common.html.HtmlDocument
import org.projectforge.common.html.HtmlTable
import org.projectforge.common.html.*
import org.projectforge.framework.i18n.translate
import org.projectforge.framework.time.PFDateTime
import org.projectforge.framework.time.PFDay
Expand Down Expand Up @@ -122,6 +119,11 @@ class ForecastOrderAnalysis {
val orderInfo =
loadOrder(orderId = orderId, orderNumber = orderNumber, snapshotDate)
?: return noAnalysis("Order with id $orderId or positions not found.")
return htmlExport(orderInfo)
}

fun htmlExport(orderInfo: OrderInfo, ): String {
val orderId = orderInfo.id
val list = exportOrderAnalysis(orderInfo)
?: return noAnalysis("No order positions found for order #${orderInfo.nummer}.")
val firstMonth = list.flatMap { it.months }.minByOrNull { it.date }?.date
Expand Down Expand Up @@ -187,6 +189,23 @@ class ForecastOrderAnalysis {
// Forecast for all positions
//
html.add(Html.H2("${translate("fibu.auftrag.forecast")} all positions"))
html.add(
Html.Alert(Html.Alert.Type.INFO).also { div ->
div.add(Html.P("Distribution of Forecast (Monatsverteilung)").add(CssClass.BOLD))
div.add(HtmlList(HtmlList.Type.ORDERED).also { list ->
list.addItem("The payment plan is used first, if available.")
.addItem().also { item ->
item.add("Fixed price", bold = true)
.add(" projects are scheduled at the end of the performance period.")
}
.addItem().also { item ->
item.add("Time and materials", bold = true)
.add(" and ")
.add("Flat-rate", bold = true)
.add(" projects are distributed monthly during the performance period.")
}
})
})
html.add(HtmlTable().also { table ->
val headRow = table.addHeadRow()
headRow.addTH(translate("label.position.short"))
Expand Down Expand Up @@ -220,7 +239,7 @@ class ForecastOrderAnalysis {
tr.addTH("Sum")
totals.forEach {
tr.addTD(it.formatCurrency()).also {
it.addClasses(CssClass.ALIGN_RIGHT, CssClass.BOLD)
it.add(CssClass.ALIGN_RIGHT, CssClass.BOLD)
}
}
}
Expand All @@ -230,10 +249,23 @@ class ForecastOrderAnalysis {
//
list.forEach { fcPosInfo ->
val posInfo = fcPosInfo.orderPosInfo
html.add(Html.H2("${translate("fibu.auftrag.position")} #${posInfo.number}", id = "pos${posInfo.number}"))
html.add(
Html.H2(
"${translate("fibu.auftrag.position")} #${posInfo.number}",
id = "pos${posInfo.number}"
)
)
if (fcPosInfo.lostBudgetWarning) {
html.add(Html.Alert(Html.Alert.Type.DANGER).also { div ->
div.add(Html.Text("There is a lost-budget warning of ${fcPosInfo.lostBudget.formatCurrency(true)}"))
div.add(
Html.Text(
"There is a lost-budget warning of ${
fcPosInfo.lostBudget.formatCurrency(
true
)
}"
)
)
})
}
// Position information:
Expand All @@ -251,7 +283,14 @@ class ForecastOrderAnalysis {
addRow(
table,
translate("fibu.auftrag.forecastType"),
"${translate(ForecastUtils.getForecastType(orderInfo, posInfo).i18nKey)}: ${translate("fibu.auftrag.forecastType.info")}"
"${
translate(
ForecastUtils.getForecastType(
orderInfo,
posInfo
).i18nKey
)
}: ${translate("fibu.auftrag.forecastType.info")}"
)
addRow(table, translate("fibu.auftrag.nettoSumme"), posInfo.netSum.formatCurrency(true))
addRow(
Expand Down Expand Up @@ -302,14 +341,15 @@ class ForecastOrderAnalysis {
tr.addTH(translate("fibu.common.reached"))
tr.addTH(translate("comment"), CssClass.EXPAND)
}
orderInfo.paymentScheduleEntries?.filter { it.positionNumber == posInfo.number }?.forEach { entry ->
table.addRow().also { row ->
row.addTD(entry.scheduleDate.formatForUser())
row.addTD(entry.amount.formatCurrency(), CssClass.ALIGN_RIGHT)
row.addTD(translate(entry.reached))
row.addTD(entry.comment, CssClass.EXPAND)
orderInfo.paymentScheduleEntries?.filter { it.positionNumber == posInfo.number }
?.forEach { entry ->
table.addRow().also { row ->
row.addTD(entry.scheduleDate.formatForUser())
row.addTD(entry.amount.formatCurrency(), CssClass.ALIGN_RIGHT)
row.addTD(translate(entry.reached))
row.addTD(entry.comment, CssClass.EXPAND)
}
}
}
})
// Forecast for position:
html.add(Html.H3("${translate("fibu.auftrag.forecast")} #${posInfo.number}")) // Forecast current position
Expand All @@ -332,7 +372,12 @@ class ForecastOrderAnalysis {
}
}

private fun addRow(table: HtmlTable, label: String, value: BigDecimal?, suppressZero: Boolean = true) {
private fun addRow(
table: HtmlTable,
label: String,
value: BigDecimal?,
suppressZero: Boolean = true
) {
if (suppressZero && (value == null || value.abs() < BigDecimal.ONE)) {
return
}
Expand All @@ -346,7 +391,10 @@ class ForecastOrderAnalysis {
return HtmlDocument(msg).add(Html.Alert(Html.Alert.Type.DANGER, msg)).toString()
}

private fun addForecastValue(row: HtmlTable.TR, month: ForecastOrderPosInfo.MonthEntry): BigDecimal {
private fun addForecastValue(
row: HtmlTable.TR,
month: ForecastOrderPosInfo.MonthEntry
): BigDecimal {
val cssClass = if (month.lostBudgetWarning) CssClass.ERROR else CssClass.ALIGN_RIGHT
val amount = maxOf(month.toBeInvoicedSum, month.invoicedSum)
val style =
Expand All @@ -355,7 +403,10 @@ class ForecastOrderAnalysis {
return amount
}

private fun filterInvoices(posInfo: OrderPositionInfo, snapshotDate: LocalDate?): Collection<RechnungPosInfo>? {
private fun filterInvoices(
posInfo: OrderPositionInfo,
snapshotDate: LocalDate?
): Collection<RechnungPosInfo>? {
val invoicePositions = auftragsRechnungCache.getRechnungsPosInfosByAuftragsPositionId(posInfo.id)
return if (snapshotDate == null) {
invoicePositions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ class OrderInfo : Serializable {
kundeId = order.kunde?.id
kundeAsString = order.kundeAsString
projektId = order.projekt?.id
projektAsString = order.projektAsString
projektAsString = order.projekt?.name
probabilityOfOccurrence = order.probabilityOfOccurrence
forecastType = order.forecastType
periodOfPerformanceBegin = order.periodOfPerformanceBegin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ internal class OrderConverterService {
}
info.calculateInvoicedSum(info.infoPositions)
info.kundeAsString = kundeAsString
info.projektAsString = projektAsString
info.projektAsString = projekt?.name
info.updatePaymentScheduleEntries(paymentSchedules)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ import org.projectforge.framework.time.DateTimeFormatter
import org.projectforge.framework.utils.NumberHelper
import java.math.BigDecimal

/**
* Central functions for calculating and formatting AI time savings.
*/
object AITimeSavings {
class Stats {
var totalDurationMillis: Long = 0
Expand Down Expand Up @@ -60,8 +63,8 @@ object AITimeSavings {
}

/**
* Returns the formatted percentage.
* Example: "10 %"
* Returns the formatted percentage (scale = 1 for <10 %. Otherwise, scale = 0).
* Example: "3,6 %", "10 %", etc.
* @param percent
* @return The formatted percentage. If the percent is null, "0 %" is returned.
*/
Expand All @@ -75,7 +78,7 @@ object AITimeSavings {

/**
* Returns the formatted time saved by AI.
* Example: "1:30h, 10 %"
* Example: "0:30h, 5,7 %", "1:30h, 10 %", etc.
* @param timesheet
* @param emptyStringIfNull If true, an empty string is returned if the time saved by AI is null.
* @return The formatted time saved by AI.
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class Html {
}

class Text(content: String, replaceNewlinesByBr: Boolean = true) :
HtmlElement("NOTAG", content = content, childrenAllowed = false, replaceNewlinesByBr = replaceNewlinesByBr)
HtmlElement("NOTAG", content = content, childrenAllowed = true, replaceNewlinesByBr = replaceNewlinesByBr)

class BR : HtmlElement("br", childrenAllowed = false)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,31 +38,30 @@ open class HtmlElement(
val id: String? = null,
replaceNewlinesByBr: Boolean = false,
) {
val content: String? = if (content != null && childrenAllowed && replaceNewlinesByBr && content.contains('\n')) {
// Example: "Hello\nWorld" -> "Hello\n<br />\nWorld"
content.split('\n').forEachIndexed { index, s ->
if (index > 0) {
this.add(Html.BR())
}
if (s.isNotBlank()) {
add(Html.Text("$s\n", replaceNewlinesByBr = false))
}
}
null
} else {
content
}
val content: String? = appendContent(this, content, replaceNewlinesByBr)
var children: MutableList<HtmlElement>? = null
var attributes: MutableMap<String, String>? = null
var classnames: MutableList<String>? = null

fun addClasses(vararg cssClass: CssClass?) {
cssClass.forEach { it?.let { addClassname((it.cls)) } }
/**
* Adds the given CSS classes to this element.
* @param cssClasses The CSS classes to add
* @return This element for chaining.
*/
fun add(vararg cssClasses: CssClass?): HtmlElement {
cssClasses.forEach { it?.let { addClassname((it.cls)) } }
return this
}

fun addClassname(classname: String) {
/**
* Adds the given CSS classes to this element.
* @param classname The CSS classes to add
* @return This element for chaining.
*/
fun addClassname(classname: String): HtmlElement {
classnames = classnames ?: mutableListOf()
classnames!!.add(classname)
return this
}

fun attr(name: String, value: String) {
Expand All @@ -87,8 +86,23 @@ open class HtmlElement(
/**
* Adds a text element to this element. Convenience method for adding text to an element.
*/
fun addText(text: String): HtmlElement {
return add(Html.Text(text))
open fun add(
text: String,
bold: Boolean = false,
replaceNewlinesByBr: Boolean = true,
vararg cssClasses: CssClass
): HtmlElement {
if (bold || cssClasses.isNotEmpty()) {
add(Html.Span(text, replaceNewlinesByBr = replaceNewlinesByBr).also { span ->
span.add(cssClasses = cssClasses)
span.add(CssClass.BOLD)
})
} else {
appendContent(this, text, replaceNewlinesByBr)?.let { content ->
add(Html.Text(content, replaceNewlinesByBr = false))
}
}
return this
}

open fun append(sb: StringBuilder, indent: Int) {
Expand All @@ -97,6 +111,7 @@ open class HtmlElement(
if (!content.isNullOrBlank()) {
indent(sb, indent)
sb.append(escape(content.trim()))
children?.forEach { it.append(sb, indent + 1) }
sb.append("\n")
}
return
Expand Down Expand Up @@ -147,6 +162,32 @@ open class HtmlElement(
}
}

/**
* Appends the given [content] to the [parent] element.
* If [replaceNewlinesByBr] is true and the content contains newlines, the content will be split by newlines and
* each part will be added as a text element with a '<br />' element in between.
* @param parent The parent element to append the content to
* @param content The content to append
* @param replaceNewlinesByBr Whether to replace newlines by '<br />' elements
* @return The content if it was not added to the parent, otherwise null.
*/
internal fun appendContent(parent: HtmlElement, content: String?, replaceNewlinesByBr: Boolean): String? {
return if (content != null && replaceNewlinesByBr && content.contains('\n')) {
// Example: "Hello\nWorld" -> "Hello\n<br />\nWorld"
content.split('\n').forEachIndexed { index, s ->
if (index > 0) {
parent.add(Html.BR())
}
if (s.isNotBlank()) {
parent.add(Html.Text("$s\n", replaceNewlinesByBr = false))
}
}
null
} else {
content
}
}

/**
* Escapes the given [str] to be used in HTML.
* Only the characters `<`, `>`, `&`, `"`, and `'` are escaped.
Expand Down
Loading