82 lines
2.8 KiB
Python
82 lines
2.8 KiB
Python
from decimal import ROUND_HALF_UP, Decimal
|
||
from typing import Any, Optional, Tuple
|
||
|
||
|
||
def cents4(value: Optional[Decimal | float | int]) -> int:
|
||
"""Convierte a centésimas (valor * 10000). Soporta None."""
|
||
v = Decimal(str(value or 0))
|
||
return int((v * 10000).to_integral_value(rounding=ROUND_HALF_UP))
|
||
|
||
|
||
def cents(value: Optional[Decimal | float | int]) -> int:
|
||
"""Convierte a centésimas (valor * 100). Soporta None."""
|
||
v = Decimal(str(value or 0))
|
||
return int((v * 100).to_integral_value(rounding=ROUND_HALF_UP))
|
||
|
||
|
||
def money_round(value: Decimal, ndigits: int = 2) -> Decimal:
|
||
"""Redondeo bancario a n decimales (por defecto 2)."""
|
||
q = Decimal((0, (1,), -ndigits)) # 10^-ndigits
|
||
return value.quantize(q, rounding=ROUND_HALF_UP)
|
||
|
||
|
||
def unscale_to_decimal(value: Any, scale: Any) -> Decimal:
|
||
"""
|
||
Convierte un valor escalado (p. ej. 24200 con scale=2) a su valor en unidades (242).
|
||
No redondea; sólo mueve el punto decimal.
|
||
"""
|
||
if value is None:
|
||
return Decimal("0")
|
||
d = Decimal(str(value))
|
||
s = int(scale or 0)
|
||
return d.scaleb(-s) # divide por 10**s
|
||
|
||
|
||
def unscale_to_str(
|
||
value: Any,
|
||
scale: Any,
|
||
*,
|
||
decimals: Optional[int] = None,
|
||
strip_trailing_zeros: bool = True
|
||
) -> str:
|
||
"""
|
||
Igual que unscale_to_decimal, pero devuelve str.
|
||
- decimals: fija nº de decimales (p. ej. 2). Si None, no fuerza decimales.
|
||
- strip_trailing_zeros: si True, quita ceros y el punto sobrantes.
|
||
"""
|
||
d = unscale_to_decimal(value, scale)
|
||
if decimals is not None:
|
||
q = Decimal("1").scaleb(-decimals) # p.ej. 2 -> Decimal('0.01')
|
||
d = d.quantize(q, rounding=ROUND_HALF_UP)
|
||
|
||
s = format(d, "f")
|
||
if strip_trailing_zeros and "." in s:
|
||
s = s.rstrip("0").rstrip(".")
|
||
return s
|
||
|
||
|
||
def calc_discount_cents4(subtotal_cents4: int, disc_pct: Optional[Decimal | float | int]) -> int:
|
||
"""
|
||
Calcula el importe de descuento en escala 4 (×10000) como ENTERO.
|
||
- subtotal_cents4: subtotal ya en escala 4
|
||
- disc_pct: porcentaje de descuento (p.ej. 10 -> 10%)
|
||
|
||
Devuelve un entero NEGATIVO (como en Delphi):
|
||
ImporteDto := (-1) * ((Subtotal * Descuento) / 100);
|
||
"""
|
||
pct = Decimal(str(disc_pct or 0))
|
||
# descuento = round(subtotal * pct / 100) en la MISMA escala (×10000)
|
||
disc = (Decimal(subtotal_cents4) * pct / Decimal(100)).to_integral_value(rounding=ROUND_HALF_UP)
|
||
return disc
|
||
|
||
|
||
def apply_discount_cents4(subtotal_cents4: int, disc_pct: Optional[Decimal | float | int]) -> Tuple[int, int]:
|
||
"""
|
||
Devuelve (discount_cents4, total_cents4) ambos en escala 4.
|
||
- discount_cents4 NEGATIVO
|
||
- total_cents4 = subtotal_cents4 + discount_cents4
|
||
"""
|
||
discount = calc_discount_cents4(subtotal_cents4, disc_pct)
|
||
total = subtotal_cents4 - discount
|
||
return discount, total
|