Files
filam3d/backend/app/services/price_engine.py
2026-03-22 14:26:45 +03:00

150 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
}
MULTICOLOR_SURCHARGE_PERCENT = 30 # наценка за многоцветную печать
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,
multicolor: bool = False,
) -> 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, multicolor=%s, post_processing=%s",
infill_percent, layer_height_mm, quantity, multicolor, 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 before multicolor (1 pc): %.2f RUB = material(%.2f) + time(%.2f) + pp(%.2f)",
subtotal, material_cost, time_cost, pp_cost)
if multicolor:
multicolor_surcharge = round(subtotal * MULTICOLOR_SURCHARGE_PERCENT / 100.0, 2)
subtotal = round(subtotal + multicolor_surcharge, 2)
logger.debug("Multicolor surcharge: +%.2f RUB (%d%%), new subtotal: %.2f",
multicolor_surcharge, MULTICOLOR_SURCHARGE_PERCENT, subtotal)
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,
)