Django_Basic_Manufacturing_3/templates/manufacturing/bom_form.html
2025-08-22 17:05:22 +07:00

330 lines
15 KiB
HTML

{% extends "module_base.html" %}
{% load static %}
{% block title %}{{ module_title }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h4 class="card-title mb-0">
<i class="fas fa-cogs me-2"></i>{{ module_title }}
</h4>
</div>
<div class="card-body">
<form method="post" class="needs-validation" novalidate>
{% csrf_token %}
<!-- BOM Details -->
<div class="row">
<div class="col-md-12">
<div class="mb-3">
<label for="{{ form.product.id_for_label }}" class="form-label">Product</label>
{{ form.product }}
{% if form.product.errors %}
<div class="invalid-feedback">
{% for error in form.product.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.version.id_for_label }}" class="form-label">Version</label>
{{ form.version }}
{% if form.version.errors %}
<div class="invalid-feedback">
{% for error in form.version.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<div class="form-check">
{{ form.is_active }}
<label for="{{ form.is_active.id_for_label }}" class="form-check-label">
Active
</label>
</div>
{% if form.is_active.errors %}
<div class="invalid-feedback">
{% for error in form.is_active.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
<!-- BOM Items Formset -->
<div class="card mt-4">
<div class="card-header">
<h5 class="mb-0">Bill of Materials Items</h5>
</div>
<div class="card-body">
{{ formset.management_form }}
<div id="bom-items">
{% for form in formset %}
<div class="bom-item mb-3 p-3 border rounded">
<div class="row">
<div class="col-md-5">
<label class="form-label">Component</label>
{{ form.component }}
{% if form.component.errors %}
<div class="invalid-feedback">
{% for error in form.component.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
<div class="col-md-3">
<label class="form-label">Quantity</label>
{{ form.quantity }}
{% if form.quantity.errors %}
<div class="invalid-feedback">
{% for error in form.quantity.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
<div class="col-md-3">
<label class="form-label">Unit of Measure</label>
{{ form.unit_of_measure }}
{% if form.unit_of_measure.errors %}
<div class="invalid-feedback">
{% for error in form.unit_of_measure.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
<div class="col-md-1">
<label class="form-label">&nbsp;</label>
<button type="button" class="btn btn-danger btn-sm remove-item">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
{{ form.id }}
<!-- Hide the DELETE checkbox -->
{% if form.DELETE %}
{{ form.DELETE.as_hidden }}
{% endif %}
</div>
{% endfor %}
</div>
<button type="button" class="btn btn-success btn-sm" id="add-item">
<i class="fas fa-plus me-1"></i>Add Component
</button>
</div>
</div>
<!-- Form Actions -->
<div class="mt-4">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>Save Bill of Material
</button>
<a href="{% url 'manufacturing:bom_list' %}" class="btn btn-secondary ms-2">
<i class="fas fa-times me-2"></i>Cancel
</a>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Auto-create first form if no forms exist
const container = document.getElementById('bom-items');
if (container && container.children.length === 0) {
const totalForms = document.querySelector('input[name$="-TOTAL_FORMS"]');
totalForms.value = 1;
// Create the first form
const templateForm = document.createElement('div');
templateForm.className = 'bom-item mb-3 p-3 border rounded';
templateForm.id = 'bom-item-0';
templateForm.innerHTML = `
<div class="row">
<div class="col-md-5">
<label class="form-label">Component</label>
<select name="bomitem_set-0-component" class="form-select" required>
<option value="">---------</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Quantity</label>
<input type="number" name="bomitem_set-0-quantity" step="0.01" min="0" class="form-control" required>
</div>
<div class="col-md-3">
<label class="form-label">Unit of Measure</label>
<select name="bomitem_set-0-unit_of_measure" class="form-select" required>
<option value="">---------</option>
</select>
</div>
<div class="col-md-1">
<label class="form-label">&nbsp;</label>
<button type="button" class="btn btn-danger btn-sm remove-item">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<input type="hidden" name="bomitem_set-0-id" id="id_bomitem_set-0-id">
<input type="hidden" name="bomitem_set-0-DELETE" id="id_bomitem_set-0-DELETE">
`;
container.appendChild(templateForm);
attachFormEvents(templateForm);
}
// Add new BOM item functionality
document.getElementById('add-item').addEventListener('click', function() {
const totalForms = document.querySelector('input[name$="-TOTAL_FORMS"]');
const maxForms = document.querySelector('input[name$="-MAX_NUM_FORMS"]');
const formNum = parseInt(totalForms.value);
// Check if we've reached the maximum number of forms
if (maxForms && parseInt(maxForms.value) > 0 && formNum >= parseInt(maxForms.value)) {
alert('Maximum number of components reached.');
return;
}
// Find the first non-deleted form to clone as template
const container = document.getElementById('bom-items');
const forms = container.querySelectorAll('.bom-item:not(.deleted-item)');
const templateForm = forms.length > 0 ? forms[0] : container.firstElementChild;
if (!templateForm) {
console.error('No template form found');
return;
}
const newForm = templateForm.cloneNode(true);
// Update form indices - replace all occurrences of -0- with the new form number
newForm.innerHTML = newForm.innerHTML.replace(
new RegExp(`-0-`, 'g'),
`-${formNum}-`
);
// Update the form ID
newForm.id = `bom-item-${formNum}`;
// Clear form values and reset state
newForm.className = 'bom-item mb-3 p-3 border rounded';
newForm.querySelectorAll('input, select, textarea').forEach(function(field) {
field.disabled = false;
if (field.type === 'checkbox') {
field.checked = false;
} else if (field.tagName === 'SELECT') {
field.selectedIndex = 0;
} else {
field.value = '';
}
// Re-enable required attribute if it was removed
if (field.name.includes('component') || field.name.includes('quantity') || field.name.includes('unit_of_measure')) {
field.setAttribute('required', 'required');
}
});
container.appendChild(newForm);
totalForms.value = formNum + 1;
// Re-attach event listeners to the new form
attachFormEvents(newForm);
});
// Auto-populate unit of measure when component is selected
document.addEventListener('change', function(e) {
if (e.target.name && e.target.name.includes('component') && e.target.tagName === 'SELECT') {
const componentSelect = e.target;
const formItem = componentSelect.closest('.bom-item');
const uomSelect = formItem.querySelector('select[name*="-unit_of_measure"]');
if (componentSelect.value && uomSelect) {
// Get the selected option
const selectedOption = componentSelect.options[componentSelect.selectedIndex];
const unitOfMeasureId = selectedOption.getAttribute('data-uom-id');
if (unitOfMeasureId) {
// Set the unit of measure
for (let i = 0; i < uomSelect.options.length; i++) {
if (uomSelect.options[i].value === unitOfMeasureId) {
uomSelect.selectedIndex = i;
uomSelect.dispatchEvent(new Event('change'));
break;
}
}
}
}
}
});
// Initialize component selects with unit of measure data
initializeComponentSelects();
});
// Function to attach events to form fields
function attachFormEvents(form) {
const removeBtn = form.querySelector('.remove-item');
if (removeBtn) {
const newBtn = removeBtn.cloneNode(true);
removeBtn.parentNode.replaceChild(newBtn, removeBtn);
newBtn.addEventListener('click', function() {
removeComponent(this);
});
}
}
function removeComponent(button) {
// Show confirmation dialog
const confirmed = confirm('Are you sure you want to delete this component? This action cannot be undone.');
if (confirmed) {
const itemForm = button.closest('.bom-item');
const deleteCheckbox = itemForm.querySelector('input[name$="-DELETE"]');
if (deleteCheckbox) {
// Mark for deletion in the form
deleteCheckbox.checked = true;
// Hide the item visually and disable all form inputs
itemForm.classList.add('d-none', 'deleted-item');
itemForm.querySelectorAll('input, select, textarea').forEach(function(field) {
field.disabled = true;
// Clear required attribute to prevent validation errors
field.removeAttribute('required');
});
}
}
}
// Initialize component selects with unit of measure data
function initializeComponentSelects() {
const componentSelects = document.querySelectorAll('select[name*="-component"]');
componentSelects.forEach(select => {
if (select.options.length > 0 && !select.options[1].hasAttribute('data-uom-id')) {
// Add data-uom-id attributes via AJAX
fetch('/inventory/api/products/')
.then(response => response.json())
.then(data => {
data.forEach(product => {
const option = select.querySelector(`option[value="${product.id}"]`);
if (option && product.unit_of_measure) {
option.setAttribute('data-uom-id', product.unit_of_measure.id);
}
});
})
.catch(error => {
console.log('Could not fetch product data for UOM auto-population', error);
});
}
});
}
</script>
{% endblock %}