385 lines
9.8 KiB
Vue
385 lines
9.8 KiB
Vue
<template>
|
|
<div v-if="loading" class="text-center py-5">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else-if="error" class="text-center py-5 text-danger">
|
|
{{ error }}
|
|
</div>
|
|
|
|
<div v-else-if="goodsReceipt" class="container-fluid py-4">
|
|
<div class="odoo-toolbar">
|
|
<div class="statusbar-wrapper">
|
|
<button
|
|
v-for="status in availableStatuses"
|
|
:key="status"
|
|
type="button"
|
|
class="status-step"
|
|
:class="{ active: status === goodsReceipt.status }"
|
|
:disabled="true"
|
|
>
|
|
<i :class="getStatusIcon(status)"></i>
|
|
<span>{{ getStatusLabel(status) }}</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="toolbar-right">
|
|
<soft-button
|
|
color="success"
|
|
variant="outline"
|
|
class="btn-toolbar btn-sm"
|
|
:disabled="!canValidate || isUpdatingStatus"
|
|
@click="handleValidate"
|
|
>
|
|
<i class="fas fa-check me-2"></i>
|
|
Valider réception
|
|
</soft-button>
|
|
|
|
<soft-button
|
|
color="warning"
|
|
variant="outline"
|
|
class="btn-toolbar btn-sm"
|
|
:disabled="!canSetDraft || isUpdatingStatus"
|
|
@click="handleSetDraft"
|
|
>
|
|
<i class="fas fa-undo me-2"></i>
|
|
Remettre en brouillon
|
|
</soft-button>
|
|
|
|
<soft-button
|
|
color="secondary"
|
|
variant="outline"
|
|
class="btn-toolbar btn-sm"
|
|
@click="handleBack"
|
|
>
|
|
<i class="fas fa-arrow-left me-2"></i>
|
|
Retour
|
|
</soft-button>
|
|
|
|
<soft-button
|
|
color="info"
|
|
variant="outline"
|
|
class="btn-toolbar btn-sm"
|
|
@click="handleEdit"
|
|
>
|
|
<i class="fas fa-edit me-2"></i>
|
|
Modifier
|
|
</soft-button>
|
|
|
|
<soft-button
|
|
color="danger"
|
|
variant="outline"
|
|
class="btn-toolbar btn-sm"
|
|
:disabled="isUpdatingStatus"
|
|
@click="handleDelete"
|
|
>
|
|
<i class="fas fa-trash me-2"></i>
|
|
Supprimer
|
|
</soft-button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="commande-detail mt-3">
|
|
<div class="form-section">
|
|
<div class="section-title">
|
|
<i class="fas fa-truck-loading"></i>
|
|
Informations générales
|
|
</div>
|
|
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-md-4">
|
|
<label class="form-label">Numéro réception</label>
|
|
<div class="info-value">{{ goodsReceipt.receipt_number }}</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Date réception</label>
|
|
<div class="info-value">{{ formatDate(goodsReceipt.receipt_date) }}</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Statut</label>
|
|
<div class="status-badge" :class="getStatusClass(goodsReceipt.status)">
|
|
<i :class="getStatusIcon(goodsReceipt.status)"></i>
|
|
{{ getStatusLabel(goodsReceipt.status) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Commande fournisseur</label>
|
|
<div class="info-value">
|
|
{{ goodsReceipt.purchase_order?.po_number || goodsReceipt.purchase_order_id }}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Entrepôt</label>
|
|
<div class="info-value">{{ goodsReceipt.warehouse?.name || "-" }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<div class="section-title">
|
|
<i class="fas fa-boxes"></i>
|
|
Lignes de réception
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-flush">
|
|
<thead class="thead-light">
|
|
<tr>
|
|
<th>Produit</th>
|
|
<th>Conditionnement</th>
|
|
<th>Colis</th>
|
|
<th>Unités</th>
|
|
<th>Prix Unitaire</th>
|
|
<th>TVA</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="line in receiptLines" :key="line.id">
|
|
<td>
|
|
<div class="d-flex flex-column">
|
|
<span class="text-sm fw-bold">{{ line.product?.nom || `Produit #${line.product_id}` }}</span>
|
|
<span class="text-xs text-secondary">{{ line.product?.reference || "-" }}</span>
|
|
</div>
|
|
</td>
|
|
<td class="text-sm">{{ line.packaging?.name || "Unité" }}</td>
|
|
<td class="text-sm">{{ line.packages_qty_received || "-" }}</td>
|
|
<td class="text-sm">{{ line.units_qty_received || "-" }}</td>
|
|
<td class="text-sm">{{ line.unit_price ? formatCurrency(line.unit_price) : "-" }}</td>
|
|
<td class="text-sm">
|
|
{{ line.tva_rate ? `${line.tva_rate.name} (${line.tva_rate.rate}%)` : "-" }}
|
|
</td>
|
|
</tr>
|
|
<tr v-if="receiptLines.length === 0">
|
|
<td colspan="6" class="text-center text-muted py-4">
|
|
Aucune ligne dans cette réception.
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, onMounted, ref } from "vue";
|
|
import { useRoute, useRouter } from "vue-router";
|
|
import { storeToRefs } from "pinia";
|
|
import SoftButton from "@/components/SoftButton.vue";
|
|
import { useGoodsReceiptStore } from "@/stores/goodsReceiptStore";
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const goodsReceiptStore = useGoodsReceiptStore();
|
|
|
|
const { currentGoodsReceipt: goodsReceipt, loading, error } = storeToRefs(goodsReceiptStore);
|
|
|
|
const isUpdatingStatus = ref(false);
|
|
|
|
const availableStatuses = ["draft", "posted"];
|
|
|
|
const canValidate = computed(() => goodsReceipt.value?.status === "draft");
|
|
const canSetDraft = computed(() => goodsReceipt.value?.status === "posted");
|
|
const receiptLines = computed(() => {
|
|
const lines = goodsReceipt.value?.lines;
|
|
if (Array.isArray(lines)) {
|
|
return lines;
|
|
}
|
|
if (lines && Array.isArray(lines.data)) {
|
|
return lines.data;
|
|
}
|
|
return [];
|
|
});
|
|
|
|
const formatDate = (dateString) => {
|
|
if (!dateString) return "-";
|
|
return new Date(dateString).toLocaleDateString("fr-FR", {
|
|
day: "numeric",
|
|
month: "long",
|
|
year: "numeric",
|
|
});
|
|
};
|
|
|
|
const formatCurrency = (value) => {
|
|
return new Intl.NumberFormat("fr-FR", {
|
|
style: "currency",
|
|
currency: "EUR",
|
|
}).format(value || 0);
|
|
};
|
|
|
|
const getStatusLabel = (status) => {
|
|
const labels = {
|
|
draft: "Brouillon",
|
|
posted: "Validée",
|
|
};
|
|
return labels[status] || status;
|
|
};
|
|
|
|
const getStatusIcon = (status) => {
|
|
const icons = {
|
|
draft: "fas fa-file-alt",
|
|
posted: "fas fa-check-circle",
|
|
};
|
|
return icons[status] || "fas fa-question-circle";
|
|
};
|
|
|
|
const getStatusClass = (status) => {
|
|
const classes = {
|
|
draft: "status-draft",
|
|
posted: "status-confirmed",
|
|
};
|
|
return classes[status] || "";
|
|
};
|
|
|
|
const changeStatus = async (newStatus) => {
|
|
if (!goodsReceipt.value || goodsReceipt.value.status === newStatus) return;
|
|
|
|
try {
|
|
isUpdatingStatus.value = true;
|
|
await goodsReceiptStore.updateGoodsReceipt({
|
|
id: goodsReceipt.value.id,
|
|
status: newStatus,
|
|
});
|
|
await goodsReceiptStore.fetchGoodsReceipt(parseInt(route.params.id));
|
|
} catch (e) {
|
|
console.error("Failed to update receipt status", e);
|
|
} finally {
|
|
isUpdatingStatus.value = false;
|
|
}
|
|
};
|
|
|
|
const handleValidate = async () => {
|
|
await changeStatus("posted");
|
|
};
|
|
|
|
const handleSetDraft = async () => {
|
|
await changeStatus("draft");
|
|
};
|
|
|
|
const handleBack = () => {
|
|
router.push("/stock/receptions");
|
|
};
|
|
|
|
const handleEdit = () => {
|
|
router.push(`/stock/receptions/${route.params.id}/edit`);
|
|
};
|
|
|
|
const handleDelete = async () => {
|
|
if (!confirm("Êtes-vous sûr de vouloir supprimer cette réception de marchandise ?")) {
|
|
return;
|
|
}
|
|
try {
|
|
await goodsReceiptStore.deleteGoodsReceipt(parseInt(route.params.id));
|
|
router.push("/stock/receptions");
|
|
} catch (e) {
|
|
console.error("Failed to delete goods receipt", e);
|
|
}
|
|
};
|
|
|
|
onMounted(async () => {
|
|
await goodsReceiptStore.fetchGoodsReceipt(parseInt(route.params.id));
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.odoo-toolbar {
|
|
background: #fff;
|
|
border: 1px solid #e9ecef;
|
|
border-radius: 12px;
|
|
padding: 1rem;
|
|
margin-bottom: 1.25rem;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.statusbar-wrapper {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.status-step {
|
|
border: 1px solid #dfe3e8;
|
|
background: #f8f9fa;
|
|
color: #67748e;
|
|
border-radius: 10px;
|
|
padding: 0.45rem 0.75rem;
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.45rem;
|
|
}
|
|
|
|
.status-step.active {
|
|
background: #2dce89;
|
|
border-color: #2dce89;
|
|
color: #fff;
|
|
}
|
|
|
|
.toolbar-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
flex-wrap: wrap;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.commande-detail {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.form-section {
|
|
background: #fff;
|
|
border: 1px solid #e9ecef;
|
|
border-radius: 12px;
|
|
padding: 1.25rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.section-title {
|
|
font-weight: 700;
|
|
color: #344767;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.info-value {
|
|
font-weight: 600;
|
|
color: #344767;
|
|
}
|
|
|
|
.status-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.4rem;
|
|
border-radius: 999px;
|
|
padding: 0.25rem 0.65rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.status-draft {
|
|
background: #e9ecef;
|
|
color: #495057;
|
|
}
|
|
|
|
.status-confirmed {
|
|
background: #d1f7e3;
|
|
color: #0a7a43;
|
|
}
|
|
</style>
|