dodil/erp-brick-manufacturing

DODIL ERP brick: manufacturing

src/brick_manufacturing/domain/settlement.py

"""Settlement (MFG-15): the order's WIP balance split into its variances, exactly.

Pure arithmetic over figures inventory already valued: nothing here reads a price. Material standard for what came out is
the planned quantity of each component, at the unit cost inventory valued the issue at, scaled to the output reported (so
normal roasting loss, which is in the BOM's planned yield, is NOT a variance; only output below plan is). The split always
sums to the balance: the category that takes the rounding is the last (activity). Scrap (MFG-14) is output that was MADE but never
received: it counts toward the standard material (so the material that burnt is not a usage variance) and its whole standard value is
expensed as abnormal scrap (IAS 2.16); only what is left of the balance is a variance.
"""
from dataclasses import dataclass
from decimal import Decimal

from brick_manufacturing.domain.values import money


@dataclass(frozen=True)
class Settled:
    wip_debit: Decimal
    wip_credit: Decimal
    balance: Decimal
    standard_material: Decimal
    abnormal_scrap: Decimal
    material_usage_variance: Decimal
    activity_variance: Decimal

    @property
    def variance(self) -> Decimal:
        """What the approval threshold is compared with: the variances' absolute size, so +300 and -300 do not net to nothing."""
        return abs(self.material_usage_variance) + abs(self.activity_variance) + abs(self.abnormal_scrap)


def settle(components: list, *, outputs_value: Decimal, activity_cost: Decimal, output_base: Decimal,
           planned_output_base: Decimal, abnormal_scrap: Decimal = Decimal("0"),
           scrap_base: Decimal = Decimal("0")) -> Settled:
    """components: [{planned_base, issued_base, issued_value}] per BOM component, sent movements only."""
    issues_value = sum((c["issued_value"] for c in components), Decimal("0"))
    ratio = (output_base + scrap_base) / planned_output_base if planned_output_base else Decimal("0")
    std = Decimal("0")
    for c in components:
        if c["issued_base"] > 0:
            std += c["planned_base"] * (c["issued_value"] / c["issued_base"]) * ratio
    std = money(std, "the standard material")
    debit, credit = money(issues_value + activity_cost, "the WIP debit"), money(outputs_value, "the WIP credit")
    balance = money(debit - credit, "the WIP balance")
    abnormal = money(abnormal_scrap, "the abnormal scrap")
    usage = money(issues_value - std, "the material usage variance")
    activity = balance - usage - abnormal
    return Settled(debit, credit, balance, std, abnormal, usage, activity)