loko/loko/stock/templates/stock/purchase_order_from_pdf.html

732 lines
45 KiB
HTML

{% extends "base.html" %}
{% load i18n static %}
{% block title %}{% translate "Créer un bon de commande depuis une offre (PDF, Scan, Photo)" %} — Loko{% endblock %}
{% block content %}
<div class="container-fluid py-3">
<!-- Fil d'Ariane & Titre -->
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-1">
<li class="breadcrumb-item"><a href="{% url 'stock:stock_purchases' %}">{% translate "Achats" %}</a></li>
<li class="breadcrumb-item active" aria-current="page">{% translate "Import d'offre" %}</li>
</ol>
</nav>
<h1 class="h3 d-flex align-items-center mb-0">
<i class="bi bi-file-earmark-pdf text-danger me-2"></i>
<i class="bi bi-camera text-primary me-2"></i>
{% translate "Créer un bon de commande depuis une offre (PDF, Scan, Photo)" %}
</h1>
</div>
<div>
<a href="{{ cancel_url }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> {% translate "Retour aux achats" %}
</a>
</div>
</div>
{% if step == 'upload' %}
<!-- ÉTAPE 1 : TÉLÉVERSEMENT DU FICHIER (PDF, SCAN OU PHOTO) -->
<div class="row justify-content-center mt-4">
<div class="col-lg-8 col-xl-7">
<div class="card border-0 shadow-sm">
<div class="card-body p-4 p-md-5">
<div class="text-center mb-4">
<div class="mb-3">
<span class="d-inline-flex align-items-center justify-content-center bg-primary bg-opacity-10 text-primary rounded-circle" style="width: 80px; height: 80px;">
<i class="bi bi-cloud-arrow-up fs-1"></i>
</span>
</div>
<h4 class="fw-bold">{% translate "Importer une offre ou un devis fournisseur" %}</h4>
<p class="text-muted">
{% translate "Déposez un devis au format PDF, un scan ou des photos prises avec un smartphone (sélectionnez toutes les pages en une fois). Le système combine et analyse automatiquement les pages, extrait les articles et prépare le bon de commande." %}
</p>
</div>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<input type="hidden" name="action" value="parse_pdf">
<!-- Sélection de l'entrepôt -->
<div class="mb-4">
<label for="id_warehouse" class="form-label fw-semibold">
<i class="bi bi-building me-1"></i> {% translate "Entrepôt de destination" %} <span class="text-danger">*</span>
</label>
<select name="warehouse" id="id_warehouse" class="form-select form-select-lg" required>
{% for w in warehouses %}
<option value="{{ w.id }}" {% if forloop.first %}selected{% endif %}>{{ w.name }}</option>
{% endfor %}
</select>
<div class="form-text">
{% translate "Les articles de la commande seront associés par défaut aux emplacements de cet entrepôt." %}
</div>
</div>
<!-- Zone de dépôt Document (PDF ou Image(s)) -->
<div class="mb-4">
<label class="form-label fw-semibold">
<i class="bi bi-file-earmark-text me-1"></i> {% translate "Fichier(s) de l'offre (PDF, scan ou photos des pages)" %} <span class="text-danger">*</span>
</label>
<div class="border border-2 border-dashed rounded-3 p-4 text-center bg-light" id="drop-zone" style="cursor: pointer;">
<div class="mb-2">
<i class="bi bi-file-earmark-pdf text-danger fs-2 me-2"></i>
<i class="bi bi-camera text-primary fs-2"></i>
</div>
<span class="fw-medium text-dark d-block mb-1">{% translate "Cliquez ou glissez-déposez vos documents ou photos ici" %}</span>
<span class="text-muted small d-block mb-2">{% translate "Formats acceptés : PDF, photos & scans (JPG, PNG, WEBP, TIFF) — Sélection multiple de photos autorisée pour plusieurs pages" %}</span>
<input type="file" name="pdf_file" id="id_pdf_file" class="d-none" accept=".pdf,image/*,.png,.jpg,.jpeg,.webp,.bmp,.tiff,.tif" multiple required>
<div id="selected-file-name" class="mt-3 fw-bold text-primary d-none">
<i class="bi bi-file-earmark-check me-1"></i> <span class="file-text"></span>
</div>
</div>
</div>
<!-- Encadré explicatif -->
<div class="alert alert-info border-0 d-flex align-items-start mb-4">
<i class="bi bi-info-circle-fill fs-5 me-2 mt-1 flex-shrink-0"></i>
<div class="small">
<strong>{% translate "Règle de gestion des articles & du fournisseur :" %}</strong><br>
{% translate "Les articles du devis sont rapprochés avec les produits existants de votre catalogue Loko. Si le fournisseur n'est pas encore enregistré, vous pourrez confirmer sa création directement lors de la revue." %}
</div>
</div>
<!-- Bouton de soumission -->
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg" id="submit-parse-btn">
<i class="bi bi-cpu me-2"></i> {% translate "Analyser le document" %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% elif step == 'review' %}
<!-- ÉTAPE 2 : REVUE ET VALIDATION DU BON DE COMMANDE -->
<form method="post" id="confirm-order-form" onsubmit="return validateOrderForm()">
{% csrf_token %}
<input type="hidden" name="action" value="confirm_order">
<input type="hidden" name="warehouse_id" value="{{ selected_warehouse.id }}">
<input type="hidden" name="temp_pdf_path" value="{{ temp_pdf_path }}">
<input type="hidden" name="orig_pdf_name" value="{{ orig_pdf_name }}">
<input type="hidden" name="temp_files_json" value="{{ temp_files_json }}">
<input type="hidden" name="item_count" value="{{ parse_result.items|length }}">
<!-- Cartes d'en-tête (Fournisseur & Commande) -->
<div class="row g-3 mb-4">
<!-- CARTE FOURNISSEUR -->
<div class="col-lg-6">
<div class="card h-100 border-0 shadow-sm">
<div class="card-header bg-white border-bottom py-3 d-flex justify-content-between align-items-center">
<span class="fw-bold text-dark"><i class="bi bi-building text-primary me-2"></i>{% translate "Fournisseur" %}</span>
{% if parse_result.matched_supplier %}
<span class="badge bg-success-subtle text-success border border-success-subtle">
<i class="bi bi-check-circle me-1"></i>{% translate "Fournisseur identifié" %}
</span>
{% else %}
<span class="badge bg-warning-subtle text-warning-emphasis border border-warning-subtle">
<i class="bi bi-exclamation-circle me-1"></i>{% translate "Fournisseur non trouvé" %}
</span>
{% endif %}
</div>
<div class="card-body">
{% if parse_result.matched_supplier %}
<div class="mb-3">
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="supplier_choice" id="sup_choice_existing" value="existing" checked>
<label class="form-check-label fw-bold" for="sup_choice_existing">
{{ parse_result.matched_supplier.name }}
</label>
</div>
<div class="text-muted small ps-4">
{% if parse_result.matched_supplier.enterprise_number %}
<div><strong>{% translate "TVA / N° Entr." %} :</strong> {{ parse_result.matched_supplier.enterprise_number }}</div>
{% endif %}
{% if parse_result.matched_supplier.contact_email %}
<div><strong>{% translate "Email" %} :</strong> {{ parse_result.matched_supplier.contact_email }}</div>
{% endif %}
</div>
<input type="hidden" name="supplier_id" value="{{ parse_result.matched_supplier.id }}">
</div>
<div class="border-top pt-2">
<a class="small text-muted text-decoration-none" data-bs-toggle="collapse" href="#changeSupplierCollapse" role="button">
<i class="bi bi-pencil me-1"></i>{% translate "Changer de fournisseur" %}
</a>
<div class="collapse mt-2" id="changeSupplierCollapse">
<select class="form-select form-select-sm" onchange="document.querySelector('input[name=supplier_id]').value = this.value;">
{% for s in suppliers %}
<option value="{{ s.id }}" {% if s.id == parse_result.matched_supplier.id %}selected{% endif %}>{{ s.name }}{% if s.enterprise_number %} ({{ s.enterprise_number }}){% endif %}</option>
{% endfor %}
</select>
</div>
</div>
{% else %}
<!-- Fournisseur non répertorié : Proposition claire de création ou sélection -->
<div class="alert alert-warning border-0 d-flex align-items-center mb-3 py-2 px-3">
<i class="bi bi-building-exclamation fs-4 me-2 text-warning flex-shrink-0"></i>
<div class="small">
<strong>{% translate "Fournisseur non trouvé dans le répertoire Loko" %}</strong><br>
{% translate "Le fournisseur détecté n'a pas été retrouvé. Souhaitez-vous le créer dans Loko ou rattacher un fournisseur existant ?" %}
</div>
</div>
<!-- Option 1 : Créer le fournisseur (pré-rempli) -->
<div class="p-3 border rounded-3 border-primary bg-white shadow-sm mb-3" id="container_create_supplier">
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="supplier_choice" id="sup_choice_new" value="new" checked onchange="toggleSupplierChoice()">
<label class="form-check-label fw-bold text-primary" for="sup_choice_new">
<i class="bi bi-plus-circle-fill me-1"></i> {% translate "Créer ce nouveau fournisseur dans le répertoire (pré-rempli)" %}
</label>
</div>
<div class="row g-2 ps-3 mt-1">
<div class="col-md-7">
<label class="form-label small fw-semibold mb-1">
{% translate "Nom de l'entreprise" %} <span class="text-danger">*</span>
</label>
<input type="text" name="new_supplier_name" id="id_new_supplier_name" class="form-control form-control-sm" value="{{ parse_result.metadata.supplier_name }}" placeholder="{% translate 'ex: Mon Fournisseur SRL' %}">
</div>
<div class="col-md-5">
<label class="form-label small fw-semibold mb-1">{% translate "N° TVA / Entreprise" %}</label>
<input type="text" name="new_supplier_vat" class="form-control form-control-sm" value="{{ parse_result.metadata.supplier_vat }}" placeholder="BE 0123.456.789">
</div>
<div class="col-md-6">
<label class="form-label small fw-semibold mb-1">{% translate "Email de contact" %}</label>
<input type="email" name="new_supplier_email" class="form-control form-control-sm" value="{{ parse_result.metadata.supplier_email }}" placeholder="contact@fournisseur.com">
</div>
<div class="col-md-6">
<label class="form-label small fw-semibold mb-1">{% translate "Téléphone" %}</label>
<input type="text" name="new_supplier_phone" class="form-control form-control-sm" value="{{ parse_result.metadata.supplier_phone }}" placeholder="+32 ...">
</div>
<div class="col-12">
<label class="form-label small fw-semibold mb-1">{% translate "Adresse" %}</label>
<input type="text" name="new_supplier_address" class="form-control form-control-sm" value="{{ parse_result.metadata.supplier_address }}" placeholder="{% translate 'Rue, code postal, ville' %}">
</div>
</div>
<div class="text-muted small ps-3 mt-2">
<i class="bi bi-info-circle me-1"></i>
{% translate "Le fournisseur sera automatiquement enregistré dans Loko et rattaché à ce bon de commande." %}
</div>
</div>
<!-- Option 2 : Rattacher à un fournisseur existant -->
<div class="p-3 border rounded-3 bg-light-subtle" id="container_existing_supplier">
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="supplier_choice" id="sup_choice_pick" value="existing" onchange="toggleSupplierChoice()">
<label class="form-check-label fw-bold text-secondary" for="sup_choice_pick">
<i class="bi bi-search me-1"></i> {% translate "Ou rattacher à un fournisseur déjà existant dans Loko :" %}
</label>
</div>
<div class="ps-3">
<select name="supplier_id" id="id_supplier_id" class="form-select form-select-sm" onfocus="document.getElementById('sup_choice_pick').checked = true; toggleSupplierChoice();">
<option value="">{% translate "— Sélectionner un fournisseur dans la liste —" %}</option>
{% for s in suppliers %}
<option value="{{ s.id }}">{{ s.name }}{% if s.enterprise_number %} ({{ s.enterprise_number }}){% endif %}</option>
{% endfor %}
</select>
</div>
</div>
{% endif %}
</div>
</div>
</div>
<!-- CARTE COMMANDE & DESTINATION -->
<div class="col-lg-6">
<div class="card h-100 border-0 shadow-sm">
<div class="card-header bg-white border-bottom py-3">
<span class="fw-bold text-dark"><i class="bi bi-cart-check text-primary me-2"></i>{% translate "Informations du bon de commande" %}</span>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label small text-muted mb-1">{% translate "Entrepôt de livraison" %}</label>
<div class="fw-bold">{{ selected_warehouse.name }}</div>
</div>
<div class="col-md-6">
<label class="form-label small text-muted mb-1">{% translate "Statut initial" %}</label>
<select name="order_status" class="form-select form-select-sm">
<option value="pending" selected>{% translate "En attente (Brouillon)" %}</option>
<option value="ordered">{% translate "Envoyée (Commandée)" %}</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label small text-muted mb-1">{% translate "N° Devis / Offre d'origine" %}</label>
<div class="fw-bold font-monospace">{{ parse_result.metadata.quote_number|default:"—" }}</div>
</div>
<div class="col-md-6">
<label class="form-label small text-muted mb-1">{% translate "Date du devis" %}</label>
<div class="fw-bold">{{ parse_result.metadata.date|default:"—" }}</div>
</div>
<div class="col-12 border-top pt-2">
<div class="small text-muted d-flex align-items-center flex-wrap gap-2">
<div>
<i class="bi bi-paperclip me-1 text-primary"></i>
{% if parse_result.is_multi_image %}
<span>{% translate "Pièces justificatives attachées :" %} <strong>{{ parse_result.image_count }} {% translate "photos / pages" %}</strong></span>
{% else %}
<span>{% translate "Pièce justificative attachée :" %} <strong>{{ orig_pdf_name }}</strong></span>
{% endif %}
</div>
{% if parse_result.is_multi_image %}
<span class="badge bg-primary-subtle text-primary border border-primary-subtle">
<i class="bi bi-images me-1"></i>{% blocktranslate with count=parse_result.image_count %}{{ count }} photos combinées{% endblocktranslate %}
</span>
{% elif parse_result.is_image %}
<span class="badge bg-info-subtle text-info-emphasis border border-info-subtle">
<i class="bi bi-camera me-1"></i>{% translate "Photo / Scan" %}
</span>
{% elif parse_result.is_ocr %}
<span class="badge bg-secondary-subtle text-secondary border">
<i class="bi bi-file-earmark-text me-1"></i>{% translate "PDF scanné" %}
</span>
{% else %}
<span class="badge bg-primary-subtle text-primary border border-primary-subtle">
<i class="bi bi-file-earmark-pdf me-1"></i>{% translate "PDF numérique" %}
</span>
{% endif %}
</div>
{% if saved_temp_files and saved_temp_files|length > 1 %}
<div class="mt-2 d-flex flex-wrap gap-1">
{% for f in saved_temp_files %}
<span class="badge bg-light text-dark border">
<i class="bi bi-file-earmark-image text-primary me-1"></i>{{ f.name }}
</span>
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- RÈGLE COMMANDE CATALOGUE -->
<div class="alert alert-info border-0 shadow-sm d-flex align-items-center mb-3 py-2 px-3">
<i class="bi bi-info-circle-fill text-primary fs-5 me-3"></i>
<div>
<strong>{% translate "Règle de commande :" %}</strong>
{% translate "Les lignes de commande doivent obligatoirement correspondre à un produit existant dans le catalogue. Si un produit n'est pas reconnu automatiquement, sélectionnez-le dans la liste ou décochez la ligne pour l'exclure." %}
</div>
</div>
<!-- TABLEAU DES ARTICLES EXTRAITS -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white border-bottom py-3 d-flex justify-content-between align-items-center">
<div>
<span class="fw-bold text-dark fs-5">
<i class="bi bi-box-seam text-primary me-2"></i>
{% translate "Articles extraits du devis" %}
</span>
<span class="badge bg-primary ms-2">{{ parse_result.items|length }} {% translate "ligne(s)" %}</span>
</div>
<div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="toggleSelectAllItems(true)">
<i class="bi bi-check-all"></i> {% translate "Tout cocher" %}
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="toggleSelectAllItems(false)">
<i class="bi bi-x"></i> {% translate "Tout décocher" %}
</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0" id="items-review-table">
<thead class="table-light small">
<tr>
<th style="width: 40px;" class="text-center">
<input type="checkbox" id="master-select-checkbox" class="form-check-input" checked onchange="toggleSelectAllItems(this.checked)">
</th>
<th style="min-width: 250px;">{% translate "Référence & Désignation fournisseur" %}</th>
<th style="min-width: 320px;">{% translate "Correspondance Produit Stock" %}</th>
<th style="width: 100px;" class="text-end">{% translate "Quantité" %}</th>
<th style="width: 120px;" class="text-end">{% translate "Prix net HT" %}</th>
<th style="width: 90px;" class="text-end">{% translate "TVA" %}</th>
<th style="width: 120px;" class="text-end">{% translate "Total HT" %}</th>
<th style="min-width: 180px;">{% translate "Emplacement stock" %}</th>
</tr>
</thead>
<tbody>
{% for item in parse_result.items %}
{% with idx=forloop.counter0 %}
<tr id="item-row-{{ idx }}" class="{% if not item.matched_product_id %}table-warning-subtle{% endif %}">
<!-- Checkbox inclusion -->
<td class="text-center">
<input type="checkbox"
name="item_{{ idx }}_include"
id="item_{{ idx }}_include"
class="form-check-input item-include-cb"
data-idx="{{ idx }}"
value="1"
{% if item.matched_product_id %}checked{% endif %}
onchange="updateTotals()">
<input type="hidden" name="item_{{ idx }}_ref" value="{{ item.reference }}">
<input type="hidden" name="item_{{ idx }}_name" value="{{ item.name }}">
<input type="hidden" name="item_{{ idx }}_qty" value="{{ item.quantity }}">
<input type="hidden" name="item_{{ idx }}_unit" value="{{ item.unit }}">
<input type="hidden" name="item_{{ idx }}_price" value="{{ item.unit_price }}">
<input type="hidden" name="item_{{ idx }}_vat" value="{{ item.vat_rate }}">
</td>
<!-- Désignation extraite -->
<td>
{% if item.reference %}
<span class="badge bg-light text-dark border font-monospace me-1">{{ item.reference }}</span>
{% endif %}
<div class="fw-semibold text-break">{{ item.name }}</div>
{% if item.discount and item.discount != "0%" %}
<small class="text-success"><i class="bi bi-tag-fill me-1"></i>Remise fournisseur : {{ item.discount }}</small>
{% endif %}
</td>
<!-- Rapprochement Catalogue Produit (Exclusif aux produits existants) -->
<td>
<div class="mb-1 d-flex align-items-center gap-1">
{% if item.matched_product_id %}
{% if item.match_type == 'exact_supplier_ref' %}
<span class="badge bg-success text-white">
<i class="bi bi-check-circle me-1"></i>{% translate "Réf. fournisseur exacte" %}
</span>
{% elif item.match_type == 'exact_sku_or_code' %}
<span class="badge bg-success text-white">
<i class="bi bi-check-circle me-1"></i>{% translate "Code/SKU exact" %}
</span>
{% elif item.match_type == 'exact_name' %}
<span class="badge bg-success text-white">
<i class="bi bi-check-circle me-1"></i>{% translate "Nom identique" %}
</span>
{% else %}
<span class="badge bg-info text-dark">
<i class="bi bi-stars me-1"></i>{% translate "Similarité trouvée" %} ({{ item.match_score|floatformat:0 }}%)
</span>
{% endif %}
<a href="{% url 'stock:product_detail' item.matched_product_id %}" target="_blank" class="small text-muted text-decoration-none ms-1" title="{% translate 'Voir la fiche produit' %}">
<i class="bi bi-box-arrow-up-right"></i>
</a>
{% else %}
<span class="badge bg-warning text-dark">
<i class="bi bi-exclamation-triangle me-1"></i>{% translate "Non associé" %}
</span>
{% endif %}
</div>
<!-- Sélecteur de produit existant -->
<select name="item_{{ idx }}_product_id"
id="item_{{ idx }}_product_id"
class="form-select form-select-sm product-select-input {% if not item.matched_product_id %}border-warning{% endif %}"
data-idx="{{ idx }}"
onchange="handleProductSelection({{ idx }})">
{% if not item.matched_product_id %}
<option value="" selected>— {% translate "Sélectionner un produit existant..." %} —</option>
{% else %}
<option value="{{ item.matched_product_id }}" selected>
{{ item.matched_product_code }} — {{ item.matched_product_name }}
</option>
<option value="">— {% translate "Ne pas associer (exclure)" %} —</option>
{% endif %}
{% if item.top_matches %}
<optgroup label="{% translate 'Suggestions pertinentes' %}">
{% for s in item.top_matches %}
{% if s.id != item.matched_product_id %}
<option value="{{ s.id }}">
{{ s.code }} — {{ s.name }} ({{ s.score|floatformat:0 }}%)
</option>
{% endif %}
{% endfor %}
</optgroup>
{% endif %}
<optgroup label="{% translate 'Tous les produits du catalogue' %}">
{% for p in products %}
{% if p.id != item.matched_product_id %}
<option value="{{ p.id }}">{{ p.code }} — {{ p.name }}</option>
{% endif %}
{% endfor %}
</optgroup>
</select>
<div class="form-text small text-danger mt-1 {% if item.matched_product_id %}d-none{% endif %}" id="item_{{ idx }}_help">
<i class="bi bi-exclamation-circle me-1"></i>{% translate "Produit requis pour commander cette ligne." %}
</div>
</td>
<!-- Quantité -->
<td class="text-end">
<span class="fw-bold">{{ item.quantity }}</span>
<span class="text-muted small d-block">{{ item.raw_unit }}</span>
</td>
<!-- Prix unitaire net HT -->
<td class="text-end">
<span class="fw-semibold">{{ item.unit_price|floatformat:2 }} €</span>
</td>
<!-- Taux TVA -->
<td class="text-end text-muted small">
{{ item.vat_rate|floatformat:0 }}%
</td>
<!-- Total HT -->
<td class="text-end fw-bold text-dark item-total-cell" data-total="{{ item.total_amount }}" data-vat="{{ item.vat_rate }}">
{{ item.total_amount|floatformat:2 }} €
</td>
<!-- Emplacement de stock -->
<td>
<select name="item_{{ idx }}_location" class="form-select form-select-sm">
{% for loc in locations %}
<option value="{{ loc.id }}" {% if loc.id == default_location.id %}selected{% endif %}>
{{ loc.name }} {% if loc.is_default %}(par défaut){% endif %}
</option>
{% empty %}
<option value="">{% translate "Aucun emplacement" %}</option>
{% endfor %}
</select>
</td>
</tr>
{% endwith %}
{% endfor %}
</tbody>
</table>
</div>
<!-- Totaux récapitulatifs & Validation -->
<div class="card-footer bg-light py-3">
<div class="row align-items-center">
<div class="col-md-6 mb-3 mb-md-0">
<div class="d-flex align-items-center gap-3">
<div>
<span class="text-muted small d-block">{% translate "Total HTVA :" %}</span>
<span class="h5 mb-0 fw-bold" id="computed-total-ht">0.00 €</span>
</div>
<div class="border-start ps-3">
<span class="text-muted small d-block">{% translate "TVA calculée :" %}</span>
<span class="h6 mb-0 text-muted" id="computed-total-tva">0.00 €</span>
</div>
<div class="border-start ps-3">
<span class="text-muted small d-block">{% translate "Total TTC estimé :" %}</span>
<span class="h5 mb-0 fw-bold text-primary" id="computed-total-ttc">0.00 €</span>
</div>
</div>
</div>
<div class="col-md-6 text-md-end d-flex justify-content-md-end gap-2">
<a href="{% url 'stock:purchase_order_from_pdf' %}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-repeat"></i> {% translate "Recommencer" %}
</a>
<button type="submit" class="btn btn-success btn-lg px-4" id="submit-confirm-btn">
<i class="bi bi-cart-check me-2"></i> {% translate "Créer le bon de commande" %}
</button>
</div>
</div>
</div>
</div>
</form>
{% endif %}
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Zone drag & drop
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('id_pdf_file');
const selectedFileDiv = document.getElementById('selected-file-name');
if (dropZone && fileInput) {
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('border-primary', 'bg-white');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('border-primary', 'bg-white');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('border-primary', 'bg-white');
if (e.dataTransfer.files.length > 0) {
fileInput.files = e.dataTransfer.files;
updateFileDisplay();
}
});
fileInput.addEventListener('change', updateFileDisplay);
function updateFileDisplay() {
if (fileInput.files.length > 0) {
const count = fileInput.files.length;
let totalBytes = 0;
const fileNames = [];
let hasImg = false;
for (let i = 0; i < count; i++) {
const file = fileInput.files[i];
totalBytes += file.size;
fileNames.push(file.name);
if (file.type.startsWith('image/') || /\.(png|jpe?g|webp|bmp|tiff?)$/i.test(file.name)) {
hasImg = true;
}
}
const icon = selectedFileDiv.querySelector('i');
if (icon) {
if (count > 1) {
icon.className = 'bi bi-images me-1 text-primary';
} else {
icon.className = hasImg ? 'bi bi-camera me-1 text-primary' : 'bi bi-file-earmark-pdf me-1 text-danger';
}
}
const totalSizeStr = (totalBytes / 1024 > 1024)
? (totalBytes / (1024 * 1024)).toFixed(2) + ' MB'
: (totalBytes / 1024).toFixed(1) + ' KB';
if (count === 1) {
selectedFileDiv.querySelector('.file-text').textContent = fileNames[0] + ' (' + totalSizeStr + ')';
} else {
selectedFileDiv.querySelector('.file-text').textContent =
count + ' {% translate "fichiers / photos sélectionnés" %} (' + totalSizeStr + ') : ' + fileNames.slice(0, 3).join(', ') + (count > 3 ? '...' : '');
}
selectedFileDiv.classList.remove('d-none');
}
}
}
// Initialisation affichage fournisseur si non identifié
toggleSupplierChoice();
// Calcul initial des totaux
updateTotals();
});
function toggleSupplierChoice() {
const isNew = document.getElementById('sup_choice_new') && document.getElementById('sup_choice_new').checked;
const cardNew = document.getElementById('container_create_supplier');
const cardExisting = document.getElementById('container_existing_supplier');
if (cardNew && cardExisting) {
if (isNew) {
cardNew.classList.remove('opacity-75', 'bg-light-subtle');
cardNew.classList.add('border-primary', 'shadow-sm', 'bg-white');
cardExisting.classList.remove('border-primary', 'shadow-sm', 'bg-white');
cardExisting.classList.add('opacity-75', 'bg-light-subtle');
} else {
cardExisting.classList.remove('opacity-75', 'bg-light-subtle');
cardExisting.classList.add('border-primary', 'shadow-sm', 'bg-white');
cardNew.classList.remove('border-primary', 'shadow-sm', 'bg-white');
cardNew.classList.add('opacity-75', 'bg-light-subtle');
}
}
}
function toggleSelectAllItems(checked) {
document.querySelectorAll('.item-include-cb').forEach(cb => {
cb.checked = checked;
});
updateTotals();
}
function handleProductSelection(idx) {
const select = document.getElementById('item_' + idx + '_product_id');
const cb = document.getElementById('item_' + idx + '_include');
const row = document.getElementById('item-row-' + idx);
const help = document.getElementById('item_' + idx + '_help');
if (select && select.value && select.value.trim() !== '') {
if (cb) cb.checked = true;
select.classList.remove('is-invalid', 'border-warning');
if (row) row.classList.remove('table-warning-subtle');
if (help) help.classList.add('d-none');
} else {
if (cb) cb.checked = false;
select.classList.add('border-warning');
if (row) row.classList.add('table-warning-subtle');
if (help) help.classList.remove('d-none');
}
updateTotals();
}
function validateOrderForm() {
// 1. Validation du fournisseur
const supChoiceNew = document.getElementById('sup_choice_new');
const supChoicePick = document.getElementById('sup_choice_pick');
if (supChoiceNew && supChoiceNew.checked) {
const nameInput = document.getElementById('id_new_supplier_name') || document.querySelector('input[name="new_supplier_name"]');
if (!nameInput || !nameInput.value.trim()) {
{% translate "Veuillez saisir le nom du nouveau fournisseur à créer." as msg_sup_name %}
alert("{{ msg_sup_name|escapejs }}");
if (nameInput) nameInput.focus();
return false;
}
} else if (supChoicePick && supChoicePick.checked) {
const supSelect = document.getElementById('id_supplier_id') || document.querySelector('select[name="supplier_id"]');
if (!supSelect || !supSelect.value.trim()) {
{% translate "Veuillez sélectionner un fournisseur existant dans la liste déroulante." as msg_sup_pick %}
alert("{{ msg_sup_pick|escapejs }}");
if (supSelect) supSelect.focus();
return false;
}
}
// 2. Validation des articles
const includeCbs = document.querySelectorAll('.item-include-cb:checked');
if (includeCbs.length === 0) {
{% translate "Veuillez cocher au moins une ligne d'article à inclure dans le bon de commande." as msg_no_item %}
alert("{{ msg_no_item|escapejs }}");
return false;
}
let unassociatedCount = 0;
let firstInvalid = null;
includeCbs.forEach(cb => {
const idx = cb.dataset.idx;
const select = document.getElementById('item_' + idx + '_product_id');
if (select && (!select.value || select.value.trim() === '')) {
unassociatedCount++;
select.classList.add('is-invalid');
if (!firstInvalid) firstInvalid = select;
}
});
if (unassociatedCount > 0) {
{% translate "Certaines lignes cochées ne sont associées à aucun produit existant. Veuillez sélectionner un produit dans la liste pour chaque ligne cochée, ou décocher les lignes non associées." as msg_unassoc %}
alert("{{ msg_unassoc|escapejs }}");
if (firstInvalid) firstInvalid.focus();
return false;
}
return true;
}
function updateTotals() {
let totalHt = 0;
let totalTva = 0;
document.querySelectorAll('#items-review-table tbody tr').forEach(row => {
const cb = row.querySelector('.item-include-cb');
if (cb && cb.checked) {
const totalCell = row.querySelector('.item-total-cell');
if (totalCell) {
const amount = parseFloat(totalCell.getAttribute('data-total')) || 0;
const vatRate = parseFloat(totalCell.getAttribute('data-vat')) || 21;
totalHt += amount;
totalTva += amount * (vatRate / 100.0);
}
}
});
const htEl = document.getElementById('computed-total-ht');
const tvaEl = document.getElementById('computed-total-tva');
const ttcEl = document.getElementById('computed-total-ttc');
if (htEl) htEl.textContent = totalHt.toFixed(2) + ' €';
if (tvaEl) tvaEl.textContent = totalTva.toFixed(2) + ' €';
if (ttcEl) ttcEl.textContent = (totalHt + totalTva).toFixed(2) + ' €';
}
</script>
{% endblock %}