Skip to content

Commit

Permalink
Merge pull request #282 from micromata/Release-8.1-SNAPSHOT
Browse files Browse the repository at this point in the history
- New wicket version 10.4.0, WicketApplication improved (AI)
- AbstractEditPage NPE for editPageSupport fixed.
- Forecast-Analysis in order edit form opens modal dialog and uses current (unsaved) values.
- Forecast improved, html package extended.
- OrderInfo.projektAsString: kunde removed.
  • Loading branch information
kreinhard authored Feb 4, 2025
2 parents 5e6213b + 408a849 commit 6bb64a4
Show file tree
Hide file tree
Showing 18 changed files with 320 additions and 180 deletions.
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

0 comments on commit 6bb64a4

Please sign in to comment.