diff --git a/pos/blueprints/inventory_bp.py b/pos/blueprints/inventory_bp.py index f9e1fff..6d3fe66 100644 --- a/pos/blueprints/inventory_bp.py +++ b/pos/blueprints/inventory_bp.py @@ -678,6 +678,110 @@ def api_return(): return jsonify({'operation_id': op_id, 'message': 'Return recorded'}) +@inventory_bp.route('/operations', methods=['GET']) +@require_auth('inventory.view') +def api_operations(): + """List inventory operations (purchases, sales, transfers, adjustments). + Supports filtering by operation_type, pagination, and date range. + """ + conn = get_tenant_conn(g.tenant_id) + cur = conn.cursor() + + op_type = request.args.get('type') + page = int(request.args.get('page', 1)) + per_page = int(request.args.get('per_page', 50)) + branch_id = request.args.get('branch_id', g.branch_id) + date_from = request.args.get('date_from') + date_to = request.args.get('date_to') + offset = (page - 1) * per_page + + # Build query dynamically + where_clauses = ['1=1'] + params = [] + + if op_type: + where_clauses.append('io.operation_type = %s') + params.append(op_type) + if branch_id: + where_clauses.append('(io.branch_id = %s OR io.branch_id IS NULL)') + params.append(branch_id) + if date_from: + where_clauses.append('io.created_at >= %s') + params.append(date_from) + if date_to: + where_clauses.append('io.created_at <= %s') + params.append(date_to + ' 23:59:59') + + where_sql = ' AND '.join(where_clauses) + + # Get total count + cur.execute(f""" + SELECT COUNT(*) FROM inventory_operations io + WHERE {where_sql} + """, params) + total = cur.fetchone()[0] + + # Get operations with product and employee info + cur.execute(f""" + SELECT + io.id, + io.operation_type, + io.quantity, + io.cost_at_time, + io.notes, + io.created_at, + io.employee_id, + e.name as employee_name, + i.id as inventory_id, + i.part_number, + i.name as product_name, + i.barcode, + io.branch_id, + b.name as branch_name + FROM inventory_operations io + LEFT JOIN inventory i ON io.inventory_id = i.id + LEFT JOIN employees e ON io.employee_id = e.id + LEFT JOIN branches b ON io.branch_id = b.id + WHERE {where_sql} + ORDER BY io.created_at DESC + LIMIT %s OFFSET %s + """, params + [per_page, offset]) + + rows = cur.fetchall() + operations = [] + for row in rows: + operations.append({ + 'id': row[0], + 'operation_type': row[1], + 'quantity': row[2], + 'cost_at_time': float(row[3]) if row[3] else None, + 'notes': row[4], + 'created_at': row[5].isoformat() if row[5] else None, + 'employee_id': row[6], + 'employee_name': row[7], + 'inventory_id': row[8], + 'part_number': row[9], + 'product_name': row[10], + 'barcode': row[11], + 'branch_id': row[12], + 'branch_name': row[13], + 'total': float(row[3] * row[2]) if row[3] and row[2] else None + }) + + cur.close() + conn.close() + + return jsonify({ + 'data': operations, + 'pagination': { + 'page': page, + 'per_page': per_page, + 'total': total, + 'total_pages': (total + per_page - 1) // per_page + } + }) + + # ─── Physical Count (two-phase: start → approve) ────────── @inventory_bp.route('/physical-count/start', methods=['POST']) diff --git a/pos/static/js/inventory.js b/pos/static/js/inventory.js index 7bdd44f..87cec3d 100644 --- a/pos/static/js/inventory.js +++ b/pos/static/js/inventory.js @@ -298,9 +298,19 @@ return; } apiFetch(API + '/adjustment', { method: 'POST', body: JSON.stringify(data) }).then(function (result) { - document.getElementById('adjustResult').innerHTML = result && result.operation_id - ? 'Ajuste registrado (op #' + result.operation_id + ')' - : '' + (result ? result.error || 'Error' : 'Error de red') + ''; + if (result && result.operation_id) { + document.getElementById('adjustResult').innerHTML = 'Ajuste registrado (op #' + result.operation_id + ')'; + closeAdjustmentModal(); + ['adjustItemId','adjustQty','adjustReason'].forEach(function(id) { + var el = document.getElementById(id); + if (el) el.value = ''; + }); + if (window.loadInventoryStats) window.loadInventoryStats(); + if (window.loadOperations) window.loadOperations('ajustes', 1); + loadItems(currentPage); + } else { + document.getElementById('adjustResult').innerHTML = '' + (result ? result.error || 'Error' : 'Error de red') + ''; + } }); } @@ -329,12 +339,119 @@ return; } apiFetch(API + '/transfer', { method: 'POST', body: JSON.stringify(data) }).then(function (result) { - document.getElementById('transferResult').innerHTML = result && result.out_operation_id - ? 'Transferencia registrada' - : '' + (result ? result.error || 'Error' : 'Error de red') + ''; + if (result && result.out_operation_id) { + document.getElementById('transferResult').innerHTML = 'Transferencia registrada'; + closeTransferModal(); + ['transferItemId','transferFrom','transferTo','transferQty','transferNotes'].forEach(function(id) { + var el = document.getElementById(id); + if (el) el.value = ''; + }); + if (window.loadInventoryStats) window.loadInventoryStats(); + if (window.loadOperations) window.loadOperations('traspasos', 1); + loadItems(currentPage); + } else { + document.getElementById('transferResult').innerHTML = '' + (result ? result.error || 'Error' : 'Error de red') + ''; + } }); } + // ===================================================================== + // OPERATIONS LIST (Entradas, Salidas, Traspasos, Ajustes) + // ===================================================================== + + var opTypeMap = { + 'entradas': 'PURCHASE', + 'salidas': 'SALE', + 'traspasos': 'TRANSFER', + 'ajustes': 'ADJUST' + }; + + function loadOperations(type, page) { + var opType = opTypeMap[type]; + if (!opType) return; + page = page || 1; + var params = new URLSearchParams({ type: opType, page: page, per_page: 50 }); + apiFetch(API + '/operations?' + params.toString()).then(function (data) { + if (!data) return; + var tbodyId = type + 'TableBody'; + var footerId = type + 'Footer'; + var pagId = type + 'Pagination'; + var tbody = document.getElementById(tbodyId); + var ops = data.data || []; + if (!ops.length) { + tbody.innerHTML = '