feat: add views and templates for stock batch creation on desktop and mobile
This commit is contained in:
parent
8b13236a26
commit
ca4d1b3c1b
3 changed files with 51 additions and 11 deletions
|
|
@ -38,6 +38,16 @@
|
||||||
{% endblock extra_head %}
|
{% endblock extra_head %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{% if messages %}
|
||||||
|
<div class="mb-3">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{% if message.tags == 'error' %}danger{% elif message.tags %}{{ message.tags }}{% else %}info{% endif %} alert-dismissible fade show small" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<form method="post" enctype="multipart/form-data" id="batch-form">
|
<form method="post" enctype="multipart/form-data" id="batch-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
|
|
@ -368,17 +378,20 @@ function initProductAutocomplete(lineEl) {
|
||||||
? `<span class="badge bg-success ms-1">${p.total_stock}</span>`
|
? `<span class="badge bg-success ms-1">${p.total_stock}</span>`
|
||||||
: `<span class="badge bg-secondary ms-1">0</span>`;
|
: `<span class="badge bg-secondary ms-1">0</span>`;
|
||||||
item.innerHTML = `<strong>${escHtml(p.text)}</strong> <small class="text-muted">${escHtml(p.code || '')}</small>${stockBadge}`;
|
item.innerHTML = `<strong>${escHtml(p.text)}</strong> <small class="text-muted">${escHtml(p.code || '')}</small>${stockBadge}`;
|
||||||
item.addEventListener('mousedown', e => {
|
const selectProduct = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
hiddenInput.value = p.id;
|
hiddenInput.value = p.id;
|
||||||
searchInput.value = p.text + (p.code ? ' (' + p.code + ')' : '');
|
searchInput.value = p.text + (p.code ? ' (' + p.code + ')' : '');
|
||||||
|
searchInput.classList.remove('is-invalid');
|
||||||
resultsBox.style.display = 'none';
|
resultsBox.style.display = 'none';
|
||||||
if (p.is_serialized) {
|
if (p.is_serialized) {
|
||||||
fetchSerializedItems(lineEl, p.id);
|
fetchSerializedItems(lineEl, p.id);
|
||||||
} else {
|
} else {
|
||||||
resetSerializedSection(lineEl);
|
resetSerializedSection(lineEl);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
item.addEventListener('pointerdown', selectProduct);
|
||||||
|
item.addEventListener('mousedown', selectProduct);
|
||||||
resultsBox.appendChild(item);
|
resultsBox.appendChild(item);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -389,7 +402,7 @@ function initProductAutocomplete(lineEl) {
|
||||||
});
|
});
|
||||||
|
|
||||||
searchInput.addEventListener('blur', () => {
|
searchInput.addEventListener('blur', () => {
|
||||||
setTimeout(() => { resultsBox.style.display = 'none'; }, 150);
|
setTimeout(() => { resultsBox.style.display = 'none'; }, 250);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -694,7 +707,8 @@ function onCompanyInputChange() {
|
||||||
// ── Filtrage des personnes par entreprise ────────────────────
|
// ── Filtrage des personnes par entreprise ────────────────────
|
||||||
function filterPersonsByCompany(companyId) {
|
function filterPersonsByCompany(companyId) {
|
||||||
const personSelect = document.getElementById('person_id');
|
const personSelect = document.getElementById('person_id');
|
||||||
const currentVal = personSelect.value;
|
if (!personSelect) return;
|
||||||
|
const currentOpt = personSelect.selectedIndex >= 0 ? personSelect.options[personSelect.selectedIndex] : null;
|
||||||
Array.from(personSelect.options).forEach(opt => {
|
Array.from(personSelect.options).forEach(opt => {
|
||||||
if (!opt.value) return; // keep the blank option always
|
if (!opt.value) return; // keep the blank option always
|
||||||
const allowedCompanies = (opt.dataset.companies || '').split(',');
|
const allowedCompanies = (opt.dataset.companies || '').split(',');
|
||||||
|
|
@ -751,13 +765,15 @@ if (bpSearchInput) {
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = 'list-group-item list-group-item-action py-2 small';
|
item.className = 'list-group-item list-group-item-action py-2 small';
|
||||||
item.innerHTML = `<strong>${escHtml(bp.text)}</strong> <span class="badge bg-secondary ms-1">${escHtml(bp.status)}</span>`;
|
item.innerHTML = `<strong>${escHtml(bp.text)}</strong> <span class="badge bg-secondary ms-1">${escHtml(bp.status)}</span>`;
|
||||||
item.addEventListener('mousedown', e => {
|
const selectBp = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
bpHiddenId.value = bp.id;
|
bpHiddenId.value = bp.id;
|
||||||
bpSearchInput.value = bp.text;
|
bpSearchInput.value = bp.text;
|
||||||
bpResults.style.display = 'none';
|
bpResults.style.display = 'none';
|
||||||
if (bpLoadBtn) bpLoadBtn.disabled = false;
|
if (bpLoadBtn) bpLoadBtn.disabled = false;
|
||||||
});
|
};
|
||||||
|
item.addEventListener('pointerdown', selectBp);
|
||||||
|
item.addEventListener('mousedown', selectBp);
|
||||||
bpResults.appendChild(item);
|
bpResults.appendChild(item);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -767,7 +783,7 @@ if (bpSearchInput) {
|
||||||
}, 280);
|
}, 280);
|
||||||
});
|
});
|
||||||
bpSearchInput.addEventListener('blur', () => {
|
bpSearchInput.addEventListener('blur', () => {
|
||||||
setTimeout(() => { bpResults.style.display = 'none'; }, 150);
|
setTimeout(() => { bpResults.style.display = 'none'; }, 250);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,17 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if messages %}
|
||||||
|
<div class="mb-3">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{% if message.tags == 'error' %}danger{% elif message.tags %}{{ message.tags }}{% else %}info{% endif %} alert-dismissible fade show small" role="alert">
|
||||||
|
{{ message }}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<form method="post" enctype="multipart/form-data" id="batch-form">
|
<form method="post" enctype="multipart/form-data" id="batch-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -947,7 +947,13 @@ def _new_stock_batch_view(request, movement_type,
|
||||||
return redirect(cancel_url_name)
|
return redirect(cancel_url_name)
|
||||||
|
|
||||||
products = Product.objects.select_related("category").order_by("name")
|
products = Product.objects.select_related("category").order_by("name")
|
||||||
locations = list(WarehouseLocation.objects.select_related("warehouse").order_by("warehouse__name", "name"))
|
locations = list(
|
||||||
|
filter_locations_by_permission(
|
||||||
|
WarehouseLocation.objects.select_related("warehouse"),
|
||||||
|
request.user,
|
||||||
|
can_manage=True
|
||||||
|
).order_by("warehouse__name", "name")
|
||||||
|
)
|
||||||
from stock.permissions import user_can_administer_warehouse
|
from stock.permissions import user_can_administer_warehouse
|
||||||
for loc in locations:
|
for loc in locations:
|
||||||
loc.can_administer = user_can_administer_warehouse(request.user, loc.warehouse)
|
loc.can_administer = user_can_administer_warehouse(request.user, loc.warehouse)
|
||||||
|
|
@ -1102,7 +1108,13 @@ def _new_stock_batch_view(request, movement_type,
|
||||||
errors.append(_("Ligne %(n)s : produit invalide.") % {'n': i + 1})
|
errors.append(_("Ligne %(n)s : produit invalide.") % {'n': i + 1})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
lines.append({'product_id': pid, 'qty': qty, 'src': src, 'dst': dst})
|
lines.append({
|
||||||
|
'product_id': pid,
|
||||||
|
'qty': qty,
|
||||||
|
'src': src,
|
||||||
|
'dst': dst,
|
||||||
|
'line_index': i,
|
||||||
|
})
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
for err in errors:
|
for err in errors:
|
||||||
|
|
@ -1153,9 +1165,10 @@ def _new_stock_batch_view(request, movement_type,
|
||||||
reason=reason,
|
reason=reason,
|
||||||
batch=batch,
|
batch=batch,
|
||||||
)
|
)
|
||||||
|
line_idx = line.get('line_index', 0)
|
||||||
# Articles tracés (numéros de série)
|
# Articles tracés (numéros de série)
|
||||||
if product.is_serialized:
|
if product.is_serialized:
|
||||||
ids_str = serialized_ids_list[i] if i < len(serialized_ids_list) else ''
|
ids_str = serialized_ids_list[line_idx] if line_idx < len(serialized_ids_list) else ''
|
||||||
s_ids = [int(x) for x in ids_str.split(',') if x.strip().isdigit()]
|
s_ids = [int(x) for x in ids_str.split(',') if x.strip().isdigit()]
|
||||||
s_items = list(SerializedItem.objects.filter(pk__in=s_ids, product=product)) if s_ids else []
|
s_items = list(SerializedItem.objects.filter(pk__in=s_ids, product=product)) if s_ids else []
|
||||||
if s_items:
|
if s_items:
|
||||||
|
|
@ -1165,7 +1178,7 @@ def _new_stock_batch_view(request, movement_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Nouveaux articles tracés (entrée uniquement)
|
# Nouveaux articles tracés (entrée uniquement)
|
||||||
new_serials_str = serialized_new_serials_list[i] if i < len(serialized_new_serials_list) else ''
|
new_serials_str = serialized_new_serials_list[line_idx] if line_idx < len(serialized_new_serials_list) else ''
|
||||||
new_items = []
|
new_items = []
|
||||||
if movement_type == 'in' and new_serials_str.strip():
|
if movement_type == 'in' and new_serials_str.strip():
|
||||||
for sn in [s.strip() for s in new_serials_str.split(',') if s.strip()]:
|
for sn in [s.strip() for s in new_serials_str.split(',') if s.strip()]:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue