A wish as old as Navision: A blanket client change via program code. And unfortunately it has remained a wish until today... or has it?
Changecompany – Problem with triggers
A typical task in Navision is, for example, the import of data from a single file into different clients. For example, stocks are read in from a WMS (warehouse management system), which are assigned to different clients in the ERP.
Also, if you want to replace a mainframe application of the 70s/80s, e.g. Trampolin or Quattro Pro on the Siemens Nixdorf, then you/your application may not even know the term "client".
Basically, exactly this reading is very simple. Mainframe replacement not so much... The receiving table is either set as DataPerCompany = NO, and so all records can be read in directly in one go,
or the receiving table is left on the default DataPerCompany = , and the client is switched for each data record to be read in with ImportTable.changecompany(target client of the data record to be read in).
Neither of these is technically challenging.
However, it will be difficult to a) bring this data correctly into the ArticleBookLeaf rows of the respective target client and b) to update this table correctly.
Why? Everyone who has worked with ChangeCompany has stumbled across it.
Let's do it with this simple example:
CLEAR(item); //Important: must be placed BEFORE ChangeCompany! Clear resets ChangeCompany, in contrast to Init. item.CHANGECOMPANY(client2) Item.INSERT(true); //The TRUE is the evil one!
Presumably, this code will work exactly the same way.
But what is happening?
The article is created in client2 , but with an article number from the current client!
Why? Let's take a look at the InsertTrigger of ItemTable 27 in the overall context:
CLEAR(item); item.CHANGECOMPANY(client2) Item.INSERT(true); -> IF "No." = '' THEN BEGIN GetInvtSetup; InvtSetup.TESTFIELD("Item Nos."); //InvtSetup comes from client1! NoSeriesMgt.InitSeries(InvtSetup."Item Nos.",xRec."No. Series",0D,"No.","No. Series"); //The number series code unit runs with all records in it in client 1! "Costing Method" := InvtSetup."Default Costing Method"; //InvtSetup comes from client1! END;
There is no way to change this behavior. Anyone working with ChangeCompany for the first time will almost inevitably stumble across this behavior. The normal expectation (in my opinion) is that the table "knows" that it works in client2, and therefore all program logic in it automatically and implicitly works with records from client2.
Mhm... Spoiler alert: It does not. The active global client applies to the session, not the object. So for the current user context.
Another funny example is this one:
Customer.get('customer number');
Customer.CHANGECOMPANY(client2);
Customer.calcfields(Balance);
IF the customer with the "customer number" exists in client 2, calcfields(Balance) will calculate the balance for... this customer number in the current client 1! No matter which customer this is! Because also the necessary Detailed Cust. Ledg. Entry (Table 339) are taken from the client of the user context, i.e. from the active client! If there is this customer number (better: the Detailed Cust. Ledg. Entry (Table 339) entries) for this customer does not exist in the current client, the result is simply 0, even if this customer has a balance in the client Client2.
The solution for the CalcFields problem is quite simple: The Calcfield must "simply" be emulated over the corresponding table, e.g.
CustLedgEntry.setrange(„Customer No.“, „customer number“);
CustLedgEntry.Calcsums(Amount);
But what do we do with the item posting sheet or any other posting sheet? Or generally when inserting records? Records can be initialized with templates, for example. Since the standard Navision logic does not perform a client change for triggers, ANY relevant logic would have to be reprogrammed with the necessary client change. A simple "Table.ChangeCompany(NewClient, Global=True)" would make our lives so much easier...
Well... unfortunately, there is no such thing. Well... There might be!
Complete client change with all triggers!
StartSession(SessionID,CodeunitNo,Company,Record) allows to start any code unit as a new session - in any client. Since the client (=Company) is valid for one session, the client given as parameter is valid for the whole runtime of this code unit - and all objects, triggers etc. called in or by this code unit.
So the key is to read the files to be imported into the respective temporary tables for each client in the import example above without any triggers at all, and then pass this temporary table to a start session.
Attention! If this is to happen in one go, then you also need temporary tables (e.g. as array), or you have to store the valid client in the table as well, and then react correctly in the called code unit!
So it would be
Case AktuelleZeileMandant of
'1' : TempTableClient1.FieldValue = Any;
'2' : TempTableClient2.FieldValue = Any;
end;
or
TempTableClient[CurrentRowClient].FieldValue = Any;
However, it is much more elegant to move the complete read-in logic / processing logic directly into the new session:
SessionStartetAt := CREATEDATETIME(WORKDATE,TIME); STARTSESSION(SessionEvent."Session ID",50010,pCompanyCode,ImportAS400); Log.Log(ObjRef,'SessionStart'+ pCompanyCode,SessionEvent."Session ID",''); SLEEP(100); ActiveSession.SETRANGE("Session ID",SessionEvent."Session ID"); WHILE ActiveSession.FINDFIRST DO BEGIN SessionDuration := ROUND((CREATEDATETIME(WORKDATE,TIME) - SessionStartetAt) / 1000,1,'<'); SessionWindow.UPDATE(2,ROUND(SessionDuration / pEstDurationS * 10000,1,'<')); SessionEvents.SETRANGE("Session Unique ID",ActiveSession."Session Unique ID"); SLEEP(1000); END; Log.Log(ObjRef,'Session Duration '+pCompanyCode,SessionDuration,pFileName); SessionEvents.SETCURRENTKEY("Session Unique ID"); IF SessionEvents.FINDLAST THEN Log.Log(ObjRef,'SessionEnd '+ pCompanyCode,SessionEvents.Comment,pFileName);
Of course, this is only the basic framework. If you have a need here, please contact me.
Especially in the area of AS/400 data transfers , there is often the need to read tables that are output across clients into separate clients in Navision Financials or Business Central. Please keep in mind that you should a) never set a standard Navision table to "Cross-Client = Yes". You are opening a Pandora's box. And b) That you can't do that since BC 15... fortunately.
But... how do you know when this session is ready?
This is the automatic following problem with the sessions. They are self-sufficient. They work on their own. When you are done, they are done.
The simplest thing is to build the tables to be processed in such a way that you can fire them off with start session, and at some point they are just finished, and that's it. Of course, this does not fit if the data to be imported build on each other. E.g. first the article master data, then the corresponding transaction data.
But if you really want to know when they are finished, you need something like a return value. StartSession even provides it: OK := StartSession(...
However OK returns only whether the session could be started, not whether it was terminated correctly. So this is no comparison to the known OK=Codeunit.run..., which a) waits, if the codeunit was finished, and b) also returns a result: TRUE: Correctly processed FALSE: An error occurred in the processing, which would have led to an error. There is no such thing with StartSession.
Well... Fortunately Navision / Business Central still has enough roots of Navision, so that you can still form a workaround around the increasingly complicated Microsoft part.
The code unit started with start session in Navision or Business Central enters a new value in the session table! And at termination an entry in the SessionEvents, if available with termination error message.
Here is a tip: Enter the two existing pages 670 & 9506 as well as a new page on the table 2000000111 Session Event directly into the menu site under Application Tools:
In the above example, waiting for the freshly started client-specific session to end is already built in!