초기화
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
# inventory package marker
|
||||
from .db_manager import (
|
||||
add_material_property,
|
||||
update_material_property,
|
||||
delete_material_property,
|
||||
get_all_material_properties,
|
||||
get_material_property_by_id,
|
||||
add_inventory_to_db,
|
||||
update_inventory_in_db,
|
||||
deplete_inventory_in_db,
|
||||
get_all_inventory_from_db,
|
||||
get_inventory_details_from_db,
|
||||
get_total_stock_for_material
|
||||
)
|
||||
from .analysis import calculate_cost_analysis
|
||||
from .router import router
|
||||
@@ -0,0 +1,221 @@
|
||||
import math
|
||||
import logging
|
||||
from .db_manager import get_inventory_details_from_db
|
||||
from setting.db_manager import get_all_settings
|
||||
|
||||
def _safe_float_conversion(value, default_value=None):
|
||||
try:
|
||||
if value is not None and value != '':
|
||||
return float(value)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
return default_value
|
||||
|
||||
def calculate_cost_analysis(inputs: dict, target_production_quantity: float = None) -> dict:
|
||||
"""
|
||||
원가 분석 계산 엔진 (순수 연산 비즈니스 로직)
|
||||
"""
|
||||
yarn_inventory_id = inputs.get("yarn_inventory_id")
|
||||
bond_inventory_id = inputs.get("bond_inventory_id")
|
||||
|
||||
if not yarn_inventory_id:
|
||||
raise ValueError("분석할 원사 재고가 선택되지 않았습니다.")
|
||||
|
||||
yarn_data_wrapper = get_inventory_details_from_db(yarn_inventory_id)
|
||||
if not yarn_data_wrapper or "inventory" not in yarn_data_wrapper:
|
||||
raise ValueError("선택된 원사의 물성 또는 단가 정보를 찾을 수 없습니다.")
|
||||
yarn_data = yarn_data_wrapper["inventory"]
|
||||
|
||||
yarn_density_g_cm3 = _safe_float_conversion(yarn_data.get("density"), 1.0)
|
||||
yarn_unit_cost = _safe_float_conversion(yarn_data.get("unit_cost"), 0.0)
|
||||
|
||||
bond_unit_cost = 0.0
|
||||
if bond_inventory_id and bond_inventory_id != 'null':
|
||||
bond_data_wrapper = get_inventory_details_from_db(bond_inventory_id)
|
||||
if bond_data_wrapper and "inventory" in bond_data_wrapper:
|
||||
bond_data = bond_data_wrapper["inventory"]
|
||||
bond_unit_cost = _safe_float_conversion(bond_data.get("unit_cost"), 0.0)
|
||||
|
||||
yarn_diameter_micron = _safe_float_conversion(inputs.get("yarn_diameter_micron"), 7)
|
||||
yarn_k = _safe_float_conversion(inputs.get("yarn_k"), 12) * 1000
|
||||
ports = _safe_float_conversion(inputs.get("ports"), 40)
|
||||
cycle_time_hz = _safe_float_conversion(inputs.get("cycle_time_hz"), 8)
|
||||
cut_length_mm = _safe_float_conversion(inputs.get("cut_length_mm"), 6)
|
||||
work_hours_day = _safe_float_conversion(inputs.get("work_hours_day"), 16)
|
||||
work_days_month = _safe_float_conversion(inputs.get("work_days_month"), 20)
|
||||
num_machines = _safe_float_conversion(inputs.get("num_machines"), 2)
|
||||
bond_percentage = _safe_float_conversion(inputs.get("bond_percentage"), 3.0) / 100.0
|
||||
|
||||
yarn_radius_cm = (yarn_diameter_micron / 2) * 1e-4
|
||||
yarn_cross_section_cm2 = math.pi * (yarn_radius_cm ** 2)
|
||||
pure_yarn_weight_kg_per_m = (yarn_cross_section_cm2 * 100 * yarn_density_g_cm3) / 1000
|
||||
cut_length_m = cut_length_mm / 1000
|
||||
production_length_m_per_hour_per_mc = cycle_time_hz * 3600 * cut_length_m
|
||||
pure_yarn_kg_per_hour_per_mc = production_length_m_per_hour_per_mc * pure_yarn_weight_kg_per_m * ports * yarn_k
|
||||
|
||||
if 0 < bond_percentage < 1.0:
|
||||
total_kg_per_hour_per_mc = pure_yarn_kg_per_hour_per_mc / (1.0 - bond_percentage)
|
||||
else:
|
||||
total_kg_per_hour_per_mc = pure_yarn_kg_per_hour_per_mc
|
||||
|
||||
if total_kg_per_hour_per_mc <= 0:
|
||||
raise ValueError("시간당 생산량이 0 이하여서 계산할 수 없습니다.")
|
||||
|
||||
settings = get_all_settings()
|
||||
settings_dict = {f"{s['setting_group']}_{s['setting_key']}": s['setting_value'] for s in settings}
|
||||
|
||||
def get_s(key, default):
|
||||
return _safe_float_conversion(settings_dict.get(key, default), default)
|
||||
|
||||
avg_power_per_machine_kw = get_s("CostAnalysis_AvgPowerPerMachine", 5000) / 1000
|
||||
electricity_price_kwh = get_s("CostAnalysis_ElectricityUnitPricePerKWH", 94.4)
|
||||
hourly_power_cost = avg_power_per_machine_kw * electricity_price_kwh
|
||||
|
||||
max_machines_per_person = get_s("CostAnalysis_MaxMachinesPerPerson", 10)
|
||||
num_people_needed = math.ceil(num_machines / max_machines_per_person)
|
||||
monthly_labor_cost_person = get_s("CostAnalysis_MonthlyLaborCostPerPerson", 6000000)
|
||||
meal_cost_day = get_s("CostAnalysis_MealCostPerDay", 15000)
|
||||
hourly_labor_cost = (num_people_needed * monthly_labor_cost_person + num_people_needed * meal_cost_day * work_days_month) / (work_days_month * work_hours_day * num_machines)
|
||||
|
||||
packaging_cost_kg = get_s("CostAnalysis_PackagingCostPerKg", 500)
|
||||
packaging_weight_kg = get_s("CostAnalysis_PackagingBaseWeightKg", 25)
|
||||
hourly_packaging_cost = (total_kg_per_hour_per_mc / packaging_weight_kg) * packaging_cost_kg if packaging_weight_kg > 0 else 0
|
||||
|
||||
monthly_rent = get_s("CostAnalysis_MonthlyRentCost", 0)
|
||||
monthly_admin = get_s("CostAnalysis_MonthlyGeneralAdminCost", 50000)
|
||||
monthly_delivery = get_s("CostAnalysis_MonthlyDeliveryCost", 300000)
|
||||
hourly_overhead_cost = (monthly_rent + monthly_admin + monthly_delivery) / (work_days_month * work_hours_day * num_machines)
|
||||
|
||||
mass_mc_cost = get_s("Investment_MassProductionEquipmentCost", 10000000)
|
||||
dep_mass_mc = get_s("Investment_DepreciationPeriodMassProduction", 3) * 12
|
||||
office_cost = get_s("Investment_OfficeSuppliesCost", 5000000)
|
||||
dep_office = get_s("Investment_DepreciationPeriodOfficeSupplies", 3) * 12
|
||||
proto_cost = get_s("Investment_ProtoEquipmentCost", 5000000)
|
||||
monthly_depreciation = (mass_mc_cost / dep_mass_mc if dep_mass_mc > 0 else 0) + \
|
||||
(office_cost / dep_office if dep_office > 0 else 0) + \
|
||||
(proto_cost / dep_office if dep_office > 0 else 0)
|
||||
hourly_depreciation = monthly_depreciation / (work_days_month * work_hours_day)
|
||||
|
||||
hourly_net_cost_per_machine = hourly_power_cost + hourly_labor_cost + hourly_packaging_cost + hourly_overhead_cost + hourly_depreciation
|
||||
|
||||
company_margin_rate = get_s("CostAnalysis_CompanyMarginRate", 30) / 100.0
|
||||
standard_processing_cost_kg = get_s("CostAnalysis_StandardProcessingCost", 0)
|
||||
|
||||
pure_processing_cost_kg = hourly_net_cost_per_machine / total_kg_per_hour_per_mc if total_kg_per_hour_per_mc > 0 else 0
|
||||
|
||||
calculated_profit_margin_kg = 0
|
||||
if pure_processing_cost_kg > 0:
|
||||
if pure_processing_cost_kg * (1 + company_margin_rate) < standard_processing_cost_kg:
|
||||
calculated_profit_margin_kg = standard_processing_cost_kg - pure_processing_cost_kg
|
||||
else:
|
||||
calculated_profit_margin_kg = pure_processing_cost_kg * company_margin_rate
|
||||
|
||||
hourly_profit_per_machine = calculated_profit_margin_kg * total_kg_per_hour_per_mc
|
||||
|
||||
analysis_result = {}
|
||||
|
||||
total_kg_hour_all_mc = total_kg_per_hour_per_mc * num_machines
|
||||
total_kg_day_all_mc = total_kg_hour_all_mc * work_hours_day
|
||||
total_kg_month = total_kg_day_all_mc * work_days_month
|
||||
|
||||
monthly_power_cost = hourly_power_cost * work_hours_day * work_days_month * num_machines
|
||||
monthly_labor_cost_total = hourly_labor_cost * work_hours_day * work_days_month * num_machines
|
||||
monthly_packaging_cost = hourly_packaging_cost * work_hours_day * work_days_month * num_machines
|
||||
monthly_overhead_cost = hourly_overhead_cost * work_hours_day * work_days_month * num_machines
|
||||
monthly_depreciation_cost_total = hourly_depreciation * work_hours_day * work_days_month * num_machines
|
||||
|
||||
monthly_total_expenses = monthly_power_cost + monthly_labor_cost_total + monthly_packaging_cost + monthly_overhead_cost + monthly_depreciation_cost_total
|
||||
|
||||
pure_yarn_kg_per_hour_all_mc = pure_yarn_kg_per_hour_per_mc * num_machines
|
||||
required_yarn_kg_month = (pure_yarn_kg_per_hour_all_mc / total_kg_hour_all_mc) * total_kg_month if total_kg_hour_all_mc > 0 else 0
|
||||
|
||||
bond_kg_per_hour_all_mc = (total_kg_hour_all_mc - pure_yarn_kg_per_hour_all_mc)
|
||||
required_bond_kg_month = (bond_kg_per_hour_all_mc / total_kg_hour_all_mc) * total_kg_month if total_kg_hour_all_mc > 0 else 0
|
||||
|
||||
current_yarn_stock = _safe_float_conversion(yarn_data.get("stock"), 0.0)
|
||||
purchase_order_yarn_kg = max(0, required_yarn_kg_month - current_yarn_stock)
|
||||
|
||||
current_bond_stock = 0.0
|
||||
purchase_order_bond_kg = 0.0
|
||||
if bond_inventory_id and bond_inventory_id != 'null':
|
||||
bond_data_wrapper = get_inventory_details_from_db(bond_inventory_id)
|
||||
if bond_data_wrapper and "inventory" in bond_data_wrapper:
|
||||
current_bond_stock = _safe_float_conversion(bond_data_wrapper["inventory"].get("stock"), 0.0)
|
||||
purchase_order_bond_kg = max(0, required_bond_kg_month - current_bond_stock)
|
||||
|
||||
total_yarn_purchase_cost = required_yarn_kg_month * yarn_unit_cost
|
||||
total_bond_purchase_cost = required_bond_kg_month * bond_unit_cost
|
||||
total_raw_material_purchase_cost = total_yarn_purchase_cost + total_bond_purchase_cost
|
||||
|
||||
analysis_result.update({
|
||||
"production_kg_per_hour": total_kg_hour_all_mc,
|
||||
"production_kg_per_day": total_kg_day_all_mc,
|
||||
"production_ton_per_month": total_kg_month / 1000,
|
||||
"total_product_kg_per_month": total_kg_month,
|
||||
"pure_processing_cost_per_kg": pure_processing_cost_kg,
|
||||
"calculated_profit_margin_per_kg": calculated_profit_margin_kg,
|
||||
"processing_cost_per_kg_total": pure_processing_cost_kg + calculated_profit_margin_kg,
|
||||
"current_yarn_stock": current_yarn_stock,
|
||||
"required_yarn_kg": required_yarn_kg_month,
|
||||
"purchase_order_kg": purchase_order_yarn_kg,
|
||||
"yarn_unit_cost": yarn_unit_cost,
|
||||
"current_bond_stock": current_bond_stock,
|
||||
"required_bond_kg": required_bond_kg_month,
|
||||
"purchase_order_bond_kg": purchase_order_bond_kg,
|
||||
"bond_unit_cost": bond_unit_cost,
|
||||
"total_monthly_expenses": monthly_total_expenses,
|
||||
"monthly_power_cost": monthly_power_cost,
|
||||
"total_monthly_kwh": (avg_power_per_machine_kw * num_machines * work_hours_day * work_days_month),
|
||||
"monthly_labor_cost": monthly_labor_cost_total,
|
||||
"monthly_meal_cost": (num_people_needed * meal_cost_day * work_days_month),
|
||||
"monthly_packaging_cost": monthly_packaging_cost,
|
||||
"monthly_depreciation_cost": monthly_depreciation_cost_total,
|
||||
"monthly_rent_cost": monthly_rent,
|
||||
"monthly_general_admin_cost_setting": monthly_admin,
|
||||
"monthly_delivery_cost": monthly_delivery,
|
||||
"total_revenue": total_raw_material_purchase_cost + ((pure_processing_cost_kg + calculated_profit_margin_kg) * total_kg_month)
|
||||
})
|
||||
|
||||
# 2. 목표 생산량 분석 (요청 시 추가 수행)
|
||||
if target_production_quantity and target_production_quantity > 0:
|
||||
total_work_hours_target = target_production_quantity / (total_kg_hour_all_mc if total_kg_hour_all_mc > 0 else 1)
|
||||
work_days_target = total_work_hours_target / work_hours_day
|
||||
|
||||
required_yarn_kg_target = (pure_yarn_kg_per_hour_all_mc / total_kg_hour_all_mc) * target_production_quantity if total_kg_hour_all_mc > 0 else 0
|
||||
purchase_order_yarn_kg_target = max(0, required_yarn_kg_target - current_yarn_stock)
|
||||
|
||||
required_bond_kg_target = (bond_kg_per_hour_all_mc / total_kg_hour_all_mc) * target_production_quantity if total_kg_hour_all_mc > 0 else 0
|
||||
purchase_order_bond_kg_target = max(0, required_bond_kg_target - current_bond_stock)
|
||||
|
||||
plan_yarn_cost = required_yarn_kg_target * yarn_unit_cost
|
||||
plan_bond_cost = required_bond_kg_target * bond_unit_cost
|
||||
plan_total_material_cost = plan_yarn_cost + plan_bond_cost
|
||||
|
||||
plan_net_processing_cost = hourly_net_cost_per_machine * num_machines * total_work_hours_target
|
||||
plan_company_margin = hourly_profit_per_machine * num_machines * total_work_hours_target
|
||||
plan_processing_fee = plan_net_processing_cost + plan_company_margin
|
||||
plan_estimated_sales = plan_processing_fee + plan_total_material_cost
|
||||
|
||||
analysis_result.update({
|
||||
"required_days_target": work_days_target,
|
||||
"current_yarn_stock_target": current_yarn_stock,
|
||||
"required_yarn_kg_target": required_yarn_kg_target,
|
||||
"purchase_order_kg_target": purchase_order_yarn_kg_target,
|
||||
"current_bond_stock_target": current_bond_stock,
|
||||
"required_bond_kg_target": required_bond_kg_target,
|
||||
"purchase_order_bond_kg_target": purchase_order_bond_kg_target,
|
||||
"plan_yarn_cost": plan_yarn_cost,
|
||||
"plan_bond_cost": plan_bond_cost,
|
||||
"plan_total_material_cost": plan_total_material_cost,
|
||||
"plan_net_processing_cost": plan_net_processing_cost,
|
||||
"plan_company_margin": plan_company_margin,
|
||||
"plan_processing_fee": plan_processing_fee,
|
||||
"plan_estimated_sales": plan_estimated_sales
|
||||
})
|
||||
|
||||
analysis_result.update({
|
||||
"hourly_net_cost_per_machine": hourly_net_cost_per_machine,
|
||||
"hourly_profit_per_machine": hourly_profit_per_machine
|
||||
})
|
||||
|
||||
return analysis_result
|
||||
@@ -0,0 +1,237 @@
|
||||
import mysql.connector
|
||||
from database.connection import get_db_connection
|
||||
import logging
|
||||
|
||||
# 물성 정보 추가
|
||||
def add_material_property(data):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
with conn.cursor(dictionary=True) as cursor:
|
||||
sql = """
|
||||
INSERT INTO material_properties (material_code, material_name, material_type, density, remarks)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
"""
|
||||
cursor.execute(sql, (
|
||||
data.get('material_code'),
|
||||
data.get('material_name'),
|
||||
data.get('material_type'),
|
||||
data.get('density'),
|
||||
data.get('remarks')
|
||||
))
|
||||
conn.commit()
|
||||
return cursor.lastrowid
|
||||
except mysql.connector.Error as err:
|
||||
logging.error(f"Error adding material property: {err}")
|
||||
return None
|
||||
|
||||
# 물성 정보 수정
|
||||
def update_material_property(data):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
with conn.cursor(dictionary=True) as cursor:
|
||||
sql = """
|
||||
UPDATE material_properties SET
|
||||
material_code = %s,
|
||||
material_name = %s,
|
||||
material_type = %s,
|
||||
density = %s,
|
||||
remarks = %s
|
||||
WHERE id = %s
|
||||
"""
|
||||
cursor.execute(sql, (
|
||||
data.get('material_code'),
|
||||
data.get('material_name'),
|
||||
data.get('material_type'),
|
||||
data.get('density'),
|
||||
data.get('remarks'),
|
||||
data.get('id')
|
||||
))
|
||||
conn.commit()
|
||||
return True
|
||||
except mysql.connector.Error as err:
|
||||
logging.error(f"Error updating material property: {err}")
|
||||
return False
|
||||
|
||||
# 물성 정보 삭제
|
||||
def delete_material_property(material_id):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
with conn.cursor(dictionary=True) as cursor:
|
||||
cursor.execute("SELECT COUNT(*) as count FROM inventory WHERE material_id = %s", (material_id,))
|
||||
if cursor.fetchone()['count'] > 0:
|
||||
logging.warning(f"Cannot delete material property {material_id} as it is in use by inventory.")
|
||||
return False
|
||||
cursor.execute("DELETE FROM material_properties WHERE id = %s", (material_id,))
|
||||
conn.commit()
|
||||
return True
|
||||
except mysql.connector.Error as err:
|
||||
logging.error(f"Error deleting material property: {err}")
|
||||
return False
|
||||
|
||||
# 모든 물성 정보 조회
|
||||
def get_all_material_properties():
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
with conn.cursor(dictionary=True) as cursor:
|
||||
cursor.execute("SELECT * FROM material_properties")
|
||||
return cursor.fetchall()
|
||||
except mysql.connector.Error as err:
|
||||
logging.error(f"Error getting all material properties: {err}")
|
||||
return []
|
||||
|
||||
# ID로 특정 물성 정보 조회
|
||||
def get_material_property_by_id(material_id):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
with conn.cursor(dictionary=True) as cursor:
|
||||
cursor.execute("SELECT * FROM material_properties WHERE id = %s", (material_id,))
|
||||
return cursor.fetchone()
|
||||
except mysql.connector.Error as err:
|
||||
logging.error(f"Error getting material property by id: {err}")
|
||||
return None
|
||||
|
||||
# 재고 입고 (추가)
|
||||
def add_inventory_to_db(data):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
with conn.cursor(dictionary=True) as cursor:
|
||||
stock = data.get('receipt_quantity', 0) - data.get('usage_quantity', 0)
|
||||
sql = """
|
||||
INSERT INTO inventory (material_id, receipt_date, receipt_quantity, usage_quantity, stock, unit_cost, remarks, is_depleted, supplier_id, item_name, item_number)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
"""
|
||||
cursor.execute(sql, (
|
||||
data.get('material_id'),
|
||||
data.get('receipt_date'),
|
||||
data.get('receipt_quantity'),
|
||||
data.get('usage_quantity', 0),
|
||||
stock,
|
||||
data.get('unit_cost'),
|
||||
data.get('remarks'),
|
||||
1 if stock <= 0 else 0,
|
||||
data.get('supplier_id'),
|
||||
data.get('item_name'),
|
||||
data.get('item_number')
|
||||
))
|
||||
conn.commit()
|
||||
return cursor.lastrowid
|
||||
except mysql.connector.Error as err:
|
||||
logging.error(f"Error adding inventory: {err}")
|
||||
return None
|
||||
|
||||
# 재고 정보 수정
|
||||
def update_inventory_in_db(data):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
with conn.cursor(dictionary=True) as cursor:
|
||||
stock = data.get('receipt_quantity', 0) - data.get('usage_quantity', 0)
|
||||
sql = """
|
||||
UPDATE inventory SET
|
||||
material_id = %s,
|
||||
receipt_date = %s,
|
||||
receipt_quantity = %s,
|
||||
usage_quantity = %s,
|
||||
stock = %s,
|
||||
unit_cost = %s,
|
||||
remarks = %s,
|
||||
is_depleted = %s,
|
||||
supplier_id = %s,
|
||||
item_name = %s,
|
||||
item_number = %s
|
||||
WHERE id = %s
|
||||
"""
|
||||
cursor.execute(sql, (
|
||||
data.get('material_id'),
|
||||
data.get('receipt_date'),
|
||||
data.get('receipt_quantity'),
|
||||
data.get('usage_quantity'),
|
||||
stock,
|
||||
data.get('unit_cost'),
|
||||
data.get('remarks'),
|
||||
1 if stock <= 0 else 0,
|
||||
data.get('supplier_id'),
|
||||
data.get('item_name'),
|
||||
data.get('item_number'),
|
||||
data.get('id')
|
||||
))
|
||||
conn.commit()
|
||||
return True
|
||||
except mysql.connector.Error as err:
|
||||
logging.error(f"Error updating inventory: {err}")
|
||||
return False
|
||||
|
||||
# 재고 소진 처리
|
||||
def deplete_inventory_in_db(inventory_id):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
with conn.cursor(dictionary=True) as cursor:
|
||||
sql = "UPDATE inventory SET is_depleted = 1, stock = 0 WHERE id = %s"
|
||||
cursor.execute(sql, (inventory_id,))
|
||||
conn.commit()
|
||||
return True
|
||||
except mysql.connector.Error as err:
|
||||
logging.error(f"Error depleting inventory: {err}")
|
||||
return False
|
||||
|
||||
# 모든 재고 목록 조회
|
||||
def get_all_inventory_from_db():
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
with conn.cursor(dictionary=True) as cursor:
|
||||
sql = """
|
||||
SELECT
|
||||
i.id, i.material_id, i.receipt_date, i.receipt_quantity, i.usage_quantity,
|
||||
i.stock, i.unit_cost, i.remarks, i.is_depleted, i.supplier_id,
|
||||
i.item_name, i.item_number,
|
||||
mp.material_code, mp.material_name, mp.material_type, mp.density,
|
||||
s.name_kr as supplier
|
||||
FROM inventory i
|
||||
LEFT JOIN material_properties mp ON i.material_id = mp.id
|
||||
LEFT JOIN suppliers s ON i.supplier_id = s.id
|
||||
ORDER BY i.receipt_date DESC
|
||||
"""
|
||||
cursor.execute(sql)
|
||||
return cursor.fetchall()
|
||||
except mysql.connector.Error as err:
|
||||
logging.error(f"Error getting all inventory: {err}")
|
||||
return []
|
||||
|
||||
# 특정 재고 상세 정보 조회
|
||||
def get_inventory_details_from_db(inventory_id):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
with conn.cursor(dictionary=True) as cursor:
|
||||
sql = """
|
||||
SELECT
|
||||
i.*,
|
||||
mp.material_code, mp.material_name, mp.material_type, mp.density,
|
||||
s.name_kr as supplier_name
|
||||
FROM inventory i
|
||||
LEFT JOIN material_properties mp ON i.material_id = mp.id
|
||||
LEFT JOIN suppliers s ON i.supplier_id = s.id
|
||||
WHERE i.id = %s
|
||||
"""
|
||||
cursor.execute(sql, (inventory_id,))
|
||||
inventory_data = cursor.fetchone()
|
||||
if not inventory_data:
|
||||
return None
|
||||
|
||||
return {
|
||||
"inventory": inventory_data
|
||||
}
|
||||
except mysql.connector.Error as err:
|
||||
logging.error(f"Error getting inventory details: {err}")
|
||||
return None
|
||||
|
||||
# 특정 자재의 총 재고량 조회
|
||||
def get_total_stock_for_material(material_id):
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
with conn.cursor(dictionary=True) as cursor:
|
||||
sql = "SELECT SUM(stock) as total_stock FROM inventory WHERE material_id = %s AND is_depleted = 0"
|
||||
cursor.execute(sql, (material_id,))
|
||||
result = cursor.fetchone()
|
||||
return result['total_stock'] if result and result['total_stock'] is not None else 0
|
||||
except mysql.connector.Error as err:
|
||||
logging.error(f"Error getting total stock for material: {err}")
|
||||
return 0
|
||||
@@ -0,0 +1,144 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
from .db_manager import (
|
||||
add_material_property,
|
||||
update_material_property,
|
||||
delete_material_property,
|
||||
get_all_material_properties,
|
||||
get_material_property_by_id,
|
||||
add_inventory_to_db,
|
||||
update_inventory_in_db,
|
||||
deplete_inventory_in_db,
|
||||
get_all_inventory_from_db,
|
||||
get_inventory_details_from_db
|
||||
)
|
||||
from .analysis import calculate_cost_analysis
|
||||
|
||||
router = APIRouter(prefix="/inventory", tags=["inventory"])
|
||||
|
||||
# --- Pydantic Schemas ---
|
||||
class MaterialPropertySchema(BaseModel):
|
||||
id: Optional[int] = None
|
||||
material_code: str
|
||||
material_name: str
|
||||
material_type: str
|
||||
density: float
|
||||
remarks: Optional[str] = None
|
||||
|
||||
class InventorySchema(BaseModel):
|
||||
id: Optional[int] = None
|
||||
material_id: int
|
||||
receipt_date: str # YYYY-MM-DD
|
||||
receipt_quantity: float
|
||||
usage_quantity: Optional[float] = 0.0
|
||||
stock: Optional[float] = None
|
||||
unit_cost: float
|
||||
remarks: Optional[str] = None
|
||||
is_depleted: Optional[int] = 0
|
||||
supplier_id: Optional[int] = None
|
||||
item_name: Optional[str] = None
|
||||
item_number: Optional[str] = None
|
||||
|
||||
class AnalysisInputSchema(BaseModel):
|
||||
yarn_inventory_id: int
|
||||
bond_inventory_id: Optional[int] = None
|
||||
yarn_diameter_micron: Optional[float] = 7.0
|
||||
yarn_k: Optional[float] = 12.0
|
||||
ports: Optional[float] = 40.0
|
||||
cycle_time_hz: Optional[float] = 8.0
|
||||
cut_length_mm: Optional[float] = 6.0
|
||||
work_hours_day: Optional[float] = 16.0
|
||||
work_days_month: Optional[float] = 20.0
|
||||
num_machines: Optional[float] = 2.0
|
||||
bond_percentage: Optional[float] = 3.0
|
||||
|
||||
class CostAnalysisRequestSchema(BaseModel):
|
||||
inputs: AnalysisInputSchema
|
||||
targetProductionQuantity: Optional[float] = None
|
||||
|
||||
# --- Material Properties APIs ---
|
||||
@router.post("/materials", response_model=dict)
|
||||
def create_material_property(material: MaterialPropertySchema):
|
||||
result = add_material_property(material.dict())
|
||||
if result is None:
|
||||
raise HTTPException(status_code=500, detail="Failed to create material property")
|
||||
return {"id": result, "status": "success"}
|
||||
|
||||
@router.put("/materials/{material_id}", response_model=dict)
|
||||
def update_material(material_id: int, material: MaterialPropertySchema):
|
||||
data = material.dict()
|
||||
data['id'] = material_id
|
||||
success = update_material_property(data)
|
||||
if not success:
|
||||
raise HTTPException(status_code=500, detail="Failed to update material property")
|
||||
return {"status": "success"}
|
||||
|
||||
@router.delete("/materials/{material_id}", response_model=dict)
|
||||
def delete_material(material_id: int):
|
||||
success = delete_material_property(material_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail="Failed to delete material property. Check if it's in use.")
|
||||
return {"status": "success"}
|
||||
|
||||
@router.get("/materials", response_model=List[MaterialPropertySchema])
|
||||
def list_material_properties():
|
||||
return get_all_material_properties()
|
||||
|
||||
@router.get("/materials/{material_id}", response_model=MaterialPropertySchema)
|
||||
def get_material_property(material_id: int):
|
||||
material = get_material_property_by_id(material_id)
|
||||
if not material:
|
||||
raise HTTPException(status_code=404, detail="Material property not found")
|
||||
return material
|
||||
|
||||
# --- Inventory APIs ---
|
||||
@router.post("/", response_model=dict)
|
||||
def create_inventory(inventory: InventorySchema):
|
||||
result = add_inventory_to_db(inventory.dict())
|
||||
if result is None:
|
||||
raise HTTPException(status_code=500, detail="Failed to create inventory record")
|
||||
return {"id": result, "status": "success"}
|
||||
|
||||
@router.put("/{inventory_id}", response_model=dict)
|
||||
def update_inventory(inventory_id: int, inventory: InventorySchema):
|
||||
data = inventory.dict()
|
||||
data['id'] = inventory_id
|
||||
success = update_inventory_in_db(data)
|
||||
if not success:
|
||||
raise HTTPException(status_code=500, detail="Failed to update inventory record")
|
||||
return {"status": "success"}
|
||||
|
||||
@router.put("/{inventory_id}/deplete", response_model=dict)
|
||||
def deplete_inventory(inventory_id: int):
|
||||
success = deplete_inventory_in_db(inventory_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=500, detail="Failed to deplete inventory")
|
||||
return {"status": "success"}
|
||||
|
||||
@router.get("/", response_model=List[dict])
|
||||
def list_inventory():
|
||||
return get_all_inventory_from_db()
|
||||
|
||||
@router.get("/{inventory_id}", response_model=dict)
|
||||
def get_inventory_details(inventory_id: int):
|
||||
details = get_inventory_details_from_db(inventory_id)
|
||||
if not details:
|
||||
raise HTTPException(status_code=404, detail="Inventory details not found")
|
||||
return details
|
||||
|
||||
# --- Cost Analysis APIs ---
|
||||
@router.post("/analysis/calculate", response_model=dict)
|
||||
def cost_analysis(request_data: CostAnalysisRequestSchema):
|
||||
try:
|
||||
inputs_dict = request_data.inputs.dict()
|
||||
# Null 문자열 파라미터 방어용 가공
|
||||
if not inputs_dict.get("bond_inventory_id"):
|
||||
inputs_dict["bond_inventory_id"] = "null"
|
||||
|
||||
result = calculate_cost_analysis(inputs_dict, request_data.targetProductionQuantity)
|
||||
return result
|
||||
except ValueError as val_err:
|
||||
raise HTTPException(status_code=400, detail=str(val_err))
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=f"Internal calculations failed: {exc}")
|
||||
Reference in New Issue
Block a user