migration Commandes fournisseurs, les Lignes de commandes de marchandises, Lignes de réception, Factures fournisseurs
This commit is contained in:
parent
f62a2db36e
commit
3bc4178a12
151
thanasoft-back/app/Http/Controllers/Api/AvoirController.php
Normal file
151
thanasoft-back/app/Http/Controllers/Api/AvoirController.php
Normal file
@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreAvoirRequest;
|
||||
use App\Http\Requests\UpdateAvoirRequest;
|
||||
use App\Http\Resources\AvoirResource;
|
||||
use App\Repositories\AvoirRepositoryInterface;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class AvoirController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected AvoirRepositoryInterface $avoirRepository
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of credits.
|
||||
*/
|
||||
public function index(): AnonymousResourceCollection|JsonResponse
|
||||
{
|
||||
try {
|
||||
$avoirs = $this->avoirRepository->all();
|
||||
return AvoirResource::collection($avoirs);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error fetching avoirs: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Une erreur est survenue lors de la récupération des avoirs.',
|
||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created credit.
|
||||
*/
|
||||
public function store(StoreAvoirRequest $request): AvoirResource|JsonResponse
|
||||
{
|
||||
try {
|
||||
$avoir = $this->avoirRepository->create($request->validated());
|
||||
return new AvoirResource($avoir);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error creating avoir: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'data' => $request->validated(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Une erreur est survenue lors de la création de l\'avoir.',
|
||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified credit.
|
||||
*/
|
||||
public function show(string $id): AvoirResource|JsonResponse
|
||||
{
|
||||
try {
|
||||
$avoir = $this->avoirRepository->find($id);
|
||||
|
||||
if (!$avoir) {
|
||||
return response()->json([
|
||||
'message' => 'Avoir non trouvé.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return new AvoirResource($avoir);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error fetching avoir: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'avoir_id' => $id,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Une erreur est survenue lors de la récupération de l\'avoir.',
|
||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified credit.
|
||||
*/
|
||||
public function update(UpdateAvoirRequest $request, string $id): AvoirResource|JsonResponse
|
||||
{
|
||||
try {
|
||||
$updated = $this->avoirRepository->update($id, $request->validated());
|
||||
|
||||
if (!$updated) {
|
||||
return response()->json([
|
||||
'message' => 'Avoir non trouvé ou échec de la mise à jour.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$avoir = $this->avoirRepository->find($id);
|
||||
return new AvoirResource($avoir);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error updating avoir: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'avoir_id' => $id,
|
||||
'data' => $request->validated(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Une erreur est survenue lors de la mise à jour de l\'avoir.',
|
||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified credit.
|
||||
*/
|
||||
public function destroy(string $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$deleted = $this->avoirRepository->delete($id);
|
||||
|
||||
if (!$deleted) {
|
||||
return response()->json([
|
||||
'message' => 'Avoir non trouvé ou échec de la suppression.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Avoir supprimé avec succès.',
|
||||
], 200);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error deleting avoir: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'avoir_id' => $id,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Une erreur est survenue lors de la suppression de l\'avoir.',
|
||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
48
thanasoft-back/app/Http/Requests/StoreAvoirRequest.php
Normal file
48
thanasoft-back/app/Http/Requests/StoreAvoirRequest.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreAvoirRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'client_id' => 'required|exists:clients,id',
|
||||
'invoice_id' => 'nullable|exists:invoices,id',
|
||||
'group_id' => 'nullable|exists:client_groups,id',
|
||||
'status' => 'required|in:brouillon,emis,applique,annule',
|
||||
'avoir_date' => 'required|date',
|
||||
'due_date' => 'nullable|date|after_or_equal:avoir_date',
|
||||
'currency' => 'required|string|size:3',
|
||||
'total_ht' => 'required|numeric',
|
||||
'total_tva' => 'required|numeric',
|
||||
'total_ttc' => 'required|numeric',
|
||||
'reason_type' => 'required|in:remboursement_total,remboursement_partiel,reduction,erreur_facturation,retour_marchandise,accord_commercial,autre',
|
||||
'reason_description' => 'nullable|string',
|
||||
'e_invoice_status' => 'nullable|in:cree,transmis,accepte,refuse,en_litige,acquitte,archive',
|
||||
'refund_status' => 'nullable|in:non_rembourse,en_cours,partiellement_rembourse,rembourse,compense',
|
||||
'refund_date' => 'nullable|date',
|
||||
'refund_method' => 'nullable|in:virement,cheque,carte_credit,compensation_future,autre',
|
||||
'compensation_invoice_id' => 'nullable|exists:invoices,id',
|
||||
'compensation_amount' => 'nullable|numeric|min:0',
|
||||
'lines' => 'required|array|min:1',
|
||||
'lines.*.product_id' => 'nullable|exists:products,id',
|
||||
'lines.*.invoice_line_id' => 'nullable|exists:invoice_lines,id',
|
||||
'lines.*.description' => 'required|string',
|
||||
'lines.*.quantity' => 'required|numeric',
|
||||
'lines.*.unit_price' => 'required|numeric',
|
||||
'lines.*.tva_rate' => 'required|numeric',
|
||||
'lines.*.total_ht' => 'required|numeric',
|
||||
'lines.*.total_tva' => 'required|numeric',
|
||||
'lines.*.total_ttc' => 'required|numeric',
|
||||
'lines.*.notes' => 'nullable|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
27
thanasoft-back/app/Http/Requests/UpdateAvoirRequest.php
Normal file
27
thanasoft-back/app/Http/Requests/UpdateAvoirRequest.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateAvoirRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'status' => 'sometimes|required|in:brouillon,emis,applique,annule',
|
||||
'avoir_date' => 'sometimes|required|date',
|
||||
'due_date' => 'nullable|date|after_or_equal:avoir_date',
|
||||
'refund_status' => 'nullable|in:non_rembourse,en_cours,partiellement_rembourse,rembourse,compense',
|
||||
'refund_date' => 'nullable|date',
|
||||
'refund_method' => 'nullable|in:virement,cheque,carte_credit,compensation_future,autre',
|
||||
'reason_description' => 'nullable|string',
|
||||
'notes' => 'nullable|string',
|
||||
];
|
||||
}
|
||||
}
|
||||
28
thanasoft-back/app/Http/Resources/AvoirLineResource.php
Normal file
28
thanasoft-back/app/Http/Resources/AvoirLineResource.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class AvoirLineResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'avoir_id' => $this->avoir_id,
|
||||
'product_id' => $this->product_id,
|
||||
'invoice_line_id' => $this->invoice_line_id,
|
||||
'description' => $this->description,
|
||||
'quantity' => $this->quantity,
|
||||
'unit_price' => $this->unit_price,
|
||||
'tva_rate' => $this->tva_rate,
|
||||
'total_ht' => $this->total_ht,
|
||||
'total_tva' => $this->total_tva,
|
||||
'total_ttc' => $this->total_ttc,
|
||||
'notes' => $this->notes,
|
||||
'product' => $this->whenLoaded('product'),
|
||||
];
|
||||
}
|
||||
}
|
||||
42
thanasoft-back/app/Http/Resources/AvoirResource.php
Normal file
42
thanasoft-back/app/Http/Resources/AvoirResource.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class AvoirResource extends JsonResource
|
||||
{
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'client_id' => $this->client_id,
|
||||
'invoice_id' => $this->invoice_id,
|
||||
'group_id' => $this->group_id,
|
||||
'avoir_number' => $this->avoir_number,
|
||||
'status' => $this->status,
|
||||
'avoir_date' => $this->avoir_date,
|
||||
'due_date' => $this->due_date,
|
||||
'currency' => $this->currency,
|
||||
'total_ht' => $this->total_ht,
|
||||
'total_tva' => $this->total_tva,
|
||||
'total_ttc' => $this->total_ttc,
|
||||
'reason_type' => $this->reason_type,
|
||||
'reason_description' => $this->reason_description,
|
||||
'e_invoice_status' => $this->e_invoice_status,
|
||||
'refund_status' => $this->refund_status,
|
||||
'refund_date' => $this->refund_date,
|
||||
'refund_method' => $this->refund_method,
|
||||
'compensation_invoice_id' => $this->compensation_invoice_id,
|
||||
'compensation_amount' => $this->compensation_amount,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
'client' => $this->whenLoaded('client'),
|
||||
'invoice' => $this->whenLoaded('invoice'),
|
||||
'group' => $this->whenLoaded('group'),
|
||||
'lines' => AvoirLineResource::collection($this->whenLoaded('lines')),
|
||||
'history' => DocumentStatusHistoryResource::collection($this->whenLoaded('history')),
|
||||
];
|
||||
}
|
||||
}
|
||||
98
thanasoft-back/app/Models/Avoir.php
Normal file
98
thanasoft-back/app/Models/Avoir.php
Normal file
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Avoir extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'client_id',
|
||||
'invoice_id',
|
||||
'group_id',
|
||||
'avoir_number',
|
||||
'status',
|
||||
'avoir_date',
|
||||
'due_date',
|
||||
'currency',
|
||||
'total_ht',
|
||||
'total_tva',
|
||||
'total_ttc',
|
||||
'reason_type',
|
||||
'reason_description',
|
||||
'e_invoice_status',
|
||||
'refund_status',
|
||||
'refund_date',
|
||||
'refund_method',
|
||||
'compensation_invoice_id',
|
||||
'compensation_amount',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'avoir_date' => 'date',
|
||||
'due_date' => 'date',
|
||||
'refund_date' => 'date',
|
||||
'total_ht' => 'decimal:2',
|
||||
'total_tva' => 'decimal:2',
|
||||
'total_ttc' => 'decimal:2',
|
||||
'compensation_amount' => 'decimal:2',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::creating(function ($avoir) {
|
||||
// Auto-generate avoir number if not provided
|
||||
if (empty($avoir->avoir_number)) {
|
||||
$prefix = 'AV-' . now()->format('Ym') . '-';
|
||||
$lastAvoir = self::where('avoir_number', 'like', $prefix . '%')
|
||||
->orderBy('avoir_number', 'desc')
|
||||
->first();
|
||||
|
||||
if ($lastAvoir) {
|
||||
// Extract numeric part
|
||||
preg_match('/(\d+)$/', $lastAvoir->avoir_number, $matches);
|
||||
$newNumber = $matches ? intval($matches[1]) + 1 : 1;
|
||||
} else {
|
||||
$newNumber = 1;
|
||||
}
|
||||
|
||||
$avoir->avoir_number = $prefix . str_pad((string)$newNumber, 4, '0', STR_PAD_LEFT);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function client()
|
||||
{
|
||||
return $this->belongsTo(Client::class);
|
||||
}
|
||||
|
||||
public function invoice()
|
||||
{
|
||||
return $this->belongsTo(Invoice::class);
|
||||
}
|
||||
|
||||
public function group()
|
||||
{
|
||||
return $this->belongsTo(ClientGroup::class, 'group_id');
|
||||
}
|
||||
|
||||
public function lines()
|
||||
{
|
||||
return $this->hasMany(AvoirLine::class);
|
||||
}
|
||||
|
||||
public function compensationInvoice()
|
||||
{
|
||||
return $this->belongsTo(Invoice::class, 'compensation_invoice_id');
|
||||
}
|
||||
|
||||
public function history()
|
||||
{
|
||||
return $this->hasMany(DocumentStatusHistory::class, 'document_id')
|
||||
->where('document_type', 'avoir')
|
||||
->orderBy('changed_at', 'desc');
|
||||
}
|
||||
}
|
||||
49
thanasoft-back/app/Models/AvoirLine.php
Normal file
49
thanasoft-back/app/Models/AvoirLine.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AvoirLine extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'avoir_id',
|
||||
'product_id',
|
||||
'invoice_line_id',
|
||||
'description',
|
||||
'quantity',
|
||||
'unit_price',
|
||||
'tva_rate',
|
||||
'total_ht',
|
||||
'total_tva',
|
||||
'total_ttc',
|
||||
'notes',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'quantity' => 'decimal:3',
|
||||
'unit_price' => 'decimal:4',
|
||||
'tva_rate' => 'decimal:2',
|
||||
'total_ht' => 'decimal:2',
|
||||
'total_tva' => 'decimal:2',
|
||||
'total_ttc' => 'decimal:2',
|
||||
];
|
||||
|
||||
public function avoir()
|
||||
{
|
||||
return $this->belongsTo(Avoir::class);
|
||||
}
|
||||
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
|
||||
public function invoiceLine()
|
||||
{
|
||||
return $this->belongsTo(InvoiceLine::class);
|
||||
}
|
||||
}
|
||||
@ -81,6 +81,16 @@ class AppServiceProvider extends ServiceProvider
|
||||
|
||||
$this->app->bind(\App\Repositories\InvoiceLineRepositoryInterface::class, \App\Repositories\InvoiceLineRepository::class);
|
||||
|
||||
$this->app->bind(\App\Repositories\AvoirRepositoryInterface::class, function ($app) {
|
||||
return new \App\Repositories\AvoirRepository(
|
||||
$app->make(\App\Models\Avoir::class),
|
||||
$app->make(\App\Repositories\AvoirLineRepositoryInterface::class),
|
||||
$app->make(\App\Repositories\ClientActivityTimelineRepositoryInterface::class)
|
||||
);
|
||||
});
|
||||
|
||||
$this->app->bind(\App\Repositories\AvoirLineRepositoryInterface::class, \App\Repositories\AvoirLineRepository::class);
|
||||
|
||||
$this->app->bind(\App\Repositories\QuoteRepositoryInterface::class, function ($app) {
|
||||
return new \App\Repositories\QuoteRepository(
|
||||
$app->make(\App\Models\Quote::class),
|
||||
|
||||
15
thanasoft-back/app/Repositories/AvoirLineRepository.php
Normal file
15
thanasoft-back/app/Repositories/AvoirLineRepository.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\AvoirLine;
|
||||
|
||||
class AvoirLineRepository extends BaseRepository implements AvoirLineRepositoryInterface
|
||||
{
|
||||
public function __construct(AvoirLine $model)
|
||||
{
|
||||
parent::__construct($model);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
interface AvoirLineRepositoryInterface extends BaseRepositoryInterface
|
||||
{
|
||||
}
|
||||
123
thanasoft-back/app/Repositories/AvoirRepository.php
Normal file
123
thanasoft-back/app/Repositories/AvoirRepository.php
Normal file
@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
use App\Models\Avoir;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class AvoirRepository extends BaseRepository implements AvoirRepositoryInterface
|
||||
{
|
||||
public function __construct(
|
||||
Avoir $model,
|
||||
protected AvoirLineRepositoryInterface $avoirLineRepository,
|
||||
protected readonly ClientActivityTimelineRepositoryInterface $timelineRepository
|
||||
) {
|
||||
parent::__construct($model);
|
||||
}
|
||||
|
||||
public function all(array $columns = ['*']): \Illuminate\Support\Collection
|
||||
{
|
||||
return $this->model->with(['client', 'lines.product'])->get($columns);
|
||||
}
|
||||
|
||||
public function create(array $data): Avoir
|
||||
{
|
||||
return DB::transaction(function () use ($data) {
|
||||
try {
|
||||
// Create the avoir
|
||||
$avoir = parent::create($data);
|
||||
|
||||
// Create the avoir lines
|
||||
if (isset($data['lines']) && is_array($data['lines'])) {
|
||||
foreach ($data['lines'] as $lineData) {
|
||||
$lineData['avoir_id'] = $avoir->id;
|
||||
$this->avoirLineRepository->create($lineData);
|
||||
}
|
||||
}
|
||||
|
||||
// Record initial status history
|
||||
$this->recordHistory((int)$avoir->id, null, $avoir->status, 'Avoir created');
|
||||
|
||||
try {
|
||||
$this->timelineRepository->logActivity([
|
||||
'client_id' => $avoir->client_id,
|
||||
'actor_type' => 'user',
|
||||
'actor_user_id' => auth()->id(),
|
||||
'event_type' => 'avoir_created',
|
||||
'entity_type' => 'avoir',
|
||||
'entity_id' => $avoir->id,
|
||||
'title' => 'Nouvel avoir créé',
|
||||
'description' => "L'avoir #{$avoir->avoir_number} a été créé.",
|
||||
'created_at' => now(),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Failed to log avoir creation activity: " . $e->getMessage());
|
||||
}
|
||||
|
||||
return $avoir;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error creating avoir with lines: ' . $e->getMessage(), [
|
||||
'exception' => $e,
|
||||
'data' => $data,
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function update(int|string $id, array $attributes): bool
|
||||
{
|
||||
return DB::transaction(function () use ($id, $attributes) {
|
||||
try {
|
||||
$avoir = $this->find($id);
|
||||
if (!$avoir) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$oldStatus = $avoir->status;
|
||||
|
||||
// Update the avoir
|
||||
$updated = parent::update($id, $attributes);
|
||||
|
||||
if ($updated) {
|
||||
$newStatus = $attributes['status'] ?? $oldStatus;
|
||||
|
||||
// If status changed, record history
|
||||
if ($oldStatus !== $newStatus) {
|
||||
$this->recordHistory((int) $id, $oldStatus, $newStatus, 'Avoir status updated');
|
||||
}
|
||||
}
|
||||
|
||||
return $updated;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Error updating avoir: ' . $e->getMessage(), [
|
||||
'id' => $id,
|
||||
'attributes' => $attributes,
|
||||
'exception' => $e,
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function find(int|string $id, array $columns = ['*']): ?Avoir
|
||||
{
|
||||
return $this->model->with(['client', 'lines.product', 'history.user'])->find($id, $columns);
|
||||
}
|
||||
|
||||
private function recordHistory(int $avoirId, ?string $oldStatus, string $newStatus, ?string $comment = null): void
|
||||
{
|
||||
\App\Models\DocumentStatusHistory::create([
|
||||
'document_type' => 'avoir',
|
||||
'document_id' => $avoirId,
|
||||
'old_status' => $oldStatus,
|
||||
'new_status' => $newStatus,
|
||||
'changed_by' => auth()->id(),
|
||||
'comment' => $comment,
|
||||
'changed_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Repositories;
|
||||
|
||||
interface AvoirRepositoryInterface extends BaseRepositoryInterface
|
||||
{
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('avoirs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('client_id');
|
||||
$table->unsignedBigInteger('invoice_id')->nullable(); // The original invoice being credited
|
||||
$table->unsignedBigInteger('group_id')->nullable();
|
||||
|
||||
$table->string('avoir_number', 191);
|
||||
$table->enum('status', ['brouillon', 'emis', 'applique', 'annule'])->default('brouillon');
|
||||
|
||||
// Dates
|
||||
$table->date('avoir_date')->useCurrent();
|
||||
$table->date('due_date')->nullable();
|
||||
|
||||
// Currency and amounts (typically negative or representing credit)
|
||||
$table->char('currency', 3)->default('EUR');
|
||||
$table->decimal('total_ht', 14, 2)->default(0);
|
||||
$table->decimal('total_tva', 14, 2)->default(0);
|
||||
$table->decimal('total_ttc', 14, 2)->default(0);
|
||||
|
||||
// Reason for credit note
|
||||
$table->enum('reason_type', [
|
||||
'remboursement_total',
|
||||
'remboursement_partiel',
|
||||
'reduction',
|
||||
'erreur_facturation',
|
||||
'retour_marchandise',
|
||||
'accord_commercial',
|
||||
'autre'
|
||||
])->default('autre');
|
||||
$table->text('reason_description')->nullable();
|
||||
|
||||
// E-invoicing status (if applicable)
|
||||
$table->enum('e_invoice_status', [
|
||||
'cree', 'transmis', 'accepte', 'refuse', 'en_litige', 'acquitte', 'archive'
|
||||
])->nullable()->default('cree');
|
||||
|
||||
// Refund information (for accounting reconciliation)
|
||||
$table->enum('refund_status', [
|
||||
'non_rembourse', 'en_cours', 'partiellement_rembourse', 'rembourse', 'compense'
|
||||
])->default('non_rembourse');
|
||||
$table->date('refund_date')->nullable();
|
||||
$table->enum('refund_method', [
|
||||
'virement', 'cheque', 'carte_credit', 'compensation_future', 'autre'
|
||||
])->nullable();
|
||||
|
||||
// Compensation (if credit is applied to future invoices)
|
||||
$table->unsignedBigInteger('compensation_invoice_id')->nullable();
|
||||
$table->decimal('compensation_amount', 14, 2)->default(0);
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
// Indexes for performance
|
||||
$table->index(['status', 'due_date'], 'idx_avoirs_status_due');
|
||||
$table->index('invoice_id', 'idx_avoirs_invoice');
|
||||
$table->index('avoir_number', 'idx_avoirs_number');
|
||||
$table->index('refund_status', 'idx_avoirs_refund_status');
|
||||
$table->index('avoir_date', 'idx_avoirs_date');
|
||||
|
||||
// Foreign key constraints
|
||||
$table->foreign('client_id', 'fk_avoirs_client')->references('id')->on('clients')->onDelete('cascade');
|
||||
$table->foreign('invoice_id', 'fk_avoirs_invoice')->references('id')->on('invoices')->onDelete('restrict');
|
||||
$table->foreign('group_id', 'fk_avoirs_group')->references('id')->on('client_groups')->onDelete('set null');
|
||||
$table->foreign('compensation_invoice_id', 'fk_avoirs_compensation')->references('id')->on('invoices')->onDelete('set null');
|
||||
});
|
||||
|
||||
Schema::create('avoir_lines', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('avoir_id');
|
||||
$table->unsignedBigInteger('product_id')->nullable();
|
||||
$table->unsignedBigInteger('invoice_line_id')->nullable(); // Reference to the original invoice line if applicable
|
||||
|
||||
$table->string('description', 500);
|
||||
$table->decimal('quantity', 10, 3)->default(1);
|
||||
$table->decimal('unit_price', 14, 4);
|
||||
$table->decimal('tva_rate', 5, 2)->default(0);
|
||||
$table->decimal('total_ht', 14, 2);
|
||||
$table->decimal('total_tva', 14, 2)->default(0);
|
||||
$table->decimal('total_ttc', 14, 2)->default(0);
|
||||
$table->text('notes')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('avoir_id', 'idx_avoir_lines_avoir');
|
||||
|
||||
$table->foreign('avoir_id', 'fk_avoir_lines_avoir')->references('id')->on('avoirs')->onDelete('cascade');
|
||||
$table->foreign('product_id', 'fk_avoir_lines_product')->references('id')->on('products')->onDelete('set null');
|
||||
$table->foreign('invoice_line_id', 'fk_avoir_lines_invoice_line')->references('id')->on('invoice_lines')->onDelete('set null');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('avoir_lines');
|
||||
Schema::dropIfExists('avoirs');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// Purchase Orders (Commandes Fournisseurs)
|
||||
Schema::create('purchase_orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('fournisseur_id'); // Use existing fournisseurs table
|
||||
|
||||
$table->string('po_number', 191);
|
||||
$table->enum('status', ['brouillon', 'confirmee', 'livree', 'facturee', 'annulee'])->default('brouillon');
|
||||
$table->date('order_date')->useCurrent();
|
||||
$table->date('expected_date')->nullable();
|
||||
$table->char('currency', 3)->default('EUR');
|
||||
$table->decimal('total_ht', 14, 2)->default(0);
|
||||
$table->decimal('total_tva', 14, 2)->default(0);
|
||||
$table->decimal('total_ttc', 14, 2)->default(0);
|
||||
$table->text('notes')->nullable();
|
||||
$table->string('delivery_address')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique('po_number', 'uq_po_number');
|
||||
$table->index('status', 'idx_po_status');
|
||||
$table->index('order_date', 'idx_po_order_date');
|
||||
|
||||
$table->foreign('fournisseur_id', 'fk_po_fournisseur')->references('id')->on('fournisseurs')->onDelete('cascade');
|
||||
});
|
||||
|
||||
// Purchase Order Lines
|
||||
Schema::create('purchase_order_lines', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('purchase_order_id');
|
||||
$table->unsignedBigInteger('product_id')->nullable();
|
||||
|
||||
$table->text('description');
|
||||
$table->decimal('quantity', 14, 3)->default(1);
|
||||
$table->decimal('unit_price', 12, 2);
|
||||
$table->decimal('tva_rate', 5, 2)->default(0); // Use rate directly, no FK to tva_rates
|
||||
$table->decimal('discount_pct', 5, 2)->default(0);
|
||||
$table->decimal('total_ht', 14, 2);
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('purchase_order_id', 'fk_pol_po')->references('id')->on('purchase_orders')->onDelete('cascade');
|
||||
$table->foreign('product_id', 'fk_pol_product')->references('id')->on('products')->onDelete('set null');
|
||||
});
|
||||
|
||||
// Goods Receipts (Réceptions de marchandises)
|
||||
Schema::create('goods_receipts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('purchase_order_id');
|
||||
|
||||
$table->string('receipt_number', 191);
|
||||
$table->date('receipt_date')->useCurrent();
|
||||
$table->enum('status', ['brouillon', 'valide', 'annule'])->default('brouillon');
|
||||
$table->text('notes')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['purchase_order_id', 'receipt_number'], 'uq_gr_po_number');
|
||||
|
||||
$table->foreign('purchase_order_id', 'fk_gr_po')->references('id')->on('purchase_orders')->onDelete('cascade');
|
||||
});
|
||||
|
||||
// Goods Receipt Lines
|
||||
Schema::create('goods_receipt_lines', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('goods_receipt_id');
|
||||
$table->unsignedBigInteger('product_id');
|
||||
$table->unsignedBigInteger('purchase_order_line_id')->nullable(); // Link to original order line
|
||||
|
||||
$table->decimal('quantity_received', 14, 3);
|
||||
$table->decimal('unit_price', 12, 2)->nullable();
|
||||
$table->text('notes')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('goods_receipt_id', 'fk_grl_gr')->references('id')->on('goods_receipts')->onDelete('cascade');
|
||||
$table->foreign('product_id', 'fk_grl_product')->references('id')->on('products');
|
||||
$table->foreign('purchase_order_line_id', 'fk_grl_pol')->references('id')->on('purchase_order_lines')->onDelete('set null');
|
||||
});
|
||||
|
||||
// Supplier Invoices (Factures Fournisseurs)
|
||||
Schema::create('supplier_invoices', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('fournisseur_id');
|
||||
$table->unsignedBigInteger('purchase_order_id')->nullable(); // Optional link to purchase order
|
||||
|
||||
$table->string('invoice_number', 191);
|
||||
$table->date('invoice_date')->useCurrent();
|
||||
$table->date('due_date')->nullable();
|
||||
$table->enum('status', ['brouillon', 'en_attente', 'payee', 'annulee'])->default('brouillon');
|
||||
$table->char('currency', 3)->default('EUR');
|
||||
$table->decimal('total_ht', 14, 2)->default(0);
|
||||
$table->decimal('total_tva', 14, 2)->default(0);
|
||||
$table->decimal('total_ttc', 14, 2)->default(0);
|
||||
$table->text('notes')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['fournisseur_id', 'invoice_number'], 'uq_supplier_invoice');
|
||||
$table->index('status', 'idx_si_status');
|
||||
$table->index('invoice_date', 'idx_si_invoice_date');
|
||||
|
||||
$table->foreign('fournisseur_id', 'fk_si_fournisseur')->references('id')->on('fournisseurs');
|
||||
$table->foreign('purchase_order_id', 'fk_si_po')->references('id')->on('purchase_orders')->onDelete('set null');
|
||||
});
|
||||
|
||||
// Supplier Invoice Lines
|
||||
Schema::create('supplier_invoice_lines', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('supplier_invoice_id');
|
||||
$table->unsignedBigInteger('product_id')->nullable();
|
||||
$table->unsignedBigInteger('purchase_order_line_id')->nullable(); // Link to original order line
|
||||
|
||||
$table->text('description');
|
||||
$table->decimal('quantity', 14, 3)->default(1);
|
||||
$table->decimal('unit_price', 12, 2);
|
||||
$table->decimal('tva_rate', 5, 2)->default(0);
|
||||
$table->decimal('total_ht', 14, 2);
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('supplier_invoice_id', 'fk_sil_si')->references('id')->on('supplier_invoices')->onDelete('cascade');
|
||||
$table->foreign('product_id', 'fk_sil_product')->references('id')->on('products')->onDelete('set null');
|
||||
$table->foreign('purchase_order_line_id', 'fk_sil_pol')->references('id')->on('purchase_order_lines')->onDelete('set null');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('supplier_invoice_lines');
|
||||
Schema::dropIfExists('supplier_invoices');
|
||||
Schema::dropIfExists('goods_receipt_lines');
|
||||
Schema::dropIfExists('goods_receipts');
|
||||
Schema::dropIfExists('purchase_order_lines');
|
||||
Schema::dropIfExists('purchase_orders');
|
||||
}
|
||||
};
|
||||
@ -80,6 +80,9 @@ Route::middleware('auth:sanctum')->group(function () {
|
||||
Route::post('/invoices/from-quote/{quoteId}', [\App\Http\Controllers\Api\InvoiceController::class, 'createFromQuote']);
|
||||
Route::apiResource('invoices', \App\Http\Controllers\Api\InvoiceController::class);
|
||||
|
||||
// Avoir management
|
||||
Route::apiResource('avoirs', \App\Http\Controllers\Api\AvoirController::class);
|
||||
|
||||
// Fournisseur management
|
||||
Route::get('/fournisseurs/searchBy', [FournisseurController::class, 'searchBy']);
|
||||
Route::apiResource('fournisseurs', FournisseurController::class);
|
||||
|
||||
@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div v-if="isLoading" class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<facture-fournisseur-detail-template v-else-if="currentFacture">
|
||||
<template #header>
|
||||
<facture-fournisseur-header
|
||||
:facture-number="currentFacture.number"
|
||||
:date="currentFacture.date"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #lines>
|
||||
<facture-fournisseur-lines-table :lines="currentFacture.lines" />
|
||||
</template>
|
||||
|
||||
<template #billing>
|
||||
<div>
|
||||
<h6 class="mb-3 text-sm">Informations Fournisseur</h6>
|
||||
<p class="text-sm mb-1">
|
||||
<strong>Nom:</strong> {{ currentFacture.supplierName }}
|
||||
</p>
|
||||
<p class="text-sm mb-1">
|
||||
<strong>Statut:</strong> {{ getStatusLabel(currentFacture.status) }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #summary>
|
||||
<facture-fournisseur-summary
|
||||
:ht="currentFacture.totalHt"
|
||||
:tva="currentFacture.totalTva"
|
||||
:ttc="currentFacture.totalTtc"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<soft-button color="secondary" variant="outline" size="sm" @click="handleBack">
|
||||
Retour
|
||||
</soft-button>
|
||||
<soft-button color="info" size="sm">
|
||||
<i class="fas fa-print me-1"></i> Imprimer
|
||||
</soft-button>
|
||||
</div>
|
||||
</template>
|
||||
</facture-fournisseur-detail-template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, defineProps } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRouter } from "vue-router";
|
||||
import FactureFournisseurDetailTemplate from "@/components/templates/FactureFournisseur/FactureFournisseurDetailTemplate.vue";
|
||||
import FactureFournisseurHeader from "@/components/molecules/FactureFournisseur/FactureFournisseurHeader.vue";
|
||||
import FactureFournisseurLinesTable from "@/components/molecules/FactureFournisseur/FactureFournisseurLinesTable.vue";
|
||||
import FactureFournisseurSummary from "@/components/molecules/FactureFournisseur/FactureFournisseurSummary.vue";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
import { useFactureFournisseurStore } from "@/stores/factureFournisseurStore";
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const factureStore = useFactureFournisseurStore();
|
||||
const { currentFacture, isLoading } = storeToRefs(factureStore);
|
||||
|
||||
const handleBack = () => {
|
||||
router.push("/fournisseurs/factures");
|
||||
};
|
||||
|
||||
const getStatusLabel = (status) => {
|
||||
const labels = {
|
||||
payee: "Payée",
|
||||
en_attente: "En attente",
|
||||
annulee: "Annulée",
|
||||
brouillon: "Brouillon",
|
||||
};
|
||||
return labels[status] || status;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
factureStore.fetchFacture(Number(props.id));
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="container-fluid py-4">
|
||||
<facture-fournisseur-list-controls
|
||||
@create="handleCreate"
|
||||
@filter="handleFilter"
|
||||
/>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<facture-fournisseur-table
|
||||
:data="filteredFactures"
|
||||
:loading="isLoading"
|
||||
@view="handleView"
|
||||
@delete="handleDelete"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, computed } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRouter } from "vue-router";
|
||||
import FactureFournisseurListControls from "@/components/molecules/FactureFournisseur/FactureFournisseurListControls.vue";
|
||||
import FactureFournisseurTable from "@/components/molecules/Tables/Fournisseurs/FactureFournisseurTable.vue";
|
||||
import { useFactureFournisseurStore } from "@/stores/factureFournisseurStore";
|
||||
|
||||
const router = useRouter();
|
||||
const factureStore = useFactureFournisseurStore();
|
||||
const { factures, isLoading } = storeToRefs(factureStore);
|
||||
|
||||
const currentFilter = ref(null);
|
||||
|
||||
const filteredFactures = computed(() => {
|
||||
if (!currentFilter.value) return factures.value;
|
||||
return factures.value.filter(f => f.status === currentFilter.value);
|
||||
});
|
||||
|
||||
const handleCreate = () => {
|
||||
router.push("/fournisseurs/factures/new");
|
||||
};
|
||||
|
||||
const handleView = (id) => {
|
||||
router.push(`/fournisseurs/factures/${id}`);
|
||||
};
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
if (confirm("Êtes-vous sûr de vouloir supprimer cette facture ?")) {
|
||||
await factureStore.deleteFacture(id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFilter = (status) => {
|
||||
currentFilter.value = status;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
factureStore.fetchFactures();
|
||||
});
|
||||
</script>
|
||||
@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row">
|
||||
<div class="col-lg-9 col-12 mx-auto">
|
||||
<div class="card">
|
||||
<div class="card-header pb-0">
|
||||
<h5 class="mb-0">Nouvelle Facture Fournisseur</h5>
|
||||
<p class="text-sm mb-0">Saisissez les informations de la facture reçue.</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<new-facture-fournisseur-form
|
||||
@submit="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from "vue-router";
|
||||
import NewFactureFournisseurForm from "@/components/molecules/FactureFournisseur/NewFactureFournisseurForm.vue";
|
||||
import { useFactureFournisseurStore } from "@/stores/factureFournisseurStore";
|
||||
|
||||
const router = useRouter();
|
||||
const factureStore = useFactureFournisseurStore();
|
||||
|
||||
const handleSubmit = async (payload) => {
|
||||
try {
|
||||
await factureStore.createFacture(payload);
|
||||
router.push("/fournisseurs/factures");
|
||||
} catch (error) {
|
||||
alert("Erreur lors de la création de la facture");
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
router.push("/fournisseurs/factures");
|
||||
};
|
||||
</script>
|
||||
@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="row justify-content-between">
|
||||
<div class="col-md-4 text-left">
|
||||
<h6 class="mb-0 text-dark font-weight-bold">Facture Fournisseur</h6>
|
||||
<p class="text-secondary text-sm">{{ factureNumber }}</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<h6 class="mb-0 text-dark font-weight-bold">Date d'émission</h6>
|
||||
<p class="text-secondary text-sm">{{ formatDate(date) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps } from "vue";
|
||||
|
||||
defineProps({
|
||||
factureNumber: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
date: {
|
||||
type: [String, Date],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return "-";
|
||||
return new Date(dateString).toLocaleDateString("fr-FR", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="table-responsive">
|
||||
<table class="table align-items-center mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2">
|
||||
Description
|
||||
</th>
|
||||
<th class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">
|
||||
Qté
|
||||
</th>
|
||||
<th class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">
|
||||
Prix Unit. HT
|
||||
</th>
|
||||
<th class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">
|
||||
Total HT
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="line in lines" :key="line.id">
|
||||
<td>
|
||||
<div class="d-flex px-2 py-1">
|
||||
<div class="d-flex flex-column justify-content-center">
|
||||
<h6 class="mb-0 text-sm">{{ line.designation }}</h6>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle text-center text-sm">
|
||||
<span class="text-secondary text-xs font-weight-bold">{{ line.quantity }}</span>
|
||||
</td>
|
||||
<td class="align-middle text-center text-sm">
|
||||
<span class="text-secondary text-xs font-weight-bold">{{ formatCurrency(line.priceHt) }}</span>
|
||||
</td>
|
||||
<td class="align-middle text-center text-sm">
|
||||
<span class="text-secondary text-xs font-weight-bold">{{ formatCurrency(line.totalHt) }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps } from "vue";
|
||||
|
||||
defineProps({
|
||||
lines: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const formatCurrency = (value) => {
|
||||
return new Intl.NumberFormat("fr-FR", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(value);
|
||||
};
|
||||
</script>
|
||||
@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="d-sm-flex justify-content-between">
|
||||
<div>
|
||||
<soft-button color="success" variant="gradient" size="sm" @click="$emit('create')">
|
||||
<i class="fas fa-plus me-1"></i> Ajouter une facture
|
||||
</soft-button>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<div class="dropdown d-inline">
|
||||
<soft-button
|
||||
id="factureFilterDropdown"
|
||||
color="dark"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="dropdown-toggle"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
Filtrer
|
||||
</soft-button>
|
||||
<ul
|
||||
class="dropdown-menu dropdown-menu-lg-start px-2 py-3"
|
||||
aria-labelledby="factureFilterDropdown"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item border-radius-md"
|
||||
href="javascript:;"
|
||||
@click="$emit('filter', 'payee')"
|
||||
>
|
||||
Statut: Payée
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item border-radius-md"
|
||||
href="javascript:;"
|
||||
@click="$emit('filter', 'en_attente')"
|
||||
>
|
||||
Statut: En attente
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item border-radius-md"
|
||||
href="javascript:;"
|
||||
@click="$emit('filter', 'brouillon')"
|
||||
>
|
||||
Statut: Brouillon
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<hr class="horizontal dark my-2" />
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item border-radius-md text-danger"
|
||||
href="javascript:;"
|
||||
@click="$emit('filter', null)"
|
||||
>
|
||||
Retirer Filtres
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<soft-button
|
||||
class="btn-icon ms-2 export"
|
||||
color="dark"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="$emit('export')"
|
||||
>
|
||||
<span class="btn-inner--icon">
|
||||
<i class="ni ni-archive-2"></i>
|
||||
</span>
|
||||
<span class="btn-inner--text">Export CSV</span>
|
||||
</soft-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineEmits } from "vue";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
|
||||
defineEmits(["create", "filter", "export"]);
|
||||
</script>
|
||||
@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-6 ms-auto">
|
||||
<div class="table-responsive">
|
||||
<table class="table align-items-center mb-0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="text-sm">Total HT:</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span class="text-sm text-dark font-weight-bold">{{ formatCurrency(ht) }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="text-sm">TVA (20%):</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span class="text-sm text-dark font-weight-bold">{{ formatCurrency(tva) }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="text-sm font-weight-bold text-info">Total TTC:</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span class="text-sm font-weight-bold text-info">{{ formatCurrency(ttc) }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps } from "vue";
|
||||
|
||||
defineProps({
|
||||
ht: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
tva: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
ttc: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const formatCurrency = (value) => {
|
||||
return new Intl.NumberFormat("fr-FR", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(value);
|
||||
};
|
||||
</script>
|
||||
@ -0,0 +1,305 @@
|
||||
<template>
|
||||
<form @submit.prevent="submitForm">
|
||||
<!-- Row 1: N° Facture, Fournisseur, Date -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">N° Facture Fournisseur *</label>
|
||||
<soft-input
|
||||
v-model="formData.number"
|
||||
type="text"
|
||||
placeholder="Ex: FR-2026-001"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Fournisseur *</label>
|
||||
<select v-model="formData.supplierId" class="form-select" @change="updateSupplierInfo" required>
|
||||
<option value="">-- Sélectionner un fournisseur --</option>
|
||||
<option v-for="supplier in suppliers" :key="supplier.id" :value="supplier.id">
|
||||
{{ supplier.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Date facture *</label>
|
||||
<soft-input
|
||||
v-model="formData.date"
|
||||
type="date"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Row 2: Statut, Notes -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Statut</label>
|
||||
<select v-model="formData.status" class="form-select">
|
||||
<option value="brouillon">Brouillon</option>
|
||||
<option value="en_attente">En attente de paiement</option>
|
||||
<option value="payee">Payée</option>
|
||||
<option value="annulee">Annulée</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Notes</label>
|
||||
<textarea
|
||||
v-model="formData.notes"
|
||||
class="form-control"
|
||||
placeholder="Remarques éventuelles..."
|
||||
rows="2"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Articles Section -->
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<label class="peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-lg font-bold">Lignes de facture</label>
|
||||
<button
|
||||
class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-8 rounded-md px-3 text-xs"
|
||||
type="button"
|
||||
@click="addLine"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plus w-4 h-4 mr-2">
|
||||
<path d="M5 12h14"></path>
|
||||
<path d="M12 5v14"></path>
|
||||
</svg>
|
||||
Ajouter ligne
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 mt-3">
|
||||
<div
|
||||
v-for="(line, index) in formData.lines"
|
||||
:key="index"
|
||||
class="row g-2 align-items-end bg-light p-3 rounded mx-0"
|
||||
>
|
||||
<div class="col-md-5">
|
||||
<label class="form-label text-xs mb-1">Désignation *</label>
|
||||
<soft-input
|
||||
v-model="line.designation"
|
||||
type="text"
|
||||
placeholder="Nom de l'article ou prestation"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label text-xs mb-1">Quantité *</label>
|
||||
<soft-input
|
||||
v-model.number="line.quantity"
|
||||
type="number"
|
||||
placeholder="Qté"
|
||||
:min="1"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label text-xs mb-1">Prix HT *</label>
|
||||
<soft-input
|
||||
v-model.number="line.priceHt"
|
||||
type="number"
|
||||
placeholder="0.00"
|
||||
step="0.01"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-2 text-end">
|
||||
<label class="form-label text-xs mb-1 d-block">Sous-total HT</label>
|
||||
<span class="text-sm font-weight-bold">
|
||||
{{ formatCurrency(line.quantity * line.priceHt) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-md-1 text-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-link text-danger mb-0"
|
||||
@click="removeLine(index)"
|
||||
:disabled="formData.lines.length === 1"
|
||||
>
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Totaux -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-5 ms-auto">
|
||||
<div class="card bg-gray-100 shadow-none border">
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="text-sm">Total HT:</span>
|
||||
<span class="text-sm font-weight-bold">{{ formatCurrency(calculateTotalHt()) }}</span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="text-sm">TVA (20%):</span>
|
||||
<span class="text-sm font-weight-bold">{{ formatCurrency(calculateTotalTva()) }}</span>
|
||||
</div>
|
||||
<hr class="horizontal dark my-2">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="text-base font-weight-bold">Total TTC:</span>
|
||||
<span class="text-base font-weight-bold text-info">{{ formatCurrency(calculateTotalTtc()) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Boutons d'action -->
|
||||
<div class="d-flex justify-content-end gap-3 mt-4">
|
||||
<soft-button
|
||||
type="button"
|
||||
color="secondary"
|
||||
variant="outline"
|
||||
@click="$emit('cancel')"
|
||||
>
|
||||
Annuler
|
||||
</soft-button>
|
||||
<soft-button
|
||||
type="submit"
|
||||
color="success"
|
||||
>
|
||||
Enregistrer la facture
|
||||
</soft-button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineEmits } from "vue";
|
||||
import SoftInput from "@/components/SoftInput.vue";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
|
||||
const emit = defineEmits(["submit", "cancel"]);
|
||||
|
||||
const suppliers = [
|
||||
{ id: "1", name: "Produits Funéraires Pro" },
|
||||
{ id: "2", name: "Thanatos Supply" },
|
||||
{ id: "3", name: "ISOFROID" },
|
||||
{ id: "4", name: "EEP Co EUROPE" },
|
||||
{ id: "5", name: "NEXTECH MEDICAL" },
|
||||
{ id: "6", name: "ACTION" },
|
||||
{ id: "7", name: "E.LECLERC" },
|
||||
];
|
||||
|
||||
const formData = ref({
|
||||
number: "",
|
||||
supplierId: "",
|
||||
supplierName: "",
|
||||
date: new Date().toISOString().split("T")[0],
|
||||
status: "brouillon",
|
||||
notes: "",
|
||||
lines: [
|
||||
{
|
||||
designation: "",
|
||||
quantity: 1,
|
||||
priceHt: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const updateSupplierInfo = () => {
|
||||
const supplier = suppliers.find(s => s.id === formData.value.supplierId);
|
||||
if (supplier) {
|
||||
formData.value.supplierName = supplier.name;
|
||||
}
|
||||
};
|
||||
|
||||
const formatCurrency = (value) => {
|
||||
return new Intl.NumberFormat("fr-FR", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
const calculateTotalHt = () => {
|
||||
return formData.value.lines.reduce((sum, line) => {
|
||||
return sum + line.quantity * (line.priceHt || 0);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const calculateTotalTva = () => {
|
||||
return calculateTotalHt() * 0.2;
|
||||
};
|
||||
|
||||
const calculateTotalTtc = () => {
|
||||
return calculateTotalHt() + calculateTotalTva();
|
||||
};
|
||||
|
||||
const addLine = () => {
|
||||
formData.value.lines.push({
|
||||
designation: "",
|
||||
quantity: 1,
|
||||
priceHt: 0,
|
||||
});
|
||||
};
|
||||
|
||||
const removeLine = (index) => {
|
||||
if (formData.value.lines.length > 1) {
|
||||
formData.value.lines.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
const submitForm = () => {
|
||||
if (!formData.value.supplierId) {
|
||||
alert("Veuillez sélectionner un fournisseur");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
...formData.value,
|
||||
totalHt: calculateTotalHt(),
|
||||
totalTva: calculateTotalTva(),
|
||||
totalTtc: calculateTotalTtc(),
|
||||
// Map lines to include totalHt for store consistency
|
||||
lines: formData.value.lines.map(line => ({
|
||||
...line,
|
||||
totalHt: line.quantity * line.priceHt
|
||||
}))
|
||||
};
|
||||
|
||||
emit("submit", payload);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Scoped styles for the custom button from user request */
|
||||
.bg-primary {
|
||||
background-color: var(--bs-primary, #cb0c9f);
|
||||
}
|
||||
.text-primary-foreground {
|
||||
color: #fff;
|
||||
}
|
||||
.bg-primary\:hover {
|
||||
background-color: #b30b8c;
|
||||
}
|
||||
|
||||
/* Tailwind-like utilities used in the provided snippet */
|
||||
.flex { display: flex; }
|
||||
.items-center { align-items: center; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.mb-4 { margin-bottom: 1.5rem; }
|
||||
.text-lg { font-size: 1.125rem; }
|
||||
.font-bold { font-weight: 700; }
|
||||
.gap-2 { gap: 0.5rem; }
|
||||
.whitespace-nowrap { white-space: nowrap; }
|
||||
.transition-colors { transition-property: background-color, border-color, color, fill, stroke; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
|
||||
.shadow { box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); }
|
||||
.rounded-md { border-radius: 0.375rem; }
|
||||
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
|
||||
.h-8 { height: 2rem; }
|
||||
.text-xs { font-size: 0.75rem; }
|
||||
.font-medium { font-weight: 500; }
|
||||
|
||||
.space-y-3 > div + div {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,218 @@
|
||||
<template>
|
||||
<div class="card mt-4">
|
||||
<div class="table-responsive">
|
||||
<table id="facture-fournisseur-list" class="table table-flush">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>N° Facture</th>
|
||||
<th>Fournisseur</th>
|
||||
<th>Date</th>
|
||||
<th>Montant TTC</th>
|
||||
<th>Statut</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="facture in data" :key="facture.id">
|
||||
<!-- Invoice Number -->
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<soft-checkbox class="me-2" />
|
||||
<p class="text-xs font-weight-bold ms-2 mb-0">
|
||||
{{ facture.number }}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Supplier -->
|
||||
<td class="text-xs font-weight-bold">
|
||||
<span>{{ facture.supplierName }}</span>
|
||||
</td>
|
||||
|
||||
<!-- Date -->
|
||||
<td class="font-weight-bold">
|
||||
<span class="my-2 text-xs">{{ formatDate(facture.date) }}</span>
|
||||
</td>
|
||||
|
||||
<!-- Total TTC -->
|
||||
<td class="text-xs font-weight-bold">
|
||||
<span class="my-2 text-xs">{{ formatCurrency(facture.totalTtc) }}</span>
|
||||
</td>
|
||||
|
||||
<!-- Status -->
|
||||
<td class="text-xs font-weight-bold">
|
||||
<div class="d-flex align-items-center">
|
||||
<soft-button
|
||||
:color="getStatusColor(facture.status)"
|
||||
variant="outline"
|
||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<i :class="getStatusIcon(facture.status)" aria-hidden="true"></i>
|
||||
</soft-button>
|
||||
<span>{{ getStatusLabel(facture.status) }}</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Actions -->
|
||||
<td class="text-xs font-weight-bold">
|
||||
<div class="d-flex align-items-center">
|
||||
<button
|
||||
class="btn btn-link text-secondary mb-0 px-2"
|
||||
:data-id="facture.id"
|
||||
data-action="view"
|
||||
title="Voir la facture"
|
||||
>
|
||||
<i class="fas fa-eye text-xs" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-link text-danger mb-0 px-2"
|
||||
:data-id="facture.id"
|
||||
data-action="delete"
|
||||
title="Supprimer la facture"
|
||||
>
|
||||
<i class="fas fa-trash text-xs" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
onMounted,
|
||||
watch,
|
||||
onUnmounted,
|
||||
defineProps,
|
||||
defineEmits,
|
||||
} from "vue";
|
||||
import { DataTable } from "simple-datatables";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
import SoftCheckbox from "@/components/SoftCheckbox.vue";
|
||||
|
||||
const emit = defineEmits(["view", "delete"]);
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const dataTableInstance = ref(null);
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return null;
|
||||
const options = {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
};
|
||||
return new Date(dateString).toLocaleDateString("fr-FR", options);
|
||||
};
|
||||
|
||||
const formatCurrency = (value) => {
|
||||
return new Intl.NumberFormat("fr-FR", {
|
||||
style: "currency",
|
||||
currency: "EUR",
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
const getStatusColor = (status) => {
|
||||
const map = {
|
||||
payee: "success",
|
||||
en_attente: "warning",
|
||||
annulee: "danger",
|
||||
brouillon: "secondary",
|
||||
};
|
||||
return map[status] || "secondary";
|
||||
};
|
||||
|
||||
const getStatusIcon = (status) => {
|
||||
const map = {
|
||||
payee: "fas fa-check",
|
||||
en_attente: "fas fa-hourglass-half",
|
||||
annulee: "fas fa-ban",
|
||||
brouillon: "fas fa-pen",
|
||||
};
|
||||
return map[status] || "fas fa-info";
|
||||
};
|
||||
|
||||
const getStatusLabel = (status) => {
|
||||
const labels = {
|
||||
payee: "Payée",
|
||||
en_attente: "En attente",
|
||||
annulee: "Annulée",
|
||||
brouillon: "Brouillon",
|
||||
};
|
||||
return labels[status] || status;
|
||||
};
|
||||
|
||||
const initializeDataTable = () => {
|
||||
if (dataTableInstance.value) {
|
||||
dataTableInstance.value.destroy();
|
||||
dataTableInstance.value = null;
|
||||
}
|
||||
|
||||
const dataTableEl = document.getElementById("facture-fournisseur-list");
|
||||
if (dataTableEl) {
|
||||
dataTableInstance.value = new DataTable(dataTableEl, {
|
||||
searchable: true,
|
||||
fixedHeight: false,
|
||||
perPageSelect: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.loading && props.data.length > 0) {
|
||||
initializeDataTable();
|
||||
}
|
||||
|
||||
// Event delegation
|
||||
const table = document.getElementById("facture-fournisseur-list");
|
||||
if (table) {
|
||||
table.addEventListener("click", (event) => {
|
||||
const btn = event.target.closest("button");
|
||||
if (!btn) return;
|
||||
|
||||
const id = btn.getAttribute("data-id");
|
||||
const action = btn.getAttribute("data-action");
|
||||
|
||||
if (id && action) {
|
||||
if (action === "view") {
|
||||
emit("view", parseInt(id));
|
||||
} else if (action === "delete") {
|
||||
emit("delete", parseInt(id));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
() => {
|
||||
if (!props.loading) {
|
||||
setTimeout(() => {
|
||||
initializeDataTable();
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
if (dataTableInstance.value) {
|
||||
dataTableInstance.value.destroy();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -45,13 +45,13 @@
|
||||
:is-active="activeTab === 'notes'"
|
||||
@click="$emit('change-tab', 'notes')"
|
||||
/>
|
||||
<TabNavigationItem
|
||||
<!-- <TabNavigationItem
|
||||
icon="fas fa-sitemap"
|
||||
label="Sous-comptes"
|
||||
:is-active="activeTab === 'children'"
|
||||
:badge="childrenCount > 0 ? childrenCount : null"
|
||||
@click="$emit('change-tab', 'children')"
|
||||
/>
|
||||
/> -->
|
||||
<TabNavigationItem
|
||||
icon="fas fa-file-invoice-dollar"
|
||||
label="Avoirs"
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col-lg-8 col-md-10 mx-auto">
|
||||
<div class="card my-4">
|
||||
<div class="card-header p-3 pb-0">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<hr class="horizontal dark my-3">
|
||||
<div class="card-body p-3 pt-0">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<slot name="lines"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-6">
|
||||
<slot name="billing"></slot>
|
||||
</div>
|
||||
<div class="col-md-6 text-end">
|
||||
<slot name="summary"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="horizontal dark mt-4 mb-3">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<div class="container-fluid py-4">
|
||||
<div class="d-sm-flex justify-content-between">
|
||||
<div>
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="mt-4">
|
||||
<slot name="table"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
@ -7,7 +7,6 @@
|
||||
class="nav-link"
|
||||
v-bind="$attrs"
|
||||
type="button"
|
||||
@click="isExpanded = !isExpanded"
|
||||
>
|
||||
<div
|
||||
class="text-center bg-white shadow icon icon-shape icon-sm border-radius-md d-flex align-items-center justify-content-center"
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div
|
||||
id="sidenav-collapse-main"
|
||||
class="w-auto h-auto collapse navbar-collapse max-height-vh-100 h-100"
|
||||
class="w-auto h-auto collapse navbar-collapse h-100"
|
||||
style="overflow: visible"
|
||||
>
|
||||
<ul class="navbar-nav">
|
||||
<!-- Render navigation items from data array -->
|
||||
@ -214,7 +215,7 @@ export default {
|
||||
id: "fournisseurs-invoices",
|
||||
route: { name: "Factures fournisseurs" },
|
||||
miniIcon: "F",
|
||||
text: "Factures",
|
||||
text: "Facture fournisseur",
|
||||
},
|
||||
{
|
||||
id: "fournisseurs-stats",
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<aside
|
||||
id="sidenav-main"
|
||||
class="my-3 overflow-auto border-0 sidenav navbar navbar-vertical navbar-expand-xs border-radius-xl"
|
||||
class="my-3 overflow-y-auto border-0 sidenav navbar navbar-vertical navbar-expand-xs border-radius-xl"
|
||||
:class="isRTL ? 'me-3 rotate-caret' : 'ms-3'"
|
||||
>
|
||||
<div class="sidenav-header">
|
||||
|
||||
@ -471,7 +471,17 @@ const routes = [
|
||||
{
|
||||
path: "/fournisseurs/factures",
|
||||
name: "Factures fournisseurs",
|
||||
component: () => import("@/views/pages/Fournisseurs/Factures.vue"),
|
||||
component: () => import("@/views/pages/Fournisseurs/FactureFournisseurList.vue"),
|
||||
},
|
||||
{
|
||||
path: "/fournisseurs/factures/new",
|
||||
name: "Nouvelle Facture Fournisseur",
|
||||
component: () => import("@/views/pages/Fournisseurs/NewFactureFournisseur.vue"),
|
||||
},
|
||||
{
|
||||
path: "/fournisseurs/factures/:id",
|
||||
name: "Facture Fournisseur Details",
|
||||
component: () => import("@/views/pages/Fournisseurs/FactureFournisseurDetail.vue"),
|
||||
},
|
||||
{
|
||||
path: "/fournisseurs/statistiques",
|
||||
|
||||
109
thanasoft-front/src/stores/factureFournisseurStore.ts
Normal file
109
thanasoft-front/src/stores/factureFournisseurStore.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
export interface FactureFournisseurLine {
|
||||
id?: number;
|
||||
designation: string;
|
||||
quantity: number;
|
||||
priceHt: number;
|
||||
totalHt: number;
|
||||
}
|
||||
|
||||
export interface FactureFournisseur {
|
||||
id: number;
|
||||
number: string;
|
||||
supplierId: string;
|
||||
supplierName: string;
|
||||
date: string;
|
||||
status: string;
|
||||
totalHt: number;
|
||||
totalTva: number;
|
||||
totalTtc: number;
|
||||
lines: FactureFournisseurLine[];
|
||||
}
|
||||
|
||||
export const useFactureFournisseurStore = defineStore("factureFournisseur", () => {
|
||||
// State
|
||||
const factures = ref<FactureFournisseur[]>([
|
||||
{
|
||||
id: 1,
|
||||
number: "FF-2026-0001",
|
||||
supplierId: "1",
|
||||
supplierName: "Produits Funéraires Pro",
|
||||
date: "2026-01-15",
|
||||
status: "payee",
|
||||
totalHt: 1250.0,
|
||||
totalTva: 250.0,
|
||||
totalTtc: 1500.0,
|
||||
lines: [
|
||||
{ id: 1, designation: "Cercueils Chêne Prestige", quantity: 2, priceHt: 625.0, totalHt: 1250.0 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
number: "FF-2026-0002",
|
||||
supplierId: "2",
|
||||
supplierName: "Thanatos Supply",
|
||||
date: "2026-01-20",
|
||||
status: "en_attente",
|
||||
totalHt: 450.0,
|
||||
totalTva: 90.0,
|
||||
totalTtc: 540.0,
|
||||
lines: [
|
||||
{ id: 2, designation: "Urnes Granit Noir", quantity: 5, priceHt: 90.0, totalHt: 450.0 }
|
||||
]
|
||||
}
|
||||
]);
|
||||
const currentFacture = ref<FactureFournisseur | null>(null);
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
// Getters
|
||||
const allFactures = computed(() => factures.value);
|
||||
const isLoading = computed(() => loading.value);
|
||||
|
||||
// Actions
|
||||
const fetchFactures = async () => {
|
||||
loading.value = true;
|
||||
// Mock API call
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const fetchFacture = async (id: number) => {
|
||||
loading.value = true;
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
const found = factures.value.find(f => f.id === id);
|
||||
currentFacture.value = found || null;
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const createFacture = async (payload: any) => {
|
||||
loading.value = true;
|
||||
await new Promise(resolve => setTimeout(resolve, 800));
|
||||
const newFacture = {
|
||||
...payload,
|
||||
id: factures.value.length + 1,
|
||||
};
|
||||
factures.value.push(newFacture);
|
||||
loading.value = false;
|
||||
return newFacture;
|
||||
};
|
||||
|
||||
const deleteFacture = async (id: number) => {
|
||||
factures.value = factures.value.filter(f => f.id !== id);
|
||||
};
|
||||
|
||||
return {
|
||||
factures,
|
||||
currentFacture,
|
||||
loading,
|
||||
error,
|
||||
allFactures,
|
||||
isLoading,
|
||||
fetchFactures,
|
||||
fetchFacture,
|
||||
createFacture,
|
||||
deleteFacture
|
||||
};
|
||||
});
|
||||
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<facture-fournisseur-detail-presentation :id="$route.params.id" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import FactureFournisseurDetailPresentation from "@/components/Organism/FactureFournisseur/FactureFournisseurDetailPresentation.vue";
|
||||
</script>
|
||||
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<facture-fournisseur-list-presentation />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import FactureFournisseurListPresentation from "@/components/Organism/FactureFournisseur/FactureFournisseurListPresentation.vue";
|
||||
</script>
|
||||
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<new-facture-fournisseur-presentation />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import NewFactureFournisseurPresentation from "@/components/Organism/FactureFournisseur/NewFactureFournisseurPresentation.vue";
|
||||
</script>
|
||||
Loading…
x
Reference in New Issue
Block a user