I am trying to automate SAP data extraction using scripting. The problem I am facing is a recorded script in SAP isn't working when I'm running it, when I use findById("id") method it comes out that cannot be found, however the tabs are there.
The idea is to move between the tabs (using session.findById("id").Select) to extract info in that panels. Use a list of Purchase Orders (PO's), make a loop and extract the information, it's simple.
However, these tabs aren't found randomly. Sometimes it works, sometimes it is not found. All PO's (if I do it manually) have tabs with the data, but in the script it doesn't work.
For example:
The red box is the tabs that I am trying to select
Output of the recorded script (just moving between tabs):
If Not IsObject(application) Then
Set SapGuiAuto = GetObject("SAPGUI")
Set application = SapGuiAuto.GetScriptingEngine
End If
If Not IsObject(connection) Then
Set connection = application.Children(0)
End If
If Not IsObject(session) Then
Set session = connection.Children(0)
End If
If IsObject(WScript) Then
WScript.ConnectObject session, "on"
WScript.ConnectObject application, "on"
End If
session.findById("wnd[0]").resizeWorkingPane 183,24,false
session.findById("wnd[0]/usr/subSUB0:SAPLMEGUI:0015/subSUB3:SAPLMEVIEWS:1100/subSUB2:SAPLMEVIEWS:1200/subSUB1:SAPLMEGUI:1301/subSUB2:SAPLMEGUI:1303/tabsITEM_DETAIL/tabpTABIDT13").select
' Extract info
session.findById("wnd[0]/usr/subSUB0:SAPLMEGUI:0019/subSUB3:SAPLMEVIEWS:1100/subSUB2:SAPLMEVIEWS:1200/subSUB1:SAPLMEGUI:1301/subSUB2:SAPLMEGUI:1303/tabsITEM_DETAIL/tabpTABIDT15").select
' Extract info
session.findById("wnd[0]/usr/subSUB0:SAPLMEGUI:0015/subSUB3:SAPLMEVIEWS:1100/subSUB2:SAPLMEVIEWS:1200/subSUB1:SAPLMEGUI:1301/subSUB2:SAPLMEGUI:1303/tabsITEM_DETAIL/tabpTABIDT18").select
' Extract info
Error:
The control could not be found by id.
I'm using:
My theory, the tab is hidden and doesn't find it, that I would have to use the arrows to move, however when I use the arrows at the time of making the script recording, it simply doesn't add them to the code.
Sorry for my English, and thanks in advance for your time.
I can only offer a workaround on this phenomenon.
for example:
...
for i = 1 to 99
on error resume next
session.findById("wnd[0]/usr/subSUB0:SAPLMEGUI:00" & right("0" & cstr(i),2) & "/subSUB3:SAPLMEVIEWS:1100/subSUB2:SAPLMEVIEWS:1200/subSUB1:SAPLMEGUI:1301/subSUB2:SAPLMEGUI:1303/tabsITEM_DETAIL/tabpTABIDT13").select
if err.number = 0 then exit for
on error goto 0
next
on error goto 0
' Extract info
for i = 1 to 99
on error resume next
session.findById("wnd[0]/usr/subSUB0:SAPLMEGUI:00" & right("0" & cstr(i),2) & "/subSUB3:SAPLMEVIEWS:1100/subSUB2:SAPLMEVIEWS:1200/subSUB1:SAPLMEGUI:1301/subSUB2:SAPLMEGUI:1303/tabsITEM_DETAIL/tabpTABIDT15").select
if err.number = 0 then exit for
on error goto 0
next
on error goto 0
' Extract info
for i = 1 to 99
on error resume next
session.findById("wnd[0]/usr/subSUB0:SAPLMEGUI:00" & right("0" & cstr(i),2) & "/subSUB3:SAPLMEVIEWS:1100/subSUB2:SAPLMEVIEWS:1200/subSUB1:SAPLMEGUI:1301/subSUB2:SAPLMEGUI:1303/tabsITEM_DETAIL/tabpTABIDT18").select
if err.number = 0 then exit for
on error goto 0
next
on error goto 0
' Extract info
Regards,
ScriptMan
Ah yes, my favorite SAP screen: ME23N. Who knows what the SAPLMEGUI:00XX will be! But, if you find the element by it's name you'll never run into this problem. Additionally, this solves the problem of the tab you want not being there; because, depending on the line item of the PO, who knows what tabs will be available.
Here's how I get over this every time, without fail.
In your sub procedure use the function below. If the tab is there it will select it and return true and you can continue extracting your data. You need the tab text and the name of the tab strip.
You can find the name of the tab strip easily. Look at what has been recorded. It has the "tabs" prefix.
session.findById("wnd[0]/usr/subSUB0:SAPLMEGUI:0015/subSUB3:SAPLMEVIEWS:1100/subSUB2:SAPLMEVIEWS:1200/subSUB1:SAPLMEGUI:1301/subSUB2:SAPLMEGUI:1303/tabsITEM_DETAIL/tabpTABIDT13")
Public Sub Main()
If IsTabThere("ITEM_DETAIL", "Account Assignment") = True Then
' Extract info
End If
If IsTabThere("ITEM_DETAIL", "Purchase Order History") = True Then
' Extract info
End If
End Sub
Public Function IsTabThere(ByVal tabStripName As String, ByVal tabText As String) As Boolean
Dim userArea As Object
Dim tabStrip As Object
Dim tabToSelect As Object
Set userArea = session.FindById("wnd[0]/usr")
Set tabStrip = userArea.FindByName(tabStripName, "GuiTabStrip").Children
For Each tabToSelect In tabStrip
If tabToSelect.Text = tabText Then
tabToSelect.Select
IsTabThere = True
' Will exit here if the tab was selected and return true
Exit Function
End If
Next
IsTabThere = False
End Function
Hope this makes your day. Good luck!
If your interested in making your script more dynamic checkout my answer on this post. I explain how to get started using the SAP GUI Scripting API.
how-to-run-sap-gui-script-from-excel-macro
Related
I am really struggling and wondered if someone could help please? I am trying to set up a database with images which I have done and used the file path of my pictures. It works well and that is great, but the next step is to have a form that just displays the pictures from each record to act as a catalogue and then When you click on a particular picture in that form it opens up another form that contains all the records and goes directly to that specified record. We have managed to get it to work for the first picture and record but can't work out how to make it go to the next record for the next image. I am trying to use a multi line form for this and only have the picture visible. I am just using a basic code as I have only just started with VBA but I believe this may not work. Can someone please help advise me?
I have used the following code which was suggested online for working with pictures and then there are a couple of other vba code to add to it. I need to create a catalogue inventory but we want to click on the picture of the inventory item which then opens a form with all the detail. I have found a work around with using the continous form and transparent buttons which take you to the specific record but you can't seem to change the layout. Ideally I am looking to have a form with just the images in a grid style going across the page and not just a list going down the page...if that makes sense?
Option Compare Database
Option Explicit
Public Function DisplayImage(ctlImageControl As Control, strImagePath As Variant) As String
On Error GoTo Err_DisplayImage
Dim strResult As String
Dim strDatabasePath As String
Dim intSlashLocation As Integer
With ctlImageControl
If IsNull(strImagePath) Then
.Visible = False
strResult = "No image name specified."
Else
If InStr(1, strImagePath, "\") = 0 Then
' Path is relative
strDatabasePath = CurrentProject.FullName
intSlashLocation = InStrRev(strDatabasePath, "\", Len(strDatabasePath))
strDatabasePath = Left(strDatabasePath, intSlashLocation)
strImagePath = strDatabasePath & strImagePath
End If
.Visible = True
.Picture = strImagePath
strResult = "Image found and displayed."
End If
End With
Exit_DisplayImage:
DisplayImage = strResult
Exit Function
Err_DisplayImage:
Select Case Err.Number
Case 2220 ' Can't find the picture.
ctlImageControl.Visible = False
strResult = "Can't find image in the specified name."
Resume Exit_DisplayImage:
Case Else ' Some other error.
MsgBox Err.Number & " " & Err.Description
strResult = "An error occurred displaying image."
Resume Exit_DisplayImage:
End Select
End Function
For various reasons, I want to set up a custom button on my forms to save the current record. I use a navigation form and want to trigger the same process (integrity-checks, user input etc.) whenever the entry is saved, thus whenever the user presses the "save"-button or switches to another form. The user will conditionally be asked to confirm the process and is thus able to cancel it as well.
Everything is running smoothly with one really odd and annoying exception: Whenever I click the save button on a new record and prompt a message within the "BeforeUpdate" event, I receive
RTE 3021 ("no current record")
Without the MsgBox, everything is fine. Even more strange:
When I trigger the save process by switching to another form using the navigation form (or simply press "outside" the form used for data entry), everything is fine as well.
Here is a minimalistic example (similar results with DoCmd.Save, Requery or acCmdSaveRecord):
Private Sub vt_save_Click()
Me.Dirty = False
End Sub
Private Form_BeforeUpdate(Cancel As Integer)
Cancel = True
MsgBox "Test"
End Sub
Any ideas? I simply can't wrap my head around that error.
You could maybe try to run a query using the values in the form while checking if the record exists or not.
Is there a primary key on the table? if so, the primary key will be your focal point.
Private Sub vt_Save_Click()
dim rst as DAO>Recordset
Dim strSQL as String
Dim strID as string
strID = me.YourPrimaryKeyField
strSQL = "SELECT * " & _
"FROM YourTableName " & _
"WHERE (((YourTableName.YourFieldName) =" & me.PrimaryKeyField & "));"
set rst = currentdb.openrecordset(strsql)
if rst.recordcount = 0 then
currentdb.execute "INSERT INTO YourTableName ( List All Fields to Add ) " & _
"SELECT List All Field controls with values to add;"
End IF
'Anything else you want the code to do from here
EndCode:
If not rst is nothing then
rst.close
set rst = nothing
End IF
End Sub
Repeat this process for the Form_LostFocus() event. If you want to make it easier, make this code a module and call within both event triggers on your form.
If this doesn't work please let me know and I will be happy to further assist.
The most straight forward and reasonable solution is to use an Error Handler - which I ignored so far tenaciously.
Private Sub save_Click()
On Error GoTo Err_Handler
Me.Dirty = False
Exit_Here:
Exit Sub
Err_Handler:
If Err.Number = 2101 Then
'ignore or message
Else
MsgBox Err.Description
End If
Resume Exit_Here
End Sub
Trying to help our Project 2013 Users out with some VBA code, and we have come to a point where we can't seem to find an answer for finding if a Project 2013 file is checked out on our PWA server using VBA. They basically have a list of Projects set as tasks in a single Project file, and the VBA code loops through the list of tasks to run FileOpenEx, do some changes, and then closes it. However, the need is to be able to check to see if the Project File is checked out prior to running FileOpenEx on each Project in the list. Here is a sample of what I'm going for that doesn't quite do what I want it to.
SelectBeginning
While ActiveCell.CellColor <> pjBlack
fname = "<>\" & ActiveCell.Task.Name
justname = ActiveCell.Task.Name
On Error Resume Next
If Application.Projects.CanCheckOut(fname) Then '<--This does not work correctly, not checking Enterprise Projects?
FileOpenEx Name:=fname, ReadOnly=false
'Do Some stuff
FileCloseEx Save:=pjSave, CheckIn:=True
FileSave
Else
MsgBox (justname & " can not be checked out")
End If
SelectCell Row:=1
Wend
If anyone has a better solution, an easy way to check this, or another workaround to finding out if an Enterprise Project is checked out through VBA code, please let me know. Thanks!
We created a workaround that works for the planners, but we do have to open the file either way. What this will do is open the file in Read Only mode, then attempt to check it out without alerts. Afterwards, if I have it checked out (which means no one else had it checked out), it'll set j=0 and save, then move on to the next project. If someone else has it checked out, then it'll go to 'errorhandler' which tells the project to close without saving, and save the filename in a string to be returned later.
SelectBeginning
While ActiveCell.CellColor <> pjBlack
fname = "<>\" & ActiveCell.Task.Name
justname = ActiveCell.Task.Name
FileOpenEx Name:=fname, ReadOnly=true
Set ProjToOpen = Application.Projects.Application.ActiveProject
j = 1
Application.DisplayAlerts = False
ProjToOpen.Checkout Project
Application.DisplayAlerts = True
If Not Application.IsCheckedOut(ProjToOpen.Name) Then
GoTo errorhandler
End If
'Perform actions here
j = 0
FileCloseEx Save:=pjSave, CheckIn:=True
FileSave
errorhandler:
If Not j = 0 Then
ReDim Preserve skippedfiles(0 to skipped) As String
skippedfiles(skipped) = justname
skipped = skipped + 1
ProjToOpen.Application.FileCloseEx Save:=pjDoNotSave
GoTo GoToNextProj
End If
GoToNextProj:
SelectCell Row:=1
Wend
msgstring = Join(skippedfiles(), vbCr)
MsgBox "Here are the files that were already checked out and therefore not changed: " & vbCr & msgstring
I have a very strange problem here. Here's the code:
reqLang = "ENG"
Select Case reqLang
Case "CRO", "ENG"
'first loop -------------------------------------
On Error GoTo reqLangVisible
i = 1
'Loop until ccCROENG's are all hidden and then go to reqLangVisible.
Do
ActiveDocument.SelectContentControlsByTag("ccCROENG")(i) _
.Range.Font.Hidden = True 'hides all CCs
i = i + 1
Loop
reqLangVisible:
'second loop -------------------------------------
On Error GoTo langOut
i = 1
'Loop until ccreqLang's are all visible and then go to langOut.
Do
ActiveDocument.SelectContentControlsByTitle("cc" & reqLang)(i) _
.Range.Font.Hidden = False 'activates reqLang CCs
i = i + 1
Loop ' CAN'T GET OUT -----------------------------------
Case "CROENG"
i = 1
'Loop until ccCROENG's are all visible and then go to langOut.
Do
On Error GoTo langOut
ActiveDocument.SelectContentControlsByTag("ccCROENG")(i) _
.Range.Font.Hidden = False 'Shows all CCs
i = i + 1
Loop
End Select
langOut:
MsgBox "Success!" '------- yeah, not really.
Stop
I hope it's clear enough what it's trying to do (at least programming-wise). I have multiple ContentControls(CCs) with same titles and tags. The problem I end up with is marked with CAN'T GET OUT, because, you guessed it - I can't get of this second loop! I end up with the Out of range error because it runs out of CCs.
What's even weirder is that it actually did get out of the first loop which has the exact same On Error statement, thought pointing to a different section.
Is it me, or did I just - however unlikely - run onto a bug in VBA?
In any case, is there a solution or at least a workaround?
Typically you only use error handling for dealing with unexpected or unpredictable situations, such as not being able to access a drive, or finding you have no network access.
Error handling is not intended as a substitute for reasonable checks which could otherwise be done. i.e. collections have a Count property which you can use when looping over their items, so avoiding any error caused by trying to access Item(n+1) when there are only n items (and here you know n from Count). Alternatively, use a For Each loop.
Here's some sample code demonstrating use of two methods for looping over your controls:
Sub Tester()
Dim cc1 As ContentControls, cc2 As ContentControls
Dim c, i As Long
With ActiveDocument
Set cc1 = .SelectContentControlsByTag("tbTag")
Set cc2 = .SelectContentControlsByTitle("tbTitle")
End With
Debug.Print "cc1 has " & cc1.Count
Debug.Print "cc2 has " & cc2.Count
'use the Count property
For i = 1 To cc1.Count
Set c = cc1(i)
c.Range.Font.Hidden = True
Next i
'use a For Each loop
For Each c In cc2
c.Range.Font.Hidden = False
Next c
End Sub
This is the type of scenario for which this type of flow control is designed.
Applied to your original code:
Sub Tester2()
Dim reqLang, cc As ContentControls, c
reqLang = "ENG"
Select Case reqLang
Case "CRO", "ENG"
Set cc = ActiveDocument.SelectContentControlsByTag("ccCROENG")
SetTextHidden cc, True
Set cc = ActiveDocument.SelectContentControlsByTitle("cc" & reqLang)
SetTextHidden cc, False
Case "CROENG"
Set cc = ActiveDocument.SelectContentControlsByTag("ccCROENG")
SetTextHidden cc, False
End Select
MsgBox "Success!" '-- yeah really
End Sub
Sub SetTextHidden(cc As ContentControls, MakeHidden As Boolean)
Dim c
For Each c In cc
c.Range.Font.Hidden = MakeHidden
Next c
End Sub
So if you've read my comment, and to formally answer your question, it is not a bug.
You just need to use Error Handling Routines correctly.
What you're trying to do is somewhat like below. HTH.
Select Case reqlang
Case "CRO", "ENG"
On Error Resume Next '~~> ignores the error when encountered
'~~> Your loop which possibly creates the error goes here
On Error Goto 0 '~~> resets the actively triggered error handling
Case "CROENG"
On Error Resume Next '~~> ignores the error when encountered
'~~> Your loop which possibly creates the error goes here
On Error Goto 0 '~~> resets the actively triggered error handling
End Select
MsgBox "Success"
But as the link suggest, you need to handle errors and not simply disregard them. Try cheking on the actual error and find a way to correct it or avoid it.
You'll be surprise that you won't even be needing the Error Handling Routine.
Before you go for the obvious: Application.DisplayAlerts = False has not solved my problem.
I have written a VBA procedure (initiated in Excel 2010) which loops around an array containing different Excel files. The loop opens the file, refreshes the data, saves and closes the file for each item in the array. I have written an error catch sub routine so I log which excel files have failed to open/refresh/save etc so a user can manually check them.
Some files are quite large and involve a large amount of data moving across the network; sometimes I get a dialog box with: Excel is waiting for another application to complete an OLE action.
I could use Application.DisplayAlerts = False to disable the message but this would presumably disable all alerts so I couldn't catch the errors?
Further I have tested using the line and it doesn't stop the dialog box pop-up. If I press enter it carries on but will likely pop-up again a few minutes later.
Is there a way to stop is message specifically without stopping other alerts?
NB. My process has a control instance of Excel which runs the VBA and opens the workbooks to be refreshed in a separate instance.
Thanks for your help
An extract of my code is below which contains the refresh elements
Sub Refresh_BoardPivots_Standard()
' On Error GoTo Errorhandler
Dim i
Dim errorText As String
Dim x
Dim objXL As Excel.Application
Set objXL = CreateObject("Excel.Application")
GetPivotsToRefresh ' populate array from SQL
For Each i In StandardBoardPiv
DoEvents
'If File_Exists(i) Then
If isFileOpen(i) = True Then
errorText = i
Failed(failedIndex) = errorText
failedIndex = failedIndex + 1
Else
objXL.Visible = True 'False
objXL.Workbooks.Open FileName:=i
If objXL.ActiveWorkbook.ReadOnly = False Then
BackgroundQuery = False
Application.DisplayAlerts = False
objXL.ActiveWorkbook.RefreshAll
objXL.Application.CalculateFull
objXL.Application.DisplayAlerts = False
objXL.ActiveWorkbook.Save
objXL.Application.DisplayAlerts = True
objXL.Quit
Else
errorText = i
Failed(failedIndex) = errorText
failedIndex = failedIndex + 1
objXL.Application.DisplayAlerts = False
objXL.Quit
Application.DisplayAlerts = True
End If
End If
' Else
' errorText = i
' Failed(failedIndex) = errorText
' failedIndex = failedIndex + 1
' End If
DoEvents
If Ref = False Then
Exit For
End If
Next i
Exit Sub
'Errorhandler:
'
'errorText = i
'Failed(failedIndex) = errorText
'failedIndex = failedIndex + 1
'Resume Next
End Sub
"Waiting for another application to complete an OLE action" isn't an alert message you can just turn off and forget, sometimes the macro will be able to continue on after, but in my experience if you are getting that error its only a matter of time until the problem crashes/freezes your whole macro so it should definitely be troubleshot and corrected.
I only get that error when I am using additional Microsoft Office Applications (other than the Excel that is running the code) as objects and one of them has an error- the Excel running the code doesn't know that an error occurred in one of the other applications so it waits and waits and waits and eventually you get the "Waiting for another application to complete an OLE action" message...
So to troubleshoot this sort of problem you got to look for the places you use other MSO apps... In your example, you have an additional instance of Excel and you are pulling data from Access, so its most likely one of those two that is causing the problems...
Below is how I would re-write this code, being more careful with where the code interacts with the other MSO apps, explicitly controlling what is happening in them.. The only piece I couldn't really do much is GetPivotsToRefresh because I cant see what exactly youre doing here, but in my code I just assumed it returned an array with a list of the excel files you want to update. See code below:
Sub Refresh_BoardPivots_Standard()
Dim pivotWB As Workbook
Dim fileList() As Variant
Dim fileCounter As Long
Application.DisplayAlerts = False
fileList = GetPivotsToRefresh 'populate array from SQL
For fileCounter = 1 To UBound(fileList, 1)
Set pivotWB = Workbooks.Open(fileList(fileCounter, 1), False, False)
If pivotWB.ReadOnly = False Then
Call refreshPivotTables(pivotWB)
pivotWB.Close (True)
Else
'... Error handler ...
pivotWB.Close (False)
End If
Next
End Sub
Public Sub refreshPivotTables(targetWB As Workbook)
Dim wsCounter As Long
Dim ptCounter As Long
For wsCounter = 1 To targetWB.Sheets.Count
With targetWB.Sheets(wsCounter)
If .PivotTables.Count > 0 Then
For ptCounter = 1 To .PivotTables.Count
.PivotTables(ptCounter).RefreshDataSourceValues
Next
.Calculate
End If
End With
Next
End Sub
So I created my own 'refreshPivotTables' but you could have embedded that into the master sub, I just thought the loops and loop counters might get a little messy at that point...
Hope this helps,
TheSilkCode