- Implementa CRUD de unidades y soporte para decimales en inventario. - Integra servicios de WhatsApp para envío de documentos y auth. - Ajusta validación de series y permisos (RoleSeeder).
247 lines
8.7 KiB
PHP
247 lines
8.7 KiB
PHP
<?php
|
|
|
|
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 InvoiceController extends Controller
|
|
{
|
|
/**
|
|
* Muestra los datos de la venta para el formulario de facturación.
|
|
*/
|
|
public function show(string $invoiceNumber)
|
|
{
|
|
$sale = Sale::where('invoice_number', $invoiceNumber)
|
|
->with([
|
|
'client',
|
|
'details.inventory.category',
|
|
'details.serials',
|
|
'user:id,name,email',
|
|
'invoiceRequests' => function ($query) {
|
|
$query->orderBy('requested_at', 'desc');
|
|
},
|
|
])->first();
|
|
|
|
if (!$sale) {
|
|
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,
|
|
]);
|
|
}
|
|
|
|
// Buscar cliente existente si la venta ya tiene cliente asociado
|
|
$existingClient = null;
|
|
if ($sale->client_id) {
|
|
$existingClient = $sale->client;
|
|
}
|
|
|
|
return ApiResponse::OK->response([
|
|
'sale' => $this->formatSaleData($sale),
|
|
'client' => $sale->client,
|
|
'existing_client' => $existingClient,
|
|
'invoice_requests' => $sale->invoiceRequests,
|
|
]);
|
|
}
|
|
|
|
|
|
/**
|
|
* Guarda los datos fiscales del cliente para la venta.
|
|
*/
|
|
public function store(InvoiceStoreRequest $request, string $invoiceNumber)
|
|
{
|
|
$sale = Sale::where('invoice_number', $invoiceNumber)
|
|
->with('details.serials')
|
|
->first();
|
|
|
|
if (!$sale) {
|
|
return ApiResponse::INTERNAL_ERROR->response([
|
|
'message' => 'Venta no encontrada'
|
|
]);
|
|
}
|
|
|
|
$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 una solicitud de facturación ' .
|
|
($existingRequest->status === InvoiceRequest::STATUS_PENDING ? 'pendiente' : 'procesada')
|
|
]);
|
|
}
|
|
|
|
// Verificar que la venta esté completada
|
|
if ($sale->status !== 'completed') {
|
|
return ApiResponse::BAD_REQUEST->response([
|
|
'message' => 'Solo se pueden facturar ventas completadas'
|
|
]);
|
|
}
|
|
|
|
// Buscar si ya existe un cliente con ese RFC
|
|
$client = Client::where('rfc', strtoupper($request->rfc))->first();
|
|
|
|
if ($client) {
|
|
// Solo actualizar campos que el usuario proporciona explícitamente
|
|
$updateData = [];
|
|
|
|
// Actualizar nombre solo si es diferente y fue proporcionado
|
|
if ($request->filled('name') && $request->name !== $client->name) {
|
|
$updateData['name'] = $request->name;
|
|
}
|
|
|
|
// Actualizar email solo si es diferente y fue proporcionado
|
|
if ($request->filled('email') && $request->email !== $client->email) {
|
|
$updateData['email'] = $request->email;
|
|
}
|
|
|
|
// Actualizar teléfono si fue proporcionado
|
|
if ($request->filled('phone')) {
|
|
$updateData['phone'] = $request->phone;
|
|
}
|
|
|
|
// Actualizar dirección si fue proporcionada
|
|
if ($request->filled('address')) {
|
|
$updateData['address'] = $request->address;
|
|
}
|
|
|
|
// Actualizar datos fiscales siempre (son obligatorios en el request)
|
|
$updateData['razon_social'] = $request->razon_social;
|
|
$updateData['regimen_fiscal'] = $request->regimen_fiscal;
|
|
$updateData['cp_fiscal'] = $request->cp_fiscal;
|
|
$updateData['uso_cfdi'] = $request->uso_cfdi;
|
|
|
|
// Solo actualizar si hay cambios
|
|
if (!empty($updateData)) {
|
|
$client->update($updateData);
|
|
}
|
|
} else {
|
|
// Crear nuevo cliente
|
|
$client = Client::create([
|
|
'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,
|
|
]);
|
|
}
|
|
|
|
// Asociar cliente a la venta solo si no está ya asociado
|
|
if ($sale->client_id !== $client->id) {
|
|
$sale->update(['client_id' => $client->id]);
|
|
}
|
|
|
|
// Crear solicitud de facturación
|
|
$invoiceRequest = InvoiceRequest::create([
|
|
'sale_id' => $sale->id,
|
|
'client_id' => $client->id,
|
|
'status' => InvoiceRequest::STATUS_PENDING,
|
|
'requested_at' => now(),
|
|
]);
|
|
|
|
return ApiResponse::OK->response([
|
|
'message' => 'Solicitud de facturación guardada correctamente',
|
|
'client' => $client->fresh(),
|
|
'sale' => $this->formatSaleData($sale->fresh('details.serials')),
|
|
'invoice_request' => $invoiceRequest,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Verificar si existe un cliente con el RFC proporcionado
|
|
*/
|
|
public function checkRfc(Request $request)
|
|
{
|
|
$request->validate([
|
|
'rfc' => ['required', 'string', 'min:12', 'max:13'],
|
|
]);
|
|
|
|
$rfc = $request->input('rfc');
|
|
|
|
$client = Client::where('rfc', strtoupper($rfc))->first();
|
|
|
|
if ($client) {
|
|
return ApiResponse::OK->response([
|
|
'exists' => true,
|
|
'client' => [
|
|
'name' => $client->name,
|
|
'email' => $client->email,
|
|
'phone' => $client->phone,
|
|
'address' => $client->address,
|
|
'rfc' => $client->rfc,
|
|
'razon_social' => $client->razon_social,
|
|
'regimen_fiscal' => $client->regimen_fiscal,
|
|
'cp_fiscal' => $client->cp_fiscal,
|
|
'uso_cfdi' => $client->uso_cfdi,
|
|
]
|
|
]);
|
|
}
|
|
|
|
return ApiResponse::OK->response([
|
|
'exists' => false
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Formatear datos de la venta incluyendo números de serie
|
|
*/
|
|
private function formatSaleData(Sale $sale): array
|
|
{
|
|
return [
|
|
'id' => $sale->id,
|
|
'invoice_number' => $sale->invoice_number,
|
|
'total' => $sale->total,
|
|
'subtotal' => $sale->subtotal,
|
|
'tax' => $sale->tax,
|
|
'payment_method' => $sale->payment_method,
|
|
'status' => $sale->status,
|
|
'created_at' => $sale->created_at,
|
|
'user' => $sale->user ? [
|
|
'id' => $sale->user->id,
|
|
'name' => $sale->user->name,
|
|
'email' => $sale->user->email,
|
|
] : null,
|
|
'items' => $sale->details->map(function ($detail) {
|
|
return [
|
|
'id' => $detail->id,
|
|
'product_name' => $detail->product_name,
|
|
'quantity' => $detail->quantity,
|
|
'unit_price' => $detail->unit_price,
|
|
'subtotal' => $detail->subtotal,
|
|
'category' => $detail->inventory->category->name ?? null,
|
|
'sku' => $detail->inventory->sku ?? null,
|
|
// Números de serie vendidos
|
|
'serial_numbers' => $detail->serials->map(function ($serial) {
|
|
return [
|
|
'serial_number' => $serial->serial_number,
|
|
'status' => $serial->status,
|
|
];
|
|
})->toArray(),
|
|
];
|
|
})->toArray(),
|
|
];
|
|
}
|
|
}
|