WIP: Serials
This commit is contained in:
parent
08871b8dde
commit
810aff1b0e
@ -15,11 +15,11 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Controlador de usuarios
|
* Controlador de usuarios
|
||||||
*
|
*
|
||||||
* Permite la administración de los usuarios en general.
|
* Permite la administración de los usuarios en general.
|
||||||
*
|
*
|
||||||
* @author Moisés Cortés C <moises.cortes@notsoweb.com>
|
* @author Moisés Cortés C <moises.cortes@notsoweb.com>
|
||||||
*
|
*
|
||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
*/
|
*/
|
||||||
class UserController extends Controller
|
class UserController extends Controller
|
||||||
@ -29,7 +29,7 @@ class UserController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$users = User::orderBy('name');
|
$users = User::orderBy('name')->where('id', '!=', 1);
|
||||||
|
|
||||||
QuerySupport::queryByKeys($users, ['name', 'email']);
|
QuerySupport::queryByKeys($users, ['name', 'email']);
|
||||||
|
|
||||||
@ -152,7 +152,7 @@ public function activity(UserActivityRequest $request, User $user)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ApiResponse::OK->response([
|
return ApiResponse::OK->response([
|
||||||
'models' =>
|
'models' =>
|
||||||
$model->orderBy('created_at', 'desc')
|
$model->orderBy('created_at', 'desc')
|
||||||
->paginate(config('app.pagination'))
|
->paginate(config('app.pagination'))
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -10,11 +10,24 @@ class ClientController extends Controller
|
|||||||
{
|
{
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$clients = Client::where('name', 'LIKE', "%{$request->q}%")
|
$query = Client::query();
|
||||||
->orderBy('name')
|
|
||||||
->paginate(config('app.pagination'));
|
|
||||||
|
|
||||||
return ApiResponse::OK->response(['clients' => $clients]);
|
if ($request->has('with')) {
|
||||||
|
$relations = explode(',', $request->with);
|
||||||
|
$query->with($relations);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('q') && $request->q) {
|
||||||
|
$query->where(function($q) use ($request) {
|
||||||
|
$q->where('name', 'like', "%{$request->q}%")
|
||||||
|
->orWhere('email', 'like', "%{$request->q}%")
|
||||||
|
->orWhere('rfc', 'like', "%{$request->q}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'clients' => $query->paginate(config('app.pagination')),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function show(Client $client)
|
public function show(Client $client)
|
||||||
|
|||||||
185
app/Http/Controllers/App/FacturaDataController.php
Normal file
185
app/Http/Controllers/App/FacturaDataController.php
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\App;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\Sale;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||||
|
|
||||||
|
class FacturaDataController 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'
|
||||||
|
])
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$sale) {
|
||||||
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
|
'message' => 'Venta no encontrada'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si ya tiene datos de facturación
|
||||||
|
if ($sale->client_id) {
|
||||||
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
|
'message' => 'Esta venta ya tiene datos de facturación registrados',
|
||||||
|
'client' => $sale->client,
|
||||||
|
'sale' => $this->formatSaleData($sale)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'sale' => $this->formatSaleData($sale)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guarda los datos fiscales del cliente para la venta.
|
||||||
|
*/
|
||||||
|
public function store(Request $request, string $invoiceNumber)
|
||||||
|
{
|
||||||
|
$sale = Sale::where('invoice_number', $invoiceNumber)
|
||||||
|
->with('details.serials')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$sale) {
|
||||||
|
return ApiResponse::INTERNAL_ERROR->response([
|
||||||
|
'message' => 'Venta no encontrada'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($sale->client_id) {
|
||||||
|
return ApiResponse::BAD_REQUEST->response([
|
||||||
|
'message' => 'Esta venta ya tiene datos de facturación registrados'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar que la venta esté completada
|
||||||
|
if ($sale->status !== 'completed') {
|
||||||
|
return ApiResponse::BAD_REQUEST->response([
|
||||||
|
'message' => 'Solo se pueden facturar ventas completadas'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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:10',
|
||||||
|
], [
|
||||||
|
'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();
|
||||||
|
|
||||||
|
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'],
|
||||||
|
]);
|
||||||
|
} 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'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asociar cliente a la venta
|
||||||
|
$sale->update(['client_id' => $client->id]);
|
||||||
|
|
||||||
|
// Recargar relaciones
|
||||||
|
$sale->load([
|
||||||
|
'client',
|
||||||
|
'details.inventory.category',
|
||||||
|
'details.serials',
|
||||||
|
'user:id,name,email'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'message' => 'Datos de facturación guardados correctamente',
|
||||||
|
'client' => $client,
|
||||||
|
'sale' => $this->formatSaleData($sale)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
171
app/Http/Controllers/App/InventorySerialController.php
Normal file
171
app/Http/Controllers/App/InventorySerialController.php
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
<?php namespace App\Http\Controllers\App;
|
||||||
|
/**
|
||||||
|
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||||
|
*/
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Inventory;
|
||||||
|
use App\Models\InventorySerial;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Notsoweb\ApiResponse\Enums\ApiResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controlador para gestión de números de serie
|
||||||
|
*
|
||||||
|
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
class InventorySerialController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Listar seriales de un producto
|
||||||
|
*/
|
||||||
|
public function index(Inventory $inventory, Request $request)
|
||||||
|
{
|
||||||
|
$query = $inventory->serials();
|
||||||
|
|
||||||
|
if ($request->has('status')) {
|
||||||
|
$query->where('status', $request->status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('q')) {
|
||||||
|
$query->where('serial_number', 'like', "%{$request->q}%");
|
||||||
|
}
|
||||||
|
|
||||||
|
$serials = $query->orderBy('created_at', 'desc')
|
||||||
|
->paginate(config('app.pagination'));
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'serials' => $serials,
|
||||||
|
'inventory' => $inventory->load('category'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mostrar un serial específico
|
||||||
|
*/
|
||||||
|
public function show(Inventory $inventory, InventorySerial $serial)
|
||||||
|
{
|
||||||
|
// Verificar que el serial pertenece al inventario
|
||||||
|
if ($serial->inventory_id !== $inventory->id) {
|
||||||
|
return ApiResponse::NOT_FOUND->response([
|
||||||
|
'message' => 'Serial no encontrado para este inventario'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'serial' => $serial->load('saleDetail'),
|
||||||
|
'inventory' => $inventory->load('category'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crear un nuevo serial
|
||||||
|
*/
|
||||||
|
public function store(Inventory $inventory, Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'serial_number' => ['required', 'string', 'unique:inventory_serials,serial_number'],
|
||||||
|
'notes' => ['nullable', 'string'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$serial = InventorySerial::create([
|
||||||
|
'inventory_id' => $inventory->id,
|
||||||
|
'serial_number' => $request->serial_number,
|
||||||
|
'status' => 'disponible',
|
||||||
|
'notes' => $request->notes,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sincronizar stock
|
||||||
|
$inventory->syncStock();
|
||||||
|
|
||||||
|
return ApiResponse::CREATED->response([
|
||||||
|
'serial' => $serial,
|
||||||
|
'inventory' => $inventory->fresh(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actualizar un serial
|
||||||
|
*/
|
||||||
|
public function update(Inventory $inventory, InventorySerial $serial, Request $request)
|
||||||
|
{
|
||||||
|
// Verificar que el serial pertenece al inventario
|
||||||
|
if ($serial->inventory_id !== $inventory->id) {
|
||||||
|
return ApiResponse::NOT_FOUND->response([
|
||||||
|
'message' => 'Serial no encontrado para este inventario'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->validate([
|
||||||
|
'serial_number' => ['sometimes', 'string', 'unique:inventory_serials,serial_number,' . $serial->id],
|
||||||
|
'status' => ['sometimes', 'in:disponible,vendido,dañado,reservado'],
|
||||||
|
'notes' => ['nullable', 'string'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$serial->update($request->only(['serial_number', 'status', 'notes']));
|
||||||
|
|
||||||
|
// Sincronizar stock del inventario
|
||||||
|
$inventory->syncStock();
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'serial' => $serial->fresh(),
|
||||||
|
'inventory' => $inventory->fresh(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eliminar un serial
|
||||||
|
*/
|
||||||
|
public function destroy(Inventory $inventory, InventorySerial $serial)
|
||||||
|
{
|
||||||
|
// Verificar que el serial pertenece al inventario
|
||||||
|
if ($serial->inventory_id !== $inventory->id) {
|
||||||
|
return ApiResponse::NOT_FOUND->response([
|
||||||
|
'message' => 'Serial no encontrado para este inventario'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$serial->delete();
|
||||||
|
|
||||||
|
// Sincronizar stock
|
||||||
|
$inventory->syncStock();
|
||||||
|
|
||||||
|
return ApiResponse::OK->response([
|
||||||
|
'message' => 'Serial eliminado exitosamente',
|
||||||
|
'inventory' => $inventory->fresh(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Importar múltiples seriales
|
||||||
|
*/
|
||||||
|
public function bulkStore(Inventory $inventory, Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'serial_numbers' => ['required', 'array', 'min:1'],
|
||||||
|
'serial_numbers.*' => ['required', 'string', 'unique:inventory_serials,serial_number'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$created = [];
|
||||||
|
|
||||||
|
foreach ($request->serial_numbers as $serialNumber) {
|
||||||
|
$serial = InventorySerial::create([
|
||||||
|
'inventory_id' => $inventory->id,
|
||||||
|
'serial_number' => $serialNumber,
|
||||||
|
'status' => 'disponible',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$created[] = $serial;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sincronizar stock
|
||||||
|
$inventory->syncStock();
|
||||||
|
|
||||||
|
return ApiResponse::CREATED->response([
|
||||||
|
'serials' => $created,
|
||||||
|
'count' => count($created),
|
||||||
|
'inventory' => $inventory->fresh(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,7 +17,7 @@ public function __construct(
|
|||||||
|
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$sales = Sale::with(['details.inventory', 'user'])
|
$sales = Sale::with(['details.inventory', 'user', 'client'])
|
||||||
->orderBy('created_at', 'desc');
|
->orderBy('created_at', 'desc');
|
||||||
|
|
||||||
if ($request->has('q') && $request->q) {
|
if ($request->has('q') && $request->q) {
|
||||||
@ -43,7 +43,7 @@ public function index(Request $request)
|
|||||||
public function show(Sale $sale)
|
public function show(Sale $sale)
|
||||||
{
|
{
|
||||||
return ApiResponse::OK->response([
|
return ApiResponse::OK->response([
|
||||||
'model' => $sale->load(['details.inventory', 'user'])
|
'model' => $sale->load(['details.inventory', 'user', 'client'])
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -37,7 +37,9 @@ public function rules(): array
|
|||||||
'items.*.quantity' => ['required', 'integer', 'min:1'],
|
'items.*.quantity' => ['required', 'integer', 'min:1'],
|
||||||
'items.*.unit_price' => ['required', 'numeric', 'min:0'],
|
'items.*.unit_price' => ['required', 'numeric', 'min:0'],
|
||||||
'items.*.subtotal' => ['required', 'numeric', 'min:0'],
|
'items.*.subtotal' => ['required', 'numeric', 'min:0'],
|
||||||
];
|
'items.*.serial_numbers' => ['nullable', 'array'],
|
||||||
|
'items.*.serial_numbers.*' => ['string', 'exists:inventory_serials,serial_number'],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
use App\Models\Price;
|
use App\Models\Price;
|
||||||
use App\Models\Category;
|
use App\Models\Category;
|
||||||
use App\Http\Requests\App\InventoryImportRequest;
|
use App\Http\Requests\App\InventoryImportRequest;
|
||||||
|
use App\Models\InventorySerial;
|
||||||
use Maatwebsite\Excel\Concerns\ToModel;
|
use Maatwebsite\Excel\Concerns\ToModel;
|
||||||
use Maatwebsite\Excel\Concerns\WithHeadingRow;
|
use Maatwebsite\Excel\Concerns\WithHeadingRow;
|
||||||
use Maatwebsite\Excel\Concerns\WithValidation;
|
use Maatwebsite\Excel\Concerns\WithValidation;
|
||||||
@ -48,6 +49,7 @@ public function map($row): array
|
|||||||
'costo' => $row['costo'] ?? null,
|
'costo' => $row['costo'] ?? null,
|
||||||
'precio_venta' => $row['precio_venta'] ?? null,
|
'precio_venta' => $row['precio_venta'] ?? null,
|
||||||
'impuesto' => $row['impuesto'] ?? null,
|
'impuesto' => $row['impuesto'] ?? null,
|
||||||
|
'numeros_serie' => $row['numeros_serie'] ?? null, // Nueva columna: separados por comas
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,13 +84,13 @@ public function model(array $row)
|
|||||||
$categoryId = $category->id;
|
$categoryId = $category->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crear el producto en inventario (solo con campos específicos)
|
// Crear el producto en inventario
|
||||||
$inventory = new Inventory();
|
$inventory = new Inventory();
|
||||||
$inventory->name = trim($row['nombre']);
|
$inventory->name = trim($row['nombre']);
|
||||||
$inventory->sku = !empty($row['sku']) ? trim($row['sku']) : null;
|
$inventory->sku = !empty($row['sku']) ? trim($row['sku']) : null;
|
||||||
$inventory->barcode = !empty($row['codigo_barras']) ? trim($row['codigo_barras']) : null;
|
$inventory->barcode = !empty($row['codigo_barras']) ? trim($row['codigo_barras']) : null;
|
||||||
$inventory->category_id = $categoryId;
|
$inventory->category_id = $categoryId;
|
||||||
$inventory->stock = (int) $row['stock'];
|
$inventory->stock = 0; // Se calculará automáticamente
|
||||||
$inventory->is_active = true;
|
$inventory->is_active = true;
|
||||||
$inventory->save();
|
$inventory->save();
|
||||||
|
|
||||||
@ -100,6 +102,37 @@ public function model(array $row)
|
|||||||
'tax' => !empty($row['impuesto']) ? (float) $row['impuesto'] : 0,
|
'tax' => !empty($row['impuesto']) ? (float) $row['impuesto'] : 0,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Crear números de serie si se proporcionan
|
||||||
|
if (!empty($row['numeros_serie'])) {
|
||||||
|
$serials = explode(',', $row['numeros_serie']);
|
||||||
|
|
||||||
|
foreach ($serials as $serial) {
|
||||||
|
$serial = trim($serial);
|
||||||
|
|
||||||
|
if (!empty($serial)) {
|
||||||
|
InventorySerial::create([
|
||||||
|
'inventory_id' => $inventory->id,
|
||||||
|
'serial_number' => $serial,
|
||||||
|
'status' => 'disponible',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Si no se proporcionan seriales, generar automáticamente
|
||||||
|
$stockQuantity = (int) $row['stock'];
|
||||||
|
|
||||||
|
for ($i = 1; $i <= $stockQuantity; $i++) {
|
||||||
|
InventorySerial::create([
|
||||||
|
'inventory_id' => $inventory->id,
|
||||||
|
'serial_number' => $inventory->sku . '-' . str_pad($i, 4, '0', STR_PAD_LEFT),
|
||||||
|
'status' => 'disponible',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sincronizar stock
|
||||||
|
$inventory->syncStock();
|
||||||
|
|
||||||
$this->imported++;
|
$this->imported++;
|
||||||
|
|
||||||
return $inventory;
|
return $inventory;
|
||||||
|
|||||||
@ -10,5 +10,14 @@ class Client extends Model
|
|||||||
'phone',
|
'phone',
|
||||||
'address',
|
'address',
|
||||||
'rfc',
|
'rfc',
|
||||||
|
'razon_social',
|
||||||
|
'regimen_fiscal',
|
||||||
|
'cp_fiscal',
|
||||||
|
'uso_cfdi',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function sales()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Sale::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,4 +37,34 @@ public function price()
|
|||||||
{
|
{
|
||||||
return $this->hasOne(Price::class);
|
return $this->hasOne(Price::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function serials()
|
||||||
|
{
|
||||||
|
return $this->hasMany(InventorySerial::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtener seriales disponibles
|
||||||
|
*/
|
||||||
|
public function availableSerials()
|
||||||
|
{
|
||||||
|
return $this->hasMany(InventorySerial::class)
|
||||||
|
->where('status', 'disponible');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcular stock basado en seriales disponibles
|
||||||
|
*/
|
||||||
|
public function getAvailableStockAttribute(): int
|
||||||
|
{
|
||||||
|
return $this->availableSerials()->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sincronizar el campo stock con los seriales disponibles
|
||||||
|
*/
|
||||||
|
public function syncStock(): void
|
||||||
|
{
|
||||||
|
$this->update(['stock' => $this->getAvailableStockAttribute()]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
64
app/Models/InventorySerial.php
Normal file
64
app/Models/InventorySerial.php
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<?php namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modelo para números de serie de inventario
|
||||||
|
*
|
||||||
|
* @author Moisés Cortés C. <moises.cortes@notsoweb.com>
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
class InventorySerial extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'inventory_id',
|
||||||
|
'serial_number',
|
||||||
|
'status',
|
||||||
|
'sale_detail_id',
|
||||||
|
'notes',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'status' => 'string',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function inventory()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Inventory::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saleDetail()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(SaleDetail::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verificar si el serial está disponible
|
||||||
|
*/
|
||||||
|
public function isAvailable(): bool
|
||||||
|
{
|
||||||
|
return $this->status === 'disponible';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marcar como vendido
|
||||||
|
*/
|
||||||
|
public function markAsSold(int $saleDetailId): void
|
||||||
|
{
|
||||||
|
$this->update([
|
||||||
|
'status' => 'vendido',
|
||||||
|
'sale_detail_id' => $saleDetailId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marcar como disponible (ej: cancelación de venta)
|
||||||
|
*/
|
||||||
|
public function markAsAvailable(): void
|
||||||
|
{
|
||||||
|
$this->update([
|
||||||
|
'status' => 'disponible',
|
||||||
|
'sale_detail_id' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,6 +17,7 @@ class Sale extends Model
|
|||||||
{
|
{
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'user_id',
|
'user_id',
|
||||||
|
'client_id',
|
||||||
'cash_register_id',
|
'cash_register_id',
|
||||||
'invoice_number',
|
'invoice_number',
|
||||||
'subtotal',
|
'subtotal',
|
||||||
@ -50,4 +51,9 @@ public function cashRegister()
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(CashRegister::class);
|
return $this->belongsTo(CashRegister::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function client()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Client::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,4 +38,17 @@ public function inventory()
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(Inventory::class);
|
return $this->belongsTo(Inventory::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function serials()
|
||||||
|
{
|
||||||
|
return $this->hasMany(InventorySerial::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtener números de serie vendidos
|
||||||
|
*/
|
||||||
|
public function getSerialNumbersAttribute(): array
|
||||||
|
{
|
||||||
|
return $this->serials()->pluck('serial_number')->toArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
use App\Models\Sale;
|
use App\Models\Sale;
|
||||||
use App\Models\SaleDetail;
|
use App\Models\SaleDetail;
|
||||||
use App\Models\Inventory;
|
use App\Models\Inventory;
|
||||||
|
use App\Models\InventorySerial;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class SaleService
|
class SaleService
|
||||||
@ -38,27 +39,60 @@ public function createSale(array $data)
|
|||||||
'status' => $data['status'] ?? 'completed',
|
'status' => $data['status'] ?? 'completed',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 2. Crear los detalles de la venta y actualizar stock
|
// 2. Crear los detalles de la venta y asignar seriales
|
||||||
foreach ($data['items'] as $item) {
|
foreach ($data['items'] as $item) {
|
||||||
// Crear detalle de venta
|
// Crear detalle de venta
|
||||||
SaleDetail::create([
|
$saleDetail = SaleDetail::create([
|
||||||
'sale_id' => $sale->id,
|
'sale_id' => $sale->id,
|
||||||
'inventory_id' => $item['inventory_id'],
|
'inventory_id' => $item['inventory_id'],
|
||||||
'product_name' => $item['product_name'],
|
'product_name' => $item['product_name'],
|
||||||
'quantity' => $item['quantity'],
|
'quantity' => $item['quantity'],
|
||||||
'unit_price' => $item['unit_price'],
|
'unit_price' => $item['unit_price'],
|
||||||
'subtotal' => $item['subtotal'],
|
'subtotal' => $item['subtotal'],
|
||||||
|
'serial_numbers' => $item['serial_numbers'] ?? null, // Si vienen del frontend
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Descontar del stock
|
// Obtener el inventario
|
||||||
$inventory = Inventory::find($item['inventory_id']);
|
$inventory = Inventory::find($item['inventory_id']);
|
||||||
|
|
||||||
if ($inventory) {
|
if ($inventory) {
|
||||||
$inventory->decrement('stock', $item['quantity']);
|
// Si se proporcionaron números de serie específicos
|
||||||
|
if (isset($item['serial_numbers']) && is_array($item['serial_numbers'])) {
|
||||||
|
foreach ($item['serial_numbers'] as $serialNumber) {
|
||||||
|
$serial = InventorySerial::where('inventory_id', $inventory->id)
|
||||||
|
->where('serial_number', $serialNumber)
|
||||||
|
->where('status', 'disponible')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($serial) {
|
||||||
|
$serial->markAsSold($saleDetail->id);
|
||||||
|
} else {
|
||||||
|
throw new \Exception("Serial {$serialNumber} no disponible");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Asignar automáticamente los primeros N seriales disponibles
|
||||||
|
$serials = InventorySerial::where('inventory_id', $inventory->id)
|
||||||
|
->where('status', 'disponible')
|
||||||
|
->limit($item['quantity'])
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($serials->count() < $item['quantity']) {
|
||||||
|
throw new \Exception("Stock insuficiente de seriales para {$item['product_name']}");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($serials as $serial) {
|
||||||
|
$serial->markAsSold($saleDetail->id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sincronizar el stock
|
||||||
|
$inventory->syncStock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Retornar la venta con sus relaciones cargadas
|
// 3. Retornar la venta con sus relaciones cargadas
|
||||||
return $sale->load(['details.inventory', 'user']);
|
return $sale->load(['details.inventory', 'details.serials', 'user']);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,18 +108,22 @@ public function cancelSale(Sale $sale)
|
|||||||
throw new \Exception('Solo se pueden cancelar ventas completadas.');
|
throw new \Exception('Solo se pueden cancelar ventas completadas.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restaurar stock de cada producto
|
// Restaurar seriales a disponible
|
||||||
foreach ($sale->details as $detail) {
|
foreach ($sale->details as $detail) {
|
||||||
$inventory = Inventory::find($detail->inventory_id);
|
$serials = InventorySerial::where('sale_detail_id', $detail->id)->get();
|
||||||
if ($inventory) {
|
|
||||||
$inventory->increment('stock', $detail->quantity);
|
foreach ($serials as $serial) {
|
||||||
|
$serial->markAsAvailable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sincronizar stock
|
||||||
|
$detail->inventory->syncStock();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marcar venta como cancelada
|
// Marcar venta como cancelada
|
||||||
$sale->update(['status' => 'cancelled']);
|
$sale->update(['status' => 'cancelled']);
|
||||||
|
|
||||||
return $sale->fresh(['details.inventory', 'user']);
|
return $sale->fresh(['details.inventory', 'details.serials', 'user']);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@ public function up(): void
|
|||||||
$table->integer('quantity');
|
$table->integer('quantity');
|
||||||
$table->decimal('unit_price', 10, 2);
|
$table->decimal('unit_price', 10, 2);
|
||||||
$table->decimal('subtotal', 10, 2);
|
$table->decimal('subtotal', 10, 2);
|
||||||
|
$table->json('serial_numbers')->nullable();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
<?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('inventory_serials', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('inventory_id')->constrained('inventories')->onDelete('cascade');
|
||||||
|
$table->string('serial_number')->unique();
|
||||||
|
$table->enum('status', ['disponible', 'vendido'])->default('disponible');
|
||||||
|
$table->foreignId('sale_detail_id')->nullable()->constrained('sale_details')->onDelete('set null');
|
||||||
|
$table->text('notes')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index(['inventory_id', 'status']);
|
||||||
|
$table->index('serial_number');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('inventory_serials');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('sales', function (Blueprint $table) {
|
||||||
|
$table->foreignId('client_id')->nullable()->after('user_id')->constrained()->onDelete('set null');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('clients', function (Blueprint $table) {
|
||||||
|
$table->string('rfc', 13)->nullable()->change();
|
||||||
|
|
||||||
|
// Datos fiscales para facturación
|
||||||
|
$table->string('razon_social')->nullable()->after('rfc');
|
||||||
|
$table->string('regimen_fiscal')->nullable()->after('razon_social');
|
||||||
|
$table->string('cp_fiscal', 5)->nullable()->after('regimen_fiscal');
|
||||||
|
$table->string('uso_cfdi')->nullable()->after('cp_fiscal');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('sales', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['client_id']);
|
||||||
|
$table->dropColumn('client_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('clients', function (Blueprint $table) {
|
||||||
|
$table->dropColumn(['razon_social', 'regimen_fiscal', 'cp_fiscal', 'uso_cfdi']);
|
||||||
|
|
||||||
|
// Revertimos el cambio en RFC a su estado original (varchar 255)
|
||||||
|
$table->string('rfc', 255)->comment('')->nullable()->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -9,6 +9,7 @@
|
|||||||
use App\Models\PermissionType;
|
use App\Models\PermissionType;
|
||||||
use App\Models\Role;
|
use App\Models\Role;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Notsoweb\LaravelCore\Traits\MySql\RolePermission;
|
use Notsoweb\LaravelCore\Traits\MySql\RolePermission;
|
||||||
use Spatie\Permission\Models\Permission;
|
use Spatie\Permission\Models\Permission;
|
||||||
|
|
||||||
@ -28,6 +29,19 @@ class RoleSeeder extends Seeder
|
|||||||
*/
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
|
|
||||||
|
// Limpiar tablas de permisos para poder re-ejecutar
|
||||||
|
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
|
||||||
|
|
||||||
|
DB::statement('SET FOREIGN_KEY_CHECKS=0;');
|
||||||
|
DB::table('role_has_permissions')->truncate();
|
||||||
|
DB::table('model_has_permissions')->truncate();
|
||||||
|
DB::table('model_has_roles')->truncate();
|
||||||
|
DB::table('permissions')->truncate();
|
||||||
|
DB::table('roles')->truncate();
|
||||||
|
DB::table('permission_types')->truncate();
|
||||||
|
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
|
||||||
|
|
||||||
$users = PermissionType::create([
|
$users = PermissionType::create([
|
||||||
'name' => 'Usuarios'
|
'name' => 'Usuarios'
|
||||||
]);
|
]);
|
||||||
@ -95,7 +109,13 @@ public function run(): void
|
|||||||
'name' => 'Inventario'
|
'name' => 'Inventario'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$inventoryIndex = $this->onIndex('inventario', 'Mostrar datos', $inventoryType, 'api');
|
|
||||||
|
$inventoryIndex = $this->onIndex('inventario', 'Mostrar datos', $inventoryType, 'api');
|
||||||
|
$inventoryCreate = $this->onCreate('inventario', 'Crear registros', $inventoryType, 'api');
|
||||||
|
$inventoryEdit = $this->onEdit('inventario', 'Actualizar registro', $inventoryType, 'api');
|
||||||
|
$inventoryDestroy = $this->onDestroy('inventario', 'Eliminar registro', $inventoryType, 'api');
|
||||||
|
$inventoryImport = $this->onPermission('inventario.import', 'Importar productos desde Excel', $inventoryType, 'api');
|
||||||
|
|
||||||
|
|
||||||
// Permisos de Clientes
|
// Permisos de Clientes
|
||||||
$clientsType = PermissionType::create([
|
$clientsType = PermissionType::create([
|
||||||
@ -146,6 +166,9 @@ public function run(): void
|
|||||||
$salesCreate,
|
$salesCreate,
|
||||||
$salesCancel,
|
$salesCancel,
|
||||||
$inventoryIndex,
|
$inventoryIndex,
|
||||||
|
$inventoryCreate,
|
||||||
|
$inventoryEdit,
|
||||||
|
$inventoryDestroy,
|
||||||
$clientIndex,
|
$clientIndex,
|
||||||
$clientCreate,
|
$clientCreate,
|
||||||
$clientEdit,
|
$clientEdit,
|
||||||
@ -168,9 +191,9 @@ public function run(): void
|
|||||||
$salesCreate, // Crear ventas
|
$salesCreate, // Crear ventas
|
||||||
// Inventario (solo lectura)
|
// Inventario (solo lectura)
|
||||||
$inventoryIndex, // Listar productos
|
$inventoryIndex, // Listar productos
|
||||||
|
$inventoryImport, // Importar productos
|
||||||
// Clientes
|
// Clientes
|
||||||
$clientIndex, // Buscar clientes
|
$clientIndex, // Buscar clientes
|
||||||
$clientCreate // Crear clientes
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
<?php namespace Database\Seeders;
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
* @copyright (c) 2025 Notsoweb Software (https://notsoweb.com) - All Rights Reserved
|
||||||
*/
|
*/
|
||||||
@ -23,32 +26,38 @@ public function run(): void
|
|||||||
{
|
{
|
||||||
$developer = UserSecureSupport::create('developer@golsystems.com');
|
$developer = UserSecureSupport::create('developer@golsystems.com');
|
||||||
|
|
||||||
User::create([
|
User::firstOrCreate(
|
||||||
'name' => 'Developer',
|
['email' => $developer->email],
|
||||||
'paternal' => 'Golsystems',
|
[
|
||||||
'maternal' => 'Dev',
|
'name' => 'Developer',
|
||||||
'email' => $developer->email,
|
'paternal' => 'Golsystems',
|
||||||
'password' => $developer->hash,
|
'maternal' => 'Dev',
|
||||||
])->assignRole(__('developer'));
|
'password' => $developer->hash,
|
||||||
|
]
|
||||||
|
)->assignRole('developer');
|
||||||
|
|
||||||
$admin = UserSecureSupport::create('admin@golsystems.com');
|
$admin = UserSecureSupport::create('admin@golsystems.com');
|
||||||
|
|
||||||
User::create([
|
User::firstOrCreate(
|
||||||
'name' => 'Admin',
|
['email' => $admin->email],
|
||||||
'paternal' => 'Golsystems',
|
[
|
||||||
'maternal' => 'Dev',
|
'name' => 'Admin',
|
||||||
'email' => $admin->email,
|
'paternal' => 'Golsystems',
|
||||||
'password' => 'SoyAdmin123..',
|
'maternal' => 'Dev',
|
||||||
])->assignRole(__('admin'));
|
'password' => 'SoyAdmin123..',
|
||||||
|
]
|
||||||
|
)->assignRole('admin');
|
||||||
|
|
||||||
$operadorPdv = UserSecureSupport::create('opv@golsystems.com');
|
$operadorPdv = UserSecureSupport::create('opv@golsystems.com');
|
||||||
|
|
||||||
User::create([
|
User::firstOrCreate(
|
||||||
'name' => 'Operador PDV',
|
['email' => $operadorPdv->email],
|
||||||
'paternal' => 'Golsystems',
|
[
|
||||||
'maternal' => 'Dev',
|
'name' => 'Operador PDV',
|
||||||
'email' => $operadorPdv->email,
|
'paternal' => 'Golsystems',
|
||||||
'password' => $operadorPdv->hash,
|
'maternal' => 'Dev',
|
||||||
])->assignRole(__('operador_pdv'));
|
'password' => $operadorPdv->hash,
|
||||||
|
]
|
||||||
|
)->assignRole('operador_pdv');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,8 @@
|
|||||||
use App\Http\Controllers\App\PriceController;
|
use App\Http\Controllers\App\PriceController;
|
||||||
use App\Http\Controllers\App\ReportController;
|
use App\Http\Controllers\App\ReportController;
|
||||||
use App\Http\Controllers\App\SaleController;
|
use App\Http\Controllers\App\SaleController;
|
||||||
|
use App\Http\Controllers\App\FacturaDataController;
|
||||||
|
use App\Http\Controllers\App\InventorySerialController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,6 +34,11 @@
|
|||||||
Route::post('inventario/import', [InventoryController::class, 'import']);
|
Route::post('inventario/import', [InventoryController::class, 'import']);
|
||||||
Route::get('inventario/template/download', [InventoryController::class, 'downloadTemplate']);
|
Route::get('inventario/template/download', [InventoryController::class, 'downloadTemplate']);
|
||||||
|
|
||||||
|
// Números de serie
|
||||||
|
Route::resource('inventario.serials', InventorySerialController::class)
|
||||||
|
->except(['create', 'edit']);
|
||||||
|
Route::post('inventario/{inventario}/serials/bulk', [InventorySerialController::class, 'bulkStore']);
|
||||||
|
|
||||||
//CATEGORIAS
|
//CATEGORIAS
|
||||||
Route::resource('categorias', CategoryController::class);
|
Route::resource('categorias', CategoryController::class);
|
||||||
|
|
||||||
@ -62,4 +69,8 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
/** Rutas públicas */
|
/** Rutas públicas */
|
||||||
// Tus 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']);
|
||||||
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user