feat: implementar controlador y modelo para solicitudes de factura, incluyendo validaciones y rutas
This commit is contained in:
parent
38e5050692
commit
f2d2fd5aaf
@ -41,6 +41,21 @@ public function index(Inventory $inventario, Request $request)
|
||||
]);
|
||||
}
|
||||
|
||||
public function search(Request $request)
|
||||
{
|
||||
$serialNumber = $request->input('serial_number');
|
||||
|
||||
$serial = InventorySerial::with(['inventory.price', 'inventory.category'])
|
||||
->where('serial_number', $serialNumber)
|
||||
->where('status', 'disponible')
|
||||
->first();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'data' => ['serial' => $serial]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar un serial específico
|
||||
*/
|
||||
|
||||
@ -3,12 +3,13 @@
|
||||
namespace App\Http\Controllers\App;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\App\InvoiceStoreRequest;
|
||||
use App\Models\Client;
|
||||
use App\Models\InvoiceRequest;
|
||||
use App\Models\Sale;
|
||||
use Illuminate\Http\Request;
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
|
||||
class FacturaDataController extends Controller
|
||||
class InvoiceController extends Controller
|
||||
{
|
||||
/**
|
||||
* Muestra los datos de la venta para el formulario de facturación.
|
||||
@ -20,26 +21,42 @@ public function show(string $invoiceNumber)
|
||||
'client',
|
||||
'details.inventory.category',
|
||||
'details.serials',
|
||||
'user:id,name,email'
|
||||
])
|
||||
->first();
|
||||
'user:id,name,email',
|
||||
'invoiceRequests' => function ($query) {
|
||||
$query->orderBy('requested_at', 'desc');
|
||||
},
|
||||
])->first();
|
||||
|
||||
if (!$sale) {
|
||||
return ApiResponse::INTERNAL_ERROR->response([
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'Venta no encontrada'
|
||||
]);
|
||||
}
|
||||
|
||||
// Verificar si ya tiene solicitud pendiente o procesada
|
||||
$existingRequest = $sale->invoiceRequests
|
||||
->whereIn('status', [InvoiceRequest::STATUS_PENDING, InvoiceRequest::STATUS_PROCESSED])
|
||||
->first();
|
||||
|
||||
if ($existingRequest) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'Esta venta ya tiene una solicitud de facturación ' .
|
||||
($existingRequest->status === InvoiceRequest::STATUS_PENDING ? 'pendiente' : 'procesada'),
|
||||
'invoice_request' => $existingRequest,
|
||||
]);
|
||||
}
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'sale' => $this->formatSaleData($sale),
|
||||
'client' => $sale->client,
|
||||
'invoice_requests' => $sale->invoiceRequests,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Guarda los datos fiscales del cliente para la venta.
|
||||
*/
|
||||
public function store(Request $request, string $invoiceNumber)
|
||||
public function store(InvoiceStoreRequest $request, string $invoiceNumber)
|
||||
{
|
||||
$sale = Sale::where('invoice_number', $invoiceNumber)
|
||||
->with('details.serials')
|
||||
@ -51,9 +68,14 @@ public function store(Request $request, string $invoiceNumber)
|
||||
]);
|
||||
}
|
||||
|
||||
if ($sale->client_id) {
|
||||
$existingRequest = InvoiceRequest::where('sale_id', $sale->id)
|
||||
->whereIn('status', [InvoiceRequest::STATUS_PENDING, InvoiceRequest::STATUS_PROCESSED])
|
||||
->first();
|
||||
|
||||
if ($existingRequest) {
|
||||
return ApiResponse::NO_CONTENT->response([
|
||||
'message' => 'Esta venta ya tiene datos de facturación registrados'
|
||||
'message' => 'Esta venta ya tiene una solicitud de facturación ' .
|
||||
($existingRequest->status === InvoiceRequest::STATUS_PENDING ? 'pendiente' : 'procesada')
|
||||
]);
|
||||
}
|
||||
|
||||
@ -64,56 +86,35 @@ public function store(Request $request, string $invoiceNumber)
|
||||
]);
|
||||
}
|
||||
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|email|max:255',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'address' => 'nullable|string|max:500',
|
||||
'rfc' => 'required|string|size:13|regex:/^[A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3}$/i',
|
||||
'razon_social' => 'required|string|max:255',
|
||||
'regimen_fiscal' => 'required|string|max:100',
|
||||
'cp_fiscal' => 'required|string|size:5|regex:/^\d{5}$/',
|
||||
'uso_cfdi' => 'required|string|max:100',
|
||||
], [
|
||||
'rfc.regex' => 'El RFC no tiene un formato válido',
|
||||
'rfc.size' => 'El RFC debe tener 13 caracteres',
|
||||
'cp_fiscal.regex' => 'El código postal debe ser de 5 dígitos',
|
||||
'cp_fiscal.size' => 'El código postal debe ser de 5 dígitos',
|
||||
'name.required' => 'El nombre es obligatorio',
|
||||
'email.required' => 'El correo electrónico es obligatorio',
|
||||
'email.email' => 'El correo electrónico debe ser válido',
|
||||
'razon_social.required' => 'La razón social es obligatoria',
|
||||
'regimen_fiscal.required' => 'El régimen fiscal es obligatorio',
|
||||
'uso_cfdi.required' => 'El uso de CFDI es obligatorio',
|
||||
]);
|
||||
|
||||
// Buscar si ya existe un cliente con ese RFC
|
||||
$client = Client::where('rfc', strtoupper($validated['rfc']))->first();
|
||||
$client = Client::where('rfc', strtoupper($request->rfc))->first();
|
||||
|
||||
if ($client) {
|
||||
// Actualizar datos del cliente existente
|
||||
$client->update([
|
||||
'name' => $validated['name'],
|
||||
'email' => $validated['email'],
|
||||
'phone' => $validated['phone'] ?? $client->phone,
|
||||
'address' => $validated['address'] ?? $client->address,
|
||||
'razon_social' => $validated['razon_social'],
|
||||
'regimen_fiscal' => $validated['regimen_fiscal'],
|
||||
'cp_fiscal' => $validated['cp_fiscal'],
|
||||
'uso_cfdi' => $validated['uso_cfdi'],
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'phone' => $request->phone ?? $client->phone,
|
||||
'address' => $request->address ?? $client->address,
|
||||
'razon_social' => $request->razon_social,
|
||||
'regimen_fiscal' => $request->regimen_fiscal,
|
||||
'cp_fiscal' => $request->cp_fiscal,
|
||||
'uso_cfdi' => $request->uso_cfdi,
|
||||
]);
|
||||
} else {
|
||||
// Crear nuevo cliente
|
||||
$client = Client::create([
|
||||
'name' => $validated['name'],
|
||||
'email' => $validated['email'],
|
||||
'phone' => $validated['phone'],
|
||||
'address' => $validated['address'],
|
||||
'rfc' => strtoupper($validated['rfc']),
|
||||
'razon_social' => $validated['razon_social'],
|
||||
'regimen_fiscal' => $validated['regimen_fiscal'],
|
||||
'cp_fiscal' => $validated['cp_fiscal'],
|
||||
'uso_cfdi' => $validated['uso_cfdi'],
|
||||
'name' => $request->name,
|
||||
'client_number' => strtoupper($request->rfc),
|
||||
'email' => $request->email,
|
||||
'phone' => $request->phone,
|
||||
'address' => $request->address,
|
||||
'rfc' => strtoupper($request->rfc),
|
||||
'razon_social' => $request->razon_social,
|
||||
'regimen_fiscal' => $request->regimen_fiscal,
|
||||
'cp_fiscal' => $request->cp_fiscal,
|
||||
'uso_cfdi' => $request->uso_cfdi,
|
||||
]);
|
||||
}
|
||||
|
||||
@ -121,17 +122,18 @@ public function store(Request $request, string $invoiceNumber)
|
||||
$sale->update(['client_id' => $client->id]);
|
||||
|
||||
// Recargar relaciones
|
||||
$sale->load([
|
||||
'client',
|
||||
'details.inventory.category',
|
||||
'details.serials',
|
||||
'user:id,name,email'
|
||||
$invoiceRequest = InvoiceRequest::create([
|
||||
'sale_id' => $sale->id,
|
||||
'client_id' => $client->id,
|
||||
'status' => InvoiceRequest::STATUS_PENDING,
|
||||
'requested_at' => now(),
|
||||
]);
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Datos de facturación guardados correctamente',
|
||||
'message' => 'Solicitud de facturación guardada correctamente',
|
||||
'client' => $client,
|
||||
'sale' => $this->formatSaleData($sale)
|
||||
'sale' => $this->formatSaleData($sale),
|
||||
'invoice_request' => $invoiceRequest,
|
||||
]);
|
||||
}
|
||||
|
||||
198
app/Http/Controllers/App/InvoiceRequestController.php
Normal file
198
app/Http/Controllers/App/InvoiceRequestController.php
Normal file
@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\App;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\App\InvoiceRequestProcessRequest;
|
||||
use App\Http\Requests\App\InvoiceRequestRejectRequest;
|
||||
use App\Models\InvoiceRequest;
|
||||
use Illuminate\Http\Request;
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
|
||||
/**
|
||||
* Controlador para gestión administrativa de solicitudes de factura
|
||||
*/
|
||||
class InvoiceRequestController extends Controller
|
||||
{
|
||||
/**
|
||||
* Listar todas las solicitudes de factura con filtros y paginación
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = InvoiceRequest::with([
|
||||
'sale:id,invoice_number,subtotal,tax,total,payment_method,status,created_at',
|
||||
'sale.user:id,name,email',
|
||||
'client:id,name,client_number,email,phone,rfc,razon_social',
|
||||
'processedBy:id,name,email'
|
||||
]);
|
||||
|
||||
// Filtro por estado
|
||||
if ($request->has('status') && $request->status !== '') {
|
||||
$query->where('status', $request->status);
|
||||
}
|
||||
|
||||
// Filtro por rango de fechas
|
||||
if ($request->has('date_from')) {
|
||||
$query->whereDate('requested_at', '>=', $request->date_from);
|
||||
}
|
||||
|
||||
if ($request->has('date_to')) {
|
||||
$query->whereDate('requested_at', '<=', $request->date_to);
|
||||
}
|
||||
|
||||
// Búsqueda por folio de venta
|
||||
if ($request->has('invoice_number') && $request->invoice_number !== '') {
|
||||
$query->whereHas('sale', function ($q) use ($request) {
|
||||
$q->where('invoice_number', 'like', '%' . $request->invoice_number . '%');
|
||||
});
|
||||
}
|
||||
|
||||
// Búsqueda por cliente
|
||||
if ($request->has('client_search') && $request->client_search !== '') {
|
||||
$query->whereHas('client', function ($q) use ($request) {
|
||||
$q->where('name', 'like', '%' . $request->client_search . '%')
|
||||
->orWhere('rfc', 'like', '%' . $request->client_search . '%')
|
||||
->orWhere('email', 'like', '%' . $request->client_search . '%');
|
||||
});
|
||||
}
|
||||
|
||||
// Ordenamiento
|
||||
$sortBy = $request->get('sort_by', 'requested_at');
|
||||
$sortOrder = $request->get('sort_order', 'desc');
|
||||
$query->orderBy($sortBy, $sortOrder);
|
||||
|
||||
// Paginación
|
||||
$perPage = $request->get('per_page', 15);
|
||||
$invoiceRequests = $query->paginate($perPage);
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'invoice_requests' => $invoiceRequests
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mostrar detalles de una solicitud específica
|
||||
*/
|
||||
public function show($id)
|
||||
{
|
||||
$invoiceRequest = InvoiceRequest::with([
|
||||
'sale.user:id,name,email',
|
||||
'sale.details:id,sale_id,inventory_id,product_name,quantity,unit_price,subtotal,discount_percentage,discount_amount',
|
||||
'sale.details:inventory_id.category_id,name',
|
||||
'sale.details.inventory:id,category_id,name,sku',
|
||||
'sale.details.inventory.category:id,name',
|
||||
'sale.details.serials:id,inventory_id,sale_detail_id,serial_number,status',
|
||||
'client',
|
||||
'processedBy:id,name,email'
|
||||
])->find($id);
|
||||
|
||||
if (!$invoiceRequest) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'Solicitud de factura no encontrada'
|
||||
]);
|
||||
}
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'invoice_request' => $invoiceRequest
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marcar una solicitud como procesada
|
||||
*
|
||||
*/
|
||||
public function process(InvoiceRequestProcessRequest $request, $id)
|
||||
{
|
||||
$invoiceRequest = InvoiceRequest::find($id);
|
||||
|
||||
if (!$invoiceRequest) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'Solicitud de factura no encontrada'
|
||||
]);
|
||||
}
|
||||
|
||||
if ($invoiceRequest->status !== InvoiceRequest::STATUS_PENDING) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'Solo se pueden procesar solicitudes pendientes',
|
||||
'current_status' => $invoiceRequest->status
|
||||
]);
|
||||
}
|
||||
|
||||
$invoiceRequest->markAsProcessed(
|
||||
$request->user()->id,
|
||||
$request->notes
|
||||
);
|
||||
|
||||
$invoiceRequest->load([
|
||||
'sale:id,invoice_number,total',
|
||||
'client:id,name,rfc,email',
|
||||
'processedBy:id,name'
|
||||
]);
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Solicitud marcada como procesada correctamente',
|
||||
'invoice_request' => $invoiceRequest
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rechazar una solicitud
|
||||
*
|
||||
*/
|
||||
public function reject(InvoiceRequestRejectRequest $request, $id)
|
||||
{
|
||||
$invoiceRequest = InvoiceRequest::find($id);
|
||||
|
||||
if (!$invoiceRequest) {
|
||||
return ApiResponse::NOT_FOUND->response([
|
||||
'message' => 'Solicitud de factura no encontrada'
|
||||
]);
|
||||
}
|
||||
|
||||
if ($invoiceRequest->status !== InvoiceRequest::STATUS_PENDING) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'Solo se pueden rechazar solicitudes pendientes',
|
||||
'current_status' => $invoiceRequest->status
|
||||
]);
|
||||
}
|
||||
|
||||
$invoiceRequest->markAsRejected(
|
||||
$request->user()->id,
|
||||
$request->notes
|
||||
);
|
||||
|
||||
$invoiceRequest->load([
|
||||
'sale:id,invoice_number,total',
|
||||
'client:id,name,rfc,email',
|
||||
'processedBy:id,name'
|
||||
]);
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Solicitud rechazada correctamente',
|
||||
'invoice_request' => $invoiceRequest
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener estadísticas de solicitudes de factura
|
||||
*/
|
||||
public function stats()
|
||||
{
|
||||
$stats = [
|
||||
'pending' => InvoiceRequest::where('status', InvoiceRequest::STATUS_PENDING)->count(),
|
||||
'processed' => InvoiceRequest::where('status', InvoiceRequest::STATUS_PROCESSED)->count(),
|
||||
'rejected' => InvoiceRequest::where('status', InvoiceRequest::STATUS_REJECTED)->count(),
|
||||
'total' => InvoiceRequest::count(),
|
||||
'today_pending' => InvoiceRequest::where('status', InvoiceRequest::STATUS_PENDING)
|
||||
->whereDate('requested_at', today())
|
||||
->count(),
|
||||
'this_month' => InvoiceRequest::whereMonth('requested_at', now()->month)
|
||||
->whereYear('requested_at', now()->year)
|
||||
->count(),
|
||||
];
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'stats' => $stats
|
||||
]);
|
||||
}
|
||||
}
|
||||
36
app/Http/Requests/App/InvoiceRequestProcessRequest.php
Normal file
36
app/Http/Requests/App/InvoiceRequestProcessRequest.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\App;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class InvoiceRequestProcessRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true; // Autorización manejada por middleware
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'notes' => ['nullable', 'string', 'max:500'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'notes.max' => 'Las notas no pueden exceder 500 caracteres',
|
||||
];
|
||||
}
|
||||
}
|
||||
38
app/Http/Requests/App/InvoiceRequestRejectRequest.php
Normal file
38
app/Http/Requests/App/InvoiceRequestRejectRequest.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\App;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class InvoiceRequestRejectRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true; // Autorización manejada por middleware
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'notes' => ['required', 'string', 'min:10', 'max:500'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom messages for validator errors.
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'notes.required' => 'Debe proporcionar una razón para rechazar la solicitud',
|
||||
'notes.min' => 'La razón debe tener al menos 10 caracteres',
|
||||
'notes.max' => 'La razón no puede exceder 500 caracteres',
|
||||
];
|
||||
}
|
||||
}
|
||||
50
app/Http/Requests/App/InvoiceStoreRequest.php
Normal file
50
app/Http/Requests/App/InvoiceStoreRequest.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php namespace App\Http\Requests\App;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class InvoiceStoreRequest 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.
|
||||
*
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'email', 'max:255'],
|
||||
'phone' => ['nullable', 'string', 'max:20'],
|
||||
'address' => ['nullable', 'string', 'max:500'],
|
||||
'rfc' => ['required', 'string', 'min:12', 'max:13', 'regex:/^[A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3}$/i'],
|
||||
'razon_social' => ['required', 'string', 'max:255'],
|
||||
'regimen_fiscal' => ['required', 'string', 'max:100'],
|
||||
'cp_fiscal' => ['required', 'string', 'size:5', 'regex:/^\d{5}$/'],
|
||||
'uso_cfdi' => ['required', 'string', 'max:100'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'rfc.regex' => 'El RFC no tiene un formato válido',
|
||||
'rfc.min' => 'El RFC debe tener al menos 12 caracteres',
|
||||
'rfc.max' => 'El RFC debe tener máximo 13 caracteres',
|
||||
'cp_fiscal.regex' => 'El código postal debe ser de 5 dígitos',
|
||||
'cp_fiscal.size' => 'El código postal debe ser de 5 dígitos',
|
||||
'name.required' => 'El nombre es obligatorio',
|
||||
'email.required' => 'El correo electrónico es obligatorio',
|
||||
'email.email' => 'El correo electrónico debe ser válido',
|
||||
'razon_social.required' => 'La razón social es obligatoria',
|
||||
'regimen_fiscal.required' => 'El régimen fiscal es obligatorio',
|
||||
'uso_cfdi.required' => 'El uso de CFDI es obligatorio',
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -67,4 +67,12 @@ public function getNetPurchasesAttribute(): float
|
||||
{
|
||||
return $this->total_purchases - $this->lifetime_returns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Solicitudes de factura del cliente
|
||||
*/
|
||||
public function invoiceRequests(): HasMany
|
||||
{
|
||||
return $this->hasMany(InvoiceRequest::class);
|
||||
}
|
||||
}
|
||||
|
||||
77
app/Models/InvoiceRequest.php
Normal file
77
app/Models/InvoiceRequest.php
Normal file
@ -0,0 +1,77 @@
|
||||
<?php namespace App\Models;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*
|
||||
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
class InvoiceRequest extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'sale_id',
|
||||
'client_id',
|
||||
'status',
|
||||
'requested_at',
|
||||
'processed_at',
|
||||
'processed_by',
|
||||
'notes',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'requested_at' => 'datetime',
|
||||
'processed_at' => 'datetime',
|
||||
];
|
||||
|
||||
const STATUS_PENDING = 'pending';
|
||||
const STATUS_PROCESSED = 'processed';
|
||||
const STATUS_REJECTED = 'rejected';
|
||||
|
||||
public function sale()
|
||||
{
|
||||
return $this->belongsTo(Sale::class);
|
||||
}
|
||||
|
||||
public function client()
|
||||
{
|
||||
return $this->belongsTo(Client::class);
|
||||
}
|
||||
|
||||
public function processedBy()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'processed_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Marcar como procesada
|
||||
*/
|
||||
public function markAsProcessed(int $userId, ?string $notes = null): bool
|
||||
{
|
||||
return $this->update([
|
||||
'status' => self::STATUS_PROCESSED,
|
||||
'processed_at' => now(),
|
||||
'processed_by' => $userId,
|
||||
'notes' => $notes ?? $this->notes,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marcar como rechazada
|
||||
*/
|
||||
public function markAsRejected(int $userId, ?string $notes = null): bool
|
||||
{
|
||||
return $this->update([
|
||||
'status' => self::STATUS_REJECTED,
|
||||
'processed_at' => now(),
|
||||
'processed_by' => $userId,
|
||||
'notes' => $notes ?? $this->notes,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -93,4 +93,13 @@ public function getNetTotalAttribute(): float
|
||||
{
|
||||
return (float) ($this->total - $this->getTotalReturnedAttribute());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Solicitudes de factura asociadas a esta venta
|
||||
*/
|
||||
public function invoiceRequests()
|
||||
{
|
||||
return $this->hasMany(InvoiceRequest::class);
|
||||
}
|
||||
}
|
||||
|
||||
@ -45,7 +45,7 @@ public function inventory()
|
||||
|
||||
public function serials()
|
||||
{
|
||||
return $this->hasMany(InventorySerial::class);
|
||||
return $this->hasMany(InventorySerial::class, 'sale_detail_id');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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('invoice_requests', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('sale_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('client_id')->constrained()->onDelete('cascade');
|
||||
$table->enum('status', ['pending', 'processed', 'rejected'])->default('pending');
|
||||
$table->timestamp('requested_at')->useCurrent();
|
||||
$table->timestamp('processed_at')->nullable();
|
||||
$table->foreignId('processed_by')->nullable()->constrained('users')->onDelete('set null');
|
||||
$table->text('notes')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['sale_id', 'status']);
|
||||
$table->index(['client_id']);
|
||||
$table->index(['status', 'requested_at']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('invoice_requests');
|
||||
}
|
||||
};
|
||||
@ -10,8 +10,9 @@
|
||||
use App\Http\Controllers\App\ReportController;
|
||||
use App\Http\Controllers\App\SaleController;
|
||||
use App\Http\Controllers\App\ReturnController;
|
||||
use App\Http\Controllers\App\FacturaDataController;
|
||||
use App\Http\Controllers\App\InvoiceController;
|
||||
use App\Http\Controllers\App\InventorySerialController;
|
||||
use App\Http\Controllers\App\InvoiceRequestController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
/**
|
||||
@ -39,6 +40,7 @@
|
||||
|
||||
// Números de serie
|
||||
Route::resource('inventario.serials', InventorySerialController::class);
|
||||
Route::get('serials/search', [InventorySerialController::class, 'search']);
|
||||
|
||||
//CATEGORIAS
|
||||
Route::resource('categorias', CategoryController::class);
|
||||
@ -98,11 +100,19 @@
|
||||
Route::delete('/{tier}', [ClientTierController::class, 'destroy']);
|
||||
Route::patch('/{tier}/toggle-active', [ClientTierController::class, 'toggleActive']);
|
||||
});
|
||||
|
||||
Route::prefix('invoice-requests')->group(function () {
|
||||
Route::get('/', [InvoiceRequestController::class, 'index']);
|
||||
Route::get('/stats', [InvoiceRequestController::class, 'stats']);
|
||||
Route::get('/{id}', [InvoiceRequestController::class, 'show']);
|
||||
Route::put('/{id}/process', [InvoiceRequestController::class, 'process']);
|
||||
Route::put('/{id}/reject', [InvoiceRequestController::class, 'reject']);
|
||||
});
|
||||
});
|
||||
|
||||
/** Rutas públicas */
|
||||
// Formulario de datos fiscales para facturación
|
||||
Route::prefix('facturacion')->group(function () {
|
||||
Route::get('/{invoiceNumber}', [FacturaDataController::class, 'show']);
|
||||
Route::post('/{invoiceNumber}', [FacturaDataController::class, 'store']);
|
||||
Route::get('/{invoiceNumber}', [InvoiceController::class, 'show']);
|
||||
Route::post('/{invoiceNumber}', [InvoiceController::class, 'store']);
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user