170 lines
5.8 KiB
Python
170 lines
5.8 KiB
Python
from app.services.product_costing_service import (
|
|
ProductCostAssumptions,
|
|
ProductCostInputItem,
|
|
calculate_product_cost_item,
|
|
)
|
|
|
|
|
|
def assumptions() -> ProductCostAssumptions:
|
|
return ProductCostAssumptions(
|
|
grading_per_kg=0.05,
|
|
cracking_per_kg=0.03,
|
|
process_costs={
|
|
"Bagging + Grading": 0.04,
|
|
"Standard Bagging": 0.02,
|
|
"PHF Horse Mixes": 0.01,
|
|
"Peckish": 0.08,
|
|
"Hay & Straw": 0.09,
|
|
},
|
|
client_margins={
|
|
"Specialty": {"distributor_margin": 0.2, "wholesale_margin": 0.1},
|
|
"Peckish": {"distributor_margin": 0.25, "wholesale_margin": 0.15},
|
|
"Hay & Straw": {"distributor_margin": 0.3, "wholesale_margin": 0.2},
|
|
"Straight Grain": {"distributor_margin": 0.1, "wholesale_margin": 0.08},
|
|
"PHF Horse Mixes": {"distributor_margin": 0.18, "wholesale_margin": 0.12},
|
|
},
|
|
bag_costs={
|
|
"20kg_bag": 0.6,
|
|
"bulka_bag": 22.0,
|
|
"own_bag_credit": 0.2,
|
|
"1_5kg_bagging": 0.35,
|
|
"peckish_bag": 0.4,
|
|
},
|
|
freight_costs={
|
|
"freight_per_pallet": 80.0,
|
|
"peckish_freight_per_pallet": 96.0,
|
|
"hay_straw_freight_per_pallet": 120.0,
|
|
},
|
|
)
|
|
|
|
|
|
def item(**overrides) -> ProductCostInputItem:
|
|
values = {
|
|
"client_category": "Specialty",
|
|
"product_name": "Pigeon Mix 20kg",
|
|
"mix_product_name": "Pigeon Mix",
|
|
"unit_type": "Standard",
|
|
"own_bag": None,
|
|
"unit_kg": 20.0,
|
|
"items_per_pallet": 40,
|
|
"bagging_process": "Bagging + Grading",
|
|
"manual_distributor_margin": None,
|
|
"manual_wholesale_margin": None,
|
|
}
|
|
values.update(overrides)
|
|
return ProductCostInputItem(**values)
|
|
|
|
|
|
def test_standard_product_uses_per_kg_components_unit_bag_freight_and_default_margins():
|
|
result = calculate_product_cost_item(item(), assumptions(), 0.5)
|
|
|
|
assert result.grading_cost_per_kg == 0.05
|
|
assert result.bagging_cost_per_kg == 0.04
|
|
assert result.bag_cost_per_unit == 0.6
|
|
assert result.freight_cost_per_unit == 2.0
|
|
assert result.finished_product_delivered_cost == 14.4
|
|
assert result.distributor_price == 18.0
|
|
assert result.wholesale_price == 16.0
|
|
assert result.warnings == []
|
|
|
|
|
|
def test_bulka_uses_per_kg_delivered_formula_and_bulka_bag_and_freight_divided_by_unit_kg():
|
|
result = calculate_product_cost_item(item(unit_type="Bulka", unit_kg=1000, items_per_pallet=1), assumptions(), 0.5)
|
|
|
|
assert result.bag_cost_per_unit == 0.022
|
|
assert result.freight_cost_per_unit == 0.08
|
|
assert result.finished_product_delivered_cost == 0.692
|
|
|
|
|
|
def test_per_unit_uses_per_kg_delivered_formula_but_standard_pallet_freight():
|
|
result = calculate_product_cost_item(item(unit_type="Per Unit", unit_kg=1, items_per_pallet=10), assumptions(), 0.5)
|
|
|
|
assert result.freight_cost_per_unit == 8.0
|
|
assert result.finished_product_delivered_cost == 8.59
|
|
|
|
|
|
def test_peckish_uses_peckish_bag_freight_and_zero_grading():
|
|
result = calculate_product_cost_item(
|
|
item(client_category="Peckish", bagging_process="Peckish", items_per_pallet=24),
|
|
assumptions(),
|
|
0.5,
|
|
)
|
|
|
|
assert result.grading_cost_per_kg == 0
|
|
assert result.bagging_cost_per_kg == 0.08
|
|
assert result.bag_cost_per_unit == 0.4
|
|
assert result.freight_cost_per_unit == 4.0
|
|
|
|
|
|
def test_hay_and_straw_uses_hay_freight_and_zero_grading():
|
|
result = calculate_product_cost_item(
|
|
item(client_category="Hay & Straw", bagging_process="Hay & Straw", items_per_pallet=30),
|
|
assumptions(),
|
|
0.5,
|
|
)
|
|
|
|
assert result.grading_cost_per_kg == 0
|
|
assert result.freight_cost_per_unit == 4.0
|
|
|
|
|
|
def test_phf_horse_mixes_have_zero_grading():
|
|
result = calculate_product_cost_item(
|
|
item(client_category="PHF Horse Mixes", bagging_process="PHF Horse Mixes"),
|
|
assumptions(),
|
|
0.5,
|
|
)
|
|
|
|
assert result.grading_cost_per_kg == 0
|
|
|
|
|
|
def test_own_bag_subtracts_credit_and_no_bag_sets_bag_cost_to_zero():
|
|
own_bag = calculate_product_cost_item(item(own_bag="Yes"), assumptions(), 0.5)
|
|
no_bag = calculate_product_cost_item(item(own_bag="No Bag"), assumptions(), 0.5)
|
|
|
|
assert own_bag.bag_cost_per_unit == 0.4
|
|
assert no_bag.bag_cost_per_unit == 0
|
|
|
|
|
|
def test_one_point_five_kg_branch_multiplies_pack_formula_by_eight():
|
|
result = calculate_product_cost_item(item(unit_type="1.5 kg", unit_kg=1.5), assumptions(), 0.5)
|
|
|
|
assert result.bag_cost_per_unit == 0.35
|
|
assert result.finished_product_delivered_cost == 10.84
|
|
|
|
|
|
def test_cracked_product_adds_cracking_cost_and_manual_margins_override_defaults():
|
|
result = calculate_product_cost_item(
|
|
item(product_name="Cracked Maize 20kg", manual_distributor_margin=0.1, manual_wholesale_margin=0.05),
|
|
assumptions(),
|
|
0.5,
|
|
)
|
|
|
|
assert result.cracking_cost_per_kg == 0.03
|
|
assert result.distributor_price == 16.6667
|
|
assert result.wholesale_price == 15.8
|
|
|
|
|
|
def test_straight_grain_bulka_wholesale_rounds_up_to_two_decimals():
|
|
result = calculate_product_cost_item(
|
|
item(client_category="Straight Grain", unit_type="Bulka", unit_kg=1000, items_per_pallet=1),
|
|
assumptions(),
|
|
0.5,
|
|
)
|
|
|
|
assert result.wholesale_price == 0.76
|
|
|
|
|
|
def test_missing_lookup_and_invalid_inputs_generate_warnings_without_prices():
|
|
result = calculate_product_cost_item(
|
|
item(unit_kg=None, items_per_pallet=0, manual_distributor_margin=1.2),
|
|
assumptions(),
|
|
None,
|
|
)
|
|
|
|
assert "Missing mix/product cost lookup" in result.warnings
|
|
assert "Missing unit kg" in result.warnings
|
|
assert "Missing pallet quantity" in result.warnings
|
|
assert "Invalid distributor margin" in result.warnings
|
|
assert result.finished_product_delivered_cost is None
|
|
assert result.distributor_price is None
|