384 lines
17 KiB
HTML
384 lines
17 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}{{ module_title }} - Manufacturing App{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid mt-4">
|
|
<div class="row">
|
|
<div class="col-md-10 offset-md-1">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h4 class="mb-0">
|
|
{% if is_create %}
|
|
<i class="fas fa-plus-circle"></i> Create Sales Order
|
|
{% else %}
|
|
<i class="fas fa-edit"></i> Edit Sales Order
|
|
{% endif %}
|
|
</h4>
|
|
</div>
|
|
<div class="card-body">
|
|
<form method="post" id="so-form">
|
|
{% csrf_token %}
|
|
|
|
<!-- Sales Order Header -->
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label for="{{ form.customer.id_for_label }}" class="form-label">
|
|
{{ form.customer.label }}{% if form.customer.field.required %}<span class="text-danger">*</span>{% endif %}
|
|
</label>
|
|
{{ form.customer }}
|
|
{% if form.customer.errors %}
|
|
<div class="text-danger small">
|
|
{% for error in form.customer.errors %}{{ error }}{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label for="{{ form.order_date.id_for_label }}" class="form-label">
|
|
{{ form.order_date.label }}{% if form.order_date.field.required %}<span class="text-danger">*</span>{% endif %}
|
|
</label>
|
|
{{ form.order_date }}
|
|
{% if form.order_date.errors %}
|
|
<div class="text-danger small">
|
|
{% for error in form.order_date.errors %}{{ error }}{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label for="{{ form.expected_delivery_date.id_for_label }}" class="form-label">
|
|
{{ form.expected_delivery_date.label }}
|
|
</label>
|
|
{{ form.expected_delivery_date }}
|
|
{% if form.expected_delivery_date.errors %}
|
|
<div class="text-danger small">
|
|
{% for error in form.expected_delivery_date.errors %}{{ error }}{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sales Order Items -->
|
|
<div class="formset-container">
|
|
<div class="formset-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-list"></i> Order Items
|
|
<button type="button" class="btn btn-sm btn-outline-primary float-end" id="add-item">
|
|
<i class="fas fa-plus"></i> Add Item
|
|
</button>
|
|
</h5>
|
|
</div>
|
|
|
|
{{ formset.management_form }}
|
|
|
|
<div id="sales-items-container">
|
|
{% for item_form in formset %}
|
|
<div class="item-form mb-3 p-3 border rounded {% if item_form.DELETE.value %}d-none deleted-item{% endif %}"
|
|
id="item-form-{{ forloop.counter0 }}">
|
|
<div class="row">
|
|
<div class="col-md-3">
|
|
<label class="form-label">Product</label>
|
|
{{ item_form.product }}
|
|
{% if item_form.product.errors %}
|
|
<div class="text-danger small">
|
|
{% for error in item_form.product.errors %}{{ error }}{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="col-md-3">
|
|
<label class="form-label">Quantity</label>
|
|
{{ item_form.quantity }}
|
|
{% if item_form.quantity.errors %}
|
|
<div class="text-danger small">
|
|
{% for error in item_form.quantity.errors %}{{ error }}{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="col-md-3">
|
|
<label class="form-label">Unit Price</label>
|
|
{{ item_form.unit_price }}
|
|
{% if item_form.unit_price.errors %}
|
|
<div class="text-danger small">
|
|
{% for error in item_form.unit_price.errors %}{{ error }}{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="col-md-3 d-flex align-items-end">
|
|
<div class="mb-3 me-2 flex-grow-1">
|
|
<label class="form-label">Total</label>
|
|
<div class="form-control-plaintext" id="item-total-{{ forloop.counter0 }}">
|
|
Rp 0,00
|
|
</div>
|
|
</div>
|
|
<div class="d-flex align-items-end">
|
|
<button type="button" class="btn btn-danger btn-sm mb-3" style="display: block;">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{ item_form.id }}
|
|
{{ item_form.DELETE }}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="{{ form.notes.id_for_label }}" class="form-label">
|
|
{{ form.notes.label }}
|
|
</label>
|
|
{{ form.notes }}
|
|
{% if form.notes.errors %}
|
|
<div class="text-danger small">
|
|
{% for error in form.notes.errors %}{{ error }}{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="d-flex justify-content-between">
|
|
<a href="{% url 'sales:so_list' %}" class="btn btn-secondary">
|
|
<i class="fas fa-times"></i> Cancel
|
|
</a>
|
|
<button type="submit" class="btn btn-primary">
|
|
{% if is_create %}
|
|
<i class="fas fa-save"></i> Create Sales Order
|
|
{% else %}
|
|
<i class="fas fa-save"></i> Update Sales Order
|
|
{% endif %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.formset-container {
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.formset-header {
|
|
background-color: #f8f9fa;
|
|
padding: 0.5rem 1rem;
|
|
margin: -1rem -1rem 1rem -1rem;
|
|
border-bottom: 1px solid #dee2e6;
|
|
}
|
|
|
|
.delete-checkbox {
|
|
margin-top: 2rem;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Auto-create first form if no forms exist
|
|
const container = document.getElementById('sales-items-container');
|
|
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 = 'item-form mb-3 p-3 border rounded';
|
|
templateForm.id = 'item-form-0';
|
|
templateForm.innerHTML = `
|
|
<div class="row">
|
|
<div class="col-md-3">
|
|
<label class="form-label">Product</label>
|
|
<select name="form-0-product" class="form-control" required>
|
|
<option value="">---------</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Quantity</label>
|
|
<input type="number" name="form-0-quantity" class="form-control" step="0.01" min="0" required>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label class="form-label">Unit Price</label>
|
|
<input type="number" name="form-0-unit_price" class="form-control" step="0.01" min="0" required>
|
|
</div>
|
|
<div class="col-md-3 d-flex align-items-end">
|
|
<div class="mb-3 me-2 flex-grow-1">
|
|
<label class="form-label">Total</label>
|
|
<div class="form-control-plaintext" id="item-total-0">Rp 0,00</div>
|
|
</div>
|
|
<div class="d-flex align-items-end">
|
|
<button type="button" class="btn btn-danger btn-sm mb-3" style="display: block;">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<input type="hidden" name="form-0-id" id="id_form-0-id">
|
|
<input type="checkbox" name="form-0-DELETE" id="id_form-0-DELETE" style="display: none;">
|
|
`;
|
|
container.appendChild(templateForm);
|
|
attachFormEvents(templateForm);
|
|
}
|
|
|
|
// Auto-calculate totals when quantity or unit price changes
|
|
document.addEventListener('input', function(e) {
|
|
if (e.target.matches('input[name$="-quantity"], input[name$="-unit_price"]')) {
|
|
calculateItemTotal(e.target.closest('.item-form'));
|
|
}
|
|
});
|
|
|
|
// Add new 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 items reached.');
|
|
return;
|
|
}
|
|
|
|
// Find the first non-deleted form to clone as template
|
|
const container = document.getElementById('sales-items-container');
|
|
const forms = container.querySelectorAll('.item-form: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 = `item-form-${formNum}`;
|
|
|
|
// Clear form values and reset state
|
|
newForm.className = 'item-form 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('product') || field.name.includes('quantity') || field.name.includes('unit_price')) {
|
|
field.setAttribute('required', 'required');
|
|
}
|
|
});
|
|
|
|
// Reset total display
|
|
const totalDisplay = newForm.querySelector('[id^="item-total-"]');
|
|
if (totalDisplay) {
|
|
totalDisplay.id = `item-total-${formNum}`;
|
|
totalDisplay.textContent = 'Rp 0,00';
|
|
}
|
|
|
|
// Always show remove button for added items (never hide it)
|
|
const removeBtn = newForm.querySelector('.btn-danger');
|
|
if (removeBtn) {
|
|
removeBtn.style.display = 'block';
|
|
}
|
|
|
|
container.appendChild(newForm);
|
|
totalForms.value = formNum + 1;
|
|
|
|
// Re-attach event listeners to the new form
|
|
attachFormEvents(newForm);
|
|
});
|
|
});
|
|
|
|
// Function to attach events to form fields
|
|
function attachFormEvents(form) {
|
|
// Remove existing event listeners to prevent duplicates
|
|
form.querySelectorAll('input[name$="-quantity"], input[name$="-unit_price"]').forEach(function(input) {
|
|
const newInput = input.cloneNode(true);
|
|
input.parentNode.replaceChild(newInput, input);
|
|
newInput.addEventListener('input', function() {
|
|
calculateItemTotal(form);
|
|
});
|
|
});
|
|
|
|
const removeBtn = form.querySelector('.btn-danger');
|
|
if (removeBtn) {
|
|
const newBtn = removeBtn.cloneNode(true);
|
|
removeBtn.parentNode.replaceChild(newBtn, removeBtn);
|
|
newBtn.addEventListener('click', function() {
|
|
removeItem(this);
|
|
});
|
|
}
|
|
}
|
|
|
|
function calculateItemTotal(form) {
|
|
const quantity = parseFloat(form.querySelector('input[name$="-quantity"]').value) || 0;
|
|
const unitPrice = parseFloat(form.querySelector('input[name$="-unit_price"]').value) || 0;
|
|
const total = quantity * unitPrice;
|
|
|
|
// Update the total display
|
|
const totalDisplay = form.querySelector('[id^="item-total-"]');
|
|
if (totalDisplay) {
|
|
totalDisplay.textContent = formatCurrency(total);
|
|
}
|
|
}
|
|
|
|
function formatCurrency(amount) {
|
|
return new Intl.NumberFormat('id-ID', {
|
|
style: 'currency',
|
|
currency: 'IDR',
|
|
minimumFractionDigits: 2
|
|
}).format(amount);
|
|
}
|
|
|
|
function removeItem(button) {
|
|
// Show confirmation dialog
|
|
const confirmed = confirm('Are you sure you want to delete this item? This action cannot be undone.');
|
|
|
|
if (confirmed) {
|
|
const itemForm = button.closest('.item-form');
|
|
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');
|
|
});
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %} |