dodil/erp-brick-inventory
DODIL ERP brick: inventory
src/brick_inventory/domain/valuation.py
"""Stock value: average cost or standard cost, appended per move, never a stored number somebody edits.
A valuation row is the state of ONE product in ONE company after ONE move: its quantity, its value, its average. The
value in the books is the sum of the moves' values, so a product at zero quantity is at zero value EXACTLY: the move that
empties the product takes the remaining value, never quantity x average, so no residue is left.
Under standard cost, value_after = round_half_up(quantity_after x standard, 2) and each move's value is the change in
value_after; the difference between what a receipt cost and the value it added is a price difference.
"""
from dataclasses import dataclass
from decimal import Decimal
from typing import Optional
from brick_inventory.domain.values import ZERO, money, q4, round_half_up
@dataclass(frozen=True)
class Position:
"""A product's stock in a company after the last move: quantity, value, and the average it implies."""
quantity: Decimal = ZERO
value: Decimal = ZERO
average: Decimal = ZERO
seq: int = 0
def as_row_fields(self) -> dict:
return {"quantity_after": self.quantity, "value_after": self.value, "average_cost_after": self.average}
@dataclass(frozen=True)
class Step:
"""What one move did: its value (signed), the position after, a price difference under standard cost."""
value_change: Decimal
after: Position
price_difference: Decimal = ZERO
def average_step(prev: Position, quantity_change: Decimal, *, value_in: Optional[Decimal] = None,
value_out: Optional[Decimal] = None) -> Step:
"""One move at average cost.
In (quantity_change > 0): worth `value_in` when the move brings its own cost (a receipt, a return at the cost it
left, an opening balance), else at the average (a found item). Out: worth `value_out` when the move names the cost it
reverses (a return to the supplier), else quantity x the average. A move that empties the product takes the remaining
value; a move out of a product at or below zero is worth quantity x the last average (negative stock)."""
after_qty = prev.quantity + quantity_change
if after_qty == 0:
change = -prev.value # the value is zero at zero quantity, whatever brought it there
elif quantity_change > 0:
change = value_in if value_in is not None else money(quantity_change * prev.average)
elif value_out is not None:
change = -money(value_out)
elif prev.quantity <= 0:
change = -money(-quantity_change * prev.average)
else:
change = -money(-quantity_change * prev.average)
after_value = prev.value + change
if after_qty > 0:
average = q4(after_value / after_qty)
else:
average = prev.average
return Step(change, Position(after_qty, after_value, average, prev.seq + 1))
def standard_step(prev: Position, quantity_change: Decimal, standard: Decimal, *,
purchase_value: Optional[Decimal] = None, reversal_value: Optional[Decimal] = None) -> Step:
"""One move at standard cost: value_after = round_half_up(quantity_after x standard, 2); the move's value is the change.
A receipt brought `purchase_value` (what purchasing paid): the difference to the value it added is a price difference."""
after_qty = prev.quantity + quantity_change
if reversal_value is not None and quantity_change > 0 and after_qty != 0:
# a reversal puts back what the move it reverses took out: the POSTED value, not today's standard
return Step(reversal_value, Position(after_qty, prev.value + reversal_value, standard, prev.seq + 1))
after_value = round_half_up(after_qty * standard, 2) if after_qty != 0 else ZERO
change = after_value - prev.value
diff = ZERO
if purchase_value is not None and quantity_change > 0:
diff = purchase_value - change
return Step(change, Position(after_qty, after_value, standard, prev.seq + 1), diff)
def revalue(prev: Position, standard: Decimal) -> Step:
"""The quantity on hand at a new standard price: a revaluation row, quantity unchanged."""
after_value = round_half_up(prev.quantity * standard, 2) if prev.quantity != 0 else ZERO
return Step(after_value - prev.value, Position(prev.quantity, after_value, standard, prev.seq + 1))
def switch_to_standard(prev: Position, standard: Decimal) -> Step:
return revalue(prev, standard)
def switch_to_average(prev: Position) -> Step:
"""From standard back to average: the value stands; the average is what it implies."""
avg = q4(prev.value / prev.quantity) if prev.quantity > 0 else prev.average
return Step(ZERO, Position(prev.quantity, prev.value, avg, prev.seq + 1))