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