Gestion des bon de receptions dans front
This commit is contained in:
parent
d8927580e7
commit
31090d12ba
@ -0,0 +1,133 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\StoreGoodsReceiptRequest;
|
||||||
|
use App\Http\Requests\UpdateGoodsReceiptRequest;
|
||||||
|
use App\Http\Resources\GoodsReceiptResource;
|
||||||
|
use App\Repositories\GoodsReceiptRepositoryInterface;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class GoodsReceiptController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly GoodsReceiptRepositoryInterface $goodsReceiptRepository
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a listing of goods receipts.
|
||||||
|
*/
|
||||||
|
public function index(): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$goodsReceipts = $this->goodsReceiptRepository->all();
|
||||||
|
return response()->json([
|
||||||
|
'data' => GoodsReceiptResource::collection($goodsReceipts),
|
||||||
|
'status' => 'success'
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error fetching goods receipts: ' . $e->getMessage());
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la récupération des réceptions de marchandises.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created goods receipt.
|
||||||
|
*/
|
||||||
|
public function store(StoreGoodsReceiptRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$goodsReceipt = $this->goodsReceiptRepository->create($request->validated());
|
||||||
|
return response()->json([
|
||||||
|
'data' => new GoodsReceiptResource($goodsReceipt),
|
||||||
|
'message' => 'Réception de marchandise créée avec succès.',
|
||||||
|
'status' => 'success'
|
||||||
|
], 201);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error creating goods receipt: ' . $e->getMessage());
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la création de la réception de marchandise.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified goods receipt.
|
||||||
|
*/
|
||||||
|
public function show(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$goodsReceipt = $this->goodsReceiptRepository->find((int) $id);
|
||||||
|
if (!$goodsReceipt) {
|
||||||
|
return response()->json(['message' => 'Réception de marchandise non trouvée.'], 404);
|
||||||
|
}
|
||||||
|
return response()->json([
|
||||||
|
'data' => new GoodsReceiptResource($goodsReceipt),
|
||||||
|
'status' => 'success'
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error fetching goods receipt: ' . $e->getMessage());
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la récupération de la réception de marchandise.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified goods receipt.
|
||||||
|
*/
|
||||||
|
public function update(UpdateGoodsReceiptRequest $request, string $id): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$updated = $this->goodsReceiptRepository->update((int) $id, $request->validated());
|
||||||
|
if (!$updated) {
|
||||||
|
return response()->json(['message' => 'Réception de marchandise non trouvée ou échec de la mise à jour.'], 404);
|
||||||
|
}
|
||||||
|
$goodsReceipt = $this->goodsReceiptRepository->find((int) $id);
|
||||||
|
return response()->json([
|
||||||
|
'data' => new GoodsReceiptResource($goodsReceipt),
|
||||||
|
'message' => 'Réception de marchandise mise à jour avec succès.',
|
||||||
|
'status' => 'success'
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error updating goods receipt: ' . $e->getMessage());
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la mise à jour de la réception de marchandise.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified goods receipt.
|
||||||
|
*/
|
||||||
|
public function destroy(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$deleted = $this->goodsReceiptRepository->delete((int) $id);
|
||||||
|
if (!$deleted) {
|
||||||
|
return response()->json(['message' => 'Réception de marchandise non trouvée ou échec de la suppression.'], 404);
|
||||||
|
}
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Réception de marchandise supprimée avec succès.',
|
||||||
|
'status' => 'success'
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error deleting goods receipt: ' . $e->getMessage());
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la suppression de la réception de marchandise.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
133
thanasoft-back/app/Http/Controllers/Api/TvaRateController.php
Normal file
133
thanasoft-back/app/Http/Controllers/Api/TvaRateController.php
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\StoreTvaRateRequest;
|
||||||
|
use App\Http\Requests\UpdateTvaRateRequest;
|
||||||
|
use App\Http\Resources\TvaRateResource;
|
||||||
|
use App\Repositories\TvaRateRepositoryInterface;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class TvaRateController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly TvaRateRepositoryInterface $tvaRateRepository
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a listing of TVA rates.
|
||||||
|
*/
|
||||||
|
public function index(): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$tvaRates = $this->tvaRateRepository->all();
|
||||||
|
return response()->json([
|
||||||
|
'data' => TvaRateResource::collection($tvaRates),
|
||||||
|
'status' => 'success'
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error fetching TVA rates: ' . $e->getMessage());
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la récupération des taux de TVA.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created TVA rate.
|
||||||
|
*/
|
||||||
|
public function store(StoreTvaRateRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$tvaRate = $this->tvaRateRepository->create($request->validated());
|
||||||
|
return response()->json([
|
||||||
|
'data' => new TvaRateResource($tvaRate),
|
||||||
|
'message' => 'Taux de TVA créé avec succès.',
|
||||||
|
'status' => 'success'
|
||||||
|
], 201);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error creating TVA rate: ' . $e->getMessage());
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la création du taux de TVA.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified TVA rate.
|
||||||
|
*/
|
||||||
|
public function show(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$tvaRate = $this->tvaRateRepository->find((int) $id);
|
||||||
|
if (!$tvaRate) {
|
||||||
|
return response()->json(['message' => 'Taux de TVA non trouvé.'], 404);
|
||||||
|
}
|
||||||
|
return response()->json([
|
||||||
|
'data' => new TvaRateResource($tvaRate),
|
||||||
|
'status' => 'success'
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error fetching TVA rate: ' . $e->getMessage());
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la récupération du taux de TVA.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified TVA rate.
|
||||||
|
*/
|
||||||
|
public function update(UpdateTvaRateRequest $request, string $id): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$updated = $this->tvaRateRepository->update((int) $id, $request->validated());
|
||||||
|
if (!$updated) {
|
||||||
|
return response()->json(['message' => 'Taux de TVA non trouvé ou échec de la mise à jour.'], 404);
|
||||||
|
}
|
||||||
|
$tvaRate = $this->tvaRateRepository->find((int) $id);
|
||||||
|
return response()->json([
|
||||||
|
'data' => new TvaRateResource($tvaRate),
|
||||||
|
'message' => 'Taux de TVA mis à jour avec succès.',
|
||||||
|
'status' => 'success'
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error updating TVA rate: ' . $e->getMessage());
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la mise à jour du taux de TVA.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified TVA rate.
|
||||||
|
*/
|
||||||
|
public function destroy(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$deleted = $this->tvaRateRepository->delete((int) $id);
|
||||||
|
if (!$deleted) {
|
||||||
|
return response()->json(['message' => 'Taux de TVA non trouvé ou échec de la suppression.'], 404);
|
||||||
|
}
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Taux de TVA supprimé avec succès.',
|
||||||
|
'status' => 'success'
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error deleting TVA rate: ' . $e->getMessage());
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la suppression du taux de TVA.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -62,7 +62,7 @@ class WarehouseController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the specified warehouse.
|
* Display specified warehouse.
|
||||||
*/
|
*/
|
||||||
public function show(string $id): JsonResponse
|
public function show(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
@ -85,7 +85,7 @@ class WarehouseController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the specified warehouse.
|
* Update specified warehouse.
|
||||||
*/
|
*/
|
||||||
public function update(UpdateWarehouseRequest $request, string $id): JsonResponse
|
public function update(UpdateWarehouseRequest $request, string $id): JsonResponse
|
||||||
{
|
{
|
||||||
@ -110,7 +110,7 @@ class WarehouseController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the specified warehouse.
|
* Remove specified warehouse.
|
||||||
*/
|
*/
|
||||||
public function destroy(string $id): JsonResponse
|
public function destroy(string $id): JsonResponse
|
||||||
{
|
{
|
||||||
@ -131,4 +131,44 @@ class WarehouseController extends Controller
|
|||||||
], 500);
|
], 500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search warehouses by name.
|
||||||
|
*/
|
||||||
|
public function searchBy(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$name = $request->query('name');
|
||||||
|
$exactMatch = $request->query('exact_match', false);
|
||||||
|
|
||||||
|
if (empty($name)) {
|
||||||
|
return response()->json([
|
||||||
|
'data' => [],
|
||||||
|
'count' => 0,
|
||||||
|
'message' => 'Le paramètre de recherche est requis.'
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$warehouses = $this->warehouseRepository->all();
|
||||||
|
|
||||||
|
$filtered = $warehouses->filter(function ($warehouse) use ($name, $exactMatch) {
|
||||||
|
if ($exactMatch) {
|
||||||
|
return strtolower($warehouse->name) === strtolower($name);
|
||||||
|
}
|
||||||
|
return stripos($warehouse->name, $name) !== false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'data' => WarehouseResource::collection($filtered),
|
||||||
|
'count' => $filtered->count(),
|
||||||
|
'message' => 'Recherche effectuée avec succès.'
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error searching warehouses: ' . $e->getMessage());
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Une erreur est survenue lors de la recherche des entrepôts.',
|
||||||
|
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StoreGoodsReceiptRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'purchase_order_id' => 'required|exists:purchase_orders,id',
|
||||||
|
'warehouse_id' => 'required|exists:warehouses,id',
|
||||||
|
'receipt_number' => 'required|string|max:191',
|
||||||
|
'receipt_date' => 'required|date',
|
||||||
|
'status' => 'nullable|in:draft,posted',
|
||||||
|
'notes' => 'nullable|string',
|
||||||
|
'lines' => 'nullable|array',
|
||||||
|
'lines.*.product_id' => 'required_with:lines|exists:products,id',
|
||||||
|
'lines.*.packaging_id' => 'nullable|exists:product_packagings,id',
|
||||||
|
'lines.*.packages_qty_received' => 'nullable|numeric|min:0',
|
||||||
|
'lines.*.units_qty_received' => 'nullable|numeric|min:0',
|
||||||
|
'lines.*.qty_received_base' => 'nullable|numeric|min:0',
|
||||||
|
'lines.*.unit_price' => 'nullable|numeric|min:0',
|
||||||
|
'lines.*.unit_price_per_package' => 'nullable|numeric|min:0',
|
||||||
|
'lines.*.tva_rate_id' => 'nullable|exists:tva_rates,id',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the error messages for the defined validation rules.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'purchase_order_id.required' => 'La commande fournisseur est requise.',
|
||||||
|
'purchase_order_id.exists' => 'La commande fournisseur spécifiée n\'existe pas.',
|
||||||
|
'warehouse_id.required' => 'L\'entrepôt est requis.',
|
||||||
|
'warehouse_id.exists' => 'L\'entrepôt spécifié n\'existe pas.',
|
||||||
|
'receipt_number.required' => 'Le numéro de réception est requis.',
|
||||||
|
'receipt_number.string' => 'Le numéro de réception doit être une chaîne de caractères.',
|
||||||
|
'receipt_number.max' => 'Le numéro de réception ne peut pas dépasser 191 caractères.',
|
||||||
|
'receipt_date.required' => 'La date de réception est requise.',
|
||||||
|
'receipt_date.date' => 'La date de réception doit être une date valide.',
|
||||||
|
'status.in' => 'Le statut doit être "draft" ou "posted".',
|
||||||
|
'notes.string' => 'Les notes doivent être une chaîne de caractères.',
|
||||||
|
'lines.array' => 'Les lignes doivent être un tableau.',
|
||||||
|
'lines.*.product_id.required_with' => 'Le produit est requis pour chaque ligne.',
|
||||||
|
'lines.*.product_id.exists' => 'Le produit spécifié dans une ligne n\'existe pas.',
|
||||||
|
'lines.*.packaging_id.exists' => 'Le conditionnement spécifié dans une ligne n\'existe pas.',
|
||||||
|
'lines.*.packages_qty_received.numeric' => 'La quantité de colis doit être un nombre.',
|
||||||
|
'lines.*.packages_qty_received.min' => 'La quantité de colis ne peut pas être négative.',
|
||||||
|
'lines.*.units_qty_received.numeric' => 'La quantité d\'unités doit être un nombre.',
|
||||||
|
'lines.*.units_qty_received.min' => 'La quantité d\'unités ne peut pas être négative.',
|
||||||
|
'lines.*.qty_received_base.numeric' => 'La quantité de base doit être un nombre.',
|
||||||
|
'lines.*.qty_received_base.min' => 'La quantité de base ne peut pas être négative.',
|
||||||
|
'lines.*.unit_price.numeric' => 'Le prix unitaire doit être un nombre.',
|
||||||
|
'lines.*.unit_price.min' => 'Le prix unitaire ne peut pas être négatif.',
|
||||||
|
'lines.*.unit_price_per_package.numeric' => 'Le prix par colis doit être un nombre.',
|
||||||
|
'lines.*.unit_price_per_package.min' => 'Le prix par colis ne peut pas être négatif.',
|
||||||
|
'lines.*.tva_rate_id.exists' => 'Le taux de TVA spécifié dans une ligne n\'existe pas.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
47
thanasoft-back/app/Http/Requests/StoreTvaRateRequest.php
Normal file
47
thanasoft-back/app/Http/Requests/StoreTvaRateRequest.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StoreTvaRateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => 'required|string|max:50',
|
||||||
|
'rate' => 'required|numeric|min:0|max:999.99',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the error messages for the defined validation rules.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name.required' => 'Le nom du taux de TVA est requis.',
|
||||||
|
'name.string' => 'Le nom doit être une chaîne de caractères.',
|
||||||
|
'name.max' => 'Le nom ne peut pas dépasser 50 caractères.',
|
||||||
|
'rate.required' => 'Le taux de TVA est requis.',
|
||||||
|
'rate.numeric' => 'Le taux doit être un nombre.',
|
||||||
|
'rate.min' => 'Le taux ne peut pas être négatif.',
|
||||||
|
'rate.max' => 'Le taux ne peut pas dépasser 999.99.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class UpdateGoodsReceiptRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'purchase_order_id' => 'sometimes|exists:purchase_orders,id',
|
||||||
|
'warehouse_id' => 'sometimes|exists:warehouses,id',
|
||||||
|
'receipt_number' => 'sometimes|string|max:191',
|
||||||
|
'receipt_date' => 'sometimes|date',
|
||||||
|
'status' => 'nullable|in:draft,posted',
|
||||||
|
'notes' => 'nullable|string',
|
||||||
|
'lines' => 'nullable|array',
|
||||||
|
'lines.*.product_id' => 'required_with:lines|exists:products,id',
|
||||||
|
'lines.*.packaging_id' => 'nullable|exists:product_packagings,id',
|
||||||
|
'lines.*.packages_qty_received' => 'nullable|numeric|min:0',
|
||||||
|
'lines.*.units_qty_received' => 'nullable|numeric|min:0',
|
||||||
|
'lines.*.qty_received_base' => 'nullable|numeric|min:0',
|
||||||
|
'lines.*.unit_price' => 'nullable|numeric|min:0',
|
||||||
|
'lines.*.unit_price_per_package' => 'nullable|numeric|min:0',
|
||||||
|
'lines.*.tva_rate_id' => 'nullable|exists:tva_rates,id',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the error messages for the defined validation rules.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'purchase_order_id.exists' => 'La commande fournisseur spécifiée n\'existe pas.',
|
||||||
|
'warehouse_id.exists' => 'L\'entrepôt spécifié n\'existe pas.',
|
||||||
|
'receipt_number.string' => 'Le numéro de réception doit être une chaîne de caractères.',
|
||||||
|
'receipt_number.max' => 'Le numéro de réception ne peut pas dépasser 191 caractères.',
|
||||||
|
'receipt_date.date' => 'La date de réception doit être une date valide.',
|
||||||
|
'status.in' => 'Le statut doit être "draft" ou "posted".',
|
||||||
|
'notes.string' => 'Les notes doivent être une chaîne de caractères.',
|
||||||
|
'lines.array' => 'Les lignes doivent être un tableau.',
|
||||||
|
'lines.*.product_id.required_with' => 'Le produit est requis pour chaque ligne.',
|
||||||
|
'lines.*.product_id.exists' => 'Le produit spécifié dans une ligne n\'existe pas.',
|
||||||
|
'lines.*.packaging_id.exists' => 'Le conditionnement spécifié dans une ligne n\'existe pas.',
|
||||||
|
'lines.*.packages_qty_received.numeric' => 'La quantité de colis doit être un nombre.',
|
||||||
|
'lines.*.packages_qty_received.min' => 'La quantité de colis ne peut pas être négative.',
|
||||||
|
'lines.*.units_qty_received.numeric' => 'La quantité d\'unités doit être un nombre.',
|
||||||
|
'lines.*.units_qty_received.min' => 'La quantité d\'unités ne peut pas être négative.',
|
||||||
|
'lines.*.qty_received_base.numeric' => 'La quantité de base doit être un nombre.',
|
||||||
|
'lines.*.qty_received_base.min' => 'La quantité de base ne peut pas être négative.',
|
||||||
|
'lines.*.unit_price.numeric' => 'Le prix unitaire doit être un nombre.',
|
||||||
|
'lines.*.unit_price.min' => 'Le prix unitaire ne peut pas être négatif.',
|
||||||
|
'lines.*.unit_price_per_package.numeric' => 'Le prix par colis doit être un nombre.',
|
||||||
|
'lines.*.unit_price_per_package.min' => 'Le prix par colis ne peut pas être négatif.',
|
||||||
|
'lines.*.tva_rate_id.exists' => 'Le taux de TVA spécifié dans une ligne n\'existe pas.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
45
thanasoft-back/app/Http/Requests/UpdateTvaRateRequest.php
Normal file
45
thanasoft-back/app/Http/Requests/UpdateTvaRateRequest.php
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class UpdateTvaRateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => 'sometimes|string|max:50',
|
||||||
|
'rate' => 'sometimes|numeric|min:0|max:999.99',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the error messages for the defined validation rules.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name.string' => 'Le nom doit être une chaîne de caractères.',
|
||||||
|
'name.max' => 'Le nom ne peut pas dépasser 50 caractères.',
|
||||||
|
'rate.numeric' => 'Le taux doit être un nombre.',
|
||||||
|
'rate.min' => 'Le taux ne peut pas être négatif.',
|
||||||
|
'rate.max' => 'Le taux ne peut pas dépasser 999.99.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class GoodsReceiptLineResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'goods_receipt_id' => $this->goods_receipt_id,
|
||||||
|
'product_id' => $this->product_id,
|
||||||
|
'product' => new ProductResource($this->whenLoaded('product')),
|
||||||
|
'packaging_id' => $this->packaging_id,
|
||||||
|
'packaging' => new ProductPackagingResource($this->whenLoaded('packaging')),
|
||||||
|
'packages_qty_received' => $this->packages_qty_received,
|
||||||
|
'units_qty_received' => $this->units_qty_received,
|
||||||
|
'qty_received_base' => $this->qty_received_base,
|
||||||
|
'unit_price' => $this->unit_price,
|
||||||
|
'unit_price_per_package' => $this->unit_price_per_package,
|
||||||
|
'tva_rate_id' => $this->tva_rate_id,
|
||||||
|
'tva_rate' => new TvaRateResource($this->whenLoaded('tvaRate')),
|
||||||
|
'created_at' => $this->created_at,
|
||||||
|
'updated_at' => $this->updated_at,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
32
thanasoft-back/app/Http/Resources/GoodsReceiptResource.php
Normal file
32
thanasoft-back/app/Http/Resources/GoodsReceiptResource.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class GoodsReceiptResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'purchase_order_id' => $this->purchase_order_id,
|
||||||
|
'purchase_order' => new PurchaseOrderResource($this->whenLoaded('purchaseOrder')),
|
||||||
|
'warehouse_id' => $this->warehouse_id,
|
||||||
|
'warehouse' => new WarehouseResource($this->whenLoaded('warehouse')),
|
||||||
|
'receipt_number' => $this->receipt_number,
|
||||||
|
'receipt_date' => $this->receipt_date,
|
||||||
|
'status' => $this->status,
|
||||||
|
'notes' => $this->notes,
|
||||||
|
'lines' => GoodsReceiptLineResource::collection($this->whenLoaded('lines')),
|
||||||
|
'created_at' => $this->created_at,
|
||||||
|
'updated_at' => $this->updated_at,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
25
thanasoft-back/app/Http/Resources/TvaRateResource.php
Normal file
25
thanasoft-back/app/Http/Resources/TvaRateResource.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class TvaRateResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'name' => $this->name,
|
||||||
|
'rate' => $this->rate,
|
||||||
|
'created_at' => $this->created_at,
|
||||||
|
'updated_at' => $this->updated_at,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
41
thanasoft-back/app/Models/GoodsReceipt.php
Normal file
41
thanasoft-back/app/Models/GoodsReceipt.php
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class GoodsReceipt extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'purchase_order_id',
|
||||||
|
'warehouse_id',
|
||||||
|
'receipt_number',
|
||||||
|
'receipt_date',
|
||||||
|
'status',
|
||||||
|
'notes',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'receipt_date' => 'date',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function purchaseOrder(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(PurchaseOrder::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function warehouse(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Warehouse::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function lines(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(GoodsReceiptLine::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
thanasoft-back/app/Models/GoodsReceiptLine.php
Normal file
53
thanasoft-back/app/Models/GoodsReceiptLine.php
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Models\Stock\ProductPackaging;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class GoodsReceiptLine extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'goods_receipt_id',
|
||||||
|
'product_id',
|
||||||
|
'packaging_id',
|
||||||
|
'packages_qty_received',
|
||||||
|
'units_qty_received',
|
||||||
|
'qty_received_base',
|
||||||
|
'unit_price',
|
||||||
|
'unit_price_per_package',
|
||||||
|
'tva_rate_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'packages_qty_received' => 'decimal:3',
|
||||||
|
'units_qty_received' => 'decimal:3',
|
||||||
|
'qty_received_base' => 'decimal:3',
|
||||||
|
'unit_price' => 'decimal:2',
|
||||||
|
'unit_price_per_package' => 'decimal:2',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function goodsReceipt(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(GoodsReceipt::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function product(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Product::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function packaging(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ProductPackaging::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tvaRate(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(TvaRate::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -123,6 +123,14 @@ class Product extends Model
|
|||||||
return $this->hasMany(StockMove::class);
|
return $this->hasMany(StockMove::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the goods receipt lines for the product.
|
||||||
|
*/
|
||||||
|
public function goodsReceiptLines(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(GoodsReceiptLine::class);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Boot the model
|
* Boot the model
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -30,7 +30,7 @@ class PurchaseOrder extends Model
|
|||||||
$newNumber = 1;
|
$newNumber = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
$purchaseOrder->po_number = $prefix . str_pad((string)$newNumber, 4, '0', STR_PAD_LEFT);
|
$purchaseOrder->po_number = $prefix . str_pad((string) $newNumber, 4, '0', STR_PAD_LEFT);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -66,4 +66,12 @@ class PurchaseOrder extends Model
|
|||||||
{
|
{
|
||||||
return $this->hasMany(PurchaseOrderLine::class);
|
return $this->hasMany(PurchaseOrderLine::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the goods receipts for this purchase order.
|
||||||
|
*/
|
||||||
|
public function goodsReceipts(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(GoodsReceipt::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
thanasoft-back/app/Models/TvaRate.php
Normal file
20
thanasoft-back/app/Models/TvaRate.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class TvaRate extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'rate',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'rate' => 'decimal:2',
|
||||||
|
];
|
||||||
|
}
|
||||||
@ -41,4 +41,12 @@ class Warehouse extends Model
|
|||||||
{
|
{
|
||||||
return $this->hasMany(StockMove::class, 'to_warehouse_id');
|
return $this->hasMany(StockMove::class, 'to_warehouse_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the goods receipts for this warehouse.
|
||||||
|
*/
|
||||||
|
public function goodsReceipts(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(GoodsReceipt::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,8 @@ class RepositoryServiceProvider extends ServiceProvider
|
|||||||
$this->app->bind(\App\Repositories\StockItemRepositoryInterface::class, \App\Repositories\StockItemRepository::class);
|
$this->app->bind(\App\Repositories\StockItemRepositoryInterface::class, \App\Repositories\StockItemRepository::class);
|
||||||
$this->app->bind(\App\Repositories\StockMoveRepositoryInterface::class, \App\Repositories\StockMoveRepository::class);
|
$this->app->bind(\App\Repositories\StockMoveRepositoryInterface::class, \App\Repositories\StockMoveRepository::class);
|
||||||
$this->app->bind(\App\Repositories\ProductPackagingRepositoryInterface::class, \App\Repositories\ProductPackagingRepository::class);
|
$this->app->bind(\App\Repositories\ProductPackagingRepositoryInterface::class, \App\Repositories\ProductPackagingRepository::class);
|
||||||
|
$this->app->bind(\App\Repositories\TvaRateRepositoryInterface::class, \App\Repositories\TvaRateRepository::class);
|
||||||
|
$this->app->bind(\App\Repositories\GoodsReceiptRepositoryInterface::class, \App\Repositories\GoodsReceiptRepository::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
87
thanasoft-back/app/Repositories/GoodsReceiptRepository.php
Normal file
87
thanasoft-back/app/Repositories/GoodsReceiptRepository.php
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
use App\Models\GoodsReceipt;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
class GoodsReceiptRepository extends BaseRepository implements GoodsReceiptRepositoryInterface
|
||||||
|
{
|
||||||
|
public function __construct(GoodsReceipt $model)
|
||||||
|
{
|
||||||
|
parent::__construct($model);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function all(array $columns = ['*']): Collection
|
||||||
|
{
|
||||||
|
return $this->model->with(['purchaseOrder', 'warehouse', 'lines.product', 'lines.packaging', 'lines.tvaRate'])->get($columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function find(int|string $id, array $columns = ['*']): ?GoodsReceipt
|
||||||
|
{
|
||||||
|
return $this->model->with(['purchaseOrder', 'warehouse', 'lines.product', 'lines.packaging', 'lines.tvaRate'])->find($id, $columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(array $attributes): GoodsReceipt
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($attributes) {
|
||||||
|
try {
|
||||||
|
$lines = $attributes['lines'] ?? [];
|
||||||
|
unset($attributes['lines']);
|
||||||
|
|
||||||
|
$goodsReceipt = parent::create($attributes);
|
||||||
|
|
||||||
|
if (!empty($lines)) {
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$goodsReceipt->lines()->create($line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $goodsReceipt->load('lines.product', 'lines.packaging', 'lines.tvaRate');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error creating GoodsReceipt with lines: ' . $e->getMessage(), [
|
||||||
|
'attributes' => $attributes,
|
||||||
|
'exception' => $e
|
||||||
|
]);
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(int|string $id, array $attributes): bool
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($id, $attributes) {
|
||||||
|
try {
|
||||||
|
$goodsReceipt = $this->find($id);
|
||||||
|
if (!$goodsReceipt) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = $attributes['lines'] ?? null;
|
||||||
|
unset($attributes['lines']);
|
||||||
|
|
||||||
|
$updated = parent::update($id, $attributes);
|
||||||
|
|
||||||
|
if ($lines !== null && $updated) {
|
||||||
|
$goodsReceipt->lines()->delete();
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$goodsReceipt->lines()->create($line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $updated;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error updating GoodsReceipt with lines: ' . $e->getMessage(), [
|
||||||
|
'id' => $id,
|
||||||
|
'attributes' => $attributes,
|
||||||
|
'exception' => $e
|
||||||
|
]);
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
interface GoodsReceiptRepositoryInterface extends BaseRepositoryInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
26
thanasoft-back/app/Repositories/TvaRateRepository.php
Normal file
26
thanasoft-back/app/Repositories/TvaRateRepository.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
use App\Models\TvaRate;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
class TvaRateRepository extends BaseRepository implements TvaRateRepositoryInterface
|
||||||
|
{
|
||||||
|
public function __construct(TvaRate $model)
|
||||||
|
{
|
||||||
|
parent::__construct($model);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function all(array $columns = ['*']): Collection
|
||||||
|
{
|
||||||
|
return $this->model->get($columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function find(int|string $id, array $columns = ['*']): ?TvaRate
|
||||||
|
{
|
||||||
|
return $this->model->find($id, $columns);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
interface TvaRateRepositoryInterface extends BaseRepositoryInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -4,8 +4,7 @@ use Illuminate\Database\Migrations\Migration;
|
|||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
return new class extends Migration
|
return new class extends Migration {
|
||||||
{
|
|
||||||
/**
|
/**
|
||||||
* Run the migrations.
|
* Run the migrations.
|
||||||
*/
|
*/
|
||||||
@ -15,7 +14,7 @@ return new class extends Migration
|
|||||||
Schema::create('purchase_orders', function (Blueprint $table) {
|
Schema::create('purchase_orders', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->unsignedBigInteger('fournisseur_id'); // Use existing fournisseurs table
|
$table->unsignedBigInteger('fournisseur_id'); // Use existing fournisseurs table
|
||||||
|
|
||||||
$table->string('po_number', 191);
|
$table->string('po_number', 191);
|
||||||
$table->enum('status', ['brouillon', 'confirmee', 'livree', 'facturee', 'annulee'])->default('brouillon');
|
$table->enum('status', ['brouillon', 'confirmee', 'livree', 'facturee', 'annulee'])->default('brouillon');
|
||||||
$table->date('order_date')->useCurrent();
|
$table->date('order_date')->useCurrent();
|
||||||
@ -26,13 +25,13 @@ return new class extends Migration
|
|||||||
$table->decimal('total_ttc', 14, 2)->default(0);
|
$table->decimal('total_ttc', 14, 2)->default(0);
|
||||||
$table->text('notes')->nullable();
|
$table->text('notes')->nullable();
|
||||||
$table->string('delivery_address')->nullable();
|
$table->string('delivery_address')->nullable();
|
||||||
|
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
||||||
$table->unique('po_number', 'uq_po_number');
|
$table->unique('po_number', 'uq_po_number');
|
||||||
$table->index('status', 'idx_po_status');
|
$table->index('status', 'idx_po_status');
|
||||||
$table->index('order_date', 'idx_po_order_date');
|
$table->index('order_date', 'idx_po_order_date');
|
||||||
|
|
||||||
$table->foreign('fournisseur_id', 'fk_po_fournisseur')->references('id')->on('fournisseurs')->onDelete('cascade');
|
$table->foreign('fournisseur_id', 'fk_po_fournisseur')->references('id')->on('fournisseurs')->onDelete('cascade');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -41,61 +40,28 @@ return new class extends Migration
|
|||||||
$table->id();
|
$table->id();
|
||||||
$table->unsignedBigInteger('purchase_order_id');
|
$table->unsignedBigInteger('purchase_order_id');
|
||||||
$table->unsignedBigInteger('product_id')->nullable();
|
$table->unsignedBigInteger('product_id')->nullable();
|
||||||
|
|
||||||
$table->text('description');
|
$table->text('description');
|
||||||
$table->decimal('quantity', 14, 3)->default(1);
|
$table->decimal('quantity', 14, 3)->default(1);
|
||||||
$table->decimal('unit_price', 12, 2);
|
$table->decimal('unit_price', 12, 2);
|
||||||
$table->decimal('tva_rate', 5, 2)->default(0); // Use rate directly, no FK to tva_rates
|
$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('discount_pct', 5, 2)->default(0);
|
||||||
$table->decimal('total_ht', 14, 2);
|
$table->decimal('total_ht', 14, 2);
|
||||||
|
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
||||||
$table->foreign('purchase_order_id', 'fk_pol_po')->references('id')->on('purchase_orders')->onDelete('cascade');
|
$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');
|
$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)
|
// Supplier Invoices (Factures Fournisseurs)
|
||||||
Schema::create('supplier_invoices', function (Blueprint $table) {
|
Schema::create('supplier_invoices', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->unsignedBigInteger('fournisseur_id');
|
$table->unsignedBigInteger('fournisseur_id');
|
||||||
$table->unsignedBigInteger('purchase_order_id')->nullable(); // Optional link to purchase order
|
$table->unsignedBigInteger('purchase_order_id')->nullable(); // Optional link to purchase order
|
||||||
|
|
||||||
$table->string('invoice_number', 191);
|
$table->string('invoice_number', 191);
|
||||||
$table->date('invoice_date')->useCurrent();
|
$table->date('invoice_date')->useCurrent();
|
||||||
$table->date('due_date')->nullable();
|
$table->date('due_date')->nullable();
|
||||||
@ -105,13 +71,13 @@ return new class extends Migration
|
|||||||
$table->decimal('total_tva', 14, 2)->default(0);
|
$table->decimal('total_tva', 14, 2)->default(0);
|
||||||
$table->decimal('total_ttc', 14, 2)->default(0);
|
$table->decimal('total_ttc', 14, 2)->default(0);
|
||||||
$table->text('notes')->nullable();
|
$table->text('notes')->nullable();
|
||||||
|
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
||||||
$table->unique(['fournisseur_id', 'invoice_number'], 'uq_supplier_invoice');
|
$table->unique(['fournisseur_id', 'invoice_number'], 'uq_supplier_invoice');
|
||||||
$table->index('status', 'idx_si_status');
|
$table->index('status', 'idx_si_status');
|
||||||
$table->index('invoice_date', 'idx_si_invoice_date');
|
$table->index('invoice_date', 'idx_si_invoice_date');
|
||||||
|
|
||||||
$table->foreign('fournisseur_id', 'fk_si_fournisseur')->references('id')->on('fournisseurs');
|
$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');
|
$table->foreign('purchase_order_id', 'fk_si_po')->references('id')->on('purchase_orders')->onDelete('set null');
|
||||||
});
|
});
|
||||||
@ -122,13 +88,13 @@ return new class extends Migration
|
|||||||
$table->unsignedBigInteger('supplier_invoice_id');
|
$table->unsignedBigInteger('supplier_invoice_id');
|
||||||
$table->unsignedBigInteger('product_id')->nullable();
|
$table->unsignedBigInteger('product_id')->nullable();
|
||||||
$table->unsignedBigInteger('purchase_order_line_id')->nullable(); // Link to original order line
|
$table->unsignedBigInteger('purchase_order_line_id')->nullable(); // Link to original order line
|
||||||
|
|
||||||
$table->text('description');
|
$table->text('description');
|
||||||
$table->decimal('quantity', 14, 3)->default(1);
|
$table->decimal('quantity', 14, 3)->default(1);
|
||||||
$table->decimal('unit_price', 12, 2);
|
$table->decimal('unit_price', 12, 2);
|
||||||
$table->decimal('tva_rate', 5, 2)->default(0);
|
$table->decimal('tva_rate', 5, 2)->default(0);
|
||||||
$table->decimal('total_ht', 14, 2);
|
$table->decimal('total_ht', 14, 2);
|
||||||
|
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
||||||
$table->foreign('supplier_invoice_id', 'fk_sil_si')->references('id')->on('supplier_invoices')->onDelete('cascade');
|
$table->foreign('supplier_invoice_id', 'fk_sil_si')->references('id')->on('supplier_invoices')->onDelete('cascade');
|
||||||
@ -144,8 +110,7 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
Schema::dropIfExists('supplier_invoice_lines');
|
Schema::dropIfExists('supplier_invoice_lines');
|
||||||
Schema::dropIfExists('supplier_invoices');
|
Schema::dropIfExists('supplier_invoices');
|
||||||
Schema::dropIfExists('goods_receipt_lines');
|
|
||||||
Schema::dropIfExists('goods_receipts');
|
|
||||||
Schema::dropIfExists('purchase_order_lines');
|
Schema::dropIfExists('purchase_order_lines');
|
||||||
Schema::dropIfExists('purchase_orders');
|
Schema::dropIfExists('purchase_orders');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,82 @@
|
|||||||
|
<?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
|
||||||
|
{
|
||||||
|
// TVA Rates (Taux de TVA)
|
||||||
|
if (!Schema::hasTable('tva_rates')) {
|
||||||
|
Schema::create('tva_rates', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name', 50);
|
||||||
|
$table->decimal('rate', 5, 2);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Goods Receipts (Réceptions de marchandises)
|
||||||
|
Schema::create('goods_receipts', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('purchase_order_id');
|
||||||
|
$table->unsignedBigInteger('warehouse_id'); // entrepôt de réception
|
||||||
|
|
||||||
|
$table->string('receipt_number', 191);
|
||||||
|
$table->date('receipt_date')->useCurrent(); // Default handled by DB if possible or model
|
||||||
|
$table->enum('status', ['draft', 'posted'])->default('draft');
|
||||||
|
$table->timestamp('created_at')->useCurrent();
|
||||||
|
// Note: Laravel default timestamps are created_at and updated_at.
|
||||||
|
// The user asked for created_at DEFAULT CURRENT_TIMESTAMP.
|
||||||
|
// Laravel's $table->timestamps() creates both created_at and updated_at nullable.
|
||||||
|
// We will stick to standard Laravel behavior but can add default if strictly needed.
|
||||||
|
// For now, let's use standard timestamps + specific columns requested.
|
||||||
|
$table->timestamp('updated_at')->nullable();
|
||||||
|
|
||||||
|
$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');
|
||||||
|
$table->foreign('warehouse_id', 'fk_gr_wh')->references('id')->on('warehouses');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Goods Receipt Lines
|
||||||
|
Schema::create('goods_receipt_lines', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('goods_receipt_id');
|
||||||
|
$table->unsignedBigInteger('product_id');
|
||||||
|
$table->unsignedBigInteger('packaging_id')->nullable();
|
||||||
|
|
||||||
|
$table->decimal('packages_qty_received', 14, 3)->nullable(); // nb de colis reçus
|
||||||
|
$table->decimal('units_qty_received', 14, 3)->nullable(); // nb d’unités reçues (si pas de colis)
|
||||||
|
$table->decimal('qty_received_base', 14, 3)->nullable(); // quantité base reçue (calculable)
|
||||||
|
$table->decimal('unit_price', 12, 2)->nullable(); // par unité produit
|
||||||
|
$table->decimal('unit_price_per_package', 12, 2)->nullable();// par colis
|
||||||
|
$table->unsignedBigInteger('tva_rate_id')->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('packaging_id', 'fk_grl_packaging')->references('id')->on('product_packagings')->onDelete('set null');
|
||||||
|
$table->foreign('tva_rate_id', 'fk_grl_tva')->references('id')->on('tva_rates')->onDelete('set null');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('goods_receipt_lines');
|
||||||
|
Schema::dropIfExists('goods_receipts');
|
||||||
|
// We do not drop tva_rates here blindly as it might have been created elsewhere if we decide to separate later.
|
||||||
|
// But since we created it conditionally, we can leave it or drop it if we are sure we own it.
|
||||||
|
// Given the instructions, I'll drop it if it's safe, but usually safe to leave shared tables or manage in separate migration.
|
||||||
|
// For this task, I'll only drop what I definitely created exclusively.
|
||||||
|
Schema::dropIfExists('tva_rates');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -21,6 +21,8 @@ use App\Http\Controllers\Api\FileAttachmentController;
|
|||||||
use App\Http\Controllers\Api\QuoteController;
|
use App\Http\Controllers\Api\QuoteController;
|
||||||
use App\Http\Controllers\Api\ClientActivityTimelineController;
|
use App\Http\Controllers\Api\ClientActivityTimelineController;
|
||||||
use App\Http\Controllers\Api\PurchaseOrderController;
|
use App\Http\Controllers\Api\PurchaseOrderController;
|
||||||
|
use App\Http\Controllers\Api\TvaRateController;
|
||||||
|
use App\Http\Controllers\Api\GoodsReceiptController;
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -99,12 +101,19 @@ Route::middleware('auth:sanctum')->group(function () {
|
|||||||
Route::patch('/products/{id}/stock', [ProductController::class, 'updateStock']);
|
Route::patch('/products/{id}/stock', [ProductController::class, 'updateStock']);
|
||||||
|
|
||||||
// Warehouse management
|
// Warehouse management
|
||||||
|
Route::get('/warehouses/searchBy', [\App\Http\Controllers\Api\WarehouseController::class, 'searchBy']);
|
||||||
Route::apiResource('warehouses', \App\Http\Controllers\Api\WarehouseController::class);
|
Route::apiResource('warehouses', \App\Http\Controllers\Api\WarehouseController::class);
|
||||||
|
|
||||||
// Stock management
|
// Stock management
|
||||||
Route::apiResource('stock-items', \App\Http\Controllers\Api\StockItemController::class);
|
Route::apiResource('stock-items', \App\Http\Controllers\Api\StockItemController::class);
|
||||||
Route::apiResource('stock-moves', \App\Http\Controllers\Api\StockMoveController::class);
|
Route::apiResource('stock-moves', \App\Http\Controllers\Api\StockMoveController::class);
|
||||||
|
|
||||||
|
// TVA Rates management
|
||||||
|
Route::apiResource('tva-rates', TvaRateController::class);
|
||||||
|
|
||||||
|
// Goods Receipts management
|
||||||
|
Route::apiResource('goods-receipts', GoodsReceiptController::class);
|
||||||
|
|
||||||
// Product Category management
|
// Product Category management
|
||||||
Route::get('/product-categories/search', [ProductCategoryController::class, 'search']);
|
Route::get('/product-categories/search', [ProductCategoryController::class, 'search']);
|
||||||
Route::get('/product-categories/active', [ProductCategoryController::class, 'active']);
|
Route::get('/product-categories/active', [ProductCategoryController::class, 'active']);
|
||||||
|
|||||||
@ -0,0 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<div class="d-sm-flex justify-content-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-0">Réceptions de Marchandises</h5>
|
||||||
|
<p class="text-sm mb-0">Gestion des réceptions de marchandises en provenance des fournisseurs.</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-sm-0 mt-3">
|
||||||
|
<soft-button color="info" variant="gradient" @click="handleCreate">
|
||||||
|
<i class="fas fa-plus me-2"></i> Nouvelle Réception
|
||||||
|
</soft-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Rechercher..."
|
||||||
|
v-model="searchQuery"
|
||||||
|
@input="handleSearch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<select class="form-control" v-model="statusFilter" @change="handleFilter">
|
||||||
|
<option value="">Tous les statuts</option>
|
||||||
|
<option value="draft">Brouillon</option>
|
||||||
|
<option value="posted">Validée</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<goods-receipt-table
|
||||||
|
:data="goodsReceipts"
|
||||||
|
:loading="loading"
|
||||||
|
@view="handleView"
|
||||||
|
@edit="handleEdit"
|
||||||
|
@delete="handleDelete"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import GoodsReceiptTable from "@/components/molecules/Tables/Stock/GoodsReceiptTable.vue";
|
||||||
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
import { useGoodsReceiptStore } from "@/stores/goodsReceiptStore";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const goodsReceiptStore = useGoodsReceiptStore();
|
||||||
|
const { goodsReceipts, loading } = storeToRefs(goodsReceiptStore);
|
||||||
|
|
||||||
|
const searchQuery = ref("");
|
||||||
|
const statusFilter = ref("");
|
||||||
|
|
||||||
|
let searchTimeout = null;
|
||||||
|
|
||||||
|
const handleCreate = () => {
|
||||||
|
router.push("/stock/receptions/new");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleView = (id) => {
|
||||||
|
router.push(`/stock/receptions/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (id) => {
|
||||||
|
router.push(`/stock/receptions/${id}/edit`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id) => {
|
||||||
|
if (confirm("Êtes-vous sûr de vouloir supprimer cette réception de marchandise ?")) {
|
||||||
|
try {
|
||||||
|
await goodsReceiptStore.deleteGoodsReceipt(id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete goods receipt", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
loadGoodsReceipts();
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilter = () => {
|
||||||
|
loadGoodsReceipts();
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadGoodsReceipts = () => {
|
||||||
|
const params = {
|
||||||
|
search: searchQuery.value || undefined,
|
||||||
|
status: statusFilter.value || undefined,
|
||||||
|
};
|
||||||
|
goodsReceiptStore.fetchGoodsReceipts(params);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadGoodsReceipts();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8 mx-auto">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header pb-0 p-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="mb-0">Nouvelle Réception de Marchandise</h6>
|
||||||
|
<soft-button
|
||||||
|
color="secondary"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
@click="goBack"
|
||||||
|
>
|
||||||
|
<i class="fas fa-arrow-left me-2"></i>Retour
|
||||||
|
</soft-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-3">
|
||||||
|
<new-reception-form
|
||||||
|
:loading="loading"
|
||||||
|
:purchase-orders="purchaseOrders"
|
||||||
|
@submit="handleSubmit"
|
||||||
|
@cancel="goBack"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import NewReceptionForm from "@/components/molecules/Stock/NewReceptionForm.vue";
|
||||||
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
import { useGoodsReceiptStore } from "@/stores/goodsReceiptStore";
|
||||||
|
import { usePurchaseOrderStore } from "@/stores/purchaseOrderStore";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const goodsReceiptStore = useGoodsReceiptStore();
|
||||||
|
const purchaseOrderStore = usePurchaseOrderStore();
|
||||||
|
|
||||||
|
const { loading } = storeToRefs(goodsReceiptStore);
|
||||||
|
|
||||||
|
const purchaseOrders = ref([]);
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
router.back();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (formData) => {
|
||||||
|
try {
|
||||||
|
await goodsReceiptStore.createGoodsReceipt(formData);
|
||||||
|
router.push("/stock/receptions");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create goods receipt", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
await purchaseOrderStore.fetchPurchaseOrders();
|
||||||
|
purchaseOrders.value = purchaseOrderStore.purchaseOrders;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load purchase orders", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -0,0 +1,189 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-fluid py-4" v-if="goodsReceipt">
|
||||||
|
<div class="d-sm-flex justify-content-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-0">Réception de Marchandise: {{ goodsReceipt.receipt_number }}</h5>
|
||||||
|
<p class="text-sm mb-0">
|
||||||
|
Créée le {{ formatDate(goodsReceipt.created_at) }} -
|
||||||
|
<soft-badge :color="getStatusColor(goodsReceipt.status)" variant="gradient">
|
||||||
|
{{ getStatusLabel(goodsReceipt.status) }}
|
||||||
|
</soft-badge>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-sm-0 mt-3">
|
||||||
|
<soft-button color="secondary" variant="gradient" class="me-2" @click="handleBack">
|
||||||
|
<i class="fas fa-arrow-left me-2"></i> Retour
|
||||||
|
</soft-button>
|
||||||
|
<soft-button color="info" variant="gradient" class="me-2" @click="handleEdit">
|
||||||
|
<i class="fas fa-edit me-2"></i> Modifier
|
||||||
|
</soft-button>
|
||||||
|
<soft-button color="danger" variant="gradient" @click="handleDelete">
|
||||||
|
<i class="fas fa-trash me-2"></i> Supprimer
|
||||||
|
</soft-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header pb-0">
|
||||||
|
<h6>Informations Générales</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="text-xs font-weight-bold">Numéro de Réception</label>
|
||||||
|
<p class="text-sm">{{ goodsReceipt.receipt_number }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="text-xs font-weight-bold">Date de Réception</label>
|
||||||
|
<p class="text-sm">{{ formatDate(goodsReceipt.receipt_date) }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="text-xs font-weight-bold">Commande Fournisseur</label>
|
||||||
|
<p class="text-sm">
|
||||||
|
{{ goodsReceipt.purchase_order?.po_number || goodsReceipt.purchase_order_id }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="text-xs font-weight-bold">Entrepôt de Destination</label>
|
||||||
|
<p class="text-sm">{{ goodsReceipt.warehouse?.name || '-' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="text-xs font-weight-bold">Notes</label>
|
||||||
|
<p class="text-sm">{{ goodsReceipt.notes || 'Aucune note' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header pb-0">
|
||||||
|
<h6>Lignes de Réception</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-flush">
|
||||||
|
<thead class="thead-light">
|
||||||
|
<tr>
|
||||||
|
<th>Produit</th>
|
||||||
|
<th>Conditionnement</th>
|
||||||
|
<th>Colis</th>
|
||||||
|
<th>Unités</th>
|
||||||
|
<th>Prix Unitaire</th>
|
||||||
|
<th>TVA</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="line in goodsReceipt.lines" :key="line.id">
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-weight-bold mb-0">
|
||||||
|
{{ line.product?.nom || 'Produit ' + line.product_id }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-secondary mb-0">
|
||||||
|
{{ line.product?.reference || '' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-xs">
|
||||||
|
{{ line.packaging?.name || 'Unité' }}
|
||||||
|
</td>
|
||||||
|
<td class="text-xs">
|
||||||
|
{{ line.packages_qty_received || '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="text-xs">
|
||||||
|
{{ line.units_qty_received || '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="text-xs">
|
||||||
|
{{ line.unit_price ? formatCurrency(line.unit_price) : '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="text-xs">
|
||||||
|
{{ line.tva_rate ? line.tva_rate.name + ' (' + line.tva_rate.rate + '%)' : '-' }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!goodsReceipt.lines || goodsReceipt.lines.length === 0">
|
||||||
|
<td colspan="6" class="text-center text-muted py-4">
|
||||||
|
Aucune ligne dans cette réception.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="container-fluid py-4">
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, computed } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import SoftBadge from "@/components/SoftBadge.vue";
|
||||||
|
import { useGoodsReceiptStore } from "@/stores/goodsReceiptStore";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const goodsReceiptStore = useGoodsReceiptStore();
|
||||||
|
const { currentGoodsReceipt: goodsReceipt, loading } = storeToRefs(goodsReceiptStore);
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
if (!dateString) return '-';
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString('fr-FR');
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCurrency = (value) => {
|
||||||
|
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'draft': return 'secondary';
|
||||||
|
case 'posted': return 'success';
|
||||||
|
default: return 'info';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusLabel = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'draft': return 'Brouillon';
|
||||||
|
case 'posted': return 'Validée';
|
||||||
|
default: return status;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
router.push("/stock/receptions");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = () => {
|
||||||
|
router.push(`/stock/receptions/${route.params.id}/edit`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (confirm("Êtes-vous sûr de vouloir supprimer cette réception de marchandise ?")) {
|
||||||
|
try {
|
||||||
|
await goodsReceiptStore.deleteGoodsReceipt(parseInt(route.params.id));
|
||||||
|
router.push("/stock/receptions");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete goods receipt", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await goodsReceiptStore.fetchGoodsReceipt(parseInt(route.params.id));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -0,0 +1,723 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="submitForm" class="reception-form">
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="section-title">
|
||||||
|
<i class="fas fa-file-invoice"></i>
|
||||||
|
Informations générales
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 1: Commande Fournisseur, Entrepôt -->
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Commande Fournisseur <span class="text-danger">*</span></label>
|
||||||
|
<select
|
||||||
|
v-model="formData.purchase_order_id"
|
||||||
|
class="form-select custom-select"
|
||||||
|
:class="{ 'is-invalid': errors.purchase_order_id }"
|
||||||
|
>
|
||||||
|
<option value="">Sélectionner une commande</option>
|
||||||
|
<option v-for="po in purchaseOrders" :key="po.id" :value="po.id">
|
||||||
|
{{ po.po_number }} - {{ po.fournisseur?.nom || 'Fournisseur ' + po.fournisseur_id }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<div v-if="errors.purchase_order_id" class="invalid-feedback">
|
||||||
|
{{ errors.purchase_order_id }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 position-relative warehouse-search-container">
|
||||||
|
<label class="form-label">Entrepôt de Destination <span class="text-danger">*</span></label>
|
||||||
|
<div class="search-input-wrapper">
|
||||||
|
<i class="fas fa-search search-icon"></i>
|
||||||
|
<soft-input
|
||||||
|
v-model="warehouseSearchQuery"
|
||||||
|
type="text"
|
||||||
|
placeholder="Rechercher un entrepôt..."
|
||||||
|
@input="handleWarehouseSearch"
|
||||||
|
@focus="showWarehouseResults = true"
|
||||||
|
required
|
||||||
|
class="search-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Results Dropdown -->
|
||||||
|
<div
|
||||||
|
v-if="showWarehouseResults && (warehouseSearchResults.length > 0 || isSearchingWarehouses)"
|
||||||
|
class="search-dropdown"
|
||||||
|
>
|
||||||
|
<div v-if="isSearchingWarehouses" class="dropdown-loading">
|
||||||
|
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<button
|
||||||
|
v-for="warehouse in warehouseSearchResults"
|
||||||
|
:key="warehouse.id"
|
||||||
|
type="button"
|
||||||
|
class="dropdown-item"
|
||||||
|
@click="selectWarehouse(warehouse)"
|
||||||
|
>
|
||||||
|
<span class="item-name">{{ warehouse.name }}</span>
|
||||||
|
<span class="item-details">
|
||||||
|
{{ warehouse.city || 'Ville non spécifiée' }} • {{ warehouse.country_code || '' }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div v-if="errors.warehouse_id" class="invalid-feedback">
|
||||||
|
{{ errors.warehouse_id }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 2: Numéro Réception, Date, Statut -->
|
||||||
|
<div class="row g-3 mb-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Numéro de Réception</label>
|
||||||
|
<soft-input
|
||||||
|
v-model="formData.receipt_number"
|
||||||
|
type="text"
|
||||||
|
placeholder="Numéro auto-généré si laissé vide"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Date de Réception <span class="text-danger">*</span></label>
|
||||||
|
<soft-input
|
||||||
|
v-model="formData.receipt_date"
|
||||||
|
type="date"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Statut</label>
|
||||||
|
<div class="select-wrapper">
|
||||||
|
<select v-model="formData.status" class="form-select custom-select">
|
||||||
|
<option value="draft">📝 Brouillon</option>
|
||||||
|
<option value="posted">✅ Validée</option>
|
||||||
|
</select>
|
||||||
|
<i class="fas fa-chevron-down select-arrow"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notes -->
|
||||||
|
<div class="mb-0">
|
||||||
|
<label class="form-label">Notes</label>
|
||||||
|
<textarea
|
||||||
|
v-model="formData.notes"
|
||||||
|
class="form-control notes-textarea"
|
||||||
|
placeholder="Notes facultatives..."
|
||||||
|
rows="2"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Articles Section -->
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-title">
|
||||||
|
<i class="fas fa-boxes"></i>
|
||||||
|
Lignes de Réception
|
||||||
|
</div>
|
||||||
|
<soft-button
|
||||||
|
type="button"
|
||||||
|
color="primary"
|
||||||
|
size="sm"
|
||||||
|
@click="addLine"
|
||||||
|
class="add-btn"
|
||||||
|
>
|
||||||
|
<i class="fas fa-plus"></i> Ajouter ligne
|
||||||
|
</soft-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lines-container">
|
||||||
|
<div
|
||||||
|
v-for="(line, index) in formData.lines"
|
||||||
|
:key="index"
|
||||||
|
class="line-item"
|
||||||
|
>
|
||||||
|
<div class="row g-2 align-items-end">
|
||||||
|
<div class="col-md-3 position-relative product-search-container">
|
||||||
|
<label class="form-label text-xs">Produit <span class="text-danger">*</span></label>
|
||||||
|
<div class="search-input-wrapper">
|
||||||
|
<i class="fas fa-search search-icon"></i>
|
||||||
|
<soft-input
|
||||||
|
v-model="line.searchQuery"
|
||||||
|
type="text"
|
||||||
|
placeholder="Rechercher un produit..."
|
||||||
|
@input="handleProductSearch(index)"
|
||||||
|
@focus="activeLineIndex = index; showProductResults = true"
|
||||||
|
required
|
||||||
|
class="search-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Product Search Results Dropdown -->
|
||||||
|
<div
|
||||||
|
v-show="showProductResults && activeLineIndex === index"
|
||||||
|
class="search-dropdown product-dropdown"
|
||||||
|
>
|
||||||
|
<div v-if="isSearchingProducts" class="dropdown-loading">
|
||||||
|
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="productSearchResults.length === 0" class="dropdown-empty">
|
||||||
|
Aucun produit trouvé
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<button
|
||||||
|
v-for="product in productSearchResults"
|
||||||
|
:key="product.id"
|
||||||
|
type="button"
|
||||||
|
class="dropdown-item"
|
||||||
|
@mousedown.prevent="selectProduct(index, product)"
|
||||||
|
>
|
||||||
|
<span class="item-name">{{ product.nom }}</span>
|
||||||
|
<span class="item-details">
|
||||||
|
Réf: {{ product.reference }} • Stock: {{ product.stock_actuel }} {{ product.unite }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label text-xs">Conditionnement</label>
|
||||||
|
<select
|
||||||
|
class="form-select form-select-sm"
|
||||||
|
v-model="line.packaging_id"
|
||||||
|
>
|
||||||
|
<option :value="null">Unité</option>
|
||||||
|
<option v-for="pkg in getPackagings(line.product_id)" :key="pkg.id" :value="pkg.id">
|
||||||
|
{{ pkg.name }} ({{ pkg.qty_base }} unités)
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label text-xs">Colis Reçus</label>
|
||||||
|
<soft-input
|
||||||
|
v-model.number="line.packages_qty_received"
|
||||||
|
type="number"
|
||||||
|
placeholder="0"
|
||||||
|
min="0"
|
||||||
|
step="0.001"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label text-xs">Unités Reçues</label>
|
||||||
|
<soft-input
|
||||||
|
v-model.number="line.units_qty_received"
|
||||||
|
type="number"
|
||||||
|
placeholder="0"
|
||||||
|
min="0"
|
||||||
|
step="0.001"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label text-xs">Prix Unitaire</label>
|
||||||
|
<soft-input
|
||||||
|
v-model.number="line.unit_price"
|
||||||
|
type="number"
|
||||||
|
placeholder="0.00"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-1 d-flex flex-column align-items-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-delete"
|
||||||
|
@click="removeLine(index)"
|
||||||
|
:disabled="formData.lines.length === 1"
|
||||||
|
title="Supprimer la ligne"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="action-buttons">
|
||||||
|
<soft-button
|
||||||
|
type="button"
|
||||||
|
color="secondary"
|
||||||
|
variant="outline"
|
||||||
|
@click="cancelForm"
|
||||||
|
class="btn-cancel"
|
||||||
|
>
|
||||||
|
<i class="fas fa-times"></i> Annuler
|
||||||
|
</soft-button>
|
||||||
|
<soft-button
|
||||||
|
type="submit"
|
||||||
|
color="success"
|
||||||
|
class="btn-submit"
|
||||||
|
:loading="loading"
|
||||||
|
>
|
||||||
|
<i class="fas fa-check"></i> Enregistrer
|
||||||
|
</soft-button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, defineEmits, defineProps, onMounted, onUnmounted } from "vue";
|
||||||
|
import SoftInput from "@/components/SoftInput.vue";
|
||||||
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
import WarehouseService from "@/services/warehouse";
|
||||||
|
import ProductService from "@/services/product";
|
||||||
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
purchaseOrders: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["submit", "cancel"]);
|
||||||
|
|
||||||
|
const notificationStore = useNotificationStore();
|
||||||
|
|
||||||
|
// Warehouse Search States
|
||||||
|
const warehouseSearchQuery = ref("");
|
||||||
|
const warehouseSearchResults = ref([]);
|
||||||
|
const isSearchingWarehouses = ref(false);
|
||||||
|
const showWarehouseResults = ref(false);
|
||||||
|
let searchTimeout = null;
|
||||||
|
|
||||||
|
const handleWarehouseSearch = () => {
|
||||||
|
if (warehouseSearchQuery.value.length < 2) {
|
||||||
|
warehouseSearchResults.value = [];
|
||||||
|
if (searchTimeout) clearTimeout(searchTimeout);
|
||||||
|
isSearchingWarehouses.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchTimeout) clearTimeout(searchTimeout);
|
||||||
|
|
||||||
|
searchTimeout = setTimeout(async () => {
|
||||||
|
if (warehouseSearchQuery.value.trim() === "") return;
|
||||||
|
|
||||||
|
isSearchingWarehouses.value = true;
|
||||||
|
showWarehouseResults.value = true;
|
||||||
|
try {
|
||||||
|
const results = await WarehouseService.searchWarehouses(warehouseSearchQuery.value);
|
||||||
|
warehouseSearchResults.value = results.filter(w => w && w.id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error searching warehouses:", error);
|
||||||
|
} finally {
|
||||||
|
isSearchingWarehouses.value = false;
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectWarehouse = (warehouse) => {
|
||||||
|
if (!warehouse || !warehouse.id) return;
|
||||||
|
if (searchTimeout) clearTimeout(searchTimeout);
|
||||||
|
|
||||||
|
formData.value.warehouse_id = warehouse.id;
|
||||||
|
warehouseSearchQuery.value = warehouse.name;
|
||||||
|
showWarehouseResults.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Product Search States
|
||||||
|
const productSearchResults = ref([]);
|
||||||
|
const isSearchingProducts = ref(false);
|
||||||
|
const showProductResults = ref(false);
|
||||||
|
const activeLineIndex = ref(null);
|
||||||
|
const productPackagings = ref({});
|
||||||
|
let productSearchTimeout = null;
|
||||||
|
|
||||||
|
const handleProductSearch = (index) => {
|
||||||
|
activeLineIndex.value = index;
|
||||||
|
const query = formData.value.lines[index].searchQuery;
|
||||||
|
if (query.length < 2) {
|
||||||
|
productSearchResults.value = [];
|
||||||
|
if (productSearchTimeout) clearTimeout(productSearchTimeout);
|
||||||
|
isSearchingProducts.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (productSearchTimeout) clearTimeout(productSearchTimeout);
|
||||||
|
|
||||||
|
productSearchTimeout = setTimeout(async () => {
|
||||||
|
if (formData.value.lines[index].searchQuery !== query) return;
|
||||||
|
|
||||||
|
isSearchingProducts.value = true;
|
||||||
|
showProductResults.value = true;
|
||||||
|
try {
|
||||||
|
const response = await ProductService.searchProducts(query);
|
||||||
|
if (activeLineIndex.value === index &&
|
||||||
|
formData.value.lines[index] &&
|
||||||
|
formData.value.lines[index].searchQuery === query) {
|
||||||
|
|
||||||
|
let results = [];
|
||||||
|
if (response && response.data) {
|
||||||
|
if (Array.isArray(response.data)) {
|
||||||
|
results = response.data;
|
||||||
|
} else if (response.data.data && Array.isArray(response.data.data)) {
|
||||||
|
results = response.data.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
productSearchResults.value = results.filter(p => p && p.id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error searching products:", error);
|
||||||
|
} finally {
|
||||||
|
if (activeLineIndex.value === index) {
|
||||||
|
isSearchingProducts.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectProduct = (index, product) => {
|
||||||
|
if (!product || !product.id) return;
|
||||||
|
if (productSearchTimeout) clearTimeout(productSearchTimeout);
|
||||||
|
|
||||||
|
const line = formData.value.lines[index];
|
||||||
|
if (!line) return;
|
||||||
|
|
||||||
|
line.product_id = product.id;
|
||||||
|
line.searchQuery = product.nom;
|
||||||
|
|
||||||
|
// Load packagings for this product
|
||||||
|
loadProductPackagings(product.id);
|
||||||
|
|
||||||
|
showProductResults.value = false;
|
||||||
|
activeLineIndex.value = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadProductPackagings = async (productId) => {
|
||||||
|
try {
|
||||||
|
const response = await ProductService.getProduct(productId);
|
||||||
|
if (response && response.data) {
|
||||||
|
// Store packagings for this product
|
||||||
|
productPackagings.value[productId] = response.data.conditionnement ? [{
|
||||||
|
id: 1,
|
||||||
|
name: response.data.conditionnement_nom || 'Conditionnement',
|
||||||
|
qty_base: response.data.conditionnement_quantite || 1
|
||||||
|
}] : [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading product packagings:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPackagings = (productId) => {
|
||||||
|
return productPackagings.value[productId] || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close dropdowns on click outside
|
||||||
|
const handleClickOutside = (event) => {
|
||||||
|
const warehouseContainer = document.querySelector('.warehouse-search-container');
|
||||||
|
if (warehouseContainer && !warehouseContainer.contains(event.target)) {
|
||||||
|
showWarehouseResults.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const productContainers = document.querySelectorAll('.product-search-container');
|
||||||
|
let clickedInsideAnyProduct = false;
|
||||||
|
productContainers.forEach(container => {
|
||||||
|
if (container.contains(event.target)) {
|
||||||
|
clickedInsideAnyProduct = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!clickedInsideAnyProduct) {
|
||||||
|
showProductResults.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', handleClickOutside);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('click', handleClickOutside);
|
||||||
|
});
|
||||||
|
|
||||||
|
const formData = ref({
|
||||||
|
purchase_order_id: "",
|
||||||
|
warehouse_id: "",
|
||||||
|
receipt_number: "",
|
||||||
|
receipt_date: new Date().toISOString().split("T")[0],
|
||||||
|
status: "draft",
|
||||||
|
notes: "",
|
||||||
|
lines: [
|
||||||
|
{
|
||||||
|
product_id: "",
|
||||||
|
searchQuery: "",
|
||||||
|
packaging_id: null,
|
||||||
|
packages_qty_received: null,
|
||||||
|
units_qty_received: null,
|
||||||
|
unit_price: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const errors = ref({});
|
||||||
|
|
||||||
|
const addLine = () => {
|
||||||
|
formData.value.lines.push({
|
||||||
|
product_id: "",
|
||||||
|
searchQuery: "",
|
||||||
|
packaging_id: null,
|
||||||
|
packages_qty_received: null,
|
||||||
|
units_qty_received: null,
|
||||||
|
unit_price: null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeLine = (index) => {
|
||||||
|
if (formData.value.lines.length > 1) {
|
||||||
|
formData.value.lines.splice(index, 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelForm = () => {
|
||||||
|
emit("cancel");
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitForm = () => {
|
||||||
|
errors.value = {};
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if (!formData.value.purchase_order_id) {
|
||||||
|
errors.value.purchase_order_id = "La commande fournisseur est requise.";
|
||||||
|
}
|
||||||
|
if (!formData.value.warehouse_id) {
|
||||||
|
errors.value.warehouse_id = "L'entrepôt est requis.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(errors.value).length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
purchase_order_id: parseInt(formData.value.purchase_order_id),
|
||||||
|
warehouse_id: parseInt(formData.value.warehouse_id),
|
||||||
|
receipt_number: formData.value.receipt_number || undefined,
|
||||||
|
receipt_date: formData.value.receipt_date,
|
||||||
|
status: formData.value.status,
|
||||||
|
notes: formData.value.notes || undefined,
|
||||||
|
lines: formData.value.lines.map(line => ({
|
||||||
|
product_id: parseInt(line.product_id),
|
||||||
|
packaging_id: line.packaging_id ? parseInt(line.packaging_id) : null,
|
||||||
|
packages_qty_received: line.packages_qty_received,
|
||||||
|
units_qty_received: line.units_qty_received,
|
||||||
|
unit_price: line.unit_price,
|
||||||
|
})).filter(line => line.product_id),
|
||||||
|
};
|
||||||
|
|
||||||
|
emit("submit", payload);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.reception-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #344767;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title i {
|
||||||
|
color: #cb0c9f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: #adb5bd;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
padding-left: 2.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 1000;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
text-align: left;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #344767;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-details {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-loading,
|
||||||
|
.dropdown-empty {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-arrow {
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: #adb5bd;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select {
|
||||||
|
appearance: none;
|
||||||
|
padding-right: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes-textarea {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lines-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-item {
|
||||||
|
background: white;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #f5365c;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.25rem;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete:hover:not(:disabled) {
|
||||||
|
color: #d63384;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-delete:disabled {
|
||||||
|
color: #adb5bd;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-submit {
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #344767;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-xs {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-danger {
|
||||||
|
color: #f5365c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid-feedback {
|
||||||
|
display: block;
|
||||||
|
color: #f5365c;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-invalid {
|
||||||
|
border-color: #f5365c !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,198 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table id="goods-receipt-list" class="table table-flush">
|
||||||
|
<thead class="thead-light">
|
||||||
|
<tr>
|
||||||
|
<th>Numéro</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Commande</th>
|
||||||
|
<th>Entrepôt</th>
|
||||||
|
<th>Statut</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="receipt in data" :key="receipt.id">
|
||||||
|
<!-- Receipt Number -->
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<span class="my-2 text-xs">{{ receipt.receipt_number }}</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Receipt Date -->
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<span class="my-2 text-xs">{{ formatDate(receipt.receipt_date) }}</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Purchase Order -->
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<span class="my-2 text-xs">
|
||||||
|
{{ receipt.purchase_order?.po_number || receipt.purchase_order_id }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Warehouse -->
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<span class="my-2 text-xs">{{ receipt.warehouse?.name || '-' }}</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Status -->
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<soft-badge :color="getStatusColor(receipt.status)" variant="gradient">
|
||||||
|
{{ getStatusLabel(receipt.status) }}
|
||||||
|
</soft-badge>
|
||||||
|
</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="receipt.id"
|
||||||
|
data-action="view"
|
||||||
|
title="Voir la réception"
|
||||||
|
>
|
||||||
|
<i class="fas fa-eye text-xs" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-link text-info mb-0 px-2"
|
||||||
|
:data-id="receipt.id"
|
||||||
|
data-action="edit"
|
||||||
|
title="Modifier la réception"
|
||||||
|
>
|
||||||
|
<i class="fas fa-edit text-xs" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-link text-danger mb-0 px-2"
|
||||||
|
:data-id="receipt.id"
|
||||||
|
data-action="delete"
|
||||||
|
title="Supprimer la réception"
|
||||||
|
>
|
||||||
|
<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 SoftBadge from "@/components/SoftBadge.vue";
|
||||||
|
|
||||||
|
const emit = defineEmits(["view", "edit", "delete"]);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataTableInstance = ref(null);
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
if (!dateString) return '-';
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString('fr-FR');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'draft':
|
||||||
|
return 'secondary';
|
||||||
|
case 'posted':
|
||||||
|
return 'success';
|
||||||
|
default:
|
||||||
|
return 'info';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusLabel = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'draft':
|
||||||
|
return 'Brouillon';
|
||||||
|
case 'posted':
|
||||||
|
return 'Validée';
|
||||||
|
default:
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initializeDataTable = () => {
|
||||||
|
if (dataTableInstance.value) {
|
||||||
|
dataTableInstance.value.destroy();
|
||||||
|
dataTableInstance.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataTableEl = document.getElementById("goods-receipt-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("goods-receipt-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 === "edit") {
|
||||||
|
emit("edit", 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>
|
||||||
@ -588,7 +588,27 @@ const routes = [
|
|||||||
name: "Statistiques ventes",
|
name: "Statistiques ventes",
|
||||||
component: () => import("@/views/pages/Ventes/Statistiques.vue"),
|
component: () => import("@/views/pages/Ventes/Statistiques.vue"),
|
||||||
},
|
},
|
||||||
// Stock
|
// Stock - Reception
|
||||||
|
{
|
||||||
|
path: "/stock/receptions",
|
||||||
|
name: "Réceptions de marchandises",
|
||||||
|
component: () => import("@/views/pages/Stock/Reception.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/stock/receptions/new",
|
||||||
|
name: "Nouvelle Réception",
|
||||||
|
component: () => import("@/views/pages/Stock/NewReception.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/stock/receptions/:id",
|
||||||
|
name: "Détails Réception",
|
||||||
|
component: () => import("@/views/pages/Stock/ReceptionDetail.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/stock/receptions/:id/edit",
|
||||||
|
name: "Modifier Réception",
|
||||||
|
component: () => import("@/views/pages/Stock/EditReception.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/stock/reception",
|
path: "/stock/reception",
|
||||||
name: "Reception stock",
|
name: "Reception stock",
|
||||||
|
|||||||
@ -4,25 +4,50 @@ export interface GoodsReceiptLine {
|
|||||||
id: number;
|
id: number;
|
||||||
goods_receipt_id: number;
|
goods_receipt_id: number;
|
||||||
product_id: number;
|
product_id: number;
|
||||||
purchase_order_line_id: number | null;
|
packaging_id: number | null;
|
||||||
quantity_received: number;
|
packages_qty_received: number | null;
|
||||||
|
units_qty_received: number | null;
|
||||||
|
qty_received_base: number | null;
|
||||||
unit_price: number | null;
|
unit_price: number | null;
|
||||||
notes: string | null;
|
unit_price_per_package: number | null;
|
||||||
|
tva_rate_id: number | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
product?: any;
|
product?: {
|
||||||
|
id: number;
|
||||||
|
nom: string;
|
||||||
|
reference: string;
|
||||||
|
};
|
||||||
|
packaging?: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
qty_base: number;
|
||||||
|
};
|
||||||
|
tva_rate?: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
rate: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GoodsReceipt {
|
export interface GoodsReceipt {
|
||||||
id: number;
|
id: number;
|
||||||
purchase_order_id: number;
|
purchase_order_id: number;
|
||||||
|
warehouse_id: number;
|
||||||
receipt_number: string;
|
receipt_number: string;
|
||||||
receipt_date: string;
|
receipt_date: string;
|
||||||
status: 'brouillon' | 'valide' | 'annule';
|
status: 'draft' | 'posted';
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
purchase_order?: any;
|
purchase_order?: {
|
||||||
|
id: number;
|
||||||
|
po_number: string;
|
||||||
|
};
|
||||||
|
warehouse?: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
lines?: GoodsReceiptLine[];
|
lines?: GoodsReceiptLine[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,14 +67,18 @@ export interface GoodsReceiptResponse {
|
|||||||
|
|
||||||
export interface CreateGoodsReceiptLinePayload {
|
export interface CreateGoodsReceiptLinePayload {
|
||||||
product_id: number;
|
product_id: number;
|
||||||
purchase_order_line_id?: number | null;
|
packaging_id?: number | null;
|
||||||
quantity_received: number;
|
packages_qty_received?: number | null;
|
||||||
|
units_qty_received?: number | null;
|
||||||
|
qty_received_base?: number | null;
|
||||||
unit_price?: number | null;
|
unit_price?: number | null;
|
||||||
notes?: string | null;
|
unit_price_per_package?: number | null;
|
||||||
|
tva_rate_id?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateGoodsReceiptPayload {
|
export interface CreateGoodsReceiptPayload {
|
||||||
purchase_order_id: number;
|
purchase_order_id: number;
|
||||||
|
warehouse_id: number;
|
||||||
receipt_number?: string;
|
receipt_number?: string;
|
||||||
receipt_date?: string;
|
receipt_date?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
@ -111,14 +140,6 @@ export const GoodsReceiptService = {
|
|||||||
});
|
});
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
|
|
||||||
async getByPurchaseOrder(purchaseOrderId: number): Promise<GoodsReceiptListResponse> {
|
|
||||||
const response = await request<GoodsReceiptListResponse>({
|
|
||||||
url: `/api/purchase-orders/${purchaseOrderId}/goods-receipts`,
|
|
||||||
method: "get",
|
|
||||||
});
|
|
||||||
return response;
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GoodsReceiptService;
|
export default GoodsReceiptService;
|
||||||
|
|||||||
73
thanasoft-front/src/services/tvaRate.ts
Normal file
73
thanasoft-front/src/services/tvaRate.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { request } from "./http";
|
||||||
|
|
||||||
|
export interface TvaRate {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
rate: number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TvaRateListResponse {
|
||||||
|
data: TvaRate[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TvaRateResponse {
|
||||||
|
data: TvaRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateTvaRatePayload {
|
||||||
|
name: string;
|
||||||
|
rate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateTvaRatePayload extends Partial<CreateTvaRatePayload> {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TvaRateService = {
|
||||||
|
async getAllTvaRates(): Promise<TvaRateListResponse> {
|
||||||
|
const response = await request<TvaRateListResponse>({
|
||||||
|
url: "/api/tva-rates",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getTvaRate(id: number): Promise<TvaRateResponse> {
|
||||||
|
const response = await request<TvaRateResponse>({
|
||||||
|
url: `/api/tva-rates/${id}`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
async createTvaRate(payload: CreateTvaRatePayload): Promise<TvaRateResponse> {
|
||||||
|
const response = await request<TvaRateResponse>({
|
||||||
|
url: "/api/tva-rates",
|
||||||
|
method: "post",
|
||||||
|
data: payload,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateTvaRate(payload: UpdateTvaRatePayload): Promise<TvaRateResponse> {
|
||||||
|
const { id, ...updateData } = payload;
|
||||||
|
const response = await request<TvaRateResponse>({
|
||||||
|
url: `/api/tva-rates/${id}`,
|
||||||
|
method: "put",
|
||||||
|
data: updateData,
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteTvaRate(id: number): Promise<{ success: boolean; message: string }> {
|
||||||
|
const response = await request<{ success: boolean; message: string }>({
|
||||||
|
url: `/api/tva-rates/${id}`,
|
||||||
|
method: "delete",
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TvaRateService;
|
||||||
@ -58,6 +58,30 @@ class WarehouseService {
|
|||||||
method: "delete",
|
method: "delete",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search warehouses by name
|
||||||
|
*/
|
||||||
|
async searchWarehouses(
|
||||||
|
query: string,
|
||||||
|
params?: {
|
||||||
|
exact_match?: boolean;
|
||||||
|
}
|
||||||
|
): Promise<Warehouse[]> {
|
||||||
|
const response = await request<{
|
||||||
|
data: Warehouse[];
|
||||||
|
count: number;
|
||||||
|
message: string;
|
||||||
|
}>({
|
||||||
|
url: "/api/warehouses/searchBy",
|
||||||
|
method: "get",
|
||||||
|
params: {
|
||||||
|
name: query,
|
||||||
|
exact_match: params?.exact_match || false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WarehouseService;
|
export default WarehouseService;
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
import GoodsReceiptService from "@/services/goodsReceipt";
|
import GoodsReceiptService, {
|
||||||
|
|
||||||
import type {
|
|
||||||
GoodsReceipt,
|
GoodsReceipt,
|
||||||
CreateGoodsReceiptPayload,
|
CreateGoodsReceiptPayload,
|
||||||
UpdateGoodsReceiptPayload,
|
UpdateGoodsReceiptPayload,
|
||||||
@ -23,13 +21,10 @@ export const useGoodsReceiptStore = defineStore("goodsReceipt", () => {
|
|||||||
|
|
||||||
const allGoodsReceipts = computed(() => goodsReceipts.value);
|
const allGoodsReceipts = computed(() => goodsReceipts.value);
|
||||||
const draftReceipts = computed(() =>
|
const draftReceipts = computed(() =>
|
||||||
goodsReceipts.value.filter((receipt) => receipt.status === "brouillon")
|
goodsReceipts.value.filter((receipt) => receipt.status === "draft")
|
||||||
);
|
);
|
||||||
const validatedReceipts = computed(() =>
|
const postedReceipts = computed(() =>
|
||||||
goodsReceipts.value.filter((receipt) => receipt.status === "valide")
|
goodsReceipts.value.filter((receipt) => receipt.status === "posted")
|
||||||
);
|
|
||||||
const cancelledReceipts = computed(() =>
|
|
||||||
goodsReceipts.value.filter((receipt) => receipt.status === "annule")
|
|
||||||
);
|
);
|
||||||
const isLoading = computed(() => loading.value);
|
const isLoading = computed(() => loading.value);
|
||||||
const hasError = computed(() => error.value !== null);
|
const hasError = computed(() => error.value !== null);
|
||||||
@ -91,7 +86,7 @@ export const useGoodsReceiptStore = defineStore("goodsReceipt", () => {
|
|||||||
const errorMessage =
|
const errorMessage =
|
||||||
err.response?.data?.message ||
|
err.response?.data?.message ||
|
||||||
err.message ||
|
err.message ||
|
||||||
"Erreur lors de la récupération des réceptions";
|
"Erreur lors de la récupération des réceptions de marchandises";
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
@ -111,7 +106,7 @@ export const useGoodsReceiptStore = defineStore("goodsReceipt", () => {
|
|||||||
const errorMessage =
|
const errorMessage =
|
||||||
err.response?.data?.message ||
|
err.response?.data?.message ||
|
||||||
err.message ||
|
err.message ||
|
||||||
"Erreur lors de la récupération de la réception";
|
"Erreur lors de la récupération de la réception de marchandise";
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
@ -132,7 +127,7 @@ export const useGoodsReceiptStore = defineStore("goodsReceipt", () => {
|
|||||||
const errorMessage =
|
const errorMessage =
|
||||||
err.response?.data?.message ||
|
err.response?.data?.message ||
|
||||||
err.message ||
|
err.message ||
|
||||||
"Erreur lors de la création de la réception";
|
"Erreur lors de la création de la réception de marchandise";
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
@ -167,7 +162,7 @@ export const useGoodsReceiptStore = defineStore("goodsReceipt", () => {
|
|||||||
const errorMessage =
|
const errorMessage =
|
||||||
err.response?.data?.message ||
|
err.response?.data?.message ||
|
||||||
err.message ||
|
err.message ||
|
||||||
"Erreur lors de la mise à jour de la réception";
|
"Erreur lors de la mise à jour de la réception de marchandise";
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
@ -195,27 +190,7 @@ export const useGoodsReceiptStore = defineStore("goodsReceipt", () => {
|
|||||||
const errorMessage =
|
const errorMessage =
|
||||||
err.response?.data?.message ||
|
err.response?.data?.message ||
|
||||||
err.message ||
|
err.message ||
|
||||||
"Erreur lors de la suppression de la réception";
|
"Erreur lors de la suppression de la réception de marchandise";
|
||||||
setError(errorMessage);
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchByPurchaseOrder = async (purchaseOrderId: number) => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await GoodsReceiptService.getByPurchaseOrder(purchaseOrderId);
|
|
||||||
setGoodsReceipts(response.data);
|
|
||||||
return response.data;
|
|
||||||
} catch (err: any) {
|
|
||||||
const errorMessage =
|
|
||||||
err.response?.data?.message ||
|
|
||||||
err.message ||
|
|
||||||
"Erreur lors de la récupération des réceptions de la commande";
|
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
@ -247,8 +222,7 @@ export const useGoodsReceiptStore = defineStore("goodsReceipt", () => {
|
|||||||
|
|
||||||
allGoodsReceipts,
|
allGoodsReceipts,
|
||||||
draftReceipts,
|
draftReceipts,
|
||||||
validatedReceipts,
|
postedReceipts,
|
||||||
cancelledReceipts,
|
|
||||||
isLoading,
|
isLoading,
|
||||||
hasError,
|
hasError,
|
||||||
getError,
|
getError,
|
||||||
@ -260,7 +234,6 @@ export const useGoodsReceiptStore = defineStore("goodsReceipt", () => {
|
|||||||
createGoodsReceipt,
|
createGoodsReceipt,
|
||||||
updateGoodsReceipt,
|
updateGoodsReceipt,
|
||||||
deleteGoodsReceipt,
|
deleteGoodsReceipt,
|
||||||
fetchByPurchaseOrder,
|
|
||||||
clearCurrentGoodsReceipt,
|
clearCurrentGoodsReceipt,
|
||||||
clearStore,
|
clearStore,
|
||||||
clearError,
|
clearError,
|
||||||
|
|||||||
210
thanasoft-front/src/views/pages/Stock/EditReception.vue
Normal file
210
thanasoft-front/src/views/pages/Stock/EditReception.vue
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<div class="d-sm-flex justify-content-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-0">Modifier la Réception de Marchandise</h5>
|
||||||
|
<p class="text-sm mb-0">Modifier les informations d'une réception existante.</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-sm-0 mt-3">
|
||||||
|
<soft-button color="secondary" variant="gradient" class="me-2" @click="handleCancel">
|
||||||
|
<i class="fas fa-times me-2"></i> Annuler
|
||||||
|
</soft-button>
|
||||||
|
<soft-button color="info" variant="gradient" @click="handleSubmit" :loading="loading">
|
||||||
|
<i class="fas fa-save me-2"></i> Enregistrer
|
||||||
|
</soft-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="goodsReceipt">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<form @submit.prevent="handleSubmit">
|
||||||
|
<!-- Main Info -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="purchase_order_id" class="form-control-label">Commande Fournisseur</label>
|
||||||
|
<select
|
||||||
|
id="purchase_order_id"
|
||||||
|
class="form-control"
|
||||||
|
v-model="form.purchase_order_id"
|
||||||
|
:class="{ 'is-invalid': errors.purchase_order_id }"
|
||||||
|
>
|
||||||
|
<option value="">Sélectionner une commande</option>
|
||||||
|
<option v-for="po in purchaseOrders" :key="po.id" :value="po.id">
|
||||||
|
{{ po.po_number }} - {{ po.fournisseur?.nom || 'Fournisseur ' + po.fournisseur_id }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<div class="invalid-feedback">{{ errors.purchase_order_id }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="warehouse_id" class="form-control-label">Entrepôt de Destination</label>
|
||||||
|
<select
|
||||||
|
id="warehouse_id"
|
||||||
|
class="form-control"
|
||||||
|
v-model="form.warehouse_id"
|
||||||
|
:class="{ 'is-invalid': errors.warehouse_id }"
|
||||||
|
>
|
||||||
|
<option value="">Sélectionner un entrepôt</option>
|
||||||
|
<option v-for="wh in warehouses" :key="wh.id" :value="wh.id">
|
||||||
|
{{ wh.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<div class="invalid-feedback">{{ errors.warehouse_id }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="receipt_number" class="form-control-label">Numéro de Réception</label>
|
||||||
|
<input
|
||||||
|
id="receipt_number"
|
||||||
|
class="form-control"
|
||||||
|
type="text"
|
||||||
|
v-model="form.receipt_number"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="receipt_date" class="form-control-label">Date de Réception</label>
|
||||||
|
<input
|
||||||
|
id="receipt_date"
|
||||||
|
class="form-control"
|
||||||
|
type="date"
|
||||||
|
v-model="form.receipt_date"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="status" class="form-control-label">Statut</label>
|
||||||
|
<select id="status" class="form-control" v-model="form.status">
|
||||||
|
<option value="draft">Brouillon</option>
|
||||||
|
<option value="posted">Validée</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="notes" class="form-control-label">Notes</label>
|
||||||
|
<textarea
|
||||||
|
id="notes"
|
||||||
|
class="form-control"
|
||||||
|
rows="3"
|
||||||
|
v-model="form.notes"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="d-flex justify-content-center py-4">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
import { useGoodsReceiptStore } from "@/stores/goodsReceiptStore";
|
||||||
|
import { useWarehouseStore } from "@/stores/warehouseStore";
|
||||||
|
import { usePurchaseOrderStore } from "@/stores/purchaseOrderStore";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const goodsReceiptStore = useGoodsReceiptStore();
|
||||||
|
const warehouseStore = useWarehouseStore();
|
||||||
|
const purchaseOrderStore = usePurchaseOrderStore();
|
||||||
|
|
||||||
|
const { currentGoodsReceipt: goodsReceipt, loading } = storeToRefs(goodsReceiptStore);
|
||||||
|
|
||||||
|
const warehouses = ref([]);
|
||||||
|
const purchaseOrders = ref([]);
|
||||||
|
const errors = ref({});
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
purchase_order_id: "",
|
||||||
|
warehouse_id: "",
|
||||||
|
receipt_number: "",
|
||||||
|
receipt_date: "",
|
||||||
|
status: "draft",
|
||||||
|
notes: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
router.push(`/stock/receptions/${route.params.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
errors.value = {};
|
||||||
|
|
||||||
|
if (!form.purchase_order_id) {
|
||||||
|
errors.value.purchase_order_id = "La commande fournisseur est requise.";
|
||||||
|
}
|
||||||
|
if (!form.warehouse_id) {
|
||||||
|
errors.value.warehouse_id = "L'entrepôt est requis.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(errors.value).length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
id: parseInt(route.params.id),
|
||||||
|
purchase_order_id: parseInt(form.purchase_order_id),
|
||||||
|
warehouse_id: parseInt(form.warehouse_id),
|
||||||
|
receipt_number: form.receipt_number,
|
||||||
|
receipt_date: form.receipt_date,
|
||||||
|
status: form.status,
|
||||||
|
notes: form.notes || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
await goodsReceiptStore.updateGoodsReceipt(payload);
|
||||||
|
router.push(`/stock/receptions/${route.params.id}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update goods receipt", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
warehouseStore.fetchWarehouses(),
|
||||||
|
purchaseOrderStore.fetchPurchaseOrders(),
|
||||||
|
goodsReceiptStore.fetchGoodsReceipt(parseInt(route.params.id)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
warehouses.value = warehouseStore.warehouses;
|
||||||
|
purchaseOrders.value = purchaseOrderStore.purchaseOrders;
|
||||||
|
|
||||||
|
if (goodsReceipt.value) {
|
||||||
|
form.purchase_order_id = goodsReceipt.value.purchase_order_id;
|
||||||
|
form.warehouse_id = goodsReceipt.value.warehouse_id;
|
||||||
|
form.receipt_number = goodsReceipt.value.receipt_number;
|
||||||
|
form.receipt_date = goodsReceipt.value.receipt_date;
|
||||||
|
form.status = goodsReceipt.value.status;
|
||||||
|
form.notes = goodsReceipt.value.notes || "";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load data", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
7
thanasoft-front/src/views/pages/Stock/NewReception.vue
Normal file
7
thanasoft-front/src/views/pages/Stock/NewReception.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<new-reception-presentation />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import NewReceptionPresentation from "@/components/Organism/Stock/NewReceptionPresentation.vue";
|
||||||
|
</script>
|
||||||
@ -1,11 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<goods-receipt-list-presentation />
|
||||||
<h1>Reception stock</h1>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import GoodsReceiptListPresentation from "@/components/Organism/Stock/GoodsReceiptListPresentation.vue";
|
||||||
name: "ReceptionStock",
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<reception-detail-presentation />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import ReceptionDetailPresentation from "@/components/Organism/Stock/ReceptionDetailPresentation.vue";
|
||||||
|
</script>
|
||||||
Loading…
x
Reference in New Issue
Block a user