<?php
namespace App\Controllers;
use DateTime;
use Mpdf\Mpdf;
use App\Models\Brands;
use App\Models\Caisse;
use App\Models\Orders;
use App\Models\Stores;
use App\Models\Company;
use App\Models\Category;
use App\Models\Products;
use App\Models\Attributes;
use App\Models\OrderItems;
use App\Models\Remise;
use App\Models\FourchettePrix;
use PhpParser\Node\Stmt\Else_;
class OrderController extends AdminController
{
public function __construct()
{
parent::__construct();
}
private $pageTitle = 'Orders';
public function index()
{
$this->verifyRole('viewOrder');
$data['page_title'] = $this->pageTitle;
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 generateSimpleSequentialBillNo(int $store_id): string
{
$storePrefixes = [
1 => 'ANTS',
2 => 'BESA',
3 => 'BYPA',
4 => 'TOAM',
];
$prefix = $storePrefixes[$store_id] ?? 'STORE';
$db = \Config\Database::connect();
$lastBill = $db->table('orders')
->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()
{
helper(['url', 'form']);
$Orders = new Orders();
$result = ['data' => []];
$data = $Orders->getOrdersData();
$session = session();
$users = $session->get('user');
// ========================================
// POUR CAISSIÈRE
// ========================================
if ($users['group_name'] == "Caissière") {
$Remise = new Remise();
foreach ($data as $key => $value) {
$date_time = date('d-m-Y h:i a', strtotime($value['date_time']));
$buttons = '';
$discount = (float)$value['discount'];
// ✅ VÉRIFICATION : Si la commande est refusée (paid_status = 0), aucun bouton
if ($value['paid_status'] == 0) {
$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
if (in_array('viewOrder', $this->permission)) {
// Afficher pour toutes les commandes sauf celles refusées
// Et pour les commandes en attente : seulement si pas de remise OU remise validée
if ($value['paid_status'] == 2) {
// En attente : vérifier la remise
if ($discount == 0 || $Remise->hasRemiseValidatedForOrder($value['id'])) {
$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 > ';
}
} 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 (seulement si paid_status = 2)
if (in_array('updateOrder', $this->permission)
& & $users["store_id"] == $value['store_id']
& & $value['paid_status'] == 2) {
// 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
if ($value['paid_status'] == 1) {
$paid_status = '< span class = "label label-success" > Payé< / span > ';
} elseif ($value['paid_status'] == 2) {
$paid_status = '< span class = "label label-warning" > En Attente< / span > ';
} elseif ($value['paid_status'] == 3) {
$paid_status = '< span class = "label label-info" > Payé et Livré< / span > ';
} else {
$paid_status = '< span class = "label label-danger" > Refusé< / span > ';
}
// Calcul délai
$date1 = new DateTime($date_time);
$date2 = new DateTime();
$interval = $date1->diff($date2);
$daysPassed = $interval->days;
$statuDate = '';
if ($value['paid_status'] == 2) {
if ($daysPassed < 8 ) {
$statuDate = '< span class = "label label-success" > depuis ' . $daysPassed . ' Jours< / span > ';
} elseif ($daysPassed >= 8 & & $daysPassed < 15 ) {
$statuDate = '< span class = "label label-warning" > depuis ' . $daysPassed . ' Jours< / span > ';
} else {
$statuDate = '< span class = "label label-danger" > depuis ' . $daysPassed . ' Jours< / span > ';
}
}
$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);
}
// ========================================
// POUR DIRECTION OU DAF
// ========================================
elseif(in_array($users['group_name'], ["Direction", "DAF", "SuperAdmin", "Administrator"])){
foreach ($data as $key => $value) {
$date_time = date('d-m-Y h:i a', strtotime($value['date_time']));
$buttons = '';
// Bouton imprimer
if (in_array('viewOrder', $this->permission) & & $users['group_name'] != "SECURITE" & & $users['group_name'] != "COMMERCIALE") {
$buttons .= '< a target = "_blank" href = "' . site_url('orders/printDiv/' . $value['id']) . '" class = "btn btn-default" > < i class = "fa fa-print" > < / i > < / a > ';
}
// ✅ Bouton modifier pour statuts 0 (Refusé) et 2 (En Attente)
if (in_array('updateOrder', $this->permission) & & in_array($value['paid_status'], [0, 2])) {
$buttons .= ' < a href = "' . site_url('orders/update/' . $value['id']) . '" class = "btn btn-default" > < i class = "fa fa-pencil" > < / i > < / a > ';
}
// ✅ Bouton supprimer pour statuts 0 et 2
if (in_array('deleteOrder', $this->permission) & & in_array($value['paid_status'], [0, 2])) {
$buttons .= ' < button type = "button" class = "btn btn-danger" onclick = "removeFunc(' . $value['id'] . ')" data-toggle = "modal" data-target = "#removeModal" > < i class = "fa fa-trash" > < / i > < / button > ';
}
// Statut de paiement
if ($value['paid_status'] == 1) {
$paid_status = '< span class = "label label-success" > payé< / span > ';
} elseif ($value['paid_status'] == 2) {
$paid_status = '< span class = "label label-warning" > En Attente< / span > ';
} elseif ($value['paid_status'] == 3) {
$paid_status = '< span class = "label label-info" > Payé et Livré< / span > ';
} else {
$paid_status = '< span class = "label label-danger" > Refusé< / span > ';
}
// Calcul délai
$date1 = new DateTime($date_time);
$date2 = new DateTime();
$interval = $date1->diff($date2);
$daysPassed = $interval->days;
$statuDate = '';
if ($value['paid_status'] == 2) {
if ($daysPassed < 8 ) {
$statuDate = '< span class = "label label-success" > depuis ' . $daysPassed . ' Jours< / span > ';
} elseif ($daysPassed >= 8 & & $daysPassed < 15 ) {
$statuDate = '< span class = "label label-warning" > depuis ' . $daysPassed . ' Jours< / span > ';
} else {
$statuDate = '< span class = "label label-danger" > depuis ' . $daysPassed . ' Jours< / span > ';
}
}
$result['data'][$key] = [
$value['bill_no'],
$value['customer_name'],
$value['customer_phone'],
$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);
}
// ========================================
// POUR LES AUTRES UTILISATEURS (COMMERCIALE, SECURITE, Cheffe d'Agence)
// ========================================
else {
foreach ($data as $key => $value) {
$date_time = date('d-m-Y h:i a', strtotime($value['date_time']));
$buttons = '';
// Bouton imprimer
if (in_array('viewOrder', $this->permission) & & $users['group_name'] != "SECURITE" & & $users['group_name'] != "COMMERCIALE") {
$buttons .= '< a target = "_blank" href = "' . site_url('orders/printDiv/' . $value['id']) . '" class = "btn btn-default" > < i class = "fa fa-print" > < / i > < / a > ';
}
// ✅ Bouton modifier pour statuts 0 et 2, ET si c'est l'utilisateur créateur
if (in_array('updateOrder', $this->permission)
& & $users["id"] == $value['user_id']
& & in_array($value['paid_status'], [0, 2])) {
$buttons .= ' < a href = "' . site_url('orders/update/' . $value['id']) . '" class = "btn btn-default" > < i class = "fa fa-pencil" > < / i > < / a > ';
}
// Bouton voir
if (in_array('viewOrder', $this->permission)) {
$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 supprimer pour statuts 0 et 2, ET si c'est l'utilisateur créateur
if (in_array('deleteOrder', $this->permission)
& & $users["id"] == $value['user_id']
& & in_array($value['paid_status'], [0, 2])) {
$buttons .= ' < button type = "button" class = "btn btn-danger" onclick = "removeFunc(' . $value['id'] . ')" data-toggle = "modal" data-target = "#removeModal" > < i class = "fa fa-trash" > < / i > < / button > ';
}
// Statut de paiement
if ($value['paid_status'] == 1) {
$paid_status = '< span class = "label label-success" > Payé< / span > ';
} elseif ($value['paid_status'] == 2) {
$paid_status = '< span class = "label label-warning" > En Attente< / span > ';
} elseif ($value['paid_status'] == 3) {
$paid_status = '< span class = "label label-info" > Payé et Livré< / span > ';
} else {
$paid_status = '< span class = "label label-danger" > Refusé< / span > ';
}
// Calcul délai
$date1 = new DateTime($date_time);
$date2 = new DateTime();
$interval = $date1->diff($date2);
$daysPassed = $interval->days;
$statuDate = '';
if ($value['paid_status'] == 2) {
if ($daysPassed < 8 ) {
$statuDate = '< span class = "label label-success" > depuis ' . $daysPassed . ' Jours< / span > ';
} elseif ($daysPassed >= 8 & & $daysPassed < 15 ) {
$statuDate = '< span class = "label label-warning" > depuis ' . $daysPassed . ' Jours< / span > ';
} else {
$statuDate = '< span class = "label label-danger" > depuis ' . $daysPassed . ' Jours< / span > ';
}
}
// ✅ CONDITION SPÉCIALE POUR SECURITE : remplacer Prix demandé et Prix de vente par Marque et Désignation
if ($users['group_name'] == "SECURITE") {
// Récupérer les infos produit
$OrderItems = new OrderItems();
$Products = new Products();
$Brands = new Brands();
$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);
}
}
/**
* Affiche le formulaire create avec les données d'une commande existante
* Pour le rôle COMMERCIALE
*/
/**
* function who check if the product is null
* and create notification about it
* @param array $product_id id of the product
* @param int $store_id id of the store
* @return void
*/
private function checkProductisNull(array $product_id, $store_id)
{
$notification = new NotificationController();
$product = new Products();
for ($i = 0; $i < count ( $ product_id ) ; $ i + + ) {
$singleProduct = $product->getProductData($product_id[$i]);
if ($singleProduct['product_sold'] == true) {
$notification->createNotification("Produit en rupture de stock", "Direction", $store_id, "products");
}
}
}
private function calculGross($request)
{
$amount = $request;
$montant = 0;
for ($i = 0; $i < \count($amount); $i++) {
$montant += $amount[$i];
}
return $montant;
}
/**
* ✅ AMÉLIORATION : Notifications centralisées pour Direction/DAF/SuperAdmin (tous stores)
*/
public function create()
{
$this->verifyRole('createOrder');
$data['page_title'] = $this->pageTitle;
$validation = \Config\Services::validation();
$products = $this->request->getPost('product[]');
if ($products !== null & & (count($products) !== count(array_unique($products)))) {
return redirect()->back()->withInput()->with('errors', ['product' => 'Chaque produit sélectionné doit être unique.']);
}
$validation->setRules([
'product[]' => 'required',
'customer_type' => 'required',
'source' => 'required'
]);
$validationData = [
'product[]' => $this->request->getPost('product[]'),
'customer_type' => $this->request->getPost('customer_type'),
'source' => $this->request->getPost('source')
];
$Orders = new Orders();
$Company = new Company();
$Products = new Products();
if ($this->request->getMethod() === 'post' & & $validation->run($validationData)) {
$session = session();
$users = $session->get('user');
$user_id = $users['id'];
$bill_no = $this->generateSimpleSequentialBillNo($users['store_id']);
$document_type = $this->request->getPost('document_type') ?? 'facture';
$posts = $this->request->getPost('product[]');
$rates = $this->request->getPost('rate_value[]');
$amounts = $this->request->getPost('amount_value[]');
$puissances = $this->request->getPost('puissance[]');
$quantities = $this->request->getPost('qty[]'); // ✅ RÉCUPÉRER LES QUANTITÉS
$discount = (float)$this->request->getPost('discount') ?? 0;
$gross_amount = $this->calculGross($amounts);
$net_amount = $gross_amount - $discount;
// Vérification prix minimal SI rabais existe
if ($discount > 0) {
$FourchettePrix = new \App\Models\FourchettePrix();
foreach ($posts as $index => $productId) {
$productId = (int)$productId;
$productData = $Products->getProductData($productId);
$fourchette = $FourchettePrix->getFourchettePrixByProductId($productId);
if ($fourchette) {
$prixMinimal = (float)$fourchette['prix_minimal'];
if ($discount < $prixMinimal) {
$prixMinimalFormatted = number_format($prixMinimal, 0, ',', ' ');
$discountFormatted = number_format($discount, 0, ',', ' ');
return redirect()->back()
->withInput()
->with('errors', [
"⚠️ Commande bloquée : Le rabais de {$discountFormatted} Ar pour « {$productData['name']} » est trop élevé."
]);
}
}
}
}
$tranche_1 = (float)$this->request->getPost('tranche_1') ?? 0;
$tranche_2 = (float)$this->request->getPost('tranche_2') ?? 0;
if ($tranche_1 > 0 & & $tranche_2 > 0) {
$total_tranches = $tranche_1 + $tranche_2;
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 = [
'bill_no' => $bill_no,
'document_type' => $document_type,
'customer_name' => $this->request->getPost('customer_name'),
'customer_address' => $this->request->getPost('customer_address'),
'customer_phone' => $this->request->getPost('customer_phone'),
'customer_cin' => $this->request->getPost('customer_cin'),
'customer_type' => $this->request->getPost('customer_type'),
'source' => $this->request->getPost('source'),
'date_time' => date('Y-m-d H:i:s'),
'service_charge_rate' => 0,
'vat_charge_rate' => 0,
'vat_charge' => 0,
'net_amount' => $net_amount,
'discount' => $discount,
'paid_status' => 2,
'user_id' => $user_id,
'amount_value' => $amounts,
'gross_amount' => $gross_amount,
'rate_value' => $rates,
'puissance' => $puissances,
'qty' => $quantities, // ✅ AJOUTER LES QUANTITÉS
'store_id' => $users['store_id'],
'tranche_1' => $tranche_1,
'tranche_2' => $tranche_2,
'order_payment_mode' => $this->request->getPost('order_payment_mode_1'),
'order_payment_mode_1' => $this->request->getPost('order_payment_mode_2')
];
$order_id = $Orders->create($data, $posts);
if ($order_id) {
// ✅ MARQUER LES PRODUITS COMME VENDUS (SEULEMENT SI QTY = 1)
$productModel = new Products();
foreach ($posts as $index => $product_id) {
$qty = isset($quantities[$index]) ? (int)$quantities[$index] : 1;
if ($qty == 1) {
// Vente unitaire : marquer comme vendu
$productModel->update($product_id, ['product_sold' => 1]);
}
// Si qty > 1, vous devriez décrémenter un stock si vous en avez un
}
session()->setFlashdata('success', 'Créé avec succès');
$Notification = new NotificationController();
$Stores = new Stores();
if ($discount > 0) {
// Demande de remise
$Order_item1 = new OrderItems();
$order_item_data = $Order_item1->getOrdersItemData($order_id);
$product_ids = array_column($order_item_data, 'product_id');
$productData = new Products();
$product_data_results = [];
foreach ($product_ids as $prod_id) {
$id = (int) $prod_id;
$product_data_results[] = $productData->getProductData($id);
}
$product_lines = [];
$product_info_lines = [];
foreach ($product_data_results as $product) {
if (isset($product['sku'], $product['price'])) {
$product_lines[] = $product['sku'] . ':' . $product['price'];
}
$product_info_lines[] = "• " . ($product['name'] ?? '-') .
" | N° Série : " . ($product['sku'] ?? '-') .
" | N° Moteur : " . ($product['numero_de_moteur'] ?? '-') .
" | Châssis : " . ($product['chasis'] ?? '-');
}
$product_output = implode("\n", $product_lines);
$data1 = [
'date_demande' => date('Y-m-d H:i:s'),
'montant_demande' => $discount,
'total_price' => $amounts,
'id_store' => $users['store_id'],
'id_order' => $order_id,
'product' => $product_output,
'demande_status' => 'En attente'
];
$Remise = new Remise();
$id_remise = $Remise->addDemande($data1);
$allStores = $Stores->getActiveStore();
$montantFormatted = number_format($discount, 0, ',', ' ');
$message = "💰 Nouvelle demande de remise : {$montantFormatted} Ar< br > " .
"Commande : {$bill_no}< br > " .
"Store : " . $this->returnStore($users['store_id']) . "< br > " .
"Demandeur : {$users['firstname']} {$users['lastname']}< br > " .
implode("< br > ", $product_info_lines);
if (is_array($allStores) & & count($allStores) > 0) {
foreach ($allStores as $store) {
$Notification->createNotification($message, "SuperAdmin", (int)$store['id'], 'remise/');
$Notification->createNotification($message . "< br > < em > Pour information< / em > ", "Direction", (int)$store['id'], 'remise/');
$Notification->createNotification($message . "< br > < em > Pour information< / em > ", "DAF", (int)$store['id'], 'remise/');
}
}
} else {
// Commande sans remise
$Notification->createNotification(
"📦 Nouvelle commande à valider : {$bill_no}< br > " .
"Client : {$data['customer_name']}< br > " .
"Montant : " . number_format($gross_amount, 0, ',', ' ') . " Ar",
"Caissière",
(int)$users['store_id'],
"orders"
);
}
if ($users["group_name"] != "COMMERCIALE") {
$this->checkProductisNull($posts, $users['store_id']);
}
return redirect()->to('orders/');
} else {
session()->setFlashdata('errors', 'Error occurred!!');
return redirect()->to('orders/create/');
}
} else {
// Affichage du formulaire
$company = $Company->getCompanyData(1);
$session = session();
$users = $session->get('user');
$store_id = $users['store_id'];
$data = [
'paid_status' => 2,
'company_data' => $company,
'is_vat_enabled' => ($company['vat_charge_value'] > 0),
'is_service_enabled' => ($company['service_charge_value'] > 0),
'products' => $Products->getProductData2($store_id),
'validation' => $validation,
'page_title' => $this->pageTitle,
];
return $this->render_template('orders/create', $data);
}
}
/**
* Marquer une commande comme livrée
* Accessible uniquement par le rôle SECURITE
*/
public function markAsDelivered()
{
// Activer le retour JSON même en cas d'erreur
ini_set('display_errors', 0);
$order_id = $this->request->getPost('order_id');
$response = ['success' => false, 'messages' => ''];
try {
$session = session();
$users = $session->get('user');
// Vérifier que l'utilisateur est SECURITE
if (!isset($users['group_name']) || $users['group_name'] !== 'SECURITE') {
$response['messages'] = "Accès refusé : seule la sécurité peut marquer une commande comme livrée.";
return $this->response->setJSON($response);
}
if (!$order_id) {
$response['messages'] = "ID de commande manquant.";
return $this->response->setJSON($response);
}
$Orders = new Orders();
// Récupérer la commande actuelle
$current_order = $Orders->getOrdersData((int)$order_id);
if (!$current_order) {
$response['messages'] = "Commande introuvable.";
return $this->response->setJSON($response);
}
// Vérifier que la commande est validée (paid_status = 1)
if ($current_order['paid_status'] != 1) {
$response['messages'] = "Cette commande doit être validée avant d'être marquée comme livrée.";
return $this->response->setJSON($response);
}
// ✅ MISE À JOUR : Enregistrer delivered_by et delivered_at
$updateData = [
'paid_status' => 3,
'delivered_by' => $users['id'], // ← NOUVEAU
'delivered_at' => date('Y-m-d H:i:s') // ← NOUVEAU
];
$updated = $Orders->update((int)$order_id, $updateData);
if ($updated) {
// ✅ Créer une notification centralisée pour tous les stores
try {
$Notification = new NotificationController();
$Stores = new Stores();
$allStores = $Stores->getActiveStore();
$messageGlobal = "📦 Commande livrée : {$current_order['bill_no']}< br > " .
"Store : " . $this->returnStore($current_order['store_id']) . "< br > " .
"Client : {$current_order['customer_name']}< br > " .
"Livrée par : {$users['firstname']} {$users['lastname']}< br > " .
"Date livraison : " . date('d/m/Y H:i');
// ✅ NOTIFIER DIRECTION, DAF, SUPERADMIN DE TOUS LES STORES
if (is_array($allStores) & & count($allStores) > 0) {
foreach ($allStores as $store) {
$Notification->createNotification(
$messageGlobal,
"Direction",
(int)$store['id'],
'orders'
);
$Notification->createNotification(
$messageGlobal,
"DAF",
(int)$store['id'],
'orders'
);
$Notification->createNotification(
$messageGlobal,
"SuperAdmin",
(int)$store['id'],
'orders'
);
}
}
} catch (\Exception $e) {
// Si la notification échoue, on continue quand même
log_message('error', 'Erreur notification: ' . $e->getMessage());
}
$response['success'] = true;
$response['messages'] = "La commande a été marquée comme livrée avec succès.";
} else {
$response['messages'] = "Erreur lors de la mise à jour du statut.";
}
} catch (\Exception $e) {
log_message('error', 'Erreur markAsDelivered: ' . $e->getMessage());
$response['messages'] = "Erreur serveur : " . $e->getMessage();
}
return $this->response->setJSON($response);
}
public function getProductValueById()
{
$product_id = $this->request->getPost('product_id');
if ($product_id) {
$Products = new \App\Models\Products();
$product_data = $Products->getProductData($product_id);
$FourchettePrix = new \App\Models\FourchettePrix();
$fourchette = $FourchettePrix->where('product_id', $product_id)->first();
// ✅ Extraire la puissance - Plusieurs méthodes
$puissance_extracted = '';
// Méthode 1: Chercher dans le champ puissance de la BD
if (!empty($product_data['puissance'])) {
$puissance_extracted = $product_data['puissance'];
}
// Méthode 2: Chercher à la fin du nom (format: |150)
if (empty($puissance_extracted) & & !empty($product_data['name'])) {
if (preg_match('/\|(\d+)(?:cc)?$/i', $product_data['name'], $matches)) {
$puissance_extracted = $matches[1];
}
}
// Méthode 3: Chercher n'importe où dans le nom (format: 150cc ou 150 CC)
if (empty($puissance_extracted) & & !empty($product_data['name'])) {
if (preg_match('/(\d+)\s*cc/i', $product_data['name'], $matches)) {
$puissance_extracted = $matches[1];
}
}
// Log pour debug (à retirer en production)
log_message('info', 'Product ID: ' . $product_id . ' - Puissance: ' . $puissance_extracted);
$response = [
'id' => $product_data['id'],
'sku' => $product_data['sku'],
'name' => $product_data['name'],
'prix_vente' => $product_data['prix_vente'],
'prix_minimal' => $fourchette ? $fourchette['prix_minimal'] : 0,
'puissance' => $puissance_extracted,
'numero_de_moteur' => $product_data['numero_de_moteur'] ?? '',
];
return $this->response->setJSON($response);
}
return $this->response->setJSON(['error' => 'Product ID missing']);
}
public function getTableProductRow()
{
$Products = new Products();
$session = session();
$users = $session->get('user');
$store_id = $users['store_id'];
$product_data = $Products->getProductData2($store_id);
// ✅ Nettoyer les données pour ne pas afficher la puissance dans le select
foreach ($product_data as & $product) {
// Extraire la puissance si elle est dans le nom
if (!empty($product['name']) & & preg_match('/\|(\d+)(?:cc)?$/i', $product['name'], $matches)) {
// Stocker la puissance séparément
if (empty($product['puissance'])) {
$product['puissance'] = $matches[1];
}
}
}
return $this->response->setJSON($product_data);
}
public function update(int $id)
{
$this->verifyRole('updateOrder');
$data['page_title'] = $this->pageTitle;
$validation = \Config\Services::validation();
$Orders = new Orders();
$current_order = $Orders->getOrdersData($id);
if (!$current_order || !isset($current_order['id'])) {
log_message('error', 'Commande introuvable pour ID: ' . $id);
session()->setFlashData('errors', 'Commande introuvable.');
return redirect()->to('orders/');
}
if (in_array($current_order['paid_status'], [1, 3])) {
session()->setFlashData('errors', 'Cette commande ne peut plus être modifiée car elle est déjà validée ou livrée.');
return redirect()->to('orders/');
}
$validation->setRules([
'product' => 'required',
'customer_type' => 'required',
'source' => 'required'
]);
$validationData = [
'product' => $this->request->getPost('product'),
'customer_type' => $this->request->getPost('customer_type'),
'source' => $this->request->getPost('source')
];
$Company = new Company();
$Products = new Products();
$OrderItems = new OrderItems();
$session = session();
$user = $session->get('user');
$role = $user['group_name'] ?? null;
if ($this->request->getMethod() === 'post' & & $validation->run($validationData)) {
$current_order_check = $Orders->getOrdersData($id);
if (in_array($current_order_check['paid_status'], [1, 3])) {
session()->setFlashData('errors', 'Cette commande ne peut plus être modifiée car elle est déjà validée ou livrée.');
return redirect()->to('orders/');
}
$old_paid_status = $current_order['paid_status'];
if ($role === 'COMMERCIALE') {
$paid_status = 2;
} elseif ($role === 'Caissière') {
$paid_status = $this->request->getPost('paid_status');
} else {
// SuperAdmin, Direction, DAF ne peuvent pas valider (paid_status=1) directement
// Leur rôle est de valider les remises via la page remise/, pas les commandes
$requested_status = (int)$this->request->getPost('paid_status');
$paid_status = ($requested_status == 1) ? $old_paid_status : $requested_status;
}
$validated_by = $current_order['validated_by'] ?? null;
$validated_at = $current_order['validated_at'] ?? null;
if ($old_paid_status != 1 & & $paid_status == 1 & & $role === 'Caissière') {
// ✅ Bloquer si la commande a une remise non encore approuvée par SuperAdmin
$currentDiscount = (float)($current_order['discount'] ?? 0);
if ($currentDiscount > 0) {
$RemiseCheck = new Remise();
if (!$RemiseCheck->hasRemiseValidatedForOrder($id)) {
session()->setFlashData('errors', 'Cette commande a une demande de remise en attente de validation par le SuperAdmin.');
return redirect()->to('orders/');
}
}
$validated_by = $user['id'];
$validated_at = date('Y-m-d H:i:s');
}
if (in_array($paid_status, [0, 2])) {
$validated_by = null;
$validated_at = null;
}
$discount = $this->request->getPost('discount');
$original_discount = $this->request->getPost('original_discount');
if ($discount === '' || $discount === null) {
$discount = $original_discount;
}
// ✅ RÉCUPÉRER LES QUANTITÉS
$posts = $this->request->getPost('product');
$rates = $this->request->getPost('rate_value');
$amounts = $this->request->getPost('amount_value');
$puissances = $this->request->getPost('puissance');
$quantities = $this->request->getPost('qty'); // ✅ NOUVEAU
$gross_amount = $this->calculGross($amounts);
$net_amount = $gross_amount - (float)$discount;
// ✅ Vérification prix minimal SI rabais existe
if ((float)$discount > 0) {
$FourchettePrix = new \App\Models\FourchettePrix();
foreach ($posts as $index => $productId) {
$productId = (int)$productId;
$productData = $Products->getProductData($productId);
$fourchette = $FourchettePrix->getFourchettePrixByProductId($productId);
if ($fourchette) {
$prixMinimal = (float)$fourchette['prix_minimal'];
if ((float)$discount < $prixMinimal) {
$prixMinimalFormatted = number_format($prixMinimal, 0, ',', ' ');
$discountFormatted = number_format($discount, 0, ',', ' ');
return redirect()->back()
->withInput()
->with('errors', [
"⚠️ Mise à jour bloquée : Le rabais de {$discountFormatted} Ar pour « {$productData['name']} » est trop élevé."
]);
}
}
}
}
$dataUpdate = [
'document_type' => $this->request->getPost('document_type'),
'customer_name' => $this->request->getPost('customer_name'),
'customer_address' => $this->request->getPost('customer_address'),
'customer_phone' => $this->request->getPost('customer_phone'),
'customer_cin' => $this->request->getPost('customer_cin'),
'customer_type' => $this->request->getPost('customer_type'),
'source' => $this->request->getPost('source'),
'gross_amount' => $gross_amount,
'service_charge_rate' => $this->request->getPost('service_charge_rate'),
'service_charge' => max(0, (float)$this->request->getPost('service_charge_value')),
'vat_charge_rate' => $this->request->getPost('vat_charge_rate'),
'vat_charge' => max(0, (float)$this->request->getPost('vat_charge_value')),
'net_amount' => $net_amount,
'discount' => (float)$discount,
'paid_status' => $paid_status,
'product' => $posts,
'product_sold' => true,
'rate_value' => $rates,
'amount_value' => $amounts,
'puissance' => $puissances,
'qty' => $quantities, // ✅ AJOUTER LES QUANTITÉS
'tranche_1' => $role !== 'COMMERCIALE' ? $this->request->getPost('tranche_1') : 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_1' => $role !== 'COMMERCIALE' ? $this->request->getPost('order_payment_mode_2') : null,
'validated_by' => $validated_by,
'validated_at' => $validated_at
];
if ($Orders->updates($id, $dataUpdate)) {
// ✅ GÉRER LE STATUT product_sold SELON LA QUANTITÉ
$productModel = new Products();
foreach ($posts as $index => $product_id) {
$qty = isset($quantities[$index]) ? (int)$quantities[$index] : 1;
if ($qty == 1) {
// Vente unitaire : marquer comme vendu
$productModel->update($product_id, ['product_sold' => 1]);
} else {
// Vente multiple : ne pas marquer comme vendu (géré par stock si applicable)
$productModel->update($product_id, ['product_sold' => 0]);
}
}
$order_item_data = $OrderItems->getOrdersItemData($id);
$product_ids = array_column($order_item_data, 'product_id');
$Notification = new NotificationController();
// ✅ NOTIFICATION CENTRALISÉE POUR VALIDATION
if ($old_paid_status == 2 & & $paid_status == 1) {
$customer_name = $this->request->getPost('customer_name');
$bill_no = $current_order['bill_no'];
// ✅ Notification SECURITE du store concerné (toujours)
$Notification->createNotification(
"Commande validée: {$bill_no} - Client: {$customer_name}",
"SECURITE",
(int)$user['store_id'],
'orders'
);
// ✅ Notifier Direction, DAF, SuperAdmin UNIQUEMENT pour les commandes avec remise
if ((float)$discount > 0) {
$Stores = new Stores();
$allStores = $Stores->getActiveStore();
$messageGlobal = "✅ Commande validée : {$bill_no}< br > " .
"Store : " . $this->returnStore($user['store_id']) . "< br > " .
"Client : {$customer_name}< br > " .
"Validée par : {$user['firstname']} {$user['lastname']}";
if (is_array($allStores) & & count($allStores) > 0) {
foreach ($allStores as $store) {
$Notification->createNotification(
$messageGlobal,
"Direction",
(int)$store['id'],
'orders'
);
$Notification->createNotification(
$messageGlobal,
"DAF",
(int)$store['id'],
'orders'
);
$Notification->createNotification(
$messageGlobal,
"SuperAdmin",
(int)$store['id'],
'orders'
);
}
}
}
}
if ((float)$discount > 0) {
$productData = new Products();
$product_data_results = [];
foreach ($product_ids as $product_id) {
$product_data_results[] = $productData->getProductData((int) $product_id);
}
$product_lines = [];
foreach ($product_data_results as $product) {
if (isset($product['sku'], $product['price'])) {
$product_lines[] = "{$product['sku']}:{$product['price']}";
}
}
$product_output = implode("\n", $product_lines);
$data1 = [
'date_demande' => date('Y-m-d H:i:s'),
'montant_demande' => $this->request->getPost('discount'),
'total_price' => $amounts,
'id_store' => $user['store_id'],
'id_order' => $id,
'product' => $product_output,
'demande_status' => 'En attente'
];
$Remise = new Remise();
$Remise->updateRemise1($id, $data1);
$Stores = new Stores();
$allStores = $Stores->getActiveStore();
$montantFormatted = number_format($discount, 0, ',', ' ');
$message = "💰 Demande de remise mise à jour : {$montantFormatted} Ar< br > " .
"Commande : {$bill_no}< br > " .
"Store : " . $this->returnStore($user['store_id']) . "< br > " .
"Demandeur : {$user['firstname']} {$user['lastname']}";
if (is_array($allStores) & & count($allStores) > 0) {
foreach ($allStores as $store) {
$Notification->createNotification($message, "SuperAdmin", (int)$store['id'], 'remise/');
$Notification->createNotification($message . "< br > < em > Pour information< / em > ", "Direction", (int)$store['id'], 'remise/');
$Notification->createNotification($message . "< br > < em > Pour information< / em > ", "DAF", (int)$store['id'], 'remise/');
}
}
}
session()->setFlashData('success', 'Commande mise à jour avec succès.');
return redirect()->to('orders/');
} else {
session()->setFlashData('errors', 'Une erreur est survenue lors de la mise à jour.');
return redirect()->to('orders/update/' . $id);
}
}
// ✅ Affichage du formulaire
$company = $Company->getCompanyData(1);
$data['company_data'] = $company;
$data['is_vat_enabled'] = ($company['vat_charge_value'] > 0);
$data['is_service_enabled'] = ($company['service_charge_value'] > 0);
$orders_data = $Orders->getOrdersData($id);
if (!$orders_data || !isset($orders_data['id'])) {
log_message('error', 'Données de commande vides pour ID: ' . $id);
session()->setFlashData('errors', 'Impossible de charger les données de la commande.');
return redirect()->to('orders/');
}
$data['is_editable'] = !in_array($orders_data['paid_status'], [1, 3]);
$orders_data['montant_tranches'] = (!empty($orders_data['discount']) & & $orders_data['discount'] > 0)
? $orders_data['discount']
: $orders_data['gross_amount'];
$result = ['order' => $orders_data];
$orders_item = $OrderItems->getOrdersItemData($orders_data['id']);
foreach ($orders_item as $item) {
$result['order_item'][] = $item;
}
$data['order_data'] = $result;
$data['products'] = $Products->getActiveProductData();
$data['validation'] = $validation;
return $this->render_template('orders/edit', $data);
}
public function lookOrder(int $id)
{
$this->verifyRole('viewOrder');
$data['page_title'] = $this->pageTitle;
$Orders = new Orders();
$Company = new Company();
$Products = new Products();
$OrderItems = new OrderItems();
$Brands = new Brands();
// En cas d’échec de la validation ou si GET
$company = $Company->getCompanyData(1);
$data['company_data'] = $company;
$data['is_vat_enabled'] = ($company['vat_charge_value'] > 0);
$data['is_service_enabled'] = ($company['service_charge_value'] > 0);
$orders_data = $Orders->getOrdersData($id);
// $sum_order_item = $OrderItems->getSumOrdersItemData($orders_data['id']);
$result = [
'order' => $orders_data,
// 'sum_order_data' => $sum_order_item
];
$orders_item = $OrderItems->getOrdersItemData($orders_data['id']);
foreach ($orders_item as $item) {
$result['order_item'][] = $item;
}
$data['order_data'] = $result;
$data['products'] = $Products->getActiveProductData();
$data['brands'] = $Brands->findAll();
return $this->response->setJSON($data);
}
/**
* return storename
* @param int $id
* @return string
*/
private function returnStore($id)
{
$Stores = new Stores();
$store = $Stores->getActiveStore();
$name = "";
foreach ($store as $key => $value) {
if ($value['id'] == $id) {
$name = $value['name'];
}
}
return $name;
}
public function print2(int $id)
{
$this->verifyRole('viewOrder');
if ($id) {
$Orders = new Orders();
$Company = new Company();
$Products = new Products();
$OrderItems = new OrderItems();
// Récupération des données
$order_data = $Orders->getOrdersData($id);
$orders_items = $OrderItems->getOrdersItemData($id);
$company_info = $Company->getCompanyData(1);
// die(\var_dump($orders_items));
$html = '';
// Vérifier si l'utilisateur a payé
if ($order_data['paid_status'] == 1) {
$paid_status = "< span style = 'color: green; font-weight: bold;' > Payé< / span > ";
} elseif ($order_data['paid_status'] == 2) {
$paid_status = "< span style = 'color: orange; font-weight: bold;' > En Attente< / span > ";
} else {
$paid_status = "< span style = 'color: red; font-weight: bold;' > Refusé< / span > ";
}
// Génération du HTML
$html .= '<!DOCTYPE html>
< html lang = "fr" >
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< link rel = "stylesheet" href = "' . base_url('assets/bower_components/bootstrap/dist/css/bootstrap.min.css') . '" >
< style >
body { font-size: 14px; font-family: Arial, sans-serif; }
.invoice-container {
max-width: 750px; /* Réduire la largeur du cadre */
margin: 20px auto;
padding: 20px;
border: 2px solid #007bff; /* Bordure plus visible */
border-radius: 10px;
background: #f0f8ff; /* Couleur de fond plus douce */
}
.invoice-header {
background: #007bff;
color: white;
text-align: center;
font-size: 18px;
font-weight: bold;
padding: 10px;
border-radius: 10px 10px 0 0;
}
.invoice-footer {
background: #007bff;
color: white;
text-align: center;
font-size: 14px;
padding: 10px;
border-radius: 0 0 10px 10px;
margin-top: 12px;
}
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px; text-align: left; border-bottom: 1px solid #ddd; }
th { background: #e9ecef; }
p, strong { color: #333; }
@media print {
body { font-size: 14px; font-family: Arial, sans-serif, margin: 1cm; }
@page { margin: 0; }
.invoice-container {
max-width: 750px;
margin: 20px auto;
padding: 20px;
border: 2px solid #007bff;
border-radius: 10px;
background: #f0f8ff;
}
.invoice-header {
background: #007bff !important;
color: white !important;
text-align: center;
font-size: 18px;
font-weight: bold;
padding: 10px;
border-radius: 10px 10px 0 0;
}
input {
border: none;
outline: none;
width: 100%;
font-size: 14px;
}
.invoice-footer {
background: #007bff !important;
color: white !important;
text-align: center;
font-size: 14px;
padding: 10px;
border-radius: 0 0 10px 10px;
margin-top: 12px;
}
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px; text-align: left; border-bottom: 1px solid #ddd; }
th { background: #e9ecef; }
p, strong { color: #333; } a
/* Supprimer l\'ombre et les bordures non nécessaires */
.invoice-container { box-shadow: none !important; border: none !important; }
/* Éviter que les couleurs soient supprimées à l\'impression */
* { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}
< / style >
< / head >
< body onload = "window.print();" >
< div class = "invoice-container" >
< div class = "invoice-header" style = "background: #007bff;color: white;text-align: center;font-size: 18px;font-weight: bold;padding: 10px;border-radius: 10px 10px 0 0; " >
' . esc($company_info['company_name']) . '
< / div >
< div style = "display: flex;justify-content: space-around;margin-top: 3%;" >
< div >
< p > < strong > Facture ID :< / strong > ' . esc($order_data['bill_no']) . '< / p >
< p > < strong > NIF :< / strong > ' . esc($company_info['NIF']) . '< / p >
< p > < strong > STAT :< / strong > ' . esc($company_info['STAT']) . '< / p >
< p style = "display: flex; gap: 10px;" >
< strong > Contact :< / strong >
< span > ' . esc($company_info['phone']) . '< / span >
< span > ' . esc($company_info['phone2']) . '< / span >
< / p >
< p > < strong > Magasin :< / strong > ' . esc($this->returnStore($order_data['store_id'])) . '< / p >
< / div >
< div >
< p > < strong > Nom:< / strong > ' . esc($order_data['customer_name']) . '< / p >
< p > < strong > Adresse:< / strong > ' . esc($order_data['customer_address']) . '< / p >
< p > < strong > Téléphone:< / strong > ' . esc($order_data['customer_phone']) . '< / p >
< p > < strong > CIN:< / strong > ' . esc($order_data['customer_cin']) . '< / p >
< br >
< br >
< p > < strong > Antananarivo le< / strong > ' . esc(date('d/m/Y')) . '< / p >
< / div >
< / div >
< table class = "table table-bordered" >
< thead >
< tr >
< th > Marque< / th >
< th > Moteur< / th >
< th > Puissance< / th >
< th > Prix< / th >
< / tr >
< / thead >
< tbody > ';
foreach ($orders_items as $item) {
$product_data = $Products->getProductData($item['product_id']);
// ✅ PRIORITÉ À LA PUISSANCE DE LA COMMANDE
$puissance_affichee = !empty($item['puissance']) ? $item['puissance'] : ($product_data['puissance'] ?? '');
$html .= '< tr >
< td > ' . esc($product_data['sku']) . '< / td >
< td > ' . esc($product_data['numero_de_moteur']) . '< / td >
< td > < input type = "text" value = "' . esc($puissance_affichee) . '" > < / td > ✅
< td style = "text-align:right;" > ' . number_format((float) $item['amount'], 2, '.', ' ') . '< / td >
< / tr > ';
}
$html .= ' < / tbody >
< / table >
< table class = "table" >
< tr >
< th > Total:< / th >
< td > ' . number_format(((float) $order_data['gross_amount'] - ((float) $order_data['gross_amount'] * 0.2)), 2, '.', ' ') . '< / td >
< / tr >
< tr >
< th > TVA:< / th >
< td > ' . number_format((((float) $order_data['gross_amount'] * 0.2)), 2, '.', ' ') . '< / td >
< / tr > ';
$html .= '< tr >
< th > Réduction:< / th >
< td > ' . number_format((float) $order_data['discount'], 2, '.', ' ') . '< / td >
< / tr >
< tr >
< th > Total à payer:< / th >
< td > < strong > ' . number_format((float) ($order_data['net_amount']), 2, '.', ' ') . '< / strong > < / td >
< / tr >
< tr >
< th > Statut:< / th >
< td > ' . $paid_status . '< / td >
< / tr > ';
// Vérification et ajout des informations de paiement
if (!empty($order_data['order_payment_mode'])) {
$html .= '< tr >
< th > Mode de paiement:< / th >
< td > < strong > ' . esc($order_data['order_payment_mode']) . '< / strong > < / td >
< / tr > ';
}
if (!empty($order_data['tranche_1'])) {
$html .= '< tr >
< th > Tranche 1:< / th >
< td > < strong > ' . number_format((float) $order_data['tranche_1'], 2, '.', ' ') . '< / strong > < / td >
< / tr > ';
}
if (!empty($order_data['tranche_2'])) {
$html .= '< tr >
< th > Tranche 2:< / th >
< td > < strong > ' . number_format((float) $order_data['tranche_2'], 2, '.', ' ') . '< / strong > < / td >
< / tr > ';
}
$html .= '< / table >
< div style = "display: flex;align-items: center;justify-content: space-around;margin-bottom: 5%;" >
< div >
< p style = "font-weight:bold;" > L\'acheteur< / p >
< / div >
< div >
< p style = "font-weight:bold;" > Le vendeur< / p >
< / div >
< / div >
< div class = "invoice-footer" >
Merci pour votre achat !< br >
< strong style = "color:white;" > Original< / strong >
< / div >
< / div >
< / body >
< / html > ';
return $this->response->setBody($html);
}
}
public function print(int $id)
{
$this->verifyRole('viewOrder');
if ($id) {
$Orders = new Orders();
$Company = new Company();
$Products = new Products();
$OrderItems = new OrderItems();
$order_data = $Orders->getOrdersData($id);
$orders_items = $OrderItems->getOrdersItemData($id);
$company_info = $Company->getCompanyData(1);
$paid_status = ($order_data['paid_status'] == 1)
? "< span style = 'color: green; font-weight: bold;' > Payé< / span > "
: "< span style = 'color: red; font-weight: bold;' > Non payé< / span > ";
foreach ($orders_items as $index => $item) {
$product_data = $Products->getProductData($item['product_id']);
// ✅ PRIORITÉ À LA PUISSANCE DE LA COMMANDE (si modifiée), sinon celle du produit
$puissance_affichee = !empty($item['puissance']) ? $item['puissance'] : ($product_data['puissance'] ?? '');
echo '<!DOCTYPE html>
< html lang = "fr" >
< head >
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< link rel = "stylesheet" href = "' . base_url('assets/bower_components/bootstrap/dist/css/bootstrap.min.css') . '" >
< style >
body { font-size: 14px; font-family: Arial, sans-serif; margin: 0; padding: 0; }
.invoice-container {
max-width: 750px;
margin: 20px auto;
padding: 20px;
border: 2px solid #007bff;
border-radius: 10px;
background: #f0f8ff;
page-break-after: always;
}
.invoice-header {
background: #007bff !important;
color: white;
text-align: center;
font-size: 18px;
font-weight: bold;
padding: 10px;
border-radius: 10px 10px 0 0;
}
.invoice-footer {
background: #007bff !important;
color: white;
text-align: center;
font-size: 14px;
padding: 10px;
border-radius: 0 0 10px 10px;
margin-top: 12px;
}
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px; text-align: left; border-bottom: 1px solid #ddd; }
th { background: #e9ecef; }
input {
border: none;
outline: none;
width: 100%;
font-size: 14px;
}
@media print {
body { font-size: 14px; font-family: Arial, sans-serif, margin: 1cm; }
@page { margin: 0; }
* { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
body, .invoice-container {
margin: 0;
padding: 0;
border: none;
box-shadow: none;
}
.invoice-container {
page-break-after: always;
}
}
< / style >
< / head >
< body onload = "window.print()" >
< div class = "invoice-container" >
< div class = "invoice-header" > ' . esc($company_info['company_name']) . '< / div >
< div style = "display: flex; justify-content: space-around; margin-top: 3%;" >
< div >
< p > < strong > Facture ID :< / strong > ' . esc($order_data['bill_no']) . '< / p >
< p > < strong > NIF :< / strong > ' . esc($company_info['NIF']) . '< / p >
< p > < strong > STAT :< / strong > ' . esc($company_info['STAT']) . '< / p >
< p style = "display: flex; gap: 10px;" >
< strong > Contact :< / strong >
< span > ' . esc($company_info['phone']) . '< / span >
< span > ' . esc($company_info['phone2']) . '< / span >
< / p >
< p > < strong > Magasin :< / strong > ' . esc($this->returnStore($order_data['store_id'])) . '< / p >
< / div >
< div >
< p > < strong > Nom:< / strong > ' . esc($order_data['customer_name']) . '< / p >
< p > < strong > Adresse:< / strong > ' . esc($order_data['customer_address']) . '< / p >
< p > < strong > Téléphone:< / strong > ' . esc($order_data['customer_phone']) . '< / p >
< p > < strong > CIN:< / strong > ' . esc($order_data['customer_cin']) . '< / p >
< br > < br >
< p > < strong > Antananarivo le< / strong > ' . esc(date('d/m/Y')) . '< / p >
< / div >
< / div >
< table class = "table" >
< thead >
< tr >
< th > Marque< / th >
< th > Moteur< / th >
< th > Puissance< / th >
< th > Prix< / th >
< / tr >
< / thead >
< tbody >
< tr >
< td > ' . esc($product_data['sku']) . '< / td >
< td > ' . esc($product_data['numero_de_moteur']) . '< / td >
< td > < input type = "text" value = "' . esc($puissance_affichee) . '" placeholder = "Remplir ici" > < / td >
< td style = "text-align:right;" > ' . number_format((float)$item['amount'], 2, '.', ' ') . '< / td >
< / tr >
< / tbody >
< / table >
< table class = "table" >
< tr >
< th > Total:< / th >
< td > ' . number_format($item['amount'] - ($item['amount'] * 0.2), 2, '.', ' ') . '< / td >
< / tr >
< tr >
< th > TVA:< / th >
< td > ' . number_format($item['amount'] * 0.2, 2, '.', ' ') . '< / td >
< / tr >
< tr >
< th > Réduction:< / th >
< td > ' . number_format($order_data['discount'], 2, '.', ' ') . '< / td >
< / tr >
< tr >
< th > Total à payer:< / th >
< td > < strong > ' . number_format($item['amount'] - $order_data['discount'], 2, '.', ' ') . '< / strong > < / td >
< / tr >
< tr >
< th > Statut:< / th >
< td > ' . $paid_status . '< / td >
< / tr > ';
if (!empty($order_data['order_payment_mode'])) {
echo '< tr > < th > Mode de paiement:< / th > < td > < strong > ' . esc($order_data['order_payment_mode']) . '< / strong > < / td > < / tr > ';
}
if (!empty($order_data['tranche_1'])) {
echo '< tr > < th > Tranche 1:< / th > < td > < strong > ' . number_format((float)$order_data['tranche_1'], 2, '.', ' ') . '< / strong > < / td > < / tr > ';
}
if (!empty($order_data['tranche_2'])) {
echo '< tr > < th > Tranche 2:< / th > < td > < strong > ' . number_format((float)$order_data['tranche_2'], 2, '.', ' ') . '< / strong > < / td > < / tr > ';
}
echo '< / table >
< div style = "display: flex; justify-content: space-around; margin-top: 5%;" >
< div > < p style = "font-weight:bold;" > L\'acheteur< / p > < / div >
< div > < p style = "font-weight:bold;" > Le vendeur< / p > < / div >
< / div >
< div class = "invoice-footer" >
Merci pour votre achat !< br >
< strong style = "color:white;" > Original< / strong >
< / div >
< / div >
< / body >
< / html > ';
}
}
}
public function remove()
{
$this->verifyRole('deleteOrder');
$order_id = $this->request->getPost('order_id');
$response = [];
if ($order_id) {
$Orders = new Orders();
$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['messages'] = "Successfully removed";
} else {
$response['success'] = false;
$response['messages'] = "Error in the database while removing the order";
}
} else {
$response['success'] = false;
$response['messages'] = "Refersh the page again!!";
}
return $this->response->setJSON($response);
}
public function createById(int $id)
{
$this->verifyRole('createOrder');
$data['page_title'] = $this->pageTitle;
$Company = new Company();
$Products = new Products();
// If validation fails
$company = $Company->getCompanyData(1);
// Prepare data for the view
$data = [
'company_data' => $company,
'is_vat_enabled' => ($company['vat_charge_value'] > 0),
'is_service_enabled' => ($company['service_charge_value'] > 0),
'products' => $Products->getProductData($id),
// 'discount' => $Products->getProductData($id)['discount'],
'pu' => $Products->getProductData($id)['prix_vente'],
'page_title' => $this->pageTitle,
];
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
public function update_caisse($data)
{
$p1 = 0;
$p2 = 0;
$op = "";
$p3 = 0;
$dest = "";
if ($data['tranche2']) {
if ($data['order']) {
# code...
}
}
}
public function print3(int $id)
{
$this->verifyRole('viewOrder');
if (!$id) {
return $this->response->setStatusCode(400, 'ID manquant');
}
$Orders = new Orders();
$Company = new Company();
$OrderItems = new OrderItems();
$Products = new Products();
$order_data = $Orders->getOrdersData($id);
$items = $OrderItems->getOrdersItemData($id);
$company_info = $Company->getCompanyData(1);
$paid_status = $order_data['paid_status'] == 1
? "< span style = 'color: green; font-weight: bold;' > Payé< / span > "
: "< span style = 'color: red; font-weight: bold;' > Refusé< / span > ";
// STYLE COMMUN
$style = '
< style >
body { font-family: Arial, sans-serif; font-size:14px; margin:0; padding:0; }
.invoice-container { max-width:750px; margin:20px auto; padding:20px;
border:2px solid #007bff; border-radius:10px; page-break-after:always; }
.invoice-header { background:#007bff !important; color:#fff; text-align:center;
font-size:18px; padding:10px; border-radius:10px 10px 0 0; }
.invoice-footer { background:#007bff !important; color:#fff; text-align:center;
font-size:14px; padding:10px; border-radius:0 0 10px 10px; margin-top:12px; }
.info { display:flex; justify-content:space-between; margin-top:15px; }
table { width:100%; border-collapse:collapse; margin-top:15px; }
th, td { border:1px solid #ddd; padding:8px; text-align:left; }
th { background:#e9ecef; }
input { border:none; outline:none; width:100%; font-size:14px; background:transparent; }
.summary { width: 80%; margin: 20px 0; margin-right: 0; }
.summary th, .summary td { border:none; padding:4px; text-align:right; }
.summary th { text-align:left; }
@media print {
body { font-size: 14px; font-family: Arial, sans-serif; margin: 1cm; }
@page { margin: 0; }
* { -webkit-print-color-adjust:exact; print-color-adjust:exact; }
}
< / style >
';
// --- FACTURES : Une par produit ---
foreach ($items as $item) {
$product_data = $Products->getProductData($item['product_id']);
// ✅ PRIORITÉ À LA PUISSANCE DE LA COMMANDE
$puissance_affichee = !empty($item['puissance'])
? $item['puissance']
: (!empty($product_data['puissance']) ? $product_data['puissance'] : '');
$unitPrice = (float)$item['amount'];
$quantity = isset($item['qty']) ? (int)$item['qty'] : 1;
$subtotal = $unitPrice * $quantity;
$vatAmount = $subtotal * 0.2;
$discount = (float)$order_data['discount'];
$totalNet = $subtotal + $vatAmount - $discount;
echo '<!DOCTYPE html>
< html lang = "fr" >
< head >
< meta charset = "utf-8" >
< link rel = "stylesheet" href = "' . base_url('assets/bower_components/bootstrap/dist/css/bootstrap.min.css') . '" >
' . $style . '
< / head >
< body onload = "window.print()" >
< div class = "invoice-container" >
< div class = "invoice-header" > FACTURE< / div >
< div class = "info" >
< div >
< p > < strong > Facture ID :< / strong > ' . esc($order_data['bill_no']) . '< / p >
< p > < strong > NIF :< / strong > ' . esc($company_info['NIF']) . '< / p >
< p > < strong > STAT :< / strong > ' . esc($company_info['STAT']) . '< / p >
< p > < strong > Contact :< / strong > ' . esc($company_info['phone']) . ' / ' . esc($company_info['phone2']) . '< / p >
< p > < strong > Magasin :< / strong > ' . esc($this->returnStore($order_data['store_id'])) . '< / p >
< / div >
< div >
< p > < strong > Nom:< / strong > ' . esc($order_data['customer_name']) . '< / p >
< p > < strong > Adresse:< / strong > ' . esc($order_data['customer_address']) . '< / p >
< p > < strong > Téléphone:< / strong > ' . esc($order_data['customer_phone']) . '< / p >
< p > < strong > CIN:< / strong > ' . esc($order_data['customer_cin']) . '< / p >
< / div >
< / div >
< table >
< thead > < tr > < th > Marque< / th > < th > Moteur< / th > < th > Puissance (CC)< / th > < th > Prix unitaire< / th > < / tr > < / thead >
< tbody >
< tr >
< td > ' . esc($product_data['sku']) . '< / td >
< td > ' . esc($product_data['numero_de_moteur']) . '< / td >
< td > < input type = "text" value = "' . esc($puissance_affichee) . '" readonly > < / td >
< td > ' . number_format($unitPrice, 2, '.', ' ') . '< / td >
< / tr >
< / tbody >
< / table >
< table class = "table summary" >
< tr > < th > Total HT:< / th > < td > ' . number_format($subtotal, 2, '.', ' ') . ' Ar< / td > < / tr >
< tr > < th > TVA (20%):< / th > < td > ' . number_format($vatAmount, 2, '.', ' ') . ' Ar< / td > < / tr >
< tr > < th > Réduction:< / th > < td > ' . number_format($discount, 2, '.', ' ') . ' Ar< / td > < / tr >
< tr > < th > Total à payer:< / th > < td > < strong > ' . number_format($totalNet, 2, '.', ' ') . ' Ar< / strong > < / td > < / tr >
< tr > < th > Statut:< / th > < td > ' . $paid_status . '< / td > < / tr > ';
if (!empty($order_data['order_payment_mode'])) {
echo '< tr > < th > Mode de paiement:< / th > < td > ' . esc($order_data['order_payment_mode']) . '< / td > < / tr > ';
}
if (!empty($order_data['tranche_1'])) {
echo '< tr > < th > Tranche 1:< / th > < td > ' . number_format((float)$order_data['tranche_1'], 2, '.', ' ') . ' Ar< / td > < / tr > ';
}
if (!empty($order_data['tranche_2'])) {
echo '< tr > < th > Tranche 2:< / th > < td > ' . number_format((float)$order_data['tranche_2'], 2, '.', ' ') . ' Ar< / td > < / tr > ';
}
echo '< / table >
< div style = "display: flex; justify-content: space-around; margin-top: 5%;" >
< div > < p style = "font-weight:bold;" > L\'acheteur< / p > < / div >
< div > < p style = "font-weight:bold;" > Le vendeur< / p > < / div >
< / div >
< div class = "invoice-footer" >
Merci pour votre achat !< br >
< strong style = "color:white;" > Original< / strong >
< / div >
< / div >
< / body >
< / html > ';
}
// --- BON DE COMMANDE : une seule fois après la boucle ---
echo '<!DOCTYPE html>
< html lang = "fr" >
< head >
< meta charset = "utf-8" >
< link rel = "stylesheet" href = "' . base_url('assets/bower_components/bootstrap/dist/css/bootstrap.min.css') . '" >
' . $style . '
< / head >
< body onload = "window.print()" >
< div class = "invoice-container" >
< div class = "invoice-header" > BON DE COMMANDE< / div >
< div class = "info" >
< div >
< p > < strong > Commande ID :< / strong > ' . esc($order_data['order_no'] ?? $order_data['bill_no']) . '< / p >
< p > < strong > Magasin :< / strong > ' . esc($this->returnStore($order_data['store_id'])) . '< / p >
< / div >
< div >
< p > < strong > Nom:< / strong > ' . esc($order_data['customer_name']) . '< / p >
< p > < strong > Adresse:< / strong > ' . esc($order_data['customer_address']) . '< / p >
< p > < strong > Téléphone:< / strong > ' . esc($order_data['customer_phone']) . '< / p >
< p > < strong > CIN:< / strong > ' . esc($order_data['customer_cin']) . '< / p >
< / div >
< / div >
< table class = "table table-bordered" >
< thead >
< tr >
< th > Marque< / th >
< th > Moteur< / th >
< th > Puissance (CC)< / th >
< th > Prix< / th >
< / tr >
< / thead >
< tbody > ';
$total_ht = 0;
foreach ($items as $item) {
$product_data = $Products->getProductData($item['product_id']);
// ✅ PRIORITÉ À LA PUISSANCE DE LA COMMANDE
$puissance_affichee = !empty($item['puissance'])
? $item['puissance']
: (!empty($product_data['puissance']) ? $product_data['puissance'] : '');
$total_ht += (float)$item['amount'];
echo '< tr >
< td > ' . esc($product_data['sku']) . '< / td >
< td > ' . esc($product_data['numero_de_moteur']) . '< / td >
< td > < input type = "text" value = "' . esc($puissance_affichee) . '" style = "border:none;width:100%;outline:none;" > < / td >
< td style = "text-align:right;" > ' . number_format((float)$item['amount'], 2, '.', ' ') . '< / td >
< / tr > ';
}
$tva = $total_ht * 0.2;
$total_ttc = $total_ht + $tva - $order_data['discount'];
echo '< / tbody >
< / table >
< table class = "table summary" >
< tr > < th > Total HT:< / th > < td > ' . number_format($total_ht, 2, '.', ' ') . ' Ar< / td > < / tr >
< tr > < th > TVA (20%):< / th > < td > ' . number_format($tva, 2, '.', ' ') . ' Ar< / td > < / tr >
< tr > < th > Réduction:< / th > < td > ' . number_format($order_data['discount'], 2, '.', ' ') . ' Ar< / td > < / tr >
< tr > < th > Total à payer:< / th > < td > < strong > ' . number_format($total_ttc, 2, '.', ' ') . ' Ar< / strong > < / td > < / tr >
< / table >
< div style = "display: flex; justify-content: space-around; margin-top: 5%;" >
< div > < p style = "font-weight:bold;" > L\'acheteur< / p > < / div >
< div > < p style = "font-weight:bold;" > Le vendeur< / p > < / div >
< / div >
< div class = "invoice-footer" >
Merci pour votre commande !< br >
< strong style = "color:white;" > Original< / strong >
< / div >
< / div >
< / body >
< / html > ';
}
// PRINT5 - Facture détaillée avec conditions générales
// ====================================
public function print5(int $id)
{
$this->verifyRole('viewOrder');
if (!$id) {
throw new \CodeIgniter\Exceptions\PageNotFoundException();
}
$Orders = new Orders();
$Company = new Company();
$OrderItems = new OrderItems();
$order = $Orders->getOrdersData($id);
$items = $OrderItems->getOrdersItemData($id);
$company = $Company->getCompanyData(1);
$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"
$isAvanceMere = false;
foreach ($items as $item) {
if (!empty($item['product_name']) & & empty($item['product_id'])) {
$isAvanceMere = true;
break;
}
}
$discount = (float) $order['discount'];
$grossAmount = (float) $order['gross_amount'];
$totalTTC = ($discount > 0) ? $discount : $grossAmount;
$totalHT = $totalTTC / 1.20;
$tva = $totalTTC - $totalHT;
$inWords = $this->numberToWords((int) round($totalTTC));
$paidLabel = $order['paid_status'] == 1 ? 'Payé' : 'Non payé';
$html = '<!DOCTYPE html>
< html lang = "fr" >
< head >
< meta charset = "utf-8" >
< title > '.$documentTitle.' '.$order['bill_no'].'< / title >
< style >
/* ✅ FORMAT A4 PAYSAGE DIVISÉ EN 2 */
@page {
size: A4 landscape;
margin: 0;
}
body {
font-family: Arial, sans-serif;
font-size: 10px;
color: #000;
margin: 0;
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 >
< / head >
< body onload = "window.print()" >
<!-- ✅ PAGE 1 : RECTO - 2 DOCUMENTS CÔTE À CÔTE -->
< div class = "page" > ';
// ✅ GÉNÉRER 2 DOCUMENTS IDENTIQUES
for ($i = 0; $i < 2 ; $ i + + ) {
$html .= '
< div class = "facture-box" >
< div class = "header" >
< div class = "infos" >
< h2 > '.esc($company['company_name']).'< / h2 >
< p > < strong > NIF :< / strong > '.esc($company['NIF']).'< / p >
< p > < strong > STAT :< / strong > '.esc($company['STAT']).'< / p >
< p > < strong > Contact :< / strong > '.esc($company['phone']).' | '.esc($company['phone2']).'< / p >
< / div >
< div style = "text-align:center;" >
< img src = "'.base_url('assets/images/company_logo.jpg').'" alt = "Logo" >
< p class = "facture-num" > '.$documentTitle.' N° '.esc($order['bill_no']).'< / p >
< / div >
< / div >
< div class = "client" >
< p > < strong > DOIT Nom :< / strong > '.esc($order['customer_name']).'< / p >
< p > < strong > Adresse :< / strong > '.esc($order['customer_address']).'< / p >
< p > < strong > CIN :< / strong > '.esc($order['customer_cin']).'< / p >
< p > < strong > Téléphone :< / strong > '.esc($order['customer_phone'] ?? '').'< / p >
< p style = "text-align:right;" > < em > Antananarivo, le '.$today.'< / em > < / p >
< / div > ';
// ✅ TABLEAU ADAPTÉ SELON LE TYPE
// Dans la boucle foreach ($items as $item)
if ($isAvanceMere) {
$html .= '
< table >
< thead >
< tr >
< th > Produit< / th >
< th > Quantité< / th > <!-- ✅ AJOUT -->
< th class = "right" > Prix unitaire (Ar)< / th >
< th class = "right" > Montant (Ar)< / th > <!-- ✅ AJOUT -->
< / tr >
< / thead >
< tbody > ';
foreach ($items as $it) {
$details = $this->getOrderItemDetails($it);
if (!$details) continue;
$qty = isset($it['qty']) ? (int)$it['qty'] : 1; // ✅ RÉCUPÉRATION
$prixUnitaire = ($discount > 0) ? ($discount / $qty) : $details['prix'];
$prixTotal = $prixUnitaire * $qty;
$html .= '< tr > < td > '.esc($details['product_name']);
if (!empty($details['commentaire'])) {
$html .= '< br > < em style = "font-size:8px; color:#666;" > '.esc($details['commentaire']).'< / em > ';
}
$html .= '< / td >
< td class = "right" > '.esc($qty).'< / td > <!-- ✅ AFFICHAGE -->
< td class = "right" > '.number_format($prixUnitaire, 0, '', ' ').'< / td >
< td class = "right" > '.number_format($prixTotal, 0, '', ' ').'< / td >
< / tr > ';
}
$html .= '
< / tbody >
< / table > ';
} else {
$html .= '
< table >
< thead >
< tr >
< th > MARQUE< / th >
< th > Désignation< / th >
< th > N° Moteur< / th >
< th > N° Châssis< / th >
< th > Puissance (CC)< / th >
< th > Quantité< / th > <!-- ✅ AJOUT -->
< th class = "right" > Prix Unit. (Ar)< / th >
< th class = "right" > Montant (Ar)< / th > <!-- ✅ AJOUT -->
< / tr >
< / thead >
< tbody > ';
foreach ($items as $it) {
$details = $this->getOrderItemDetails($it);
if (!$details) continue;
$qty = isset($it['qty']) ? (int)$it['qty'] : 1; // ✅ RÉCUPÉRATION
$prixUnitaire = ($discount > 0) ? ($discount / $qty) : $details['prix'];
$prixTotal = $prixUnitaire * $qty;
$html .= '
< tr >
< td > '.esc($details['marque']).'< / td >
< td > '.esc($details['product_name']).'< / td >
< td > '.esc($details['numero_moteur']).'< / td >
< td > '.esc($details['numero_chassis']).'< / td >
< td > '.esc($details['puissance']).'< / td >
< td class = "right" > '.esc($qty).'< / td > <!-- ✅ AFFICHAGE -->
< td class = "right" > '.number_format($prixUnitaire, 0, '', ' ').'< / td >
< td class = "right" > '.number_format($prixTotal, 0, '', ' ').'< / td >
< / tr > ';
}
$html .= '
< / tbody >
< / table > ';
}
$html .= '
< table >
< tr >
< td > < strong > Prix (HT) :< / strong > < / td >
< td class = "right" > '.number_format($totalHT, 0, '', ' ').' Ar< / td >
< / tr >
< tr >
< td > < strong > TVA (20%) :< / strong > < / td >
< td class = "right" > '.number_format($tva, 0, '', ' ').' Ar< / td >
< / tr >
< tr >
< td > < strong > Total (TTC) :< / strong > < / td >
< td class = "right" > '.number_format($totalTTC, 0, '', ' ').' Ar< / td >
< / tr >
< / table >
< div class = "words-box" >
< strong > Arrêté à la somme de :< / strong > < br >
'.$inWords.'
< / div >
< div class = "signature" >
< div > L\'Acheteur< br > < br > __________________< / div >
< div > Le Vendeur< br > < br > __________________< / div >
< / div >
< / div > ';
}
$html .= '
< / 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;" >
< h3 > Conditions Générales< / h3 >
< img src = "'.base_url('assets/images/company_logo.jpg').'" alt = "Logo" >
< / div >
< 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 > Le client doit vérifier soigneusement la marchandise avant de quitter notre établissement.< / li >
< li > Aucun service après-vente n\'est fourni.< / 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 >
< / ul >
< div class = "buyer-signature" > L\'Acheteur< / div >
< / div > ';
}
$html .= '
< / div >
< / body >
< / html > ';
return $this->response->setBody($html);
}
/**
* Convertit un nombre entier en texte (français, sans décimales).
* Usage basique, pour Ariary.
*/
private function numberToWords(int $num): string
{
// Cas zéro
if ($num === 0) {
return 'zéro ariary';
}
// Tableaux de base
$units = [
'', 'un', 'deux', 'trois', 'quatre', 'cinq', 'six',
'sept', 'huit', 'neuf', 'dix', 'onze', 'douze',
'treize', 'quatorze', 'quinze', 'seize', 'dix-sept', 'dix-huit', 'dix-neuf'
];
$tens = [
2 => 'vingt', 3 => 'trente', 4 => 'quarante',
5 => 'cinquante', 6 => 'soixante',
7 => 'soixante-dix', 8 => 'quatre-vingt', 9 => 'quatre-vingt-dix'
];
// Fonction récursive interne (sans la monnaie)
$convert = function(int $n) use (& $convert, $units, $tens): string {
if ($n < 20 ) {
return $units[$n];
}
if ($n < 100 ) {
$d = intdiv($n, 10);
$r = $n % 10;
// 70–79 et 90–99
if ($d === 7 || $d === 9) {
$base = $d === 7 ? 60 : 80;
return $tens[$d] . ($r ? '-' . $units[$n - $base] : '');
}
// 20–69 ou 80–89
return $tens[$d] . ($r ? '-' . $units[$r] : '');
}
if ($n < 1000 ) {
$h = intdiv($n, 100);
$rest = $n % 100;
$hundredText = $h > 1 ? $units[$h] . ' cent' : 'cent';
// « deux cents » prend un « s » si pas de reste
if ($h > 1 & & $rest === 0) {
$hundredText .= 's';
}
return $hundredText . ($rest ? ' ' . $convert($rest) : '');
}
if ($n < 1000000 ) {
$k = intdiv($n, 1000);
$rest = $n % 1000;
$thousandText = $k > 1 ? $convert($k) . ' mille' : 'mille';
return $thousandText . ($rest ? ' ' . $convert($rest) : '');
}
// millions et plus
$m = intdiv($n, 1000000);
$rest = $n % 1000000;
$millionText = $m > 1 ? $convert($m) . ' million' : 'un million';
// pas de 's' à million en francais
return $millionText . ($rest ? ' ' . $convert($rest) : '');
};
// Construit le texte sans la monnaie, puis ajoute 'ariary' à la fin
$words = $convert($num);
return trim($words) . ' ariary';
}
/**
* ✅ NOUVELLE MÉTHODE : Vérifier si une commande provient d'une avance "sur mer"
*/
private function isFromAvanceMere($order_id)
{
$db = \Config\Database::connect();
// Vérifier s'il existe une avance "sur mer" liée à cette commande
$avance = $db->table('avances')
->select('type_avance, product_name')
->where('is_order', 1)
->where('customer_name',
$db->table('orders')
->select('customer_name')
->where('id', $order_id)
->get()
->getRow()->customer_name ?? ''
)
->where('type_avance', 'mere')
->get()
->getRowArray();
return $avance !== null;
}
/**
* ✅ NOUVELLE MÉTHODE : Récupérer les détails d'un item de commande
* Gère à la fois les produits normaux et ceux des avances "sur mer"
*/
private function getOrderItemDetails($item)
{
$Products = new Products();
$Brand = new Brands();
// Si l'item a un product_name (avance sur mer), on l'utilise directement
if (!empty($item['product_name']) & & empty($item['product_id'])) {
return [
'type' => 'mere',
'product_name' => $item['product_name'],
'marque' => $item['marque'] ?? $item['product_name'],
'numero_moteur' => $item['numero_moteur'] ?? '',
'numero_chassis' => $item['numero_chassis'] ?? '',
'puissance' => $item['puissance'] ?? '',
'commentaire' => $item['commentaire'] ?? '',
'prix' => (float)$item['amount']
];
}
// Sinon, récupérer depuis la table products (avance sur terre ou commande normale)
if (empty($item['product_id'])) {
return null;
}
$product = $Products->getProductData($item['product_id']);
if (!$product) {
return null;
}
// Récupérer le nom de la marque
$brandName = 'N/A';
if (!empty($product['marque'])) {
$brandData = $Brand->find($product['marque']);
if ($brandData & & isset($brandData['name'])) {
$brandName = $brandData['name'];
}
}
return [
'type' => 'terre',
'product_name' => $product['name'],
'marque' => $brandName,
'numero_moteur' => $product['numero_de_moteur'] ?? '',
'numero_chassis' => $product['chasis'] ?? $product['sku'] ?? '',
'puissance' => !empty($item['puissance']) ? $item['puissance'] : ($product['puissance'] ?? ''), // ✅ Priorité à la commande
'commentaire' => '',
'prix' => (float)$item['amount']
];
}
/**
* ✅ PRINT7 - Bon de commande adapté pour avances "sur mer" et "sur terre"
*/
public function print7(int $id)
{
$this->verifyRole('viewOrder');
if (!$id) {
throw new \CodeIgniter\Exceptions\PageNotFoundException();
}
$Orders = new Orders();
$Company = new Company();
$OrderItems = new OrderItems();
$order = $Orders->getOrdersData($id);
$items = $OrderItems->getOrdersItemData($id);
$company = $Company->getCompanyData(1);
$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"
$isAvanceMere = false;
foreach ($items as $item) {
if (!empty($item['product_name']) & & empty($item['product_id'])) {
$isAvanceMere = true;
break;
}
}
$discount = (float) $order['discount'];
$grossAmount = (float) $order['gross_amount'];
$totalTTC = ($discount > 0) ? $discount : $grossAmount;
$totalHT = $totalTTC / 1.20;
$tva = $totalTTC - $totalHT;
$paidLabel = $order['paid_status'] == 1 ? 'Payé' : 'Non payé';
$html = '<!DOCTYPE html>
< html lang = "fr" >
< head >
< meta charset = "utf-8" >
< title > '.$documentTitle.' '.$order['bill_no'].'< / title >
< 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:20px; }
.header .infos { line-height:1.4; }
.header img { max-height:80px; }
.client { margin:20px; }
table { width:calc(100% - 40px); margin:0 20px 20px; border-collapse:collapse; }
th, td { border:1px solid #000; padding:8px; }
th { background:#f0f0f0; }
.right { text-align:right; }
.signature { display:flex; justify-content:space-between; margin:50px 20px 20px; }
.signature div { text-align:center; }
.footer { text-align:center; margin:20px; font-size:12px; color:#666; }
.conditions { page-break-before: always; padding:20px; line-height:1.5; }
@media print {
@page { margin:1cm; }
* { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}
< / style >
< / head >
< body onload = "window.print()" >
< div class = "header" >
< div class = "infos" >
< h2 style = "margin:0;" > '.esc($company['company_name']).'< / h2 >
< p style = "margin:2px 0;" > < strong > NIF :< / strong > '.esc($company['NIF']).'< / p >
< p style = "margin:2px 0;" > < strong > STAT :< / strong > '.esc($company['STAT']).'< / p >
< p style = "margin:2px 0;" > < strong > Contact :< / strong > '.esc($company['phone']).' | '.esc($company['phone2']).'< / p >
< / div >
< div style = "text-align:center;" >
< img src = "'.base_url('assets/images/company_logo.jpg').'" alt = "Logo" >
< p style = "margin:5px 0; font-weight:bold;" > '.$documentTitle.' N° '.esc($order['bill_no']).'< / p >
< / div >
< / div >
< div class = "client" >
< p > < strong > Client :< / strong > '.esc($order['customer_name']).'< / p >
< p > < strong > Adresse :< / strong > '.esc($order['customer_address']).'< / p >
< p > < strong > Téléphone :< / strong > '.esc($order['customer_phone']).'< / p >
< p > < strong > CIN :< / strong > '.esc($order['customer_cin']).'< / p >
< p style = "text-align:right;" > < em > Antananarivo, le '.$today.'< / em > < / p >
< / div > ';
// ✅ TABLEAU ADAPTÉ SELON LE TYPE
if ($isAvanceMere) {
$html .= '
< table >
< thead >
< tr >
< th > Produit< / th >
< th > Quantité< / th > <!-- ✅ AJOUT -->
< th class = "right" > Prix Unitaire (Ar)< / th >
< th class = "right" > Montant (Ar)< / th > <!-- ✅ AJOUT -->
< / tr >
< / thead >
< tbody > ';
foreach ($items as $item) {
$details = $this->getOrderItemDetails($item);
if (!$details) continue;
$qty = isset($item['qty']) ? (int)$item['qty'] : 1; // ✅ RÉCUPÉRATION
$prixUnitaire = ($discount > 0) ? ($discount / $qty) : $details['prix'];
$prixTotal = $prixUnitaire * $qty;
$html .= '
< tr >
< td > '.esc($details['product_name']);
if (!empty($details['commentaire'])) {
$html .= '< br > < em style = "font-size:12px; color:#666;" > Remarque : '.esc($details['commentaire']).'< / em > ';
}
$html .= '< / td >
< td class = "right" > '.esc($qty).'< / td > <!-- ✅ AFFICHAGE -->
< td class = "right" > '.number_format($prixUnitaire, 0, '', ' ').'< / td >
< td class = "right" > '.number_format($prixTotal, 0, '', ' ').'< / td >
< / tr > ';
}
$html .= '
< / tbody >
< / table > ';
} else {
$html .= '
< table >
< thead >
< tr >
< th > Nom< / th >
< th > Marque< / th >
< th > Catégorie< / th >
< th > N° Moteur< / th >
< th > Châssis< / th >
< th > Puissance (CC)< / th >
< th > Quantité< / th > <!-- ✅ AJOUT -->
< th class = "right" > Prix Unit. (Ar)< / th >
< th class = "right" > Montant (Ar)< / th > <!-- ✅ AJOUT -->
< / tr >
< / thead >
< tbody > ';
$Products = new Products();
$Brand = new Brands();
$Category = new Category();
foreach ($items as $item) {
$details = $this->getOrderItemDetails($item);
if (!$details) continue;
$qty = isset($item['qty']) ? (int)$item['qty'] : 1; // ✅ RÉCUPÉRATION
$prixUnitaire = ($discount > 0) ? ($discount / $qty) : $details['prix'];
$prixTotal = $prixUnitaire * $qty;
$categoryName = 'Non définie';
if ($details['type'] === 'terre' & & !empty($item['product_id'])) {
$p = $Products->getProductData($item['product_id']);
if (!empty($p['categorie_id'])) {
$categoryData = $Category->find($p['categorie_id']);
if ($categoryData & & isset($categoryData['name'])) {
$categoryName = $categoryData['name'];
}
}
}
$html .= '
< tr >
< td > '.esc($details['product_name']).'< / td >
< td > '.esc($details['marque']).'< / td >
< td > '.esc($categoryName).'< / td >
< td > '.esc($details['numero_moteur']).'< / td >
< td > '.esc($details['numero_chassis']).'< / td >
< td > '.esc($details['puissance']).'< / td >
< td class = "right" > '.esc($qty).'< / td > <!-- ✅ AFFICHAGE -->
< td class = "right" > '.number_format($prixUnitaire, 0, '', ' ').'< / td >
< td class = "right" > '.number_format($prixTotal, 0, '', ' ').'< / td >
< / tr > ';
}
$html .= '
< / tbody >
< / table > ';
}
$html .= '
< table style = "width:calc(100% - 40px); margin:0 20px 20px;" >
< tr >
< td > < strong > Total HT :< / strong > < / td >
< td class = "right" > '.number_format($totalHT, 0, '', ' ').' Ar< / td >
< / tr >
< tr >
< td > < strong > TVA (20%) :< / strong > < / td >
< td class = "right" > '.number_format($tva, 0, '', ' ').' Ar< / td >
< / tr >
< tr >
< td > < strong > Total TTC :< / strong > < / td >
< td class = "right" > '.number_format($totalTTC, 0, '', ' ').' Ar< / td >
< / tr >
< tr >
< td > < strong > Statut :< / strong > < / td >
< td class = "right" > '.$paidLabel.'< / td >
< / tr > ';
if (!empty($order['order_payment_mode'])) {
$html .= '
< tr >
< td > < strong > Mode de paiement :< / strong > < / td >
< td class = "right" > '.esc($order['order_payment_mode']).'< / td >
< / tr > ';
}
if (!empty($order['tranche_1'])) {
$html .= '
< tr >
< td > < strong > Tranche 1 :< / strong > < / td >
< td class = "right" > '.number_format((float)$order['tranche_1'], 0, '', ' ').' Ar< / td >
< / tr > ';
}
if (!empty($order['tranche_2'])) {
$html .= '
< tr >
< td > < strong > Tranche 2 :< / strong > < / td >
< td class = "right" > '.number_format((float)$order['tranche_2'], 0, '', ' ').' Ar< / td >
< / tr > ';
}
$html .= '
< / table >
< div class = "signature" >
< div > L\'acheteur< br > < br > __________________< / div >
< div > Le vendeur< br > < br > __________________< / div >
< / div >
< div class = "footer" >
Merci pour votre confiance
< / div >
< div class = "conditions" >
< div style = "display:flex; justify-content:space-between; align-items:center;" >
< h3 style = "margin:0;" > Conditions Générales< / h3 >
< img src = "'.base_url('assets/images/company_logo.jpg').'" alt = "Logo" style = "height:60px;" >
< / div >
< 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 > Le client doit vérifier soigneusement la marchandise avant de quitter notre établissement.< / li >
< li > Aucun service après-vente n\'est fourni.< / 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 >
< / ul >
< div style = "text-align:center; margin-top:50px;" > L\'Acheteur< / div >
< / div >
< / body >
< / html > ';
return $this->response->setBody($html);
}
// ====================================
// PRINT31 - Facture + Bon de commande (pages séparées)
// ====================================
public function print31(int $id)
{
$this->verifyRole('viewOrder');
if (!$id) {
return $this->response->setStatusCode(400, 'ID manquant');
}
$Orders = new Orders();
$Company = new Company();
$OrderItems = new OrderItems();
$Products = new Products();
$Brand = new Brands();
$Category = new Category();
$order_data = $Orders->getOrdersData($id);
$items = $OrderItems->getOrdersItemData($id);
$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"
$isAvanceMere = false;
foreach ($items as $item) {
if (!empty($item['product_name']) & & empty($item['product_id'])) {
$isAvanceMere = true;
break;
}
}
$paid_status = $order_data['paid_status'] === 1
? "< span style = 'color: green; font-weight: bold;' > Payé< / span > "
: "< span style = 'color: red; font-weight: bold;' > Refusé< / span > ";
$discount = (float) $order_data['discount'];
$grossAmount = (float) $order_data['gross_amount'];
$totalTTC = ($discount > 0) ? $discount : $grossAmount;
$totalHT = $totalTTC / 1.20;
$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; }
.center { text-align:center; }
.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 ---
foreach ($items as $item) {
$details = $this->getOrderItemDetails($item);
if (!$details) continue;
// ✅ RÉCUPÉRATION DE LA QUANTITÉ
$qty = isset($item['qty']) ? (int)$item['qty'] : 1;
$unitPrice = $details['prix'];
// Calcul du prix selon la quantité et la remise
if ($discount > 0 & & $isAvanceMere) {
$prixTotal = $discount;
$prixUnitaire = $discount / $qty;
} else {
$prixUnitaire = $unitPrice / $qty;
$prixTotal = $unitPrice;
}
echo '<!DOCTYPE html> ';
echo '< html lang = "fr" > ';
echo '< head > < meta charset = "utf-8" > ';
echo '< link rel = "stylesheet" href = "' . base_url('assets/bower_components/bootstrap/dist/css/bootstrap.min.css') . '" > ';
echo $style;
echo '< / head > < body onload = "window.print()" > ';
echo '< div class = "page-break" > ';
echo '< div class = "header" > ';
echo '< div class = "infos" > ';
echo '< h2 style = "margin:0;" > ' . esc($company_info['company_name']) . '< / h2 > ';
echo '< p style = "margin:2px 0;" > < strong > NIF :< / strong > ' . esc($company_info['NIF']) . '< / p > ';
echo '< p style = "margin:2px 0;" > < strong > STAT :< / strong > ' . esc($company_info['STAT']) . '< / p > ';
echo '< p style = "margin:2px 0;" > < strong > Contact :< / strong > ' . esc($company_info['phone']) . ' | ' . esc($company_info['phone2']) . '< / p > ';
echo '< p style = "margin:2px 0;" > < strong > Magasin :< / strong > ' . esc($this->returnStore($order_data['store_id'])) . '< / p > ';
echo '< / div > ';
echo '< div style = "text-align:center;" > ';
echo '< img src = "' . base_url('assets/images/company_logo.jpg') . '" alt = "Logo" > ';
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 '< / div > ';
echo '< / div > ';
echo '< div class = "client" > ';
echo '< p > < strong > DOIT :< / strong > ' . esc($order_data['customer_name']) . '< / p > ';
echo '< p > < strong > Adresse :< / strong > ' . esc($order_data['customer_address']) . '< / p > ';
echo '< p > < strong > Téléphone :< / strong > ' . esc($order_data['customer_phone']) . '< / p > ';
echo '< p > < strong > CIN :< / strong > ' . esc($order_data['customer_cin']) . '< / p > ';
echo '< / div > ';
// ✅ TABLEAU ADAPTÉ SELON LE TYPE
if ($isAvanceMere) {
echo '< table > < thead > < tr > ';
echo '< th > Désignation< / th > ';
echo '< th > Produit à compléter< / th > ';
echo '< th class = "center" > Quantité< / th > ';
echo '< th class = "right" > Prix Unit. (Ar)< / th > ';
echo '< th class = "right" > Montant (Ar)< / th > ';
echo '< / tr > < / thead > < tbody > ';
echo '< tr > ';
echo '< td > ' . esc($details['product_name']) . '< / td > ';
echo '< td > < span class = "to-fill" > < / span > < / td > ';
echo '< td class = "center" > ' . esc($qty) . '< / td > ';
echo '< td class = "right" > ' . number_format($prixUnitaire, 0, '', ' ') . '< / td > ';
echo '< td class = "right" > ' . number_format($prixTotal, 0, '', ' ') . '< / td > ';
echo '< / tr > ';
if (!empty($details['commentaire'])) {
echo '< tr > < td colspan = "5" > < em style = "font-size:12px; color:#666;" > Remarque : ' . esc($details['commentaire']) . '< / em > < / td > < / tr > ';
}
echo '< / tbody > < / table > ';
} else {
// TABLE COMPLÈTE POUR AVANCE "SUR TERRE" OU COMMANDE NORMALE
$categoryName = 'Non définie';
if ($details['type'] === 'terre' & & !empty($item['product_id'])) {
$p = $Products->getProductData($item['product_id']);
if (!empty($p['categorie_id'])) {
$categoryData = $Category->find($p['categorie_id']);
if ($categoryData & & isset($categoryData['name'])) {
$categoryName = $categoryData['name'];
}
}
}
echo '< table > < thead > < tr > ';
echo '< th > Nom< / th > ';
echo '< th > Marque< / th > ';
echo '< th > Catégorie< / th > ';
echo '< th > N° Moteur< / th > ';
echo '< th > Châssis< / th > ';
echo '< th > Puissance (CC)< / th > ';
echo '< th class = "center" > Qté< / th > ';
echo '< th class = "right" > Prix Unit. (Ar)< / th > ';
echo '< th class = "right" > Montant (Ar)< / th > ';
echo '< / tr > < / thead > < tbody > ';
echo '< tr > ';
echo '< td > ' . esc($details['product_name']) . '< / td > ';
echo '< td > ' . esc($details['marque']) . '< / td > ';
echo '< td > ' . esc($categoryName) . '< / td > ';
echo '< td > ' . esc($details['numero_moteur']) . '< / td > ';
echo '< td > ' . esc($details['numero_chassis']) . '< / td > ';
echo '< td > ' . esc($details['puissance']) . '< / td > ';
echo '< td class = "center" > ' . esc($qty) . '< / td > ';
echo '< td class = "right" > ' . number_format($prixUnitaire, 0, '', ' ') . '< / td > ';
echo '< td class = "right" > ' . number_format($prixTotal, 0, '', ' ') . '< / td > ';
echo '< / tr > ';
echo '< / tbody > < / table > ';
}
// Récapitulatif pour cette facture
$itemHT = $prixTotal / 1.20;
$itemTVA = $prixTotal - $itemHT;
echo '< table > ';
echo '< tr > < td > < strong > Total HT :< / strong > < / td > < td class = "right" > ' . number_format($itemHT, 0, '', ' ') . ' Ar< / td > < / tr > ';
echo '< tr > < td > < strong > TVA (20%) :< / strong > < / td > < td class = "right" > ' . number_format($itemTVA, 0, '', ' ') . ' Ar< / td > < / tr > ';
echo '< tr > < td > < strong > Total TTC :< / strong > < / td > < td class = "right" > ' . number_format($prixTotal, 0, '', ' ') . ' Ar< / td > < / tr > ';
echo '< tr > < td > < strong > Statut :< / strong > < / td > < td class = "right" > ' . $paid_status . '< / td > < / tr > ';
if (!empty($order_data['order_payment_mode'])) {
echo '< tr > < td > < strong > Mode de paiement :< / strong > < / td > < td class = "right" > ' . esc($order_data['order_payment_mode']) . '< / td > < / tr > ';
}
if (!empty($order_data['tranche_1'])) {
echo '< tr > < td > < strong > Tranche 1 :< / strong > < / td > < td class = "right" > ' . number_format((float)$order_data['tranche_1'], 0, '', ' ') . ' Ar< / td > < / tr > ';
}
if (!empty($order_data['tranche_2'])) {
echo '< tr > < td > < strong > Tranche 2 :< / strong > < / td > < td class = "right" > ' . number_format((float)$order_data['tranche_2'], 0, '', ' ') . ' Ar< / td > < / tr > ';
}
echo '< / table > ';
echo '< div class = "signature" > ';
echo '< div > L\'Acheteur< br > < br > __________________< / div > ';
echo '< div > Le Vendeur< br > < br > __________________< / div > ';
echo '< / div > ';
echo '< / div > '; // fin page-break
echo '< / body > < / html > ';
}
// --- BON DE LIVRAISON (UNE SEULE PAGE) ---
echo '<!DOCTYPE html> ';
echo '< html lang = "fr" > ';
echo '< head > < meta charset = "utf-8" > ';
echo '< link rel = "stylesheet" href = "' . base_url('assets/bower_components/bootstrap/dist/css/bootstrap.min.css') . '" > ';
echo $style;
echo '< / head > < body > ';
echo '< div class = "header" > ';
echo '< div class = "infos" > ';
echo '< h2 style = "margin:0;" > ' . esc($company_info['company_name']) . '< / h2 > ';
echo '< p style = "margin:2px 0;" > < strong > NIF :< / strong > ' . esc($company_info['NIF']) . '< / p > ';
echo '< p style = "margin:2px 0;" > < strong > STAT :< / strong > ' . esc($company_info['STAT']) . '< / p > ';
echo '< p style = "margin:2px 0;" > < strong > Contact :< / strong > ' . esc($company_info['phone']) . ' | ' . esc($company_info['phone2']) . '< / p > ';
echo '< / div > ';
echo '< div style = "text-align:center;" > ';
echo '< img src = "' . base_url('assets/images/company_logo.jpg') . '" alt = "Logo" > ';
echo '< p style = "margin:5px 0; font-weight:bold;" > ' . $documentTitle . ' N° ' . esc($order_data['bill_no']) . '< / p > ';
echo '< / div > ';
echo '< / div > ';
echo '< div class = "client" > ';
echo '< p > < strong > Client :< / strong > ' . esc($order_data['customer_name']) . '< / p > ';
echo '< p > < strong > Adresse :< / strong > ' . esc($order_data['customer_address']) . '< / p > ';
echo '< p > < strong > Téléphone :< / strong > ' . esc($order_data['customer_phone']) . '< / p > ';
echo '< p > < strong > CIN :< / strong > ' . esc($order_data['customer_cin']) . '< / p > ';
echo '< / div > ';
// ✅ TABLEAU RÉCAPITULATIF SELON LE TYPE
if ($isAvanceMere) {
echo '< table > < thead > < tr > ';
echo '< th > Désignation< / th > ';
echo '< th > Produit à compléter< / th > ';
echo '< th class = "center" > Quantité< / th > ';
echo '< th class = "right" > Prix Unit. (Ar)< / th > ';
echo '< th class = "right" > Montant (Ar)< / th > ';
echo '< / tr > < / thead > < tbody > ';
foreach ($items as $item) {
$details = $this->getOrderItemDetails($item);
if (!$details) continue;
$qty = isset($item['qty']) ? (int)$item['qty'] : 1;
if ($discount > 0) {
$prixTotal = $discount;
$prixUnitaire = $discount / $qty;
} else {
$prixUnitaire = $details['prix'] / $qty;
$prixTotal = $details['prix'];
}
echo '< tr > ';
echo '< td > ' . esc($details['product_name']) . '< / td > ';
echo '< td > < span class = "to-fill" > < / span > < / td > ';
echo '< td class = "center" > ' . esc($qty) . '< / td > ';
echo '< td class = "right" > ' . number_format($prixUnitaire, 0, '', ' ') . '< / td > ';
echo '< td class = "right" > ' . number_format($prixTotal, 0, '', ' ') . '< / td > ';
echo '< / tr > ';
}
echo '< / tbody > < / table > ';
} else {
echo '< table > < thead > < tr > ';
echo '< th > Nom< / th > ';
echo '< th > Marque< / th > ';
echo '< th > Catégorie< / th > ';
echo '< th > N° Moteur< / th > ';
echo '< th > Châssis< / th > ';
echo '< th > Puissance< / th > ';
echo '< th class = "center" > Qté< / th > ';
echo '< th class = "right" > Prix Unit. (Ar)< / th > ';
echo '< th class = "right" > Montant (Ar)< / th > ';
echo '< / tr > < / thead > < tbody > ';
foreach ($items as $item) {
$details = $this->getOrderItemDetails($item);
if (!$details) continue;
$qty = isset($item['qty']) ? (int)$item['qty'] : 1;
if ($discount > 0) {
$prixTotal = $discount;
$prixUnitaire = $discount / $qty;
} else {
$prixUnitaire = $details['prix'] / $qty;
$prixTotal = $details['prix'];
}
$categoryName = 'Non définie';
if ($details['type'] === 'terre' & & !empty($item['product_id'])) {
$p = $Products->getProductData($item['product_id']);
if (!empty($p['categorie_id'])) {
$categoryData = $Category->find($p['categorie_id']);
if ($categoryData) $categoryName = $categoryData['name'];
}
}
echo '< tr > ';
echo '< td > ' . esc($details['product_name']) . '< / td > ';
echo '< td > ' . esc($details['marque']) . '< / td > ';
echo '< td > ' . esc($categoryName) . '< / td > ';
echo '< td > ' . esc($details['numero_moteur']) . '< / td > ';
echo '< td > ' . esc($details['numero_chassis']) . '< / td > ';
echo '< td > ' . esc($details['puissance']) . '< / td > ';
echo '< td class = "center" > ' . esc($qty) . '< / td > ';
echo '< td class = "right" > ' . number_format($prixUnitaire, 0, '', ' ') . '< / td > ';
echo '< td class = "right" > ' . number_format($prixTotal, 0, '', ' ') . '< / td > ';
echo '< / tr > ';
}
echo '< / tbody > < / table > ';
}
echo '< table > ';
echo '< tr > < td > < strong > Total HT :< / strong > < / td > < td class = "right" > ' . number_format($totalHT, 0, '', ' ') . ' Ar< / td > < / tr > ';
echo '< tr > < td > < strong > TVA :< / strong > < / td > < td class = "right" > ' . number_format($tva, 0, '', ' ') . ' Ar< / td > < / tr > ';
echo '< tr > < td > < strong > Réduction :< / strong > < / td > < td class = "right" > ' . number_format($discount, 0, '', ' ') . ' Ar< / td > < / tr > ';
echo '< tr > < td > < strong > Total TTC :< / strong > < / td > < td class = "right" > ' . number_format($totalTTC, 0, '', ' ') . ' Ar< / td > < / tr > ';
echo '< / table > ';
echo '< div class = "signature" > ';
echo '< div > L\'Acheteur< br > < br > __________________< / div > ';
echo '< div > Le Vendeur< br > < br > __________________< / div > ';
echo '< / div > ';
// --- CONDITIONS GÉNÉRALES ---
echo '< div class = "conditions" > ';
echo '< div style = "display:flex; justify-content:space-between; align-items:center;" > ';
echo '< h3 style = "margin:0;" > Conditions Générales< / h3 > ';
echo '< img src = "' . base_url('assets/images/company_logo.jpg') . '" alt = "Logo" style = "height:60px;" > ';
echo '< / div > ';
echo '< ul > ';
echo '< 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 > ';
echo '< li > Le client doit vérifier soigneusement la marchandise avant de quitter notre établissement.< / li > ';
echo '< li > Aucun service après-vente n\'est fourni.< / li > ';
echo '< li > La moto est vendue sans garantie, car il s\'agit d\'un modèle d\'occasion.< / li > ';
echo '< 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 > ';
echo '< / ul > ';
echo '< div style = "text-align:center; margin-top:50px;" > L\'Acheteur< / div > ';
echo '< / div > ';
echo '< / body > < / html > ';
}
}