🔧 FIX: Restore missing calculator files from src/ to app/ directory
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Executable
+638
@@ -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 = '<option value="">Select Sub Type</option>';
|
||||
|
||||
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 = '<option value="">Select Specific Type</option>';
|
||||
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 = '<option value="">Select Specific Type</option>';
|
||||
|
||||
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 = '<option value="">Select Type</option>';
|
||||
|
||||
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 = `
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
Form has been reset. You can now create a new quotation.
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
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 = '<i class="fas fa-spinner fa-spin me-2"></i>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 = `
|
||||
<i class="fas fa-check-circle me-2"></i>
|
||||
Quotation saved successfully! ID: ${saveResult.quotation_id}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
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 = `
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
${saveResult.message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
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 = `
|
||||
<i class="fas fa-exclamation-circle me-2"></i>
|
||||
Error saving quotation. Please try again later.
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
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 = `
|
||||
<div class="card bg-white shadow-sm p-4 mb-4" style="border-radius: 1rem; border: 1px solid #e3e6ea;">
|
||||
<div class="mb-4 pb-2 border-bottom d-flex align-items-center" style="font-size:1.4rem;font-weight:600;color:#1976d2;">
|
||||
<i class="fas fa-clipboard-list me-2"></i>Calculation Results
|
||||
</div>
|
||||
<!-- Factory Information -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex align-items-center mb-2" style="color:#1976d2;font-weight:600;font-size:1.1rem;">
|
||||
<i class="fas fa-industry me-2"></i>Factory Information
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="card p-3 h-100 bg-light border-0">
|
||||
<div class="fw-bold">Mold Shop</div>
|
||||
<div>${data.mold_shop_name || '-'}</div>
|
||||
<div class="text-muted small">${data.mold_shop_country || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card p-3 h-100 bg-light border-0">
|
||||
<div class="fw-bold">T1 Factory</div>
|
||||
<div>${data.t1_shoe_factory_name || '-'}</div>
|
||||
<div class="text-muted small">${data.t1_shoe_factory_country || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card p-3 h-100 bg-light border-0">
|
||||
<div class="fw-bold">T2 Factory</div>
|
||||
<div>${data.t2_component_factory_name || '-'}</div>
|
||||
<div class="text-muted small">${data.t2_component_factory_country || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mold Information -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex align-items-center mb-2" style="color:#1976d2;font-weight:600;font-size:1.1rem;">
|
||||
<i class="fas fa-cube me-2"></i>Mold Information
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="card p-3 h-100">
|
||||
<div class="fw-bold">Model</div>
|
||||
<div>${data.model_name || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card p-3 h-100">
|
||||
<div class="fw-bold">Tooling</div>
|
||||
<div>${data.tooling_id || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card p-3 h-100">
|
||||
<div class="fw-bold">Season & Stage</div>
|
||||
<div>${(data.season || '-') + '/' + (data.stage || '-')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mold Specifications -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex align-items-center mb-2" style="color:#1976d2;font-weight:600;font-size:1.1rem;">
|
||||
<i class="fas fa-wrench me-2"></i>Mold Specifications
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card p-3 h-100">
|
||||
<div class="fw-bold">Mold Type</div>
|
||||
<div>${data.mold_main_type || '-'}</div>
|
||||
<div class="text-muted small">${data.mold_sub_type || ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card p-3 h-100">
|
||||
<div class="fw-bold mb-2">Complexity Factors</div>
|
||||
<div class="row">
|
||||
<div class="col-6 small">High Sidewall</div>
|
||||
<div class="col-6 small text-end">${data.complexity_high_sidewall === 'yes' ? '<b>Yes</b>' : '-'}</div>
|
||||
<div class="col-6 small">Sliders</div>
|
||||
<div class="col-6 small text-end">${data.sliders_count ? `<b>${data.sliders_count} slider${data.sliders_count > 1 ? 's' : ''}</b>` : '-'}</div>
|
||||
<div class="col-6 small">Cavities</div>
|
||||
<div class="col-6 small text-end">${data.cavity_count ? `<b>${data.cavity_count} pair</b>` : '-'}</div>
|
||||
<div class="col-6 small">Digital Texture</div>
|
||||
<div class="col-6 small text-end">${data.digital_texture_type ? `<b>${data.digital_texture_type}</b>` : '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cost Breakdown & Approvals -->
|
||||
<div class="row g-3">
|
||||
<div class="col-md-8">
|
||||
<div class="mb-4">
|
||||
<div class="d-flex align-items-center mb-2" style="color:#1976d2;font-weight:600;font-size:1.1rem;">
|
||||
<i class="fas fa-table me-2"></i>Cost Breakdown
|
||||
</div>
|
||||
<div class="card p-0">
|
||||
<table class="table mb-0">
|
||||
<tbody>
|
||||
<tr><td>Base Price</td><td class="text-end">$${Number(data.base_price).toFixed(2)}</td></tr>
|
||||
<tr><td>Complexity Adjustment</td><td class="text-end">$${Number(data.complexity_cost).toFixed(2)}</td></tr>
|
||||
<tr><td>Digital Texture Cost</td><td class="text-end">$${Number(data.texture_cost).toFixed(2)}</td></tr>
|
||||
<tr class="table-primary fw-bold"><td>Total Cost</td><td class="text-end">$${Number(data.total_cost).toFixed(2)}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-4">
|
||||
<div class="d-flex align-items-center mb-2" style="color:#1976d2;font-weight:600;font-size:1.1rem;">
|
||||
<i class="fas fa-check-circle me-2"></i>Approvals
|
||||
</div>
|
||||
<div class="card p-3">
|
||||
<div class="mb-3 text-muted small"><i class="far fa-calendar-alt me-1"></i>Issue Date: <b>${data.issue_date || '-'}</b></div>
|
||||
<div class="mb-2">Mold Shop<br><b>${data.approved_by_mold_shop || '-'}</b></div>
|
||||
<div class="mb-2">T1 Tooling<br><b>${data.approved_by_t1_tooling || '-'}</b></div>
|
||||
<div>LO Tooling<br><b>${data.approved_by_lo_tooling || '-'}</b></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
Executable
+215
@@ -0,0 +1,215 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block head %}
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>Mold Cost Calculator</h2>
|
||||
{% if current_user.is_authenticated and current_user.is_admin %}
|
||||
<a href="{{ url_for('admin.dashboard') }}" class="btn btn-outline-primary">
|
||||
<i class="fas fa-cog me-2"></i>Admin Portal
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<form id="calcForm" class="row g-3" method="POST" action="/calculate">
|
||||
{{ form.csrf_token }}
|
||||
|
||||
<!-- Factory Information -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title mb-3">Factory Information</h6>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
{{ form.mold_shop_name.label(class="form-label") }}
|
||||
{{ form.mold_shop_name(class="form-control") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
{{ form.mold_shop_country.label(class="form-label") }}
|
||||
{{ form.mold_shop_country(class="form-control") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
{{ form.t1_shoe_factory_name.label(class="form-label") }}
|
||||
{{ form.t1_shoe_factory_name(class="form-control") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
{{ form.t1_shoe_factory_country.label(class="form-label") }}
|
||||
{{ form.t1_shoe_factory_country(class="form-control") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
{{ form.t2_component_factory_name.label(class="form-label") }}
|
||||
{{ form.t2_component_factory_name(class="form-control") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
{{ form.t2_component_factory_country.label(class="form-label") }}
|
||||
{{ form.t2_component_factory_country(class="form-control") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mold Information -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title mb-3">Mold Information</h6>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
{{ form.model_name.label(class="form-label") }}
|
||||
{{ form.model_name(class="form-control") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
{{ form.tooling_id.label(class="form-label") }}
|
||||
{{ form.tooling_id(class="form-control") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
{{ form.season.label(class="form-label") }}
|
||||
{{ form.season(class="form-control", id="season") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
{{ form.stage.label(class="form-label") }}
|
||||
{{ form.stage(class="form-control", id="stage") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
{{ form.type.label(class="form-label") }}
|
||||
{{ form.type(class="form-control", id="type") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mold Specifications -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title mb-3">Mold Specifications</h6>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
{{ form.mold_main_type.label(class="form-label") }}
|
||||
{{ form.mold_main_type(class="form-control", id="mold_main_type") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
{{ form.mold_sub_type.label(class="form-label") }}
|
||||
{{ form.mold_sub_type(class="form-control", id="mold_sub_type") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
{{ form.mold_specific_type.label(class="form-label") }}
|
||||
{{ form.mold_specific_type(class="form-control", id="mold_specific_type") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
{{ form.sliders_count.label(class="form-label") }}
|
||||
{{ form.sliders_count(class="form-control") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
{{ form.cavity_count.label(class="form-label") }}
|
||||
{{ form.cavity_count(class="form-control") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
{{ form.digital_texture_type.label(class="form-label") }}
|
||||
{{ form.digital_texture_type(class="form-control") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
{{ form.complexity_high_sidewall.label(class="form-label") }}
|
||||
{{ form.complexity_high_sidewall(class="form-control") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dates & Approvals -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title mb-3">Dates & Approvals</h6>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
{{ form.issue_date.label(class="form-label") }}
|
||||
{{ form.issue_date(class="form-control", type="date") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
{{ form.approved_by_mold_shop.label(class="form-label") }}
|
||||
{{ form.approved_by_mold_shop(class="form-control") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
{{ form.approved_by_t1_tooling.label(class="form-label") }}
|
||||
{{ form.approved_by_t1_tooling(class="form-control") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
{{ form.approved_by_lo_tooling.label(class="form-label") }}
|
||||
{{ form.approved_by_lo_tooling(class="form-control") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="text-center">
|
||||
<button type="submit" id="calculateBtn" class="btn btn-primary btn-lg">Calculate</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Results Section -->
|
||||
<div id="result" class="mt-4"></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script id="mold-calculator-script" src="{{ url_for('static', filename='js/mold_calculator.js', v='1.4') }}"></script>
|
||||
{% endblock %}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user