insert($data); } catch (\Exception $e) { log_message('error', 'Erreur lors de l\'ajout de l\'avance : ' . $e->getMessage()); return false; } } public function updateAvance(int $id, array $data) { if ($id <= 0) { log_message('error', 'ID invalide pour la mise à jour du recouvrement : ' . $id); return false; } try { if (!empty($data['type']) && !empty($data['avance_date'])) { if (strtolower($data['type']) === 'avance sur terre') { $data['deadline'] = date('Y-m-d', strtotime($data['avance_date'] . ' +15 days')); } elseif (strtolower($data['type']) === 'avance sur mer') { $data['deadline'] = date('Y-m-d', strtotime($data['avance_date'] . ' +2 months')); } } return $this->update($id, $data); } catch (\Exception $e) { log_message('error', 'Erreur lors de la mise à jour de l\'avance : ' . $e->getMessage()); return false; } } public function getAllAvanceData(int $id=null) { $session = session(); $users = $session->get('user'); $isAdmin = in_array($users['group_name'], ['Conseil', 'Direction']); if($isAdmin) { if($id){ try { return $this->where('user_id',$id) ->where('is_order',0) ->where('active',1) ->orderBy('avance_date', 'DESC') ->findAll(); } catch (\Exception $e) { log_message('error', 'Erreur lors de la récupération des recouvrements : ' . $e->getMessage()); return false; } } try { return $this ->where('is_order',0) ->where('active',1) ->orderBy('avance_date', 'DESC') ->findAll(); } catch (\Exception $e) { log_message('error', 'Erreur lors de la récupération des recouvrements : ' . $e->getMessage()); return false; } } else { if($id){ try { return $this->where('user_id',$id) ->where('is_order',0) ->where('active',1) ->where('store_id',$users['store_id']) ->orderBy('avance_date', 'DESC') ->findAll(); } catch (\Exception $e) { log_message('error', 'Erreur lors de la récupération des recouvrements : ' . $e->getMessage()); return false; } } try { return $this ->where('is_order',0) ->where('active',1) ->where('store_id',$users['store_id']) ->orderBy('avance_date', 'DESC') ->findAll(); } catch (\Exception $e) { log_message('error', 'Erreur lors de la récupération des recouvrements : ' . $e->getMessage()); return false; } } } public function fetchSingleAvance(int $avance_id){ return $this->select('avances.*, products.name as product_name_db, products.prix_vente as product_price') ->join('products', 'products.id = avances.product_id', 'left') ->where('avances.avance_id', $avance_id) ->first(); } public function removeAvance(int $avance_id){ return $this->delete($avance_id); } // ✅ CORRECTION : getTotalAvance pour la caissière public function getTotalAvance() { $session = session(); $users = $session->get('user'); $isAdmin = in_array($users['group_name'], ['Conseil', 'Direction']); try { $builder = $this->select('SUM(avance_amount) AS ta') ->where('is_order', 0) ->where('active', 1); // ✅ Ajout du filtre active if (!$isAdmin) { $builder->where('store_id', $users['store_id']); // ✅ Filtre par store pour caissière } return $builder->get()->getRowObject(); } catch (\Exception $e) { log_message('error', 'Erreur lors du total du montant des avances : ' . $e->getMessage()); return (object) ['ta' => 0]; // ✅ Retourner un objet avec ta = 0 en cas d'erreur } } // ✅ CORRECTION PRINCIPALE : getPaymentModesAvance pour la caissière public function getPaymentModesAvance() { $session = session(); $users = $session->get('user'); $isAdmin = in_array($users['group_name'], ['Conseil', 'Direction']); try { $builder = $this->db->table('avances') ->select(' SUM(avance_amount) AS total, SUM(CASE WHEN LOWER(type_payment) = "mvola" THEN avance_amount ELSE 0 END) AS total_mvola, SUM(CASE WHEN LOWER(type_payment) = "en espèce" THEN avance_amount ELSE 0 END) AS total_espece, SUM(CASE WHEN LOWER(type_payment) = "virement bancaire" THEN avance_amount ELSE 0 END) AS total_virement_bancaire ') ->where('active', 1) ->where('is_order', 0); // ✅ Exclure les avances devenues orders // ✅ CORRECTION : Ajouter le filtre store_id pour la caissière if (!$isAdmin) { $builder->where('store_id', $users['store_id']); } $result = $builder->get()->getRowObject(); // ✅ Gérer le cas où il n'y a pas de résultats if (!$result) { return (object) [ 'total' => 0, 'total_mvola' => 0, 'total_espece' => 0, 'total_virement_bancaire' => 0 ]; } return $result; } catch (\Exception $e) { log_message('error', 'Erreur getPaymentModesAvance: ' . $e->getMessage()); return (object) [ 'total' => 0, 'total_mvola' => 0, 'total_espece' => 0, 'total_virement_bancaire' => 0 ]; } } public function getAllAvanceData1(int $id=null) { $session = session(); $users = $session->get('user'); $isAdmin = in_array($users['group_name'], ['Conseil', 'Direction']); if($isAdmin) { if($id){ try { return $this->where('user_id',$id) ->where('is_order',1) ->where('active',1) ->orderBy('avance_date', 'DESC') ->findAll(); } catch (\Exception $e) { log_message('error', 'Erreur lors de la récupération des recouvrements : ' . $e->getMessage()); return false; } } try { return $this ->where('is_order',1) ->where('active',1) ->orderBy('avance_date', 'DESC') ->findAll(); } catch (\Exception $e) { log_message('error', 'Erreur lors de la récupération des recouvrements : ' . $e->getMessage()); return false; } } else { if($id){ try { return $this->where('user_id',$id) ->where('is_order',1) ->where('active',1) ->where('store_id',$users['store_id']) ->orderBy('avance_date', 'DESC') ->findAll(); } catch (\Exception $e) { log_message('error', 'Erreur lors de la récupération des recouvrements : ' . $e->getMessage()); return false; } } try { return $this ->where('is_order',1) // ✅ Correction: devrait être 1, pas 0 ->where('active',1) // ✅ Ajout du filtre active ->where('store_id',$users['store_id']) // ✅ Ajout du filtre store ->orderBy('avance_date', 'DESC') ->findAll(); } catch (\Exception $e) { log_message('error', 'Erreur lors de la récupération des recouvrements : ' . $e->getMessage()); return false; } } } public function getAllAvanceData2(int $id=null) { $session = session(); $users = $session->get('user'); $isAdmin = in_array($users['group_name'], ['Conseil', 'Direction']); if($isAdmin) { if($id){ try { return $this->where('user_id',$id) ->where('is_order',0) ->where('active',0) ->orderBy('avance_date', 'DESC') ->findAll(); } catch (\Exception $e) { log_message('error', 'Erreur lors de la récupération des recouvrements : ' . $e->getMessage()); return false; } } try { return $this ->where('is_order',0) ->where('active',0) ->orderBy('avance_date', 'DESC') ->findAll(); } catch (\Exception $e) { log_message('error', 'Erreur lors de la récupération des recouvrements : ' . $e->getMessage()); return false; } } else { if($id){ try { return $this->where('user_id',$id) ->where('is_order',0) ->where('active',0) ->where('store_id',$users['store_id']) ->orderBy('avance_date', 'DESC') ->findAll(); } catch (\Exception $e) { log_message('error', 'Erreur lors de la récupération des recouvrements : ' . $e->getMessage()); return false; } } try { return $this ->where('is_order',0) ->where('active',0) ->where('store_id',$users['store_id']) ->orderBy('avance_date', 'DESC') ->findAll(); } catch (\Exception $e) { log_message('error', 'Erreur lors de la récupération des recouvrements : ' . $e->getMessage()); return false; } } } public function checkExpiredAvance() { $now = date('Y-m-d'); $avances = $this->where('active', '1') ->where('deadline <', $now) ->findAll(); if (!empty($avances)) { $productModel = new Products(); foreach ($avances as $avance) { $this->update($avance['avance_id'], ['active' => '0']); if (!empty($avance['product_id'])) { // ✅ Vérifier que product_id existe $productModel->update($avance['product_id'], ['product_sold' => 0]); } } } } public function getAvancesNearDeadline($days = 3) { $alertDate = date('Y-m-d', strtotime("+{$days} days")); return $this->select('avances.*, users.store_id') ->join('users', 'users.id = avances.user_id') ->where('avances.is_order', 0) ->where('avances.active', 1) ->where('avances.amount_due >', 0) ->where('DATE(avances.deadline)', $alertDate) ->findAll(); } public function getIncompleteAvances(int $id = null) { $session = session(); $users = $session->get('user'); $isAdmin = in_array($users['group_name'], ['Conseil', 'Direction']); $builder = $this->where('is_order', 0) ->where('active', 1) ->where('amount_due >', 0); if (!$isAdmin) { $builder->where('store_id', $users['store_id']); } if ($id) { $builder->where('user_id', $id); } return $builder->orderBy('avance_date', 'DESC')->findAll(); } public function getCompletedAvances(int $id = null) { $session = session(); $users = $session->get('user'); $isAdmin = in_array($users['group_name'], ['Conseil', 'Direction']); $builder = $this->where('is_order', 0) ->where('active', 1) ->where('amount_due', 0); if (!$isAdmin) { $builder->where('store_id', $users['store_id']); } if ($id) { $builder->where('user_id', $id); } return $builder->orderBy('avance_date', 'DESC')->findAll(); } public function markAsPrinted($avance_id) { try { return $this->update($avance_id, [ 'is_printed' => 1 ]); } catch (\Exception $e) { log_message('error', 'Erreur markAsPrinted: ' . $e->getMessage()); return false; } } /** * Marquer une avance comme non imprimée (quand elle est modifiée) */ public function markAsNotPrinted($avance_id) { try { return $this->update($avance_id, [ 'is_printed' => 0, 'last_modified_at' => date('Y-m-d H:i:s') ]); } catch (\Exception $e) { log_message('error', 'Erreur markAsNotPrinted: ' . $e->getMessage()); return false; } } /** * Vérifier si une avance a déjà été imprimée */ public function isPrinted($avance_id) { try { $avance = $this->find($avance_id); return $avance ? (bool)$avance['is_printed'] : false; } catch (\Exception $e) { log_message('error', 'Erreur isPrinted: ' . $e->getMessage()); return false; } } /** * Récupérer un produit avec le nom de sa marque * @param int $product_id * @return array|null */ public function getProductWithBrand($product_id) { try { return $this->select('products.*, brands.name as brand_name') ->join('brands', 'brands.id = products.marque', 'left') ->where('products.id', $product_id) ->first(); } catch (\Exception $e) { log_message('error', 'Erreur getProductWithBrand: ' . $e->getMessage()); return null; } } // À ajouter dans App\Models\Avance.php /** * ✅ Convertir une avance complète en commande * Appelé automatiquement quand amount_due atteint 0 * * @param int $avance_id * @return int|false ID de la commande créée ou false */ public function convertToOrder(int $avance_id) { try { $avance = $this->find($avance_id); if (!$avance) { log_message('error', "Avance introuvable : {$avance_id}"); return false; } // ✅ MODIFICATION PRINCIPALE : Vérifier que c'est bien une avance sur 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)"); return false; } // ✅ Vérifier que l'avance est bien complète if ((float)$avance['amount_due'] > 0) { log_message('warning', "Avance TERRE {$avance_id} non complète (amount_due={$avance['amount_due']})"); return false; } // ✅ Vérifier qu'elle n'a pas déjà été convertie if ((int)$avance['is_order'] === 1) { log_message('info', "Avance TERRE {$avance_id} déjà convertie en commande"); return false; } // ✅ Vérifier que le produit existe (obligatoire pour avance TERRE) if (empty($avance['product_id'])) { log_message('error', "Avance TERRE {$avance_id} sans product_id - Impossible de convertir"); return false; } $db = \Config\Database::connect(); $db->transStart(); // ✅ 1. Créer la commande $orderModel = new \App\Models\Orders(); $bill_no = 'BILAV-' . strtoupper(substr(md5(uniqid(mt_rand(), true)), 0, 4)); $orderData = [ 'bill_no' => $bill_no, 'customer_name' => $avance['customer_name'], 'customer_address' => $avance['customer_address'], 'customer_phone' => $avance['customer_phone'], 'customer_cin' => $avance['customer_cin'], 'date_time' => date('Y-m-d H:i:s'), 'gross_amount' => $avance['gross_amount'], 'net_amount' => $avance['gross_amount'], 'discount' => 0, 'paid_status' => 2, // En attente validation caissière 'user_id' => $avance['user_id'], 'store_id' => $avance['store_id'], 'service_charge_rate' => 0, 'vat_charge_rate' => 0, 'vat_charge' => 0, 'tranche_1' => $avance['avance_amount'], 'tranche_2' => null, 'order_payment_mode' => $avance['type_payment'] ?? 'En espèce', 'order_payment_mode_1' => null, ]; $order_id = $orderModel->insert($orderData); if (!$order_id) { throw new \Exception("Échec création commande pour avance TERRE {$avance_id}"); } log_message('info', "✅ Commande {$bill_no} créée depuis avance TERRE {$avance_id}"); // ✅ 2. Créer les items de commande $orderItemModel = new \App\Models\OrderItems(); $productModel = new \App\Models\Products(); $product = $productModel->find($avance['product_id']); 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)"); // ✅ Le produit reste marqué comme vendu (product_sold = 1) // Il sera géré dans la commande maintenant } else { log_message('warning', "Produit {$avance['product_id']} introuvable pour avance TERRE {$avance_id}"); } // ✅ 3. Marquer l'avance comme convertie $this->update($avance_id, [ 'is_order' => 1, 'active' => 0, // Désactiver l'avance (elle devient commande) ]); $db->transComplete(); if ($db->transStatus() === false) { log_message('error', "Transaction échouée pour avance TERRE {$avance_id}"); return false; } // ✅ 4. Notification à la caissière $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})"); return $order_id; } catch (\Exception $e) { log_message('error', "Erreur conversion avance TERRE→commande : " . $e->getMessage()); return false; } } /** * ✅ Hook appelé automatiquement lors du paiement d'une avance * Intégrer ceci dans votre fonction de paiement existante */ public function afterPayment(int $avance_id) { $avance = $this->find($avance_id); if (!$avance) { return false; } // ✅ Si l'avance est maintenant complète ET que c'est une avance TERRE if ((float)$avance['amount_due'] <= 0 && $avance['type_avance'] === 'terre') { log_message('info', "💰 Avance TERRE {$avance_id} complète ! Conversion automatique en commande..."); return $this->convertToOrder($avance_id); } // ✅ Si c'est une avance MER complète, on ne fait rien (elle reste dans la liste) if ((float)$avance['amount_due'] <= 0 && $avance['type_avance'] === 'mere') { log_message('info', "💰 Avance MER {$avance_id} complète ! Elle reste dans la liste des avances."); } return true; } /** * ✅ Générer un numéro de facture unique */ private function generateBillNumber($store_id) { $db = \Config\Database::connect(); // Récupérer le dernier numéro pour ce store $query = $db->query( "SELECT bill_no FROM orders WHERE store_id = ? ORDER BY id DESC LIMIT 1", [$store_id] ); $result = $query->getRow(); if ($result && preg_match('/(\d+)$/', $result->bill_no, $matches)) { $lastNumber = intval($matches[1]); $newNumber = $lastNumber + 1; } else { $newNumber = 1; } // Format: BILL-STORE{store_id}-{number} return 'BILL-STORE' . $store_id . '-' . str_pad($newNumber, 5, '0', STR_PAD_LEFT); } /** * ✅ Récupérer toutes les avances complètes non converties */ public function getCompletedNotConverted() { return $this->where('amount_due', 0) ->where('is_order', 0) ->where('active', 1) ->where('type_avance', 'terre') // ✅ Uniquement TERRE à convertir ->findAll(); } /** * ✅ NOUVELLE MÉTHODE : Récupérer les avances MER complètes (pour statistiques) */ public function getCompletedMerAvances() { $session = session(); $users = $session->get('user'); $isAdmin = in_array($users['group_name'], ['Conseil', 'Direction']); $builder = $this->where('amount_due', 0) ->where('active', 1) ->where('type_avance', 'mere'); if (!$isAdmin) { $builder->where('store_id', $users['store_id']); } return $builder->orderBy('avance_date', 'DESC')->findAll(); } }