from .manufacturing_variance_types import CostVariances, LabourActual, MaterialActual, Variance from .math_rational import Rational, multiply_rational, rational, rational_to_integer from .math_round_div import RoundingMode from .money_amount import Money, assert_same_currency, money MAX_SAFE = 2**53 - 1 def _not_negative(name: str, value: Rational) -> Rational: r = rational(value.numerator, value.denominator) if r.numerator < 0: raise ValueError("%s must not be negative" % (name,)) return r def _cost_of(name: str, value: Money, unit: Money) -> int: assert_same_currency(unit, value) if value.minor < 0: raise ValueError("%s must not be negative, received %d" % (name, value.minor)) return value.minor def _variance(signed: int, currency: str) -> Variance: effect = "adverse" if signed > 0 else "favourable" if signed < 0 else "none" return Variance(amount=money(abs(signed), currency), effect=effect) def cost_variances(material: MaterialActual, labour: LabourActual, actual_output: int, mode: RoundingMode) -> CostVariances: """Material price and usage, labour rate and efficiency variances, flexed to actual output. The four flexed costs are each rounded once and every variance is a difference of them, so the variances always sum exactly. """ if isinstance(actual_output, bool) or not isinstance(actual_output, int) or actual_output < 0 or actual_output > MAX_SAFE: raise ValueError("actualOutput must be a whole number, not negative, received %r" % (actual_output,)) unit = material.standard_price currency = unit.currency sp = _cost_of("material standardPrice", material.standard_price, unit) mat_actual = _cost_of("material actualCost", material.actual_cost, unit) sr = _cost_of("labour standardRate", labour.standard_rate, unit) lab_actual = _cost_of("labour actualCost", labour.actual_cost, unit) sq = _not_negative("material standardQuantity", material.standard_quantity) aq = _not_negative("material actualQuantity", material.actual_quantity) sh = _not_negative("labour standardHours", labour.standard_hours) ah = _not_negative("labour actualHours", labour.actual_hours) output = rational(actual_output, 1) actual_at_standard = rational_to_integer(multiply_rational(aq, rational(sp, 1)), mode) standard_material = rational_to_integer(multiply_rational(multiply_rational(sq, output), rational(sp, 1)), mode) hours_at_standard = rational_to_integer(multiply_rational(ah, rational(sr, 1)), mode) standard_labour = rational_to_integer(multiply_rational(multiply_rational(sh, output), rational(sr, 1)), mode) standard_cost = standard_material + standard_labour actual_cost = mat_actual + lab_actual return CostVariances( material_price=_variance(mat_actual - actual_at_standard, currency), material_usage=_variance(actual_at_standard - standard_material, currency), labour_rate=_variance(lab_actual - hours_at_standard, currency), labour_efficiency=_variance(hours_at_standard - standard_labour, currency), total=_variance(actual_cost - standard_cost, currency), standard_cost=money(standard_cost, currency), actual_cost=money(actual_cost, currency), )