add: devoluciones wip
This commit is contained in:
parent
2c8189ca59
commit
b9e694f6ca
1
.gitignore
vendored
1
.gitignore
vendored
@ -24,3 +24,4 @@ yarn-error.log
|
|||||||
/.nova
|
/.nova
|
||||||
/.vscode
|
/.vscode
|
||||||
/.zed
|
/.zed
|
||||||
|
CLAUDE.md
|
||||||
|
|||||||
136
app/Http/Controllers/App/ReturnController.php
Normal file
136
app/Http/Controllers/App/ReturnController.php
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\App;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\App\ReturnStoreRequest;
|
||||||
|
use App\Services\ReturnService;
|
||||||
|
use App\Models\Returns;
|
||||||
|
use App\Models\Sale;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||||
|
|
||||||
|
class ReturnController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected ReturnService $returnService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listar devoluciones
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$query = Returns::with([
|
||||||
|
'sale',
|
||||||
|
'user',
|
||||||
|
'cashRegister',
|
||||||
|
'details.inventory',
|
||||||
|
])->orderBy('created_at', 'desc');
|
||||||
|
|
||||||
|
// Filtros
|
||||||
|
if ($request->has('q') && $request->q) {
|
||||||
|
$query->where('return_number', 'like', "%{$request->q}%")
|
||||||
|
->orWhereHas('sale', fn ($q) =>
|
||||||
|
$q->where('invoice_number', 'like', "%{$request->q}%")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('sale_id')) {
|
||||||
|
$query->where('sale_id', $request->sale_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('reason')) {
|
||||||
|
$query->where('reason', $request->reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('cash_register_id')) {
|
||||||
|
$query->where('cash_register_id', $request->cash_register_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('from') && $request->has('to')) {
|
||||||
|
$query->whereBetween('created_at', [$request->from, $request->to]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$returns = $query->paginate(config('app.pagination', 15));
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'returns' => $returns,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ver detalle de una devolución
|
||||||
|
*/
|
||||||
|
public function show(Returns $return)
|
||||||
|
{
|
||||||
|
$return->load([
|
||||||
|
'details.inventory',
|
||||||
|
'details.serials',
|
||||||
|
'sale.details',
|
||||||
|
'user',
|
||||||
|
'cashRegister',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'model' => $return,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crear nueva devolución
|
||||||
|
*/
|
||||||
|
public function store(ReturnStoreRequest $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$return = $this->returnService->createReturn($request->validated());
|
||||||
|
|
||||||
|
return ApiResponse::CREATED->response([
|
||||||
|
'model' => $return,
|
||||||
|
'message' => 'Devolución procesada exitosamente. Reembolso: $'.number_format($return->total, 2),
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ApiResponse::BAD_REQUEST->response([
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancelar una devolución (caso excepcional)
|
||||||
|
*/
|
||||||
|
public function cancel(Returns $return)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$cancelledReturn = $this->returnService->cancelReturn($return);
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'model' => $cancelledReturn,
|
||||||
|
'message' => 'Devolución cancelada exitosamente. Productos restaurados a vendido.',
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ApiResponse::BAD_REQUEST->response([
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtener items elegibles para devolución de una venta
|
||||||
|
*/
|
||||||
|
public function returnable(Sale $sale)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$returnableItems = $this->returnService->getReturnableItems($sale);
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'sale' => $sale->load('cashRegister'),
|
||||||
|
'returnable_items' => $returnableItems,
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return ApiResponse::BAD_REQUEST->response([
|
||||||
|
'message' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
152
app/Http/Requests/App/ReturnStoreRequest.php
Normal file
152
app/Http/Requests/App/ReturnStoreRequest.php
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\App;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use App\Models\Sale;
|
||||||
|
use App\Models\SaleDetail;
|
||||||
|
use App\Models\ReturnDetail;
|
||||||
|
use App\Models\InventorySerial;
|
||||||
|
|
||||||
|
class ReturnStoreRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'sale_id' => ['required', 'exists:sales,id'],
|
||||||
|
'user_id' => ['required', 'exists:users,id'],
|
||||||
|
'cash_register_id' => ['nullable', 'exists:cash_registers,id'],
|
||||||
|
'reason' => ['required', 'in:defective,wrong_product,change_of_mind,damaged,other'],
|
||||||
|
'refund_method' => ['nullable', 'in:cash,credit_card,debit_card'],
|
||||||
|
'notes' => ['nullable', 'string', 'max:500'],
|
||||||
|
|
||||||
|
// Items a devolver
|
||||||
|
'items' => ['required', 'array', 'min:1'],
|
||||||
|
'items.*.sale_detail_id' => ['required', 'exists:sale_details,id'],
|
||||||
|
'items.*.quantity_returned' => ['required', 'integer', 'min:1'],
|
||||||
|
'items.*.serial_numbers' => ['nullable', 'array'],
|
||||||
|
'items.*.serial_numbers.*' => ['string', 'exists:inventory_serials,serial_number'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'sale_id.required' => 'La venta es obligatoria.',
|
||||||
|
'sale_id.exists' => 'La venta seleccionada no existe.',
|
||||||
|
'reason.required' => 'El motivo de devolución es obligatorio.',
|
||||||
|
'reason.in' => 'El motivo debe ser: defectuoso, producto incorrecto, cambio de opinión, dañado u otro.',
|
||||||
|
'refund_method.in' => 'El método de reembolso debe ser: efectivo, tarjeta de crédito o débito.',
|
||||||
|
|
||||||
|
'items.required' => 'Debe incluir al menos un producto a devolver.',
|
||||||
|
'items.min' => 'Debe incluir al menos un producto.',
|
||||||
|
'items.*.sale_detail_id.required' => 'El detalle de venta es obligatorio.',
|
||||||
|
'items.*.sale_detail_id.exists' => 'El detalle de venta no existe.',
|
||||||
|
'items.*.quantity_returned.required' => 'La cantidad devuelta es obligatoria.',
|
||||||
|
'items.*.quantity_returned.integer' => 'La cantidad debe ser un número entero.',
|
||||||
|
'items.*.quantity_returned.min' => 'La cantidad debe ser al menos 1.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withValidator($validator)
|
||||||
|
{
|
||||||
|
$validator->after(function ($validator) {
|
||||||
|
// 1. Validar que la venta exista y esté completada
|
||||||
|
$sale = Sale::find($this->sale_id);
|
||||||
|
|
||||||
|
if (!$sale) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sale->status !== 'completed') {
|
||||||
|
$validator->errors()->add(
|
||||||
|
'sale_id',
|
||||||
|
'Solo se pueden devolver productos de ventas completadas.'
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Validar que refund_method coincida con payment_method (si se especifica)
|
||||||
|
if ($this->refund_method && $this->refund_method !== $sale->payment_method) {
|
||||||
|
$validator->errors()->add(
|
||||||
|
'refund_method',
|
||||||
|
"El método de reembolso debe coincidir con el método de pago original ({$sale->payment_method})."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Validar cada item
|
||||||
|
foreach ($this->items ?? [] as $index => $item) {
|
||||||
|
$saleDetail = SaleDetail::find($item['sale_detail_id']);
|
||||||
|
|
||||||
|
if (!$saleDetail) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar que el sale_detail pertenece a la venta
|
||||||
|
if ($saleDetail->sale_id !== $sale->id) {
|
||||||
|
$validator->errors()->add(
|
||||||
|
"items.{$index}.sale_detail_id",
|
||||||
|
'El producto no pertenece a esta venta.'
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar cantidades
|
||||||
|
$alreadyReturned = ReturnDetail::where('sale_detail_id', $saleDetail->id)
|
||||||
|
->sum('quantity_returned');
|
||||||
|
|
||||||
|
$maxReturnable = $saleDetail->quantity - $alreadyReturned;
|
||||||
|
|
||||||
|
if ($item['quantity_returned'] > $maxReturnable) {
|
||||||
|
$validator->errors()->add(
|
||||||
|
"items.{$index}.quantity_returned",
|
||||||
|
"Solo puedes devolver {$maxReturnable} unidades de {$saleDetail->product_name}. " .
|
||||||
|
"Ya se devolvieron {$alreadyReturned} de {$saleDetail->quantity} vendidas."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar seriales si fueron especificados
|
||||||
|
if (isset($item['serial_numbers'])) {
|
||||||
|
if (count($item['serial_numbers']) !== $item['quantity_returned']) {
|
||||||
|
$validator->errors()->add(
|
||||||
|
"items.{$index}.serial_numbers",
|
||||||
|
"Debes especificar exactamente {$item['quantity_returned']} números de serie."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($item['serial_numbers'] as $serialIndex => $serialNumber) {
|
||||||
|
$serial = InventorySerial::where('serial_number', $serialNumber)
|
||||||
|
->where('sale_detail_id', $saleDetail->id)
|
||||||
|
->where('status', 'vendido')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$serial) {
|
||||||
|
$validator->errors()->add(
|
||||||
|
"items.{$index}.serial_numbers.{$serialIndex}",
|
||||||
|
"El serial {$serialNumber} no pertenece a esta venta o ya fue devuelto."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Validar caja registradora si se especificó
|
||||||
|
if ($this->cash_register_id) {
|
||||||
|
$cashRegister = \App\Models\CashRegister::find($this->cash_register_id);
|
||||||
|
|
||||||
|
if ($cashRegister && !$cashRegister->isOpen()) {
|
||||||
|
$validator->errors()->add(
|
||||||
|
'cash_register_id',
|
||||||
|
'La caja registradora debe estar abierta para procesar devoluciones.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -16,6 +16,10 @@ class CashRegister extends Model
|
|||||||
'difference',
|
'difference',
|
||||||
'total_sales',
|
'total_sales',
|
||||||
'sales_count',
|
'sales_count',
|
||||||
|
'total_returns',
|
||||||
|
'cash_returns',
|
||||||
|
'card_returns',
|
||||||
|
'returns_count',
|
||||||
'notes',
|
'notes',
|
||||||
'status',
|
'status',
|
||||||
];
|
];
|
||||||
@ -28,6 +32,9 @@ class CashRegister extends Model
|
|||||||
'expected_cash' => 'decimal:2',
|
'expected_cash' => 'decimal:2',
|
||||||
'difference' => 'decimal:2',
|
'difference' => 'decimal:2',
|
||||||
'total_sales' => 'decimal:2',
|
'total_sales' => 'decimal:2',
|
||||||
|
'total_returns' => 'decimal:2',
|
||||||
|
'cash_returns' => 'decimal:2',
|
||||||
|
'card_returns' => 'decimal:2',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function user()
|
public function user()
|
||||||
@ -40,6 +47,11 @@ public function sales()
|
|||||||
return $this->hasMany(Sale::class);
|
return $this->hasMany(Sale::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function returns()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Returns::class, 'cash_register_id');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verificar si la caja está abierta
|
* Verificar si la caja está abierta
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -15,6 +15,7 @@ class InventorySerial extends Model
|
|||||||
'serial_number',
|
'serial_number',
|
||||||
'status',
|
'status',
|
||||||
'sale_detail_id',
|
'sale_detail_id',
|
||||||
|
'return_detail_id',
|
||||||
'notes',
|
'notes',
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -32,6 +33,11 @@ public function saleDetail()
|
|||||||
return $this->belongsTo(SaleDetail::class);
|
return $this->belongsTo(SaleDetail::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function returnDetail()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ReturnDetail::class);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verificar si el serial está disponible
|
* Verificar si el serial está disponible
|
||||||
*/
|
*/
|
||||||
@ -61,4 +67,35 @@ public function markAsAvailable(): void
|
|||||||
'sale_detail_id' => null,
|
'sale_detail_id' => null,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marcar como devuelto
|
||||||
|
*/
|
||||||
|
public function markAsReturned(int $returnDetailId): void
|
||||||
|
{
|
||||||
|
$this->update([
|
||||||
|
'status' => 'devuelto',
|
||||||
|
'return_detail_id' => $returnDetailId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restaurar a disponible desde devuelto
|
||||||
|
*/
|
||||||
|
public function restoreFromReturn(): void
|
||||||
|
{
|
||||||
|
$this->update([
|
||||||
|
'status' => 'disponible',
|
||||||
|
'sale_detail_id' => null,
|
||||||
|
'return_detail_id' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verificar si el serial está devuelto
|
||||||
|
*/
|
||||||
|
public function isReturned(): bool
|
||||||
|
{
|
||||||
|
return $this->status === 'devuelto';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
62
app/Models/ReturnDetail.php
Normal file
62
app/Models/ReturnDetail.php
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?php namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class ReturnDetail extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'return_id',
|
||||||
|
'sale_detail_id',
|
||||||
|
'inventory_id',
|
||||||
|
'product_name',
|
||||||
|
'quantity_returned',
|
||||||
|
'unit_price',
|
||||||
|
'subtotal',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'unit_price' => 'decimal:2',
|
||||||
|
'subtotal' => 'decimal:2',
|
||||||
|
'quantity_returned' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relación con devolución principal
|
||||||
|
*/
|
||||||
|
public function return()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Returns::class, 'return_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relación con detalle de venta original
|
||||||
|
*/
|
||||||
|
public function saleDetail()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(SaleDetail::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relación con inventario
|
||||||
|
*/
|
||||||
|
public function inventory()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Inventory::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seriales devueltos en este detalle
|
||||||
|
*/
|
||||||
|
public function serials()
|
||||||
|
{
|
||||||
|
return $this->hasMany(InventorySerial::class, 'return_detail_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtener números de serie devueltos
|
||||||
|
*/
|
||||||
|
public function getSerialNumbersAttribute(): array
|
||||||
|
{
|
||||||
|
return $this->serials()->pluck('serial_number')->toArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
76
app/Models/Returns.php
Normal file
76
app/Models/Returns.php
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<?php namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
class Returns extends Model
|
||||||
|
{
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'sale_id',
|
||||||
|
'user_id',
|
||||||
|
'cash_register_id',
|
||||||
|
'return_number',
|
||||||
|
'subtotal',
|
||||||
|
'tax',
|
||||||
|
'total',
|
||||||
|
'refund_method',
|
||||||
|
'reason',
|
||||||
|
'notes'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'subtotal' => 'decimal:2',
|
||||||
|
'tax' => 'decimal:2',
|
||||||
|
'total' => 'decimal:2',
|
||||||
|
'created_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relación con la venta original
|
||||||
|
*/
|
||||||
|
public function sale()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Sale::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relación con usuario que procesó la devolución
|
||||||
|
*/
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relación con caja registradora
|
||||||
|
*/
|
||||||
|
public function cashRegister()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(CashRegister::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detalles de la devolución
|
||||||
|
*/
|
||||||
|
public function details()
|
||||||
|
{
|
||||||
|
return $this->hasMany(ReturnDetail::class, 'return_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtener texto descriptivo de la razón
|
||||||
|
*/
|
||||||
|
public function getReasonTextAttribute(): string
|
||||||
|
{
|
||||||
|
return match($this->reason) {
|
||||||
|
'defective' => 'Producto defectuoso',
|
||||||
|
'wrong_product' => 'Producto incorrecto',
|
||||||
|
'change_of_mind' => 'Cambio de opinión',
|
||||||
|
'damaged' => 'Producto dañado',
|
||||||
|
'other' => 'Otra razón',
|
||||||
|
default => 'No especificado',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -56,4 +56,36 @@ public function client()
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(Client::class);
|
return $this->belongsTo(Client::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Devoluciones asociadas a esta venta
|
||||||
|
*/
|
||||||
|
public function returns()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Returns::class, 'sale_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total devuelto de esta venta
|
||||||
|
*/
|
||||||
|
public function getTotalReturnedAttribute(): float
|
||||||
|
{
|
||||||
|
return (float) $this->returns()->sum('total');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verificar si la venta tiene devoluciones
|
||||||
|
*/
|
||||||
|
public function hasReturns(): bool
|
||||||
|
{
|
||||||
|
return $this->returns()->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total neto (total - devoluciones)
|
||||||
|
*/
|
||||||
|
public function getNetTotalAttribute(): float
|
||||||
|
{
|
||||||
|
return (float) ($this->total - $this->getTotalReturnedAttribute());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,4 +51,44 @@ public function getSerialNumbersAttribute(): array
|
|||||||
{
|
{
|
||||||
return $this->serials()->pluck('serial_number')->toArray();
|
return $this->serials()->pluck('serial_number')->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Devoluciones de este detalle
|
||||||
|
*/
|
||||||
|
public function returnDetails()
|
||||||
|
{
|
||||||
|
return $this->hasMany(ReturnDetail::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cantidad total devuelta
|
||||||
|
*/
|
||||||
|
public function getQuantityReturnedAttribute(): int
|
||||||
|
{
|
||||||
|
return $this->returnDetails()->sum('quantity_returned');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cantidad restante (no devuelta)
|
||||||
|
*/
|
||||||
|
public function getQuantityRemainingAttribute(): int
|
||||||
|
{
|
||||||
|
return $this->quantity - $this->getQuantityReturnedAttribute();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verificar si se puede devolver más
|
||||||
|
*/
|
||||||
|
public function canReturn(): bool
|
||||||
|
{
|
||||||
|
return $this->getQuantityRemainingAttribute() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cantidad máxima que se puede devolver
|
||||||
|
*/
|
||||||
|
public function getMaxReturnableQuantityAttribute(): int
|
||||||
|
{
|
||||||
|
return $this->getQuantityRemainingAttribute();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
use App\Models\CashRegister;
|
use App\Models\CashRegister;
|
||||||
use App\Models\Sale;
|
use App\Models\Sale;
|
||||||
|
use App\Models\Returns;
|
||||||
|
|
||||||
class CashRegisterService
|
class CashRegisterService
|
||||||
{
|
{
|
||||||
@ -38,19 +39,29 @@ public function closeRegister(CashRegister $register, array $data)
|
|||||||
->where('status', 'completed')
|
->where('status', 'completed')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
// Calcular devoluciones
|
||||||
|
$returns = Returns::where('cash_register_id', $register->id)->get();
|
||||||
|
|
||||||
// Calcular efectivo real (recibido - devuelto)
|
// Calcular efectivo real (recibido - devuelto)
|
||||||
$cashSales = $sales->where('payment_method', 'cash')
|
$cashSales = $sales->where('payment_method', 'cash')
|
||||||
->sum(function ($sale) {
|
->sum(function ($sale) {
|
||||||
return ($sale->cash_received ?? 0) - ($sale->change ?? 0);
|
return ($sale->cash_received ?? 0) - ($sale->change ?? 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Efectivo de devoluciones
|
||||||
|
$cashReturns = $returns->where('refund_method', 'cash')->sum('total');
|
||||||
|
|
||||||
|
// Devoluciones por tarjeta
|
||||||
|
$cardReturns = $returns->whereIn('refund_method', ['credit_card', 'debit_card'])->sum('total');
|
||||||
|
|
||||||
$totalCashReceived = $sales->where('payment_method', 'cash')->sum('cash_received');
|
$totalCashReceived = $sales->where('payment_method', 'cash')->sum('cash_received');
|
||||||
$totalChangeGiven = $sales->where('payment_method', 'cash')->sum('change');
|
$totalChangeGiven = $sales->where('payment_method', 'cash')->sum('change');
|
||||||
$cardSales = $sales->whereIn('payment_method', ['credit_card', 'debit_card'])->sum('total');
|
$cardSales = $sales->whereIn('payment_method', ['credit_card', 'debit_card'])->sum('total');
|
||||||
$totalSales = $sales->sum('total');
|
$totalSales = $sales->sum('total');
|
||||||
|
$totalReturns = $returns->sum('total');
|
||||||
|
|
||||||
// Efectivo esperado
|
// Efectivo esperado (ajustado por devoluciones)
|
||||||
$expectedCash = $register->initial_cash + $cashSales;
|
$expectedCash = $register->initial_cash + $cashSales - $cashReturns;
|
||||||
|
|
||||||
// Diferencia (sobrante o faltante)
|
// Diferencia (sobrante o faltante)
|
||||||
$difference = $data['final_cash'] - $expectedCash;
|
$difference = $data['final_cash'] - $expectedCash;
|
||||||
@ -66,6 +77,13 @@ public function closeRegister(CashRegister $register, array $data)
|
|||||||
'card_sales' => $cardSales,
|
'card_sales' => $cardSales,
|
||||||
'total_cash_received' => $totalCashReceived,
|
'total_cash_received' => $totalCashReceived,
|
||||||
'total_change_given' => $totalChangeGiven,
|
'total_change_given' => $totalChangeGiven,
|
||||||
|
|
||||||
|
// Campos nuevos de devoluciones
|
||||||
|
'total_returns' => $totalReturns,
|
||||||
|
'cash_returns' => $cashReturns,
|
||||||
|
'card_returns' => $cardReturns,
|
||||||
|
'returns_count' => $returns->count(),
|
||||||
|
|
||||||
'notes' => $data['notes'] ?? null,
|
'notes' => $data['notes'] ?? null,
|
||||||
'status' => 'closed',
|
'status' => 'closed',
|
||||||
]);
|
]);
|
||||||
@ -82,12 +100,19 @@ public function getCurrentSummary(CashRegister $register)
|
|||||||
->where('status', 'completed')
|
->where('status', 'completed')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
$returns = Returns::where('cash_register_id', $register->id)->get();
|
||||||
|
|
||||||
// Calcular efectivo real en caja (recibido - devuelto)
|
// Calcular efectivo real en caja (recibido - devuelto)
|
||||||
$cashSales = $sales->where('payment_method', 'cash')
|
$cashSales = $sales->where('payment_method', 'cash')
|
||||||
->sum(function ($sale) {
|
->sum(function ($sale) {
|
||||||
return ($sale->cash_received ?? 0) - ($sale->change ?? 0);
|
return ($sale->cash_received ?? 0) - ($sale->change ?? 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Devoluciones
|
||||||
|
$cashReturns = $returns->where('refund_method', 'cash')->sum('total');
|
||||||
|
$cardReturns = $returns->whereIn('refund_method', ['credit_card', 'debit_card'])->sum('total');
|
||||||
|
$totalReturns = $returns->sum('total');
|
||||||
|
|
||||||
// Confirmación, envio de los totales
|
// Confirmación, envio de los totales
|
||||||
$totalCashReceived = $sales->where('payment_method', 'cash')->sum('cash_received');
|
$totalCashReceived = $sales->where('payment_method', 'cash')->sum('cash_received');
|
||||||
$totalChangeGiven = $sales->where('payment_method', 'cash')->sum('change');
|
$totalChangeGiven = $sales->where('payment_method', 'cash')->sum('change');
|
||||||
@ -110,12 +135,21 @@ public function getCurrentSummary(CashRegister $register)
|
|||||||
'cash_sales' => (float) $cashSales,
|
'cash_sales' => (float) $cashSales,
|
||||||
'card_sales' => (float) $cardSales,
|
'card_sales' => (float) $cardSales,
|
||||||
|
|
||||||
|
// Totales de devoluciones
|
||||||
|
'total_returns' => (float) $totalReturns,
|
||||||
|
'cash_returns' => (float) $cashReturns,
|
||||||
|
'card_returns' => (float) $cardReturns,
|
||||||
|
'returns_count' => $returns->count(),
|
||||||
|
|
||||||
//Desglose de efectivo
|
//Desglose de efectivo
|
||||||
'total_cash_received' => (float) $totalCashReceived,
|
'total_cash_received' => (float) $totalCashReceived,
|
||||||
'total_change_given' => (float) $totalChangeGiven,
|
'total_change_given' => (float) $totalChangeGiven,
|
||||||
|
|
||||||
// Efectivo esperado
|
// Efectivo esperado (ajustado por devoluciones en efectivo)
|
||||||
'expected_cash' => (float) ($register->initial_cash + $cashSales)
|
'expected_cash' => (float) ($register->initial_cash + $cashSales - $cashReturns),
|
||||||
|
|
||||||
|
// Ventas netas (después de devoluciones)
|
||||||
|
'net_sales' => (float) ($totalSales - $totalReturns),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
253
app/Services/ReturnService.php
Normal file
253
app/Services/ReturnService.php
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Returns;
|
||||||
|
use App\Models\ReturnDetail;
|
||||||
|
use App\Models\Sale;
|
||||||
|
use App\Models\SaleDetail;
|
||||||
|
use App\Models\InventorySerial;
|
||||||
|
use App\Models\CashRegister;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class ReturnService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Crear una nueva devolución con sus detalles
|
||||||
|
*/
|
||||||
|
public function createReturn(array $data): Returns
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($data) {
|
||||||
|
// 1. Validaciones de negocio
|
||||||
|
$sale = Sale::with(['details.serials', 'cashRegister'])->findOrFail($data['sale_id']);
|
||||||
|
|
||||||
|
if ($sale->status !== 'completed') {
|
||||||
|
throw new \Exception('Solo se pueden devolver productos de ventas completadas.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar que la caja esté abierta si se especificó cash_register_id
|
||||||
|
if (isset($data['cash_register_id'])) {
|
||||||
|
$cashRegister = CashRegister::findOrFail($data['cash_register_id']);
|
||||||
|
if (!$cashRegister->isOpen()) {
|
||||||
|
throw new \Exception('La caja registradora debe estar abierta para procesar devoluciones.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Calcular totales
|
||||||
|
$subtotal = 0;
|
||||||
|
$tax = 0;
|
||||||
|
|
||||||
|
foreach ($data['items'] as $item) {
|
||||||
|
$saleDetail = SaleDetail::findOrFail($item['sale_detail_id']);
|
||||||
|
|
||||||
|
// Validar que no se devuelva más de lo vendido (restando lo ya devuelto)
|
||||||
|
$alreadyReturned = ReturnDetail::where('sale_detail_id', $saleDetail->id)
|
||||||
|
->sum('quantity_returned');
|
||||||
|
|
||||||
|
$maxReturnable = $saleDetail->quantity - $alreadyReturned;
|
||||||
|
|
||||||
|
if ($item['quantity_returned'] > $maxReturnable) {
|
||||||
|
throw new \Exception(
|
||||||
|
"Solo puedes devolver {$maxReturnable} unidades de {$saleDetail->product_name}. " .
|
||||||
|
"Ya se devolvieron {$alreadyReturned} de {$saleDetail->quantity} vendidas."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$itemSubtotal = $saleDetail->unit_price * $item['quantity_returned'];
|
||||||
|
$subtotal += $itemSubtotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcular impuesto proporcional
|
||||||
|
if ($sale->subtotal > 0) {
|
||||||
|
$taxRate = $sale->tax / $sale->subtotal;
|
||||||
|
$tax = $subtotal * $taxRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = $subtotal + $tax;
|
||||||
|
|
||||||
|
// 3. Crear registro de devolución
|
||||||
|
$return = Returns::create([
|
||||||
|
'sale_id' => $sale->id,
|
||||||
|
'user_id' => $data['user_id'],
|
||||||
|
'cash_register_id' => $data['cash_register_id'] ?? $this->getCurrentCashRegister($data['user_id']),
|
||||||
|
'return_number' => $this->generateReturnNumber(),
|
||||||
|
'subtotal' => $subtotal,
|
||||||
|
'tax' => $tax,
|
||||||
|
'total' => $total,
|
||||||
|
'refund_method' => $data['refund_method'] ?? $sale->payment_method,
|
||||||
|
'reason' => $data['reason'],
|
||||||
|
'notes' => $data['notes'] ?? null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 4. Crear detalles y restaurar seriales
|
||||||
|
foreach ($data['items'] as $item) {
|
||||||
|
$saleDetail = SaleDetail::findOrFail($item['sale_detail_id']);
|
||||||
|
|
||||||
|
$returnDetail = ReturnDetail::create([
|
||||||
|
'return_id' => $return->id,
|
||||||
|
'sale_detail_id' => $saleDetail->id,
|
||||||
|
'inventory_id' => $saleDetail->inventory_id,
|
||||||
|
'product_name' => $saleDetail->product_name,
|
||||||
|
'quantity_returned' => $item['quantity_returned'],
|
||||||
|
'unit_price' => $saleDetail->unit_price,
|
||||||
|
'subtotal' => $saleDetail->unit_price * $item['quantity_returned'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Gestionar seriales
|
||||||
|
if (isset($item['serial_numbers']) && is_array($item['serial_numbers'])) {
|
||||||
|
// Seriales específicos proporcionados
|
||||||
|
foreach ($item['serial_numbers'] as $serialNumber) {
|
||||||
|
$serial = InventorySerial::where('serial_number', $serialNumber)
|
||||||
|
->where('sale_detail_id', $saleDetail->id)
|
||||||
|
->where('status', 'vendido')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$serial) {
|
||||||
|
throw new \Exception(
|
||||||
|
"El serial {$serialNumber} no pertenece a esta venta o ya fue devuelto."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marcar como devuelto y vincular a devolución
|
||||||
|
$serial->markAsReturned($returnDetail->id);
|
||||||
|
|
||||||
|
// Luego restaurar a disponible
|
||||||
|
$serial->restoreFromReturn();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Seleccionar automáticamente los primeros N seriales vendidos
|
||||||
|
$serials = InventorySerial::where('sale_detail_id', $saleDetail->id)
|
||||||
|
->where('status', 'vendido')
|
||||||
|
->limit($item['quantity_returned'])
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($serials->count() < $item['quantity_returned']) {
|
||||||
|
throw new \Exception(
|
||||||
|
"No hay suficientes seriales disponibles para devolver de {$saleDetail->product_name}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($serials as $serial) {
|
||||||
|
$serial->markAsReturned($returnDetail->id);
|
||||||
|
$serial->restoreFromReturn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sincronizar el stock del inventario
|
||||||
|
$saleDetail->inventory->syncStock();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Retornar con relaciones cargadas
|
||||||
|
return $return->load([
|
||||||
|
'details.inventory',
|
||||||
|
'details.serials',
|
||||||
|
'sale',
|
||||||
|
'user',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancelar una devolución (caso excepcional, restaurar venta)
|
||||||
|
*/
|
||||||
|
public function cancelReturn(Returns $return): Returns
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($return) {
|
||||||
|
// Restaurar seriales a estado vendido
|
||||||
|
foreach ($return->details as $detail) {
|
||||||
|
$serials = InventorySerial::where('return_detail_id', $detail->id)->get();
|
||||||
|
|
||||||
|
foreach ($serials as $serial) {
|
||||||
|
$serial->update([
|
||||||
|
'status' => 'vendido',
|
||||||
|
'sale_detail_id' => $detail->sale_detail_id,
|
||||||
|
'return_detail_id' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sincronizar stock
|
||||||
|
$detail->inventory->syncStock();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eliminar la devolución (soft delete)
|
||||||
|
$return->delete();
|
||||||
|
|
||||||
|
return $return->fresh(['details.inventory', 'details.serials', 'sale']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtener items elegibles para devolución de una venta
|
||||||
|
*/
|
||||||
|
public function getReturnableItems(Sale $sale): array
|
||||||
|
{
|
||||||
|
if ($sale->status !== 'completed') {
|
||||||
|
throw new \Exception('Solo se pueden ver items de ventas completadas.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$returnableItems = [];
|
||||||
|
|
||||||
|
foreach ($sale->details as $detail) {
|
||||||
|
$alreadyReturned = ReturnDetail::where('sale_detail_id', $detail->id)
|
||||||
|
->sum('quantity_returned');
|
||||||
|
|
||||||
|
$maxReturnable = $detail->quantity - $alreadyReturned;
|
||||||
|
|
||||||
|
if ($maxReturnable > 0) {
|
||||||
|
// Obtener seriales vendidos que aún no han sido devueltos
|
||||||
|
$availableSerials = InventorySerial::where('sale_detail_id', $detail->id)
|
||||||
|
->where('status', 'vendido')
|
||||||
|
->get()
|
||||||
|
->map(fn ($serial) => [
|
||||||
|
'serial_number' => $serial->serial_number,
|
||||||
|
'status' => $serial->status,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$returnableItems[] = [
|
||||||
|
'sale_detail_id' => $detail->id,
|
||||||
|
'inventory_id' => $detail->inventory_id,
|
||||||
|
'product_name' => $detail->product_name,
|
||||||
|
'quantity_sold' => $detail->quantity,
|
||||||
|
'quantity_already_returned' => $alreadyReturned,
|
||||||
|
'quantity_returnable' => $maxReturnable,
|
||||||
|
'unit_price' => $detail->unit_price,
|
||||||
|
'available_serials' => $availableSerials,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $returnableItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generar número de devolución único
|
||||||
|
* Formato: RET-YYYYMMDD-0001
|
||||||
|
*/
|
||||||
|
private function generateReturnNumber(): string
|
||||||
|
{
|
||||||
|
$prefix = 'DEV-';
|
||||||
|
$date = now()->format('Ymd');
|
||||||
|
|
||||||
|
$lastReturn = Returns::whereDate('created_at', today())
|
||||||
|
->orderBy('id', 'desc')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$sequential = $lastReturn
|
||||||
|
? (intval(substr($lastReturn->return_number, -4)) + 1)
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
return $prefix . $date . '-' . str_pad($sequential, 4, '0', STR_PAD_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtener caja registradora activa del usuario
|
||||||
|
*/
|
||||||
|
private function getCurrentCashRegister($userId): ?int
|
||||||
|
{
|
||||||
|
$register = CashRegister::where('user_id', $userId)
|
||||||
|
->where('status', 'open')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
return $register?->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -108,6 +108,11 @@ public function cancelSale(Sale $sale)
|
|||||||
throw new \Exception('Solo se pueden cancelar ventas completadas.');
|
throw new \Exception('Solo se pueden cancelar ventas completadas.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verificar que la venta no tenga devoluciones procesadas
|
||||||
|
if ($sale->returns()->exists()) {
|
||||||
|
throw new \Exception('No se puede cancelar una venta que tiene devoluciones procesadas.');
|
||||||
|
}
|
||||||
|
|
||||||
// Restaurar seriales a disponible
|
// Restaurar seriales a disponible
|
||||||
foreach ($sale->details as $detail) {
|
foreach ($sale->details as $detail) {
|
||||||
$serials = InventorySerial::where('sale_detail_id', $detail->id)->get();
|
$serials = InventorySerial::where('sale_detail_id', $detail->id)->get();
|
||||||
|
|||||||
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('returns', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('sale_id')->constrained()->onDelete('restrict');
|
||||||
|
$table->foreignId('user_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->foreignId('cash_register_id')->nullable()->constrained()->onDelete('restrict');
|
||||||
|
$table->string('return_number')->unique();
|
||||||
|
$table->decimal('subtotal', 10, 2);
|
||||||
|
$table->decimal('tax', 10, 2);
|
||||||
|
$table->decimal('total', 10, 2);
|
||||||
|
$table->enum('refund_method', ['cash', 'credit_card', 'debit_card']);
|
||||||
|
$table->enum('reason', ['defective', 'wrong_product', 'change_of_mind', 'damaged', 'other']);
|
||||||
|
$table->text('notes')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->softDeletes();
|
||||||
|
|
||||||
|
// Índices para optimizar consultas
|
||||||
|
$table->index('sale_id');
|
||||||
|
$table->index('return_number');
|
||||||
|
$table->index('cash_register_id');
|
||||||
|
$table->index('created_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('returns');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('return_details', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('return_id')->constrained()->onDelete('cascade');
|
||||||
|
$table->foreignId('sale_detail_id')->constrained()->onDelete('restrict');
|
||||||
|
$table->foreignId('inventory_id')->constrained()->onDelete('restrict');
|
||||||
|
$table->string('product_name');
|
||||||
|
$table->integer('quantity_returned');
|
||||||
|
$table->decimal('unit_price', 10, 2);
|
||||||
|
$table->decimal('subtotal', 10, 2);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Índices
|
||||||
|
$table->index('return_id');
|
||||||
|
$table->index('sale_detail_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('return_details');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Agregar nuevo estado 'devuelto' al enum status
|
||||||
|
DB::statement("ALTER TABLE inventory_serials MODIFY COLUMN status ENUM('disponible', 'vendido', 'devuelto') NOT NULL DEFAULT 'disponible'");
|
||||||
|
|
||||||
|
Schema::table('inventory_serials', function (Blueprint $table) {
|
||||||
|
$table->foreignId('return_detail_id')
|
||||||
|
->nullable()
|
||||||
|
->after('sale_detail_id')
|
||||||
|
->constrained()->onDelete('set null');
|
||||||
|
|
||||||
|
$table->index('return_detail_id');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('inventory_serials', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['return_detail_id']);
|
||||||
|
$table->dropIndex(['return_detail_id']);
|
||||||
|
$table->dropColumn('return_detail_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::statement("ALTER TABLE inventory_serials MODIFY COLUMN status ENUM('disponible', 'vendido') NOT NULL DEFAULT 'disponible'");
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('cash_registers', function (Blueprint $table) {
|
||||||
|
$table->decimal('cash_sales', 10, 2)->default(0)->after('total_sales');
|
||||||
|
$table->decimal('card_sales', 10, 2)->default(0)->after('cash_sales');
|
||||||
|
$table->decimal('total_cash_received', 10, 2)->default(0)->after('card_sales');
|
||||||
|
$table->decimal('total_change_given', 10, 2)->default(0)->after('total_cash_received');
|
||||||
|
$table->decimal('total_returns', 10, 2)->default(0)->after('total_change_given');
|
||||||
|
$table->decimal('cash_returns', 10, 2)->default(0)->after('total_returns');
|
||||||
|
$table->decimal('card_returns', 10, 2)->default(0)->after('cash_returns');
|
||||||
|
$table->integer('returns_count')->default(0)->after('card_returns');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('cash_registers', function (Blueprint $table) {
|
||||||
|
$table->dropColumn([
|
||||||
|
'cash_sales',
|
||||||
|
'card_sales',
|
||||||
|
'total_cash_received',
|
||||||
|
'total_change_given',
|
||||||
|
'total_returns',
|
||||||
|
'cash_returns',
|
||||||
|
'card_returns',
|
||||||
|
'returns_count',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -7,6 +7,7 @@
|
|||||||
use App\Http\Controllers\App\PriceController;
|
use App\Http\Controllers\App\PriceController;
|
||||||
use App\Http\Controllers\App\ReportController;
|
use App\Http\Controllers\App\ReportController;
|
||||||
use App\Http\Controllers\App\SaleController;
|
use App\Http\Controllers\App\SaleController;
|
||||||
|
use App\Http\Controllers\App\ReturnController;
|
||||||
use App\Http\Controllers\App\FacturaDataController;
|
use App\Http\Controllers\App\FacturaDataController;
|
||||||
use App\Http\Controllers\App\InventorySerialController;
|
use App\Http\Controllers\App\InventorySerialController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
@ -47,6 +48,17 @@
|
|||||||
Route::resource('/sales', SaleController::class);
|
Route::resource('/sales', SaleController::class);
|
||||||
Route::put('/sales/{sale}/cancel', [SaleController::class, 'cancel']);
|
Route::put('/sales/{sale}/cancel', [SaleController::class, 'cancel']);
|
||||||
|
|
||||||
|
// DEVOLUCIONES
|
||||||
|
Route::prefix('returns')->group(function () {
|
||||||
|
Route::get('/', [ReturnController::class, 'index']);
|
||||||
|
Route::get('/{return}', [ReturnController::class, 'show']);
|
||||||
|
Route::post('/', [ReturnController::class, 'store']);
|
||||||
|
Route::put('/{return}/cancel', [ReturnController::class, 'cancel']);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Items que pueden ser devueltos de una venta
|
||||||
|
Route::get('/sales/{sale}/returnable', [ReturnController::class, 'returnable']);
|
||||||
|
|
||||||
// Rutas de caja
|
// Rutas de caja
|
||||||
Route::prefix('cash-registers')->group(function () {
|
Route::prefix('cash-registers')->group(function () {
|
||||||
Route::get('/', [CashRegisterController::class, 'index']);
|
Route::get('/', [CashRegisterController::class, 'index']);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user