init
This commit is contained in:
140
backend/app/services/price_engine.py
Normal file
140
backend/app/services/price_engine.py
Normal file
@@ -0,0 +1,140 @@
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from app.services.file_parser import FileInfo
|
||||
|
||||
logger = logging.getLogger("app.services.price_engine")
|
||||
|
||||
TIME_RATE_PER_HOUR = 200.0 # руб/час
|
||||
SETUP_TIME_MIN = 15.0 # минуты
|
||||
TRAVEL_TIME_PER_LAYER_MIN = 0.3
|
||||
|
||||
POST_PROCESSING_COSTS = {
|
||||
"sanding": 300.0,
|
||||
"painting": 500.0,
|
||||
"threading": 200.0,
|
||||
"acetone_smoothing": 400.0,
|
||||
}
|
||||
|
||||
QUANTITY_DISCOUNTS = [
|
||||
(1, 0),
|
||||
(2, 5),
|
||||
(6, 10),
|
||||
(21, 15),
|
||||
(101, 20),
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class PriceResult:
|
||||
weight_grams: float
|
||||
material_cost_rub: float
|
||||
print_time_hours: float
|
||||
time_cost_rub: float
|
||||
post_processing_cost_rub: float
|
||||
subtotal_rub: float
|
||||
quantity: int
|
||||
quantity_discount_percent: int
|
||||
total_rub: float
|
||||
estimated_days: int
|
||||
|
||||
|
||||
def get_quantity_discount(quantity: int) -> int:
|
||||
discount = 0
|
||||
for min_qty, disc in QUANTITY_DISCOUNTS:
|
||||
if quantity >= min_qty:
|
||||
discount = disc
|
||||
logger.debug("Quantity discount for %d pcs: %d%%", quantity, discount)
|
||||
return discount
|
||||
|
||||
|
||||
def estimate_print_time(file_info: FileInfo, layer_height_mm: float, flow_rate_mm3_s: float) -> float:
|
||||
"""Estimate print time in hours."""
|
||||
z_height = file_info.bounding_box_mm.get("z", 10.0)
|
||||
layers = max(z_height / layer_height_mm, 1)
|
||||
volume_mm3 = file_info.volume_cm3 * 1000.0
|
||||
volume_per_layer = volume_mm3 / layers
|
||||
time_per_layer_min = volume_per_layer / flow_rate_mm3_s / 60.0
|
||||
total_min = layers * (time_per_layer_min + TRAVEL_TIME_PER_LAYER_MIN) + SETUP_TIME_MIN
|
||||
hours = round(total_min / 60.0, 1)
|
||||
logger.debug("Print time estimate: z=%.1fmm, layers=%.0f, vol_per_layer=%.1fmm3, "
|
||||
"time_per_layer=%.2fmin, total=%.1fmin (%.1fh)",
|
||||
z_height, layers, volume_per_layer, time_per_layer_min, total_min, hours)
|
||||
return hours
|
||||
|
||||
|
||||
def calculate_price(
|
||||
file_info: FileInfo,
|
||||
density_g_cm3: float,
|
||||
price_per_gram: float,
|
||||
flow_rate_mm3_s: float,
|
||||
infill_percent: int = 30,
|
||||
layer_height_mm: float = 0.2,
|
||||
quantity: int = 1,
|
||||
post_processing: list[str] | None = None,
|
||||
) -> PriceResult:
|
||||
post_processing = post_processing or []
|
||||
|
||||
logger.info("=== Price calculation start ===")
|
||||
logger.info("Input: volume=%.2f cm3, density=%.2f g/cm3, price_per_gram=%.1f RUB",
|
||||
file_info.volume_cm3, density_g_cm3, price_per_gram)
|
||||
logger.info("Params: infill=%d%%, layer=%.2fmm, qty=%d, post_processing=%s",
|
||||
infill_percent, layer_height_mm, quantity, post_processing)
|
||||
|
||||
effective_volume = file_info.volume_cm3 * (infill_percent / 100.0) * 0.7 + file_info.volume_cm3 * 0.3
|
||||
logger.debug("Effective volume: %.2f cm3 (infill-scaled: %.2f + walls: %.2f)",
|
||||
effective_volume,
|
||||
file_info.volume_cm3 * (infill_percent / 100.0) * 0.7,
|
||||
file_info.volume_cm3 * 0.3)
|
||||
|
||||
weight_g = round(effective_volume * density_g_cm3, 1)
|
||||
material_cost = round(weight_g * price_per_gram, 2)
|
||||
logger.debug("Weight: %.1f g, material cost: %.2f RUB", weight_g, material_cost)
|
||||
|
||||
print_time_h = estimate_print_time(file_info, layer_height_mm, flow_rate_mm3_s)
|
||||
time_cost = round(print_time_h * TIME_RATE_PER_HOUR, 2)
|
||||
logger.debug("Print time: %.1f h, time cost: %.2f RUB (rate: %.0f RUB/h)", print_time_h, time_cost, TIME_RATE_PER_HOUR)
|
||||
|
||||
pp_cost = 0.0
|
||||
for pp in post_processing:
|
||||
cost = POST_PROCESSING_COSTS.get(pp, 0)
|
||||
logger.debug("Post-processing '%s': %.0f RUB", pp, cost)
|
||||
pp_cost += cost
|
||||
pp_cost = round(pp_cost, 2)
|
||||
logger.debug("Total post-processing cost: %.2f RUB", pp_cost)
|
||||
|
||||
subtotal = round(material_cost + time_cost + pp_cost, 2)
|
||||
logger.debug("Subtotal (1 pc): %.2f RUB = material(%.2f) + time(%.2f) + pp(%.2f)",
|
||||
subtotal, material_cost, time_cost, pp_cost)
|
||||
|
||||
discount_pct = get_quantity_discount(quantity)
|
||||
total = round(subtotal * quantity * (1 - discount_pct / 100.0), 2)
|
||||
logger.info("Total: %.2f RUB (qty=%d, discount=%d%%, subtotal_per_unit=%.2f)",
|
||||
total, quantity, discount_pct, subtotal)
|
||||
|
||||
if print_time_h <= 2:
|
||||
estimated_days = 2
|
||||
elif print_time_h <= 8:
|
||||
estimated_days = 3
|
||||
else:
|
||||
estimated_days = 5
|
||||
|
||||
if quantity > 10:
|
||||
estimated_days += 2
|
||||
if quantity > 50:
|
||||
estimated_days += 3
|
||||
|
||||
logger.info("Estimated days: %d", estimated_days)
|
||||
logger.info("=== Price calculation complete ===")
|
||||
|
||||
return PriceResult(
|
||||
weight_grams=weight_g,
|
||||
material_cost_rub=material_cost,
|
||||
print_time_hours=print_time_h,
|
||||
time_cost_rub=time_cost,
|
||||
post_processing_cost_rub=pp_cost,
|
||||
subtotal_rub=subtotal,
|
||||
quantity=quantity,
|
||||
quantity_discount_percent=discount_pct,
|
||||
total_rub=total,
|
||||
estimated_days=estimated_days,
|
||||
)
|
||||
Reference in New Issue
Block a user