1225 lines
66 KiB
HTML
1225 lines
66 KiB
HTML
{% extends "base.html" %}
|
||
{% load i18n %}
|
||
{% load short_name %}
|
||
{% load contract_filters %}
|
||
{% load static %}
|
||
|
||
{% block head %}
|
||
<link rel="stylesheet" href="{% static 'contracts/timeline.css' %}">
|
||
|
||
<style>
|
||
.status-badge.pending { background-color: #ffc107; color: black; }
|
||
.status-badge.sent { background-color: #0dcaf0; color: white; }
|
||
.status-badge.completed { background-color: #198754; color: white; }
|
||
.status-badge.invoiced { background-color: #6f42c1; color: white; }
|
||
.status-badge.paid { background-color: #28a745; color: white; }
|
||
.status-badge.cancelled { background-color: #dc3545; color: white; }
|
||
|
||
|
||
/* Carousel documents */
|
||
#orderDocumentCarousel .carousel-control-prev,
|
||
#orderDocumentCarousel .carousel-control-next {
|
||
width: auto;
|
||
height: auto;
|
||
top: 50%;
|
||
bottom: auto;
|
||
transform: translateY(-50%);
|
||
padding: 0 4px;
|
||
background-color: transparent;
|
||
}
|
||
#orderDocumentCarousel .carousel-control-prev:hover,
|
||
#orderDocumentCarousel .carousel-control-next:hover {
|
||
background-color: transparent;
|
||
}
|
||
#orderDocumentCarousel .carousel-control-prev-icon,
|
||
#orderDocumentCarousel .carousel-control-next-icon {
|
||
filter: invert(0);
|
||
}
|
||
.order-doc-thumbnail-btn.active img {
|
||
border: 3px solid #0d6efd !important;
|
||
}
|
||
</style>
|
||
|
||
{% endblock head %}
|
||
|
||
{% block content %}
|
||
<div class="container mt-5">
|
||
<h2>{% translate "Détail de la commande" %}</h2>
|
||
|
||
{% if messages %}
|
||
{% for message in messages %}
|
||
<div class="alert {% if message.tags == 'success' %}alert-success{% else %}alert-danger{% endif %} alert-dismissible fade show" role="alert">
|
||
{{ message }}
|
||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||
</div>
|
||
{% endfor %}
|
||
{% endif %}
|
||
|
||
<!-- Timeline de statut -->
|
||
<div class="card mt-4 mb-3">
|
||
<div class="card-body py-2">
|
||
<div class="d-flex justify-content-between align-items-center">
|
||
<div class="timeline flex-grow-1" id="statusTimeline"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card mt-4">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<strong>{% translate "Commande" %} :</strong> {{ order.order_code }}
|
||
<div>
|
||
<span class="badge rounded-pill status-badge {{ order.order_status }}">
|
||
{{ order.get_order_status_display }}
|
||
</span>
|
||
{% if order.order_status == 'pending' and is_order_edit_allowed == True %}
|
||
<a href="{% url 'contracts:contracts_orders_change' order.id %}" class="btn btn-sm btn-light mx-2">
|
||
<i class="bi bi-pencil small"></i>
|
||
</a>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<p><strong>{% translate "Contrat lié" %} :</strong> {{ order.contract.contract_number }} - {{ order.contract }}</p>
|
||
|
||
{% if order.description %}
|
||
<p><strong>{% translate "Description" %} :</strong> {{ order.description }}</p>
|
||
{% endif %}
|
||
|
||
<p><strong>{% translate "Date de commande" %} :</strong> {{ order.order_date|date:"d/m/Y" }}</p>
|
||
<p><strong>{% translate "Date de livraison" %} :</strong> {{ order.delivery_date|date:"d/m/Y" }}</p>
|
||
|
||
<hr>
|
||
|
||
<p>
|
||
<strong>{% translate "Montant prévisionnel" %} :</strong>
|
||
{% translate "Non renseigné" as not_set %}
|
||
{{ order.total_amount|default:not_set }} €
|
||
</p>
|
||
|
||
<div class="mb-2 d-flex align-items-center gap-2">
|
||
<div class="text-nowrap" style="min-width: 130px;"><strong>{% translate 'Montant "presté"' %}</strong></div>
|
||
<div class="text-nowrap">{{ order.get_total_realized_amount|floatformat:2 }} €</div>
|
||
<div class="flex-grow-1" style="max-width: 400px;">
|
||
<div class="progress" style="height: 8px;">
|
||
<div class="progress-bar bg-info"
|
||
role="progressbar"
|
||
style="width: {{ order.realized_percent|floatformat:0 }}%;"
|
||
aria-valuenow="{{ order.realized_percent|floatformat:0 }}"
|
||
aria-valuemin="0" aria-valuemax="100">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="text-nowrap text-muted" style="width: 40px; text-align: right;">
|
||
{% if order.total_amount %} {{ order.realized_percent|floatformat:0 }}% {% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-2 d-flex align-items-center gap-2">
|
||
<div class="text-nowrap" style="min-width: 130px;"><strong>{% translate "Montant validé" %}</strong></div>
|
||
<div class="text-nowrap">{{ order.get_total_validated_amount|floatformat:2 }} €</div>
|
||
<div class="flex-grow-1" style="max-width: 400px;">
|
||
<div class="progress" style="height: 8px;">
|
||
<div class="progress-bar bg-success"
|
||
role="progressbar"
|
||
style="width: {{ order.validated_percent|floatformat:0 }}%;"
|
||
aria-valuenow="{{ order.validated_percent|floatformat:0 }}"
|
||
aria-valuemin="0" aria-valuemax="100">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="text-nowrap text-muted" style="width: 40px; text-align: right;">
|
||
{{ order.validated_percent|floatformat:0 }}%
|
||
</div>
|
||
</div>
|
||
|
||
<hr>
|
||
|
||
<div class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center mb-3">
|
||
<div class="mb-3 mb-lg-0">
|
||
<h5 class="mb-1">{% translate "Devis" %}</h5>
|
||
{% if quote %}
|
||
<p class="mb-1"><strong>{% translate "Montant total" %} :</strong> {{ quote_total|floatformat:2 }} €</p>
|
||
{% if quote.is_finalized %}
|
||
<p class="mb-1 text-success"><i class="bi bi-lock-fill me-1"></i> {% translate "Devis figé" %}
|
||
{% if quote.finalized_at %}
|
||
<span class="text-muted">({{ quote.finalized_at|date:'d/m/Y H:i' }})</span>
|
||
{% endif %}
|
||
</p>
|
||
{% endif %}
|
||
<p class="mb-0 text-muted">
|
||
{% blocktrans count item_count=quote.items.count %}
|
||
{{ item_count }} poste dans le devis.
|
||
{% plural %}
|
||
{{ item_count }} postes dans le devis.
|
||
{% endblocktrans %}
|
||
</p>
|
||
{% else %}
|
||
<p class="mb-0 text-muted">{% translate "Aucun devis n'est associé à cette commande pour le moment." %}</p>
|
||
{% endif %}
|
||
</div>
|
||
<div class="btn-group" role="group">
|
||
{% if is_quote_edit_allowed %}
|
||
<a href="{% url 'contracts:contracts_orders_quote' order.id %}" class="btn btn-outline-primary btn-sm">
|
||
{% if quote and quote.is_finalized %}
|
||
<i class="bi bi-eye"></i> {% translate "Voir le devis" %}
|
||
{% else %}
|
||
<i class="bi bi-pencil"></i> {% translate "Gérer le devis" %}
|
||
{% endif %}
|
||
</a>
|
||
{% endif %}
|
||
{% if quote and quote.items.count %}
|
||
<a href="{% url 'contracts:contracts_orders_quote_pdf' order.id %}" target="_blank" class="btn btn-outline-secondary btn-sm">
|
||
<i class="bi bi-file-earmark-pdf"></i> {% translate "Exporter en PDF" %}
|
||
</a>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<hr>
|
||
|
||
{% if order.ordered_by %}
|
||
<p><strong>{% translate "Commandé par" %} :</strong> {{ order.ordered_by|short_name }}</p>
|
||
{% endif %}
|
||
|
||
<p>
|
||
<strong>{% translate "Fichier de commande" %} :</strong>
|
||
{% if order.order_file %}
|
||
<a href="{{ private_url_order_file }}" target="_blank" class="btn btn-sm btn-outline-primary">
|
||
📄 {% translate "Télécharger le fichier" %}
|
||
</a>
|
||
{% endif %}
|
||
</p>
|
||
|
||
</div>
|
||
<div class="card-footer text-end">
|
||
<a href="{% url 'contracts:contracts_index' %}" class="btn btn-secondary">{% translate "Retour à la liste" %}</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Documents / Pièces jointes -->
|
||
<div class="card mt-4">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<h4 class="mb-0">{% translate "Documents" %}</h4>
|
||
{% if is_order_edit_allowed %}
|
||
<button class="btn btn-light border-0" title="{% translate 'Ajouter des documents' %}"
|
||
data-bs-toggle="modal" data-bs-target="#addOrderAttachmentModal">
|
||
<i class="bi bi-plus-circle text-primary"></i>
|
||
</button>
|
||
{% endif %}
|
||
</div>
|
||
<div class="card-body">
|
||
{% if attachments %}
|
||
<div class="row" id="orderAttachmentsContainer">
|
||
{% for att in attachments %}
|
||
<div id="order-att-card-{{ att.id }}" class="col-6 col-md-3 col-lg-2 mb-3">
|
||
<div class="position-relative">
|
||
{% if is_order_edit_allowed %}
|
||
<button type="button"
|
||
class="btn btn-link text-danger p-0 position-absolute top-0 end-0 delete-order-att-btn bg-white"
|
||
data-att-id="{{ att.id }}"
|
||
data-att-name="{{ att.filename|escape }}"
|
||
title="{% translate 'Supprimer' %}">
|
||
<i class="bi bi-trash fs-6"></i>
|
||
</button>
|
||
{% endif %}
|
||
<a href="#"
|
||
class="d-block order-att-thumb"
|
||
data-bs-toggle="modal"
|
||
data-bs-target="#orderDocumentCarouselModal"
|
||
data-att-index="{{ forloop.counter0 }}"
|
||
data-att-url="{{ att.file_url }}"
|
||
data-att-name="{{ att.filename }}">
|
||
<div class="img-thumbnail" style="width:100%;height:100px;overflow:hidden;">
|
||
{% if att.filename|lower|slice:"-4:" == ".pdf" %}
|
||
<div class="d-flex align-items-center justify-content-center h-100 bg-light">
|
||
<i class="bi bi-file-earmark-pdf text-danger" style="font-size:2.5rem;"></i>
|
||
</div>
|
||
{% else %}
|
||
<img src="{{ att.thumbnail_url }}"
|
||
alt="{{ att.filename }}"
|
||
style="width:100%;height:100%;object-fit:cover;object-position:center;">
|
||
{% endif %}
|
||
</div>
|
||
</a>
|
||
</div>
|
||
<div class="text-center text-muted mt-1" style="font-size:0.8em;">
|
||
{{ att.upload_date|date:"d/m/Y H:i" }}<br>
|
||
{{ att.uploaded_by|short_name }}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% else %}
|
||
<p class="text-muted mb-0" id="orderAttachmentsEmpty">{% translate "Aucun document associé à cette commande." %}</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
{% if is_order_edit_allowed %}
|
||
<!-- Modal – confirmation suppression -->
|
||
<div class="modal fade" id="deleteOrderAttachmentModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">{% translate "Supprimer le document" %}</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p>{% translate "Voulez-vous supprimer ce document ?" %}</p>
|
||
<p class="fw-semibold" id="orderAttNameToDelete"></p>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Annuler" %}</button>
|
||
<button type="button" class="btn btn-danger" id="confirmDeleteOrderAtt">{% translate "Supprimer" %}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal – ajout de documents -->
|
||
<div class="modal fade" id="addOrderAttachmentModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">{% translate "Ajouter des documents" %}</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="addOrderAttachmentForm"
|
||
method="post"
|
||
enctype="multipart/form-data"
|
||
action="{% url 'contracts:order_add_attachment' order.id %}">
|
||
{% csrf_token %}
|
||
{{ add_attachment_form.as_p }}
|
||
<div class="modal-footer px-0 pb-0">
|
||
<button type="submit" class="btn btn-primary" id="orderAttSubmitBtn">
|
||
{% translate "Ajouter" %}
|
||
<span class="spinner-border spinner-border-sm ms-2 d-none" id="orderAttSpinner"></span>
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Modal – carousel -->
|
||
<div class="modal fade" id="orderDocumentCarouselModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog modal-dialog-centered modal-xl">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">{% translate "Documents" %}</h5>
|
||
<div class="d-flex align-items-center ms-auto">
|
||
<a href="#" id="downloadCurrentOrderDoc" class="btn btn-sm btn-outline-primary me-2" download
|
||
title="{% translate 'Télécharger' %}">
|
||
<i class="bi bi-download"></i> {% translate "Télécharger" %}
|
||
</a>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
</div>
|
||
<div class="modal-body p-0">
|
||
<div id="orderDocumentCarousel" class="carousel slide" data-bs-ride="false">
|
||
<div class="carousel-inner">
|
||
{% for att in attachments %}
|
||
<div class="carousel-item {% if forloop.first %}active{% endif %}"
|
||
data-att-url="{{ att.file_url }}"
|
||
data-att-filename="{{ att.filename }}"
|
||
data-is-pdf="{% if att.filename|lower|slice:'-4:' == '.pdf' %}true{% else %}false{% endif %}">
|
||
<div class="d-flex justify-content-center align-items-center"
|
||
style="min-height:60vh;background-color:#f8f9fa;">
|
||
{% if att.filename|lower|slice:"-4:" == ".pdf" %}
|
||
<iframe src="{{ att.file_url }}"
|
||
style="width:100%;height:70vh;border:none;"
|
||
title="{{ att.filename }}"></iframe>
|
||
{% else %}
|
||
<img src="{{ att.file_url }}"
|
||
class="d-block"
|
||
alt="{{ att.filename }}"
|
||
style="max-height:70vh;max-width:100%;object-fit:contain;">
|
||
{% endif %}
|
||
</div>
|
||
<div class="carousel-caption d-block"
|
||
style="position:relative;left:0;right:0;bottom:0;padding:10px;background-color:rgba(0,0,0,.5);">
|
||
<p class="mb-0"><strong>{{ att.filename }}</strong></p>
|
||
<p class="mb-0" style="font-size:.9em;">
|
||
{{ att.upload_date|date:"d/m/Y H:i" }} – {{ att.uploaded_by|short_name }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
|
||
{% if attachments|length > 1 %}
|
||
<button class="carousel-control-prev" type="button"
|
||
data-bs-target="#orderDocumentCarousel" data-bs-slide="prev">
|
||
<span class="carousel-control-prev-icon"></span>
|
||
<span class="visually-hidden">{% translate "Précédent" %}</span>
|
||
</button>
|
||
<button class="carousel-control-next" type="button"
|
||
data-bs-target="#orderDocumentCarousel" data-bs-slide="next">
|
||
<span class="carousel-control-next-icon"></span>
|
||
<span class="visually-hidden">{% translate "Suivant" %}</span>
|
||
</button>
|
||
{% endif %}
|
||
</div>
|
||
|
||
{% if attachments|length > 1 %}
|
||
<div class="p-3" style="background-color:#f8f9fa;border-top:1px solid #dee2e6;">
|
||
<div class="d-flex flex-wrap justify-content-center gap-2" id="orderCarouselThumbnails">
|
||
{% for att in attachments %}
|
||
<button type="button"
|
||
class="order-doc-thumbnail-btn {% if forloop.first %}active{% endif %}"
|
||
data-bs-target="#orderDocumentCarousel"
|
||
data-bs-slide-to="{{ forloop.counter0 }}"
|
||
aria-label="{{ att.filename }}"
|
||
style="border:3px solid transparent;padding:0;background:none;cursor:pointer;transition:border-color .2s;">
|
||
{% if att.filename|lower|slice:"-4:" == ".pdf" %}
|
||
<div style="width:80px;height:80px;border-radius:4px;background:#f1f1f1;display:flex;align-items:center;justify-content:center;">
|
||
<i class="bi bi-file-earmark-pdf text-danger" style="font-size:1.8rem;"></i>
|
||
</div>
|
||
{% else %}
|
||
<img src="{{ att.thumbnail_url }}"
|
||
alt="{{ att.filename }}"
|
||
style="width:80px;height:80px;object-fit:cover;border-radius:4px;">
|
||
{% endif %}
|
||
</button>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Documents financiers -->
|
||
<div class="card mt-4">
|
||
<div class="card-header">
|
||
<h4>{% translate "Documents financiers" %}</h4>
|
||
</div>
|
||
<div class="card-body">
|
||
<p>
|
||
<a class="btn btn-outline-secondary" target="_blank" href="{% url 'contracts:order_state_of_works_pdf_preview' order.id %}">
|
||
<i class="bi bi-pencil-square"></i> {% translate "Générer un draft de l'état de travaux" %}
|
||
</a>
|
||
<a class="btn btn-outline-secondary" target="_blank" href="{% url 'contracts:order_claim_declaration_pdf_preview' order.id %}">
|
||
<i class="bi bi-pencil-square"></i> {% translate "Générer un draft de la déclaration de créance" %}
|
||
</a>
|
||
</p>
|
||
{% if is_generate_documents_allowed %}
|
||
<p>
|
||
<a class="btn btn-outline-primary mb-1" target="_self" href="{% url 'contracts:generate_documents_for_order' order.id %}">
|
||
<i class="bi bi-pencil-square"></i> {% translate "Générer la déclaration de créance" %}
|
||
</a>
|
||
<br>
|
||
<span class="text-muted fst-italic" style="font-size: 0.85em;">
|
||
<i class="bi bi-exclamation-triangle"></i> {% translate 'Cette opération va générer un état de travaux, une déclaration de créance et faire passer les interventions validées au statut "comptabilisé".' %}
|
||
</span>
|
||
</p>
|
||
{% endif %}
|
||
<ul class="list-group">
|
||
{% regroup documents by progress_state.index as docs_by_state %}
|
||
|
||
{% for group in docs_by_state %}
|
||
{% if group.grouper %}
|
||
<li class="list-group-item list-group-item-primary">
|
||
{% blocktrans with num=group.grouper %}État d'avancement {{ num }}{% endblocktrans %}
|
||
</li>
|
||
{% else %}
|
||
<li class="list-group-item list-group-item-primary">
|
||
{% translate "Autres documents" %}
|
||
</li>
|
||
{% endif %}
|
||
|
||
{% for doc in group.list %}
|
||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||
<div>
|
||
{% if doc.document.name|lower|slice:"-5:" == ".xlsx" or doc.document.name|lower|slice:"-4:" == ".xls" %}
|
||
<i class="bi bi-file-earmark-excel text-success"></i>
|
||
{% else %}
|
||
<i class="bi bi-file-earmark-pdf text-danger"></i>
|
||
{% endif %}
|
||
<a href="{{ doc.private_url }}" target="_blank">
|
||
{{ doc.title }}
|
||
</a>
|
||
<br>
|
||
<small class="text-muted">
|
||
{% translate "Type" %} : {{ doc.get_document_type_display }} |
|
||
{% translate "Ajouté le" %} {{ doc.uploaded_at|date:"d/m/Y H:i" }}
|
||
{% if doc.document_type == 'claim_declaration' %}
|
||
| <i class="bi bi-paperclip"></i> {{ doc.attachments.count }} {% translate "justificatif(s)" %}
|
||
{% endif %}
|
||
</small>
|
||
</div>
|
||
{% if doc.document_type == 'claim_declaration' %}
|
||
<a href="{% url 'contracts:claim_declaration_attachments' doc.id %}" class="btn btn-sm btn-outline-secondary" title="{% translate 'Gérer les justificatifs' %}">
|
||
<i class="bi bi-paperclip"></i> {% translate "Justificatifs" %}{% if doc.attachments.count %} <span class="badge bg-secondary">{{ doc.attachments.count }}</span>{% endif %}
|
||
</a>
|
||
{% endif %}
|
||
</li>
|
||
{% endfor %}
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Consommations -->
|
||
<div class="card mt-4">
|
||
<div class="card-header">
|
||
<h4>{% translate "Consommation des postes" %}</h4>
|
||
</div>
|
||
<div class="card-body">
|
||
{% if consumption_summary %}
|
||
<div class="table-responsive">
|
||
<table class="table table-striped table-bordered align-middle">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th scope="col">{% translate "Poste" %}</th>
|
||
<th scope="col">{% translate "Code" %}</th>
|
||
<th scope="col">{% translate "Description" %}</th>
|
||
{% if quote %}
|
||
<th scope="col">{% translate "Quantité prévue" %}</th>
|
||
{% endif %}
|
||
{% for ps in progress_states %}
|
||
<th scope="col" class="text-center bg-light" title="{% translate 'État d avancement du' %} {{ ps.created_at|date:'d/m/Y' }}">
|
||
<small>{% translate "État" %} {{ forloop.counter }}<br>{{ ps.created_at|date:"d/m/Y" }}</small>
|
||
</th>
|
||
{% endfor %}
|
||
<th scope="col">{% translate "Quantité consommée" %}</th>
|
||
{% if quote %}
|
||
<th scope="col">{% translate "Quantité restante" %}</th>
|
||
{% endif %}
|
||
<th scope="col">{% translate "Prix unitaire" %}</th>
|
||
<th scope="col">{% translate "Montant consommé" %}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for item in consumption_summary %}
|
||
<tr>
|
||
<td>{{ item.contract_post.order_number }}</td>
|
||
<td>{{ item.contract_post.code }}</td>
|
||
<td>
|
||
{% if LANGUAGE_CODE == "nl" %}
|
||
{{ item.contract_post.description_nl|default:item.contract_post.description_fr }}
|
||
{% else %}
|
||
{{ item.contract_post.description_fr|default:item.contract_post.description_nl }}
|
||
{% endif %}
|
||
</td>
|
||
{% if quote %}
|
||
<td>{{ item.planned_quantity|floatformat:2 }}</td>
|
||
{% endif %}
|
||
{% for ps in progress_states %}
|
||
<td class="text-center bg-light">
|
||
{% if item.progress_states %}
|
||
{{ item.progress_states|dict_get:ps.id|floatformat:2|default:"0.00" }}
|
||
{% else %}
|
||
0.00
|
||
{% endif %}
|
||
</td>
|
||
{% endfor %}
|
||
<td>{{ item.consumed_quantity|floatformat:2 }}</td>
|
||
{% if quote %}
|
||
<td class="{% if item.remaining_quantity < 0 %}text-danger{% endif %}">{{ item.remaining_quantity|floatformat:2 }}</td>
|
||
{% endif %}
|
||
<td>{{ item.contract_post.unit_price|floatformat:2 }} €</td>
|
||
<td>{{ item.consumed_amount|floatformat:2 }} €</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% else %}
|
||
<p class="text-muted mb-0">{% translate "Aucune consommation enregistrée pour cette commande." %}</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Projets associés -->
|
||
{% if order.projects.exists %}
|
||
<div class="mt-4"><strong>{% translate "Projets associés" %} :</strong>
|
||
{% for project in order.projects.all %}
|
||
<a href="{% url 'projects:projects_detail' project.id %}" class="badge bg-primary text-decoration-none">{{ project.code }} - {{ project.name }}</a>{% if not forloop.last %}, {% endif %}
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Commandes directes -->
|
||
<div class="card mt-4" id="directPostsCard">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<h4 class="mb-0">{% translate "Commandes directes" %}</h4>
|
||
{% if can_manage_direct_posts %}
|
||
<button class="btn btn-sm btn-light" id="addDirectPostBtn" title="{% translate 'Ajouter un poste direct' %}">
|
||
<i class="bi bi-plus-circle text-primary"></i>
|
||
</button>
|
||
{% endif %}
|
||
</div>
|
||
<div class="card-body">
|
||
<p class="text-muted fst-italic mb-3" style="font-size:0.9em;">
|
||
<i class="bi bi-info-circle"></i>
|
||
{% translate "Les commandes directes permettent d'enregistrer des postes de marché consommés sans passer par une intervention. Les postes validés sont inclus dans les totaux, les états d'avancement et les déclarations de créance." %}
|
||
</p>
|
||
<div class="table-responsive">
|
||
<table class="table table-bordered align-middle" id="directPostsTable">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th>{% translate "N°" %}</th>
|
||
<th>{% translate "Code" %}</th>
|
||
<th>{% translate "Description" %}</th>
|
||
<th>{% translate "Unité" %}</th>
|
||
<th>{% translate "Prix unit." %}</th>
|
||
<th>{% translate "Quantité" %}</th>
|
||
<th>{% translate "Qté facturée" %}</th>
|
||
<th>{% translate "Montant" %}</th>
|
||
<th>{% translate "Commentaire" %}</th>
|
||
<th>{% translate "Statut" %}</th>
|
||
<th>{% translate "Actions" %}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="directPostsTbody">
|
||
{% for dp in direct_posts %}
|
||
<tr id="dp-row-{{ dp.id }}">
|
||
<td>{{ dp.contract_post.order_number }}</td>
|
||
<td><code>{{ dp.contract_post.code }}</code></td>
|
||
<td>
|
||
{% if LANGUAGE_CODE == "nl" %}
|
||
{{ dp.contract_post.description_nl|default:dp.contract_post.description_fr }}
|
||
{% else %}
|
||
{{ dp.contract_post.description_fr|default:dp.contract_post.description_nl }}
|
||
{% endif %}
|
||
</td>
|
||
<td>{{ dp.contract_post.unit }}</td>
|
||
<td class="text-end">{{ dp.contract_post.unit_price|floatformat:2 }} €</td>
|
||
<td class="text-end dp-qty">{{ dp.quantity|floatformat:2 }}</td>
|
||
<td class="text-end dp-invoiced-qty text-muted">{{ dp.invoiced_quantity|floatformat:2 }}</td>
|
||
<td class="text-end dp-total">{{ dp.total_price|floatformat:2 }} €</td>
|
||
<td class="text-muted dp-comment">{{ dp.comment|default:"" }}</td>
|
||
<td class="dp-status">
|
||
{% if dp.control_status == 'validated' %}
|
||
<span class="badge bg-success">{% translate "Approuvé" %}</span>
|
||
{% if dp.validated_by %}
|
||
<br><small class="text-muted">{{ dp.validated_by.get_full_name }} – {{ dp.validated_at|date:"d/m/Y" }}</small>
|
||
{% endif %}
|
||
{% elif dp.control_status == 'refused' %}
|
||
<span class="badge bg-danger">{% translate "Refusé" %}</span>
|
||
{% elif dp.control_status == 'invoiced' %}
|
||
<span class="badge bg-secondary">{% translate "Facturé" %}</span>
|
||
{% else %}
|
||
<span class="badge bg-warning text-dark">{% translate "En attente" %}</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
<div class="d-flex gap-1">
|
||
{% if can_manage_direct_posts and dp.control_status != 'validated' %}
|
||
<button class="btn btn-sm btn-outline-secondary dp-edit-btn"
|
||
data-id="{{ dp.id }}"
|
||
data-qty="{{ dp.quantity }}"
|
||
data-comment="{{ dp.comment|default:'' }}"
|
||
title="{% translate 'Modifier' %}">
|
||
<i class="bi bi-pencil"></i>
|
||
</button>
|
||
{% if not dp.invoiced_quantity or dp.invoiced_quantity <= 0 %}
|
||
<button class="btn btn-sm btn-outline-danger dp-delete-btn"
|
||
data-id="{{ dp.id }}"
|
||
data-code="{{ dp.contract_post.code }}"
|
||
title="{% translate 'Supprimer' %}">
|
||
<i class="bi bi-trash"></i>
|
||
</button>
|
||
{% endif %}
|
||
{% endif %}
|
||
{% if can_validate_direct_posts %}
|
||
{% if dp.control_status != 'validated' %}
|
||
<button class="btn btn-sm btn-outline-success dp-validate-btn"
|
||
data-id="{{ dp.id }}"
|
||
data-status="validated"
|
||
title="{% translate 'Valider' %}">
|
||
<i class="bi bi-check-circle"></i>
|
||
</button>
|
||
{% endif %}
|
||
{% if dp.control_status != 'refused' %}
|
||
<button class="btn btn-sm btn-outline-warning dp-validate-btn"
|
||
data-id="{{ dp.id }}"
|
||
data-status="refused"
|
||
title="{% translate 'Refuser' %}">
|
||
<i class="bi bi-x-circle"></i>
|
||
</button>
|
||
{% endif %}
|
||
{% if dp.control_status != 'pending' %}
|
||
<button class="btn btn-sm btn-outline-secondary dp-validate-btn"
|
||
data-id="{{ dp.id }}"
|
||
data-status="pending"
|
||
title="{% translate 'Remettre en attente' %}">
|
||
<i class="bi bi-arrow-counterclockwise"></i>
|
||
</button>
|
||
{% endif %}
|
||
{% endif %}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% empty %}
|
||
<tr id="directPostsEmptyRow">
|
||
<td colspan="11" class="text-muted text-center">{% translate "Aucune commande directe enregistrée." %}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{% if can_manage_direct_posts %}
|
||
<!-- Modal : ajouter/modifier un poste direct -->
|
||
<div class="modal fade" id="directPostModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="directPostModalTitle">{% translate "Ajouter un poste direct" %}</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<input type="hidden" id="dpEditId" value="">
|
||
<div class="mb-3" id="dpPostSelectGroup">
|
||
<label class="form-label fw-semibold">{% translate "Poste de marché" %} <span class="text-danger">*</span></label>
|
||
<input type="text" id="dpPostSearch" class="form-control" placeholder="{% translate 'Rechercher par code, numéro ou description…' %}" autocomplete="off">
|
||
<input type="hidden" id="dpPostId">
|
||
<div id="dpPostSearchResults" class="list-group mt-1" style="max-height:220px;overflow-y:auto;position:absolute;z-index:9999;width:100%;"></div>
|
||
<div id="dpPostSelected" class="mt-2 p-2 bg-light rounded d-none">
|
||
<span id="dpPostSelectedLabel"></span>
|
||
<button type="button" class="btn btn-sm btn-link text-danger p-0 ms-2" id="dpPostClear">✕</button>
|
||
</div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label fw-semibold">{% translate "Quantité" %} <span class="text-danger">*</span></label>
|
||
<input type="number" id="dpQty" class="form-control" min="0.01" step="0.01" placeholder="0.00">
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label">{% translate "Commentaire" %}</label>
|
||
<textarea id="dpComment" class="form-control" rows="2"></textarea>
|
||
</div>
|
||
<div id="dpFormError" class="alert alert-danger d-none"></div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Annuler" %}</button>
|
||
<button type="button" class="btn btn-primary" id="dpSaveBtn">
|
||
{% translate "Enregistrer" %}
|
||
<span class="spinner-border spinner-border-sm ms-2 d-none" id="dpSaveSpinner"></span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Modal : confirmation suppression poste direct -->
|
||
<div class="modal fade" id="dpDeleteModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">{% translate "Supprimer le poste direct" %}</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p>{% translate "Voulez-vous supprimer ce poste direct ?" %} <strong id="dpDeleteCode"></strong></p>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Annuler" %}</button>
|
||
<button type="button" class="btn btn-danger" id="dpDeleteConfirmBtn">{% translate "Supprimer" %}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
<div class="card mt-4 mb-4">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<h4>Interventions</h4>
|
||
{% if can_add_intervention_for_order %}
|
||
<button class="btn btn-sm btn-light" type="button" data-bs-toggle="modal" data-bs-target="#interventionTypeModal">
|
||
<i class="bi bi-plus-circle"></i> {% translate "Ajouter une intervention" %}
|
||
</button>
|
||
{% endif %}
|
||
</div>
|
||
<div class="card-body">
|
||
<table id="interventions-table" class="table"
|
||
data-height="550"
|
||
data-pagination="true"
|
||
data-page-size="25"
|
||
data-click-to-select="true" >
|
||
<thead>
|
||
<tr>
|
||
<th data-field="code">{% translate "Code" %}</th>
|
||
<th data-field="title">{% translate "Titre" %}</th>
|
||
<th data-field="begin_date">{% translate "Début" %}</th>
|
||
<th data-field="statut">{% translate "Statut" %}</th>
|
||
</tr>
|
||
<tbody>
|
||
{% for intervention in interventions %}
|
||
<tr>
|
||
<td><a href="{% url 'interventions:interventions_index' %}{{ intervention.id }}">{{ intervention.code }}</a></td>
|
||
<td>{{ intervention.title }}</td>
|
||
<td>{{ intervention.begin_time|date:"d-m-Y H:i" }}</td>
|
||
<td><span class="badge status-{{ intervention.status }}">{{ intervention.get_status_display }}</span></td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</thead>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
{% if can_add_intervention_for_order %}
|
||
{% with order.id|stringformat:"s" as order_id %}
|
||
{% with extra_query_params='&order_id='|add:order_id|add:'&redirect_to_order=true' %}
|
||
{% include "home/_intervention_type_modal.html" %}
|
||
{% endwith %}
|
||
{% endwith %}
|
||
{% endif %}
|
||
</div>
|
||
|
||
{% endblock %}
|
||
|
||
{% block script %}
|
||
<script src="{% url 'javascript-catalog' %}"></script>
|
||
<input type="hidden" id="orderId" value="{{ order.id }}">
|
||
{{ status_choices|json_script:"status_choices" }}
|
||
<script>
|
||
const STATUS_CHOICES = JSON.parse(document.getElementById("status_choices").textContent);
|
||
const currentStatus = '{{ order.order_status }}';
|
||
const allowedStatuses = {{ allowed_statuses|safe }};
|
||
</script>
|
||
<script src="{% static 'contracts/timeline.js' %}"></script>
|
||
|
||
<script>
|
||
// ── Carousel : ouverture sur la bonne slide ────────────────────────────
|
||
document.querySelectorAll('.order-att-thumb').forEach(function(link) {
|
||
link.addEventListener('click', function(e) {
|
||
e.preventDefault();
|
||
var idx = parseInt(this.dataset.attIndex || 0);
|
||
var carousel = document.getElementById('orderDocumentCarousel');
|
||
if (carousel) {
|
||
var bsCarousel = bootstrap.Carousel.getOrCreateInstance(carousel);
|
||
bsCarousel.to(idx);
|
||
}
|
||
});
|
||
});
|
||
|
||
// ── Carousel : sync vignettes ──────────────────────────────────────────
|
||
var orderCarousel = document.getElementById('orderDocumentCarousel');
|
||
if (orderCarousel) {
|
||
orderCarousel.addEventListener('slid.bs.carousel', function(e) {
|
||
document.querySelectorAll('.order-doc-thumbnail-btn').forEach(function(btn, i) {
|
||
btn.classList.toggle('active', i === e.to);
|
||
btn.style.borderColor = (i === e.to) ? '#0d6efd' : 'transparent';
|
||
});
|
||
// Mettre à jour le bouton de téléchargement
|
||
var items = orderCarousel.querySelectorAll('.carousel-item');
|
||
var activeItem = items[e.to];
|
||
if (activeItem) {
|
||
var dlBtn = document.getElementById('downloadCurrentOrderDoc');
|
||
if (dlBtn) {
|
||
dlBtn.href = activeItem.dataset.attUrl;
|
||
dlBtn.download = activeItem.dataset.attFilename || '';
|
||
}
|
||
}
|
||
});
|
||
|
||
// Init du bouton téléchargement sur la première slide
|
||
var firstItem = orderCarousel.querySelector('.carousel-item.active');
|
||
if (firstItem) {
|
||
var dlBtn = document.getElementById('downloadCurrentOrderDoc');
|
||
if (dlBtn) {
|
||
dlBtn.href = firstItem.dataset.attUrl;
|
||
dlBtn.download = firstItem.dataset.attFilename || '';
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Suppression pièce jointe ───────────────────────────────────────────
|
||
var pendingDeleteAttId = null;
|
||
|
||
document.querySelectorAll('.delete-order-att-btn').forEach(function(btn) {
|
||
btn.addEventListener('click', function() {
|
||
pendingDeleteAttId = this.dataset.attId;
|
||
var nameEl = document.getElementById('orderAttNameToDelete');
|
||
if (nameEl) nameEl.textContent = this.dataset.attName;
|
||
var modal = new bootstrap.Modal(document.getElementById('deleteOrderAttachmentModal'));
|
||
modal.show();
|
||
});
|
||
});
|
||
|
||
var confirmBtn = document.getElementById('confirmDeleteOrderAtt');
|
||
if (confirmBtn) {
|
||
confirmBtn.addEventListener('click', function() {
|
||
if (!pendingDeleteAttId) return;
|
||
var orderId = document.getElementById('orderId').value;
|
||
var csrfToken = document.cookie.match(/csrftoken=([^;]+)/);
|
||
csrfToken = csrfToken ? csrfToken[1] : '';
|
||
|
||
fetch('/contracts/orders/' + orderId + '/attachments/' + pendingDeleteAttId + '/delete/', {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRFToken': csrfToken,
|
||
'X-Requested-With': 'XMLHttpRequest',
|
||
},
|
||
})
|
||
.then(function(resp) { return resp.json(); })
|
||
.then(function(data) {
|
||
if (data.success) {
|
||
var card = document.getElementById('order-att-card-' + pendingDeleteAttId);
|
||
if (card) card.remove();
|
||
bootstrap.Modal.getInstance(document.getElementById('deleteOrderAttachmentModal')).hide();
|
||
// Afficher message vide si plus aucun document
|
||
var container = document.getElementById('orderAttachmentsContainer');
|
||
if (container && container.querySelectorAll('[id^="order-att-card-"]').length === 0) {
|
||
container.innerHTML = '';
|
||
var emptyMsg = document.createElement('p');
|
||
emptyMsg.className = 'text-muted mb-0';
|
||
emptyMsg.textContent = gettext('Aucun document associé à cette commande.');
|
||
container.parentNode.replaceChild(emptyMsg, container);
|
||
}
|
||
}
|
||
})
|
||
.catch(function() {
|
||
alert(gettext('Une erreur est survenue.'));
|
||
});
|
||
});
|
||
}
|
||
|
||
// ── Spinner lors de l'upload ───────────────────────────────────────────
|
||
var attForm = document.getElementById('addOrderAttachmentForm');
|
||
if (attForm) {
|
||
attForm.addEventListener('submit', function() {
|
||
var btn = document.getElementById('orderAttSubmitBtn');
|
||
var spinner = document.getElementById('orderAttSpinner');
|
||
if (btn) btn.disabled = true;
|
||
if (spinner) spinner.classList.remove('d-none');
|
||
});
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════════
|
||
// Commandes directes
|
||
// ══════════════════════════════════════════════════════════════════════
|
||
(function () {
|
||
var orderId = document.getElementById('orderId').value;
|
||
var csrfToken = function () {
|
||
var m = document.cookie.match(/csrftoken=([^;]+)/);
|
||
return m ? m[1] : '';
|
||
};
|
||
|
||
var STATUS_LABELS = {
|
||
'validated': '<span class="badge bg-success">{% translate "Approuvé" %}</span>',
|
||
'refused': '<span class="badge bg-danger">{% translate "Refusé" %}</span>',
|
||
'pending': '<span class="badge bg-warning text-dark">{% translate "En attente" %}</span>',
|
||
'invoiced': '<span class="badge bg-secondary">{% translate "Facturé" %}</span>',
|
||
};
|
||
|
||
var canManage = {{ can_manage_direct_posts|yesno:"true,false" }};
|
||
var canValidate = {{ can_validate_direct_posts|yesno:"true,false" }};
|
||
|
||
// ─── helpers ─────────────────────────────────────────────────────
|
||
|
||
function buildActionBtns(post) {
|
||
var html = '<div class="d-flex gap-1">';
|
||
if (canManage && post.control_status !== 'validated') {
|
||
html += '<button class="btn btn-sm btn-outline-secondary dp-edit-btn"'
|
||
+ ' data-id="' + post.id + '"'
|
||
+ ' data-qty="' + post.quantity + '"'
|
||
+ ' data-comment="' + escHtml(post.comment) + '"'
|
||
+ ' title="{% translate "Modifier" %}"><i class="bi bi-pencil"></i></button>';
|
||
if (!(parseFloat(post.invoiced_quantity || 0) > 0)) {
|
||
html += '<button class="btn btn-sm btn-outline-danger dp-delete-btn"'
|
||
+ ' data-id="' + post.id + '"'
|
||
+ ' data-code="' + escHtml(post.post_code) + '"'
|
||
+ ' title="{% translate "Supprimer" %}"><i class="bi bi-trash"></i></button>';
|
||
}
|
||
}
|
||
if (canValidate) {
|
||
if (post.control_status !== 'validated') {
|
||
html += '<button class="btn btn-sm btn-outline-success dp-validate-btn"'
|
||
+ ' data-id="' + post.id + '" data-status="validated"'
|
||
+ ' title="{% translate "Valider" %}"><i class="bi bi-check-circle"></i></button>';
|
||
}
|
||
if (post.control_status !== 'refused') {
|
||
html += '<button class="btn btn-sm btn-outline-warning dp-validate-btn"'
|
||
+ ' data-id="' + post.id + '" data-status="refused"'
|
||
+ ' title="{% translate "Refuser" %}"><i class="bi bi-x-circle"></i></button>';
|
||
}
|
||
if (post.control_status !== 'pending') {
|
||
html += '<button class="btn btn-sm btn-outline-secondary dp-validate-btn"'
|
||
+ ' data-id="' + post.id + '" data-status="pending"'
|
||
+ ' title="{% translate "Remettre en attente" %}"><i class="bi bi-arrow-counterclockwise"></i></button>';
|
||
}
|
||
}
|
||
html += '</div>';
|
||
return html;
|
||
}
|
||
|
||
function buildStatusCell(post) {
|
||
var html = STATUS_LABELS[post.control_status] || STATUS_LABELS['pending'];
|
||
if (post.control_status === 'validated' && post.validated_by) {
|
||
html += '<br><small class="text-muted">' + escHtml(post.validated_by) + ' – ' + escHtml(post.validated_at) + '</small>';
|
||
}
|
||
return html;
|
||
}
|
||
|
||
function escHtml(str) {
|
||
return (str || '').toString()
|
||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||
.replace(/"/g, '"').replace(/'/g, ''');
|
||
}
|
||
|
||
function appendRow(post) {
|
||
var tbody = document.getElementById('directPostsTbody');
|
||
var emptyRow = document.getElementById('directPostsEmptyRow');
|
||
if (emptyRow) emptyRow.remove();
|
||
|
||
var tr = document.createElement('tr');
|
||
tr.id = 'dp-row-' + post.id;
|
||
tr.innerHTML = [
|
||
'<td>' + escHtml(post.post_order_number) + '</td>',
|
||
'<td><code>' + escHtml(post.post_code) + '</code></td>',
|
||
'<td>' + escHtml(post.post_description) + '</td>',
|
||
'<td>' + escHtml(post.unit) + '</td>',
|
||
'<td class="text-end">' + parseFloat(post.unit_price).toFixed(2) + ' €</td>',
|
||
'<td class="text-end dp-qty">' + parseFloat(post.quantity).toFixed(2) + '</td>',
|
||
'<td class="text-end dp-invoiced-qty text-muted">' + parseFloat(post.invoiced_quantity || 0).toFixed(2) + '</td>',
|
||
'<td class="text-end dp-total">' + parseFloat(post.total_price).toFixed(2) + ' €</td>',
|
||
'<td class="text-muted dp-comment">' + escHtml(post.comment) + '</td>',
|
||
'<td class="dp-status">' + buildStatusCell(post) + '</td>',
|
||
'<td>' + buildActionBtns(post) + '</td>',
|
||
].join('');
|
||
tbody.appendChild(tr);
|
||
bindRowEvents(tr);
|
||
}
|
||
|
||
function updateRow(tr, post) {
|
||
tr.querySelector('.dp-qty').textContent = parseFloat(post.quantity).toFixed(2);
|
||
if (tr.querySelector('.dp-invoiced-qty')) tr.querySelector('.dp-invoiced-qty').textContent = parseFloat(post.invoiced_quantity || 0).toFixed(2);
|
||
tr.querySelector('.dp-total').textContent = parseFloat(post.total_price).toFixed(2) + ' €';
|
||
if (tr.querySelector('.dp-comment')) tr.querySelector('.dp-comment').textContent = post.comment || '';
|
||
tr.querySelector('.dp-status').innerHTML = buildStatusCell(post);
|
||
tr.querySelector('td:last-child').innerHTML = buildActionBtns(post);
|
||
bindRowEvents(tr);
|
||
}
|
||
|
||
function bindRowEvents(tr) {
|
||
tr.querySelectorAll('.dp-edit-btn').forEach(function (btn) {
|
||
btn.addEventListener('click', onEditClick);
|
||
});
|
||
tr.querySelectorAll('.dp-delete-btn').forEach(function (btn) {
|
||
btn.addEventListener('click', onDeleteClick);
|
||
});
|
||
tr.querySelectorAll('.dp-validate-btn').forEach(function (btn) {
|
||
btn.addEventListener('click', onValidateClick);
|
||
});
|
||
}
|
||
|
||
// Init existing rows
|
||
document.querySelectorAll('#directPostsTbody tr[id^="dp-row-"]').forEach(bindRowEvents);
|
||
|
||
// ─── Add button ──────────────────────────────────────────────────
|
||
var addBtn = document.getElementById('addDirectPostBtn');
|
||
if (addBtn) {
|
||
addBtn.addEventListener('click', function () {
|
||
openModal(null);
|
||
});
|
||
}
|
||
|
||
// ─── Post autocomplete ────────────────────────────────────────────
|
||
var dpPostSearch = document.getElementById('dpPostSearch');
|
||
var dpPostResults = document.getElementById('dpPostSearchResults');
|
||
var dpPostId = document.getElementById('dpPostId');
|
||
var dpPostSelected = document.getElementById('dpPostSelected');
|
||
var dpPostSelectedLabel = document.getElementById('dpPostSelectedLabel');
|
||
var dpPostClear = document.getElementById('dpPostClear');
|
||
|
||
if (dpPostSearch) {
|
||
var searchTimeout;
|
||
dpPostSearch.addEventListener('input', function () {
|
||
clearTimeout(searchTimeout);
|
||
var q = this.value.trim();
|
||
if (!q) { dpPostResults.innerHTML = ''; return; }
|
||
searchTimeout = setTimeout(function () {
|
||
var contractId = '{{ order.contract_id }}';
|
||
fetch('/contracts/autocomplete-posts/?q=' + encodeURIComponent(q) + '&contract_id=' + contractId)
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (data) {
|
||
dpPostResults.innerHTML = '';
|
||
data.forEach(function (post) {
|
||
var btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.className = 'list-group-item list-group-item-action';
|
||
btn.textContent = '[' + post.order_number + '] ' + post.code + ' – ' + post.description;
|
||
btn.addEventListener('click', function () {
|
||
dpPostId.value = post.id;
|
||
dpPostSelectedLabel.textContent = '[' + post.order_number + '] ' + post.code + ' – ' + post.description;
|
||
dpPostSelected.classList.remove('d-none');
|
||
dpPostSearch.value = '';
|
||
dpPostResults.innerHTML = '';
|
||
});
|
||
dpPostResults.appendChild(btn);
|
||
});
|
||
});
|
||
}, 300);
|
||
});
|
||
|
||
document.addEventListener('click', function (e) {
|
||
if (!dpPostResults.contains(e.target) && e.target !== dpPostSearch) {
|
||
dpPostResults.innerHTML = '';
|
||
}
|
||
});
|
||
}
|
||
|
||
if (dpPostClear) {
|
||
dpPostClear.addEventListener('click', function () {
|
||
dpPostId.value = '';
|
||
dpPostSelected.classList.add('d-none');
|
||
dpPostSelectedLabel.textContent = '';
|
||
});
|
||
}
|
||
|
||
// ─── Open modal ───────────────────────────────────────────────────
|
||
function openModal(editId, qty, comment) {
|
||
var modalEl = document.getElementById('directPostModal');
|
||
if (!modalEl) return;
|
||
document.getElementById('dpEditId').value = editId || '';
|
||
document.getElementById('dpQty').value = qty || '';
|
||
document.getElementById('dpComment').value = comment || '';
|
||
document.getElementById('dpFormError').classList.add('d-none');
|
||
var titleEl = document.getElementById('directPostModalTitle');
|
||
if (editId) {
|
||
titleEl.textContent = '{% translate "Modifier le poste direct" %}';
|
||
document.getElementById('dpPostSelectGroup').classList.add('d-none');
|
||
} else {
|
||
titleEl.textContent = '{% translate "Ajouter un poste direct" %}';
|
||
document.getElementById('dpPostSelectGroup').classList.remove('d-none');
|
||
if (dpPostId) { dpPostId.value = ''; }
|
||
if (dpPostSelected) { dpPostSelected.classList.add('d-none'); }
|
||
if (dpPostSelectedLabel) { dpPostSelectedLabel.textContent = ''; }
|
||
if (dpPostSearch) { dpPostSearch.value = ''; }
|
||
}
|
||
new bootstrap.Modal(modalEl).show();
|
||
}
|
||
|
||
function onEditClick() {
|
||
openModal(this.dataset.id, this.dataset.qty, this.dataset.comment);
|
||
}
|
||
|
||
// ─── Save (add or update) ─────────────────────────────────────────
|
||
var saveBtn = document.getElementById('dpSaveBtn');
|
||
if (saveBtn) {
|
||
saveBtn.addEventListener('click', function () {
|
||
var editId = document.getElementById('dpEditId').value;
|
||
var qty = document.getElementById('dpQty').value;
|
||
var comment = document.getElementById('dpComment').value;
|
||
var errorEl = document.getElementById('dpFormError');
|
||
var spinner = document.getElementById('dpSaveSpinner');
|
||
|
||
errorEl.classList.add('d-none');
|
||
|
||
var url, body;
|
||
if (editId) {
|
||
url = '/contracts/orders/' + orderId + '/direct-posts/' + editId + '/update/';
|
||
body = JSON.stringify({ quantity: qty, comment: comment });
|
||
} else {
|
||
var postId = dpPostId ? dpPostId.value : '';
|
||
if (!postId) {
|
||
errorEl.textContent = '{% translate "Veuillez sélectionner un poste de marché." %}';
|
||
errorEl.classList.remove('d-none');
|
||
return;
|
||
}
|
||
url = '/contracts/orders/' + orderId + '/direct-posts/add/';
|
||
body = JSON.stringify({ contract_post_id: postId, quantity: qty, comment: comment });
|
||
}
|
||
|
||
saveBtn.disabled = true;
|
||
spinner.classList.remove('d-none');
|
||
|
||
fetch(url, {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRFToken': csrfToken(),
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: body,
|
||
})
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (data) {
|
||
if (data.success) {
|
||
if (editId) {
|
||
var tr = document.getElementById('dp-row-' + editId);
|
||
if (tr) updateRow(tr, data.post);
|
||
} else {
|
||
appendRow(data.post);
|
||
}
|
||
bootstrap.Modal.getInstance(document.getElementById('directPostModal')).hide();
|
||
} else {
|
||
errorEl.textContent = data.error || '{% translate "Une erreur est survenue." %}';
|
||
errorEl.classList.remove('d-none');
|
||
}
|
||
})
|
||
.catch(function () {
|
||
errorEl.textContent = '{% translate "Une erreur est survenue." %}';
|
||
errorEl.classList.remove('d-none');
|
||
})
|
||
.finally(function () {
|
||
saveBtn.disabled = false;
|
||
spinner.classList.add('d-none');
|
||
});
|
||
});
|
||
}
|
||
|
||
// ─── Delete ───────────────────────────────────────────────────────
|
||
var pendingDeleteDpId = null;
|
||
function onDeleteClick() {
|
||
pendingDeleteDpId = this.dataset.id;
|
||
var codeEl = document.getElementById('dpDeleteCode');
|
||
if (codeEl) codeEl.textContent = this.dataset.code;
|
||
new bootstrap.Modal(document.getElementById('dpDeleteModal')).show();
|
||
}
|
||
|
||
var dpDeleteConfirmBtn = document.getElementById('dpDeleteConfirmBtn');
|
||
if (dpDeleteConfirmBtn) {
|
||
dpDeleteConfirmBtn.addEventListener('click', function () {
|
||
if (!pendingDeleteDpId) return;
|
||
fetch('/contracts/orders/' + orderId + '/direct-posts/' + pendingDeleteDpId + '/delete/', {
|
||
method: 'POST',
|
||
headers: { 'X-CSRFToken': csrfToken() },
|
||
})
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (data) {
|
||
if (data.success) {
|
||
var tr = document.getElementById('dp-row-' + pendingDeleteDpId);
|
||
if (tr) tr.remove();
|
||
bootstrap.Modal.getInstance(document.getElementById('dpDeleteModal')).hide();
|
||
if (!document.querySelector('#directPostsTbody tr[id^="dp-row-"]')) {
|
||
var tbody = document.getElementById('directPostsTbody');
|
||
tbody.innerHTML = '<tr id="directPostsEmptyRow"><td colspan="10" class="text-muted text-center">{% translate "Aucune commande directe enregistrée." %}</td></tr>';
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// ─── Validate / Refuse / Reset ─────────────────────────────────────
|
||
function onValidateClick() {
|
||
var dpId = this.dataset.id;
|
||
var status = this.dataset.status;
|
||
var tr = document.getElementById('dp-row-' + dpId);
|
||
fetch('/contracts/orders/' + orderId + '/direct-posts/' + dpId + '/validate/', {
|
||
method: 'POST',
|
||
headers: {
|
||
'X-CSRFToken': csrfToken(),
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({ status: status }),
|
||
})
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (data) {
|
||
if (data.success && tr) {
|
||
updateRow(tr, data.post);
|
||
}
|
||
});
|
||
}
|
||
})();
|
||
</script>
|
||
{% endblock script %}
|