diff --git a/app/static/css/calculator.css b/app/static/css/calculator.css new file mode 100644 index 0000000..f1648a5 --- /dev/null +++ b/app/static/css/calculator.css @@ -0,0 +1,91 @@ +/* Calculator Page Styles */ +.container-main { + max-width: 1200px; + margin: 2rem auto; + padding: 0 1rem; +} + +.calculator-card { + background-color: #ffffff; + border-radius: 0.5rem; + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); +} + +.section-header { + background-color: #f8f9fa; + padding: 1rem; + border-radius: 0.5rem; + margin-bottom: 1.5rem; + border-left: 4px solid #0d6efd; +} + +.section-content { + background-color: #ffffff; + padding: 1.5rem; + border-radius: 0.5rem; + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); + margin-bottom: 2rem; +} + +.form-label { + color: #495057; + font-weight: 500; +} + +.form-select, .form-control { + border: 1px solid #ced4da; +} + +.form-select:focus, .form-control:focus { + border-color: #86b7fe; + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); +} + +/* Table Styles */ +.table { + margin-bottom: 0; +} + +.table th { + background-color: #f8f9fa; + font-weight: 600; + color: #495057; +} + +.table td { + vertical-align: middle; +} + +/* Button Styles */ +.btn-primary { + background-color: #0d6efd; + border-color: #0d6efd; + font-weight: 500; +} + +.btn-primary:hover { + background-color: #0b5ed7; + border-color: #0a58ca; +} + +/* Form Text */ +.form-text { + color: #6c757d; + font-size: 0.875rem; + margin-top: 0.25rem; +} + +/* Responsive Adjustments */ +@media (max-width: 768px) { + .container-main { + margin: 1rem auto; + } + + .section-content { + padding: 1rem; + } + + .col-md-3 { + margin-bottom: 1rem; + } +} \ No newline at end of file diff --git a/app/static/js/mold_calculator.js b/app/static/js/mold_calculator.js new file mode 100755 index 0000000..4c8990f --- /dev/null +++ b/app/static/js/mold_calculator.js @@ -0,0 +1,638 @@ +// static/js/mold_calculator.js +// Check if script is already initialized +if (window.moldCalculatorInitialized) { + console.log('Mold calculator already initialized, skipping...'); +} else { + window.moldCalculatorInitialized = true; + + // Wait for DOM to be fully loaded + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializeCalculator); + } else { + initializeCalculator(); + } +} + +function initializeCalculator() { + console.log('DOM Content Loaded - Initializing mold calculator'); + + // Get form elements + const form = document.getElementById('calcForm'); + const mainSelect = document.getElementById('mold_main_type'); + const subSelect = document.getElementById('mold_sub_type'); + const specificTypeSelect = document.getElementById('mold_specific_type'); + const stageSelect = document.getElementById('stage'); + const typeSelect = document.getElementById('type'); + const toolingIdInput = document.getElementById('tooling_id'); + const submitButton = document.getElementById('calculateBtn'); + + console.log('Main select element:', mainSelect); + console.log('Sub select element:', subSelect); + console.log('Specific type select element:', specificTypeSelect); + console.log('Stage select element:', stageSelect); + console.log('Type select element:', typeSelect); + console.log('Tooling ID input:', toolingIdInput); + console.log('Submit button:', submitButton); + + // Check if submit button exists + if (!submitButton) { + console.error('Submit button not found! Looking for element with ID "calculateBtn"'); + // Try alternative selectors + const alternativeButton = form.querySelector('button[type="submit"]'); + if (alternativeButton) { + console.log('Found submit button using alternative selector:', alternativeButton); + } else { + console.error('No submit button found with any selector!'); + return; // Exit initialization if we can't find the submit button + } + } + + // Define mappings +const moldTypeMapping = { + 'Rubber': ['Flat Outsole', 'Cupsole', 'Rubber Sole'], + 'CMEVA': ['2 Plates Mold', '3 Plates Mold', '2 Plates with in-mold channel', 'Breathable + in-mold channel'], + 'IMEVA': ['2 Plates Mold', '3 Plates Mold'], + 'Sandal': ['Sandal with Upper Bandage', '2 Plate without Upper Bandage'], + 'Co-shot mold': ['Co-shot mold', 'Foaming mold'], + 'PU': ['2 Plates Mold', '3 Plates Mold'] +}; + + const specificTypeOptions = { + 'Flat Outsole': ['Flat', 'Flat with Top/Heel Tip'], + 'Cupsole': ['Top PL', 'Visible PL'], + 'Rubber Sole': ['Top PL', 'Short PL (Heel/Forefoot)', 'Visible PL', 'Wave PL'] +}; + +const stageTypeMapping = { + 'CR0': ['Sample'], + 'CR1': ['Sample'], + 'CR2': ['Sample'], + 'SMS': ['A Set'], + 'commercialization': ['A Set'], + 'production': ['Additional'] +}; + + // Function to update sub-type options + function updateSubTypeOptions() { + console.log('Populating sub-types'); + const selectedType = mainSelect.value; + console.log('Selected type:', selectedType); + + // Clear current options + subSelect.innerHTML = ''; + + if (selectedType && moldTypeMapping[selectedType]) { + console.log('Found mapping for type:', selectedType); + const options = moldTypeMapping[selectedType]; + console.log('Adding options:', options); + + options.forEach(option => { + const opt = document.createElement('option'); + opt.value = option; + opt.textContent = option; + subSelect.appendChild(opt); + }); + } + + // Reset specific type + specificTypeSelect.innerHTML = ''; + specificTypeSelect.disabled = true; + } + + // Function to update specific type options + function updateSpecificTypeOptions() { + const selectedSubType = subSelect.value; + console.log('Selected sub-type:', selectedSubType); + + // Clear current options + specificTypeSelect.innerHTML = ''; + + if (selectedSubType && specificTypeOptions[selectedSubType]) { + console.log('Found specific type options for:', selectedSubType); + const options = specificTypeOptions[selectedSubType]; + console.log('Adding specific type options:', options); + + options.forEach(option => { + const opt = document.createElement('option'); + opt.value = option; + opt.textContent = option; + specificTypeSelect.appendChild(opt); + }); + + specificTypeSelect.disabled = false; + } else { + specificTypeSelect.disabled = true; + } + } + + // Function to update type options based on stage + function updateTypeOptions() { + const selectedStage = stageSelect.value; + console.log('Selected stage:', selectedStage); + + // Clear current options + typeSelect.innerHTML = ''; + + if (selectedStage && stageTypeMapping[selectedStage]) { + const options = stageTypeMapping[selectedStage]; + console.log('Adding type options:', options); + + options.forEach(option => { + const opt = document.createElement('option'); + opt.value = option; + opt.textContent = option; + typeSelect.appendChild(opt); + }); + + // Enable the type select + typeSelect.disabled = false; + } else { + // Disable the type select if no stage is selected + typeSelect.disabled = true; + } + } + + // Add event listeners + mainSelect.addEventListener('change', function() { + console.log('Main type changed:', this.value); + updateSubTypeOptions(); + }); + + subSelect.addEventListener('change', function() { + console.log('Sub type changed:', this.value); + updateSpecificTypeOptions(); + }); + + stageSelect.addEventListener('change', function() { + console.log('Stage changed:', this.value); + updateTypeOptions(); + }); + + // Add tooling ID validation + if (toolingIdInput) { + console.log('Tooling ID input found, adding validation'); + toolingIdInput.addEventListener('input', function(e) { + console.log('Tooling ID input event triggered, value:', this.value); + // Remove any non-numeric characters + let value = this.value.replace(/\D/g, ''); + + // Limit to 5 digits + if (value.length > 5) { + value = value.substring(0, 5); + } + + this.value = value; + console.log('Tooling ID value after validation:', this.value); + }); + + toolingIdInput.addEventListener('paste', function(e) { + console.log('Tooling ID paste event triggered'); + e.preventDefault(); + const pastedText = (e.clipboardData || window.clipboardData).getData('text'); + const numericOnly = pastedText.replace(/\D/g, '').substring(0, 5); + this.value = numericOnly; + console.log('Tooling ID value after paste validation:', this.value); + }); + } else { + console.error('Tooling ID input element not found!'); + // Try alternative selectors + const alternativeSelectors = [ + 'input[name="tooling_id"]', + 'input[placeholder*="tooling"]', + 'input[placeholder*="Tooling"]' + ]; + + for (let selector of alternativeSelectors) { + const element = document.querySelector(selector); + if (element) { + console.log('Found tooling ID input using selector:', selector, element); + break; + } + } + } + + // Initialize options + updateSubTypeOptions(); + updateTypeOptions(); + + // Function to reset form + function resetForm(form) { + console.log('Resetting form...'); + + // Reset all select elements to their first option + const selects = form.querySelectorAll('select'); + selects.forEach(select => { + select.selectedIndex = 0; + select.dispatchEvent(new Event('change')); + }); + + // Reset all input elements + const inputs = form.querySelectorAll('input'); + inputs.forEach(input => { + input.value = ''; + }); + + // Reset all checkboxes and radio buttons + const checkboxes = form.querySelectorAll('input[type="checkbox"], input[type="radio"]'); + checkboxes.forEach(checkbox => { + checkbox.checked = false; + }); + + // Disable dependent dropdowns + if (subSelect) subSelect.disabled = true; + if (specificTypeSelect) specificTypeSelect.disabled = true; + if (typeSelect) typeSelect.disabled = true; + + // Show reset notification + const notification = document.createElement('div'); + notification.className = 'alert alert-info alert-dismissible fade show mt-3'; + notification.innerHTML = ` + + Form has been reset. You can now create a new quotation. + + `; + form.insertBefore(notification, form.firstChild); + + console.log('Form reset complete'); + } + + // Handle form submission + form.addEventListener('submit', async function(e) { + e.preventDefault(); + console.log('Form submitted'); + + // Get submit button robustly + let submitButton = document.getElementById('calculateBtn') || form.querySelector('button[type="submit"]'); + + if (!submitButton) { + console.error('Submit button not found during form submission!'); + alert('Error: Submit button not found. Please refresh the page and try again.'); + return; + } + + // Check if submission is already in progress + if (submitButton.disabled || form.dataset.submitting === 'true') { + console.log('Form submission already in progress or cooldown active, ignoring duplicate click'); + return; + } + + // Set submission state + form.dataset.submitting = 'true'; + + // Disable submit button and show loading state + submitButton.disabled = true; + const originalButtonText = submitButton.innerHTML; + submitButton.innerHTML = 'Calculating...'; + submitButton.classList.add('btn-secondary'); + submitButton.classList.remove('btn-primary'); + submitButton.title = 'Please wait while calculating...'; + + try { + // Log all form fields + console.log('Form fields:'); + for (let [key, value] of new FormData(form).entries()) { + console.log(`${key}: ${value}`); + } + + // Validate required fields + const mainType = mainSelect.value; + const subType = subSelect.value; + const specificType = specificTypeSelect.value; + const stage = stageSelect.value; + const type = typeSelect.value; + const toolingId = toolingIdInput ? toolingIdInput.value : ''; + + console.log('Required fields:', { + mainType, + subType, + specificType, + stage, + type, + toolingId + }); + + if (!mainType) { + alert('Please select a main type'); + return; + } + if (!subType) { + alert('Please select a sub type'); + return; + } + if (!stage) { + alert('Please select a stage'); + return; + } + if (!type) { + alert('Please select a type'); + return; + } + + // Validate tooling ID + if (!toolingId) { + alert('Please enter a Tooling ID'); + return; + } + if (!/^\d{5}$/.test(toolingId)) { + alert('Tooling ID must be exactly 5 digits'); + return; + } + + // Check if specific type is required + const validSpecificTypes = specificTypeOptions[subType] || []; + if (validSpecificTypes.length > 0 && !specificType) { + alert('Please select a specific type'); + return; + } + + // Create form data + const formData = new FormData(form); + + // Add CSRF token to form data + const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); + formData.append('csrf_token', csrfToken); + + // Remove specific type if it's not needed + if (validSpecificTypes.length === 0) { + formData.delete('mold_specific_type'); + } + + // Log the final form data being sent + console.log('Final form data being sent:'); + for (let [key, value] of formData.entries()) { + console.log(`${key}: ${value}`); + } + + const response = await fetch('/calculate', { + method: 'POST', + body: formData + }); + + console.log('Response status:', response.status); + const responseText = await response.text(); + console.log('Response text:', responseText); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}, body: ${responseText}`); + } + + const data = JSON.parse(responseText); + console.log('Calculation result:', data); + + // Get the result element + const resultElement = document.getElementById('result'); + + // Update the result display + updateResultsDisplay(data); + + // Save the quotation to the database + try { + console.log('Saving quotation to database...'); + const saveResponse = await fetch('/save-quotation', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': csrfToken + }, + body: JSON.stringify(data) + }); + + const saveResult = await saveResponse.json(); + console.log('Save result:', saveResult); + + if (saveResult.success) { + console.log('Quotation saved successfully with ID:', saveResult.quotation_id); + + // Show success message + const successMessage = document.createElement('div'); + successMessage.className = 'alert alert-success alert-dismissible fade show'; + successMessage.innerHTML = ` + + Quotation saved successfully! ID: ${saveResult.quotation_id} + + `; + resultElement.insertBefore(successMessage, resultElement.firstChild); + + // Reset form after successful save + resetForm(form); + + // Refresh quote history + setTimeout(() => { + refreshQuoteHistory(); + }, 1000); + } else if (saveResult.duplicate) { + console.log('Duplicate submission detected:', saveResult.message); + const duplicateMessage = document.createElement('div'); + duplicateMessage.className = 'alert alert-warning alert-dismissible fade show'; + duplicateMessage.innerHTML = ` + + ${saveResult.message} + + `; + resultElement.insertBefore(duplicateMessage, resultElement.firstChild); + + // Reset form even on duplicate to prevent repeated attempts + resetForm(form); + } + } catch (saveError) { + console.error('Error saving quotation:', saveError); + const errorMessage = document.createElement('div'); + errorMessage.className = 'alert alert-danger alert-dismissible fade show'; + errorMessage.innerHTML = ` + + Error saving quotation. Please try again later. + + `; + resultElement.insertBefore(errorMessage, resultElement.firstChild); + } + } catch (error) { + console.error('Error:', error); + alert('An error occurred while calculating. Please check the console for details.'); + } finally { + // Re-enable submit button and restore original state after a delay + setTimeout(() => { + let submitButton = document.getElementById('calculateBtn') || form.querySelector('button[type="submit"]'); + + if (submitButton) { + submitButton.disabled = false; + submitButton.innerHTML = originalButtonText; + submitButton.classList.remove('btn-secondary'); + submitButton.classList.add('btn-primary'); + submitButton.title = 'Calculate mold cost'; + } + + // Reset submission state + form.dataset.submitting = 'false'; + }, 2000); // 2 second cooldown before allowing new submission + } + }); +} + +// Function to update results display +function updateResultsDisplay(data) { + const resultElement = document.getElementById('result'); + if (!resultElement) { + console.error('Result element not found'); + return; + } + + // Format the results to match the provided screenshot + const formattedResults = ` +
+
+ Calculation Results +
+ +
+
+ Factory Information +
+
+
+
+
Mold Shop
+
${data.mold_shop_name || '-'}
+
${data.mold_shop_country || '-'}
+
+
+
+
+
T1 Factory
+
${data.t1_shoe_factory_name || '-'}
+
${data.t1_shoe_factory_country || '-'}
+
+
+
+
+
T2 Factory
+
${data.t2_component_factory_name || '-'}
+
${data.t2_component_factory_country || '-'}
+
+
+
+
+ + +
+
+ Mold Information +
+
+
+
+
Model
+
${data.model_name || '-'}
+
+
+
+
+
Tooling
+
${data.tooling_id || '-'}
+
+
+
+
+
Season & Stage
+
${(data.season || '-') + '/' + (data.stage || '-')}
+
+
+
+
+ + +
+
+ Mold Specifications +
+
+
+
+
Mold Type
+
${data.mold_main_type || '-'}
+
${data.mold_sub_type || ''}
+
+
+
+
+
Complexity Factors
+
+
High Sidewall
+
${data.complexity_high_sidewall === 'yes' ? 'Yes' : '-'}
+
Sliders
+
${data.sliders_count ? `${data.sliders_count} slider${data.sliders_count > 1 ? 's' : ''}` : '-'}
+
Cavities
+
${data.cavity_count ? `${data.cavity_count} pair` : '-'}
+
Digital Texture
+
${data.digital_texture_type ? `${data.digital_texture_type}` : '-'}
+
+
+
+
+
+ + +
+
+
+
+ Cost Breakdown +
+
+ + + + + + + +
Base Price$${Number(data.base_price).toFixed(2)}
Complexity Adjustment$${Number(data.complexity_cost).toFixed(2)}
Digital Texture Cost$${Number(data.texture_cost).toFixed(2)}
Total Cost$${Number(data.total_cost).toFixed(2)}
+
+
+
+
+
+
+ Approvals +
+
+
Issue Date: ${data.issue_date || '-'}
+
Mold Shop
${data.approved_by_mold_shop || '-'}
+
T1 Tooling
${data.approved_by_t1_tooling || '-'}
+
LO Tooling
${data.approved_by_lo_tooling || '-'}
+
+
+
+
+
+ `; + + resultElement.innerHTML = formattedResults; + } + + // Helper function to format currency + function formatCurrency(value) { + if (value === null || value === undefined) { + return 'N/A'; + } + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD' + }).format(value); +} + +// Refresh quote history +async function refreshQuoteHistory() { + try { + const response = await fetch('/quote-history'); + if (response.ok) { + const data = await response.json(); + // Update quote history display + const historyContainer = document.getElementById('quote-history'); + if (historyContainer) { + historyContainer.innerHTML = data.html; + } + } + } catch (error) { + console.error('Error refreshing quote history:', error); + } +} \ No newline at end of file diff --git a/app/templates/calculator.html b/app/templates/calculator.html new file mode 100755 index 0000000..d5cc203 --- /dev/null +++ b/app/templates/calculator.html @@ -0,0 +1,215 @@ +{% extends "base.html" %} + +{% block head %} + +{% endblock %} + +{% block content %} +
+
+

Mold Cost Calculator

+ {% if current_user.is_authenticated and current_user.is_admin %} + + Admin Portal + + {% endif %} +
+ +
+ {{ form.csrf_token }} + + +
+
+
Factory Information
+
+
+
+ {{ form.mold_shop_name.label(class="form-label") }} + {{ form.mold_shop_name(class="form-control") }} +
+
+
+
+ {{ form.mold_shop_country.label(class="form-label") }} + {{ form.mold_shop_country(class="form-control") }} +
+
+
+
+
+
+ {{ form.t1_shoe_factory_name.label(class="form-label") }} + {{ form.t1_shoe_factory_name(class="form-control") }} +
+
+
+
+ {{ form.t1_shoe_factory_country.label(class="form-label") }} + {{ form.t1_shoe_factory_country(class="form-control") }} +
+
+
+
+
+
+ {{ form.t2_component_factory_name.label(class="form-label") }} + {{ form.t2_component_factory_name(class="form-control") }} +
+
+
+
+ {{ form.t2_component_factory_country.label(class="form-label") }} + {{ form.t2_component_factory_country(class="form-control") }} +
+
+
+
+
+ + +
+
+
Mold Information
+
+
+
+ {{ form.model_name.label(class="form-label") }} + {{ form.model_name(class="form-control") }} +
+
+
+
+ {{ form.tooling_id.label(class="form-label") }} + {{ form.tooling_id(class="form-control") }} +
+
+
+
+
+
+ {{ form.season.label(class="form-label") }} + {{ form.season(class="form-control", id="season") }} +
+
+
+
+ {{ form.stage.label(class="form-label") }} + {{ form.stage(class="form-control", id="stage") }} +
+
+
+
+ {{ form.type.label(class="form-label") }} + {{ form.type(class="form-control", id="type") }} +
+
+
+
+
+ + +
+
+
Mold Specifications
+
+
+
+ {{ form.mold_main_type.label(class="form-label") }} + {{ form.mold_main_type(class="form-control", id="mold_main_type") }} +
+
+
+
+ {{ form.mold_sub_type.label(class="form-label") }} + {{ form.mold_sub_type(class="form-control", id="mold_sub_type") }} +
+
+
+
+ {{ form.mold_specific_type.label(class="form-label") }} + {{ form.mold_specific_type(class="form-control", id="mold_specific_type") }} +
+
+
+
+
+
+ {{ form.sliders_count.label(class="form-label") }} + {{ form.sliders_count(class="form-control") }} +
+
+
+
+ {{ form.cavity_count.label(class="form-label") }} + {{ form.cavity_count(class="form-control") }} +
+
+
+
+
+
+ {{ form.digital_texture_type.label(class="form-label") }} + {{ form.digital_texture_type(class="form-control") }} +
+
+
+
+ {{ form.complexity_high_sidewall.label(class="form-label") }} + {{ form.complexity_high_sidewall(class="form-control") }} +
+
+
+
+
+ + +
+
+
Dates & Approvals
+
+
+
+ {{ form.issue_date.label(class="form-label") }} + {{ form.issue_date(class="form-control", type="date") }} +
+
+
+
+
+
+ {{ form.approved_by_mold_shop.label(class="form-label") }} + {{ form.approved_by_mold_shop(class="form-control") }} +
+
+
+
+ {{ form.approved_by_t1_tooling.label(class="form-label") }} + {{ form.approved_by_t1_tooling(class="form-control") }} +
+
+
+
+ {{ form.approved_by_lo_tooling.label(class="form-label") }} + {{ form.approved_by_lo_tooling(class="form-control") }} +
+
+
+
+
+ + +
+ +
+
+ + +
+
+{% endblock %} + +{% block scripts %} +{{ super() }} + +{% endblock %} \ No newline at end of file diff --git a/app/utils/calculator.py b/app/utils/calculator.py new file mode 100644 index 0000000..c7d7dc3 --- /dev/null +++ b/app/utils/calculator.py @@ -0,0 +1,56 @@ +from ..utils.cost_factor_loader import get_cost_factors + +def calculate_base_price(mold_main_type, mold_sub_type, mold_specific_type, sliders_count, cavity_count, digital_texture_type, complexity_high_sidewall=False): + """ + Calculate the base price for a mold based on various factors. + + Args: + mold_main_type (str): The main type of the mold + mold_sub_type (str): The sub type of the mold + mold_specific_type (str): The specific type of the mold + sliders_count (int): Number of sliders + cavity_count (int): Number of cavities + digital_texture_type (str): Type of digital texture + complexity_high_sidewall (bool): Whether the mold has high sidewall complexity + + Returns: + dict: A dictionary containing all cost components + """ + # Get cost factors from database (or fallback to defaults) + cost_factors, tax_rates, default_net_base = get_cost_factors() + + # Start with the default net base + base_price = default_net_base + + # Apply mold type factor + if mold_specific_type in cost_factors['mold']: + base_price *= cost_factors['mold'][mold_specific_type] + + # Calculate complexity cost + complexity_cost = 0 + # Add slider complexity + if sliders_count > 0: + complexity_cost += base_price * (sliders_count * cost_factors['complexity']['slider']) + + # Add cavity complexity + if cavity_count > 1: + complexity_cost += base_price * ((cavity_count - 1) * cost_factors['complexity']['cavity']) + + # Add high sidewall complexity + if complexity_high_sidewall: + complexity_cost += base_price * cost_factors['complexity']['high_sidewall'] + + # Calculate texture cost + texture_cost = 0 + if digital_texture_type in cost_factors['texture']: + texture_cost = base_price * cost_factors['texture'][digital_texture_type] + + # Calculate total cost + total_cost = base_price + complexity_cost + texture_cost + + return { + 'base_price': round(base_price, 2), + 'complexity_cost': round(complexity_cost, 2), + 'texture_cost': round(texture_cost, 2), + 'total_cost': round(total_cost, 2) + } \ No newline at end of file diff --git a/app/utils/cost_factor_loader.py b/app/utils/cost_factor_loader.py new file mode 100644 index 0000000..20c1efb --- /dev/null +++ b/app/utils/cost_factor_loader.py @@ -0,0 +1,139 @@ +from ..models import CostFactor +from .. import db +import logging + +logger = logging.getLogger(__name__) + +# Single source of truth for default cost factors +DEFAULT_COST_FACTORS = [ + # Mold Type Factors + {'category': 'mold', 'name': 'Flat', 'factor_value': 1.0, 'description': 'Standard flat outsole'}, + {'category': 'mold', 'name': 'Flat with Top/Heel Tip', 'factor_value': 1.25, 'description': 'Flat with additional tip features'}, + {'category': 'mold', 'name': 'Top PL', 'factor_value': 1.05, 'description': 'Top part line mold'}, + {'category': 'mold', 'name': 'Visible PL', 'factor_value': 1.28, 'description': 'Visible part line mold'}, + {'category': 'mold', 'name': 'Short PL (Heel/Forefoot)', 'factor_value': 1.18, 'description': 'Short part line for specific areas'}, + {'category': 'mold', 'name': 'Wave PL', 'factor_value': 1.32, 'description': 'Wave pattern part line'}, + {'category': 'mold', 'name': 'Multi-color PL', 'factor_value': 1.35, 'description': 'Multi-color part line mold'}, + {'category': 'mold', 'name': '2 Plates Mold', 'factor_value': 1.0, 'description': 'Standard 2-plate configuration'}, + {'category': 'mold', 'name': '3 Plates Mold', 'factor_value': 1.35, 'description': 'Complex 3-plate configuration'}, + {'category': 'mold', 'name': '2 Plates with in-mold channel', 'factor_value': 1.25, 'description': '2-plate with channel features'}, + {'category': 'mold', 'name': 'Breathable + in-mold channel', 'factor_value': 1.45, 'description': 'Breathable with channel features'}, + {'category': 'mold', 'name': 'Sandal with Upper Bandage', 'factor_value': 1.15, 'description': 'Sandal with bandage features'}, + {'category': 'mold', 'name': '2 Plate without Upper Bandage', 'factor_value': 1.05, 'description': 'Standard 2-plate sandal'}, + {'category': 'mold', 'name': 'Co-shot mold', 'factor_value': 1.25, 'description': 'Multi-material injection'}, + {'category': 'mold', 'name': 'Foaming mold', 'factor_value': 1.4, 'description': 'Foam injection process'}, + + # Complexity Factors + {'category': 'complexity', 'name': 'high_sidewall', 'factor_value': 0.18, 'calculation_formula': 'Base Price × 0.18', 'description': 'Additional cost for high sidewall molds'}, + {'category': 'complexity', 'name': 'slider', 'factor_value': 0.1, 'calculation_formula': 'Base Price × (Number of Sliders × 0.10)', 'description': 'Cost per slider added to mold'}, + {'category': 'complexity', 'name': 'cavity', 'factor_value': 0.12, 'calculation_formula': 'Base Price × ((Number of Cavities - 1) × 0.12)', 'description': 'Cost for additional cavities beyond 1'}, + + # Texture Factors + {'category': 'texture', 'name': 'adidas_digital', 'factor_value': 0.0, 'percentage': '0%', 'description': 'No additional cost for standard adidas texture'}, + {'category': 'texture', 'name': 'customized texture', 'factor_value': 0.15, 'percentage': '15%', 'description': '15% of base price for custom texture'}, + {'category': 'texture', 'name': 'laser', 'factor_value': 0.2, 'percentage': '20%', 'description': '20% of base price for laser texture'}, + + # Tax Rates + {'category': 'tax', 'name': 'China', 'factor_value': 0.06, 'percentage': '6%', 'description': '6% VAT for China'}, + {'category': 'tax', 'name': 'Vietnam', 'factor_value': 0.10, 'percentage': '10%', 'description': '10% VAT for Vietnam'}, + {'category': 'tax', 'name': 'Indonesia', 'factor_value': 0.07, 'percentage': '7%', 'description': '7% VAT for Indonesia'}, + + # Base Price + {'category': 'base', 'name': 'DEFAULT_NET_BASE', 'factor_value': 12.5, 'description': 'Default base price for calculations'} +] + +def load_cost_factors_from_db(): + """ + Load cost factors from database and return them in the same format as constants. + Falls back to hardcoded values if database is not available. + """ + try: + # Get all active cost factors + factors = CostFactor.query.filter_by(is_active=True).all() + + if not factors: + logger.warning("No cost factors found in database, using hardcoded defaults") + return get_default_cost_factors() + + # Build the cost factors structure + cost_factors = { + 'mold': {}, + 'complexity': {}, + 'texture': {} + } + + tax_rates = {} + default_net_base = 12.5 # Default value + + for factor in factors: + if factor.category == 'mold': + cost_factors['mold'][factor.name] = factor.factor_value + elif factor.category == 'complexity': + cost_factors['complexity'][factor.name] = factor.factor_value + elif factor.category == 'texture': + cost_factors['texture'][factor.name] = factor.factor_value + elif factor.category == 'tax': + tax_rates[factor.name] = factor.factor_value + elif factor.category == 'base': + default_net_base = factor.factor_value + + logger.info(f"Loaded {len(factors)} cost factors from database") + return cost_factors, tax_rates, default_net_base + + except Exception as e: + logger.error(f"Error loading cost factors from database: {str(e)}") + logger.info("Falling back to hardcoded cost factors") + return get_default_cost_factors() + +def get_default_cost_factors(): + """Return hardcoded cost factors as fallback - derived from single source of truth""" + cost_factors = { + 'mold': {}, + 'complexity': {}, + 'texture': {} + } + + tax_rates = {} + default_net_base = 12.5 + + # Build the structure from the single source of truth + for factor_data in DEFAULT_COST_FACTORS: + category = factor_data['category'] + name = factor_data['name'] + value = factor_data['factor_value'] + + if category == 'mold': + cost_factors['mold'][name] = value + elif category == 'complexity': + cost_factors['complexity'][name] = value + elif category == 'texture': + cost_factors['texture'][name] = value + elif category == 'tax': + tax_rates[name] = value + elif category == 'base': + default_net_base = value + + return cost_factors, tax_rates, default_net_base + +def get_cost_factors(): + """Get current cost factors (from database or defaults)""" + return load_cost_factors_from_db() + +def initialize_default_cost_factors(): + """Initialize the database with default cost factors if none exist""" + try: + if CostFactor.query.count() == 0: + logger.info("Initializing database with default cost factors") + + # Use the single source of truth + for factor_data in DEFAULT_COST_FACTORS: + factor = CostFactor(**factor_data) + db.session.add(factor) + + db.session.commit() + logger.info(f"Initialized {len(DEFAULT_COST_FACTORS)} default cost factors") + + except Exception as e: + logger.error(f"Error initializing default cost factors: {str(e)}") + db.session.rollback() + raise \ No newline at end of file