Bien pues para hacer una venta necesitamos un catalogo de los productos los cuales vamos a vender.
En el catalogo de productos necesitamos los siguientes datos en datos generales:

- Empresa
- Sucursal
- Categoria
- Descripción
- Unidad
- Código de barras
- Imagen de producto
Para los datos de control de inventario necesitaremos los siguientes datos:

- stock
- Validar stock (checkbox)
- Inventario Riguroso(checkbox)
Para los datos del calculo de precios agregaremos los siguientes datos:

- Precio de compra
- Precio de venta
- Porcentaje de utilidad
- Porcentaje de IVA
- Porcentaje IVA retenido
- Porcentaje ISR retenido
También vamos necesitar los datos de facturación:

- Clave unidad SAT
- Clave Producto SAT
Bien primero que nada necesitamos crear la tabla en la base de datos, normalmente creamos la tabla directamente en la base de datos, pero como estamos trabajando en CodeIgniter 4 creamos el archivo de migración en app/Database/Migrations/2023-04-24060002_Products.php
El código quedaría de la siguiente manera
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class Products extends Migration
{
public function up()
{
// Products
$this->forge->addField([
'id' => ['type' => 'int', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true],
'idEmpresa' => ['type' => 'bigint', 'null' => true],
'idCategory' => ['type' => 'int', 'constraint' => 11, 'null' => true],
'code' => ['type' => 'varchar', 'constraint' => 64, 'null' => true],
'barcode' => ['type' => 'varchar', 'constraint' => 64, 'null' => true],
'unidad' => ['type' => 'varchar', 'constraint' => 64, 'null' => true],
'description' => ['type' => 'varchar', 'constraint' => 512, 'null' => true],
'stock' => ['type' => 'decimal', 'constraint' => '18,2', 'null' => true],
'validateStock' => ['type' => 'varchar', 'constraint' => 4, 'null' => true],
'inventarioRiguroso' => ['type' => 'varchar', 'constraint' => 4, 'null' => true],
'buyPrice' => ['type' => 'decimal', 'constraint' => '18,2', 'null' => true],
'salePrice' => ['type' => 'decimal', 'constraint' => '18,2', 'null' => true],
'porcentSale' => ['type' => 'int', 'constraint' => 11, 'null' => true],
'porcentTax' => ['type' => 'int', 'constraint' => 11, 'null' => true],
'unidadSAT' => ['type' => 'varchar', 'constraint' => 64, 'null' => true],
'claveProductoSAT' => ['type' => 'varchar', 'constraint' => 64, 'null' => true],
'nombreUnidadSAT' => ['type' => 'varchar', 'constraint' => 256, 'null' => true],
'nombreClaveProducto' => ['type' => 'varchar', 'constraint' => 256, 'null' => true],
'porcentIVARetenido' => ['type' => 'decimal', 'constraint' => '18,4', 'null' => true],
'porcentISRRetenido' => ['type' => 'decimal', 'constraint' => '18,4', 'null' => true],
'routeImage' => ['type' => 'varchar', 'constraint' => 256, 'null' => true],
'created_at' => ['type' => 'datetime', 'null' => true],
'updated_at' => ['type' => 'datetime', 'null' => true],
'deleted_at' => ['type' => 'datetime', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->createTable('products', true);
}
public function down()
{
$this->forge->dropTable('products', true);
}
}
Una vez creado ejecutan el comando para generar la tabla
php spark migrate
Ahora creamos el archivo del modelo con las funciones necesarias para el manejo de la tabla en app/models/productosModel.php
<?php
namespace App\Models;
use CodeIgniter\Model;
class ProductsModel extends Model {
protected $table = 'products';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
protected $useSoftDeletes = true;
protected $allowedFields = [
'id'
, 'idEmpresa'
, 'code'
, 'idCategory'
, 'description'
, 'stock'
, 'validateStock'
, 'inventarioRiguroso'
, 'buyPrice'
, 'salePrice'
, 'porcentSale'
, 'porcentTax'
, 'porcentIVARetenido'
, 'porcentISRRetenido'
, 'routeImage'
, 'created_at'
, 'deleted_at'
, 'updated_at'
, 'unidadSAT'
, 'claveProductoSAT'
, 'unidad'
, 'nombreUnidadSAT'
, 'nombreClaveProducto'
, 'barcode'
];
protected $useTimestamps = true;
protected $createdField = 'created_at';
protected $deletedField = 'deleted_at';
protected $validationRules = [];
protected $validationMessages = [];
protected $skipValidation = false;
public function mdlProductos($empresas) {
$resultado = $this->db->table('products a, empresas b')
->select('a.id
,a.code
,a.idCategory
,a.validateStock
,a.inventarioRiguroso
,a.description
,a.stock
,a.buyPrice
,a.salePrice
,a.porcentSale
,a.porcentTax
,a.routeImage
,a.created_at
,a.deleted_at
,a.updated_at
,a.barcode
,a.unidad
,b.nombre as nombreEmpresa
,a.porcentIVARetenido
,a.porcentISRRetenido
,a.nombreUnidadSAT
,a.nombreClaveProducto
,a.unidadSAT
,a.claveProductoSAT')
->where('a.idEmpresa', 'b.id', FALSE)
->whereIn('idEmpresa', $empresas)
->where('a.deleted_at', null);
return $resultado;
}
public function mdlProductosEmpresa($empresas, $empresa) {
$resultado2 = $this->db->table('products a, empresas b, saldos c, storages d')
->select('a.id,a.code
,a.idCategory
,a.validateStock
,a.inventarioRiguroso
,a.description
,c.cantidad as stock
,a.buyPrice
,a.salePrice
,a.porcentSale
,a.porcentTax
,a.routeImage
,a.created_at
,a.deleted_at
,a.updated_at
,a.barcode
,a.unidad
,b.nombre as nombreEmpresa
,a.porcentIVARetenido
,a.porcentISRRetenido
,a.nombreUnidadSAT
,a.nombreClaveProducto
,a.unidadSAT
,c.lote as lote
,c.idAlmacen
,d.name as almacen
,a.claveProductoSAT')
->where('c.idProducto', 'a.id', FALSE)
->where('a.idEmpresa', 'b.id', FALSE)
->where('c.cantidad >', '0')
->where('a.idEmpresa', 'c.idEmpresa', FALSE)
->where('c.idAlmacen', 'd.id', FALSE)
->where('a.idEmpresa', $empresa)
->where('a.deleted_at', null)
->where('a.inventarioRiguroso', 'on')
->where('a.validateStock', 'on')
->whereIn('c.idEmpresa', $empresas);
$resultado = $this->db->table('products a, empresas b')
->select('a.id,a.code
,a.idCategory
,a.validateStock
,a.inventarioRiguroso
,a.description
,a.stock as stock
,a.buyPrice
,a.salePrice
,a.porcentSale
,a.porcentTax
,a.routeImage
,a.created_at
,a.deleted_at
,a.updated_at
,a.barcode
,a.unidad
,b.nombre as nombreEmpresa
,a.porcentIVARetenido
,a.porcentISRRetenido
,a.nombreUnidadSAT
,a.nombreClaveProducto
,a.unidadSAT
,\'\' as lote
, 0 as idAlmacen
,\'\' as almacen
,a.claveProductoSAT')
->where('a.idEmpresa', 'b.id', FALSE)
->where('a.idEmpresa', $empresa)
->groupStart()
->where('a.inventarioRiguroso', "off")
->orWhere("a.inventarioRiguroso","NULL")
->orWhere("a.inventarioRiguroso",NULL)
->groupEnd()
->where('a.deleted_at', null)
->whereIn('idEmpresa', $empresas);
$resultado->union($resultado2);
$this->db->query("DROP TABLE IF EXISTS tempProducts");
$this->db->query("create table tempProducts " . $resultado->getCompiledSelect());
return $this->db->table('tempProducts');
}
/**
* Lista Para inventario Riguroso
* @param type $empresas
* @param type $empresa
* @return type
*/
public function mdlProductosEmpresaInventarioEntrada($empresas, $empresa) {
$resultado = $this->db->table('products a, empresas b')
->select('a.id,a.code
,a.idCategory
,a.validateStock
,a.inventarioRiguroso
,a.description
,a.stock
,a.buyPrice
,a.salePrice
,a.porcentSale
,a.porcentTax
,a.routeImage
,a.created_at
,a.deleted_at
,a.updated_at
,a.barcode
,a.unidad
, b.nombre as nombreEmpresa
,a.porcentIVARetenido
,a.porcentISRRetenido
,a.nombreUnidadSAT
,a.nombreClaveProducto
,a.unidadSAT
,"" as lote
,"" as almacen
,a.claveProductoSAT')
->where('a.idEmpresa', 'b.id', FALSE)
->where('a.idEmpresa', $empresa)
->where('a.deleted_at', null)
->where('a.inventarioRiguroso', 'on')
->where('a.validateStock', 'on')
->whereIn('idEmpresa', $empresas);
return $resultado;
}
public function mdlProductosEmpresaInventarioSalida($empresas, $empresa) {
$resultado = $this->db->table('products a, empresas b, saldos c, storages d')
->select('a.id,a.code
,a.idCategory
,a.validateStock
,a.inventarioRiguroso
,a.description
,c.cantidad as stock
,a.buyPrice
,a.salePrice
,a.porcentSale
,a.porcentTax
,a.routeImage
,a.created_at
,a.deleted_at
,a.updated_at
,a.barcode
,a.unidad
,b.nombre as nombreEmpresa
,a.porcentIVARetenido
,a.porcentISRRetenido
,a.nombreUnidadSAT
,a.nombreClaveProducto
,a.unidadSAT
,c.lote as lote
,c.idAlmacen
,d.name as almacen
,a.claveProductoSAT')
->where('c.idProducto', 'a.id', FALSE)
->where('a.idEmpresa', 'b.id', FALSE)
->where('a.idEmpresa', 'c.idEmpresa', FALSE)
->where('c.idAlmacen', 'd.id', FALSE)
->where('a.idEmpresa', $empresa)
->where('a.deleted_at', null)
->where('a.inventarioRiguroso', 'on')
->where('a.validateStock', 'on')
->whereIn('c.idEmpresa', $empresas);
return $resultado;
}
public function mdlGetProductoEmpresa($empresas, $idProducto) {
$resultado = $this->db->table('products a, empresas b, categorias c ')
->select('a.id
,a.code
,a.idEmpresa
,a.validateStock
,a.inventarioRiguroso
,a.idCategory
,c.clave
,c.descripcion as descripcionCategoria
,a.description
,a.stock
,a.buyPrice
,a.salePrice,a.porcentSale
,a.porcentTax,a.routeImage
,a.created_at,a.deleted_at
,a.updated_at
,a.barcode
,a.unidad
, b.nombre as nombreEmpresa
,a.porcentIVARetenido
,a.porcentISRRetenido
,a.nombreUnidadSAT
,a.nombreClaveProducto
,a.unidadSAT
,a.claveProductoSAT')
->where('a.idEmpresa', 'b.id', FALSE)
->where('a.idCategory', 'c.id', FALSE)
->where('a.id', $idProducto)
->where('a.deleted_at', null)
->whereIn('a.idEmpresa', $empresas)->get()->getFirstRow();
return $resultado;
}
}
Agregamos el archivo del controlador para las operaciones de validacion altas bajas y cambios en app/controllers/ProductsController.php
<?php
namespace App\Controllers;
use App\Controllers\BaseController;
use App\Models\ProductsModel;
use App\Models\LogModel;
use CodeIgniter\API\ResponseTrait;
use App\Models\CategoriasModel;
use App\Models\EmpresasModel;
use App\Models\QuotesDetailsModel;
use App\Models\SellsDetailsModel;
use App\Models\Tipos_movimientos_inventarioModel;
class ProductsController extends BaseController {
use ResponseTrait;
protected $log;
protected $products;
protected $empresa;
protected $categorias;
protected $sellsDetails;
protected $quoteDetails;
protected $tiposMovimientoInventario;
public function __construct() {
$this->products = new ProductsModel();
$this->log = new LogModel();
$this->categorias = new CategoriasModel();
$this->empresa = new EmpresasModel();
$this->sellsDetails = new SellsDetailsModel();
$this->quoteDetails = new QuotesDetailsModel();
$this->tiposMovimientoInventario = new Tipos_movimientos_inventarioModel();
helper('menu');
}
public function index() {
helper('auth');
$idUser = user()->id;
$titulos["empresas"] = $this->empresa->mdlEmpresasPorUsuario($idUser);
if (count($titulos["empresas"]) == "0") {
$empresasID[0] = "0";
} else {
$empresasID = array_column($titulos["empresas"], "id");
}
if ($this->request->isAJAX()) {
$datos = $this->products->mdlProductos($empresasID);
return \Hermawan\DataTables\DataTable::of($datos)->toJson(true);
}
$titulos["categorias"] = $this->categorias->select("*")->where("deleted_at", null)->asArray()->findAll();
$titulos["title"] = lang('products.title');
$titulos["subtitle"] = lang('products.subtitle');
return view('products', $titulos);
}
public function getAllProducts($empresa) {
helper('auth');
$idUser = user()->id;
$titulos["empresas"] = $this->empresa->mdlEmpresasPorUsuario($idUser);
if (count($titulos["empresas"]) == "0") {
$empresasID[0] = "0";
} else {
$empresasID = array_column($titulos["empresas"], "id");
}
if ($this->request->isAJAX()) {
$datos = $this->products->mdlProductosEmpresa($empresasID, $empresa);
return \Hermawan\DataTables\DataTable::of($datos)->toJson(true);
}
}
public function getAllProductsInventory($empresa, $idStorage, $idTipoMovimiento) {
helper('auth');
$idUser = user()->id;
$titulos["empresas"] = $this->empresa->mdlEmpresasPorUsuario($idUser);
if (count($titulos["empresas"]) == "0") {
$empresasID[0] = "0";
} else {
$empresasID = array_column($titulos["empresas"], "id");
}
//BUSCAMOS EL TIPO DE MOVIMIENTO SI ES ENTRADA O SALIDA
$tiposMovimiento = $this->tiposMovimientoInventario->select("*")
->wherein("idEmpresa", $empresasID)
->where("id", $idTipoMovimiento)->first();
if ($tiposMovimiento == null) {
$datos = $this->products->mdlProductosEmpresaInventarioEntrada($empresasID, $empresa);
return \Hermawan\DataTables\DataTable::of($datos)->toJson(true);
}
if ($tiposMovimiento["tipo"] == "ENT") {
if ($this->request->isAJAX()) {
$datos = $this->products->mdlProductosEmpresaInventarioEntrada($empresasID, $empresa);
return \Hermawan\DataTables\DataTable::of($datos)->toJson(true);
}
}
if ($tiposMovimiento["tipo"] == "SAL") {
if ($this->request->isAJAX()) {
$datos = $this->products->mdlProductosEmpresaInventarioSalida($empresasID, $empresa);
return \Hermawan\DataTables\DataTable::of($datos)->toJson(true);
}
}
$datos = $this->products->mdlProductosEmpresaInventarioEntrada($empresasID, $empresa);
return \Hermawan\DataTables\DataTable::of($datos)->toJson(true);
}
/**
* Get Unidad SAT via AJax
*/
public function getUnidadSATAjax() {
$request = service('request');
$postData = $request->getPost();
$response = array();
// Read new token and assign in $response['token']
$response['token'] = csrf_hash();
if (!isset($postData['searchTerm'])) {
// Fetch record
$listUnidadesSAT = $this->catalogosSAT->clavesUnidades40()->searchByField("texto", "%$%", 100);
} else {
$searchTerm = $postData['searchTerm'];
// Fetch record
$listUnidadesSAT = $this->catalogosSAT->clavesUnidades40()->searchByField("texto", "%$searchTerm%", 100);
}
$data = array();
foreach ($listUnidadesSAT as $unidadSAT => $value) {
$data[] = array(
"id" => $value->id(),
"text" => $value->id() . ' ' . $value->texto(),
);
}
$response['data'] = $data;
return $this->response->setJSON($response);
}
/**
* Get Unidad SAT via AJax
*/
public function getProductosSATAjax() {
$request = service('request');
$postData = $request->getPost();
$response = array();
// Read new token and assign in $response['token']
$response['token'] = csrf_hash();
if (!isset($postData['searchTerm'])) {
// Fetch record
$listProducts = $this->catalogosSAT->productosServicios40()->searchByField("texto", "%$searchTerm%", 50);
} else {
$searchTerm = $postData['searchTerm'];
// Fetch record
$listProducts = $this->catalogosSAT->productosServicios40()->searchByField("texto", "%$searchTerm %", 50);
}
$data = array();
foreach ($listProducts as $productosSAT => $value) {
$data[] = array(
"id" => $value->id(),
"text" => $value->id() . ' ' . $value->texto(),
);
}
$response['data'] = $data;
return $this->response->setJSON($response);
}
/**
* Get Products via AJax
*/
public function getProductsAjaxSelect2() {
$request = service('request');
$postData = $request->getPost();
$response = array();
// Read new token and assign in $response['token']
$response['token'] = csrf_hash();
$products = new ProductsModel();
$idEmpresa = $postData['idEmpresa'];
if (!isset($postData['searchTerm'])) {
// Fetch record
$listProducts = $products->select('id,code,description')->where("deleted_at", null)
->where('idEmpresa', $idEmpresa)
->orderBy('id')
->orderBy('code')
->orderBy('description')
->findAll(1000);
} else {
$searchTerm = $postData['searchTerm'];
// Fetch record
$listProducts = $products->select('id,code,description')->where("deleted_at", null)
->where('idEmpresa', $idEmpresa)
->groupStart()
->like('description', $searchTerm)
->orLike('id', $searchTerm)
->orLike('code', $searchTerm)
->groupEnd()
->findAll(1000);
}
$data = array();
$data[] = array(
"id" => 0,
"text" => "0 Todos Los Productos",
);
foreach ($listProducts as $product) {
$data[] = array(
"id" => $product['id'],
"text" => $product['id'] . ' ' . $product['id'] . ' ' . $product['code'] . ' ' . $product['description'],
);
}
$response['data'] = $data;
return $this->response->setJSON($response);
}
/**
* Read Products
*/
public function getProducts() {
helper('auth');
$idUser = user()->id;
$titulos["empresas"] = $this->empresa->mdlEmpresasPorUsuario($idUser);
if (count($titulos["empresas"]) == "0") {
$empresasID[0] = "0";
} else {
$empresasID = array_column($titulos["empresas"], "id");
}
$idProducts = $this->request->getPost("idProducts");
$datosProducts = $this->products->mdlGetProductoEmpresa($empresasID, $idProducts);
echo json_encode($datosProducts);
}
/**
* Save or update Products
*/
public function save() {
helper('auth');
$userName = user()->username;
$idUser = user()->id;
$datos = $this->request->getPost();
var_dump($datos);
$imagenProducto = $this->request->getFile("imagenProducto");
$datos["routeImage"] = "";
if ($imagenProducto) {
if ($imagenProducto->getClientExtension() <> "png") {
return lang("empresas.pngFileExtensionIncorrect");
}
$datos["routeImage"] = $imagenProducto->getRandomName();
}
if ($datos["idProducts"] == 0) {
try {
if ($this->products->save($datos) === false) {
$errores = $this->products->errors();
foreach ($errores as $field => $error) {
echo $error . " ";
}
return;
}
$dateLog["description"] = lang("vehicles.logDescription") . json_encode($datos);
$dateLog["user"] = $userName;
$this->log->save($dateLog);
if ($imagenProducto <> null) {
$imagenProducto->move("images/products", $datos["routeImage"]);
}
echo "Guardado Correctamente";
} catch (\PHPUnit\Framework\Exception $ex) {
echo "Error al guardar " . $ex->getMessage();
}
} else {
$dataPrevious = $this->products->find($datos["idProducts"]);
if ($this->products->update($datos["idProducts"], $datos) == false) {
$errores = $this->products->errors();
foreach ($errores as $field => $error) {
echo $error . " ";
}
return;
} else {
$dateLog["description"] = lang("products.logUpdated") . json_encode($datos);
$dateLog["user"] = $userName;
$this->log->save($dateLog);
if ($imagenProducto <> null) {
if (file_exists("images/products/" . $dataPrevious["routeImage"])) {
unlink("images/products/" . $dataPrevious["routeImage"]);
}
$imagenProducto->move("images/products", $datos["routeImage"]);
}
echo "Actualizado Correctamente";
return;
}
}
return;
}
/**
* Delete Products
* @param type $id
* @return type
*/
public function delete($id) {
if ($this->sellsDetails->select("id")->where("idProduct", $id)->countAllResults() > 0) {
$this->products->db->transRollback();
return $this->failValidationError("No se puede borrar ya que hay ventas con este producto");
}
if ($this->quoteDetails->select("id")->where("idProduct", $id)->countAllResults() > 0) {
$this->products->db->transRollback();
return $this->failValidationError("No se puede borrar ya que hay cotizaciones con este producto");
}
$infoProducts = $this->products->find($id);
helper('auth');
$userName = user()->username;
if (!$found = $this->products->delete($id)) {
$this->products->db->transRollback();
return $this->failNotFound(lang('products.msg.msg_get_fail'));
}
if ($infoProducts["routeImage"] != "") {
if (file_exists("images/products/" . $infoProducts["routeImage"])) {
unlink("images/products/" . $infoProducts["routeImage"]);
}
}
$this->products->purgeDeleted();
$logData["description"] = lang("products.logDeleted") . json_encode($infoProducts);
$logData["user"] = $userName;
$this->log->save($logData);
$this->products->db->transCommit();
return $this->respondDeleted($found, lang('products.msg_delete'));
}
/**
* Get Vehiculos via AJax
*/
public function getProductsAjax() {
$request = service('request');
$postData = $request->getPost();
$response = array();
// Read new token and assign in $response['token']
$response['token'] = csrf_hash();
$custumers = new VehiculosModel();
$idEmpresa = $postData['idEmpresa'];
if (!isset($postData['searchTerm'])) {
// Fetch record
$listProducts = $products->select('id,description')->where("deleted_at", null)
->where('idEmpresa', $idEmpresa)
->orderBy('id')
->orderBy('descripcion')
->findAll(1000);
} else {
$searchTerm = $postData['searchTerm'];
// Fetch record
$listProducts = $products->select('id,description')->where("deleted_at", null)
->where('idEmpresa', $idEmpresa)
->groupStart()
->like('descripcion', $searchTerm)
->orLike('id', $searchTerm)
->groupEnd()
->findAll(1000);
}
$data = array();
foreach ($listProducts as $product) {
$data[] = array(
"id" => $custumers['id'],
"text" => $custumers['id'] . ' ' . $product['description'],
);
}
$response['data'] = $data;
return $this->response->setJSON($response);
}
}
Ahora creamos la interfaz es decir la vista, para ello tendremos el archivo principal app/views/products.php y este incluirá los archivos secundarios, una forma que a mi parecer se organiza mejor para no tener todo en un solo archivo.
En el diseño estaran los bloques separados en pestañas “TABS”
Los archivos secundarios serán los siguientes
- app/views/modulesProducts/generalsProducts.php
- app/views/modulesProducts/imageProduct.php
- app/views/modulesProducts/inventoryProducts.php
- app/views/modulesProducts/modalCaptureProducts.php
- app/views/modulesProducts/priceProducts.php
- app/views/modulesProducts/productsCFDIV4.php
El archivo principal app/views/products.php contiene el siguiente código, incluye a los demas y genera el datable
<?= $this->include('load/toggle') ?>
<?= $this->include('julio101290\boilerplate\Views\load\select2') ?>
<?= $this->include('julio101290\boilerplate\Views\load\datatables') ?>
<?= $this->include('julio101290\boilerplate\Views\load\nestable') ?>
<!-- Extend from layout index -->
<?= $this->extend('julio101290\boilerplate\Views\layout\index') ?>
<!-- Section content -->
<?= $this->section('content') ?>
<?= $this->include('modulesProducts/modalCaptureProducts') ?>
<!-- SELECT2 EXAMPLE -->
<div class="card card-default">
<div class="card-header">
<div class="float-right">
<div class="btn-group">
<button class="btn btn-primary btnAddProducts" data-toggle="modal" data-target="#modalAddProducts"><i class="fa fa-plus"></i>
<?= lang('products.add') ?>
</button>
</div>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-12">
<div class="table-responsive">
<table id="tableProducts" class="table table-striped table-hover va-middle tableProducts">
<thead>
<tr>
<th>#</th>
<th>
Empresa
</th>
<th>
Clave
</th>
<th>
<?= lang('products.fields.idCategory') ?>
</th>
<th>
<?= lang('products.fields.barcode') ?>
</th>
<th>
<?= lang('products.fields.description') ?>
</th>
<th>
<?= lang('products.fields.stock') ?>
</th>
<th>
<?= lang('products.fields.buyPrice') ?>
</th>
<th>
<?= lang('products.fields.salePrice') ?>
</th>
<th>
<?= lang('products.fields.porcentSale') ?>
</th>
<th>
<?= lang('products.fields.porcentTax') ?>
</th>
<th>
<?= lang('products.fields.routeImage') ?>
</th>
<th>
<?= lang('products.fields.created_at') ?>
</th>
<th>
<?= lang('products.fields.deleted_at') ?>
</th>
<th>
<?= lang('products.fields.updated_at') ?>
</th>
<th>
<?= lang('products.fields.actions') ?>
</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- /.card -->
<?= $this->endSection() ?>
<?= $this->section('js') ?>
<script>
/**
* Cargamos la tabla
*/
var tableProducts = $('#tableProducts').DataTable({
processing: true,
serverSide: true,
responsive: true,
autoWidth: false,
order: [
[1, 'asc']
],
ajax: {
url: '<?= base_url('admin/products') ?>',
method: 'GET',
dataType: "json"
},
columnDefs: [{
orderable: false,
targets: [11, 15],
searchable: false,
targets: [11, 15]
}],
columns: [{
'data': 'id'
},
{
'data': 'nombreEmpresa'
},
{
'data': 'code'
},
{
'data': 'idCategory'
},
{
'data': 'barcode'
},
{
'data': 'description'
},
{
'data': 'stock'
},
{
'data': 'buyPrice'
},
{
'data': 'salePrice'
},
{
'data': 'porcentSale'
},
{
'data': 'porcentTax'
},
{
"data": function (data) {
if (data.routeImage == "") {
data.routeImage = "anonymous.png";
}
return `<td class="text-right py-0 align-middle">
<div class="btn-group btn-group-sm">
<img src="<?= base_URL("images/products") ?>/${data.routeImage}" data-action="zoom" width="40px" class="" style="">
</div>
</td>`
}
},
{
'data': 'created_at'
},
{
'data': 'deleted_at'
},
{
'data': 'updated_at'
},
{
"data": function (data) {
return `<td class="text-right py-0 align-middle">
<div class="btn-group btn-group-sm">
<button class="btn btn-warning btnEditProducts" data-toggle="modal" idProducts="${data.id}" data-target="#modalAddProducts"> <i class=" fa fa-edit"></i></button>
<button class="btn btn-danger btn-delete" data-id="${data.id}"><i class="fas fa-trash"></i></button>
</div>
</td>`
}
}
]
});
/**
* Carga datos actualizar
*/
/*=============================================
EDITAR Products
=============================================*/
$(".tableProducts").on("click", ".btnEditProducts", function () {
var idProducts = $(this).attr("idProducts");
var datos = new FormData();
datos.append("idProducts", idProducts);
if (idEmpresa == 0) {
Toast.fire({
icon: 'error',
title: "Tiene que seleccionar la empresa"
});
}
$.ajax({
url: "<?= base_url('admin/products/getProducts') ?>",
method: "POST",
data: datos,
cache: false,
contentType: false,
processData: false,
dataType: "json",
success: function (respuesta) {
$("#idProducts").val(respuesta["id"]);
$("#idEmpresa").val(respuesta["idEmpresa"]);
$("#idEmpresa").trigger("change");
var newOption = new Option(respuesta["clave"] + ' ' + respuesta["descripcionCategoria"], respuesta["idCategory"], true, true);
$('#idCategory').append(newOption).trigger('change');
$("#idCategory").val(respuesta["idCategory"]);
var newOptionUnidad = new Option(respuesta["nombreUnidadSAT"], respuesta["unidadSAT"], true, true);
$('#unidadSAT').append(newOptionUnidad).trigger('change');
$("#unidadSAT").val(respuesta["unidadSAT"]);
$("#unidad").val(respuesta["unidad"]);
var newOptionClaveProducto = new Option(respuesta["nombreClaveProducto"], respuesta["claveProductoSAT"], true, true);
$('#claveProductoSAT').append(newOptionClaveProducto).trigger('change');
$("#claveProductoSAT").val(respuesta["claveProductoSAT"]);
$("#clave").val(respuesta["clave"]);
$("#description").val(respuesta["description"]);
$("#stock").val(respuesta["stock"]);
$("#buyPrice").val(respuesta["buyPrice"]);
$("#salePrice").val(respuesta["salePrice"]);
$("#porcentSale").val(respuesta["porcentSale"]);
$("#porcentTax").val(respuesta["porcentTax"]);
$("#porcentIVARetenido").val(respuesta["porcentIVARetenido"]);
$("#porcentISRRetenido").val(respuesta["porcentISRRetenido"]);
$("#barcode").val(respuesta["barcode"]);
$("#validateStock").bootstrapToggle(respuesta["validateStock"]);
$("#inventarioRiguroso").bootstrapToggle(respuesta["inventarioRiguroso"]);
//$("#routeImage").val(respuesta["routeImage"]);
if (respuesta["routeImage"] == "") {
$(".previsualizarLogo").attr("src", '<?= base_URL("images/products/") ?>anonymous.png');
} else {
$(".previsualizarLogo").attr("src", '<?= base_URL("images/products") ?>/' + respuesta["routeImage"]);
}
$("#code").val(respuesta["code"]);
}
})
})
/*=============================================
ELIMINAR products
=============================================*/
$(".tableProducts").on("click", ".btn-delete", function () {
var idProducts = $(this).attr("data-id");
Swal.fire({
title: '<?= lang('boilerplate.global.sweet.title') ?>',
text: "<?= lang('boilerplate.global.sweet.text') ?>",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: '<?= lang('boilerplate.global.sweet.confirm_delete') ?>'
})
.then((result) => {
if (result.value) {
$.ajax({
url: `<?= base_url('admin/products') ?>/` + idProducts,
method: 'DELETE',
}).done((data, textStatus, jqXHR) => {
Toast.fire({
icon: 'success',
title: jqXHR.statusText,
});
tableProducts.ajax.reload();
}).fail((error) => {
Toast.fire({
icon: 'error',
title: error.responseJSON.messages.error,
});
})
}
})
})
$(function () {
$("#modalAddProducts").draggable();
});
/*=============================================
SUBIENDO LA FOTO DEL USUARIO
=============================================*/
$(".imagenProducto").change(function () {
var imagen = this.files[0];
/*=============================================
VALIDAMOS EL FORMATO DE LA IMAGEN SEA JPG O PNG
=============================================*/
if (imagen["type"] != "image/png") {
$(".imagenProducto").val("");
Toast.fire({
icon: 'error',
title: "<?= lang('empresas.imagenesFormato') ?>",
});
} else if (imagen["size"] > 2000000) {
$(".imagenProducto").val("");
Toast.fire({
icon: 'error',
title: "<?= lang('empresas.imagenesPeso') ?>",
});
} else {
var datosImagen = new FileReader;
datosImagen.readAsDataURL(imagen);
$(datosImagen).on("load", function (event) {
var rutaImagen = event.target.result;
$(".previsualizarLogo").attr("src", rutaImagen);
})
}
});
</script>
<?= $this->endSection() ?>
Luego tenemos app/views/modulesProducts/generalsProducts.php que son los datos generales del producto
<p>
<h3>Datos Generales</h3>
<div class="form-group row">
<label for="emitidoRecibido" class="col-sm-2 col-form-label">Empresa</label>
<div class="col-sm-10">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-pencil-alt"></i></span>
</div>
<select class="form-control idEmpresa form-controlProducts" name="idEmpresa" id="idEmpresa" style="width:80%;">
<option value="0">Seleccione empresa</option>
<?php
foreach ($empresas as $key => $value) {
echo "<option value='$value[id]'>$value[id] - $value[nombre] </option> ";
}
?>
</select>
</div>
</div>
</div>
<div class="form-group row">
<label for="idCategory" class="col-sm-2 col-form-label">
<?= lang('products.fields.idCategory') ?>
</label>
<div class="col-sm-10">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-pencil-alt"></i></span>
</div>
<select name="idCategory" id="idCategory" style="width: 90%;" class="form-control idCategory form-controlProducts">
<option value="0" selected>
<?= lang('products.fields.idSelectCategory') ?>
</option>
</select>
</div>
</div>
</div>
<div class="form-group row">
<label for="code" class="col-sm-2 col-form-label">
Clave
</label>
<div class="col-sm-10">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-pencil-alt"></i></span>
</div>
<input type="text" name="code" id="code" class="form-control form-controlProducts <?= session('error.code') ? 'is-invalid' : '' ?>" value="" placeholder="Clave" autocomplete="off" readonly>
</div>
</div>
</div>
<div class="form-group row">
<label for="barcode" class="col-sm-2 col-form-label">
<?= lang('products.fields.barcode') ?>
</label>
<div class="col-sm-10">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-pencil-alt"></i></span>
</div>
<input type="text" name="barcode" id="barcode" class="form-control form-controlProducts <?= session('error.barcode') ? 'is-invalid' : '' ?>" value="<?= old('barcode') ?>" placeholder="<?= lang('products.fields.barcode') ?>" autocomplete="off">
</div>
</div>
</div>
<div class="form-group row">
<label for="description" class="col-sm-2 col-form-label">
<?= lang('products.fields.description') ?>
</label>
<div class="col-sm-10">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-pencil-alt"></i></span>
</div>
<input type="text" name="description" id="description" class="form-control form-controlProducts <?= session('error.description') ? 'is-invalid' : '' ?>" value="<?= old('description') ?>" placeholder="<?= lang('products.fields.description') ?>" autocomplete="off">
</div>
</div>
</div>
<div class="form-group row">
<label for="unidad" class="col-sm-2 col-form-label">
Unidad
</label>
<div class="col-sm-10">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-pencil-alt"></i></span>
</div>
<input type="text" name="unidad" id="unidad" class="form-control form-controlProducts <?= session('error.unidad') ? 'is-invalid' : '' ?>" value="<?= old('unidad') ?>" placeholder="Unidad" autocomplete="off">
</div>
</div>
</div>
</p>
Agregamos el código donde capturamos los datos del inventario app/views/modulesProducts/inventoryProducts.php
<p>
<h3>Datos Inventario</h3>
<div class="form-group row">
<label for="stock" class="col-sm-2 col-form-label">
<?= lang('products.fields.stock') ?>
</label>
<div class="col-sm-10">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-pencil-alt"></i></span>
</div>
<input type="number" name="stock" id="stock" class="form-control form-controlProducts <?= session('error.stock') ? 'is-invalid' : '' ?>" value="<?= old('stock') ?>" placeholder="<?= lang('products.fields.stock') ?>" autocomplete="off" min="0">
</div>
</div>
</div>
<div class="form-group row">
<label for="stock" class="col-sm-2 col-form-label">
Valida Stock
</label>
<div class="col-sm-10">
<div class="input-group">
<div class="input-group-prepend">
</div>
<input type="checkbox" id="validateStock" name="validateStock" class="validateStock" data-width="250" data-height="40" checked data-toggle="toggle" data-on="Valida Stock" data-off="No valida stock" data-onstyle="success" data-offstyle="danger">
</div>
</div>
</div>
<div class="form-group row">
<label for="stock" class="col-sm-2 col-form-label">
Inventario Riguroso
</label>
<div class="col-sm-10">
<div class="input-group">
<div class="input-group-prepend">
</div>
<input type="checkbox" id="inventarioRiguroso" name="inventarioRiguroso" class="inventarioRiguroso" data-width="250" data-height="40" checked data-toggle="toggle" data-on="Inventario Riguroso" data-off="Inventario básico" data-onstyle="success" data-offstyle="danger">
</div>
</div>
</div>
</p>
Este es el código para capturar la imagen del producto app/views/modulesProducts/imageProduct.php
<p>
<h3>Imagenes</h3>
<div class="form-group ">
<input type="file" class="imagenProducto" name="imagenProducto" id="imagenProducto">
<p class="help-block">
<?= lang("empresas.imagenesPesoMaximo") ?>
</p>
<img src="<?= base_url("images/products/anonymous.png") ?>" class="img-thumbnail previsualizarLogo" width="100px">
<input type="hidden" name="imagenActual" id="imagenActual">
</div>
</p>
Capturamos el archivo base del modal aquí van configuradas las pestañas “TABS” app/views/modulesProducts/modalCaptureProducts.php
<!-- Modal Products -->
<div class="modal fade" id="modalAddProducts" tabindex="-1" role="dialog" aria-labelledby="modalAddProducts" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<?= lang('products.createEdit') ?>
</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="home-tab" data-toggle="tab" data-target="#generales" type="button" role="tab" aria-controls="home" aria-selected="true">Generales</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="profile-tab" data-toggle="tab" data-target="#inventoryProducts" type="button" role="tab" aria-controls="profile" aria-selected="false">Inventario</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="emailsetting-tab" data-toggle="tab" data-target="#precios" type="button" role="tab" aria-controls="emailSettings" aria-selected="false">Precios</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="contact-tab" data-toggle="tab" data-target="#imageProduct" type="button" role="tab" aria-controls="contact" aria-selected="false">Logos / Imagenes</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="contact-tab" data-toggle="tab" data-target="#productsCFDIV4" type="button" role="tab" aria-controls="contact" aria-selected="false">Datos CFDI V4 </button>
</li>
</ul>
<form id="form-products" class="form-horizontal">
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="generales" role="tabpanel" aria-labelledby="generales">
<?= $this->include('modulesProducts/generalsProducts') ?>
</div>
<div class="tab-pane fade" id="inventoryProducts" role="tabpanel" aria-labelledby="datosFacturacion">
<?= $this->include('modulesProducts/inventoryProducts') ?>
</div>
<div class="tab-pane fade" id="precios" role="tabpanel" aria-labelledby="contact-tab">
<?= $this->include('modulesProducts/priceProducts') ?>
</div>
<div class="tab-pane fade" id="imageProduct" role="tabpanel" aria-labelledby="contact-tab">
<?= $this->include('modulesProducts/imageProduct') ?>
</div>
<div class="tab-pane fade" id="productsCFDIV4" role="tabpanel" aria-labelledby="contact-tab">
<?= $this->include('modulesProducts/productsCFDIV4') ?>
</div>
</div>
<input type="hidden" id="idProducts" name="idProducts" value="0">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">
<?= lang('boilerplate.global.close') ?>
</button>
<button type="button" class="btn btn-primary btn-sm" id="btnSaveProducts">
<?= lang('boilerplate.global.save') ?>
</button>
</div>
</div>
</div>
</div>
<?= $this->section('js') ?>
<script>
$("#idEmpresa").select2();
/**
* Categorias por empresa
*/
$(".unidadSAT").select2({
ajax: {
url: "<?= base_url('admin/products/getUnidadSATAjax') ?>",
type: "post",
dataType: 'json',
delay: 250,
data: function(params) {
// CSRF Hash
var csrfName = $('.txt_csrfname').attr('name'); // CSRF Token name
var csrfHash = $('.txt_csrfname').val(); // CSRF hash
var idEmpresa = $('.idEmpresa').val(); // CSRF hash
return {
searchTerm: params.term, // search term
[csrfName]: csrfHash, // CSRF Token
idEmpresa: idEmpresa // search term
};
},
processResults: function(response) {
// Update CSRF Token
$('.txt_csrfname').val(response.token);
return {
results: response.data
};
},
cache: true
}
});
/**
* Categorias por empresa
*/
$(".claveProductoSAT").select2({
ajax: {
url: "<?= base_url('admin/products/getProductosSATAjax') ?>",
type: "post",
dataType: 'json',
delay: 250,
data: function(params) {
// CSRF Hash
var csrfName = $('.txt_csrfname').attr('name'); // CSRF Token name
var csrfHash = $('.txt_csrfname').val(); // CSRF hash
var idEmpresa = $('.idEmpresa').val(); // CSRF hash
return {
searchTerm: params.term, // search term
[csrfName]: csrfHash, // CSRF Token
idEmpresa: idEmpresa // search term
};
},
processResults: function(response) {
// Update CSRF Token
$('.txt_csrfname').val(response.token);
return {
results: response.data
};
},
cache: true
}
});
/**
* Categorias por empresa
*/
$(".idCategory").select2({
ajax: {
url: "<?= base_url('admin/categorias/getCategoriasAjax') ?>",
type: "post",
dataType: 'json',
delay: 250,
data: function(params) {
// CSRF Hash
var csrfName = $('.txt_csrfname').attr('name'); // CSRF Token name
var csrfHash = $('.txt_csrfname').val(); // CSRF hash
var idEmpresa = $('.idEmpresa').val(); // CSRF hash
return {
searchTerm: params.term, // search term
[csrfName]: csrfHash, // CSRF Token
idEmpresa: idEmpresa // search term
};
},
processResults: function(response) {
// Update CSRF Token
$('.txt_csrfname').val(response.token);
return {
results: response.data
};
},
cache: true
}
});
/**
* When change id Control
*/
$("#idCategory").on("change", function() {
var idCategoria = $(this).val();
var idEmpresa = $(".idEmpresa").val();
var idProduct = $("#idProducts").val();
var datos = new FormData();
datos.append("idCategoria", idCategoria);
datos.append("idEmpresa", idEmpresa);
$.ajax({
url: "<?= base_url('admin/categorias/buscarFolio') ?>",
method: "POST",
data: datos,
cache: false,
contentType: false,
processData: false,
success: function(respuesta) {
if (idProduct == 0) {
$("#code").val(respuesta);
}
}
}
)
});
$("#porcentSale").on("keyup", function() {
var porcentaje = $(this).val() / 100;
var precioCompra = $("#buyPrice").val()
var precioVenta = $("#salePrice").val()
if (!(precioCompra > 0)) {
return;
}
precioVenta = precioCompra * (porcentaje + 1)
$("#salePrice").val(precioVenta);
});
$(document).on('click', '.btnAddProducts', function(e) {
$(".form-controlProducts").val("");
$("#idProducts").val("0");
$("#btnSaveProducts").removeAttr("disabled");
$("#idCategory").val("0");
$("#idCategory").trigger("change");
$("#idEmpresa").val("0");
$("#idEmpresa").trigger("change");
$("#porcentSale").val("40");
$("#porcentTax").val("0");
$("#porcentIVARetenido").val("0");
$("#porcentISRRetenido").val("0");
$("#unidadSAT").val("0");
$("#unidadSAT").trigger("change");
$("#claveProductoSAT").val("0");
$("#claveProductoSAT").trigger("change");
$("#facturacionRD").bootstrapToggle("off");
});
/*
* AL hacer click al editar
*/
$(document).on('click', '.btnEditProducts', function(e) {
var idProducts = $(this).attr("idProducts");
//LIMPIAMOS CONTROLES
$(".form-control").val("");
$("#idProducts").val(idProducts);
$("#btnGuardarProducts").removeAttr("disabled");
});
$(document).on('click', '#btnSaveProducts', function(e) {
var idEmpresa = $("#idEmpresa").val();
var idProducts = $("#idProducts").val();
var clave = $("#code").val();
var idCategory = $("#idCategory").val();
var barcode = $("#barcode").val();
var description = $("#description").val();
var stock = $("#stock").val();
var buyPrice = $("#buyPrice").val();
var salePrice = $("#salePrice").val();
var porcentSale = $("#porcentSale").val();
var porcentTax = $("#porcentTax").val();
var porcentIVARetenido = $("#porcentIVARetenido").val();
var porcentISRRetenido = $("#porcentISRRetenido").val();
var routeImage = $("#routeImage").val();
var unidadSAT = $("#unidadSAT").val();
var unidad = $("#unidad").val();
var claveProductoSAT = $("#claveProductoSAT").val();
var nombreUnidadSAT = $("#unidadSAT option:selected").text();
var nombreClaveProducto = $("#claveProductoSAT option:selected").text();
var imagenProducto = $("#imagenProducto").prop("files")[0];
if ($("#validateStock").is(':checked')) {
var validateStock = "on";
} else {
var validateStock = "off";
}
if ($("#inventarioRiguroso").is(':checked')) {
var inventarioRiguroso = "on";
} else {
var inventarioRiguroso = "off";
}
if (idEmpresa == 0 || idEmpresa == "") {
Toast.fire({
icon: 'error',
title: "Tiene que seleccionar la empresa"
});
return;
}
if (idCategory == 0 || idCategory == "") {
Toast.fire({
icon: 'error',
title: "Tiene que seleccionar la categoria"
});
return;
}
if (porcentTax == "") {
Toast.fire({
icon: 'error',
title: "Tiene que ingregar el porcentaje de impuesto"
});
return;
}
$("#btnSaveProducts").attr("disabled", true);
var datos = new FormData();
datos.append("idEmpresa", idEmpresa);
datos.append("idProducts", idProducts);
datos.append("code", clave);
datos.append("idCategory", idCategory);
datos.append("description", description);
datos.append("stock", stock);
datos.append("buyPrice", buyPrice);
datos.append("salePrice", salePrice);
datos.append("porcentSale", porcentSale);
datos.append("porcentTax", porcentTax);
datos.append("porcentIVARetenido", porcentIVARetenido);
datos.append("porcentISRRetenido", porcentISRRetenido);
datos.append("imagenProducto", imagenProducto);
datos.append("barcode", barcode);
datos.append("validateStock", validateStock);
datos.append("inventarioRiguroso", inventarioRiguroso);
datos.append("unidadSAT", unidadSAT);
datos.append("unidad", unidad);
datos.append("claveProductoSAT", claveProductoSAT);
datos.append("nombreUnidadSAT", nombreUnidadSAT);
datos.append("nombreClaveProducto", nombreClaveProducto);
$.ajax({
url: "<?= base_url('admin/products/save') ?>",
method: "POST",
data: datos,
cache: false,
contentType: false,
processData: false,
success: function(respuesta) {
if (respuesta.match(/Correctamente.*/)) {
Toast.fire({
icon: 'success',
title: "Guardado Correctamente"
});
tableProducts.ajax.reload();
$("#btnSaveProducts").removeAttr("disabled");
$('#modalAddProducts').modal('hide');
} else {
Toast.fire({
icon: 'error',
title: respuesta
});
$("#btnSaveProducts").removeAttr("disabled");
}
}
}
)
});
</script>
<?= $this->endSection() ?>
Este es el codigo para capturar el precio de los productos app/views/modulesProducts/priceProducts.php
<p>
<h3>Precios e Impuestos</h3>
<div class="form-group row ">
<div class="col-sm-6 ">
<label for="buyPrice" class="col-sm-12 col-form-label">
<?= lang('products.fields.buyPrice') ?>
</label>
<div class="col-sm-12">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-arrow-down"></i></span>
</div>
<input type="number" name="buyPrice" id="buyPrice" class="form-control form-controlProducts <?= session('error.buyPrice') ? 'is-invalid' : '' ?>" value="<?= old('buyPrice') ?>" step=".01" placeholder="<?= lang('products.fields.buyPrice') ?>" autocomplete="off">
</div>
</div>
</div>
<div class="col-sm-6 ">
<label for="salePrice" class="col-sm-12 col-form-label">
<?= lang('products.fields.salePrice') ?>
</label>
<div class="col-sm-12">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-arrow-up"></i></span>
</div>
<input type="number" name="salePrice" id="salePrice" class="form-control form-controlProducts <?= session('error.salePrice') ? 'is-invalid' : '' ?>" value="<?= old('salePrice') ?>" step=".01" pattern="^\d*(\.\d{0,2})?$" placeholder="<?= lang('products.fields.salePrice') ?>" autocomplete="off">
</div>
</div>
</div>
</div>
<div class="form-group row ">
<div class="col-sm-6"></div>
<div class="col-sm-6 ">
<label for="porcentSale" class="col-sm-12 col-form-label">
<?= lang('products.fields.porcentSale') ?>
</label>
<div class="col-sm-12">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-percent"></i></span>
</div>
<input type="number" name="porcentSale" id="porcentSale" class="form-control form-controlProducts <?= session('error.porcentSale') ? 'is-invalid' : '' ?>" value="<?= old('porcentSale') ?>" placeholder="<?= lang('products.fields.porcentSale') ?>" autocomplete="off" min="0" value="40">
</div>
</div>
</div>
</div>
<div class="form-group row">
<label for="porcentTax" class="col-sm-2 col-form-label">
<?= lang('products.fields.porcentTax') ?>
</label>
<div class="col-sm-10">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-percent"></i></span>
</div>
<input type="number" name="porcentTax" id="porcentTax" class="form-control form-controlProducts <?= session('error.porcentTax') ? 'is-invalid' : '' ?>" value="<?= old('porcentTax') ?>" placeholder="<?= lang('products.fields.porcentTax') ?>" autocomplete="off">
</div>
</div>
</div>
<div class="form-group row">
<label for="porcentTax" class="col-sm-2 col-form-label">
Iva Retenido
</label>
<div class="col-sm-10">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-percent"></i></span>
</div>
<input type="number" name="porcentIVARetenido" id="porcentIVARetenido" class="form-control form-controlProducts <?= session('error.porcentIVARetenido') ? 'is-invalid' : '' ?>" value="<?= old('porcentIVARetenido') ?>" placeholder="Iva Retenido" autocomplete="off">
</div>
</div>
</div>
<div class="form-group row">
<label for="porcentTax" class="col-sm-2 col-form-label">
ISR Retenido
</label>
<div class="col-sm-10">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-percent"></i></span>
</div>
<input type="number" name="porcentISRRetenido" id="porcentISRRetenido" class="form-control form-controlProducts <?= session('error.porcentISRRetenido') ? 'is-invalid' : '' ?>" value="<?= old('porcentISRRetenido') ?>" placeholder="ISR Retenido" autocomplete="off">
</div>
</div>
</div>
</p>
Con este código generamos los controles para capturar los datos del SAT del producto app/views/modulesProducts/productsCFDIV4.php
Creamos las rutas necesarias en app/Config/routes.php
$routes->post('products/save', 'ProductsController::save');
$routes->post('products/getProducts', 'ProductsController::getProducts');
$routes->get('products/getAllProducts/(:any)', 'ProductsController::getAllProducts/$1');
$routes->get('products/getAllProductsInventory/(:any)/(:any)/(:any)', 'ProductsController::getAllProductsInventory/$1/$2/$3');
$routes->post('products/getUnidadSATAjax', 'ProductsController::getUnidadSATAjax');
$routes->post('products/getProductosSATAjax', 'ProductsController::getProductosSATAjax');
$routes->post('products/getProductsAjax', 'ProductsController::getProductsAjaxSelect2');
Creamos el menú para entrar a productos

Y listo ya tenemos nuestro modulo hecho

Como el tutorial se hizo ya con el fuente avanzado algunos catálogos necesarios como sucursales puede que no estén a la fecha, pero se agregaran