Sarobidy22 3 weeks ago
parent
commit
778e8332ee
  1. 29
      app/Config/Routes.php
  2. 106
      app/Controllers/AvanceController.php
  3. 450
      app/Controllers/OrderController.php
  4. 281
      app/Models/Avance.php
  5. 25
      app/Models/Orders.php
  6. 113
      app/Models/Products.php
  7. 27
      app/Models/Remise.php
  8. 151
      app/Views/orders/edit.php
  9. 201
      app/Views/orders/index.php

29
app/Config/Routes.php

@ -174,12 +174,12 @@ $routes->group('', ['filter' => 'auth'], function ($routes) {
$routes->get('update/(:num)', [ProductCOntroller::class, 'update']); $routes->get('update/(:num)', [ProductCOntroller::class, 'update']);
$routes->post('update/(:num)', [ProductCOntroller::class, 'update']); $routes->post('update/(:num)', [ProductCOntroller::class, 'update']);
$routes->post('remove', [ProductCOntroller::class, 'remove']); $routes->post('remove', [ProductCOntroller::class, 'remove']);
// $routes->get('generateqrcode/(:num)', [QrCodeController::class, 'generate']);
$routes->post('assign_store', [ProductCOntroller::class, 'assign_store']); $routes->post('assign_store', [ProductCOntroller::class, 'assign_store']);
$routes->post('createByExcel', [ProductCOntroller::class, 'createByExcel']); $routes->post('createByExcel', [ProductCOntroller::class, 'createByExcel']);
$routes->post('checkProductAvailability', [ProductCOntroller::class, 'checkProductAvailability']);
}); });
/** /**
* route for the orders * route for the orders
*/ */
@ -296,10 +296,12 @@ $routes->group('/sortieCaisse', function ($routes) {
}); });
// avance // avance
// ✅ DANS app/Config/Routes.php
$routes->group('/avances', function ($routes) { $routes->group('/avances', function ($routes) {
$routes->get('/', [AvanceController::class, 'index']); $routes->get('/', [AvanceController::class, 'index']);
// Routes pour récupérer les données (GET) // Routes pour récupérer les données (GET)
$routes->get('fetchAvanceData', [AvanceController::class, 'fetchAvanceData']); $routes->get('fetchAvanceData', [AvanceController::class, 'fetchAvanceData']);
$routes->get('fetchAvanceBecameOrder', [AvanceController::class, 'fetchAvanceBecameOrder']); $routes->get('fetchAvanceBecameOrder', [AvanceController::class, 'fetchAvanceBecameOrder']);
$routes->get('fetchExpiredAvance', [AvanceController::class, 'fetchExpiredAvance']); $routes->get('fetchExpiredAvance', [AvanceController::class, 'fetchExpiredAvance']);
@ -310,22 +312,27 @@ $routes->group('/avances', function ($routes) {
$routes->get('getFullInvoiceForPrint/(:num)', [AvanceController::class, 'getFullInvoiceForPrint/$1']); $routes->get('getFullInvoiceForPrint/(:num)', [AvanceController::class, 'getFullInvoiceForPrint/$1']);
$routes->get('printInvoice/(:num)', [AvanceController::class, 'printInvoice/$1']); $routes->get('printInvoice/(:num)', [AvanceController::class, 'printInvoice/$1']);
// Routes POST pour modifications // Routes POST pour modifications
$routes->post('createAvance', [AvanceController::class, 'createAvance']); $routes->post('createAvance', [AvanceController::class, 'createAvance']);
$routes->post('updateAvance', [AvanceController::class, 'updateAvance']); $routes->post('updateAvance', [AvanceController::class, 'updateAvance']);
$routes->post('deleteAvance', [AvanceController::class, 'removeAvance']); $routes->post('deleteAvance', [AvanceController::class, 'removeAvance']);
$routes->post('notifyPrintInvoice', [AvanceController::class, 'notifyPrintInvoice']); $routes->post('notifyPrintInvoice', [AvanceController::class, 'notifyPrintInvoice']);
// ✅ AJOUTER CETTE ROUTE MANQUANTE
$routes->post('processExpiredAvances', [AvanceController::class, 'processExpiredAvances']); $routes->post('processExpiredAvances', [AvanceController::class, 'processExpiredAvances']);
// ✅ Route CRON (optionnel) // ✅ CORRECTION : Routes pour paiement et conversion
$routes->get('checkDeadlineAlerts', [AvanceController::class, 'checkDeadlineAlerts']); $routes->post('payAvance', [AvanceController::class, 'payAvance']);
$routes->post('payAvance', 'AvanceController::payAvance'); // ✅ AJOUT : Routes GET ET POST pour la conversion manuelle
$routes->get('forceConvertToOrder/(:num)', 'AvanceController::forceConvertToOrder/$1'); $routes->get('checkAndConvertCompleted', [AvanceController::class, 'checkAndConvertCompleted']);
$routes->post('checkAndConvertCompleted', 'AvanceController::checkAndConvertCompleted'); $routes->post('checkAndConvertCompleted', [AvanceController::class, 'checkAndConvertCompleted']);
// Route pour forcer la conversion d'une avance spécifique
$routes->get('forceConvertToOrder/(:num)', [AvanceController::class, 'forceConvertToOrder/$1']);
// Route CRON (optionnel)
$routes->get('checkDeadlineAlerts', [AvanceController::class, 'checkDeadlineAlerts']);
}); });
// historique // historique
$routes->group('historique', ['filter' => 'auth'], static function ($routes) { $routes->group('historique', ['filter' => 'auth'], static function ($routes) {
$routes->get('/', 'HistoriqueController::index'); $routes->get('/', 'HistoriqueController::index');

106
app/Controllers/AvanceController.php

@ -2114,6 +2114,13 @@ public function payAvance()
$avance_id = $this->request->getPost('avance_id'); $avance_id = $this->request->getPost('avance_id');
$montant_paye = (float)$this->request->getPost('montant_paye'); $montant_paye = (float)$this->request->getPost('montant_paye');
if (!$avance_id || $montant_paye <= 0) {
return $this->response->setJSON([
'success' => false,
'message' => 'Données invalides'
]);
}
$avanceModel = new \App\Models\Avance(); $avanceModel = new \App\Models\Avance();
$avance = $avanceModel->find($avance_id); $avance = $avanceModel->find($avance_id);
@ -2124,52 +2131,62 @@ public function payAvance()
]); ]);
} }
// Calcul du nouveau montant dû // ✅ Vérifier si déjà convertie
if ($avance['is_order'] == 1) {
return $this->response->setJSON([
'success' => false,
'message' => '⚠️ Cette avance a déjà été convertie en commande'
]);
}
// ✅ Calcul nouveau montant dû
$amount_due = max(0, (float)$avance['amount_due'] - $montant_paye); $amount_due = max(0, (float)$avance['amount_due'] - $montant_paye);
// ✅ Mise à jour de l'avance // ✅ Mise à jour avance
$avanceModel->update($avance_id, [ $avanceModel->update($avance_id, [
'avance_amount' => (float)$avance['avance_amount'] + $montant_paye, 'avance_amount' => (float)$avance['avance_amount'] + $montant_paye,
'amount_due' => $amount_due, 'amount_due' => $amount_due,
]); ]);
// ✅ NOUVEAU : Conversion automatique UNIQUEMENT pour avances TERRE log_message('info', "💰 Paiement {$montant_paye} Ar sur avance {$avance_id} (Type: {$avance['type_avance']})");
// ✅ CONVERSION si paiement complet
if ($amount_due <= 0) { if ($amount_due <= 0) {
if ($avance['type_avance'] === 'terre') { if ($avance['type_avance'] === 'terre') {
// ✅ Avance TERRE complète → Conversion en commande log_message('info', "🔄 Avance TERRE {$avance_id} complétée → Conversion en commande...");
log_message('info', "💰 Avance TERRE {$avance_id} complétée ! Conversion en commande...");
$order_id = $avanceModel->convertToOrder($avance_id); $order_id = $avanceModel->convertToOrder($avance_id);
if ($order_id) { if ($order_id) {
log_message('info', "✅ Conversion réussie → Commande {$order_id}");
return $this->response->setJSON([ return $this->response->setJSON([
'success' => true, 'success' => true,
'message' => '✅ Paiement effectué ! L\'avance TERRE a été convertie en commande.', 'message' => '✅ Paiement effectué ! L\'avance a été convertie en commande.',
'converted' => true, 'converted' => true,
'type' => 'terre', 'type' => 'terre',
'order_id' => $order_id, 'order_id' => $order_id,
'redirect_url' => site_url('orders/update/' . $order_id) 'redirect_url' => site_url('orders/update/' . $order_id)
]); ]);
} else { } else {
log_message('error', "Échec conversion avance TERRE {$avance_id} en commande"); log_message('error', "❌ Échec conversion avance {$avance_id}");
return $this->response->setJSON([ return $this->response->setJSON([
'success' => true, 'success' => false,
'message' => '⚠️ Paiement effectué mais erreur lors de la création de la commande.', 'message' => '⚠️ Paiement enregistré mais erreur lors de la conversion. Contactez l\'administrateur.'
'converted' => false
]); ]);
} }
} else { } else {
// ✅ Avance MER complète → Reste dans la liste // ✅ Avance MER complète
log_message('info', "💰 Avance MER {$avance_id} complétée ! Elle reste dans la liste des avances."); log_message('info', "✅ Avance MER {$avance_id} complétée (pas de conversion)");
return $this->response->setJSON([ return $this->response->setJSON([
'success' => true, 'success' => true,
'message' => '✅ Paiement effectué ! L\'avance MER est maintenant complète.', 'message' => '✅ Paiement effectué ! L\'avance MER est maintenant complète.',
'converted' => false, 'converted' => false,
'type' => 'mere', 'type' => 'mere'
'status' => 'completed'
]); ]);
} }
} }
@ -2177,8 +2194,9 @@ public function payAvance()
// ✅ Paiement partiel // ✅ Paiement partiel
return $this->response->setJSON([ return $this->response->setJSON([
'success' => true, 'success' => true,
'message' => '✅ Paiement partiel enregistré avec succès', 'message' => '✅ Paiement partiel enregistré',
'amount_due_remaining' => $amount_due, 'amount_due_remaining' => $amount_due,
'amount_due_formatted' => number_format($amount_due, 0, ',', ' ') . ' Ar',
'type' => $avance['type_avance'] 'type' => $avance['type_avance']
]); ]);
} }
@ -2187,16 +2205,33 @@ public function payAvance()
*/ */
public function forceConvertToOrder($avance_id) public function forceConvertToOrder($avance_id)
{ {
$this->verifyRole('updateAvance'); // Adapter selon vos permissions $this->verifyRole('updateAvance');
$avanceModel = new \App\Models\Avance(); $avanceModel = new \App\Models\Avance();
$avance = $avanceModel->find($avance_id);
if (!$avance) {
session()->setFlashdata('errors', 'Avance introuvable');
return redirect()->back();
}
if ($avance['type_avance'] !== 'terre') {
session()->setFlashdata('errors', 'Seules les avances TERRE peuvent être converties');
return redirect()->back();
}
if ($avance['amount_due'] > 0) {
session()->setFlashdata('errors', 'L\'avance doit être complètement payée avant conversion');
return redirect()->back();
}
$order_id = $avanceModel->convertToOrder($avance_id); $order_id = $avanceModel->convertToOrder($avance_id);
if ($order_id) { if ($order_id) {
session()->setFlashdata('success', 'Avance convertie en commande avec succès !'); session()->setFlashdata('success', 'Avance convertie en commande avec succès !');
return redirect()->to('orders/update/' . $order_id); return redirect()->to('orders/update/' . $order_id);
} else { } else {
session()->setFlashdata('errors', 'Erreur lors de la conversion de l\'avance.'); session()->setFlashdata('errors', 'Erreur lors de la conversion');
return redirect()->back(); return redirect()->back();
} }
} }
@ -2208,16 +2243,33 @@ public function forceConvertToOrder($avance_id)
public function checkAndConvertCompleted() public function checkAndConvertCompleted()
{ {
try { try {
$Avance = new Avance(); log_message('info', "=== DÉBUT checkAndConvertCompleted (manuel) ===");
// ✅ Récupérer uniquement les avances TERRE complètes non converties $Avance = new \App\Models\Avance();
$completedTerreAvances = $Avance->getCompletedNotConverted();
// ✅ Récupérer toutes les avances TERRE complètes non converties
$completedTerreAvances = $Avance
->where('type_avance', 'terre')
->where('amount_due <=', 0)
->where('is_order', 0)
->where('active', 1)
->findAll();
if (empty($completedTerreAvances)) {
return $this->response->setJSON([
'success' => true,
'message' => 'Aucune avance complète à convertir',
'converted' => 0
]);
}
$convertedCount = 0; $convertedCount = 0;
$errorCount = 0; $errorCount = 0;
$details = []; $details = [];
foreach ($completedTerreAvances as $avance) { foreach ($completedTerreAvances as $avance) {
log_message('info', "🔍 Traitement avance {$avance['avance_id']}...");
$order_id = $Avance->convertToOrder($avance['avance_id']); $order_id = $Avance->convertToOrder($avance['avance_id']);
if ($order_id) { if ($order_id) {
@ -2225,32 +2277,35 @@ public function checkAndConvertCompleted()
$details[] = [ $details[] = [
'avance_id' => $avance['avance_id'], 'avance_id' => $avance['avance_id'],
'customer' => $avance['customer_name'], 'customer' => $avance['customer_name'],
'type' => 'terre',
'order_id' => $order_id, 'order_id' => $order_id,
'status' => 'success' 'status' => 'success'
]; ];
log_message('info', "✅ Avance {$avance['avance_id']} → Commande {$order_id}");
} else { } else {
$errorCount++; $errorCount++;
$details[] = [ $details[] = [
'avance_id' => $avance['avance_id'], 'avance_id' => $avance['avance_id'],
'customer' => $avance['customer_name'], 'customer' => $avance['customer_name'],
'type' => 'terre', 'status' => 'error',
'status' => 'error' 'reason' => 'Voir logs pour détails'
]; ];
log_message('error', "❌ Échec conversion avance {$avance['avance_id']}");
} }
} }
log_message('info', "=== FIN checkAndConvertCompleted - Convertis: {$convertedCount}, Erreurs: {$errorCount} ===");
return $this->response->setJSON([ return $this->response->setJSON([
'success' => true, 'success' => true,
'message' => 'Vérification terminée', 'message' => 'Vérification terminée',
'converted' => $convertedCount, 'converted' => $convertedCount,
'errors' => $errorCount, 'errors' => $errorCount,
'note' => 'Seules les avances TERRE sont converties. Les avances MER restent dans la liste.', 'total_checked' => count($completedTerreAvances),
'details' => $details 'details' => $details
]); ]);
} catch (\Exception $e) { } catch (\Exception $e) {
log_message('error', "Erreur checkAndConvertCompleted: " . $e->getMessage()); log_message('error', "Erreur checkAndConvertCompleted: " . $e->getMessage());
return $this->response->setJSON([ return $this->response->setJSON([
'success' => false, 'success' => false,
'messages' => 'Erreur: ' . $e->getMessage() 'messages' => 'Erreur: ' . $e->getMessage()
@ -2258,4 +2313,5 @@ public function checkAndConvertCompleted()
} }
} }
} }

450
app/Controllers/OrderController.php

@ -41,26 +41,46 @@ class OrderController extends AdminController
* @param int $store_id * @param int $store_id
* @return string * @return string
*/ */
private function generateBillNo(int $store_id): string private function generateSimpleSequentialBillNo(int $store_id): string
{ {
// Mapping des préfixes par magasin
$storePrefixes = [ $storePrefixes = [
1 => 'ANTS', // ANTSAKAVIRO 1 => 'ANTS',
2 => 'BESA', // BESARETY 2 => 'BESA',
3 => 'BYPA', // BYPASS 3 => 'BYPA',
4 => 'TOAM', // TOAMASINA 4 => 'TOAM',
]; ];
// Récupérer le préfixe du magasin, ou utiliser un préfixe par défaut $prefix = $storePrefixes[$store_id] ?? 'STORE';
$prefix = $storePrefixes[$store_id] ?? 'BILPR';
// Générer un identifiant unique $db = \Config\Database::connect();
$uniqueId = strtoupper(substr(md5(uniqid(mt_rand(), true)), 0, 6));
// Retourner le numéro de facture formaté $lastBill = $db->table('orders')
return $prefix . '-' . $uniqueId; ->select('bill_no')
->like('bill_no', $prefix . '-', 'after')
->orderBy('id', 'DESC')
->limit(1)
->get()
->getRowArray();
if ($lastBill && !empty($lastBill['bill_no'])) {
// Extraire le numéro (ex: "BESA-001" -> 1)
preg_match('/-(\d+)$/', $lastBill['bill_no'], $matches);
if (isset($matches[1])) {
$lastNumber = (int)$matches[1];
$newNumber = $lastNumber + 1;
} else {
$newNumber = 1;
}
} else {
$newNumber = 1;
}
// Formater avec zéros (ex: 001, 002, 010, 100)
return $prefix . '-' . str_pad($newNumber, 3, '0', STR_PAD_LEFT);
} }
public function fetchOrdersData() public function fetchOrdersData()
{ {
helper(['url', 'form']); helper(['url', 'form']);
@ -75,43 +95,92 @@ class OrderController extends AdminController
// POUR CAISSIÈRE // POUR CAISSIÈRE
// ======================================== // ========================================
if ($users['group_name'] == "Caissière") { if ($users['group_name'] == "Caissière") {
$Remise = new Remise();
foreach ($data as $key => $value) { foreach ($data as $key => $value) {
$date_time = date('d-m-Y h:i a', strtotime($value['date_time'])); $date_time = date('d-m-Y h:i a', strtotime($value['date_time']));
$buttons = ''; $buttons = '';
$discount = (float)$value['discount'];
// Bouton imprimer (sauf pour SECURITE) // ✅ VÉRIFICATION : Si la commande est refusée (paid_status = 0), aucun bouton
if (in_array('viewOrder', $this->permission) && $users['group_name'] != "SECURITE" && $users['group_name'] != "COMMERCIALE") { if ($value['paid_status'] == 0) {
$buttons .= '<a target="_blank" href="' . site_url('orders/printDiv/' . $value['id']) . '" class="btn btn-default"><i class="fa fa-print"></i></a>'; $buttons = '<span class="label label-danger"><i class="fa fa-ban"></i> Accès bloqué</span>';
} } else {
// ✅ Bouton imprimer
if (in_array('viewOrder', $this->permission)) {
// CAS 1 : Commande payée (1) ou livrée (3) → Toujours afficher imprimer
if (in_array($value['paid_status'], [1, 3])) {
$buttons .= '<a target="_blank" href="' . site_url('orders/printDiv/' . $value['id']) . '" class="btn btn-default"><i class="fa fa-print"></i></a>';
}
// CAS 2 : Commande en attente (2) SANS remise → Afficher imprimer
elseif ($value['paid_status'] == 2 && $discount == 0) {
$buttons .= '<a target="_blank" href="' . site_url('orders/printDiv/' . $value['id']) . '" class="btn btn-default"><i class="fa fa-print"></i></a>';
}
// CAS 3 : Commande en attente (2) AVEC remise validée → Afficher imprimer
elseif ($value['paid_status'] == 2 && $Remise->hasRemiseValidatedForOrder($value['id'])) {
$buttons .= '<a target="_blank" href="' . site_url('orders/printDiv/' . $value['id']) . '" class="btn btn-default"><i class="fa fa-print"></i></a>';
}
// CAS 4 : Commande en attente (2) AVEC remise en attente → Indicateur
elseif ($value['paid_status'] == 2 && $Remise->hasRemisePendingForOrder($value['id'])) {
$buttons .= '<button class="btn btn-warning btn-sm" disabled title="En attente de validation"><i class="fa fa-clock-o"></i></button>';
}
}
// Bouton voir // ✅ Bouton voir
if (in_array('viewOrder', $this->permission)) { if (in_array('viewOrder', $this->permission)) {
$buttons .= ' // Afficher pour toutes les commandes sauf celles refusées
<a // Et pour les commandes en attente : seulement si pas de remise OU remise validée
href="#" if ($value['paid_status'] == 2) {
data-order-id="' . $value['id'] . '" // En attente : vérifier la remise
class="btn btn-default btn-view" if ($discount == 0 || $Remise->hasRemiseValidatedForOrder($value['id'])) {
data-toggle="tooltip" $buttons .= '
title="Voir"> <a
<i class="fa fa-eye"></i> href="#"
</a>'; data-order-id="' . $value['id'] . '"
} class="btn btn-default btn-view"
data-toggle="tooltip"
title="Voir">
<i class="fa fa-eye"></i>
</a>';
}
} else {
// Payé ou Livré : toujours afficher
$buttons .= '
<a
href="#"
data-order-id="' . $value['id'] . '"
class="btn btn-default btn-view"
data-toggle="tooltip"
title="Voir">
<i class="fa fa-eye"></i>
</a>';
}
}
// ✅ Bouton modifier pour statuts 0 (Refusé) et 2 (En Attente) // ✅ Bouton modifier (seulement si paid_status = 2)
if (in_array('updateOrder', $this->permission) if (in_array('updateOrder', $this->permission)
&& $users["store_id"] == $value['store_id'] && $users["store_id"] == $value['store_id']
&& in_array($value['paid_status'], [0, 2])) { && $value['paid_status'] == 2) {
$buttons .= ' <a href="' . site_url('orders/update/' . $value['id']) . '" class="btn btn-primary"><i class="fa fa-pencil"></i></a>';
// CAS 1 : Pas de remise → Afficher le bouton modifier
if ($discount == 0) {
$buttons .= ' <a href="' . site_url('orders/update/' . $value['id']) . '" class="btn btn-primary"><i class="fa fa-pencil"></i></a>';
}
// CAS 2 : Remise validée → Afficher le bouton modifier
elseif ($Remise->hasRemiseValidatedForOrder($value['id'])) {
$buttons .= ' <a href="' . site_url('orders/update/' . $value['id']) . '" class="btn btn-primary"><i class="fa fa-pencil"></i></a>';
}
}
} }
// Statut de paiement // Statut de paiement
if ($value['paid_status'] == 1) { if ($value['paid_status'] == 1) {
$paid_status = '<span class="label label-success">payé</span>'; $paid_status = '<span class="label label-success">Payé</span>';
} elseif ($value['paid_status'] == 2) { } elseif ($value['paid_status'] == 2) {
$paid_status = '<span class="label label-warning">En Attente</span>'; $paid_status = '<span class="label label-warning">En Attente</span>';
} elseif ($value['paid_status'] == 3) { } elseif ($value['paid_status'] == 3) {
$paid_status = '<span class="label label-info">payé et Livré</span>'; $paid_status = '<span class="label label-info">Payé et Livré</span>';
} else { } else {
$paid_status = '<span class="label label-danger">Refusé</span>'; $paid_status = '<span class="label label-danger">Refusé</span>';
} }
@ -213,7 +282,7 @@ class OrderController extends AdminController
} }
// ======================================== // ========================================
// POUR LES AUTRES UTILISATEURS (COMMERCIALE, SECURITE, etc.) // POUR LES AUTRES UTILISATEURS (COMMERCIALE, SECURITE, Cheffe d'Agence)
// ======================================== // ========================================
else { else {
foreach ($data as $key => $value) { foreach ($data as $key => $value) {
@ -281,20 +350,58 @@ class OrderController extends AdminController
} }
} }
$result['data'][$key] = [ // ✅ CONDITION SPÉCIALE POUR SECURITE : remplacer Prix demandé et Prix de vente par Marque et Désignation
$value['product_names'], if ($users['group_name'] == "SECURITE") {
$value['user_name'], // Récupérer les infos produit
$date_time . " <br >" . $statuDate, $OrderItems = new OrderItems();
number_format((int) $value['discount'], 0, ',', ' '), $Products = new Products();
number_format((int) $value['gross_amount'], 0, ',', ' '), $Brands = new Brands();
$paid_status,
$buttons $order_items = $OrderItems->getOrdersItemData($value['id']);
];
$marque = 'N/A';
$numero_serie = 'N/A';
if (!empty($order_items[0])) {
$product = $Products->getProductData($order_items[0]['product_id']);
if ($product) {
$numero_serie = $product['sku'] ?? 'N/A'; // ✅ Numéro de série
if (!empty($product['marque'])) {
$brand = $Brands->find($product['marque']);
if ($brand) {
$marque = $brand['name'];
}
}
}
}
$result['data'][$key] = [
$value['product_names'],
$value['user_name'],
$date_time . " <br >" . $statuDate,
$marque, // ✅ Remplace Prix demandé
$numero_serie, // ✅ Remplace Prix de vente
$paid_status,
$buttons
];
} else {
// Pour les autres (COMMERCIALE, Cheffe d'Agence)
$result['data'][$key] = [
$value['product_names'],
$value['user_name'],
$date_time . " <br >" . $statuDate,
number_format((int) $value['discount'], 0, ',', ' '),
number_format((int) $value['gross_amount'], 0, ',', ' '),
$paid_status,
$buttons
];
}
} }
return $this->response->setJSON($result); return $this->response->setJSON($result);
} }
} }
/** /**
* Affiche le formulaire create avec les données d'une commande existante * Affiche le formulaire create avec les données d'une commande existante
* Pour le rôle COMMERCIALE * Pour le rôle COMMERCIALE
@ -369,7 +476,11 @@ public function create()
$users = $session->get('user'); $users = $session->get('user');
$user_id = $users['id']; $user_id = $users['id'];
$bill_no = $this->generateBillNo($users['store_id']); // Générer le numéro séquentiel
$bill_no = $this->generateSimpleSequentialBillNo($users['store_id']);
// Récupérer le type de document
$document_type = $this->request->getPost('document_type') ?? 'facture';
$posts = $this->request->getPost('product[]'); $posts = $this->request->getPost('product[]');
$rates = $this->request->getPost('rate_value[]'); $rates = $this->request->getPost('rate_value[]');
@ -377,6 +488,8 @@ public function create()
$puissances = $this->request->getPost('puissance[]'); $puissances = $this->request->getPost('puissance[]');
$discount = (float)$this->request->getPost('discount') ?? 0; $discount = (float)$this->request->getPost('discount') ?? 0;
$gross_amount = $this->calculGross($amounts); $gross_amount = $this->calculGross($amounts);
$net_amount = $gross_amount - $discount;
// Vérification prix minimal SI rabais existe // Vérification prix minimal SI rabais existe
if ($discount > 0) { if ($discount > 0) {
@ -405,19 +518,32 @@ public function create()
} }
} }
$montant_a_payer = ($discount > 0) ? $discount : $gross_amount; $discount = (float)$this->request->getPost('discount') ?? 0;
$gross_amount = $this->calculGross($amounts);
$net_amount = $gross_amount - $discount;
$tranche_1 = (float)$this->request->getPost('tranche_1') ?? 0; $tranche_1 = (float)$this->request->getPost('tranche_1') ?? 0;
$tranche_2 = (float)$this->request->getPost('tranche_2') ?? 0; $tranche_2 = (float)$this->request->getPost('tranche_2') ?? 0;
// Si des tranches sont définies, vérifier la cohérence
if ($tranche_1 > 0 && $tranche_2 > 0) { if ($tranche_1 > 0 && $tranche_2 > 0) {
$net_amount = $tranche_1 + $tranche_2; $total_tranches = $tranche_1 + $tranche_2;
} else { // S'assurer que les tranches correspondent au net_amount
$net_amount = $montant_a_payer; if (abs($total_tranches - $net_amount) > 0.01) {
return redirect()->back()
->withInput()
->with('errors', [
'Les tranches de paiement ne correspondent pas au montant total (' .
number_format($net_amount, 0, ',', ' ') . ' Ar)'
]);
}
} }
$data = [ $data = [
'bill_no' => $bill_no, 'bill_no' => $bill_no,
'document_type' => $document_type,
'customer_name' => $this->request->getPost('customer_name'), 'customer_name' => $this->request->getPost('customer_name'),
'customer_address' => $this->request->getPost('customer_address'), 'customer_address' => $this->request->getPost('customer_address'),
'customer_phone' => $this->request->getPost('customer_phone'), 'customer_phone' => $this->request->getPost('customer_phone'),
@ -446,8 +572,15 @@ public function create()
$order_id = $Orders->create($data, $posts); $order_id = $Orders->create($data, $posts);
if ($order_id) { if ($order_id) {
// ✅ NOUVEAU : Marquer immédiatement les produits comme réservés
$productModel = new Products();
foreach ($posts as $product_id) {
$productModel->update($product_id, ['product_sold' => 1]);
}
session()->setFlashdata('success', 'Créé avec succès'); session()->setFlashdata('success', 'Créé avec succès');
$Notification = new NotificationController(); $Notification = new NotificationController();
$Stores = new Stores(); $Stores = new Stores();
@ -856,6 +989,7 @@ public function update(int $id)
} }
$dataUpdate = [ $dataUpdate = [
'document_type' => $this->request->getPost('document_type'), // ✅ AJOUTER CETTE LIGNE
'customer_name' => $this->request->getPost('customer_name'), 'customer_name' => $this->request->getPost('customer_name'),
'customer_address' => $this->request->getPost('customer_address'), 'customer_address' => $this->request->getPost('customer_address'),
'customer_phone' => $this->request->getPost('customer_phone'), 'customer_phone' => $this->request->getPost('customer_phone'),
@ -1510,18 +1644,32 @@ public function update(int $id)
if ($order_id) { if ($order_id) {
$Orders = new Orders(); $Orders = new Orders();
if ($Orders->remove($order_id)) { $OrderItems = new OrderItems();
$Products = new Products();
// ✅ Récupérer tous les produits de la commande
$orderItems = $OrderItems->getOrdersItemData($order_id);
// ✅ Libérer chaque produit (remettre product_sold = 0)
foreach ($orderItems as $item) {
if (!empty($item['product_id'])) {
$Products->update($item['product_id'], ['product_sold' => 0]);
}
}
// Supprimer la commande
if ($Orders->remove($order_id)) {
$response['success'] = true; $response['success'] = true;
$response['messages'] = "Successfully removed"; $response['messages'] = "Successfully removed";
} else { } else {
$response['success'] = false; $response['success'] = false;
$response['messages'] = "Error in the database while removing the product information"; $response['messages'] = "Error in the database while removing the order";
} }
} else { } else {
$response['success'] = false; $response['success'] = false;
$response['messages'] = "Refersh the page again!!"; $response['messages'] = "Refersh the page again!!";
} }
return $this->response->setJSON($response); return $this->response->setJSON($response);
} }
@ -1550,6 +1698,39 @@ public function update(int $id)
return $this->render_template('orders/createbyid', $data); return $this->render_template('orders/createbyid', $data);
} }
public function printDiv(int $id)
{
$Orders = new Orders();
$order = $Orders->getOrdersData($id);
$docType = $order['document_type'] ?? 'facture';
// Rediriger vers la bonne méthode selon le type
switch($docType) {
case 'facture':
return $this->print31($id); // Factures individuelles
case 'bl':
return $this->print7($id); // Bon de livraison
case 'both':
return $this->print31($id); // Factures + Bon de livraison
default:
return $this->print31($id);
}
}
public function printDivBL(int $id)
{
// Force le bon de livraison
return $this->print7($id);
}
public function printDivBLF(int $id)
{
// Force facture + bon de livraison
return $this->print31($id);
}
// update caisse // update caisse
public function update_caisse($data) public function update_caisse($data)
{ {
@ -1801,6 +1982,24 @@ public function print5(int $id)
$company = $Company->getCompanyData(1); $company = $Company->getCompanyData(1);
$today = date('d/m/Y'); $today = date('d/m/Y');
// ✅ RÉCUPÉRER LE TYPE DE DOCUMENT
$documentType = $order['document_type'] ?? 'facture';
$documentTitle = '';
switch($documentType) {
case 'facture':
$documentTitle = 'FACTURE';
break;
case 'bl':
$documentTitle = 'BON DE LIVRAISON';
break;
case 'both':
$documentTitle = 'FACTURE & BON DE LIVRAISON';
break;
default:
$documentTitle = 'FACTURE';
}
// ✅ Vérifier si c'est une avance "sur mer" // ✅ Vérifier si c'est une avance "sur mer"
$isAvanceMere = false; $isAvanceMere = false;
foreach ($items as $item) { foreach ($items as $item) {
@ -1823,7 +2022,7 @@ public function print5(int $id)
<html lang="fr"> <html lang="fr">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Facture '.$order['bill_no'].'</title> <title>'.$documentTitle.' '.$order['bill_no'].'</title>
<style> <style>
/* ✅ FORMAT A4 PAYSAGE DIVISÉ EN 2 */ /* ✅ FORMAT A4 PAYSAGE DIVISÉ EN 2 */
@page { @page {
@ -2001,10 +2200,10 @@ public function print5(int $id)
</head> </head>
<body onload="window.print()"> <body onload="window.print()">
<!-- ✅ PAGE 1 : RECTO - 2 FACTURES CÔTE À CÔTE --> <!-- ✅ PAGE 1 : RECTO - 2 DOCUMENTS CÔTE À CÔTE -->
<div class="page">'; <div class="page">';
// ✅ GÉNÉRER 2 FACTURES IDENTIQUES // ✅ GÉNÉRER 2 DOCUMENTS IDENTIQUES
for ($i = 0; $i < 2; $i++) { for ($i = 0; $i < 2; $i++) {
$html .= ' $html .= '
<div class="facture-box"> <div class="facture-box">
@ -2017,7 +2216,7 @@ public function print5(int $id)
</div> </div>
<div style="text-align:center;"> <div style="text-align:center;">
<img src="'.base_url('assets/images/company_logo.jpg').'" alt="Logo"> <img src="'.base_url('assets/images/company_logo.jpg').'" alt="Logo">
<p class="facture-num">Facture N° '.esc($order['bill_no']).'</p> <p class="facture-num">'.$documentTitle.' N° '.esc($order['bill_no']).'</p>
</div> </div>
</div> </div>
@ -2059,7 +2258,6 @@ public function print5(int $id)
</tr>'; </tr>';
} }
// ✅ CORRECTION : Fermer le tableau pour avance
$html .= ' $html .= '
</tbody> </tbody>
</table>'; </table>';
@ -2097,7 +2295,6 @@ public function print5(int $id)
</tr>'; </tr>';
} }
// ✅ Fermer le tableau pour produit normal
$html .= ' $html .= '
</tbody> </tbody>
</table>'; </table>';
@ -2322,17 +2519,33 @@ public function print7(int $id)
throw new \CodeIgniter\Exceptions\PageNotFoundException(); throw new \CodeIgniter\Exceptions\PageNotFoundException();
} }
// Modèles
$Orders = new Orders(); $Orders = new Orders();
$Company = new Company(); $Company = new Company();
$OrderItems = new OrderItems(); $OrderItems = new OrderItems();
// Récupération des données
$order = $Orders->getOrdersData($id); $order = $Orders->getOrdersData($id);
$items = $OrderItems->getOrdersItemData($id); $items = $OrderItems->getOrdersItemData($id);
$company = $Company->getCompanyData(1); $company = $Company->getCompanyData(1);
$today = date('d/m/Y'); $today = date('d/m/Y');
// ✅ RÉCUPÉRER LE TYPE DE DOCUMENT
$documentType = $order['document_type'] ?? 'bl';
$documentTitle = '';
switch($documentType) {
case 'facture':
$documentTitle = 'FACTURE';
break;
case 'bl':
$documentTitle = 'BON DE LIVRAISON';
break;
case 'both':
$documentTitle = 'FACTURE & BON DE LIVRAISON';
break;
default:
$documentTitle = 'BON DE LIVRAISON';
}
// ✅ VÉRIFIER SI C'EST UNE AVANCE "SUR MER" // ✅ VÉRIFIER SI C'EST UNE AVANCE "SUR MER"
$isAvanceMere = false; $isAvanceMere = false;
foreach ($items as $item) { foreach ($items as $item) {
@ -2342,7 +2555,6 @@ public function print7(int $id)
} }
} }
// ✅ LOGIQUE DE REMISE
$discount = (float) $order['discount']; $discount = (float) $order['discount'];
$grossAmount = (float) $order['gross_amount']; $grossAmount = (float) $order['gross_amount'];
$totalTTC = ($discount > 0) ? $discount : $grossAmount; $totalTTC = ($discount > 0) ? $discount : $grossAmount;
@ -2351,12 +2563,11 @@ public function print7(int $id)
$paidLabel = $order['paid_status'] == 1 ? 'Payé' : 'Non payé'; $paidLabel = $order['paid_status'] == 1 ? 'Payé' : 'Non payé';
// Démarrage du HTML
$html = '<!DOCTYPE html> $html = '<!DOCTYPE html>
<html lang="fr"> <html lang="fr">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Bon de commande '.$order['bill_no'].'</title> <title>'.$documentTitle.' '.$order['bill_no'].'</title>
<style> <style>
body { font-family: Arial, sans-serif; font-size:14px; color:#000; margin:0; padding:0; } body { font-family: Arial, sans-serif; font-size:14px; color:#000; margin:0; padding:0; }
.header { display:flex; justify-content:space-between; align-items:center; margin:20px; } .header { display:flex; justify-content:space-between; align-items:center; margin:20px; }
@ -2388,7 +2599,7 @@ public function print7(int $id)
</div> </div>
<div style="text-align:center;"> <div style="text-align:center;">
<img src="'.base_url('assets/images/company_logo.jpg').'" alt="Logo"> <img src="'.base_url('assets/images/company_logo.jpg').'" alt="Logo">
<p style="margin:5px 0; font-weight:bold;">Bon de commande N° '.esc($order['bill_no']).'</p> <p style="margin:5px 0; font-weight:bold;">'.$documentTitle.' N° '.esc($order['bill_no']).'</p>
</div> </div>
</div> </div>
@ -2400,11 +2611,8 @@ public function print7(int $id)
<p style="text-align:right;"><em>Antananarivo, le '.$today.'</em></p> <p style="text-align:right;"><em>Antananarivo, le '.$today.'</em></p>
</div>'; </div>';
// ========================================
// ✅ TABLEAU ADAPTÉ SELON LE TYPE // ✅ TABLEAU ADAPTÉ SELON LE TYPE
// ========================================
if ($isAvanceMere) { if ($isAvanceMere) {
// --- TABLE SIMPLIFIÉE POUR AVANCE "SUR MER" (2-3 COLONNES) ---
$html .= ' $html .= '
<table> <table>
<thead> <thead>
@ -2426,7 +2634,6 @@ public function print7(int $id)
<tr> <tr>
<td>'.esc($details['product_name']); <td>'.esc($details['product_name']);
// Afficher le commentaire s'il existe
if (!empty($details['commentaire'])) { if (!empty($details['commentaire'])) {
$html .= '<br><em style="font-size:12px; color:#666;">Remarque : '.esc($details['commentaire']).'</em>'; $html .= '<br><em style="font-size:12px; color:#666;">Remarque : '.esc($details['commentaire']).'</em>';
} }
@ -2441,7 +2648,6 @@ public function print7(int $id)
</table>'; </table>';
} else { } else {
// --- TABLE COMPLÈTE POUR AVANCE "SUR TERRE" OU COMMANDE NORMALE (7 COLONNES) ---
$html .= ' $html .= '
<table> <table>
<thead> <thead>
@ -2468,7 +2674,6 @@ public function print7(int $id)
$prixAffiche = ($discount > 0) ? $discount : $details['prix']; $prixAffiche = ($discount > 0) ? $discount : $details['prix'];
// Récupérer la catégorie si c'est un produit terre
$categoryName = 'Non définie'; $categoryName = 'Non définie';
if ($details['type'] === 'terre' && !empty($item['product_id'])) { if ($details['type'] === 'terre' && !empty($item['product_id'])) {
$p = $Products->getProductData($item['product_id']); $p = $Products->getProductData($item['product_id']);
@ -2497,9 +2702,6 @@ public function print7(int $id)
</table>'; </table>';
} }
// ========================================
// TABLEAU RÉCAPITULATIF (IDENTIQUE POUR TOUS)
// ========================================
$html .= ' $html .= '
<table style="width:calc(100% - 40px); margin:0 20px 20px;"> <table style="width:calc(100% - 40px); margin:0 20px 20px;">
<tr> <tr>
@ -2555,7 +2757,6 @@ public function print7(int $id)
Merci pour votre confiance Merci pour votre confiance
</div> </div>
<!-- Conditions Générales avec saut de page -->
<div class="conditions"> <div class="conditions">
<div style="display:flex; justify-content:space-between; align-items:center;"> <div style="display:flex; justify-content:space-between; align-items:center;">
<h3 style="margin:0;">Conditions Générales</h3> <h3 style="margin:0;">Conditions Générales</h3>
@ -2576,6 +2777,7 @@ public function print7(int $id)
return $this->response->setBody($html); return $this->response->setBody($html);
} }
// ==================================== // ====================================
// PRINT31 - Facture + Bon de commande (pages séparées) // PRINT31 - Facture + Bon de commande (pages séparées)
// ==================================== // ====================================
@ -2598,6 +2800,24 @@ public function print31(int $id)
$items = $OrderItems->getOrdersItemData($id); $items = $OrderItems->getOrdersItemData($id);
$company_info = $Company->getCompanyData(1); $company_info = $Company->getCompanyData(1);
// ✅ RÉCUPÉRER LE TYPE DE DOCUMENT
$documentType = $order_data['document_type'] ?? 'facture';
$documentTitle = '';
switch($documentType) {
case 'facture':
$documentTitle = 'FACTURE';
break;
case 'bl':
$documentTitle = 'BON DE LIVRAISON';
break;
case 'both':
$documentTitle = 'FACTURE & BON DE LIVRAISON';
break;
default:
$documentTitle = 'FACTURE';
}
// ✅ Vérifier si c'est une avance "sur mer" // ✅ Vérifier si c'est une avance "sur mer"
$isAvanceMere = false; $isAvanceMere = false;
foreach ($items as $item) { foreach ($items as $item) {
@ -2611,51 +2831,49 @@ public function print31(int $id)
? "<span style='color: green; font-weight: bold;'>Payé</span>" ? "<span style='color: green; font-weight: bold;'>Payé</span>"
: "<span style='color: red; font-weight: bold;'>Refusé</span>"; : "<span style='color: red; font-weight: bold;'>Refusé</span>";
// Calculs globaux
$discount = (float) $order_data['discount']; $discount = (float) $order_data['discount'];
$grossAmount = (float) $order_data['gross_amount']; $grossAmount = (float) $order_data['gross_amount'];
$totalTTC = ($discount > 0) ? $discount : $grossAmount; $totalTTC = ($discount > 0) ? $discount : $grossAmount;
$totalHT = $totalTTC / 1.20; $totalHT = $totalTTC / 1.20;
$tva = $totalTTC - $totalHT; $tva = $totalTTC - $totalHT;
$style = '
<style>
body { font-family: Arial, sans-serif; font-size:14px; color:#000; margin:0; padding:0; }
.header { display:flex; justify-content:space-between; align-items:center; margin-bottom:20px; }
.header .infos { line-height:1.4; }
.header img { max-height:80px; }
.client { margin-bottom:20px; }
table { width:100%; border-collapse:collapse; margin-bottom:20px; }
th, td { border:1px solid #000; padding:6px; text-align:left; }
th { background:#f0f0f0; }
.right { text-align:right; }
.signature { display:flex; justify-content:space-between; margin-top:50px; }
.signature div { text-align:center; }
.page-break { page-break-after: always; }
.to-fill { border-bottom: 1px dotted #000; min-width: 150px; display: inline-block; }
.conditions { page-break-before: always; padding:20px; line-height:1.5; }
@media print {
body { font-size: 14px; }
@page { margin: 1cm; }
* { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}
</style>';
// --- FACTURES : Une par produit --- // --- FACTURES : Une par produit ---
foreach ($items as $item) { foreach ($items as $item) {
// ✅ Utiliser getOrderItemDetails au lieu de getProductData directement
$details = $this->getOrderItemDetails($item); $details = $this->getOrderItemDetails($item);
if (!$details) continue; if (!$details) continue;
$unitPrice = $details['prix']; $unitPrice = $details['prix'];
$quantity = isset($item['qty']) ? (int) $item['qty'] : 1;
$subtotal = $unitPrice * $quantity;
// ✅ Pour avance sur mer avec remise, utiliser la remise comme prix
$prixAffiche = ($discount > 0 && $isAvanceMere) ? $discount : $unitPrice; $prixAffiche = ($discount > 0 && $isAvanceMere) ? $discount : $unitPrice;
echo '<!DOCTYPE html>'; echo '<!DOCTYPE html>';
echo '<html lang="fr">'; echo '<html lang="fr">';
echo '<head><meta charset="utf-8">'; echo '<head><meta charset="utf-8">';
echo '<link rel="stylesheet" href="' . base_url('assets/bower_components/bootstrap/dist/css/bootstrap.min.css') . '">'; echo '<link rel="stylesheet" href="' . base_url('assets/bower_components/bootstrap/dist/css/bootstrap.min.css') . '">';
echo "<style> echo $style;
body { font-family: Arial, sans-serif; font-size:14px; color:#000; margin:0; padding:0; }
.header { display:flex; justify-content:space-between; align-items:center; margin-bottom:20px; }
.header .infos { line-height:1.4; }
.header img { max-height:80px; }
.client { margin-bottom:20px; }
table { width:100%; border-collapse:collapse; margin-bottom:20px; }
th, td { border:1px solid #000; padding:6px; text-align:left; }
th { background:#f0f0f0; }
.right { text-align:right; }
.signature { display:flex; justify-content:space-between; margin-top:50px; }
.signature div { text-align:center; }
.page-break { page-break-after: always; }
.to-fill { border-bottom: 1px dotted #000; min-width: 150px; display: inline-block; }
@media print {
body { font-size: 14px; }
@page { margin: 1cm; }
* { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}
</style>";
echo '</head><body onload="window.print()">'; echo '</head><body onload="window.print()">';
echo '<div class="page-break">'; echo '<div class="page-break">';
echo '<div class="header">'; echo '<div class="header">';
@ -2668,7 +2886,7 @@ public function print31(int $id)
echo '</div>'; echo '</div>';
echo '<div style="text-align:center;">'; echo '<div style="text-align:center;">';
echo '<img src="' . base_url('assets/images/company_logo.jpg') . '" alt="Logo">'; echo '<img src="' . base_url('assets/images/company_logo.jpg') . '" alt="Logo">';
echo '<p style="margin:5px 0; font-weight:bold;">Facture N° ' . esc($order_data['bill_no']) . '</p>'; echo '<p style="margin:5px 0; font-weight:bold;">' . $documentTitle . ' N° ' . esc($order_data['bill_no']) . '</p>';
echo '<p style="margin:5px 0;"><em>Antananarivo, le ' . date('d/m/Y') . '</em></p>'; echo '<p style="margin:5px 0;"><em>Antananarivo, le ' . date('d/m/Y') . '</em></p>';
echo '</div>'; echo '</div>';
echo '</div>'; echo '</div>';
@ -2694,7 +2912,6 @@ public function print31(int $id)
echo '<td class="right">' . number_format($prixAffiche, 0, '', ' ') . '</td>'; echo '<td class="right">' . number_format($prixAffiche, 0, '', ' ') . '</td>';
echo '</tr>'; echo '</tr>';
// Afficher commentaire si existant
if (!empty($details['commentaire'])) { if (!empty($details['commentaire'])) {
echo '<tr><td colspan="3"><em style="font-size:12px; color:#666;">Remarque : ' . esc($details['commentaire']) . '</em></td></tr>'; echo '<tr><td colspan="3"><em style="font-size:12px; color:#666;">Remarque : ' . esc($details['commentaire']) . '</em></td></tr>';
} }
@ -2702,7 +2919,6 @@ public function print31(int $id)
echo '</tbody></table>'; echo '</tbody></table>';
} else { } else {
// TABLE COMPLÈTE POUR AVANCE "SUR TERRE" OU COMMANDE NORMALE // TABLE COMPLÈTE POUR AVANCE "SUR TERRE" OU COMMANDE NORMALE
// Récupérer catégorie
$categoryName = 'Non définie'; $categoryName = 'Non définie';
if ($details['type'] === 'terre' && !empty($item['product_id'])) { if ($details['type'] === 'terre' && !empty($item['product_id'])) {
$p = $Products->getProductData($item['product_id']); $p = $Products->getProductData($item['product_id']);
@ -2762,31 +2978,12 @@ public function print31(int $id)
echo '</body></html>'; echo '</body></html>';
} }
// --- BON DE COMMANDE (UNE SEULE PAGE) --- // --- BON DE LIVRAISON (UNE SEULE PAGE) ---
echo '<!DOCTYPE html>'; echo '<!DOCTYPE html>';
echo '<html lang="fr">'; echo '<html lang="fr">';
echo '<head><meta charset="utf-8">'; echo '<head><meta charset="utf-8">';
echo '<link rel="stylesheet" href="' . base_url('assets/bower_components/bootstrap/dist/css/bootstrap.min.css') . '">'; echo '<link rel="stylesheet" href="' . base_url('assets/bower_components/bootstrap/dist/css/bootstrap.min.css') . '">';
echo "<style> echo $style;
body { font-family: Arial, sans-serif; font-size:14px; color:#000; margin:0; padding:0; }
.header { display:flex; justify-content:space-between; align-items:center; margin-bottom:20px; }
.header .infos { line-height:1.4; }
.header img { max-height:80px; }
.client { margin-bottom:20px; }
table { width:100%; border-collapse:collapse; margin-bottom:20px; }
th, td { border:1px solid #000; padding:6px; text-align:left; }
th { background:#f0f0f0; }
.right { text-align:right; }
.signature { display:flex; justify-content:space-between; margin-top:50px; }
.signature div { text-align:center; }
.to-fill { border-bottom: 1px dotted #000; min-width: 150px; display: inline-block; }
.conditions { page-break-before: always; padding:20px; line-height:1.5; }
@media print {
body { font-size: 14px; }
@page { margin: 1cm; }
* { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}
</style>";
echo '</head><body>'; echo '</head><body>';
echo '<div class="header">'; echo '<div class="header">';
@ -2798,7 +2995,7 @@ public function print31(int $id)
echo '</div>'; echo '</div>';
echo '<div style="text-align:center;">'; echo '<div style="text-align:center;">';
echo '<img src="' . base_url('assets/images/company_logo.jpg') . '" alt="Logo">'; echo '<img src="' . base_url('assets/images/company_logo.jpg') . '" alt="Logo">';
echo '<p style="margin:5px 0; font-weight:bold;">Bon de Commande N° ' . esc($order_data['bill_no']) . '</p>'; echo '<p style="margin:5px 0; font-weight:bold;">' . $documentTitle . ' N° ' . esc($order_data['bill_no']) . '</p>';
echo '</div>'; echo '</div>';
echo '</div>'; echo '</div>';
@ -2842,7 +3039,6 @@ public function print31(int $id)
$prixAffiche = ($discount > 0) ? $discount : $details['prix']; $prixAffiche = ($discount > 0) ? $discount : $details['prix'];
// Catégorie
$categoryName = 'Non définie'; $categoryName = 'Non définie';
if ($details['type'] === 'terre' && !empty($item['product_id'])) { if ($details['type'] === 'terre' && !empty($item['product_id'])) {
$p = $Products->getProductData($item['product_id']); $p = $Products->getProductData($item['product_id']);

281
app/Models/Avance.php

@ -443,194 +443,178 @@ public function getProductWithBrand($product_id)
*/ */
public function convertToOrder(int $avance_id) public function convertToOrder(int $avance_id)
{ {
$db = \Config\Database::connect();
try { try {
$db->transStart();
$avance = $this->find($avance_id); $avance = $this->find($avance_id);
if (!$avance) { if (!$avance) {
log_message('error', "Avance introuvable : {$avance_id}"); log_message('error', "Avance {$avance_id} introuvable");
return false; return false;
} }
// ✅ Vérifier que c'est bien une avance sur TERRE // ✅ VÉRIFICATION 1 : Type d'avance
if ($avance['type_avance'] !== 'terre') { if ($avance['type_avance'] !== 'terre') {
log_message('info', "Avance {$avance_id} de type '{$avance['type_avance']}' - Conversion ignorée (seules les avances TERRE sont converties)"); log_message('info', "⚠️ Avance {$avance_id} est MER, pas de conversion");
return false; return false;
} }
// ✅ Vérifier que l'avance est bien complète // ✅ VÉRIFICATION 2 : Paiement complet
if ((float)$avance['amount_due'] > 0) { if ($avance['amount_due'] > 0) {
log_message('warning', "Avance TERRE {$avance_id} non complète (amount_due={$avance['amount_due']})"); log_message('warning', "⚠️ Avance {$avance_id} non complète (reste: {$avance['amount_due']})");
return false; return false;
} }
// ✅ Vérifier qu'elle n'a pas déjà été convertie // ✅ VÉRIFICATION 3 : Déjà convertie ?
if ((int)$avance['is_order'] === 1) { if ($avance['is_order'] == 1) {
log_message('info', "Avance TERRE {$avance_id} déjà convertie en commande"); log_message('info', "⚠️ Avance {$avance_id} déjà convertie");
return false;
// ✅ Récupérer l'ID de la commande existante
$existingOrder = $db->table('orders')
->select('id')
->where('customer_name', $avance['customer_name'])
->where('customer_phone', $avance['customer_phone'])
->where('source', 'Avance convertie')
->orderBy('id', 'DESC')
->limit(1)
->get()
->getRowArray();
return $existingOrder ? $existingOrder['id'] : false;
} }
// ✅ Vérifier que le produit existe (obligatoire pour avance TERRE) // ✅ VÉRIFICATION 4 : Produit existe ?
if (empty($avance['product_id'])) { $Products = new \App\Models\Products();
log_message('error', "Avance TERRE {$avance_id} sans product_id - Impossible de convertir"); $product = $Products->find($avance['product_id']);
if (!$product) {
log_message('error', "❌ Produit {$avance['product_id']} introuvable");
return false; return false;
} }
$db = \Config\Database::connect(); // ✅ Récupérer l'utilisateur actuel
$db->transStart(); $session = session();
$user = $session->get('user');
// ✅ 1. Créer la commande // ✅ Générer le numéro de commande
$orderModel = new \App\Models\Orders(); $bill_no = $this->generateBillNo($avance['store_id']);
$bill_no = 'BILAV-' . strtoupper(substr(md5(uniqid(mt_rand(), true)), 0, 4));
// ✅ Préparer les données de commande
$orderData = [ $orderData = [
'bill_no' => $bill_no, 'bill_no' => $bill_no,
'customer_name' => $avance['customer_name'], 'document_type' => 'facture',
'customer_address' => $avance['customer_address'], 'customer_name' => $avance['customer_name'],
'customer_phone' => $avance['customer_phone'], 'customer_address' => $avance['customer_address'],
'customer_cin' => $avance['customer_cin'], 'customer_phone' => $avance['customer_phone'],
'date_time' => date('Y-m-d H:i:s'), 'customer_cin' => $avance['customer_cin'],
'gross_amount' => $avance['gross_amount'], 'customer_type' => 'Particulier',
'net_amount' => $avance['gross_amount'], 'source' => 'Avance convertie', // ✅ MARQUEUR IMPORTANT
'discount' => 0, 'date_time' => date('Y-m-d H:i:s'),
'paid_status' => 2, // En attente validation caissière 'gross_amount' => $avance['gross_amount'],
'user_id' => $avance['user_id'], 'net_amount' => $avance['gross_amount'],
'store_id' => $avance['store_id'], 'discount' => 0,
'service_charge_rate' => 0, 'paid_status' => 2, // En attente de validation
'vat_charge_rate' => 0, 'user_id' => $user['id'] ?? $avance['user_id'],
'vat_charge' => 0, 'store_id' => $avance['store_id'],
'tranche_1' => $avance['avance_amount'], 'tranche_1' => $avance['avance_amount'],
'tranche_2' => null, 'tranche_2' => 0,
'order_payment_mode' => $avance['type_payment'] ?? 'En espèce', 'order_payment_mode' => $avance['type_payment'] ?? 'En espèce',
'order_payment_mode_1' => null, 'service_charge_rate' => 0,
'vat_charge_rate' => 0,
'vat_charge' => 0,
]; ];
$order_id = $orderModel->insert($orderData); // ✅ Créer la commande
$db->table('orders')->insert($orderData);
$order_id = $db->insertID();
if (!$order_id) { if (!$order_id) {
throw new \Exception("Échec création commande pour avance TERRE {$avance_id}"); throw new \Exception("Échec création commande pour avance {$avance_id}");
} }
log_message('info', "✅ Commande {$bill_no} créée depuis avance TERRE {$avance_id}"); // ✅ CORRECTION CRITIQUE : Créer l'item de commande SANS 'qty'
$orderItemData = [
// ✅ 2. Créer les items de commande 'order_id' => $order_id,
$orderItemModel = new \App\Models\OrderItems(); 'product_id' => $avance['product_id'],
$productModel = new \App\Models\Products(); 'rate' => $avance['gross_amount'],
'amount' => $avance['gross_amount'],
$product = $productModel->find($avance['product_id']); 'puissance' => $product['puissance'] ?? '',
];
if ($product) {
$orderItemModel->insert([
'order_id' => $order_id,
'product_id' => $avance['product_id'],
'rate' => $avance['gross_amount'],
'qty' => 1,
'amount' => $avance['gross_amount'],
]);
log_message('info', "Item ajouté : produit {$avance['product_id']} (TERRE)"); $db->table('orders_item')->insert($orderItemData);
} else {
log_message('warning', "Produit {$avance['product_id']} introuvable pour avance TERRE {$avance_id}");
}
// ✅ 3. Marquer l'avance comme convertie // ✅ MARQUER L'AVANCE COMME CONVERTIE
$this->update($avance_id, [ $this->update($avance_id, [
'is_order' => 1, 'is_order' => 1,
'active' => 0, 'active' => 0 // ✅ Désactiver pour ne plus apparaître dans les listes
]); ]);
// ✅ Le produit RESTE product_sold = 1 (déjà marqué lors de la création de l'avance)
$db->transComplete(); $db->transComplete();
if ($db->transStatus() === false) { if ($db->transStatus() === false) {
log_message('error', "Transaction échouée pour avance TERRE {$avance_id}"); log_message('error', "Transaction échouée pour avance {$avance_id}");
return false; return false;
} }
// ✅ 4. NOUVEAU : Envoyer notifications à TOUS les stores log_message('info', "✅ Avance {$avance_id} convertie en commande {$order_id}");
$this->sendConversionNotifications($avance, $order_id, $bill_no);
// ✅ 5. Notification à la caissière du store concerné
$notificationController = new \App\Controllers\NotificationController();
$notificationController->createNotification(
"Nouvelle commande issue d'une avance TERRE complète - {$bill_no}",
"Caissière",
(int)$avance['store_id'],
"orders"
);
log_message('info', "✅ Avance TERRE {$avance_id} convertie en commande {$order_id} ({$bill_no})"); // ✅ Envoyer notification
$this->sendConversionNotification($avance, $order_id, $bill_no);
return $order_id; return $order_id;
} catch (\Exception $e) { } catch (\Exception $e) {
log_message('error', "Erreur conversion avance TERRE→commande : " . $e->getMessage()); $db->transRollback();
log_message('error', "❌ Erreur conversion avance {$avance_id}: " . $e->getMessage());
return false; return false;
} }
} }
private function sendConversionNotifications($avance, $order_id, $bill_no) private function sendConversionNotification($avance, $order_id, $bill_no)
{ {
try { try {
$Notification = new \App\Controllers\NotificationController(); $Notification = new \App\Controllers\NotificationController();
$db = \Config\Database::connect(); $Stores = new \App\Models\Stores();
// Récupérer tous les stores $allStores = $Stores->getActiveStore();
$storesQuery = $db->table('stores')->select('id')->get();
$allStores = $storesQuery->getResultArray(); $message = "🔄 Avance TERRE convertie en commande<br>" .
"N° Avance : #{$avance['avance_id']}<br>" .
// Récupérer les infos de l'utilisateur "Client : {$avance['customer_name']}<br>" .
$userQuery = $db->table('users') "Commande : {$bill_no}<br>" .
->select('firstname, lastname') "Montant : " . number_format($avance['gross_amount'], 0, ',', ' ') . " Ar";
->where('id', $avance['user_id'])
->get(); // ✅ Notifier tous les stores (Direction, DAF, SuperAdmin)
$user = $userQuery->getRowArray(); if (is_array($allStores) && count($allStores) > 0) {
foreach ($allStores as $store) {
$userName = $user ? "{$user['firstname']} {$user['lastname']}" : 'Utilisateur inconnu'; foreach (['Direction', 'DAF', 'SuperAdmin'] as $role) {
$Notification->createNotification(
// Préparer le message $message,
$customerName = $avance['customer_name']; $role,
$avanceNumber = str_pad($avance['avance_id'], 5, '0', STR_PAD_LEFT); (int)$store['id'],
$avanceAmount = number_format((float)$avance['gross_amount'], 0, ',', ' '); 'orders'
$typeAvance = strtoupper($avance['type_avance']); );
}
$notificationMessage = "🎉 Avance {$typeAvance} N°{$avanceNumber} convertie en COMMANDE {$bill_no} - Client: {$customerName} - Montant: {$avanceAmount} Ar - Par: {$userName}"; }
// ✅ Envoyer notification à DAF, Direction et SuperAdmin de TOUS les stores
foreach ($allStores as $store) {
$storeId = (int)$store['id'];
// Notification pour DAF
$Notification->createNotification(
$notificationMessage,
"DAF",
$storeId,
'orders'
);
// Notification pour Direction
$Notification->createNotification(
$notificationMessage,
"Direction",
$storeId,
'orders'
);
// Notification pour SuperAdmin
$Notification->createNotification(
$notificationMessage,
"SuperAdmin",
$storeId,
'orders'
);
} }
log_message('info', "✅ Notifications conversion envoyées pour avance {$avance['avance_id']} → commande {$order_id} à tous les stores"); // ✅ Caissière du store concerné
$Notification->createNotification(
$message,
"Caissière",
(int)$avance['store_id'],
'orders'
);
} catch (\Exception $e) { } catch (\Exception $e) {
log_message('error', "Erreur envoi notifications conversion: " . $e->getMessage()); log_message('error', '❌ Erreur notification: ' . $e->getMessage());
} }
} }
/** /**
* ✅ Hook appelé automatiquement lors du paiement d'une avance * ✅ Hook appelé automatiquement lors du paiement d'une avance
* Intégrer ceci dans votre fonction de paiement existante * Intégrer ceci dans votre fonction de paiement existante
@ -660,38 +644,45 @@ public function afterPayment(int $avance_id)
/** /**
* ✅ Générer un numéro de facture unique * ✅ Générer un numéro de facture unique
*/ */
private function generateBillNumber($store_id) private function generateBillNo(int $store_id): string
{ {
$db = \Config\Database::connect(); $storePrefixes = [
1 => 'ANTS',
2 => 'BESA',
3 => 'BYPA',
4 => 'TOAM',
];
// Récupérer le dernier numéro pour ce store $prefix = $storePrefixes[$store_id] ?? 'STORE';
$query = $db->query(
"SELECT bill_no FROM orders WHERE store_id = ? ORDER BY id DESC LIMIT 1",
[$store_id]
);
$result = $query->getRow(); $db = \Config\Database::connect();
if ($result && preg_match('/(\d+)$/', $result->bill_no, $matches)) { $lastBill = $db->table('orders')
$lastNumber = intval($matches[1]); ->select('bill_no')
$newNumber = $lastNumber + 1; ->like('bill_no', $prefix . '-', 'after')
->orderBy('id', 'DESC')
->limit(1)
->get()
->getRowArray();
if ($lastBill && !empty($lastBill['bill_no'])) {
preg_match('/-(\d+)$/', $lastBill['bill_no'], $matches);
$newNumber = isset($matches[1]) ? (int)$matches[1] + 1 : 1;
} else { } else {
$newNumber = 1; $newNumber = 1;
} }
// Format: BILL-STORE{store_id}-{number} return $prefix . '-' . str_pad($newNumber, 3, '0', STR_PAD_LEFT);
return 'BILL-STORE' . $store_id . '-' . str_pad($newNumber, 5, '0', STR_PAD_LEFT);
} }
/** /**
* ✅ Récupérer toutes les avances complètes non converties * ✅ Récupérer toutes les avances complètes non converties
*/ */
public function getCompletedNotConverted() public function getCompletedNotConverted()
{ {
return $this->where('amount_due', 0) return $this->where('type_avance', 'terre')
->where('amount_due <=', 0)
->where('is_order', 0) ->where('is_order', 0)
->where('active', 1) ->where('active', 1)
->where('type_avance', 'terre') // ✅ Uniquement TERRE à convertir
->findAll(); ->findAll();
} }

25
app/Models/Orders.php

@ -16,6 +16,7 @@ class Orders extends Model
protected $table = 'orders'; protected $table = 'orders';
protected $allowedFields = [ protected $allowedFields = [
'bill_no', 'bill_no',
'document_type',
'customer_name', 'customer_name',
'customer_address', 'customer_address',
'customer_phone', 'customer_phone',
@ -202,34 +203,29 @@ class Orders extends Model
$orderItemModel = new OrderItems(); $orderItemModel = new OrderItems();
$productModel = new Products(); $productModel = new Products();
// ✅ RÉCUPÉRER LE TABLEAU DES PUISSANCES
$puissances = $data['puissance'] ?? []; $puissances = $data['puissance'] ?? [];
// Loop through products and insert order items // Loop through products and insert order items
$count_product = count($post); $count_product = count($post);
for ($x = 0; $x < $count_product; $x++) { for ($x = 0; $x < $count_product; $x++) {
// ✅ AJOUT DE LA PUISSANCE
$items = [ $items = [
'order_id' => $order_id, 'order_id' => $order_id,
'product_id' => $post[$x], 'product_id' => $post[$x],
'rate' => $data['rate_value'][$x], 'rate' => $data['rate_value'][$x],
'qty' => 1, 'qty' => 1,
'amount' => $data['amount_value'][$x], 'amount' => $data['amount_value'][$x],
'puissance' => $puissances[$x] ?? 1, // ✅ CORRECTION ICI 'puissance' => $puissances[$x] ?? 1,
]; ];
$orderItemModel->insert($items); $orderItemModel->insert($items);
// Decrease stock for the product // ✅ CORRECTION : Marquer product_sold = 1 dès la création
$product_data = $productModel->find($post[$x]); // Peu importe le statut initial (En Attente, Payé, etc.)
if((int)$data['paid_status'] == 1){ if (in_array((int)$data['paid_status'], [1, 2, 3])) {
if ($product_data) { $productModel->update($post[$x], ['product_sold' => 1]);
$product_sold = true; } else {
$productModel->update($post[$x], ['product_sold' => $product_sold]); // Si statut = 0 (Refusé), laisser disponible
} else { $productModel->update($post[$x], ['product_sold' => 0]);
$product_sold = false;
$productModel->update($post[$x], ['product_sold' => $product_sold]);
}
} }
} }
return $order_id; return $order_id;
@ -238,7 +234,6 @@ class Orders extends Model
return false; return false;
} }
} }
/** /**
* count order item * count order item
* @param int $order_id * @param int $order_id
@ -467,7 +462,7 @@ class Orders extends Model
ELSE 0 ELSE 0
END) AS total_virement_bancaire2 END) AS total_virement_bancaire2
') ')
->whereIn('orders.paid_status', [1, 3]); // ← CHANGEZ CETTE LIGNE ->whereIn('orders.paid_status', [1, 2, 3]); // ← CHANGEZ CETTE LIGNE
if (!$isAdmin) { if (!$isAdmin) {
$baseQuery->where('orders.store_id', $users['store_id']); $baseQuery->where('orders.store_id', $users['store_id']);

113
app/Models/Products.php

@ -11,34 +11,26 @@ class Products extends Model
protected $allowedFields = ['name', 'sku', 'price', 'product_sold', 'qty', 'image', 'description', 'numero_de_moteur', 'marque', 'chasis', 'store_id', 'availability', 'is_piece', 'prix_vente', 'date_arivage', 'puissance', 'cler', 'categorie_id', 'etats','type', 'infoManquekit', 'info', 'infoManque']; protected $allowedFields = ['name', 'sku', 'price', 'product_sold', 'qty', 'image', 'description', 'numero_de_moteur', 'marque', 'chasis', 'store_id', 'availability', 'is_piece', 'prix_vente', 'date_arivage', 'puissance', 'cler', 'categorie_id', 'etats','type', 'infoManquekit', 'info', 'infoManque'];
/** /**
* ✅ NOUVELLE MÉTHODE : Récupérer les produits selon le rôle et le store de l'utilisateur * ✅ Récupérer les produits selon le rôle et le store de l'utilisateur
* @param int|null $id
* @return array|object|null
*/ */
public function getProductDataByRole(int $id = null) public function getProductDataByRole(int $id = null)
{ {
$session = session(); $session = session();
$user = $session->get('user'); $user = $session->get('user');
// Vérifier si l'utilisateur est admin (Conseil ou Direction)
$isAdmin = in_array($user['group_name'], ['SuperAdmin', 'Direction', 'DAF']); $isAdmin = in_array($user['group_name'], ['SuperAdmin', 'Direction', 'DAF']);
$builder = $this->where('is_piece', 0) $builder = $this->where('is_piece', 0)
->where('product_sold', 0); ->where('product_sold', 0);
// ✅ Si pas admin ET a un store_id valide, filtrer par son magasin
if (!$isAdmin) { if (!$isAdmin) {
// ✅ Si l'utilisateur n'a pas de store_id (NULL ou 0), ne retourner aucun produit
if (empty($user['store_id']) || $user['store_id'] == 0) { if (empty($user['store_id']) || $user['store_id'] == 0) {
// Retourner une requête impossible pour avoir 0 résultats
$builder->where('id', -1); $builder->where('id', -1);
} else { } else {
// Filtrer par le store_id de l'utilisateur
$builder->where('store_id', $user['store_id']); $builder->where('store_id', $user['store_id']);
} }
} }
// Si un ID spécifique est demandé
if ($id) { if ($id) {
return $builder->where('id', $id)->first(); return $builder->where('id', $id)->first();
} }
@ -46,11 +38,6 @@ class Products extends Model
return $builder->orderBy('id', 'DESC')->findAll(); return $builder->orderBy('id', 'DESC')->findAll();
} }
/**
* get the brand data
* @param int $id
* @return array|object|null
*/
public function getProductData(int $id = null) public function getProductData(int $id = null)
{ {
if ($id) { if ($id) {
@ -92,17 +79,29 @@ class Products extends Model
->where('availability', 1) ->where('availability', 1)
->where('store_id', $store_id); ->where('store_id', $store_id);
$db = \Config\Database::connect();
// Sous-requête pour exclure les produits en avance
if ($excludeAvance) { if ($excludeAvance) {
$db = \Config\Database::connect(); $subQueryAvances = $db->table('avances')
$subQuery = $db->table('avances')
->select('product_id') ->select('product_id')
->where('active', 1) ->where('active', 1)
->where('is_order', 0) ->where('is_order', 0)
->getCompiledSelect(); ->getCompiledSelect();
$builder->where("id NOT IN ($subQuery)", null, false); $builder->where("id NOT IN ($subQueryAvances)", null, false);
} }
// ✅ LISTE : Exclure TOUS les produits ayant une commande (statuts 1, 2, 3)
$subQueryOrders = $db->table('orders_item')
->select('orders_item.product_id')
->join('orders', 'orders.id = orders_item.order_id')
->whereIn('orders.paid_status', [1, 2, 3]) // ✅ Disparaît de la liste dès qu'il y a une commande
->getCompiledSelect();
$builder->where("id NOT IN ($subQueryOrders)", null, false);
// Exception pour le produit actuel lors de modification de commande
if ($currentProductId) { if ($currentProductId) {
$builder->orWhere('id', $currentProductId); $builder->orWhere('id', $currentProductId);
} }
@ -143,19 +142,32 @@ class Products extends Model
return $this->delete($id) ? true : false; return $this->delete($id) ? true : false;
} }
/**
* ✅ NOUVELLE MÉTHODE : Compteur DASHBOARD (exclut uniquement statut 3)
* Utilisé pour afficher le nombre total de produits dans le dashboard
*/
public function countTotalProducts() public function countTotalProducts()
{ {
$db = \Config\Database::connect(); $db = \Config\Database::connect();
$subQuery = $db->table('avances') // Exclure produits en avance
$subQueryAvances = $db->table('avances')
->select('product_id') ->select('product_id')
->where('active', 1) ->where('active', 1)
->where('is_order', 0) ->where('is_order', 0)
->getCompiledSelect(); ->getCompiledSelect();
// ✅ Exclure UNIQUEMENT les produits avec statut 3 (livré)
$subQueryOrders = $db->table('orders_item')
->select('orders_item.product_id')
->join('orders', 'orders.id = orders_item.order_id')
->where('orders.paid_status', 3) // ✅ Décompté UNIQUEMENT quand livré
->getCompiledSelect();
return $this->where('is_piece', 0) return $this->where('is_piece', 0)
->where('product_sold', 0) ->where('product_sold', 0)
->where("id NOT IN ($subQuery)", null, false) ->where("id NOT IN ($subQueryAvances)", null, false)
->where("id NOT IN ($subQueryOrders)", null, false)
->countAllResults(); ->countAllResults();
} }
@ -197,38 +209,43 @@ class Products extends Model
} }
/** /**
* Compter les produits par store selon le rôle de l'utilisateur * ✅ NOUVELLE MÉTHODE : Compteur DASHBOARD par store (exclut uniquement statut 3)
* @return int * Compter les produits par store selon le rôle de l'utilisateur
*/ */
public function countProductsByUserStore() public function countProductsByUserStore()
{ {
$session = session(); $session = session();
$user = $session->get('user'); $user = $session->get('user');
// Vérifier si l'utilisateur est admin $isAdmin = in_array($user['group_name'], ['DAF', 'Direction', 'SuperAdmin']);
$isAdmin = in_array($user['group_name'], ['DAF', 'Direction', 'SuperAdmin']);
$db = \Config\Database::connect(); $db = \Config\Database::connect();
// Sous-requête pour exclure les produits en avance // Exclure avances
$subQuery = $db->table('avances') $subQueryAvances = $db->table('avances')
->select('product_id') ->select('product_id')
->where('active', 1) ->where('active', 1)
->where('is_order', 0) ->where('is_order', 0)
->getCompiledSelect(); ->getCompiledSelect();
$builder = $this->where('is_piece', 0) // ✅ Exclure UNIQUEMENT les produits livrés (statut 3)
->where('product_sold', 0) $subQueryOrders = $db->table('orders_item')
->where("id NOT IN ($subQuery)", null, false); ->select('orders_item.product_id')
->join('orders', 'orders.id = orders_item.order_id')
// Si pas admin ET a un store_id valide, filtrer par son magasin ->where('orders.paid_status', 3) // ✅ Décompté UNIQUEMENT quand livré
if (!$isAdmin && !empty($user['store_id']) && $user['store_id'] != 0) { ->getCompiledSelect();
$builder->where('store_id', $user['store_id']);
} elseif (!$isAdmin) {
// Utilisateur sans store = 0 produits
return 0;
}
return $builder->countAllResults(); $builder = $this->where('is_piece', 0)
} ->where('product_sold', 0)
->where("id NOT IN ($subQueryAvances)", null, false)
->where("id NOT IN ($subQueryOrders)", null, false);
if (!$isAdmin && !empty($user['store_id']) && $user['store_id'] != 0) {
$builder->where('store_id', $user['store_id']);
} elseif (!$isAdmin) {
return 0;
}
return $builder->countAllResults();
}
} }

27
app/Models/Remise.php

@ -104,4 +104,31 @@ class Remise extends Model
return "Nouvelle remise créée avec succès."; return "Nouvelle remise créée avec succès.";
} }
} }
/**
* Vérifier si une commande a une demande de remise en attente
* @param int $order_id
* @return bool true si remise en attente, false sinon
*/
public function hasRemisePendingForOrder(int $order_id): bool
{
$result = $this->where('id_order', $order_id)
->where('demande_status', 'En attente')
->first();
return $result !== null;
}
/**
* Vérifier si une commande a une remise validée
* @param int $order_id
* @return bool
*/
public function hasRemiseValidatedForOrder(int $order_id): bool
{
$result = $this->where('id_order', $order_id)
->whereIn('demande_status', ['Accepté', 'Validé'])
->first();
return $result !== null;
}
} }

151
app/Views/orders/edit.php

@ -64,10 +64,30 @@
<label for="time" class="col-sm-12 control-label">Heure: <?php echo date('h:i a') ?></label> <label for="time" class="col-sm-12 control-label">Heure: <?php echo date('h:i a') ?></label>
</div> </div>
<div class="col-md-4 col-xs-12 pull pull-left"> <div class="col-md-4 col-xs-12 pull pull-left">
<!-- ✅ NOUVEAU : Type de document -->
<div class="form-group">
<label for="document_type" class="col-sm-5 control-label" style="text-align:left;">
Type de document <span class="text-danger">*</span>
</label>
<div class="col-sm-7">
<select name="document_type" id="document_type" class="form-control" required>
<option value="facture" <?php echo (isset($order_data['order']['document_type']) && $order_data['order']['document_type'] == 'facture') ? 'selected' : ''; ?>>
Facture
</option>
<option value="bl" <?php echo (isset($order_data['order']['document_type']) && $order_data['order']['document_type'] == 'bl') ? 'selected' : ''; ?>>
Bon de Livraison
</option>
<option value="both" <?php echo (isset($order_data['order']['document_type']) && $order_data['order']['document_type'] == 'both') ? 'selected' : ''; ?>>
Facture & Bon de Livraison
</option>
</select>
</div>
</div>
<!-- Types d'impression (ancien système - peut être supprimé si vous voulez) -->
<div class="form-group"> <div class="form-group">
<label for="types" class="col-sm-5 control-label" style="text-align:left;">Types</label> <label for="types" class="col-sm-5 control-label" style="text-align:left;">Types d'impression</label>
<div class="col-sm-7"> <div class="col-sm-7">
<select name="" id="typesCommande" class="form-control"> <select name="" id="typesCommande" class="form-control">
<option value="1">Facture</option> <option value="1">Facture</option>
@ -331,11 +351,69 @@
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
var base_url = "<?php echo base_url(); ?>"; var base_url = "<?php echo base_url(); ?>";
var idData = "<?php echo $order_data['order']['id']; ?>"; var idData = "<?php echo $order_data['order']['id']; ?>";
let Imprimente = document.getElementById('Imprimente'); let Imprimente = document.getElementById('Imprimente');
let typesCommande = document.getElementById('typesCommande'); let typesCommande = document.getElementById('typesCommande');
let documentType = document.getElementById('document_type');
function updatePrintLink() {
let type = documentType ? documentType.value : 'facture';
// Synchroniser l'ancien select (si vous le gardez)
if (typesCommande) {
if (type === 'facture') {
typesCommande.value = 1;
} else if (type === 'bl') {
typesCommande.value = 3;
} else if (type === 'both') {
typesCommande.value = 2;
}
}
// Mettre à jour le lien d'impression
if (Imprimente) {
Imprimente.removeAttribute("href");
switch(type) {
case 'facture':
Imprimente.setAttribute("href", base_url + 'orders/printDiv/' + idData);
break;
case 'bl':
Imprimente.setAttribute("href", base_url + 'orders/printDivBL/' + idData);
break;
case 'both':
Imprimente.setAttribute("href", base_url + 'orders/printDivBLF/' + idData);
break;
default:
Imprimente.setAttribute("href", base_url + 'orders/printDiv/' + idData);
}
}
}
// ✅ Écouter les changements sur le nouveau select
if (documentType) {
documentType.addEventListener('change', updatePrintLink);
// Initialiser au chargement
updatePrintLink();
}
// ✅ Garder la compatibilité avec l'ancien système (optionnel)
if (typesCommande) {
typesCommande.addEventListener('change', function () {
// Synchroniser avec le nouveau select
if (documentType) {
if (typesCommande.value == 1) {
documentType.value = 'facture';
} else if (typesCommande.value == 3) {
documentType.value = 'bl';
} else {
documentType.value = 'both';
}
}
updatePrintLink();
});
}
typesCommande.addEventListener('change', function () { typesCommande.addEventListener('change', function () {
if (typesCommande.value == 1) { if (typesCommande.value == 1) {
Imprimente.removeAttribute("href"); Imprimente.removeAttribute("href");
@ -501,10 +579,10 @@
var tableProductLength = $("#product_info_table tbody tr").length; var tableProductLength = $("#product_info_table tbody tr").length;
var totalSubAmount = 0; var totalSubAmount = 0;
for (x = 0; x < tableProductLength; x++) { for (x = 0; x < tableProductLength; x++) {
var tr = $("#product_info_table tbody tr")[x]; var tr = $("#product_info_table tbody tr")[x];
var count = $(tr).attr('id'); var count = $(tr).attr('id');
count = count.substring(4); count = count.substring(4);
totalSubAmount = Number(totalSubAmount) + Number($("#amount_" + count).val()); totalSubAmount = Number(totalSubAmount) + Number($("#amount_" + count).val());
} }
totalSubAmount = totalSubAmount.toFixed(2); totalSubAmount = totalSubAmount.toFixed(2);
@ -521,30 +599,25 @@
$("#service_charge").val(service); $("#service_charge").val(service);
$("#service_charge_value").val(service); $("#service_charge_value").val(service);
var totalAmount = (Number(totalSubAmount)); // ✅ CORRECTION : net_amount = gross_amount - discount
totalAmount = totalAmount.toFixed(2); var discount = Number($("#discount").val()) || 0;
var grossAmount = Number(totalSubAmount);
var netAmount = grossAmount - discount;
var discount = $("#discount").val(); netAmount = netAmount.toFixed(2);
if (discount) { $("#net_amount").val(netAmount);
var grandTotal = Number(totalAmount) - Number(discount); $("#net_amount_value").val(netAmount);
grandTotal = grandTotal.toFixed(2);
$("#net_amount").val(grandTotal);
$("#net_amount_value").val(grandTotal);
} else {
$("#net_amount").val(totalAmount);
$("#net_amount_value").val(totalAmount);
}
var paid_amount = Number($("#paid_amount").val()); var paid_amount = Number($("#paid_amount").val());
if (paid_amount) { if (paid_amount) {
var net_amount_value = Number($("#net_amount_value").val()); var net_amount_value = Number($("#net_amount_value").val());
var remaning = net_amount_value - paid_amount; var remaning = net_amount_value - paid_amount;
$("#remaining").val(remaning.toFixed(2)); $("#remaining").val(remaning.toFixed(2));
$("#remaining_value").val(remaning.toFixed(2)); $("#remaining_value").val(remaning.toFixed(2));
} }
updateMontantTranches(); updateMontantTranches();
} }
function paidAmount() { function paidAmount() {
var grandTotal = $("#net_amount_value").val(); var grandTotal = $("#net_amount_value").val();
@ -564,10 +637,13 @@
function getMontantPourTranches() { function getMontantPourTranches() {
var discount = parseFloat($("#discount").val()) || 0; var discount = parseFloat($("#discount").val()) || 0;
var grossAmount = parseFloat($("#gross_amount_value").val()) || 0; var grossAmount = parseFloat($("#gross_amount_value").val()) || 0;
return discount > 0 ? discount : grossAmount;
}
function updateMontantTranches() { // Si discount existe, on utilise gross_amount - discount
// Sinon on utilise gross_amount
return discount > 0 ? (grossAmount - discount) : grossAmount;
}
function updateMontantTranches() {
var montant = getMontantPourTranches(); var montant = getMontantPourTranches();
var discount = parseFloat($("#discount").val()) || 0; var discount = parseFloat($("#discount").val()) || 0;
@ -575,26 +651,25 @@
$("#montant_total_tranches_value").val(montant); $("#montant_total_tranches_value").val(montant);
if (discount > 0) { if (discount > 0) {
$("#montant_source_label").text("(Prix avec remise acceptée)"); $("#montant_source_label").text("(Prix avec remise)");
} else { } else {
$("#montant_source_label").text("(Montant brut)"); $("#montant_source_label").text("(Montant brut)");
} }
var paymentMode = $("#payment_mode").val(); var paymentMode = $("#payment_mode").val();
if (parseInt(paymentMode) === 1) { if (parseInt(paymentMode) === 1) {
$("#payment_amount_1").val(montant.toFixed(2)); $("#payment_amount_1").val(montant.toFixed(2));
} else { } else {
calculerTranche2(); calculerTranche2();
} }
} }
function calculerTranche2() {
function calculerTranche2() {
var montantTotal = getMontantPourTranches(); var montantTotal = getMontantPourTranches();
var tranche1 = parseFloat($("#payment_amount_1").val()) || 0; var tranche1 = parseFloat($("#payment_amount_1").val()) || 0;
var tranche2 = montantTotal - tranche1; var tranche2 = montantTotal - tranche1;
if (tranche2 < 0) tranche2 = 0; if (tranche2 < 0) tranche2 = 0;
$("#payment_amount_2").val(tranche2.toFixed(2)); $("#payment_amount_2").val(tranche2.toFixed(2));
} }
$("#discount").on('keyup', function() { $("#discount").on('keyup', function() {
subAmount(); subAmount();

201
app/Views/orders/index.php

@ -72,7 +72,23 @@
<?php <?php
$session = session(); $session = session();
$users = $session->get('user'); $users = $session->get('user');
if ($users['group_name'] === 'COMMERCIALE' || $users['group_name'] === 'Caissière' || $users['group_name'] === 'SECURITE' || $users['group_name'] === "Cheffe d'Agence") { // Interface spécifique pour SECURITE
if ($users['group_name'] === 'SECURITE') {
?>
<th>Nom du produit</th>
<th>Commerciale</th>
<th>Date et Heure</th>
<th>Marque</th>
<th>Numéro de Série</th>
<th>Status</th>
<?php if (
in_array('viewOrder', $user_permission)
|| in_array('updateOrder', $user_permission)
) { ?>
<th>Action</th>
<?php } ?>
<?php } elseif ($users['group_name'] === 'COMMERCIALE' || $users['group_name'] === 'Caissière' || $users['group_name'] === "Cheffe d'Agence") {
// Interface pour les autres rôles (COMMERCIALE, Caissière, Cheffe d'Agence)
?> ?>
<th>Nom du produit</th> <th>Nom du produit</th>
<th>Commerciale</th> <th>Commerciale</th>
@ -184,20 +200,14 @@
<!-- Tableau produits avec toutes les infos sur une ligne --> <!-- Tableau produits avec toutes les infos sur une ligne -->
<h4>Informations de la commande</h4> <h4>Informations de la commande</h4>
<table class="table table-bordered table-striped" id="view_products_table"> <table class="table table-bordered table-striped" id="view_products_table">
<thead> <thead>
<tr> <tr id="table_headers">
<th>Marque</th> <!-- Les en-têtes seront générés dynamiquement par JavaScript -->
<th>Désignation</th> </tr>
<th>Numéro de Série</th> </thead>
<th>N° de Moteur</th> <tbody></tbody>
<th>Châssis</th> </table>
<th>N° Facture</th>
<th>Statut</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@ -371,65 +381,92 @@
// ✅ FONCTION MODIFIÉE POUR AFFICHER LES NOUVELLES COLONNES // ✅ FONCTION MODIFIÉE POUR AFFICHER LES NOUVELLES COLONNES
$(document).on('click', '.btn-view', function(e) { $(document).on('click', '.btn-view', function(e) {
e.preventDefault(); e.preventDefault();
var orderId = $(this).data('order-id'); var orderId = $(this).data('order-id');
currentOrderId = orderId; // Stocker l'ID de la commande currentOrderId = orderId;
var url = base_url + 'orders/lookOrder/' + orderId; var url = base_url + 'orders/lookOrder/' + orderId;
// Réinitialiser les messages $('#modal-messages').empty();
$('#modal-messages').empty();
$.getJSON(url, function(response) {
// Requête AJAX pour récupérer les données JSON var d = response.order_data.order;
$.getJSON(url, function(response) {
var d = response.order_data.order; // Remplir les champs de la modal
$('#order_date').text(d.date_time || '');
// Remplir les champs de la modal $('#customer_name').text(d.customer_name);
$('#order_date').text(d.date_time || ''); $('#customer_address').text(d.customer_address);
$('#customer_name').text(d.customer_name); $('#customer_phone').text(d.customer_phone);
$('#customer_address').text(d.customer_address); $('#customer_cin').text(d.customer_cin);
$('#customer_phone').text(d.customer_phone);
$('#customer_cin').text(d.customer_cin); // Déterminer le statut une seule fois
var statutHtml = '';
// Déterminer le statut une seule fois if (d.paid_status == 1) {
var statutHtml = ''; statutHtml = '<span class="label label-success">Payé</span>';
if (d.paid_status == 1) { <?php if ($users['group_name'] === 'SECURITE'): ?>
statutHtml = '<span class="label label-success">Payé</span>'; $('#btn-mark-delivered').show();
<?php if ($users['group_name'] === 'SECURITE'): ?> <?php endif; ?>
$('#btn-mark-delivered').show(); } else if (d.paid_status == 2) {
<?php endif; ?> statutHtml = '<span class="label label-warning">En Attente</span>';
} else if (d.paid_status == 2) { $('#btn-mark-delivered').hide();
statutHtml = '<span class="label label-warning">En Attente</span>'; } else if (d.paid_status == 3) {
$('#btn-mark-delivered').hide(); statutHtml = '<span class="label label-info">Payé et Livré</span>';
} else if (d.paid_status == 3) { $('#btn-mark-delivered').hide();
statutHtml = '<span class="label label-info">Payé et Livré</span>'; } else {
$('#btn-mark-delivered').hide(); statutHtml = '<span class="label label-danger">Refusé</span>';
} else { $('#btn-mark-delivered').hide();
statutHtml = '<span class="label label-danger">Refusé</span>'; }
$('#btn-mark-delivered').hide();
} // ✅ GÉNÉRER LES EN-TÊTES SELON LE RÔLE
var $headers = $('#table_headers');
$headers.empty();
<?php if ($users['group_name'] === 'SECURITE'): ?>
// En-têtes pour SECURITE
$headers.append(
'<th>Marque</th>' +
'<th>Désignation</th>' +
'<th>Numéro de Série</th>' +
'<th>N° de Moteur</th>' +
'<th>Châssis</th>' +
'<th>N° Facture</th>' +
'<th>Statut</th>'
);
<?php else: ?>
// En-têtes pour les autres rôles
$headers.append(
'<th>Marque</th>' +
'<th>Désignation</th>' +
'<th>Prix de vente</th>' +
'<th>Prix demandé</th>' +
'<th>Remise</th>' +
'<th>Statut</th>'
);
<?php endif; ?>
// ✅ GÉNÉRER LES LIGNES DU TABLEAU SELON LE RÔLE
var $tb = $('#view_products_table tbody');
$tb.empty();
$.each(response.order_data.order_item, function(_, item) {
var product = null;
$.each(response.products, function(_, p) {
if (p.id == item.product_id) {
product = p;
return false;
}
});
// Produits avec les nouvelles colonnes if (product) {
var $tb = $('#view_products_table tbody'); var brandName = '';
$tb.empty(); $.each(response.brands, function(_, brand) {
$.each(response.order_data.order_item, function(_, item) { if (brand.id == product.marque) {
var product = null; brandName = brand.name;
$.each(response.products, function(_, p) {
if (p.id == item.product_id) {
product = p;
return false; return false;
} }
}); });
if (product) { <?php if ($users['group_name'] === 'SECURITE'): ?>
var brandName = ''; // ✅ AFFICHAGE POUR SECURITE
$.each(response.brands, function(_, brand) {
if (brand.id == product.marque) {
brandName = brand.name;
return false;
}
});
// ✅ AJOUT DES NOUVELLES COLONNES : N° Moteur, Châssis, N° Facture, Statut
$tb.append( $tb.append(
'<tr>' + '<tr>' +
'<td>' + (brandName || 'Aucune marque') + '</td>' + '<td>' + (brandName || 'Aucune marque') + '</td>' +
@ -441,12 +478,28 @@
'<td>' + statutHtml + '</td>' + '<td>' + statutHtml + '</td>' +
'</tr>' '</tr>'
); );
} <?php else: ?>
}); // ✅ AFFICHAGE POUR LES AUTRES RÔLES
var prixVente = parseFloat(product.prix_vente || 0);
var prixDemande = parseFloat(d.discount || 0); // Prix demandé = discount
var netAmount = parseFloat(d.net_amount || 0); // Remise = net_amount
// Afficher la modal $tb.append(
$('#viewOrderModal').modal('show'); '<tr>' +
'<td>' + (brandName || 'Aucune marque') + '</td>' +
'<td>' + (product.name || '') + '</td>' +
'<td>' + prixVente.toLocaleString('fr-FR') + ' Ar</td>' +
'<td>' + prixDemande.toLocaleString('fr-FR') + ' Ar</td>' +
'<td>' + netAmount.toLocaleString('fr-FR') + ' Ar</td>' +
'<td>' + statutHtml + '</td>' +
'</tr>'
);
<?php endif; ?>
}
}); });
});
// Afficher la modal
$('#viewOrderModal').modal('show');
});
});
</script> </script>
Loading…
Cancel
Save