feat(inventory): list operations in Entradas/Salidas/Traspasos/Ajustes tabs

- Add GET /operations endpoint with filtering by type, pagination, date range
- Join with inventory, employees, branches for rich display
- Add tbody IDs and footer/pagination IDs to operation tables in HTML
- Add loadOperations() JS function with renderOperationRow() per type
- Integrate loadOperations into switchTab for auto-load on tab change
- Update recordPurchase/Adjustment/Transfer to refresh respective lists
- Expose loadOperations globally for HTML inline script access
This commit is contained in:
2026-05-18 06:00:58 +00:00
parent bfa7bc2997
commit 60dd8162f7
3 changed files with 244 additions and 18 deletions

View File

@@ -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'])