motorbike/app/Controllers/OrderController.php
andrymodeste e02671e860 feat: corrections du 09-04-2026
- Page Historique : remplacement SKU par N° de série (vue + export CSV)
- Facture : colonne N° CHASSIS remplacée par N° CHASSIS / MOTEUR (affiche les deux valeurs)
- Facture : montant en lettres vérifié et fonctionnel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 07:37:04 +02:00

3218 lines
128 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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 App\Models\Historique;
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'];
$itemCount = (int)$value['item_count'];
// Génération des boutons d'impression
if ($itemCount > 1) {
$printButtons = '<a target="_blank" href="' . site_url('orders/printDivBL/' . $value['id']) . '" class="btn btn-default" title="Bon de livraison"><i class="fa fa-print"></i></a>';
} else {
$printButtons = '<a target="_blank" href="' . site_url('orders/printDiv/' . $value['id']) . '" class="btn btn-default" title="Facture"><i class="fa fa-print"></i></a>';
$printButtons .= ' <a target="_blank" href="' . site_url('orders/printDivBL/' . $value['id']) . '" class="btn btn-info" title="Bon de livraison"><i class="fa fa-file-text-o"></i></a>';
}
// ✅ 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 .= $printButtons;
}
// CAS 2 : Commande en attente (2) SANS remise → Afficher imprimer
elseif ($value['paid_status'] == 2 && $discount == 0) {
$buttons .= $printButtons;
}
// CAS 3 : Commande en attente (2) AVEC remise validée → Afficher imprimer
elseif ($value['paid_status'] == 2 && $Remise->hasRemiseValidatedForOrder($value['id'])) {
$buttons .= $printButtons;
}
// 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 = '';
$itemCount = (int)$value['item_count'];
// Bouton imprimer
if (in_array('viewOrder', $this->permission) && $users['group_name'] != "SECURITE" && $users['group_name'] != "COMMERCIALE") {
if ($itemCount > 1) {
$buttons .= '<a target="_blank" href="' . site_url('orders/printDivBL/' . $value['id']) . '" class="btn btn-default" title="Bon de livraison"><i class="fa fa-print"></i></a>';
} else {
$buttons .= '<a target="_blank" href="' . site_url('orders/printDiv/' . $value['id']) . '" class="btn btn-default" title="Facture"><i class="fa fa-print"></i></a>';
$buttons .= ' <a target="_blank" href="' . site_url('orders/printDivBL/' . $value['id']) . '" class="btn btn-info" title="Bon de livraison"><i class="fa fa-file-text-o"></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 = '';
$itemCount = (int)$value['item_count'];
// Bouton imprimer
if (in_array('viewOrder', $this->permission) && $users['group_name'] != "SECURITE" && $users['group_name'] != "COMMERCIALE") {
if ($itemCount > 1) {
$buttons .= '<a target="_blank" href="' . site_url('orders/printDivBL/' . $value['id']) . '" class="btn btn-default" title="Bon de livraison"><i class="fa fa-print"></i></a>';
} else {
$buttons .= '<a target="_blank" href="' . site_url('orders/printDiv/' . $value['id']) . '" class="btn btn-default" title="Facture"><i class="fa fa-print"></i></a>';
$buttons .= ' <a target="_blank" href="' . site_url('orders/printDivBL/' . $value['id']) . '" class="btn btn-info" title="Bon de livraison"><i class="fa fa-file-text-o"></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->notifyGroupsByPermission('notifProduit', "Produit en rupture de stock", $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 (strtolower($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 RÉSERVÉS (product_sold = 2)
// Le produit reste dans le stock mais n'est plus commandable
// Il passera à product_sold = 1 quand la sécurité confirmera la livraison
$productModel = new Products();
foreach ($posts as $index => $product_id) {
$qty = isset($quantities[$index]) ? (int)$quantities[$index] : 1;
if ($qty == 1) {
$productModel->update($product_id, ['product_sold' => 2]);
}
}
session()->setFlashdata('success', 'Créé avec succès');
// Log de l'action
$historique = new Historique();
$customerName = $this->request->getPost('customer_name');
$historique->logAction('orders', 'CREATE', $order_id, "Création de la commande pour le client: {$customerName}");
$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'];
}
$name = $product['name'] ?? '-';
$sku = $product['sku'] ?? '-';
$moteur = $product['numero_de_moteur'] ?? '-';
$chasis = $product['chasis'] ?? '-';
$puissance= $product['puissance'] ?? '-';
$image = $product['image'] ?? '';
$imgUrl = base_url('assets/images/product_image/' . $image);
$details = htmlspecialchars(json_encode([
'name' => $name,
'sku' => $sku,
'moteur' => $moteur,
'chasis' => $chasis,
'puissance' => $puissance,
'image' => $image ? $imgUrl : '',
], JSON_UNESCAPED_UNICODE), ENT_QUOTES);
$imgTag = $image
? "<img src='{$imgUrl}' style='width:50px;height:40px;object-fit:cover;border-radius:4px;margin-right:8px;vertical-align:middle;' alt='{$name}'>"
: "<span style='display:inline-block;width:50px;height:40px;background:#eee;border-radius:4px;margin-right:8px;vertical-align:middle;text-align:center;line-height:40px;font-size:10px;color:#999;'>No img</span>";
$product_info_lines[] =
"<div style='display:flex;align-items:center;padding:4px 0;'>" .
$imgTag .
"<div style='flex:1;'><strong>{$name}</strong><br><small>N\u00b0 S\u00e9rie : {$sku}</small></div>" .
"<button onclick='event.preventDefault();event.stopPropagation();showMotoDetails(this)' " .
"data-moto='{$details}' " .
"style='background:none;border:none;cursor:pointer;color:#3c8dbc;font-size:18px;padding:0 4px;' title='Voir d\u00e9tails'>" .
"<i class='fa fa-info-circle'></i></button>" .
"</div>";
}
$product_output = implode("\n", $product_lines);
$data1 = [
'date_demande' => date('Y-m-d H:i:s'),
'montant_demande' => $discount,
'total_price' => $gross_amount,
'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);
$Notification->notifyGroupsByPermissionAllStores('notifRemise', $message, 'remise/');
// Notifier la Caissière (notifCommande) et l'admin/direction (notifSortieCaisse)
$messageCommande = "📦 Nouvelle commande (avec remise) à traiter : {$bill_no}<br>" .
"Client : {$data['customer_name']}<br>" .
"Montant demandé : " . number_format($discount, 0, ',', ' ') . " Ar<br>" .
"En attente de validation remise";
$Notification->notifyGroupsByPermission('notifCommande', $messageCommande, (int)$users['store_id'], "orders");
$Notification->notifyGroupsByPermission('notifSortieCaisse', $messageCommande, (int)$users['store_id'], "orders");
} else {
// Commande sans remise - notifier Caissière (notifCommande) et admin/direction (notifSortieCaisse)
$messageCommande = "📦 Nouvelle commande à traiter : {$bill_no}<br>" .
"Client : {$data['customer_name']}<br>" .
"Montant : " . number_format($gross_amount, 0, ',', ' ') . " Ar";
$Notification->notifyGroupsByPermission('notifCommande', $messageCommande, (int)$users['store_id'], "orders");
$Notification->notifyGroupsByPermission('notifSortieCaisse', $messageCommande, (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) {
// ✅ MARQUER LES PRODUITS COMME VENDUS (product_sold = 1) À LA LIVRAISON
$OrderItems = new OrderItems();
$productModel = new Products();
$orderItems = $OrderItems->getOrdersItemData((int)$order_id);
foreach ($orderItems as $item) {
if (!empty($item['product_id'])) {
$productModel->update($item['product_id'], ['product_sold' => 1]);
}
}
// ✅ 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 TOUS LES GROUPES AYANT validateCommande1
$Notification->notifyGroupsByPermissionAllStores('notifCommande', $messageGlobal, '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 (strtolower($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;
} else {
$paid_status = $this->request->getPost('paid_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') {
$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)) {
// ✅ MARQUER LES PRODUITS COMME RÉSERVÉS (product_sold = 2)
// Livraison confirmée par la sécurité → product_sold = 1
$productModel = new Products();
foreach ($posts as $index => $product_id) {
$qty = isset($quantities[$index]) ? (int)$quantities[$index] : 1;
if ($qty == 1) {
$productModel->update($product_id, ['product_sold' => 2]);
} else {
$productModel->update($product_id, ['product_sold' => 0]);
}
}
$order_item_data = $OrderItems->getOrdersItemData($id);
$product_ids = array_column($order_item_data, 'product_id');
// Log de l'action paiement
$historique = new Historique();
$bill_no_log = $current_order['bill_no'];
if ($old_paid_status == 2 && $paid_status == 1) {
$historique->logAction('orders', 'PAYMENT', $id, "Paiement de la commande {$bill_no_log}");
} else {
$historique->logAction('orders', 'UPDATE', $id, "Modification de la commande {$bill_no_log}");
}
$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'];
// ✅ CRÉER LES ENTRÉES DANS LA TABLE SECURITE POUR CHAQUE PRODUIT
$SecuriteModel = new \App\Models\Securite();
foreach ($order_item_data as $item) {
if (!empty($item['product_id'])) {
$SecuriteModel->insert([
'product_id' => $item['product_id'],
'bill_no' => $bill_no,
'store_id' => (int)$user['store_id'],
'status' => 'PENDING',
'date' => date('Y-m-d H:i:s'),
'active' => 1
]);
}
}
// ✅ Notification groupes ayant createSecurite
$Notification->notifyGroupsByPermission(
'notifSecurite',
"Commande validée: {$bill_no} - Client: {$customer_name}",
(int)$user['store_id'],
'orders'
);
// ✅ RÉCUPÉRER TOUS LES STORES
$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']}";
// ✅ NOTIFIER TOUS LES GROUPES AYANT validateCommande1
$Notification->notifyGroupsByPermissionAllStores('notifCommande', $messageGlobal, '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' => $gross_amount,
'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']}";
$Notification->notifyGroupsByPermissionAllStores('notifRemise', $message, '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)) {
// Log suppression
$historique = new Historique();
$historique->logAction('orders', 'DELETE', $order_id, "Suppression de la commande ID: {$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();
$OrderItems = new OrderItems();
$order = $Orders->getOrdersData($id);
$items = $OrderItems->getOrdersItemData($id);
// Plus d'1 produit = BL, 1 seul produit = Facture
if (count($items) > 1) {
return $this->print7($id); // Bon de livraison
} else {
return $this->print5($id); // Facture
}
}
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');
$year = date('Y');
$discount = (float) $order['discount'];
$grossAmount = (float) $order['gross_amount'];
$totalAPayer = ($discount > 0) ? $discount : $grossAmount;
$remiseAmount = ($discount > 0) ? ($grossAmount - $discount) : 0;
$inWords = strtoupper($this->numberToWords((int) round($totalAPayer)));
// Construire les lignes du tableau
$tableRows = '';
$totalPrixIndividuels = 0;
foreach ($items as $it) {
$details = $this->getOrderItemDetails($it);
if (!$details) continue;
$qty = isset($it['qty']) ? (int)$it['qty'] : 1;
$prixItem = $details['prix'] * $qty;
$totalPrixIndividuels += $prixItem;
$chassis = esc($details['numero_chassis']);
$moteur = esc($details['numero_moteur']);
$chassisMoteur = '';
if (!empty($chassis) && $chassis !== 'N/A') {
$chassisMoteur .= $chassis;
}
if (!empty($moteur) && $moteur !== 'N/A') {
$chassisMoteur .= (!empty($chassisMoteur) ? ' / ' : '') . $moteur;
}
if (empty($chassisMoteur)) {
$chassisMoteur = 'N/A';
}
$tableRows .= '<tr>
<td>'.esc($details['marque']).'</td>
<td>'.$chassisMoteur.'</td>
<td>'.esc($details['puissance']).'</td>
<td>'.number_format($prixItem, 0, ' ', ' ').'</td>
</tr>';
}
// Lignes vides
$itemCount = count($items);
for ($j = $itemCount; $j < 3; $j++) {
$tableRows .= '<tr><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td></tr>';
}
$billNo = esc($order['bill_no']);
$customerName = esc($order['customer_name']);
$customerAddress = esc($order['customer_address'] ?? '');
$customerCin = esc($order['customer_cin'] ?? '');
$companyName = esc($company['company_name']);
$companyNIF = esc($company['NIF']);
$companySTAT = esc($company['STAT']);
$companyPhone = esc($company['phone']);
$companyPhone2 = esc($company['phone2']);
$companyAddress = esc($company['address'] ?? '');
$totalFormatted = number_format($totalAPayer, 0, ' ', ' ');
$qrValue = "FACTURE N° {$billNo} | Client: {$customerName} | Montant: {$totalFormatted} MGA | Date: {$today} | Facebook: https://www.facebook.com/MOTORBIKESTORE2021/";
$html = '<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>FACTURE '.$billNo.'</title>
<style>
@page { size: A4 portrait; margin: 0; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: Arial, sans-serif;
font-size: 13px;
line-height: 1.3;
color: #000;
}
.page {
display: flex;
flex-direction: column;
width: 210mm;
height: 297mm;
}
.facture-box {
flex: 1;
height: 148.5mm;
padding: 6mm 8mm;
display: flex;
flex-direction: column;
border-bottom: 2px dashed #aaa;
overflow: hidden;
}
.facture-box:last-child {
border-bottom: none;
}
/* Header */
.f-title {
font-size: 24px;
font-weight: bold;
text-decoration: underline;
}
.f-company {
font-size: 12px;
line-height: 1.3;
margin-bottom: 8px;
}
.f-company strong { font-size: 14px; }
.f-doit {
font-weight: bold;
font-size: 14px;
margin-bottom: 6px;
}
.f-client {
font-size: 12px;
line-height: 1.3;
margin-bottom: 10px;
}
.f-client .field { margin: 3px 0; }
/* Table */
.f-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 6px;
}
.f-table th, .f-table td {
border-left: 2px solid #000;
border-right: 2px solid #000;
padding: 3px 8px;
text-align: center;
font-size: 12px;
}
.f-table th {
font-weight: bold;
border-top: 2px solid #000;
border-bottom: 2px solid #000;
}
.f-table td { height: 20px; }
.f-table tbody tr:last-child td {
border-bottom: 2px solid #000;
}
/* Total en lettres */
.f-words {
font-size: 12px;
line-height: 1.3;
margin-bottom: 8px;
}
/* Signatures */
.f-signatures {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.f-sig-box {
border: 2px solid #000;
width: 38%;
min-height: 55px;
padding: 5px 8px;
}
.f-sig-box .sig-label {
font-weight: bold;
font-size: 13px;
}
/* Date + N° + QR en bas */
.f-bottom {
margin-top: auto;
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.f-bottom .f-date-info {
font-size: 12px;
}
.f-bottom canvas {
width: 70px;
height: 70px;
}
/* === VERSO === */
.conditions-page {
page-break-before: always;
display: flex;
flex-direction: column;
width: 210mm;
height: 297mm;
}
.cond-box {
flex: 1;
height: 148.5mm;
padding: 6mm 8mm;
display: flex;
flex-direction: column;
border-bottom: 2px dashed #aaa;
line-height: 1.3;
}
.cond-box:last-child { border-bottom: none; }
.cond-title {
font-size: 16px;
font-weight: bold;
font-style: italic;
text-decoration: underline;
margin-bottom: 15px;
}
.cond-box ul {
list-style: none;
padding: 0;
font-size: 12px;
line-height: 1.3;
}
.cond-box li {
margin-bottom: 10px;
}
.cond-box li::before {
content: "*";
font-weight: bold;
margin-right: 3px;
}
.cond-buyer {
margin-top: 30px;
font-size: 11px;
}
.cond-bottom {
margin-top: auto;
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.cond-bottom canvas {
width: 70px;
height: 70px;
}
.cond-bottom img {
height: 60px;
}
@media print {
* { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
.no-print { display: none !important; }
}
</style>
</head>
<body>
<!-- RECTO : 2 factures côte à côte -->
<div class="page">';
for ($i = 0; $i < 2; $i++) {
$qrId = 'qr_recto_'.$i;
$html .= '
<div class="facture-box">
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:8px;">
<div class="f-company" style="margin-bottom:0;">
<strong>'.$companyName.'</strong><br>
NIF : '.$companyNIF.'<br>
STAT : '.$companySTAT.'<br>
Contact : '.$companyPhone.' / '.$companyPhone2.'<br>
'.$companyAddress.'
</div>
<div style="text-align:center; flex:1;">
<div class="f-title">FACTURE</div>
</div>
<div style="text-align:right; font-size:12px;">
<div>DATE : '.$today.'</div>
<div>N° : '.$billNo.'</div>
<canvas id="'.$qrId.'" style="margin-top:5px;"></canvas>
</div>
</div>
<div class="f-doit">DOIT</div>
<div class="f-client">
<div class="field">Nom : '.$customerName.'</div>
<div class="field">Prenom : </div>
<div class="field">Adresse : '.$customerAddress.'</div>
<div class="field">CIN : </div>
</div>
<table class="f-table">
<thead>
<tr>
<th>MARQUE</th>
<th>N° CHASSIS / MOTEUR</th>
<th>PUISSANCE</th>
<th>PRIX (Ariary)</th>
</tr>
</thead>
<tbody>
'.$tableRows.'
</tbody>
</table>
<div class="f-words">
<strong>Total :</strong> '.number_format($totalPrixIndividuels, 0, ' ', ' ').' Ariary<br>';
if ($remiseAmount > 0) {
$html .= '<strong>Remise :</strong> -'.number_format($remiseAmount, 0, ' ', ' ').' Ariary<br>
<strong>Total à payer :</strong> '.number_format($totalAPayer, 0, ' ', ' ').' Ariary<br>';
}
$html .= '<strong>Arrête à la somme de :</strong> '.$inWords.'
</div>
<div class="f-signatures">
<div class="f-sig-box">
<div class="sig-label">ACHETEUR</div>
</div>
<div class="f-sig-box">
<div class="sig-label">VENDEUR</div>
</div>
</div>
</div>';
}
$html .= '
</div>
<!-- VERSO : 2 conditions générales côte à côte -->
<div class="conditions-page">';
for ($i = 0; $i < 2; $i++) {
$qrIdV = 'qr_verso_'.$i;
$html .= '
<div class="cond-box">
<div class="cond-title" style="text-align:center;">Conditions Générales :</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 s\'assurer de 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 que la marchandise soit fonctionnelle ou pas</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 un délai prévu par la loi.</li>
</ul>
<div class="cond-buyer" style="text-align:center;">Acheteur</div>
<div class="cond-bottom">
<img src="'.base_url($this->getCompanyLogo()).'?v='.time().'" alt="Logo">
<canvas id="'.$qrIdV.'"></canvas>
</div>
</div>';
}
$html .= '
</div>
<script src="https://cdn.jsdelivr.net/npm/qrious@4.0.2/dist/qrious.min.js"></script>
<script>
var qrValue = '.json_encode($qrValue).';
["qr_recto_0","qr_recto_1","qr_verso_0","qr_verso_1"].forEach(function(id) {
var el = document.getElementById(id);
if (el) {
new QRious({ element: el, value: qrValue, size: 70, level: "H" });
}
});
window.onload = function() { window.print(); };
</script>
</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;
// 7079 et 9099
if ($d === 7 || $d === 9) {
$base = $d === 7 ? 60 : 80;
return $tens[$d] . ($r ? '-' . $units[$n - $base] : '');
}
// 2069 ou 8089
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();
$Products = new Products();
$order = $Orders->getOrdersData($id);
$items = $OrderItems->getOrdersItemData($id);
$company = $Company->getCompanyData(1);
$today = date('d/m/Y');
$discount = (float) $order['discount'];
$grossAmount = (float) $order['gross_amount'];
$totalAPayer = ($discount > 0) ? $discount : $grossAmount;
$remiseAmount = ($discount > 0) ? ($grossAmount - $discount) : 0;
$inWords = strtoupper($this->numberToWords((int) round($totalAPayer)));
// Construire les lignes du tableau - prix individuel de chaque moto
$tableRows = '';
$totalPrixIndividuels = 0;
foreach ($items as $item) {
$details = $this->getOrderItemDetails($item);
if (!$details) continue;
$qty = isset($item['qty']) ? (int)$item['qty'] : 1;
$prixItem = $details['prix'] * $qty;
$totalPrixIndividuels += $prixItem;
$tableRows .= '<tr>
<td>'.esc($details['marque']).'</td>
<td>'.esc($details['numero_moteur']).'</td>
<td>'.esc($details['puissance']).'</td>
<td>'.number_format($prixItem, 0, '', ' ').'</td>
</tr>';
}
// Lignes vides pour compléter le tableau (min 4 lignes)
$itemCount = count($items);
for ($i = $itemCount; $i < 4; $i++) {
$tableRows .= '<tr><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td></tr>';
}
$html = '<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>BON DE LIVRAISON '.esc($order['bill_no']).'</title>
<style>
@media print {
@page { size: A4 portrait; margin: 8mm; }
body { margin: 0; padding: 0; }
.no-print { display: none !important; }
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: Arial, sans-serif;
font-size: 14px;
line-height: 1.3;
background: #fff;
}
.page {
width: 210mm;
min-height: 297mm;
padding: 10mm 15mm;
margin: 0 auto;
display: flex;
flex-direction: column;
}
/* Header */
.bl-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15px;
}
.bl-company {
font-size: 13px;
line-height: 1.3;
}
.bl-company strong {
font-size: 16px;
}
.bl-title-block {
text-align: center;
}
.bl-title {
font-size: 26px;
font-weight: bold;
letter-spacing: 2px;
margin-bottom: 5px;
}
.bl-date {
font-size: 13px;
margin-top: 5px;
}
.qr-code {
width: 90px;
height: 90px;
margin-top: 8px;
}
/* Client info */
.bl-client {
margin-bottom: 12px;
font-size: 14px;
line-height: 1.3;
}
.bl-client .field {
margin: 4px 0;
}
.bl-client .field strong {
display: inline-block;
min-width: 90px;
}
.bl-doit {
font-weight: bold;
font-size: 15px;
margin-bottom: 8px;
}
/* Table */
.bl-table {
width: 100%;
border-collapse: collapse;
}
.bl-table th, .bl-table td {
border-left: 2px solid #000;
border-right: 2px solid #000;
padding: 5px 10px;
text-align: center;
font-size: 14px;
}
.bl-table th {
font-weight: bold;
font-size: 14px;
background: #fff;
border-top: 2px solid #000;
border-bottom: 2px solid #000;
}
.bl-table tbody tr:last-child td {
border-bottom: 2px solid #000;
}
/* Footer */
.bl-footer {
margin-top: 15px;
}
.bl-total {
font-size: 15px;
line-height: 1.3;
margin-bottom: 15px;
}
.bl-total strong {
display: inline-block;
min-width: 180px;
}
.bl-signatures {
display: flex;
justify-content: space-between;
}
.bl-sig-box {
border: 2px solid #000;
width: 35%;
min-height: 70px;
padding: 8px 12px;
}
.bl-sig-box .sig-label {
font-weight: bold;
font-size: 13px;
}
</style>
</head>
<body>
<div class="page">
<!-- Titre centré -->
<div class="bl-title" style="text-align:center;">BON DE LIVRAISON</div>
<!-- Header -->
<div class="bl-header">
<div class="bl-company">
<strong>'.esc($company['company_name']).'</strong><br>
NIF : '.esc($company['NIF']).'<br>
STAT : '.esc($company['STAT']).'<br>
Contact : '.esc($company['phone']).' / '.esc($company['phone2']).'<br>
'.esc($company['address']).'
</div>
<div style="text-align:right; font-size:12px;">
<div>Date : <strong>'.$today.'</strong></div>
<canvas id="qrcode" class="qr-code" style="margin-top:8px;"></canvas>
</div>
</div>
<!-- DOIT + Client -->
<div class="bl-client">
<div class="bl-doit">DOIT</div>
<div class="field"><strong>Nom :</strong> '.esc($order['customer_name']).'</div>
<div class="field"><strong>Prenom :</strong> </div>
<div class="field"><strong>Adresse :</strong> '.esc($order['customer_address']).'</div>
<div class="field"><strong>CIN :</strong> '.esc($order['customer_cin']).'</div>
</div>
<!-- Tableau -->
<table class="bl-table">
<thead>
<tr>
<th>MARQUE</th>
<th>N°MOTEUR</th>
<th>PUISSANCE</th>
<th>PRIX (Ariary)</th>
</tr>
</thead>
<tbody>
'.$tableRows.'
</tbody>
</table>
<!-- Footer -->
<div class="bl-footer">
<div class="bl-total">
<strong>Total :</strong> '.number_format($totalPrixIndividuels, 0, '', ' ').' Ariary<br>';
if ($remiseAmount > 0) {
$html .= '<strong>Remise :</strong> -'.number_format($remiseAmount, 0, '', ' ').' Ariary<br>
<strong>Total à payer :</strong> '.number_format($totalAPayer, 0, '', ' ').' Ariary<br>';
}
$html .= '<strong>Arrête à la somme de :</strong> '.$inWords.'
</div>
<div class="bl-signatures">
<div class="bl-sig-box">
<div class="sig-label">ACHETEUR</div>
</div>
<div class="bl-sig-box">
<div class="sig-label">VENDEUR</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/qrious@4.0.2/dist/qrious.min.js"></script>
<script>
new QRious({
element: document.getElementById("qrcode"),
value: "BON DE LIVRAISON\\nN° '.esc($order['bill_no']).'\\nClient: '.esc($order['customer_name']).'\\nMontant: '.number_format($totalAPayer, 0, '', ' ').' Ar\\nDate: '.$today.'\\nFacebook: https://www.facebook.com/MOTORBIKESTORE2021/",
size: 90,
level: "H"
});
window.onload = function() { window.print(); };
</script>
</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($this->getCompanyLogo()) . '?v=' . time() . '" 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">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</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($this->getCompanyLogo()) . '?v=' . time() . '" 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">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</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($this->getCompanyLogo()) . '?v=' . time() . '" 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>';
}
}