@ -36,6 +36,31 @@ class OrderController extends AdminController
return $this->render_template('orders/index', $data);
return $this->render_template('orders/index', $data);
}
}
/**
* Génère un numéro de facture personnalisé selon le magasin
* @param int $store_id
* @return string
*/
private function generateBillNo(int $store_id): string
{
// Mapping des préfixes par magasin
$storePrefixes = [
1 => 'ANTS', // ANTSAKAVIRO
2 => 'BESA', // BESARETY
3 => 'BYPA', // BYPASS
4 => 'TOAM', // TOAMASINA
];
// Récupérer le préfixe du magasin, ou utiliser un préfixe par défaut
$prefix = $storePrefixes[$store_id] ?? 'BILPR';
// Générer un identifiant unique
$uniqueId = strtoupper(substr(md5(uniqid(mt_rand(), true)), 0, 6));
// Retourner le numéro de facture formaté
return $prefix . '-' . $uniqueId;
}
public function fetchOrdersData()
public function fetchOrdersData()
{
{
helper(['url', 'form']);
helper(['url', 'form']);
@ -335,13 +360,15 @@ class OrderController extends AdminController
$session = session();
$session = session();
$users = $session->get('user');
$users = $session->get('user');
$user_id = $users['id'];
$user_id = $users['id'];
$bill_no = 'BILPR-' . strtoupper(substr(md5(uniqid(mt_rand(), true)), 0, 4));
// ✅ UTILISER LA NOUVELLE MÉTHODE
$bill_no = $this->generateBillNo($users['store_id']);
// Récupération des produits
// Récupération des produits
$posts = $this->request->getPost('product[]');
$posts = $this->request->getPost('product[]');
$rates = $this->request->getPost('rate_value[]');
$rates = $this->request->getPost('rate_value[]');
$amounts = $this->request->getPost('amount_value[]');
$amounts = $this->request->getPost('amount_value[]');
$puissances = $this->request->getPost('puissance[]'); // ✅ AJOUTER CETTE LIGNE
$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);
@ -352,14 +379,12 @@ class OrderController extends AdminController
foreach ($posts as $index => $productId) {
foreach ($posts as $index => $productId) {
$productId = (int)$productId;
$productId = (int)$productId;
// Récupérer données produit + prix minimal
$productData = $Products->getProductData($productId);
$productData = $Products->getProductData($productId);
$fourchette = $FourchettePrix->getFourchettePrixByProductId($productId);
$fourchette = $FourchettePrix->getFourchettePrixByProductId($productId);
if ($fourchette) {
if ($fourchette) {
$prixMinimal = (float)$fourchette['prix_minimal'];
$prixMinimal = (float)$fourchette['prix_minimal'];
// ✅ Le rabais devient le prix de vente
if ($discount < $prixMinimal) {
if ($discount < $prixMinimal) {
$prixMinimalFormatted = number_format($prixMinimal, 0, ',', ' ');
$prixMinimalFormatted = number_format($prixMinimal, 0, ',', ' ');
$discountFormatted = number_format($discount, 0, ',', ' ');
$discountFormatted = number_format($discount, 0, ',', ' ');
@ -374,23 +399,19 @@ class OrderController extends AdminController
}
}
}
}
// ✅ NOUVELLE LOGIQUE : Calculer le montant à payer et net_amount
// ✅ Calculer le montant à payer et net_amount
$montant_a_payer = ($discount > 0) ? $discount : $gross_amount;
$montant_a_payer = ($discount > 0) ? $discount : $gross_amount;
// Récupérer les tranches
$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;
// Calculer net_amount selon les tranches
if ($tranche_1 > 0 & & $tranche_2 > 0) {
if ($tranche_1 > 0 & & $tranche_2 > 0) {
// Paiement en 2 tranches
$net_amount = $tranche_1 + $tranche_2;
$net_amount = $tranche_1 + $tranche_2;
} else {
} else {
// Paiement en 1 tranche ou pas de tranches
$net_amount = $montant_a_payer;
$net_amount = $montant_a_payer;
}
}
// ✅ Création de la commande avec la nouvelle logique
// ✅ Création de la commande
$data = [
$data = [
'bill_no' => $bill_no,
'bill_no' => $bill_no,
'customer_name' => $this->request->getPost('customer_name'),
'customer_name' => $this->request->getPost('customer_name'),
@ -408,7 +429,7 @@ class OrderController extends AdminController
'amount_value' => $amounts,
'amount_value' => $amounts,
'gross_amount' => $gross_amount,
'gross_amount' => $gross_amount,
'rate_value' => $rates,
'rate_value' => $rates,
'puissance' => $puissances, // ✅ AJOUTER CETTE LIGNE
'puissance' => $puissances,
'store_id' => $users['store_id'],
'store_id' => $users['store_id'],
'tranche_1' => $tranche_1,
'tranche_1' => $tranche_1,
'tranche_2' => $tranche_2,
'tranche_2' => $tranche_2,
@ -423,9 +444,8 @@ class OrderController extends AdminController
$Notification = new NotificationController();
$Notification = new NotificationController();
// ✅ WORKFLOW SELON LA REMISE
if ($discount > 0) {
if ($discount > 0) {
// AVEC REMISE : Créer demande + Notifier Conseil
// Logique demande de remise...
$Order_item1 = new OrderItems();
$Order_item1 = new OrderItems();
$order_item_data = $Order_item1->getOrdersItemData($order_id);
$order_item_data = $Order_item1->getOrdersItemData($order_id);
$product_ids = array_column($order_item_data, 'product_id');
$product_ids = array_column($order_item_data, 'product_id');
@ -462,7 +482,6 @@ class OrderController extends AdminController
$Remise = new Remise();
$Remise = new Remise();
$id_remise = $Remise->addDemande($data1);
$id_remise = $Remise->addDemande($data1);
// Notification au CONSEIL
$Notification->createNotification(
$Notification->createNotification(
"Nouvelle demande de remise à valider - Commande " . $bill_no,
"Nouvelle demande de remise à valider - Commande " . $bill_no,
"Direction",
"Direction",
@ -471,7 +490,6 @@ class OrderController extends AdminController
);
);
} else {
} else {
// SANS REMISE : Notifier directement la Caissière
$Notification->createNotification(
$Notification->createNotification(
"Nouvelle commande à valider - " . $bill_no,
"Nouvelle commande à valider - " . $bill_no,
"Caissière",
"Caissière",
@ -480,7 +498,6 @@ class OrderController extends AdminController
);
);
}
}
// Redirection selon le rôle
if ($users["group_name"] != "COMMERCIALE") {
if ($users["group_name"] != "COMMERCIALE") {
$this->checkProductisNull($posts, $users['store_id']);
$this->checkProductisNull($posts, $users['store_id']);
}
}
@ -714,6 +731,22 @@ public function getTableProductRow()
$paid_status = $this->request->getPost('paid_status');
$paid_status = $this->request->getPost('paid_status');
}
}
// ✅ AJOUT : TRACER LA VALIDATION PAR LE CAISSIER
$validated_by = $current_order['validated_by'] ?? null; // Garder l'ancienne valeur si existe
$validated_at = $current_order['validated_at'] ?? null;
// Si le statut passe à "Validé" (1) et que l'utilisateur est un caissier
if ($old_paid_status != 1 & & $paid_status == 1 & & $role === 'Caissière') {
$validated_by = $user['id'];
$validated_at = date('Y-m-d H:i:s');
}
// Si le statut repasse à "En attente" ou "Refusé", effacer la validation
if (in_array($paid_status, [0, 2])) {
$validated_by = null;
$validated_at = null;
}
$discount = $this->request->getPost('discount');
$discount = $this->request->getPost('discount');
$original_discount = $this->request->getPost('original_discount');
$original_discount = $this->request->getPost('original_discount');
if ($discount === '' || $discount === null) {
if ($discount === '' || $discount === null) {
@ -737,11 +770,14 @@ public function getTableProductRow()
'product_sold' => true,
'product_sold' => true,
'rate_value' => $this->request->getPost('rate_value'),
'rate_value' => $this->request->getPost('rate_value'),
'amount_value' => $this->request->getPost('amount_value'),
'amount_value' => $this->request->getPost('amount_value'),
'puissance' => $this->request->getPost('puissance'), // ✅ AJOUT PUISSANCE
'puissance' => $this->request->getPost('puissance'),
'tranche_1' => $role !== 'COMMERCIALE' ? $this->request->getPost('tranche_1') : null,
'tranche_1' => $role !== 'COMMERCIALE' ? $this->request->getPost('tranche_1') : null,
'tranche_2' => $role !== 'COMMERCIALE' ? $this->request->getPost('tranche_2') : null,
'tranche_2' => $role !== 'COMMERCIALE' ? $this->request->getPost('tranche_2') : null,
'order_payment_mode' => $role !== 'COMMERCIALE' ? $this->request->getPost('order_payment_mode_1') : null,
'order_payment_mode' => $role !== 'COMMERCIALE' ? $this->request->getPost('order_payment_mode_1') : null,
'order_payment_mode_1' => $role !== 'COMMERCIALE' ? $this->request->getPost('order_payment_mode_2') : null
'order_payment_mode_1' => $role !== 'COMMERCIALE' ? $this->request->getPost('order_payment_mode_2') : null,
// ✅ AJOUT DES CHAMPS DE TRACABILITÉ
'validated_by' => $validated_by,
'validated_at' => $validated_at
];
];
if ($Orders->updates($id, $dataUpdate)) {
if ($Orders->updates($id, $dataUpdate)) {
@ -760,6 +796,16 @@ public function getTableProductRow()
(int)$user['store_id'],
(int)$user['store_id'],
'orders'
'orders'
);
);
// ✅ AJOUT : Notification pour la Direction quand un caissier valide
if ($role === 'Caissière') {
$Notification->createNotification(
"Commande validée par la caisse: {$bill_no}",
"Direction",
(int)$user['store_id'],
'orders'
);
}
}
}
if ((float)$discount > 0) {
if ((float)$discount > 0) {
@ -1608,12 +1654,10 @@ public function print5(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);
@ -1628,7 +1672,6 @@ public function print5(int $id)
}
}
}
}
// Calculs
$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;
@ -1638,39 +1681,205 @@ public function print5(int $id)
$paidLabel = $order['paid_status'] == 1 ? 'Payé' : 'Non payé';
$paidLabel = $order['paid_status'] == 1 ? 'Payé' : 'Non payé';
// Début 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 > Facture '.$order['bill_no'].'< / title >
< title > Facture '.$order['bill_no'].'< / title >
< style >
< style >
body { font-family: Arial, sans-serif; font-size:14px; color:#000;margin:0; padding:0; }
/* ✅ FORMAT A4 PAYSAGE DIVISÉ EN 2 */
.header { display:flex; justify-content:space-between; align-items:center; margin-bottom:20px; }
@page {
.header .infos { line-height:1.4; }
size: A4 landscape;
.header img { max-height:80px; }
margin: 0;
.client { margin-bottom:20px; }
}
table { width:100%; border-collapse:collapse; margin-bottom:20px; }
th, td { border:1px solid #000; padding:6px; }
body {
th { background:#f0f0f0; }
font-family: Arial, sans-serif;
.right { text-align:right; }
font-size: 10px;
.signature { display:flex; justify-content:space-between; margin-top:50px; }
color: #000;
.signature div { text-align:center; }
margin: 0;
.conditions { page-break-before: always; padding:20px; line-height:1.5; }
padding: 0;
}
/* ✅ CONTENEUR : 2 COLONNES CÔTE À CÔTE */
.page {
display: flex;
width: 297mm;
height: 210mm;
margin: 0;
padding: 0;
}
/* ✅ CHAQUE FACTURE = 50% DE LA LARGEUR */
.facture-box {
flex: 1;
width: 148.5mm;
padding: 10mm;
box-sizing: border-box;
border-right: 2px dashed #999;
}
.facture-box:last-child {
border-right: none;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.header .infos {
line-height: 1.4;
}
.header .infos h2 {
margin: 0;
font-size: 14px;
}
.header .infos p {
margin: 2px 0;
font-size: 9px;
}
.header img {
max-height: 60px;
}
.header p.facture-num {
margin: 5px 0;
font-weight: bold;
font-size: 10px;
}
.client {
margin-bottom: 15px;
}
.client p {
margin: 3px 0;
font-size: 9px;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 15px;
font-size: 9px;
}
th, td {
border: 1px solid #000;
padding: 4px;
}
th {
background: #f0f0f0;
}
.right {
text-align: right;
}
.words-box {
border: 1px solid #000;
padding: 8px;
margin-bottom: 20px;
font-size: 9px;
}
.words-box strong {
font-size: 9px;
}
.signature {
display: flex;
justify-content: space-between;
margin-top: 30px;
}
.signature div {
text-align: center;
font-size: 9px;
}
/* ✅ CONDITIONS SUR PAGE SÉPARÉE (VERSO) */
.conditions-page {
page-break-before: always;
display: flex;
width: 297mm;
height: 210mm;
margin: 0;
padding: 0;
}
.conditions-box {
flex: 1;
width: 148.5mm;
padding: 15px;
box-sizing: border-box;
line-height: 1.5;
border-right: 2px dashed #999;
}
.conditions-box:last-child {
border-right: none;
}
.conditions-box h3 {
margin: 0 0 15px 0;
font-size: 12px;
}
.conditions-box ul {
margin: 0;
padding-left: 20px;
font-size: 9px;
}
.conditions-box li {
margin-bottom: 8px;
}
.conditions-box .buyer-signature {
text-align: center;
margin-top: 40px;
font-size: 10px;
}
.conditions-box img {
height: 50px;
}
@media print {
* {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
< / style >
< / style >
< / head >
< / head >
< body onload = "window.print()" >
< body onload = "window.print()" >
<!-- ✅ PAGE 1 : RECTO - 2 FACTURES CÔTE À CÔTE -->
< div class = "page" > ';
// ✅ GÉNÉRER 2 FACTURES IDENTIQUES
for ($i = 0; $i < 2 ; $ i + + ) {
$html .= '
< div class = "facture-box" >
< div class = "header" >
< div class = "header" >
< div class = "infos" >
< div class = "infos" >
< h2 style = "margin:0;" > '.esc($company['company_name']).'< / h2 >
< h2 > '.esc($company['company_name']).'< / h2 >
< p style = "margin:2px 0;" > < strong > NIF :< / strong > '.esc($company['NIF']).'< / p >
< p > < strong > NIF :< / strong > '.esc($company['NIF']).'< / p >
< p style = "margin:2px 0;" > < strong > STAT :< / strong > '.esc($company['STAT']).'< / p >
< p > < strong > STAT :< / strong > '.esc($company['STAT']).'< / p >
< p style = "margin:2px 0;" > < strong > Contact :< / strong > '.esc($company['phone']).' | '.esc($company['phone2']).'< / p >
< p > < strong > Contact :< / strong > '.esc($company['phone']).' | '.esc($company['phone2']).'< / p >
< / 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;" > Facture N° '.esc($order['bill_no']).'< / p >
< p class = "facture-num "> Facture N° '.esc($order['bill_no']).'< / p >
< / div >
< / div >
< / div >
< / div >
@ -1684,9 +1893,6 @@ public function print5(int $id)
// ✅ TABLEAU ADAPTÉ SELON LE TYPE
// ✅ TABLEAU ADAPTÉ SELON LE TYPE
if ($isAvanceMere) {
if ($isAvanceMere) {
// ========================================
// TABLE SIMPLIFIÉE POUR AVANCE "SUR MER"
// ========================================
$html .= '
$html .= '
< table >
< table >
< thead >
< thead >
@ -1704,10 +1910,10 @@ public function print5(int $id)
$prixAffiche = ($discount > 0) ? $discount : $details['prix'];
$prixAffiche = ($discount > 0) ? $discount : $details['prix'];
$html .= '< tr > < 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:12 px; color:#666;" > '.esc($details['commentaire']).'< / em > ';
$html .= '< br > < em style = "font-size:8 px; color:#666;" > '.esc($details['commentaire']).'< / em > ';
}
}
$html .= '< / td >
$html .= '< / td >
@ -1715,10 +1921,12 @@ public function print5(int $id)
< / tr > ';
< / tr > ';
}
}
// ✅ CORRECTION : Fermer le tableau pour avance
$html .= '
< / tbody >
< / table > ';
} else {
} else {
// ========================================
// TABLE COMPLÈTE POUR AVANCE "SUR TERRE" OU COMMANDE NORMALE
// ========================================
$html .= '
$html .= '
< table >
< table >
< thead >
< thead >
@ -1740,23 +1948,24 @@ public function print5(int $id)
$prixAffiche = ($discount > 0) ? $discount : $details['prix'];
$prixAffiche = ($discount > 0) ? $discount : $details['prix'];
$html .= '
$html .= '
< tr >
< tr >
< td > '.esc($details['marque']).'< / td >
< td > '.esc($details['marque']).'< / td >
< td > '.esc($details['product_name']).'< / td >
< td > '.esc($details['product_name']).'< / td >
< td > '.esc($details['numero_moteur']).'< / td >
< td > '.esc($details['numero_moteur']).'< / td >
< td > '.esc($details['numero_chassis']).'< / td >
< td > '.esc($details['numero_chassis']).'< / td >
< td > '.esc($details['puissance']).'< / td > <!-- ✅ ICI -->
< td > '.esc($details['puissance']).'< / td >
< td class = "right" > '.number_format($prixAffiche, 0, '', ' ').'< / td >
< td class = "right" > '.number_format($prixAffiche, 0, '', ' ').'< / td >
< / tr > ';
< / tr > ';
}
}
}
// ✅ Fermer le tableau pour produit normal
$html .= '
$html .= '
< / tbody >
< / tbody >
< / table >
< / table > ';
}
$html .= '
< table >
< table >
< tr >
< tr >
< td > < strong > Prix (HT) :< / strong > < / td >
< td > < strong > Prix (HT) :< / strong > < / td >
@ -1772,7 +1981,7 @@ public function print5(int $id)
< / tr >
< / tr >
< / table >
< / table >
< div style = "border:1px solid #000; padding:10px; margin-bottom:30px; ">
< div class = "words-box ">
< strong > Arrêté à la somme de :< / strong > < br >
< strong > Arrêté à la somme de :< / strong > < br >
'.$inWords.'
'.$inWords.'
< / div >
< / div >
@ -1781,12 +1990,22 @@ public function print5(int $id)
< div > L\'Acheteur< br > < br > __________________< / div >
< div > L\'Acheteur< br > < br > __________________< / div >
< div > Le Vendeur< br > < br > __________________< / div >
< div > Le Vendeur< br > < br > __________________< / div >
< / div >
< / div >
< / div > ';
}
<!-- Conditions Générales avec saut de page -->
$html .= '
< div class = "conditions" >
< / div >
<!-- ✅ PAGE 2 : VERSO - 2 CONDITIONS GÉNÉRALES CÔTE À CÔTE -->
< div class = "conditions-page" > ';
// ✅ GÉNÉRER 2 CONDITIONS IDENTIQUES
for ($i = 0; $i < 2 ; $ i + + ) {
$html .= '
< div class = "conditions-box" >
< 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 > Conditions Générales< / h3 >
< img src = "'.base_url('assets/images/company_logo.jpg').'" alt = "Logo" style = "height:60px;" >
< img src = "'.base_url('assets/images/company_logo.jpg').'" alt = "Logo" >
< / div >
< / div >
< ul >
< ul >
< li > Aucun accessoire (casque, rétroviseur, batterie, etc.) n\'est inclus avec la moto. Si le client en a besoin, il doit les acheter séparément.< / li >
< li > Aucun accessoire (casque, rétroviseur, batterie, etc.) n\'est inclus avec la moto. Si le client en a besoin, il doit les acheter séparément.< / li >
@ -1795,7 +2014,11 @@ public function print5(int $id)
< li > La moto est vendue sans garantie, car il s\'agit d\'un modèle d\'occasion.< / li >
< li > La moto est vendue sans garantie, car il s\'agit d\'un modèle d\'occasion.< / li >
< li > La facture étant un document provisoire ne peut se substituer au certificat modèle (si requis) délivré au client au moment de l\'achat. Il appartient à ce dernier de procéder à l\'immatriculation dans le délai prévu par la loi.< / li >
< li > La facture étant un document provisoire ne peut se substituer au certificat modèle (si requis) délivré au client au moment de l\'achat. Il appartient à ce dernier de procéder à l\'immatriculation dans le délai prévu par la loi.< / li >
< / ul >
< / ul >
< div style = "text-align:center; margin-top:50px;" > L\'Acheteur< / div >
< div class = "buyer-signature" > L\'Acheteur< / div >
< / div > ';
}
$html .= '
< / div >
< / div >
< / body >
< / body >