feat: unidades de medida y mensajería WhatsApp
- 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).
This commit is contained in:
parent
41a84d05a0
commit
562397402c
@ -87,3 +87,10 @@ VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
|
||||
VITE_REVERB_HOST="${REVERB_HOST}"
|
||||
VITE_REVERB_PORT="${REVERB_PORT}"
|
||||
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
|
||||
|
||||
# WhatsApp API Configuracion
|
||||
WHATSAPP_LOGIN_URL=https://whatsapp.golsystems.mx/api/login
|
||||
WHATSAPP_API_URL=https://whatsapp.golsystems.mx/api/send-whatsapp
|
||||
WHATSAPP_ORG_ID=1
|
||||
WHATSAPP_AUTH_EMAIL=
|
||||
WHATSAPP_AUTH_PASSWORD=
|
||||
|
||||
@ -481,6 +481,7 @@ public function inventoryReport(Request $request)
|
||||
$query = Inventory::with([
|
||||
'category:id,name',
|
||||
'price:inventory_id,cost,retail_price',
|
||||
'unitOfMeasure:id,abbreviation',
|
||||
'serials'
|
||||
])
|
||||
->when($request->q, function($q) use ($request) {
|
||||
@ -535,6 +536,7 @@ public function inventoryReport(Request $request)
|
||||
'sku' => $inventory->sku ?? 'N/A',
|
||||
'name' => $inventory->name,
|
||||
'category' => $inventory->category?->name ?? 'Sin categoría',
|
||||
'unit' => $inventory->unitOfMeasure?->abbreviation ?? 'u',
|
||||
'stock' => $inventory->stock,
|
||||
'quantity_sold' => $quantitySold,
|
||||
'serials_total' => $serialsTotal,
|
||||
@ -684,25 +686,26 @@ public function inventoryReport(Request $request)
|
||||
'B' => 'SKU',
|
||||
'C' => 'NOMBRE',
|
||||
'D' => 'CATEGORÍA',
|
||||
'E' => "STOCK\nDISPONIBLE",
|
||||
'F' => "CANTIDAD\nVENDIDA",
|
||||
'G' => "SERIALES\nTOTALES",
|
||||
'H' => "SERIALES\nDISPONIBLES",
|
||||
'I' => "SERIALES\nVENDIDOS",
|
||||
'J' => "SERIALES\nDEVUELTOS",
|
||||
'K' => "COSTO\nUNITARIO",
|
||||
'L' => "PRECIO\nVENTA",
|
||||
'M' => "TOTAL\nVENDIDO",
|
||||
'N' => "VALOR\nINVENTARIO",
|
||||
'O' => "UTILIDAD\nPOR UNIDAD",
|
||||
'P' => "UTILIDAD\nTOTAL",
|
||||
'E' => 'UNIDAD',
|
||||
'F' => "STOCK\nDISPONIBLE",
|
||||
'G' => "CANTIDAD\nVENDIDA",
|
||||
'H' => "SERIALES\nTOTALES",
|
||||
'I' => "SERIALES\nDISPONIBLES",
|
||||
'J' => "SERIALES\nVENDIDOS",
|
||||
'K' => "SERIALES\nDEVUELTOS",
|
||||
'L' => "COSTO\nUNITARIO",
|
||||
'M' => "PRECIO\nVENTA",
|
||||
'N' => "TOTAL\nVENDIDO",
|
||||
'O' => "VALOR\nINVENTARIO",
|
||||
'P' => "UTILIDAD\nPOR UNIDAD",
|
||||
'Q' => "UTILIDAD\nTOTAL",
|
||||
];
|
||||
|
||||
foreach ($headers as $col => $text) {
|
||||
$sheet->setCellValue("{$col}{$h}", $text);
|
||||
}
|
||||
|
||||
$sheet->getStyle("A{$h}:P{$h}")->applyFromArray($styleTableHeader);
|
||||
$sheet->getStyle("A{$h}:Q{$h}")->applyFromArray($styleTableHeader);
|
||||
$sheet->getRowDimension($h)->setRowHeight(35);
|
||||
|
||||
// --- LLENADO DE DATOS ---
|
||||
@ -715,27 +718,28 @@ public function inventoryReport(Request $request)
|
||||
$sheet->setCellValue('B' . $row, $item['sku']);
|
||||
$sheet->setCellValue('C' . $row, $item['name']);
|
||||
$sheet->setCellValue('D' . $row, $item['category']);
|
||||
$sheet->setCellValue('E' . $row, $item['unit']);
|
||||
|
||||
// NÚMEROS SIN FORMATO
|
||||
$sheet->setCellValueExplicit('E' . $row, (int) $item['stock'], DataType::TYPE_NUMERIC);
|
||||
$sheet->setCellValueExplicit('F' . $row, (int) $item['quantity_sold'], DataType::TYPE_NUMERIC);
|
||||
$sheet->setCellValueExplicit('G' . $row, (int) $item['serials_total'], DataType::TYPE_NUMERIC);
|
||||
$sheet->setCellValueExplicit('H' . $row, (int) $item['serials_available'], DataType::TYPE_NUMERIC);
|
||||
$sheet->setCellValueExplicit('I' . $row, (int) $item['serials_sold'], DataType::TYPE_NUMERIC);
|
||||
$sheet->setCellValueExplicit('J' . $row, (int) $item['serials_returned'], DataType::TYPE_NUMERIC);
|
||||
$sheet->setCellValueExplicit('F' . $row, (float) $item['stock'], DataType::TYPE_NUMERIC);
|
||||
$sheet->setCellValueExplicit('G' . $row, (float) $item['quantity_sold'], DataType::TYPE_NUMERIC);
|
||||
$sheet->setCellValueExplicit('H' . $row, (int) $item['serials_total'], DataType::TYPE_NUMERIC);
|
||||
$sheet->setCellValueExplicit('I' . $row, (int) $item['serials_available'], DataType::TYPE_NUMERIC);
|
||||
$sheet->setCellValueExplicit('J' . $row, (int) $item['serials_sold'], DataType::TYPE_NUMERIC);
|
||||
$sheet->setCellValueExplicit('K' . $row, (int) $item['serials_returned'], DataType::TYPE_NUMERIC);
|
||||
|
||||
// NÚMEROS CON FORMATO MONEDA
|
||||
$sheet->setCellValueExplicit('K' . $row, (float) $item['cost'], DataType::TYPE_NUMERIC);
|
||||
$sheet->setCellValueExplicit('L' . $row, (float) $item['price'], DataType::TYPE_NUMERIC);
|
||||
$sheet->setCellValueExplicit('M' . $row, (float) $item['total_sold'], DataType::TYPE_NUMERIC);
|
||||
$sheet->setCellValueExplicit('N' . $row, (float) $item['inventory_value'], DataType::TYPE_NUMERIC);
|
||||
$sheet->setCellValueExplicit('O' . $row, (float) $item['unit_profit'], DataType::TYPE_NUMERIC);
|
||||
$sheet->setCellValueExplicit('P' . $row, (float) $item['total_profit'], DataType::TYPE_NUMERIC);
|
||||
$sheet->setCellValueExplicit('L' . $row, (float) $item['cost'], DataType::TYPE_NUMERIC);
|
||||
$sheet->setCellValueExplicit('M' . $row, (float) $item['price'], DataType::TYPE_NUMERIC);
|
||||
$sheet->setCellValueExplicit('N' . $row, (float) $item['total_sold'], DataType::TYPE_NUMERIC);
|
||||
$sheet->setCellValueExplicit('O' . $row, (float) $item['inventory_value'], DataType::TYPE_NUMERIC);
|
||||
$sheet->setCellValueExplicit('P' . $row, (float) $item['unit_profit'], DataType::TYPE_NUMERIC);
|
||||
$sheet->setCellValueExplicit('Q' . $row, (float) $item['total_profit'], DataType::TYPE_NUMERIC);
|
||||
|
||||
$totalProfit += $item['total_profit'];
|
||||
|
||||
// Estilos de fila
|
||||
$sheet->getStyle("A{$row}:P{$row}")->applyFromArray([
|
||||
$sheet->getStyle("A{$row}:Q{$row}")->applyFromArray([
|
||||
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN]],
|
||||
'alignment' => ['vertical' => Alignment::VERTICAL_CENTER],
|
||||
'font' => ['size' => 10]
|
||||
@ -750,20 +754,21 @@ public function inventoryReport(Request $request)
|
||||
$sheet->getStyle("H{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
$sheet->getStyle("I{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
$sheet->getStyle("J{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
$sheet->getStyle("K{$row}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
|
||||
|
||||
// Formato moneda para columnas K-P
|
||||
$sheet->getStyle("K{$row}:P{$row}")->getNumberFormat()->setFormatCode('$#,##0.00');
|
||||
// Formato moneda para columnas L-Q
|
||||
$sheet->getStyle("L{$row}:Q{$row}")->getNumberFormat()->setFormatCode('$#,##0.00');
|
||||
|
||||
// Color de utilidad (verde si es positiva)
|
||||
if ($item['total_profit'] > 0) {
|
||||
$sheet->getStyle("P{$row}")->getFont()->getColor()->setRGB('008000');
|
||||
$sheet->getStyle("Q{$row}")->getFont()->getColor()->setRGB('008000');
|
||||
} elseif ($item['total_profit'] < 0) {
|
||||
$sheet->getStyle("P{$row}")->getFont()->getColor()->setRGB('FF0000');
|
||||
$sheet->getStyle("Q{$row}")->getFont()->getColor()->setRGB('FF0000');
|
||||
}
|
||||
|
||||
// Color alterno de filas
|
||||
if ($i % 2 == 0) {
|
||||
$sheet->getStyle("A{$row}:P{$row}")->getFill()
|
||||
$sheet->getStyle("A{$row}:Q{$row}")->getFill()
|
||||
->setFillType(Fill::FILL_SOLID)
|
||||
->getStartColor()->setRGB('F2F2F2');
|
||||
}
|
||||
@ -773,22 +778,23 @@ public function inventoryReport(Request $request)
|
||||
}
|
||||
|
||||
// --- ANCHOS DE COLUMNA ---
|
||||
$sheet->getColumnDimension('A')->setWidth(18);
|
||||
$sheet->getColumnDimension('B')->setWidth(18);
|
||||
$sheet->getColumnDimension('C')->setWidth(18);
|
||||
$sheet->getColumnDimension('A')->setWidth(6);
|
||||
$sheet->getColumnDimension('B')->setWidth(15);
|
||||
$sheet->getColumnDimension('C')->setWidth(25);
|
||||
$sheet->getColumnDimension('D')->setWidth(18);
|
||||
$sheet->getColumnDimension('E')->setWidth(18);
|
||||
$sheet->getColumnDimension('F')->setWidth(18);
|
||||
$sheet->getColumnDimension('G')->setWidth(18);
|
||||
$sheet->getColumnDimension('H')->setWidth(18);
|
||||
$sheet->getColumnDimension('I')->setWidth(18);
|
||||
$sheet->getColumnDimension('J')->setWidth(18);
|
||||
$sheet->getColumnDimension('K')->setWidth(18);
|
||||
$sheet->getColumnDimension('L')->setWidth(18);
|
||||
$sheet->getColumnDimension('M')->setWidth(18);
|
||||
$sheet->getColumnDimension('E')->setWidth(10);
|
||||
$sheet->getColumnDimension('F')->setWidth(15);
|
||||
$sheet->getColumnDimension('G')->setWidth(15);
|
||||
$sheet->getColumnDimension('H')->setWidth(15);
|
||||
$sheet->getColumnDimension('I')->setWidth(15);
|
||||
$sheet->getColumnDimension('J')->setWidth(15);
|
||||
$sheet->getColumnDimension('K')->setWidth(15);
|
||||
$sheet->getColumnDimension('L')->setWidth(15);
|
||||
$sheet->getColumnDimension('M')->setWidth(15);
|
||||
$sheet->getColumnDimension('N')->setWidth(18);
|
||||
$sheet->getColumnDimension('O')->setWidth(18);
|
||||
$sheet->getColumnDimension('P')->setWidth(18);
|
||||
$sheet->getColumnDimension('Q')->setWidth(18);
|
||||
|
||||
$writer = new Xlsx($spreadsheet);
|
||||
$writer->save($filePath);
|
||||
|
||||
@ -24,7 +24,7 @@ public function __construct(
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$products = Inventory::with(['category', 'price'])->withCount('serials')
|
||||
$products = Inventory::with(['category', 'price', 'unitOfMeasure'])->withCount('serials')
|
||||
->where('is_active', true);
|
||||
|
||||
|
||||
@ -61,7 +61,7 @@ public function index(Request $request)
|
||||
public function show(Inventory $inventario)
|
||||
{
|
||||
return ApiResponse::OK->response([
|
||||
'model' => $inventario->load(['category', 'price'])->loadCount('serials')
|
||||
'model' => $inventario->load(['category', 'price', 'unitOfMeasure'])->loadCount('serials')
|
||||
]);
|
||||
}
|
||||
|
||||
@ -96,7 +96,7 @@ public function destroy(Inventory $inventario)
|
||||
public function getProductsByWarehouse(Request $request, int $warehouseId)
|
||||
{
|
||||
$query = Inventory::query()
|
||||
->with(['category', 'price'])
|
||||
->with(['category', 'price', 'unitOfMeasure'])
|
||||
->where('is_active', true)
|
||||
->whereHas('warehouses', function ($q) use ($warehouseId) {
|
||||
$q->where('warehouse_id', $warehouseId)
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
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
|
||||
@ -46,13 +47,21 @@ public function show(string $invoiceNumber)
|
||||
]);
|
||||
}
|
||||
|
||||
// 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.
|
||||
*/
|
||||
@ -86,22 +95,43 @@ public function store(InvoiceStoreRequest $request, string $invoiceNumber)
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
// Buscar si ya existe un cliente con ese RFC
|
||||
$client = Client::where('rfc', strtoupper($request->rfc))->first();
|
||||
|
||||
if ($client) {
|
||||
// Actualizar datos del cliente existente
|
||||
$client->update([
|
||||
'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,
|
||||
]);
|
||||
// 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([
|
||||
@ -118,10 +148,12 @@ public function store(InvoiceStoreRequest $request, string $invoiceNumber)
|
||||
]);
|
||||
}
|
||||
|
||||
// Asociar cliente a la venta
|
||||
$sale->update(['client_id' => $client->id]);
|
||||
// Asociar cliente a la venta solo si no está ya asociado
|
||||
if ($sale->client_id !== $client->id) {
|
||||
$sale->update(['client_id' => $client->id]);
|
||||
}
|
||||
|
||||
// Recargar relaciones
|
||||
// Crear solicitud de facturación
|
||||
$invoiceRequest = InvoiceRequest::create([
|
||||
'sale_id' => $sale->id,
|
||||
'client_id' => $client->id,
|
||||
@ -131,12 +163,47 @@ public function store(InvoiceStoreRequest $request, string $invoiceNumber)
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Solicitud de facturación guardada correctamente',
|
||||
'client' => $client,
|
||||
'sale' => $this->formatSaleData($sale),
|
||||
'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
|
||||
*/
|
||||
|
||||
76
app/Http/Controllers/App/UnitOfMeasurementController.php
Normal file
76
app/Http/Controllers/App/UnitOfMeasurementController.php
Normal file
@ -0,0 +1,76 @@
|
||||
<?php namespace App\Http\Controllers\App;
|
||||
/**
|
||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||
*/
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\App\UnitOfMeasurementStoreRequest;
|
||||
use App\Http\Requests\App\UnitOfMeasurementUpdateRequest;
|
||||
use App\Models\UnitOfMeasurement;
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
|
||||
/**
|
||||
* Descripción
|
||||
*/
|
||||
class UnitOfMeasurementController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$units = UnitOfMeasurement::orderBy('name')->get();
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'units' => $units
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(UnitOfMeasurement $unit)
|
||||
{
|
||||
return ApiResponse::OK->response([
|
||||
'unit' => $unit
|
||||
]);
|
||||
}
|
||||
|
||||
public function active()
|
||||
{
|
||||
$units = UnitOfMeasurement::active()->orderBy('name')->get();
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'units' => $units
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(UnitOfMeasurementStoreRequest $request)
|
||||
{
|
||||
$unit = UnitOfMeasurement::create($request->validated());
|
||||
|
||||
return ApiResponse::CREATED->response([
|
||||
'message' => 'Unidad de medida creada correctamente.',
|
||||
'unit' => $unit
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UnitOfMeasurementUpdateRequest $request, UnitOfMeasurement $unit)
|
||||
{
|
||||
$unit->update($request->validated());
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Unidad de medida actualizada correctamente.'
|
||||
]);
|
||||
}
|
||||
|
||||
public function destroy(UnitOfMeasurement $unit)
|
||||
{
|
||||
// Verificar si hay productos asociados a esta unidad de medida
|
||||
if ($unit->inventories()->exists()) {
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'No se puede eliminar esta unidad de medida porque hay productos asociados a ella.'
|
||||
]);
|
||||
}
|
||||
|
||||
$unit->delete();
|
||||
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Unidad de medida eliminada correctamente.'
|
||||
]);
|
||||
}
|
||||
}
|
||||
121
app/Http/Controllers/App/WhatsappController.php
Normal file
121
app/Http/Controllers/App/WhatsappController.php
Normal file
@ -0,0 +1,121 @@
|
||||
<?php namespace App\Http\Controllers\App;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\WhatsappService;
|
||||
use Illuminate\Http\Request;
|
||||
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||
|
||||
class WhatsappController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected WhatsappService $whatsAppService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Enviar documento por WhatsApp
|
||||
*/
|
||||
public function sendDocument(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'phone_number' => ['required', 'string', 'regex:/^[0-9]{10,13}$/'],
|
||||
'document_url' => ['required', 'url'],
|
||||
'caption' => ['required', 'string', 'max:1000'],
|
||||
'filename' => ['required', 'string', 'max:255'],
|
||||
'ticket' => ['required', 'string', 'max:100'],
|
||||
'customer_name' => ['required', 'string', 'max:255'],
|
||||
], [
|
||||
'phone_number.required' => 'El número de teléfono es obligatorio',
|
||||
'phone_number.regex' => 'El número de teléfono debe tener entre 10 y 13 dígitos',
|
||||
'document_url.required' => 'La URL del documento es obligatoria',
|
||||
'document_url.url' => 'La URL del documento no es válida',
|
||||
]);
|
||||
|
||||
$result = $this->whatsAppService->sendDocument(
|
||||
phoneNumber: $validated['phone_number'],
|
||||
documentUrl: $validated['document_url'],
|
||||
caption: $validated['caption'],
|
||||
filename: $validated['filename'],
|
||||
userEmail: auth()->user()->email,
|
||||
ticket: $validated['ticket'],
|
||||
customerName: $validated['customer_name']
|
||||
);
|
||||
|
||||
if ($result['success']) {
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Documento enviado correctamente por WhatsApp',
|
||||
'data' => $result
|
||||
]);
|
||||
}
|
||||
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => $result['message'],
|
||||
'error' => $result['error'] ?? null
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enviar factura por WhatsApp
|
||||
*/
|
||||
public function sendInvoice(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'phone_number' => ['required', 'string', 'regex:/^[0-9]{10,13}$/'],
|
||||
'invoice_number' => ['required', 'string'],
|
||||
'pdf_url' => ['required', 'url'],
|
||||
'xml_url' => ['nullable', 'url'],
|
||||
'customer_name' => ['required', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
$result = $this->whatsAppService->sendInvoice(
|
||||
phoneNumber: $validated['phone_number'],
|
||||
pdfUrl: $validated['pdf_url'],
|
||||
xmlUrl: $validated['xml_url'] ?? null,
|
||||
invoiceNumber: $validated['invoice_number'],
|
||||
customerName: $validated['customer_name']
|
||||
);
|
||||
|
||||
if ($result['success']) {
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Factura enviada correctamente por WhatsApp',
|
||||
'data' => $result
|
||||
]);
|
||||
}
|
||||
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => 'Error al enviar la factura',
|
||||
'error' => $result
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enviar ticket de venta por WhatsApp
|
||||
*/
|
||||
public function sendSaleTicket(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'phone_number' => ['required', 'string', 'regex:/^[0-9]{10,13}$/'],
|
||||
'sale_number' => ['required', 'string'],
|
||||
'ticket_url' => ['required', 'url'],
|
||||
'customer_name' => ['required', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
$result = $this->whatsAppService->sendSaleTicket(
|
||||
phoneNumber: $validated['phone_number'],
|
||||
ticketUrl: $validated['ticket_url'],
|
||||
saleNumber: $validated['sale_number'],
|
||||
customerName: $validated['customer_name']
|
||||
);
|
||||
|
||||
if ($result['success']) {
|
||||
return ApiResponse::OK->response([
|
||||
'message' => 'Ticket enviado correctamente por WhatsApp',
|
||||
'data' => $result
|
||||
]);
|
||||
}
|
||||
|
||||
return ApiResponse::BAD_REQUEST->response([
|
||||
'message' => $result['message'],
|
||||
'error' => $result['error'] ?? null
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Requests\App;
|
||||
|
||||
use App\Models\Inventory;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class InventoryEntryRequest extends FormRequest
|
||||
@ -22,7 +23,7 @@ public function rules(): array
|
||||
// Validación del array de productos
|
||||
'products' => 'required|array|min:1',
|
||||
'products.*.inventory_id' => 'required|exists:inventories,id',
|
||||
'products.*.quantity' => 'required|integer|min:1',
|
||||
'products.*.quantity' => 'required|numeric|min:0.001',
|
||||
'products.*.unit_cost' => 'required|numeric|min:0',
|
||||
'products.*.serial_numbers' => 'nullable|array',
|
||||
'products.*.serial_numbers.*' => 'required|string|distinct|unique:inventory_serials,serial_number',
|
||||
@ -32,7 +33,7 @@ public function rules(): array
|
||||
return [
|
||||
'inventory_id' => 'required|exists:inventories,id',
|
||||
'warehouse_id' => 'required|exists:warehouses,id',
|
||||
'quantity' => 'required|integer|min:1',
|
||||
'quantity' => 'required|numeric|min:0.001',
|
||||
'unit_cost' => 'required|numeric|min:0',
|
||||
'invoice_reference' => 'required|string|max:255',
|
||||
'notes' => 'nullable|string|max:1000',
|
||||
@ -77,4 +78,50 @@ public function messages(): array
|
||||
'invoice_reference.required' => 'La referencia de la factura es requerida',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validaciones adicionales de negocio
|
||||
*/
|
||||
public function withValidator($validator)
|
||||
{
|
||||
$validator->after(function ($validator) {
|
||||
// Extraer productos del request (entrada simple o múltiple)
|
||||
$products = $this->has('products') ? $this->products : [[
|
||||
'inventory_id' => $this->inventory_id,
|
||||
'quantity' => $this->quantity,
|
||||
'serial_numbers' => $this->serial_numbers ?? null,
|
||||
]];
|
||||
|
||||
foreach ($products as $index => $product) {
|
||||
$inventory = Inventory::with('unitOfMeasure')->find($product['inventory_id']);
|
||||
|
||||
if (!$inventory || !$inventory->unitOfMeasure) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// VALIDACIÓN 1: Cantidades decimales solo con unidades que permiten decimales
|
||||
$quantity = $product['quantity'];
|
||||
$isDecimal = floor($quantity) != $quantity;
|
||||
|
||||
if ($isDecimal && !$inventory->unitOfMeasure->allows_decimals) {
|
||||
$field = $this->has('products') ? "products.{$index}.quantity" : 'quantity';
|
||||
$validator->errors()->add(
|
||||
$field,
|
||||
"El producto '{$inventory->name}' usa la unidad '{$inventory->unitOfMeasure->name}' que no permite cantidades decimales. Use cantidades enteras."
|
||||
);
|
||||
}
|
||||
|
||||
// VALIDACIÓN 2: No permitir seriales con unidades decimales
|
||||
$serialNumbers = $product['serial_numbers'] ?? null;
|
||||
|
||||
if (!empty($serialNumbers) && $inventory->unitOfMeasure->allows_decimals) {
|
||||
$field = $this->has('products') ? "products.{$index}.serial_numbers" : 'serial_numbers';
|
||||
$validator->errors()->add(
|
||||
$field,
|
||||
"No se pueden registrar números de serie para el producto '{$inventory->name}' porque usa la unidad '{$inventory->unitOfMeasure->name}' que permite cantidades decimales. Los seriales solo son válidos para unidades discretas como 'Pieza'."
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Requests\App;
|
||||
|
||||
use App\Models\Inventory;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class InventoryExitRequest extends FormRequest
|
||||
@ -22,7 +23,7 @@ public function rules(): array
|
||||
// Validación del array de productos
|
||||
'products' => 'required|array|min:1',
|
||||
'products.*.inventory_id' => 'required|exists:inventories,id',
|
||||
'products.*.quantity' => 'required|integer|min:1',
|
||||
'products.*.quantity' => 'required|numeric|min:0.001',
|
||||
'products.*.serial_numbers' => 'nullable|array',
|
||||
'products.*.serial_numbers.*' => 'required|string|exists:inventory_serials,serial_number',
|
||||
];
|
||||
@ -32,7 +33,7 @@ public function rules(): array
|
||||
return [
|
||||
'inventory_id' => 'required|exists:inventories,id',
|
||||
'warehouse_id' => 'required|exists:warehouses,id',
|
||||
'quantity' => 'required|integer|min:1',
|
||||
'quantity' => 'required|numeric|min:0.001',
|
||||
'notes' => 'nullable|string|max:1000',
|
||||
'serial_numbers' => 'nullable|array',
|
||||
'serial_numbers.*' => 'required|string|exists:inventory_serials,serial_number',
|
||||
@ -68,4 +69,48 @@ public function messages(): array
|
||||
'quantity.min' => 'La cantidad debe ser al menos 1',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validación adicional: cantidades decimales solo con unidades que permiten decimales
|
||||
*/
|
||||
public function withValidator($validator)
|
||||
{
|
||||
$validator->after(function ($validator) {
|
||||
$products = $this->has('products') ? $this->products : [[
|
||||
'inventory_id' => $this->inventory_id,
|
||||
'quantity' => $this->quantity,
|
||||
'serial_numbers' => $this->serial_numbers ?? null,
|
||||
]];
|
||||
|
||||
foreach ($products as $index => $product) {
|
||||
$inventory = Inventory::with('unitOfMeasure')->find($product['inventory_id']);
|
||||
|
||||
if (!$inventory || !$inventory->unitOfMeasure) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$quantity = $product['quantity'];
|
||||
$isDecimal = floor($quantity) != $quantity;
|
||||
|
||||
// Si la cantidad es decimal pero la unidad no permite decimales
|
||||
if ($isDecimal && !$inventory->unitOfMeasure->allows_decimals) {
|
||||
$field = $this->has('products') ? "products.{$index}.quantity" : 'quantity';
|
||||
$validator->errors()->add(
|
||||
$field,
|
||||
"El producto '{$inventory->name}' usa la unidad '{$inventory->unitOfMeasure->name}' que no permite cantidades decimales. Use cantidades enteras."
|
||||
);
|
||||
}
|
||||
|
||||
$serialNumbers = $product['serial_numbers'] ?? null;
|
||||
|
||||
if (!empty($serialNumbers) && $inventory->unitOfMeasure->allows_decimals) {
|
||||
$field = $this->has('products') ? "products.{$index}.serial_numbers" : 'serial_numbers';
|
||||
$validator->errors()->add(
|
||||
$field,
|
||||
"No se pueden registrar números de serie para el producto '{$inventory->name}' porque usa la unidad '{$inventory->unitOfMeasure->name}' que permite cantidades decimales. Los seriales solo son válidos para unidades discretas como 'Pieza'."
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Requests\App;
|
||||
|
||||
use App\Models\Inventory;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class InventoryTransferRequest extends FormRequest
|
||||
@ -23,7 +24,7 @@ public function rules(): array
|
||||
// Validación del array de productos
|
||||
'products' => 'required|array|min:1',
|
||||
'products.*.inventory_id' => 'required|exists:inventories,id',
|
||||
'products.*.quantity' => 'required|integer|min:1',
|
||||
'products.*.quantity' => 'required|numeric|min:0.001',
|
||||
'products.*.serial_numbers' => 'nullable|array',
|
||||
'products.*.serial_numbers.*' => 'required|string|exists:inventory_serials,serial_number',
|
||||
];
|
||||
@ -34,7 +35,7 @@ public function rules(): array
|
||||
'inventory_id' => 'required|exists:inventories,id',
|
||||
'warehouse_from_id' => 'required|exists:warehouses,id',
|
||||
'warehouse_to_id' => 'required|exists:warehouses,id|different:warehouse_from_id',
|
||||
'quantity' => 'required|integer|min:1',
|
||||
'quantity' => 'required|numeric|min:0.001',
|
||||
'notes' => 'nullable|string|max:1000',
|
||||
'serial_numbers' => 'nullable|array',
|
||||
'serial_numbers.*' => 'required|string|exists:inventory_serials,serial_number',
|
||||
@ -73,4 +74,50 @@ public function messages(): array
|
||||
'quantity.min' => 'La cantidad debe ser al menos 1',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validación adicional: cantidades decimales solo con unidades que permiten decimales
|
||||
*/
|
||||
public function withValidator($validator)
|
||||
{
|
||||
$validator->after(function ($validator) {
|
||||
// Extraer productos del request (entrada simple o múltiple)
|
||||
$products = $this->has('products') ? $this->products : [[
|
||||
'inventory_id' => $this->inventory_id,
|
||||
'quantity' => $this->quantity,
|
||||
'serial_numbers' => $this->serial_numbers ?? null,
|
||||
]];
|
||||
|
||||
foreach ($products as $index => $product) {
|
||||
$inventory = Inventory::with('unitOfMeasure')->find($product['inventory_id']);
|
||||
|
||||
if (!$inventory || !$inventory->unitOfMeasure) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// VALIDACIÓN 1: Cantidades decimales solo con unidades que permiten decimales
|
||||
$quantity = $product['quantity'];
|
||||
$isDecimal = floor($quantity) != $quantity;
|
||||
|
||||
if ($isDecimal && !$inventory->unitOfMeasure->allows_decimals) {
|
||||
$field = $this->has('products') ? "products.{$index}.quantity" : 'quantity';
|
||||
$validator->errors()->add(
|
||||
$field,
|
||||
"El producto '{$inventory->name}' usa la unidad '{$inventory->unitOfMeasure->name}' que no permite cantidades decimales. Use cantidades enteras."
|
||||
);
|
||||
}
|
||||
|
||||
// VALIDACIÓN 2: No permitir seriales con unidades decimales
|
||||
$serialNumbers = $product['serial_numbers'] ?? null;
|
||||
|
||||
if (!empty($serialNumbers) && $inventory->unitOfMeasure->allows_decimals) {
|
||||
$field = $this->has('products') ? "products.{$index}.serial_numbers" : 'serial_numbers';
|
||||
$validator->errors()->add(
|
||||
$field,
|
||||
"No se pueden registrar números de serie para el producto '{$inventory->name}' porque usa la unidad '{$inventory->unitOfMeasure->name}' que permite cantidades decimales. Los seriales solo son válidos para unidades discretas como 'Pieza'."
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
41
app/Http/Requests/App/UnitOfMeasurementStoreRequest.php
Normal file
41
app/Http/Requests/App/UnitOfMeasurementStoreRequest.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\App;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UnitOfMeasurementStoreRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:50', 'unique:units_of_measurement,name'],
|
||||
'abbreviation' => ['required', 'string', 'max:10', 'unique:units_of_measurement,abbreviation'],
|
||||
'allows_decimals' => ['required', 'boolean'],
|
||||
'is_active' => ['boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.required' => 'El nombre es obligatorio.',
|
||||
'name.unique' => 'Ya existe una unidad con este nombre.',
|
||||
'name.max' => 'El nombre no debe exceder los 50 caracteres.',
|
||||
'abbreviation.required' => 'La abreviatura es obligatoria.',
|
||||
'abbreviation.unique' => 'Ya existe una unidad con esta abreviatura.',
|
||||
'abbreviation.max' => 'La abreviatura no debe exceder los 10 caracteres.',
|
||||
'allows_decimals.required' => 'Debe especificar si la unidad permite cantidades decimales.',
|
||||
'allows_decimals.boolean' => 'El campo permite decimales debe ser verdadero o falso.',
|
||||
'is_active.boolean' => 'El campo activo debe ser verdadero o falso.',
|
||||
];
|
||||
}
|
||||
}
|
||||
71
app/Http/Requests/App/UnitOfMeasurementUpdateRequest.php
Normal file
71
app/Http/Requests/App/UnitOfMeasurementUpdateRequest.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\App;
|
||||
|
||||
use App\Models\UnitOfMeasurement;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UnitOfMeasurementUpdateRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$unitId = $this->route('unidad');
|
||||
|
||||
return [
|
||||
'name' => ['sometimes', 'string', 'max:50', 'unique:units_of_measurement,name,' . $unitId],
|
||||
'abbreviation' => ['sometimes', 'string', 'max:10', 'unique:units_of_measurement,abbreviation,' . $unitId],
|
||||
'allows_decimals' => ['sometimes', 'boolean'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
];
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'name.unique' => 'Ya existe una unidad con este nombre.',
|
||||
'name.max' => 'El nombre no debe exceder los 50 caracteres.',
|
||||
'abbreviation.unique' => 'Ya existe una unidad con esta abreviatura.',
|
||||
'abbreviation.max' => 'La abreviatura no debe exceder los 10 caracteres.',
|
||||
'allows_decimals.boolean' => 'El campo permite decimales debe ser verdadero o falso.',
|
||||
'is_active.boolean' => 'El campo activo debe ser verdadero o falso.',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validación adicional: no permitir cambiar allows_decimals si hay productos con seriales
|
||||
*/
|
||||
public function withValidator($validator)
|
||||
{
|
||||
$validator->after(function ($validator) {
|
||||
$unitId = $this->route('unidad');
|
||||
$unit = UnitOfMeasurement::find($unitId);
|
||||
|
||||
if (!$unit) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Si se intenta cambiar allows_decimals
|
||||
if ($this->has('allows_decimals') && $this->allows_decimals !== $unit->allows_decimals) {
|
||||
// Verificar si hay productos con track_serials usando esta unidad
|
||||
$hasProductsWithSerials = $unit->inventories()
|
||||
->where('track_serials', true)
|
||||
->exists();
|
||||
|
||||
if ($hasProductsWithSerials) {
|
||||
$validator->errors()->add(
|
||||
'allows_decimals',
|
||||
'No se puede cambiar el tipo de unidad (decimal/entero) porque hay productos con números de serie que la utilizan'
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -24,12 +24,17 @@ public function entry(array $data): InventoryMovement
|
||||
return DB::transaction(function () use ($data) {
|
||||
$inventory = Inventory::findOrFail($data['inventory_id']);
|
||||
$warehouse = Warehouse::findOrFail($data['warehouse_id']);
|
||||
$quantity = (int) $data['quantity'];
|
||||
$quantity = (float) $data['quantity'];
|
||||
$unitCost = (float) $data['unit_cost'];
|
||||
$serialNumbers = $data['serial_numbers'] ?? null;
|
||||
|
||||
// Validar seriales si el producto los requiere
|
||||
if ($inventory->track_serials) {
|
||||
// cargar la unidad de medida
|
||||
$inventory->load('unitOfMeasure');
|
||||
|
||||
// Solo validar seriales si track_serials Y la unidad NO permite decimales
|
||||
$requiresSerials = $inventory->track_serials && !$inventory->unitOfMeasure?->allows_decimals;
|
||||
|
||||
if ($requiresSerials) {
|
||||
if (empty($serialNumbers)) {
|
||||
throw new \Exception('Este producto requiere números de serie. Debe proporcionar ' . $quantity . ' seriales.');
|
||||
}
|
||||
@ -48,13 +53,13 @@ public function entry(array $data): InventoryMovement
|
||||
}
|
||||
}
|
||||
|
||||
//Obtener stock actual para calcular costo promedio ponderado
|
||||
$curentStock = $inventory->stock;
|
||||
// Obtener stock actual para calcular costo promedio ponderado
|
||||
$currentStock = $inventory->stock;
|
||||
$currentCost = $inventory->price?->cost ?? 0.00;
|
||||
|
||||
// Calcular nuevo costo promedio ponderado
|
||||
$newCost = $this->calculateWeightedAverageCost(
|
||||
$curentStock,
|
||||
$currentStock,
|
||||
$currentCost,
|
||||
$quantity,
|
||||
$unitCost
|
||||
@ -63,8 +68,8 @@ public function entry(array $data): InventoryMovement
|
||||
// Actualizar costo en prices
|
||||
$this->updateProductCost($inventory, $newCost);
|
||||
|
||||
// Crear seriales si se proporcionan
|
||||
if (!empty($serialNumbers)) {
|
||||
// Solo crear seriales si se proporcionaron
|
||||
if ($requiresSerials && !empty($serialNumbers)) {
|
||||
foreach ($serialNumbers as $serialNumber) {
|
||||
InventorySerial::create([
|
||||
'inventory_id' => $inventory->id,
|
||||
@ -82,7 +87,7 @@ public function entry(array $data): InventoryMovement
|
||||
// Sincronizar stock desde seriales
|
||||
$inventory->syncStock();
|
||||
} else {
|
||||
// Sin seriales, actualizar stock manualmente
|
||||
// **SIN SERIALES**: Actualizar stock manualmente (permite decimales)
|
||||
$this->updateWarehouseStock($inventory->id, $warehouse->id, $quantity);
|
||||
}
|
||||
|
||||
@ -112,7 +117,7 @@ public function bulkEntry(array $data): array
|
||||
|
||||
foreach ($data['products'] as $productData) {
|
||||
$inventory = Inventory::findOrFail($productData['inventory_id']);
|
||||
$quantity = (int) $productData['quantity'];
|
||||
$quantity = (float) $productData['quantity'];
|
||||
$unitCost = (float)$productData['unit_cost'];
|
||||
$serialNumbers = $productData['serial_numbers'] ?? null;
|
||||
|
||||
@ -207,12 +212,15 @@ public function bulkExit(array $data): array
|
||||
$movements = [];
|
||||
|
||||
foreach ($data['products'] as $productData) {
|
||||
$inventory = Inventory::findOrFail($productData['inventory_id']);
|
||||
$quantity = (int) $productData['quantity'];
|
||||
$serialNumbers = (array) $productData['serial_numbers'] ?? null;
|
||||
$inventory = Inventory::with('unitOfMeasure')->findOrFail($productData['inventory_id']);
|
||||
$quantity = (float) $productData['quantity'];
|
||||
$serialNumbers = $productData['serial_numbers'] ?? null;
|
||||
|
||||
// **NUEVO**: Solo exigir seriales si track_serials Y unidad NO permite decimales
|
||||
$requiresSerials = $inventory->track_serials && !$inventory->unitOfMeasure?->allows_decimals;
|
||||
|
||||
// Validar y procesar seriales si el producto los requiere
|
||||
if ($inventory->track_serials) {
|
||||
if ($requiresSerials) {
|
||||
if (empty($serialNumbers)) {
|
||||
throw new \Exception("El producto '{$inventory->name}' requiere números de serie. Debe proporcionar {$quantity} seriales.");
|
||||
}
|
||||
@ -295,7 +303,7 @@ public function bulkTransfer(array $data): array
|
||||
|
||||
foreach ($data['products'] as $productData) {
|
||||
$inventory = Inventory::findOrFail($productData['inventory_id']);
|
||||
$quantity = (int) $productData['quantity'];
|
||||
$quantity = (float) $productData['quantity'];
|
||||
$serialNumbers = (array) ($productData['serial_numbers'] ?? null);
|
||||
|
||||
// Validar y procesar seriales si el producto los requiere
|
||||
@ -370,9 +378,9 @@ public function bulkTransfer(array $data): array
|
||||
* Calcular costo promedio ponderado
|
||||
*/
|
||||
protected function calculateWeightedAverageCost(
|
||||
int $currentStock,
|
||||
float $currentStock,
|
||||
float $currentCost,
|
||||
int $entryQuantity,
|
||||
float $entryQuantity,
|
||||
float $entryCost
|
||||
): float {
|
||||
if ($currentStock <= 0) {
|
||||
@ -399,11 +407,17 @@ public function exit(array $data): InventoryMovement
|
||||
return DB::transaction(function () use ($data) {
|
||||
$inventory = Inventory::findOrFail($data['inventory_id']);
|
||||
$warehouse = Warehouse::findOrFail($data['warehouse_id']);
|
||||
$quantity = (int) $data['quantity'];
|
||||
$serialNumbers = (array) ($data['serial_numbers'] ?? null);
|
||||
$quantity = (float) $data['quantity'];
|
||||
$serialNumbers = $data['serial_numbers'] ?? null;
|
||||
|
||||
// **NUEVO**: Cargar la unidad de medida
|
||||
$inventory->load('unitOfMeasure');
|
||||
|
||||
// **CAMBIO**: Solo validar seriales si track_serials Y la unidad NO permite decimales
|
||||
$requiresSerials = $inventory->track_serials && !$inventory->unitOfMeasure?->allows_decimals;
|
||||
|
||||
// Validar y procesar seriales si el producto los requiere
|
||||
if ($inventory->track_serials) {
|
||||
if ($requiresSerials) {
|
||||
if (empty($serialNumbers)) {
|
||||
throw new \Exception('Este producto requiere números de serie. Debe proporcionar ' . $quantity . ' seriales.');
|
||||
}
|
||||
@ -439,14 +453,14 @@ public function exit(array $data): InventoryMovement
|
||||
}
|
||||
|
||||
// Eliminar los seriales (salida definitiva)
|
||||
InventorySerial::whereIn('serial_number', $serialNumbers)
|
||||
InventorySerial::whereIn('serial_numbers', $serialNumbers)
|
||||
->where('inventory_id', $inventory->id)
|
||||
->delete();
|
||||
|
||||
// Sincronizar stock desde seriales
|
||||
$inventory->syncStock();
|
||||
} else {
|
||||
// Sin seriales, validar y decrementar stock manualmente
|
||||
// **SIN SERIALES**: Validar y decrementar stock manualmente
|
||||
$this->validateStock($inventory->id, $warehouse->id, $quantity);
|
||||
$this->updateWarehouseStock($inventory->id, $warehouse->id, -$quantity);
|
||||
}
|
||||
@ -470,19 +484,22 @@ public function exit(array $data): InventoryMovement
|
||||
public function transfer(array $data): InventoryMovement
|
||||
{
|
||||
return DB::transaction(function () use ($data) {
|
||||
$inventory = Inventory::findOrFail($data['inventory_id']);
|
||||
$inventory = Inventory::with('unitOfMeasure')->findOrFail($data['inventory_id']);
|
||||
$warehouseFrom = Warehouse::findOrFail($data['warehouse_from_id']);
|
||||
$warehouseTo = Warehouse::findOrFail($data['warehouse_to_id']);
|
||||
$quantity = (int) $data['quantity'];
|
||||
$serialNumbers = (array) ($data['serial_numbers'] ?? null);
|
||||
$quantity = (float) $data['quantity'];
|
||||
$serialNumbers = $data['serial_numbers'] ?? null;
|
||||
|
||||
// Validar que no sea el mismo almacén
|
||||
if ($warehouseFrom->id === $warehouseTo->id) {
|
||||
throw new \Exception('No se puede traspasar al mismo almacén.');
|
||||
}
|
||||
|
||||
// **NUEVO**: Solo exigir seriales si track_serials Y unidad NO permite decimales
|
||||
$requiresSerials = $inventory->track_serials && !$inventory->unitOfMeasure?->allows_decimals;
|
||||
|
||||
// Validar y procesar seriales si el producto los requiere
|
||||
if ($inventory->track_serials) {
|
||||
if ($requiresSerials) {
|
||||
if (empty($serialNumbers)) {
|
||||
throw new \Exception('Este producto requiere números de serie. Debe proporcionar ' . $quantity . ' seriales.');
|
||||
}
|
||||
@ -547,7 +564,7 @@ public function transfer(array $data): InventoryMovement
|
||||
/**
|
||||
* Actualizar stock en inventory_warehouse
|
||||
*/
|
||||
public function updateWarehouseStock(int $inventoryId, int $warehouseId, int $quantityChange): void
|
||||
public function updateWarehouseStock(int $inventoryId, int $warehouseId, float $quantityChange): void
|
||||
{
|
||||
$record = InventoryWarehouse::firstOrCreate(
|
||||
[
|
||||
@ -569,7 +586,7 @@ public function updateWarehouseStock(int $inventoryId, int $warehouseId, int $qu
|
||||
/**
|
||||
* Validar stock disponible
|
||||
*/
|
||||
public function validateStock(int $inventoryId, int $warehouseId, int $requiredQuantity): void
|
||||
public function validateStock(int $inventoryId, int $warehouseId, float $requiredQuantity): void
|
||||
{
|
||||
$record = InventoryWarehouse::where('inventory_id', $inventoryId)
|
||||
->where('warehouse_id', $warehouseId)
|
||||
@ -585,7 +602,7 @@ public function validateStock(int $inventoryId, int $warehouseId, int $requiredQ
|
||||
/**
|
||||
* Registrar movimiento de venta
|
||||
*/
|
||||
public function recordSale(int $inventoryId, int $warehouseId, int $quantity, int $saleId): InventoryMovement
|
||||
public function recordSale(int $inventoryId, int $warehouseId, float $quantity, int $saleId): InventoryMovement
|
||||
{
|
||||
return InventoryMovement::create([
|
||||
'inventory_id' => $inventoryId,
|
||||
@ -602,7 +619,7 @@ public function recordSale(int $inventoryId, int $warehouseId, int $quantity, in
|
||||
/**
|
||||
* Registrar movimiento de devolución
|
||||
*/
|
||||
public function recordReturn(int $inventoryId, int $warehouseId, int $quantity, int $returnId): InventoryMovement
|
||||
public function recordReturn(int $inventoryId, int $warehouseId, float $quantity, int $returnId): InventoryMovement
|
||||
{
|
||||
return InventoryMovement::create([
|
||||
'inventory_id' => $inventoryId,
|
||||
|
||||
@ -14,6 +14,7 @@ public function createProduct(array $data)
|
||||
'sku' => $data['sku'],
|
||||
'barcode' => $data['barcode'] ?? null,
|
||||
'category_id' => $data['category_id'],
|
||||
'unit_of_measure_id' => $data['unit_of_measure_id'],
|
||||
'track_serials' => $data['track_serials'] ?? false,
|
||||
]);
|
||||
|
||||
@ -24,7 +25,7 @@ public function createProduct(array $data)
|
||||
'tax' => $data['tax'] ?? 16.00,
|
||||
]);
|
||||
|
||||
return $inventory->load(['category', 'price']);
|
||||
return $inventory->load(['category', 'price', 'unitOfMeasure']);
|
||||
});
|
||||
}
|
||||
|
||||
@ -37,6 +38,7 @@ public function updateProduct(Inventory $inventory, array $data)
|
||||
'sku' => $data['sku'] ?? null,
|
||||
'barcode' => $data['barcode'] ?? null,
|
||||
'category_id' => $data['category_id'] ?? null,
|
||||
'unit_of_measure_id' => $data['unit_of_measure_id'] ?? null,
|
||||
'track_serials' => $data['track_serials'] ?? null,
|
||||
], fn($value) => $value !== null);
|
||||
|
||||
@ -58,7 +60,7 @@ public function updateProduct(Inventory $inventory, array $data)
|
||||
);
|
||||
}
|
||||
|
||||
return $inventory->fresh(['category', 'price']);
|
||||
return $inventory->fresh(['category', 'price', 'unitOfMeasure']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
94
app/Services/WhatsAppAuthService.php
Normal file
94
app/Services/WhatsAppAuthService.php
Normal file
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Servicio de autenticación para WhatsApp API
|
||||
*/
|
||||
class WhatsAppAuthService
|
||||
{
|
||||
protected string $loginUrl;
|
||||
protected string $email;
|
||||
protected string $password;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->loginUrl = config('services.whatsapp.login_url', 'https://whatsapp.golsystems.mx/api/login');
|
||||
$this->email = config('services.whatsapp.auth_email', 'juan.zapata@golsystems.com.mx');
|
||||
$this->password = config('services.whatsapp.auth_password');
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtener token de acceso
|
||||
*/
|
||||
public function getAccessToken(): string
|
||||
{
|
||||
if (!$this->email || !$this->password) {
|
||||
throw new \Exception('Las credenciales de WhatsApp no están configuradas en .env');
|
||||
}
|
||||
|
||||
return Cache::remember('whatsapp_access_token', 3000, function () {
|
||||
try {
|
||||
$response = Http::timeout(30)
|
||||
->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
])
|
||||
->post($this->loginUrl, [
|
||||
'email' => $this->email,
|
||||
'password' => $this->password,
|
||||
]);
|
||||
|
||||
if (!$response->successful()) {
|
||||
Log::channel('daily')->error('WhatsApp Auth Error', [
|
||||
'status' => $response->status(),
|
||||
'body' => $response->body(),
|
||||
]);
|
||||
|
||||
throw new \Exception('Error al autenticar con WhatsApp API: ' . $response->body());
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
// La API puede retornar el token en diferentes formatos
|
||||
// Ajusta según la respuesta real de la API
|
||||
$token = $data['token']
|
||||
?? $data['access_token']
|
||||
?? $data['data']['token']
|
||||
?? null;
|
||||
|
||||
if (!$token) {
|
||||
Log::channel('daily')->error('WhatsApp Auth Error: No token in response', [
|
||||
'response' => $data
|
||||
]);
|
||||
|
||||
throw new \Exception('La API de WhatsApp no retornó un token válido');
|
||||
}
|
||||
|
||||
Log::channel('daily')->info('WhatsApp token obtenido exitosamente');
|
||||
|
||||
return $token;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::channel('daily')->error('WhatsApp Auth Exception', [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Forzar renovación del token
|
||||
*/
|
||||
public function refreshToken(): string
|
||||
{
|
||||
Cache::forget('whatsapp_access_token');
|
||||
return $this->getAccessToken();
|
||||
}
|
||||
}
|
||||
213
app/Services/WhatsappService.php
Normal file
213
app/Services/WhatsappService.php
Normal file
@ -0,0 +1,213 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Servicio para envío de mensajes de WhatsApp
|
||||
*/
|
||||
class WhatsAppService
|
||||
{
|
||||
protected string $apiUrl;
|
||||
protected int $orgId;
|
||||
protected WhatsAppAuthService $authService;
|
||||
|
||||
public function __construct(WhatsAppAuthService $authService)
|
||||
{
|
||||
$this->apiUrl = config('services.whatsapp.api_url', 'https://whatsapp.golsystems.mx/api/send-whatsapp');
|
||||
$this->orgId = config('services.whatsapp.org_id', 1);
|
||||
$this->authService = $authService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enviar documento por WhatsApp
|
||||
*/
|
||||
public function sendDocument(
|
||||
string $phoneNumber,
|
||||
string $documentUrl,
|
||||
string $caption,
|
||||
string $filename,
|
||||
string $userEmail,
|
||||
string $ticket,
|
||||
string $customerName
|
||||
): array {
|
||||
try {
|
||||
// Obtener token de autenticación
|
||||
$token = $this->authService->getAccessToken();
|
||||
|
||||
// Construir el mensaje de WhatsApp
|
||||
$whatsappMessage = json_encode([
|
||||
'messaging_product' => 'whatsapp',
|
||||
'recipient_type' => 'individual',
|
||||
'to' => $this->cleanPhoneNumber($phoneNumber),
|
||||
'type' => 'document',
|
||||
'document' => [
|
||||
'link' => $documentUrl,
|
||||
'caption' => $caption,
|
||||
'filename' => $filename,
|
||||
],
|
||||
]);
|
||||
|
||||
// Preparar payload completo
|
||||
$payload = [
|
||||
'message' => $whatsappMessage,
|
||||
'email' => $userEmail,
|
||||
'org_id' => $this->orgId,
|
||||
'ticket' => $ticket,
|
||||
'customer' => $customerName,
|
||||
];
|
||||
|
||||
// Enviar petición HTTP con token Bearer
|
||||
$response = Http::timeout(30)
|
||||
->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
'Authorization' => 'Bearer ' . $token,
|
||||
])
|
||||
->post($this->apiUrl, $payload);
|
||||
|
||||
// Si el token expiró (401), renovar e intentar de nuevo
|
||||
if ($response->status() === 401) {
|
||||
Log::channel('daily')->warning('WhatsApp token expired, refreshing...');
|
||||
|
||||
$token = $this->authService->refreshToken();
|
||||
|
||||
$response = Http::timeout(30)
|
||||
->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
'Authorization' => 'Bearer ' . $token,
|
||||
])
|
||||
->post($this->apiUrl, $payload);
|
||||
}
|
||||
|
||||
// Registrar en logs
|
||||
Log::channel('daily')->info('WhatsApp message sent', [
|
||||
'phone' => $phoneNumber,
|
||||
'ticket' => $ticket,
|
||||
'customer' => $customerName,
|
||||
'status_code' => $response->status(),
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'Mensaje enviado correctamente',
|
||||
'data' => $response->json(),
|
||||
];
|
||||
}
|
||||
|
||||
// Log detallado del error
|
||||
Log::channel('daily')->error('WhatsApp API error', [
|
||||
'phone' => $phoneNumber,
|
||||
'status_code' => $response->status(),
|
||||
'error_body' => $response->body(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Error al enviar el mensaje',
|
||||
'error' => $response->body(),
|
||||
'status_code' => $response->status(),
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::channel('daily')->error('WhatsApp sending failed', [
|
||||
'phone' => $phoneNumber,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Error al conectar con el servicio de WhatsApp',
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enviar factura por WhatsApp
|
||||
*/
|
||||
public function sendInvoice(
|
||||
string $phoneNumber,
|
||||
string $pdfUrl,
|
||||
?string $xmlUrl,
|
||||
string $invoiceNumber,
|
||||
string $customerName
|
||||
): array {
|
||||
$userEmail = auth()->user()->email ?? config('mail.from.address');
|
||||
|
||||
// Enviar PDF
|
||||
$pdfResult = $this->sendDocument(
|
||||
phoneNumber: $phoneNumber,
|
||||
documentUrl: $pdfUrl,
|
||||
caption: "Factura {$invoiceNumber} - {$customerName}",
|
||||
filename: "Factura_{$invoiceNumber}.pdf",
|
||||
userEmail: $userEmail,
|
||||
ticket: $invoiceNumber,
|
||||
customerName: $customerName
|
||||
);
|
||||
|
||||
// Si hay XML y el PDF se envió correctamente, enviarlo también
|
||||
if ($xmlUrl && $pdfResult['success']) {
|
||||
$xmlResult = $this->sendDocument(
|
||||
phoneNumber: $phoneNumber,
|
||||
documentUrl: $xmlUrl,
|
||||
caption: "XML - Factura {$invoiceNumber}",
|
||||
filename: "Factura_{$invoiceNumber}.xml",
|
||||
userEmail: $userEmail,
|
||||
ticket: "{$invoiceNumber}_XML",
|
||||
customerName: $customerName
|
||||
);
|
||||
|
||||
return [
|
||||
'success' => $pdfResult['success'] && $xmlResult['success'],
|
||||
'pdf' => $pdfResult,
|
||||
'xml' => $xmlResult,
|
||||
];
|
||||
}
|
||||
|
||||
return $pdfResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enviar ticket de venta por WhatsApp
|
||||
*/
|
||||
public function sendSaleTicket(
|
||||
string $phoneNumber,
|
||||
string $ticketUrl,
|
||||
string $saleNumber,
|
||||
string $customerName
|
||||
): array {
|
||||
$userEmail = auth()->user()->email ?? config('mail.from.address');
|
||||
|
||||
return $this->sendDocument(
|
||||
phoneNumber: $phoneNumber,
|
||||
documentUrl: $ticketUrl,
|
||||
caption: "Ticket de venta {$saleNumber} - {$customerName}. Gracias por su compra.",
|
||||
filename: "Ticket_{$saleNumber}.pdf",
|
||||
userEmail: $userEmail,
|
||||
ticket: $saleNumber,
|
||||
customerName: $customerName
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Limpiar número de teléfono
|
||||
*/
|
||||
protected function cleanPhoneNumber(string $phone): string
|
||||
{
|
||||
// Eliminar todo excepto números
|
||||
$cleaned = preg_replace('/[^0-9]/', '', $phone);
|
||||
|
||||
// Asegurar que tenga código de país (52 para México)
|
||||
if (strlen($cleaned) === 10) {
|
||||
$cleaned = '52' . $cleaned;
|
||||
}
|
||||
|
||||
return $cleaned;
|
||||
}
|
||||
}
|
||||
@ -35,4 +35,12 @@
|
||||
],
|
||||
],
|
||||
|
||||
'whatsapp' => [
|
||||
'login_url' => env('WHATSAPP_LOGIN_URL', 'https://whatsapp.golsystems.mx/api/login'),
|
||||
'api_url' => env('WHATSAPP_API_URL', 'https://whatsapp.golsystems.mx/api/send-whatsapp'),
|
||||
'org_id' => env('WHATSAPP_ORG_ID', 1),
|
||||
'auth_email' => env('WHATSAPP_AUTH_EMAIL', 'juan.zapata@golsystems.com.mx'),
|
||||
'auth_password' => env('WHATSAPP_AUTH_PASSWORD'),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@ -92,7 +92,7 @@ public function run(): void
|
||||
$salesCreate = $this->onCreate('sales', 'Crear registros', $salesType, 'api');
|
||||
$salesCancel = $this->onPermission('sales.cancel', 'Cancelar venta', $salesType, 'api');
|
||||
|
||||
// Permisos de Inventario (solo lectura)
|
||||
// Permisos de Inventario
|
||||
$inventoryType = PermissionType::firstOrCreate([
|
||||
'name' => 'Inventario'
|
||||
]);
|
||||
@ -104,6 +104,26 @@ public function run(): void
|
||||
$inventoryDestroy = $this->onDestroy('inventario', 'Eliminar registro', $inventoryType, 'api');
|
||||
$inventoryImport = $this->onPermission('inventario.import', 'Importar productos desde Excel', $inventoryType, 'api');
|
||||
|
||||
// Permisos de Categorías
|
||||
$categoriesType = PermissionType::firstOrCreate([
|
||||
'name' => 'Categorías de productos'
|
||||
]);
|
||||
|
||||
$categoriesIndex = $this->onIndex('categorias', 'Mostrar datos', $categoriesType, 'api');
|
||||
$categoriesCreate = $this->onCreate('categorias', 'Crear registros', $categoriesType, 'api');
|
||||
$categoriesEdit = $this->onEdit('categorias', 'Actualizar registro', $categoriesType, 'api');
|
||||
$categoriesDestroy = $this->onDestroy('categorias', 'Eliminar registro', $categoriesType, 'api');
|
||||
|
||||
//Permisos de Unidades de medida
|
||||
$unitsType = PermissionType::firstOrCreate([
|
||||
'name' => 'Unidades de medida'
|
||||
]);
|
||||
|
||||
$unitsIndex = $this->onIndex('units', 'Mostrar datos', $unitsType, 'api');
|
||||
$unitsCreate = $this->onCreate('units', 'Crear registros', $unitsType, 'api');
|
||||
$unitsEdit = $this->onEdit('units', 'Actualizar registro', $unitsType, 'api');
|
||||
$unitsDestroy = $this->onDestroy('units', 'Eliminar registro', $unitsType, 'api');
|
||||
|
||||
|
||||
// Permisos de Clientes
|
||||
$clientsType = PermissionType::firstOrCreate([
|
||||
@ -199,6 +219,10 @@ public function run(): void
|
||||
$inventoryCreate,
|
||||
$inventoryEdit,
|
||||
$inventoryDestroy,
|
||||
$categoriesIndex,
|
||||
$categoriesCreate,
|
||||
$categoriesEdit,
|
||||
$categoriesDestroy,
|
||||
$clientIndex,
|
||||
$clientCreate,
|
||||
$clientEdit,
|
||||
@ -220,7 +244,11 @@ public function run(): void
|
||||
$movementsIndex,
|
||||
$movementsCreate,
|
||||
$movementsEdit,
|
||||
$movementsDestroy
|
||||
$movementsDestroy,
|
||||
$unitsIndex,
|
||||
$unitsCreate,
|
||||
$unitsEdit,
|
||||
$unitsDestroy
|
||||
);
|
||||
|
||||
//Operador PDV (solo permisos de operación de caja y ventas)
|
||||
|
||||
@ -15,7 +15,9 @@
|
||||
use App\Http\Controllers\App\InvoiceRequestController;
|
||||
use App\Http\Controllers\App\InventoryMovementController;
|
||||
use App\Http\Controllers\App\KardexController;
|
||||
use App\Http\Controllers\App\UnitOfMeasurementController;
|
||||
use App\Http\Controllers\App\WarehouseController;
|
||||
use App\Http\Controllers\App\WhatsappController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
/**
|
||||
@ -59,6 +61,16 @@
|
||||
Route::post('/traspaso', [InventoryMovementController::class, 'transfer']);
|
||||
});
|
||||
|
||||
// UNIDADES DE MEDIDA
|
||||
Route::prefix('unidades-medida')->group(function () {
|
||||
Route::get('/', [UnitOfMeasurementController::class, 'index']);
|
||||
Route::get('/active', [UnitOfMeasurementController::class, 'active']);
|
||||
Route::get('/{unidad}', [UnitOfMeasurementController::class, 'show']);
|
||||
Route::post('/', [UnitOfMeasurementController::class, 'store']);
|
||||
Route::put('/{unidad}', [UnitOfMeasurementController::class, 'update']);
|
||||
Route::delete('/{unidad}', [UnitOfMeasurementController::class, 'destroy']);
|
||||
});
|
||||
|
||||
//CATEGORIAS
|
||||
Route::resource('categorias', CategoryController::class);
|
||||
|
||||
@ -129,11 +141,19 @@
|
||||
Route::put('/{id}/reject', [InvoiceRequestController::class, 'reject']);
|
||||
Route::post('/{id}/upload', [InvoiceRequestController::class, 'uploadInvoiceFile']);
|
||||
});
|
||||
|
||||
// WHATSAPP
|
||||
Route::prefix('whatsapp')->group(function () {
|
||||
Route::post('/send-document', [WhatsappController::class, 'sendDocument']);
|
||||
Route::post('/send-invoice', [WhatsappController::class, 'sendInvoice']);
|
||||
Route::post('/send-ticket', [WhatsappController::class, 'sendSaleTicket']);
|
||||
});
|
||||
});
|
||||
|
||||
/** Rutas públicas */
|
||||
// Formulario de datos fiscales para facturación
|
||||
Route::prefix('facturacion')->group(function () {
|
||||
Route::get('/check-rfc', [InvoiceController::class, 'checkRfc']);
|
||||
Route::get('/{invoiceNumber}', [InvoiceController::class, 'show']);
|
||||
Route::post('/{invoiceNumber}', [InvoiceController::class, 'store']);
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user