ProductPresentation.kt
/*
* This file is part of GNU Taler
* (C) 2026 Taler Systems S.A.
*
* GNU Taler 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; either version 3, or (at your option) any later version.
*
* GNU Taler 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
* GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
package net.taler.merchantpos.history
import net.taler.common.Amount
import net.taler.merchantlib.MerchantUnit
import net.taler.merchantlib.OrderDetailProduct
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.NumberFormat
import java.util.Locale
private val decimalQuantity = Regex("[0-9]+(?:\\.[0-9]{1,8})?")
private fun OrderDetailProduct.numericQuantity(): BigDecimal?
{
val raw = unitQuantity ?: quantity.toString()
return raw.takeIf(decimalQuantity::matches)?.toBigDecimalOrNull()
}
internal fun OrderDetailProduct.quantityLabel(
units: Map<String, MerchantUnit>,
locale: Locale = Locale.getDefault(),
): String
{
val definition = units[unit]
val label = definition?.shortLabel(locale) ?: unit?.takeIf(String::isNotBlank)
val count =
numericQuantity()?.let {
NumberFormat
.getNumberInstance(locale)
.apply {
maximumFractionDigits = definition?.precision?.coerceIn(0, 8) ?: 8
minimumFractionDigits = 0
roundingMode = RoundingMode.HALF_EVEN
isGroupingUsed = false
}.format(it)
} ?: (unitQuantity ?: quantity.toString())
return if (label == null) count else "$count\u202f$label"
}
internal data class ProductUnitPrice(
val amount: Amount,
val approximate: Boolean,
)
/** Derive the historic unit price from the sold line, never from current inventory prices. */
internal fun OrderDetailProduct.unitPrice(currency: String?): ProductUnitPrice?
{
val count = numericQuantity()?.takeIf { it.signum() > 0 } ?: return null
val total = priceForCurrency(currency) ?: return null
val decimalTotal = BigDecimal.valueOf(total.value).add(BigDecimal.valueOf(total.fraction.toLong(), 8))
val perUnit = decimalTotal.divide(count, 8, RoundingMode.HALF_EVEN)
if (perUnit.signum() < 0 ||
perUnit > BigDecimal.valueOf(Amount.MAX_VALUE).add(BigDecimal("0.99999999")))
{
return null
}
val whole = perUnit.toLong()
val fraction = perUnit.subtract(BigDecimal.valueOf(whole)).movePointRight(8).intValueExact()
return ProductUnitPrice(
Amount(total.currency, whole, fraction),
perUnit.multiply(count).compareTo(decimalTotal) != 0,
)
}