330 lines
15 KiB
HTML
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"> </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"> </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 %} |