Using python with the BOI

SOLVED

We have a python (3.10) application that we want to use to create a sales order in Sage 100.

I have installed pywin32 so the python script can access win32/COM.

This code works up to the point where I try to call a method on the SY_Session object:

import win32com.client as win32
oScript = win32.dynamic.Dispatch("ProvideX.Script") #dynamic = late binding
print( oScript.State )
print( oScript.Instance ) 
oScript.Init("C:\Sage100_v2021\MAS90\Home")
print( oScript.State ) 
oSS = oScript.NewObject("SY_Session") 
r = oSS.nLogon() 

As expected the code outputs the following:

0
{A587B8B2-210E-4BF7-BC88-427744952D5F}
1

but when it tries to call the nLogon method on the session object it throws an error:

TypeError: 'int' object is not callable

The same program as VBS runs successfully on the workstation:

Set oScript = CreateObject ("ProvideX.Script")
MsgBox oScript.State
oScript.Init("C:\Sage100_v2021\MAS90\Home")
MsgBox oScript.State
Set oSS = oScript.NewObject("SY_Session")
retVal = oSS.nLogon()
If retVal=0 Then
	retVal = oss.nSetUser("craig","password") 
End If
MsgBox oSS.sUserName

Has anyone had any success using python and the BOI? 

  • 0

    It may be that Python requires escaping your back slashes on your init call.

    oScript.Init("C:\\Sage100_v2021\\MAS90\\Home")

  • 0 in reply to scmember

    I tried your suggestion - I'm getting the same result.

    Interestingly both oScript.Init("C:\\Sage100_v2021\\MAS90\\Home") and oScript.Init("C:\Sage100_v2021\MAS90\Home") produce the same results, so to test it I put in a bad path and ran it and did receive an error "The specified path does not exist", so python sees \ and \\ as the same inside the ""s.

  • 0 in reply to CraigJ

    Have you tried a simple non-Sage COM/OLE example in Python to make sure COM works? I got PHP COM to work a couple years ago but unable to get it to work in the current release. Python and PHP are mostly used with Linux and Windows seems not as well supported., Good luck with your effort.

  • 0 in reply to scmember

    It's partially working - I can interact with the ProvideX.Script object just fine - I can get the state, init it, ect.

    I've done further investigation and it seems that I can access properties of the SY_Session object.  For example:

    print( "Last Error Message " + oSS.sLastErrorMsg )
    print( "Registered Name " + oSS.sRegisteredName ) 

    Both work as expected.

    It seems that the issue is specifically related to calling a method.  

    retVal = oSS.nLogon()  is causing a TypeError: 'int' object is not callable

  • 0 in reply to CraigJ

    The problem could be passing an object as an argument with Python COM.

  • +1 in reply to CraigJ
    verified answer

    Let me preface this response by saying that my Python skills are very limited. That said, I debugged the OLE server (ProvideX Script) and noticed that no arguments were being passed to the methods, and all calls were flagged as property get's. I was able to get this working by wrapping the session object as a DumbDispatch (no ITypeInfo) and then using _FlagAsMethod for the methods that would be called.

    oSS = oScript.NewObject("SY_Session")
    
    oScript.TraceOn = 1
    
    session = win32.dynamic.DumbDispatch(oSS)
    
    session._FlagAsMethod("nSetUser")
    session._FlagAsMethod("nLogon")
    
    print(session)
    
    ret = session.nLogon()
    ret = session.nSetUser( "all" , "" )
    
    session.FinalDropObject()

    A couple of notes. First, the method names are case sensitive when using _FlagAsMethod, so use the same casing when calling the method. Second, when your done testing you can remove the oScript.TraceOn. I used it to determine what was happening on the backend, but is not needed for final code.  

    Hope this helps.

    Russell

  • 0 in reply to Russell Libby

    Thank you so much for figuring that out.  In retrospect it shouldn't be a surprise that Python needs to be explicitly told what a method is. 

    I will post the completed example here once I have it actually creating sales orders.

    Thanks again.

  • 0

    Here's the final test script to create a sales order in Sage. Thanks again to Russell

    # Original test script: CEJ-7/2022
    
    # install pywin32 via pip
    # https://pypi.org/project/pywin32/
    
    import win32com.client as win32
    import pythoncom
    import sys
    
    oScript = win32.dynamic.Dispatch("ProvideX.Script") # late binding
    oScript.Init( "C:\Sage100_v2021\MAS90\Home" )
    
    # python will not understand the typeinfo of the SY_Session object
    # so spool it up using DumbDispatch to create a python object, but with no type info
    oSession = win32.dynamic.DumbDispatch( oScript.NewObject("SY_Session") )
    
    # no type info so declare to python that the following are methods
    oSession._FlagAsMethod("nSetUser")
    oSession._FlagAsMethod("nLogon")
    oSession._FlagAsMethod("nSetCompany")
    oSession._FlagAsMethod("nSetDate")
    oSession._FlagAsMethod("nSetModule")
    oSession._FlagAsMethod("nSetProgram")
    oSession._FlagAsMethod("nLookupTask")
    
    # standard SY_Session stuff
    retVal = oSession.nLogon() 
    retVal = oSession.nSetUser("cjacobs", "12345678")
    retVal = oSession.nSetCompany("ABC")
    retVal = oSession.nSetDate("S/O","20220729")
    retVal = oSession.nSetModule("S/O")
    
    oSecurity = win32.dynamic.DumbDispatch( oSession.nSetProgram( oSession.nLookupTask("SO_SalesOrder_ui") ) )
    
    oSalesOrder = win32.dynamic.DumbDispatch( oScript.NewObject( "SO_SalesOrder_bus", oSession ) )
    oSalesOrder._FlagAsMethod("nGetNextSalesOrderNo")
    oSalesOrder._FlagAsMethod("nSetKey")
    oSalesOrder._FlagAsMethod("nSetValue")
    oSalesOrder._FlagAsMethod("nAddLine")
    oSalesOrder._FlagAsMethod("nWrite")
    
    # we need the lines child to be a python object as well, and we need to specify the methods we're going to use
    oSalesOrderLines = win32.dynamic.DumbDispatch( oSalesOrder.oLines )
    oSalesOrderLines._FlagAsMethod("nAddLine")
    oSalesOrderLines._FlagAsMethod("nSetValue")
    oSalesOrderLines._FlagAsMethod("nWrite")
    
    # this is fun - python doesn't support pass by reference, but the GetNextSalesOrderNo method passes back the so number by reference
    # win32com.client allows us to create an object variant and specify it with BYREF so we can get around this
    # define by ref, as a string, initial value ""
    SalesOrderNo = win32.VARIANT( pythoncom.VT_BYREF | pythoncom.VT_BSTR, "" )
    # oSalesOrder is a win32com.client.dynamic.DumbDispatch object and knows to pass the variable as a string, and catch the return in the value property
    if oSalesOrder.nGetNextSalesOrderNo( SalesOrderNo )<1:
        sys.exit( "oSalesOrder.nGetNextSalesOrderNo Last Error Message " + oSalesOrder.sLastErrorMsg )
        
    if oSalesOrder.nSetKey( SalesOrderNo.value )<2:
        sys.exit( "oSalesOrder.nSetKey Last Error Message " + oSalesOrder.sLastErrorMsg )
    else:   
        oSalesOrder.nSetValue("ARDivisionNo$", "02")
        oSalesOrder.nSetValue("CustomerNo$", "ORANGE")
        oSalesOrder.nSetValue("CustomerPONo$", "BUSOBJTSTPYTHON")
    
    if oSalesOrderLines.nAddLine()<2:
        sys.exit( "oSalesOrderLines.nAddLine() Last Error Message " )
    else:
        oSalesOrderLines.nSetValue("ItemCode$", "1001-HON-H252")
        oSalesOrderLines.nSetValue("WareHouseCode$", "000")
        oSalesOrderLines.nSetValue("QuantityOrdered", 2)
        # write line
        if oSalesOrderLines.nWrite() == 1:
            # write header
            if oSalesOrder.nWrite() == 1:
                sys.exit( "Sales Order " + str(SalesOrderNo.value) + " created " )
            else:
                sys.exit( "oSalesOrder.nWrite() Last Error Message " + oSalesOrderLines.sLastErrorMsg )
        else:
             sys.exit( " oSalesOrderLines.nWrite() Last Error Message " + oSalesOrderLines.sLastErrorMsg )

  • 0 in reply to CraigJ

    Congrats!

    Looks great.