funcion: notas de producto #36

Merged
atasistro merged 15 commits from funcion/notas-producto into master 2024-11-12 22:08:37 -03:00
18 changed files with 229 additions and 57 deletions

View File

@ -233,10 +233,36 @@ class GrupoDeCompra extends Model
// Guardar en un archivo .csv
try {
$writer = Writer::createFromPath(resource_path('csv/exports/total-pedidos.csv'), 'w');
$writer->insertAll($planilla);
$writer = Writer::createFromPath(resource_path('csv/exports/total-pedidos.csv'), 'w');
$writer->insertAll($planilla);
} catch (CannotInsertRecord $e) {
var_export($e->getRecords());
var_export($e->getRecords());
}
}
public static function exportarProductosConNotasEnCSV() {
$gdcs = GrupoDeCompra::all();
foreach ($gdcs as $i => $gdc) {
$productos_en_pedido = DB::table('pedidos_aprobados')->where('grupo_de_compra_id', $gdc->id)->get()->keyBy('producto_id');
$pedidos = $gdc->pedidosAprobados();
foreach ($productos_en_pedido as $id => $producto_pedido) {
foreach ($pedidos as $pedido) {
$producto = $pedido->productos()->find($id);
if ($producto != null && $producto->requiere_notas) {
$planilla[$i+1][0] = $gdc->nombre;
$planilla[$i+1][1] = $producto->nombre;
$planilla[$i+1][2] = $producto->pivot->cantidad;
$planilla[$i+1][3] = $producto->pivot->notas;
}
}
}
}
// Guardar en un archivo .csv
try {
$writer = Writer::createFromPath(resource_path('csv/exports/pedidos-notas.csv'), 'w');
$writer->insertAll($planilla);
} catch (CannotInsertRecord $e) {
var_export($e->getRecords());
}
}
}

View File

@ -79,6 +79,7 @@ class SubpedidoController extends Controller
$valid = request()->validate([
'cantidad' => 'required|min:0',
'notas' => 'nullable',
'producto_id' => [
'required',
Rule::in(Producto::all()->pluck('id')),
@ -86,7 +87,11 @@ class SubpedidoController extends Controller
]);
$producto = Producto::find($valid['producto_id']);
$subpedido->syncProducto($producto, $valid['cantidad']);
$notas = $valid['notas'];
if ($notas == null) {
$notas = "";
}
$subpedido->syncProducto($producto, $valid['cantidad'], $notas);
return new SubpedidoResource($subpedido);
}

View File

@ -15,6 +15,12 @@ class ComprasController
$file = resource_path('csv/exports/total-pedidos.csv');
return response()->download($file);
}
public function descargarNotas() {
GrupoDeCompra::exportarProductosConNotasEnCSV();
$file = resource_path('csv/exports/pedidos-notas.csv');
return response()->download($file);
}
public function show()
{

View File

@ -25,7 +25,8 @@ class ProductoResource extends JsonResource
'imagen' => optional($this->poster)->url(),
'descripcion' => $this->descripcion,
'apto_veganxs' => $this->apto_veganxs,
'apto_celiacxs' => $this->apto_celiacxs
'apto_celiacxs' => $this->apto_celiacxs,
'requiere_notas' => $this->requiere_notas,
];
}
}

View File

@ -15,7 +15,7 @@ class Producto extends Model
public function subpedidos()
{
return $this->belongsToMany('App\Subpedido','productos_subpedidos')->withPivot(["cantidad"]);
return $this->belongsToMany('App\Subpedido','productos_subpedidos')->withPivot(["cantidad", "notas"]);
}
public function proveedor()

View File

@ -15,7 +15,7 @@ class Subpedido extends Model
public function productos()
{
return $this->belongsToMany('App\Producto')->withPivot(["cantidad","total"]);
return $this->belongsToMany('App\Producto')->withPivot(["cantidad","total", "notas"]);
}
//Bonos del MPS, Sororo, etc. NO devuelve bonos de transporte
@ -84,13 +84,14 @@ class Subpedido extends Model
}
//Actualiza el pedido, agregando o quitando del subpedido según sea necesario. Debe ser llamado desde el controlador de subpedidos, luego de validar que los parámetros $producto y $cantidad son correctos. También calcula el subtotal por producto.
public function syncProducto(Producto $producto, Int $cantidad) {
public function syncProducto(Producto $producto, Int $cantidad, string $notas) {
if ($cantidad){
//si la cantidad es 1 o más se agrega el producto o actualiza la cantidad
$this->productos()->syncWithoutDetaching([
$producto->id => [
'cantidad' => $cantidad,
'total' => $cantidad * $producto->precio
'total' => $cantidad * $producto->precio,
'notas' => $notas,
]
]);
} else {

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class NotasProducto extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('producto_subpedido', function (Blueprint $table) {
$table->string('notas')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('producto_subpedido', function (Blueprint $table) {
$table->dropColumn('notas');
});
}
}

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ProductoRequiereNotas extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('productos', function (Blueprint $table) {
$table->boolean('requiere_notas')->default(false);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('productos', function (Blueprint $table) {
$table->dropColumn('requiere_notas');
});
}
}

View File

@ -61,7 +61,8 @@ class CanastaSeeder extends Seeder
'nombre' => trim(str_replace('*', ' ',$registro['Producto'])),
'precio' => $registro['Precio'],
'proveedor_id' => $this->obtenerProveedor($registro['Producto']),
'bono' => $registro[$this::FILA_HEADER] == "B"
'bono' => $registro[$this::FILA_HEADER] == "B",
'requiere_notas'=> $registro[$this::FILA_HEADER] =="PTC",
];
}

View File

@ -13,5 +13,6 @@ class DatabaseSeeder extends Seeder
public function run()
{
$this->call(CanastaSeeder::class);
$this->call(GrupoDeCompraSeeder::class);
}
}

View File

@ -31,14 +31,14 @@ class GrupoDeCompraSeeder extends Seeder
$usersToInsert[] = [
'name' => $registro['barrio'],
'password' => Hash::make($registro['barrio']),
'password' => Hash::make("asd"),
"is_admin" => 0,
'grupo_de_compra_id' => $key
];
$usersToInsert[] = [
'name' => $registro['barrio'] . "_admin",
'password' => Hash::make($registro['barrio'] . "admin"),
'password' => Hash::make("asd"),
"is_admin" => 1,
'grupo_de_compra_id' => $key
];

9
resources/js/app.js vendored
View File

@ -70,6 +70,10 @@ const app = new Vue({
cantidad(producto) {
let pedido = this.productos.some(p => p.id == producto.id)
return pedido ? this.productos.find(p => p.id == producto.id).pivot.cantidad : 0
},
notas(producto) {
let pedido = this.productos.some(p => p.id == producto.id);
return pedido ? this.productos.find(p => p.id == producto.id).pivot.notas : "";
},
settearDevoluciones() {
axios.get(`/api/grupos-de-compra/${this.gdc}/devoluciones`)
@ -99,14 +103,15 @@ const app = new Vue({
}
})
})
Event.$on('sync-subpedido', (cantidad, id) => {
Event.$on('sync-subpedido', (cantidad, id, notas) => {
if (this.pedido.aprobado) {
this.$toast('No se puede modificar un pedido ya aprobado', 2000);
return;
}
axios.post("/api/subpedidos/" + this.pedido.id + "/sync", {
cantidad: cantidad,
producto_id: id
producto_id: id,
notas: notas,
}).then((response) => {
this.pedido = response.data.data
this.$toast('Pedido actualizado exitosamente')

View File

@ -11,6 +11,16 @@
</a>
</p>
</div>
<div class="field">
<p class="control">
<a href="/compras/pedidos/notas" class="button">
<span class="icon is-small">
<i class="fas fa-sticky-note"></i>
</span>
<span>Descargar planilla de notas</span>
</a>
</p>
</div>
</div>
</div>
</template>

View File

@ -1,28 +1,44 @@
<template>
<div class="field has-addons contador">
<div class="control">
<button class="button is-small" @click.capture="decrementar();">
<i class="fa fa-solid fa-minus"></i>
<div>
<div class="field has-addons contador">
<div class="control">
<button class="button is-small" @click.capture="decrementar();">
<i class="fa fa-solid fa-minus"></i>
</button>
</div>
<div class="control">
<input id="cantidad" v-model="cantidad" class="input is-small" type="number" style="text-align: center">
</div>
<div class="control" @click="incrementar();">
<button class="button is-small">
<i class="fa fa-solid fa-plus"></i>
</button>
</div>
<button :disabled="disableConfirm()" class="button is-small is-success ml-1" @click="confirmar()">
<span class="icon">
<i class="fas fa-check"></i>
</span>
</button>
<button :disabled="!puedeBorrar()" class="button is-small is-danger ml-1" @click="borrar()">
<span class="icon">
<i class="fas fa-trash-alt"></i>
</span>
</button>
</div>
<div class="control">
<input id="cantidad" v-model="cantidad" class="input is-small" type="number" style="text-align: center">
</div>
<div class="control" @click="incrementar();">
<button class="button is-small">
<i class="fa fa-solid fa-plus"></i>
</button>
</div>
<button :disabled="!hayCambios()" class="button is-small is-success ml-1" @click="confirmar()">
<span class="icon">
<i class="fas fa-check"></i>
<div v-if="producto.requiere_notas" v-bind:class="{'has-icons-right': notas_warning_visible}" class="control is-full-width has-icons-left">
<span class="icon is-small is-left">
<i class="fas fa-sticky-note"></i>
</span>
</button>
<button :disabled="!puedeBorrar()" class="button is-small is-danger ml-1" @click="borrar()">
<span class="icon">
<i class="fas fa-trash-alt"></i>
<input v-model="notas" v-bind:class="{'is-danger': notas_warning_visible}" id="notas" class="input" type="text" placeholder="Talle o color" />
<span v-if="notas_warning_visible" class="icon is-small is-right">
<i class="fas fa-exclamation-triangle"></i>
</span>
</button>
<article v-if="notas_warning_visible" class="message is-danger is-small">
<div class="message-body">
No se puede dejar este campo vac&iacute;o
</div>
</article>
</div>
</div>
</template>
@ -33,21 +49,24 @@
},
data() {
return {
cantidad: this.producto.cantidad,
enChismosa: this.producto.cantidad,
cantidad: this.cantidadEnChismosa(),
notas: this.notasEnChismosa(),
notas_warning_visible: false,
}
},
mounted() {
if (this.producto.pivot !== undefined) {
this.cantidad = this.producto.pivot.cantidad;
this.enChismosa = this.cantidad;
}
Event.$on('sync-subpedido', (cantidad,productoId) => {
if (this.producto.id === productoId)
this.sincronizar(cantidad);
Event.$on('sync-subpedido', (cantidad, productoId, notas) => {
if (this.producto.id === productoId)
this.sincronizar(cantidad, notas);
});
},
methods: {
notasEnChismosa() {
return this.producto.pivot !== undefined ? this.producto.pivot.notas : "";
},
cantidadEnChismosa() {
return this.producto.pivot !== undefined ? this.producto.pivot.cantidad : 0;
},
decrementar() {
this.cantidad -= 1;
},
@ -55,26 +74,39 @@
this.cantidad += 1;
},
confirmar() {
Event.$emit('sync-subpedido', this.cantidad, this.producto.id);
if (this.warningNotas()) {
this.notas_warning_visible = true;
return;
}
console.log("Emit sync " + this.cantidad + " " + this.notas);
Event.$emit('sync-subpedido', this.cantidad, this.producto.id, this.notas);
},
borrar() {
this.cantidad = 0;
this.confirmar();
},
sincronizar(cantidad) {
sincronizar(cantidad, notas) {
this.notas_warning_visible = false;
this.notas = notas;
this.cantidad = cantidad;
if (this.producto.pivot != null) {
if (this.producto.pivot !== undefined) {
this.producto.pivot.cantidad = cantidad;
} else {
this.producto.cantidad = cantidad;
this.producto.pivot.notas = notas;
}
this.enChismosa = cantidad;
},
hayCambios() {
return this.cantidad != this.enChismosa;
if (this.cantidad != this.cantidadEnChismosa()) return true;
return this.cantidad > 0 && this.notas != this.notasEnChismosa();
},
puedeBorrar() {
return this.enChismosa > 0;
return this.cantidadEnChismosa() > 0;
},
warningNotas() {
return this.producto.requiere_notas && this.cantidad > 0 && !this.notas;
},
disableConfirm() {
return !this.hayCambios();
},
}
}
@ -97,4 +129,12 @@
.contador {
min-width: 178px;
}
.is-danger {
background-color: #fca697;
}
.is-danger::placeholder {
color: #fff;
opacity: 1; /* Firefox */
}
</style>

View File

@ -8,12 +8,13 @@ export default {
return {
cantidad: this.producto.cantidad,
enChismosa: this.producto.cantidad,
notas: this.producto.notas,
}
},
mounted() {
Event.$on('sync-subpedido', (cantidad,productoId) => {
Event.$on('sync-subpedido', (cantidad, productoId, notas) => {
if (this.producto.id === productoId)
this.sincronizar(cantidad);
this.sincronizar(cantidad, notas);
});
},
methods: {
@ -24,19 +25,21 @@ export default {
this.cantidad += 1;
},
confirmar() {
Event.$emit('sync-subpedido', this.cantidad, this.producto.id);
Event.$emit('sync-subpedido', this.cantidad, this.producto.id, this.notas);
},
borrar() {
this.cantidad = 0;
this.confirmar();
},
sincronizar(cantidad) {
sincronizar(cantidad, notas) {
this.cantidad = cantidad;
this.producto.cantidad = cantidad;
this.enChismosa = cantidad;
this.notas = notas;
this.producto.notas = notas;
},
hayCambios() {
return this.cantidad != this.enChismosa;
return this.cantidad != this.enChismosa || this.notas != this.producto.notas;
},
puedeBorrar() {
return this.enChismosa > 0;

View File

@ -34,7 +34,11 @@ export default {
params: this.params(filtro,valor)
}).then(response => {
this.productos = response.data.data;
this.productos.forEach(p => p.cantidad = this.$root.cantidad(p))
this.productos.forEach(p => {
p.pivot = {};
p.pivot.cantidad = this.$root.cantidad(p);
p.pivot.notas = this.$root.notas(p);
});
});
this.visible = true;
Event.$emit("migas-agregar",this.miga);

View File

@ -27,9 +27,13 @@
@foreach($subpedido->productos as $producto)
@if(!$producto->bono)
<tr>
<td>
{{ $producto->nombre }}
@if($producto->pivot->notas)
<br /><b>Talle/Color:</b> {{ $producto->pivot->notas }}
@endif
</td>
<td style="text-align: center">
{{ $producto->pivot->cantidad }}

View File

@ -81,4 +81,5 @@ Route::get('/compras', 'ComprasController@show')->name('compras_login.show');
Route::middleware(['compras'])->group( function() {
Route::get('/compras/pedidos', 'ComprasController@indexPedidos')->name('compras.pedidos');
Route::get('/compras/pedidos/descargar', 'ComprasController@descargarPedidos')->name('compras.pedidos.descargar');
Route::get('/compras/pedidos/notas', 'ComprasController@descargarNotas')->name('compras.pedidos.descargar');
});