chore: update localization files and templates for stock and intervention modules
This commit is contained in:
parent
fd10b3f8f7
commit
261ce2dd14
25 changed files with 17939 additions and 16231 deletions
Binary file not shown.
|
|
@ -298,10 +298,8 @@ msgid "Autre"
|
|||
msgstr "Other"
|
||||
|
||||
#: contracts/models.py:225
|
||||
#, fuzzy
|
||||
#| msgid "Date d'entrée"
|
||||
msgid "Date d'absence"
|
||||
msgstr "Creation date"
|
||||
msgstr "Absence date"
|
||||
|
||||
#: contracts/models.py:231 contracts/templates/contracts/teams_index.html:888
|
||||
msgid "Motif d'absence"
|
||||
|
|
@ -314,22 +312,16 @@ msgid "Commentaire"
|
|||
msgstr "Comment"
|
||||
|
||||
#: contracts/models.py:240
|
||||
#, fuzzy
|
||||
#| msgid "Enregistrer"
|
||||
msgid "Enregistré par"
|
||||
msgstr "Save"
|
||||
msgstr "Recorded by"
|
||||
|
||||
#: contracts/models.py:242
|
||||
#, fuzzy
|
||||
#| msgid "Créé par"
|
||||
msgid "Créé le"
|
||||
msgstr "Created by"
|
||||
msgstr "Created on"
|
||||
|
||||
#: contracts/models.py:248
|
||||
#, fuzzy
|
||||
#| msgid "Aucun membre"
|
||||
msgid "Absence d'un membre"
|
||||
msgstr "No member"
|
||||
msgstr "Member absence"
|
||||
|
||||
#: contracts/models.py:249
|
||||
msgid "Absences des membres"
|
||||
|
|
@ -546,10 +538,8 @@ msgid "Raison du blocage temporaire du contrat."
|
|||
msgstr ""
|
||||
|
||||
#: contracts/models.py:407
|
||||
#, fuzzy
|
||||
#| msgid "Date de commande"
|
||||
msgid "Date de blocage"
|
||||
msgstr "Order date"
|
||||
msgstr "Blocking date"
|
||||
|
||||
#: contracts/models.py:408
|
||||
msgid "Date et heure auxquelles le contrat a été marqué comme bloqué."
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -307,10 +307,8 @@ msgid "Autre"
|
|||
msgstr "Autre"
|
||||
|
||||
#: contracts/models.py:225
|
||||
#, fuzzy
|
||||
#| msgid "Date d'entrée"
|
||||
msgid "Date d'absence"
|
||||
msgstr "Date d'entrée"
|
||||
msgstr "Date d'absence"
|
||||
|
||||
#: contracts/models.py:231 contracts/templates/contracts/teams_index.html:888
|
||||
msgid "Motif d'absence"
|
||||
|
|
@ -323,22 +321,16 @@ msgid "Commentaire"
|
|||
msgstr "Commentaire"
|
||||
|
||||
#: contracts/models.py:240
|
||||
#, fuzzy
|
||||
#| msgid "Enregistrer"
|
||||
msgid "Enregistré par"
|
||||
msgstr "Enregistrer"
|
||||
msgstr "Enregistré par"
|
||||
|
||||
#: contracts/models.py:242
|
||||
#, fuzzy
|
||||
#| msgid "Créé par"
|
||||
msgid "Créé le"
|
||||
msgstr "Créé par"
|
||||
msgstr "Créé le"
|
||||
|
||||
#: contracts/models.py:248
|
||||
#, fuzzy
|
||||
#| msgid "Aucun membre"
|
||||
msgid "Absence d'un membre"
|
||||
msgstr "Aucun membre"
|
||||
msgstr "Absence d'un membre"
|
||||
|
||||
#: contracts/models.py:249
|
||||
msgid "Absences des membres"
|
||||
|
|
@ -578,10 +570,8 @@ msgid "Raison du blocage temporaire du contrat."
|
|||
msgstr ""
|
||||
|
||||
#: contracts/models.py:407
|
||||
#, fuzzy
|
||||
#| msgid "Date de commande"
|
||||
msgid "Date de blocage"
|
||||
msgstr "Date de commande"
|
||||
msgstr "Date de blocage"
|
||||
|
||||
#: contracts/models.py:408
|
||||
msgid "Date et heure auxquelles le contrat a été marqué comme bloqué."
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -299,10 +299,8 @@ msgid "Autre"
|
|||
msgstr "Andere"
|
||||
|
||||
#: contracts/models.py:225
|
||||
#, fuzzy
|
||||
#| msgid "Date d'entrée"
|
||||
msgid "Date d'absence"
|
||||
msgstr "Aanmaakdatum"
|
||||
msgstr "Afwezigheidsdatum"
|
||||
|
||||
#: contracts/models.py:231 contracts/templates/contracts/teams_index.html:888
|
||||
msgid "Motif d'absence"
|
||||
|
|
@ -315,22 +313,16 @@ msgid "Commentaire"
|
|||
msgstr "Opmerking"
|
||||
|
||||
#: contracts/models.py:240
|
||||
#, fuzzy
|
||||
#| msgid "Enregistrer"
|
||||
msgid "Enregistré par"
|
||||
msgstr "Opslaan"
|
||||
msgstr "Geregistreerd door"
|
||||
|
||||
#: contracts/models.py:242
|
||||
#, fuzzy
|
||||
#| msgid "Créé par"
|
||||
msgid "Créé le"
|
||||
msgstr "Aangemaakt door"
|
||||
msgstr "Aangemaakt op"
|
||||
|
||||
#: contracts/models.py:248
|
||||
#, fuzzy
|
||||
#| msgid "Aucun membre"
|
||||
msgid "Absence d'un membre"
|
||||
msgstr "Geen lid"
|
||||
msgstr "Afwezigheid van een lid"
|
||||
|
||||
#: contracts/models.py:249
|
||||
msgid "Absences des membres"
|
||||
|
|
@ -547,10 +539,8 @@ msgid "Raison du blocage temporaire du contrat."
|
|||
msgstr ""
|
||||
|
||||
#: contracts/models.py:407
|
||||
#, fuzzy
|
||||
#| msgid "Date de commande"
|
||||
msgid "Date de blocage"
|
||||
msgstr "Besteldatum"
|
||||
msgstr "Blokkeringsdatum"
|
||||
|
||||
#: contracts/models.py:408
|
||||
msgid "Date et heure auxquelles le contrat a été marqué comme bloqué."
|
||||
|
|
|
|||
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
|
@ -7,20 +7,6 @@
|
|||
<span>{% translate "Matériel / Stock" %}</span>
|
||||
</h4>
|
||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||
{% if material_stats.has_material %}
|
||||
<span class="badge bg-light text-dark border py-2 px-2" id="desktop-material-stats-badge" title="{% translate 'Matériel utilisé sur cette intervention' %}">
|
||||
<i class="bi bi-tools text-primary me-1"></i>
|
||||
<span id="desktop-material-used-count">{{ material_stats.used }}</span> / <span id="desktop-material-total-count">{{ material_stats.total }}</span> {% translate "utilisé(s)" %}
|
||||
</span>
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-success d-flex align-items-center gap-1"
|
||||
id="btn-desktop-toggle-all-material"
|
||||
onclick="toggleAllMaterialUsed('{{ intervention.id }}', null)"
|
||||
{% if not can_edit_intervention %}disabled title="{% translate 'Permissions insuffisantes' %}"{% else %}title="{% translate 'Tout cocher ou tout décocher pour cette intervention' %}"{% endif %}>
|
||||
<i class="bi bi-check-all fs-6"></i>
|
||||
<span id="btn-desktop-toggle-all-text">{% if material_stats.all_used %}{% translate "Tout décocher" %}{% else %}{% translate "Tout cocher" %}{% endif %}</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if can_create_preparation_order %}
|
||||
<a href="{% url 'stock:new_preparation_order_from_intervention' intervention.id %}"
|
||||
class="btn btn-sm btn-primary d-flex align-items-center gap-1"
|
||||
|
|
|
|||
|
|
@ -1112,23 +1112,6 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if material_stats.has_material %}
|
||||
<div class="bg-light px-3 py-2 border-bottom d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<span class="badge bg-white text-dark border py-1 px-2" id="mobile-material-stats-badge">
|
||||
<i class="bi bi-tools text-primary me-1"></i>
|
||||
<span id="mobile-material-used-count">{{ material_stats.used }}</span> / <span id="mobile-material-total-count">{{ material_stats.total }}</span> {% translate "utilisé(s)" %}
|
||||
</span>
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-outline-success d-flex align-items-center gap-1 py-1 px-2"
|
||||
id="btn-mobile-toggle-all-material"
|
||||
onclick="toggleAllMaterialUsed('{{ intervention.id }}', null)"
|
||||
{% if not can_edit_intervention %}disabled{% endif %}
|
||||
style="font-size:0.75rem;">
|
||||
<i class="bi bi-check-all fs-6"></i>
|
||||
<span id="btn-mobile-toggle-all-text">{% if material_stats.all_used %}{% translate "Tout décocher" %}{% else %}{% translate "Tout cocher" %}{% endif %}</span>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card-body p-2">
|
||||
{% if preparation_orders %}
|
||||
{% for order in preparation_orders %}
|
||||
|
|
|
|||
|
|
@ -308,3 +308,38 @@ class MaterialUsedTests(TestCase):
|
|||
self.assertIn('material_stats', response_mobile.context)
|
||||
self.assertContains(response_mobile, 'id="section-stock"')
|
||||
self.assertContains(response_mobile, 'material-used-checkbox')
|
||||
|
||||
def test_material_translations(self):
|
||||
"""Vérifie la traduction des termes liés au matériel utilisé en FR, NL et EN."""
|
||||
from django.utils.translation import activate, gettext as _
|
||||
|
||||
# Français
|
||||
activate('fr')
|
||||
self.assertEqual(_("Matériel / Stock"), "Matériel / Stock")
|
||||
self.assertEqual(_("Utilisé"), "Utilisé")
|
||||
self.assertEqual(_("Non utilisé"), "Non utilisé")
|
||||
self.assertEqual(_("Tout cocher"), "Tout cocher")
|
||||
self.assertEqual(_("Tout décocher"), "Tout décocher")
|
||||
self.assertEqual(_("utilisé(s)"), "utilisé(s)")
|
||||
self.assertEqual(_("Hors catalogue"), "Hors catalogue")
|
||||
|
||||
# Néerlandais
|
||||
activate('nl')
|
||||
self.assertEqual(_("Matériel / Stock"), "Materiaal / Voorraad")
|
||||
self.assertEqual(_("Utilisé"), "Gebruikt")
|
||||
self.assertEqual(_("Non utilisé"), "Niet gebruikt")
|
||||
self.assertIn(_("Tout cocher"), ["Alles selecteren", "Alles aanvinken"])
|
||||
self.assertIn(_("Tout décocher"), ["Alles deselecteren", "Alles uitvinken"])
|
||||
self.assertEqual(_("utilisé(s)"), "gebruikt")
|
||||
self.assertEqual(_("Hors catalogue"), "Buiten catalogus")
|
||||
|
||||
# Anglais
|
||||
activate('en')
|
||||
self.assertEqual(_("Matériel / Stock"), "Material / Stock")
|
||||
self.assertEqual(_("Utilisé"), "Used")
|
||||
self.assertEqual(_("Non utilisé"), "Not used")
|
||||
self.assertEqual(_("Tout cocher"), "Check all")
|
||||
self.assertEqual(_("Tout décocher"), "Uncheck all")
|
||||
self.assertEqual(_("utilisé(s)"), "used")
|
||||
self.assertEqual(_("Hors catalogue"), "Out of catalog")
|
||||
|
||||
|
|
|
|||
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
|
@ -785,9 +785,7 @@ if (bpLoadBtn) {
|
|||
bpInfo.style.display = '';
|
||||
return;
|
||||
}
|
||||
bpInfo.innerHTML = `{% translate "Chargement de" %} <strong>${data.items.length}</strong> {% translate "produit(s) depuis le bon" %} <strong>${escHtml(data.order_number)}</strong>`;
|
||||
bpInfo.className = 'alert alert-info mt-2 mb-0 py-2 small';
|
||||
bpInfo.style.display = '';
|
||||
|
||||
// Pré-remplir l'emplacement de destination si unique
|
||||
if (data.default_location_id) {
|
||||
const globalDestSelect = document.getElementById('global_dest_location_id');
|
||||
|
|
@ -796,11 +794,17 @@ if (bpLoadBtn) {
|
|||
globalDestSelect.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
|
||||
// Séparer les articles à réintégrer (non utilisés, qty > 0) et les articles marqués comme utilisés
|
||||
const itemsToReturn = data.items.filter(item => item.quantity > 0);
|
||||
const usedItems = data.items.filter(item => data.has_intervention && item.is_used);
|
||||
|
||||
// Effacer les lignes existantes
|
||||
document.getElementById('lines-container').innerHTML = '';
|
||||
updateNoLinesPlaceholder();
|
||||
// Ajouter une ligne par produit
|
||||
data.items.forEach(item => {
|
||||
|
||||
// Ajouter une ligne par produit à réintégrer
|
||||
itemsToReturn.forEach(item => {
|
||||
addLine();
|
||||
const lineEls = document.querySelectorAll('#lines-container .product-line');
|
||||
const lineEl = lineEls[lineEls.length - 1];
|
||||
|
|
@ -811,6 +815,59 @@ if (bpLoadBtn) {
|
|||
if (hiddenInput) hiddenInput.value = item.product_id;
|
||||
if (qtyInput) qtyInput.value = item.quantity;
|
||||
});
|
||||
|
||||
// Construction du message d'information bpInfo
|
||||
let infoHtml = '';
|
||||
if (data.has_intervention && usedItems.length > 0 && itemsToReturn.length > 0) {
|
||||
bpInfo.className = 'alert alert-info mt-2 mb-0 py-2 small';
|
||||
infoHtml = `<div>{% translate "Préremplissage de" %} <strong>${itemsToReturn.length}</strong> {% translate "produit(s) non utilisé(s) à réintégrer en stock depuis le bon" %} <strong>${escHtml(data.order_number)}</strong> (<strong>${usedItems.length}</strong> {% translate "marqué(s) comme utilisé(s) sur l'intervention" %}).</div>`;
|
||||
} else if (data.has_intervention && itemsToReturn.length === 0 && usedItems.length > 0) {
|
||||
bpInfo.className = 'alert alert-warning mt-2 mb-0 py-2 small';
|
||||
infoHtml = `<div><i class="bi bi-info-circle me-1"></i>{% translate "Tous les articles de ce bon ont été marqués comme utilisés lors de l'intervention" %} (<strong>${escHtml(data.order_number)}</strong>). {% translate "Aucun article à réintégrer automatiquement." %}</div>`;
|
||||
} else {
|
||||
bpInfo.className = 'alert alert-info mt-2 mb-0 py-2 small';
|
||||
infoHtml = `<div>{% translate "Chargement de" %} <strong>${itemsToReturn.length}</strong> {% translate "produit(s) depuis le bon" %} <strong>${escHtml(data.order_number)}</strong>.</div>`;
|
||||
}
|
||||
|
||||
if (usedItems.length > 0) {
|
||||
infoHtml += `<div class="mt-2 pt-2 border-top border-secondary-subtle">
|
||||
<span class="text-muted d-block mb-1">{% translate "Matériel marqué comme utilisé sur l'intervention (cliquer pour ajouter au retour si surplus) :" %}</span>
|
||||
<div class="d-flex flex-wrap gap-1">`;
|
||||
usedItems.forEach(item => {
|
||||
infoHtml += `<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-2 btn-add-used-item"
|
||||
data-id="${item.product_id}"
|
||||
data-name="${escHtml(item.product_name)}"
|
||||
data-code="${escHtml(item.product_code || '')}"
|
||||
data-qty="${item.initial_quantity || 1}">
|
||||
<i class="bi bi-plus-circle me-1"></i>${escHtml(item.product_name)} (${item.initial_quantity})
|
||||
</button>`;
|
||||
});
|
||||
infoHtml += `</div></div>`;
|
||||
}
|
||||
|
||||
bpInfo.innerHTML = infoHtml;
|
||||
bpInfo.style.display = '';
|
||||
|
||||
// Gestionnaire d'ajout rapide pour le matériel utilisé
|
||||
bpInfo.querySelectorAll('.btn-add-used-item').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const prodId = this.dataset.id;
|
||||
const prodName = this.dataset.name;
|
||||
const prodCode = this.dataset.code;
|
||||
const prodQty = this.dataset.qty || 1;
|
||||
addLine();
|
||||
const lineEls = document.querySelectorAll('#lines-container .product-line');
|
||||
const lineEl = lineEls[lineEls.length - 1];
|
||||
const searchInput = lineEl.querySelector('.product-search-input');
|
||||
const hiddenInput = lineEl.querySelector('.product-id-input');
|
||||
const qtyInput = lineEl.querySelector('input[name="quantity"]');
|
||||
if (searchInput) searchInput.value = prodName + (prodCode ? ' (' + prodCode + ')' : '');
|
||||
if (hiddenInput) hiddenInput.value = prodId;
|
||||
if (qtyInput) qtyInput.value = prodQty;
|
||||
this.disabled = true;
|
||||
this.classList.add('opacity-50');
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
bpInfo.textContent = '{% translate "Erreur lors du chargement du bon de préparation." %}';
|
||||
|
|
|
|||
|
|
@ -979,9 +979,7 @@ if (bpLoadBtn) {
|
|||
bpInfo.style.display = '';
|
||||
return;
|
||||
}
|
||||
bpInfo.innerHTML = `{% translate "Chargement de" %} <strong>${data.items.length}</strong> {% translate "produit(s) depuis le bon" %} <strong>${escHtml(data.order_number)}</strong>`;
|
||||
bpInfo.className = 'alert alert-info mt-2 mb-0 py-2 small';
|
||||
bpInfo.style.display = '';
|
||||
|
||||
// Pré-remplir l'emplacement de destination si unique
|
||||
if (data.default_location_id) {
|
||||
const globalDestSelect = document.getElementById('global_dest_location_id');
|
||||
|
|
@ -990,11 +988,17 @@ if (bpLoadBtn) {
|
|||
globalDestSelect.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
|
||||
// Séparer les articles à réintégrer (non utilisés, qty > 0) et les articles marqués comme utilisés
|
||||
const itemsToReturn = data.items.filter(item => item.quantity > 0);
|
||||
const usedItems = data.items.filter(item => data.has_intervention && item.is_used);
|
||||
|
||||
// Effacer les lignes existantes
|
||||
document.getElementById('lines-container').innerHTML = '';
|
||||
updateNoLinesPlaceholder();
|
||||
// Ajouter une ligne par produit
|
||||
data.items.forEach(item => {
|
||||
|
||||
// Ajouter une ligne par produit à réintégrer
|
||||
itemsToReturn.forEach(item => {
|
||||
addLine();
|
||||
const lineEls = document.querySelectorAll('#lines-container .product-line');
|
||||
const lineEl = lineEls[lineEls.length - 1];
|
||||
|
|
@ -1007,6 +1011,62 @@ if (bpLoadBtn) {
|
|||
// Charger les emplacements pour ce produit
|
||||
fetchLocationsForProduct(lineEl, item.product_id);
|
||||
});
|
||||
|
||||
// Construction du message d'information bpInfo
|
||||
let infoHtml = '';
|
||||
if (data.has_intervention && usedItems.length > 0 && itemsToReturn.length > 0) {
|
||||
bpInfo.className = 'alert alert-info mt-2 mb-0 py-2 small';
|
||||
infoHtml = `<div>{% translate "Préremplissage de" %} <strong>${itemsToReturn.length}</strong> {% translate "produit(s) non utilisé(s) à réintégrer en stock depuis le bon" %} <strong>${escHtml(data.order_number)}</strong> (<strong>${usedItems.length}</strong> {% translate "marqué(s) comme utilisé(s) sur l'intervention" %}).</div>`;
|
||||
} else if (data.has_intervention && itemsToReturn.length === 0 && usedItems.length > 0) {
|
||||
bpInfo.className = 'alert alert-warning mt-2 mb-0 py-2 small';
|
||||
infoHtml = `<div><i class="bi bi-info-circle me-1"></i>{% translate "Tous les articles de ce bon ont été marqués comme utilisés lors de l'intervention" %} (<strong>${escHtml(data.order_number)}</strong>). {% translate "Aucun article à réintégrer automatiquement." %}</div>`;
|
||||
} else {
|
||||
bpInfo.className = 'alert alert-info mt-2 mb-0 py-2 small';
|
||||
infoHtml = `<div>{% translate "Chargement de" %} <strong>${itemsToReturn.length}</strong> {% translate "produit(s) depuis le bon" %} <strong>${escHtml(data.order_number)}</strong>.</div>`;
|
||||
}
|
||||
|
||||
if (usedItems.length > 0) {
|
||||
infoHtml += `<div class="mt-2 pt-2 border-top border-secondary-subtle">
|
||||
<span class="text-muted d-block mb-1">{% translate "Matériel marqué comme utilisé sur l'intervention (cliquer pour ajouter au retour si surplus) :" %}</span>
|
||||
<div class="d-flex flex-wrap gap-1">`;
|
||||
usedItems.forEach(item => {
|
||||
infoHtml += `<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-2 btn-add-used-item"
|
||||
data-id="${item.product_id}"
|
||||
data-name="${escHtml(item.product_name)}"
|
||||
data-code="${escHtml(item.product_code || '')}"
|
||||
data-qty="${item.initial_quantity || 1}">
|
||||
<i class="bi bi-plus-circle me-1"></i>${escHtml(item.product_name)} (${item.initial_quantity})
|
||||
</button>`;
|
||||
});
|
||||
infoHtml += `</div></div>`;
|
||||
}
|
||||
|
||||
bpInfo.innerHTML = infoHtml;
|
||||
bpInfo.style.display = '';
|
||||
|
||||
// Gestionnaire d'ajout rapide pour le matériel utilisé
|
||||
bpInfo.querySelectorAll('.btn-add-used-item').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const prodId = this.dataset.id;
|
||||
const prodName = this.dataset.name;
|
||||
const prodCode = this.dataset.code;
|
||||
const prodQty = this.dataset.qty || 1;
|
||||
addLine();
|
||||
const lineEls = document.querySelectorAll('#lines-container .product-line');
|
||||
const lineEl = lineEls[lineEls.length - 1];
|
||||
const searchInput = lineEl.querySelector('.product-search-input');
|
||||
const hiddenInput = lineEl.querySelector('.product-id-input');
|
||||
const qtyInput = lineEl.querySelector('input[name="quantity"]');
|
||||
if (searchInput) searchInput.value = prodName + (prodCode ? ' (' + prodCode + ')' : '');
|
||||
if (hiddenInput) hiddenInput.value = prodId;
|
||||
if (qtyInput) qtyInput.value = prodQty;
|
||||
if (typeof fetchLocationsForProduct === 'function') {
|
||||
fetchLocationsForProduct(lineEl, prodId);
|
||||
}
|
||||
this.disabled = true;
|
||||
this.classList.add('opacity-50');
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
bpInfo.textContent = '{% translate "Erreur lors du chargement du bon de préparation." %}';
|
||||
|
|
|
|||
|
|
@ -1769,6 +1769,98 @@ class PreparationOrderAutocompleteTests(TestCase):
|
|||
data = response.json()
|
||||
self.assertEqual(data["default_location_id"], loc.id)
|
||||
|
||||
def test_return_api_with_used_and_unused_items(self):
|
||||
from stock.models import Product, PreparationOrder, PreparationOrderItem
|
||||
prod1 = Product.objects.create(name="Câble RJ45", code="CAB-RJ45", sku="SKU-CAB-RJ45", total_stock=20)
|
||||
prod2 = Product.objects.create(name="Routeur WiFi", code="ROUT-WIFI", sku="SKU-ROUT-WIFI", total_stock=10)
|
||||
|
||||
po = PreparationOrder.objects.create(
|
||||
order_number="BP-RET-USED-01",
|
||||
intervention=self.intervention,
|
||||
status="processed",
|
||||
)
|
||||
# item1 utilisé lors de l'intervention -> return_qty doit être 0
|
||||
PreparationOrderItem.objects.create(
|
||||
preparation_order=po,
|
||||
product=prod1,
|
||||
quantity=5,
|
||||
is_used=True,
|
||||
)
|
||||
# item2 NON utilisé lors de l'intervention -> return_qty doit être 2
|
||||
PreparationOrderItem.objects.create(
|
||||
preparation_order=po,
|
||||
product=prod2,
|
||||
quantity=2,
|
||||
is_used=False,
|
||||
)
|
||||
|
||||
url = reverse("stock:preparation_order_items_api", args=[po.pk])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
|
||||
self.assertTrue(data["has_intervention"])
|
||||
self.assertEqual(len(data["items"]), 2)
|
||||
|
||||
item1_data = next(it for it in data["items"] if it["product_id"] == prod1.pk)
|
||||
self.assertEqual(item1_data["quantity"], 0)
|
||||
self.assertEqual(item1_data["initial_quantity"], 5)
|
||||
self.assertTrue(item1_data["is_used"])
|
||||
|
||||
item2_data = next(it for it in data["items"] if it["product_id"] == prod2.pk)
|
||||
self.assertEqual(item2_data["quantity"], 2)
|
||||
self.assertEqual(item2_data["initial_quantity"], 2)
|
||||
self.assertFalse(item2_data["is_used"])
|
||||
|
||||
def test_return_api_with_all_items_used(self):
|
||||
from stock.models import Product, PreparationOrder, PreparationOrderItem
|
||||
prod = Product.objects.create(name="Antenne 5G", code="ANT-5G", sku="SKU-ANT-5G", total_stock=5)
|
||||
po = PreparationOrder.objects.create(
|
||||
order_number="BP-RET-ALL-USED",
|
||||
intervention=self.intervention,
|
||||
status="processed",
|
||||
)
|
||||
PreparationOrderItem.objects.create(
|
||||
preparation_order=po,
|
||||
product=prod,
|
||||
quantity=3,
|
||||
is_used=True,
|
||||
)
|
||||
|
||||
url = reverse("stock:preparation_order_items_api", args=[po.pk])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
|
||||
self.assertTrue(data["has_intervention"])
|
||||
self.assertEqual(len(data["items"]), 1)
|
||||
self.assertEqual(data["items"][0]["quantity"], 0)
|
||||
self.assertTrue(data["items"][0]["is_used"])
|
||||
|
||||
def test_return_api_without_intervention(self):
|
||||
from stock.models import Product, PreparationOrder, PreparationOrderItem
|
||||
prod = Product.objects.create(name="Outil Simple", code="TOOL-1", sku="SKU-TOOL-1", total_stock=5)
|
||||
po = PreparationOrder.objects.create(
|
||||
order_number="BP-RET-NO-INT",
|
||||
intervention=None,
|
||||
status="processed",
|
||||
)
|
||||
PreparationOrderItem.objects.create(
|
||||
preparation_order=po,
|
||||
product=prod,
|
||||
quantity=4,
|
||||
is_used=True, # même si is_used est True, sans intervention le bon préremplit la quantité normale
|
||||
)
|
||||
|
||||
url = reverse("stock:preparation_order_items_api", args=[po.pk])
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
|
||||
self.assertFalse(data["has_intervention"])
|
||||
self.assertEqual(len(data["items"]), 1)
|
||||
self.assertEqual(data["items"][0]["quantity"], 4)
|
||||
|
||||
|
||||
class PreparationOrderDiscussionTests(TestCase):
|
||||
def setUp(self):
|
||||
|
|
|
|||
|
|
@ -418,21 +418,46 @@ def preparation_order_items_api(request, pk):
|
|||
"""
|
||||
API endpoint retournant les articles d'un bon de préparation.
|
||||
Utilisé pour pré-remplir les lignes produit lors d'un retour de stock.
|
||||
Si le bon est lié à une intervention, les quantités à retourner sont calculées
|
||||
sur base du matériel utilisé : le matériel marqué comme utilisé (is_used=True)
|
||||
n'est pas réintégré (quantité 0), tandis que le matériel non utilisé (is_used=False)
|
||||
est proposé au retour avec sa quantité restante.
|
||||
"""
|
||||
po = get_object_or_404(PreparationOrder, pk=pk)
|
||||
has_intervention = bool(po.intervention_id)
|
||||
|
||||
executed_summary = po.get_executed_output_summary()
|
||||
has_executed = any(s['out'] > 0 for s in executed_summary.values())
|
||||
|
||||
items = []
|
||||
for item in po.preparationorderitem_set.select_related('product').all():
|
||||
p_id = item.product_id
|
||||
if has_executed:
|
||||
prod_summary = executed_summary.get(p_id, {'out': 0, 'in': 0, 'net': 0})
|
||||
base_qty = max(0, prod_summary['net'])
|
||||
else:
|
||||
base_qty = item.quantity
|
||||
|
||||
is_used = bool(item.is_used)
|
||||
if has_intervention and is_used:
|
||||
return_qty = 0
|
||||
else:
|
||||
return_qty = base_qty
|
||||
|
||||
items.append({
|
||||
'product_id': item.product.pk,
|
||||
'product_name': item.product.name,
|
||||
'product_code': item.product.code or '',
|
||||
'quantity': item.quantity,
|
||||
'quantity': return_qty,
|
||||
'initial_quantity': base_qty,
|
||||
'is_used': is_used,
|
||||
'unit': item.product.unit or '',
|
||||
})
|
||||
default_loc_id = po.get_default_return_location_id()
|
||||
return JsonResponse({
|
||||
'items': items,
|
||||
'order_number': po.order_number,
|
||||
'has_intervention': has_intervention,
|
||||
'default_location_id': default_loc_id,
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue