diff --git a/pos/blueprints/inventory_bp.py b/pos/blueprints/inventory_bp.py index d741e9a..590e316 100644 --- a/pos/blueprints/inventory_bp.py +++ b/pos/blueprints/inventory_bp.py @@ -2271,6 +2271,22 @@ def list_inventory_subcategories(category_id): conn.close() +@inventory_bp.route('/categories/all', methods=['GET']) +@require_auth() +def list_all_inventory_categories(): + """Return all active categories (flat, with parent_id) for selectors.""" + conn = get_tenant_conn(g.tenant_id) + try: + cur = conn.cursor() + cur.execute( + "SELECT id, name, parent_id FROM categories WHERE is_active = true ORDER BY name" + ) + rows = cur.fetchall() + return jsonify({'categories': [{'id': r[0], 'name': r[1], 'parent_id': r[2]} for r in rows]}) + finally: + conn.close() + + # ─── Global Tier Discounts ─────────────────────── @inventory_bp.route('/tier-discounts', methods=['GET']) diff --git a/pos/static/js/inventory.js b/pos/static/js/inventory.js index 9cbdab4..b23c686 100644 --- a/pos/static/js/inventory.js +++ b/pos/static/js/inventory.js @@ -17,6 +17,11 @@ var compatSource = 'both'; // default, loaded from config var inventorySearchController = null; + var user = window.POS_USER || {}; + var userRole = (user.role || '').toLowerCase(); + var userPerms = user.permissions || []; + var canEditPrices = userRole === 'owner' || userRole === 'admin' || userPerms.indexOf('config.edit_prices') !== -1; + // Load compatibility source setting (function loadCompatSource() { fetch('/pos/api/config/vehicle-compat-source', { headers: { 'Authorization': 'Bearer ' + token } }) @@ -181,6 +186,7 @@ '' + esc(it.location) + '' + '' + ' ' + + ' ' + ' ' + ' ' + ' ' + @@ -307,39 +313,49 @@ // CREATE ITEM (createModal) // ===================================================================== - function loadCategories() { + function loadCategories(selectedId) { var sel = document.getElementById('newCategory'); if (!sel) return; - apiFetch(API + '/categories').then(function(data) { + apiFetch(API + '/categories/all').then(function(data) { if (!data || !data.categories) return; - sel.innerHTML = ''; - data.categories.forEach(function(c) { - sel.innerHTML += ''; + var cats = data.categories; + var top = cats.filter(function(c) { return !c.parent_id; }); + var subs = cats.filter(function(c) { return c.parent_id; }); + sel.innerHTML = ''; + top.forEach(function(c) { + sel.innerHTML += '' + + ''; + subs.filter(function(s) { return s.parent_id === c.id; }).forEach(function(s) { + sel.innerHTML += ''; + }); + sel.innerHTML += ''; }); }); } window.loadCategories = loadCategories; function onCategoryChange(categoryId) { - var subSel = document.getElementById('newSubcategory'); - if (!subSel) return; - if (!categoryId) { - subSel.innerHTML = ''; - subSel.disabled = true; - return; - } - apiFetch(API + '/categories/' + categoryId + '/subcategories').then(function(data) { - if (!data || !data.subcategories) return; - subSel.innerHTML = ''; - data.subcategories.forEach(function(s) { - subSel.innerHTML += ''; - }); - subSel.disabled = false; - }); + // Kept for backwards compatibility; selector is now flat. } window.onCategoryChange = onCategoryChange; + function resetCreateModal() { + document.getElementById('editItemId').value = ''; + document.getElementById('createModalTitle').textContent = 'Nuevo Producto'; + document.getElementById('createModalBtn').textContent = 'Crear Producto'; + var ids = ['newPartNumber','newName','newBrand','newBarcode','newSku2','newSku3','newUnit','newCost','newPrice1','newMinStock','newInitialStock','newMaxStock','newTaxRate','newLocation','newDescription']; + ids.forEach(function(id) { + var el = document.getElementById(id); + if (el) el.value = ''; + }); + document.getElementById('newCategory').innerHTML = ''; + document.getElementById('newIsActive').value = 'true'; + document.getElementById('initialStockField').style.display = ''; + document.querySelectorAll('#createModal .price-field input').forEach(function(el) { el.disabled = false; }); + } + function showCreateModal() { + resetCreateModal(); document.getElementById('createModal').classList.add('is-open'); loadCategories(); // Attach AI classification on part number blur @@ -357,6 +373,41 @@ } } + function showEditItemModal(itemId) { + resetCreateModal(); + document.getElementById('editItemId').value = itemId; + document.getElementById('createModalTitle').textContent = 'Editar Producto'; + document.getElementById('createModalBtn').textContent = 'Guardar Cambios'; + document.getElementById('initialStockField').style.display = 'none'; + document.getElementById('createModal').classList.add('is-open'); + apiFetch(API + '/items/' + itemId).then(function(it) { + if (!it) return; + document.getElementById('newPartNumber').value = it.part_number || ''; + document.getElementById('newName').value = it.name || ''; + document.getElementById('newBrand').value = it.brand || ''; + document.getElementById('newBarcode').value = it.barcode || ''; + document.getElementById('newUnit').value = it.unit || ''; + document.getElementById('newCost').value = it.cost != null ? it.cost : ''; + document.getElementById('newPrice1').value = it.price_1 != null ? it.price_1 : ''; + document.getElementById('newMinStock').value = it.min_stock != null ? it.min_stock : ''; + document.getElementById('newMaxStock').value = it.max_stock != null ? it.max_stock : ''; + document.getElementById('newTaxRate').value = it.tax_rate != null ? it.tax_rate : ''; + document.getElementById('newLocation').value = it.location || ''; + document.getElementById('newDescription').value = it.description || ''; + document.getElementById('newIsActive').value = (it.is_active === false ? 'false' : 'true'); + document.getElementById('newSku2').value = (it.sku_aliases && it.sku_aliases[0]) ? it.sku_aliases[0].sku : ''; + document.getElementById('newSku3').value = (it.sku_aliases && it.sku_aliases[1]) ? it.sku_aliases[1].sku : ''; + if (!canEditPrices) { + document.querySelectorAll('#createModal .price-field input').forEach(function(el) { el.disabled = true; }); + } + loadCategories(it.category_id); + }).catch(function(e) { + alert('Error al cargar producto: ' + e.message); + closeCreateModal(); + }); + } + window.showEditItemModal = showEditItemModal; + function classifyPartNumber(partNumber) { var resultEl = document.getElementById('createResult'); resultEl.innerHTML = 'Consultando IA...'; @@ -387,58 +438,57 @@ function closeCreateModal() { document.getElementById('createModal').classList.remove('is-open'); document.getElementById('createResult').innerHTML = ''; - var catSel = document.getElementById('newCategory'); - var subSel = document.getElementById('newSubcategory'); - if (catSel) catSel.innerHTML = ''; - if (subSel) { subSel.innerHTML = ''; subSel.disabled = true; } + resetCreateModal(); } function createItem() { - var elPrice2 = document.getElementById('newPrice2'); - var elPrice3 = document.getElementById('newPrice3'); + var editId = document.getElementById('editItemId').value; var data = { part_number: document.getElementById('newPartNumber').value.trim(), name: document.getElementById('newName').value.trim(), brand: document.getElementById('newBrand').value.trim(), barcode: document.getElementById('newBarcode').value.trim() || undefined, + unit: document.getElementById('newUnit').value.trim() || undefined, cost: parseFloat(document.getElementById('newCost').value) || 0, price_1: parseFloat(document.getElementById('newPrice1').value) || 0, - price_2: elPrice2 ? (parseFloat(elPrice2.value) || 0) : 0, - price_3: elPrice3 ? (parseFloat(elPrice3.value) || 0) : 0, min_stock: parseInt(document.getElementById('newMinStock').value) || 0, - initial_stock: parseInt(document.getElementById('newInitialStock').value) || 0, + max_stock: parseInt(document.getElementById('newMaxStock').value) || 0, + tax_rate: parseFloat(document.getElementById('newTaxRate').value) || 0, location: document.getElementById('newLocation').value.trim(), + description: document.getElementById('newDescription').value.trim(), + is_active: document.getElementById('newIsActive').value === 'true', sku_aliases: [] }; var sku2 = document.getElementById('newSku2').value.trim(); var sku3 = document.getElementById('newSku3').value.trim(); var categoryId = document.getElementById('newCategory').value; - var subcategoryId = document.getElementById('newSubcategory').value; + if (categoryId) data.category_id = parseInt(categoryId); if (sku2) data.sku_aliases.push({sku: sku2, label: 'Alternativo 1'}); if (sku3) data.sku_aliases.push({sku: sku3, label: 'Alternativo 2'}); - if (subcategoryId) { - data.category_id = parseInt(subcategoryId); - } else if (categoryId) { - data.category_id = parseInt(categoryId); - } if (!data.part_number || !data.name) { document.getElementById('createResult').innerHTML = 'Numero de parte y nombre son obligatorios'; return; } - apiFetch(API + '/items', { method: 'POST', body: JSON.stringify(data) }).then(function (result) { - if (result && result.id) { - var msg = 'Creado ID ' + result.id + ' | Barcode: ' + result.barcode; - if (result.vehicle_compatibilities_added > 0) { + + var url = API + '/items'; + var method = 'POST'; + if (editId) { + url = API + '/items/' + editId; + method = 'PUT'; + // Stock inicial solo en creación + } else { + data.initial_stock = parseInt(document.getElementById('newInitialStock').value) || 0; + } + + apiFetch(url, { method: method, body: JSON.stringify(data) }).then(function (result) { + if (result && (result.id || result.message)) { + var msg = editId ? 'Producto actualizado' : ('Creado ID ' + result.id + ' | Barcode: ' + result.barcode); + if (!editId && result.vehicle_compatibilities_added > 0) { msg += ' | ' + result.vehicle_compatibilities_added + ' vehiculo(s) asignado(s) por IA'; } document.getElementById('createResult').innerHTML = '' + msg + ''; loadItems(currentPage); - // Close modal, clear form, refresh badges closeCreateModal(); - ['newPartNumber','newName','newBrand','newBarcode','newSku2','newSku3','newCost','newPrice1','newMinStock','newInitialStock','newLocation'].forEach(function(id) { - var el = document.getElementById(id); - if (el) el.value = ''; - }); if (window.loadInventoryStats) window.loadInventoryStats(); } else { document.getElementById('createResult').innerHTML = '' + (result ? result.error || 'Error' : 'Error de red') + ''; diff --git a/pos/templates/inventory.html b/pos/templates/inventory.html index 3967057..df89914 100644 --- a/pos/templates/inventory.html +++ b/pos/templates/inventory.html @@ -15,7 +15,7 @@ - + @@ -710,42 +710,48 @@ - +
-

Nuevo Producto

+

Nuevo Producto

+
- -
-
- +
-
-
+
+
+
-
+
+
+
+
+ +
+
@@ -1058,7 +1064,7 @@ - +