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\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) {
|
$this->app->bind(\App\Repositories\QuoteRepositoryInterface::class, function ($app) {
|
||||||
return new \App\Repositories\QuoteRepository(
|
return new \App\Repositories\QuoteRepository(
|
||||||
$app->make(\App\Models\Quote::class),
|
$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::post('/invoices/from-quote/{quoteId}', [\App\Http\Controllers\Api\InvoiceController::class, 'createFromQuote']);
|
||||||
Route::apiResource('invoices', \App\Http\Controllers\Api\InvoiceController::class);
|
Route::apiResource('invoices', \App\Http\Controllers\Api\InvoiceController::class);
|
||||||
|
|
||||||
|
// Avoir management
|
||||||
|
Route::apiResource('avoirs', \App\Http\Controllers\Api\AvoirController::class);
|
||||||
|
|
||||||
// Fournisseur management
|
// Fournisseur management
|
||||||
Route::get('/fournisseurs/searchBy', [FournisseurController::class, 'searchBy']);
|
Route::get('/fournisseurs/searchBy', [FournisseurController::class, 'searchBy']);
|
||||||
Route::apiResource('fournisseurs', FournisseurController::class);
|
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'"
|
:is-active="activeTab === 'notes'"
|
||||||
@click="$emit('change-tab', 'notes')"
|
@click="$emit('change-tab', 'notes')"
|
||||||
/>
|
/>
|
||||||
<TabNavigationItem
|
<!-- <TabNavigationItem
|
||||||
icon="fas fa-sitemap"
|
icon="fas fa-sitemap"
|
||||||
label="Sous-comptes"
|
label="Sous-comptes"
|
||||||
:is-active="activeTab === 'children'"
|
:is-active="activeTab === 'children'"
|
||||||
:badge="childrenCount > 0 ? childrenCount : null"
|
:badge="childrenCount > 0 ? childrenCount : null"
|
||||||
@click="$emit('change-tab', 'children')"
|
@click="$emit('change-tab', 'children')"
|
||||||
/>
|
/> -->
|
||||||
<TabNavigationItem
|
<TabNavigationItem
|
||||||
icon="fas fa-file-invoice-dollar"
|
icon="fas fa-file-invoice-dollar"
|
||||||
label="Avoirs"
|
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"
|
class="nav-link"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
type="button"
|
type="button"
|
||||||
@click="isExpanded = !isExpanded"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="text-center bg-white shadow icon icon-shape icon-sm border-radius-md d-flex align-items-center justify-content-center"
|
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>
|
<template>
|
||||||
<div
|
<div
|
||||||
id="sidenav-collapse-main"
|
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">
|
<ul class="navbar-nav">
|
||||||
<!-- Render navigation items from data array -->
|
<!-- Render navigation items from data array -->
|
||||||
@ -214,7 +215,7 @@ export default {
|
|||||||
id: "fournisseurs-invoices",
|
id: "fournisseurs-invoices",
|
||||||
route: { name: "Factures fournisseurs" },
|
route: { name: "Factures fournisseurs" },
|
||||||
miniIcon: "F",
|
miniIcon: "F",
|
||||||
text: "Factures",
|
text: "Facture fournisseur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "fournisseurs-stats",
|
id: "fournisseurs-stats",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<aside
|
<aside
|
||||||
id="sidenav-main"
|
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'"
|
:class="isRTL ? 'me-3 rotate-caret' : 'ms-3'"
|
||||||
>
|
>
|
||||||
<div class="sidenav-header">
|
<div class="sidenav-header">
|
||||||
|
|||||||
@ -471,7 +471,17 @@ const routes = [
|
|||||||
{
|
{
|
||||||
path: "/fournisseurs/factures",
|
path: "/fournisseurs/factures",
|
||||||
name: "Factures fournisseurs",
|
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",
|
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