Wednesday, 20 May 2026

Multi-company forms

Yesterday I was asked to create a multi-company form. I know how to create a multi-company report but I had yet to create a form like this. Here are the conditions for a multi-company report:

• A displayed column, with a Column Name of TITLE and a Table Name of ENVIRONMENT. • A hidden column, with a Column Name of DNAME and a Table Name of ENVIRONMENT. Its Expression/Condition should be: = SQL.ENV

Naively, I added these fields to the form. As it happens, my customer wanted the company name to appear anyway, so ENVIRONMENT.TITLE was necessary. When I opened the form, there was massive duplication of data (a cartesian join); basically there were no conditions on the Environment table which is why everything appeared several times.

I then turned to the SDK; as it happens, I have a new copy of the SDK for version 25.1 that documents many things that previously were not documented, but unfortunately there is very little about multi-company forms. There is a section heading that reads simply To prevent users from defining a given form as a multi-company form, specify x in the Oneto-many column, but it doesn't mention exactly how to create one as opposed to preventing one. There are a few more mentions about multi-company forms but these are about variables and not relevant.

That one sentence did give me an idea, though. I went to the one-to-many column in the form header and saw that there was an option 'm' that defines a multi-company form. Choosing that option gave me the message 'There must be a field called ZOOMDNAME, a character string of length 8 characters. I guessed that this should replace the field with the column name DNAME (even though the field name is still DNAME as this is the name of the field within the ENVIRONMENT table), and that the TITLE column should be replaced by ZOOMDTITLE. 

Lo and behold, the cartesian join disappeared and the correct data appeared. So this functionality is still undocumented. There are at least 20 standard forms with the 'm' flag (the actual number depends on the version) so one can learn from these.

Tuesday, 14 April 2026

WINHTML flags

WINHTML is the procedure to call when one wants to print/save/view an HTML document. There are two flavours to this procedure: the earlier flavour receives a linked table of records and the later flavour that allows each record to be printed separately.

There is an error in the documentation for the later flavour that I will show here. The syntax is

EXECUTE WINHTML '-d', 'document_name', 'table', 'linked_file', '-v', 'record_id', ['-trc', debug_file,] ['-s',] ['-e',] ['-edoc' | '-signpdf',] ['-format', format_num,] ['-lang', lang_num,] ['-AMAIL',] ['-o' |'-pdf' | '-wo' | '-wpdf',] ['output_file',]

Here is an example using this syntax. The output is sent to a PDF file that is later sent to an email address.

:FNAME = STRCAT (:DISK, :PAR2, '.PDF'); EXECUTE WINHTML '-d', :ENAME, '', '', '-v', :ADOC, '-format, -10', '-pdf', :FNAME; MAILMSG 5 TO EMAIL :EMAIL DATA :FNAME;

Returning to the documentation, the description of the '-s' flag is that it supresses the notification window that pops up when preparing the document. In my experience, it does not supress the notification, but more importantly, it causes the document not to appear, or in the case of my example code, no PDF code is created!

So DO NOT USE THE -s FLAG!

Monday, 16 February 2026

Error messages from a procedure invoked by a form trigger are not displayed

I have a private POST-UPDATE trigger in form ORDERS that invokes a procedure whose task is to extract certain lines from the current order and insert them into a new order (this isn't particularly relevant). The procedure checks certain data and displays error messages if the checks fail. It transpires that these error messages are displayed when the procedure is run from a menu, but not when invoked from a form, which is when they are needed the most.

Below I describe a slightly complicated way to get around this problem. First I defined a new table TEST_ERRMSGS that has four fields: AKEY, USER, PROG and MESSAGE, where the first three fields create the composite primary key ('U' in Priority-speak). I can't be sure that someone else is going to run the same procedure, probably on a different order but who knows, so I need maximum 'separation'. AKEY will be the order number, USER is of course the user's number and PROG is the number of the procedure. Here is the relevant part of the form trigger pre-enhancement

SELECT SQL.TMPFILE INTO :ECORDERS FROM DUMMY; LINK ORDERS OEC TO :ECORDERS; GOTO 999 WHERE :RETVAL <= 0; INSERT INTO ORDERS OEC SELECT * FROM ORDERS ORIG WHERE ORD = :$.ORD; EXECUTE WINACTIV '-P', 'TEST_SEPARATE', 'ORDERS', :ECORDERS; UNLINK AND REMOVE ORDERS OEC;

Here are the lines that have to be added before the line 'EXECUTE WINACTIV'; one private integer field is utilised although this could be any field in the order as long as the procedure doesn't need it in the course of its normal execution. There's no need to worry about overwriting important data as it's a linked table that is being updated. Of course, it's best to use a private field. 

:TEST_PROG = 0; SELECT EXEC INTO :TEST_PROG FROM EXEC WHERE TYPE = ‘P’ AND ENAME = 'TEST_SEPARATE' ; SELECT SQL.TMPFILE INTO :ECORDERS FROM DUMMY; LINK ORDERS OEC TO :ECORDERS; GOTO 999 WHERE :RETVAL <= 0; INSERT INTO ORDERS OEC SELECT * FROM ORDERS ORIG WHERE ORD = :$.ORD ; UPDATE ORDERS OEC SET TEST_FIELD = -1 /* this signifies that the procedure is called from an order */ WHERE ORD = :$.ORD;

At the beginning of the procedure, the following query is executed to get the order number

LINK ORDERS TO :$.PAR; ERRMSG 1 WHERE :RETVAL <= 0; SELECT ORD, ORDNAME, CUST INTO :OLDORD, :ONAME, ::CUST FROM ORDERS WHERE ORD > 0; UNLINK ORDERS;

To the SELECT list, I add the private field TEST_FIELD that is selected into the variable :FROMFORM. After this is the block

GOTO 10 WHERE :FROMFORM <> -1; :TEST_PROG = 0; SELECT EXEC INTO :TEST_PROG FROM EXEC WHERE TYPE = 'P' AND ENAME = '$'; DELETE FROM TEST_ERRMSGS WHERE USER = SQL.USER AND AKEY = :ORD AND PROG = :TEST_PROG; LABEL 10;

In other words, if there is already an entry in the table for this user, order and procedure, clear it so that we can start afresh. Now the procedure runs, and every time that it detects an error, the rather cumbersome code that appears below has to be included. If before the code was

ERRMSG 62 FROM ORDERSA WHERE ORD = :ORD AND TEST_CONDITION = 'Y';

It now becomes

:ERR = 0; SELECT 62 INTO :ERR FROM ORDERSA WHERE ORD = :OLDORD AND TEST_CONDITION = 'Y'; GOTO 862 WHERE :ERR = 0; GOSUB 9990; ERRMSG 62; LABEL 862; ... SUB 9990; GOTO 9991 WHERE :FROMFORM <> -1; SELECT ENTMESSAGE ('$', 'P', :ERR) INTO :PAR1 FROM DUMMY; INSERT INTO TEST_ERRMSGS (AKEY, USER, PROG, MESSAGE) VALUES (:OLDORD, SQL.USER, :GLOB_PROG, :PAR1); LABEL 9991; RETURN;

Going back to the form trigger, after the line 'EXECUTE WINACTIV', the following has to be added

SELECT MESSAGE INTO :PAR1 FROM TEST_ERRMSGS WHERE USER = SQL.USER AND AKEY = :$.ORD AND PROG = :TEST_PROG; ERRMSG 990 WHERE :RETVAL = 1;

In other words, without digging into the code, the calling trigger has to signify to the procedure that it is being run from a trigger (TEST_FIELD = -1). The procedure then inserts a record into a special table if an error is detected, where the record includes the order number, the user, the procedure and the error message to be displayed. When the trigger resumes execution, it checks whether there is a suitable record and if so, displays the error message. This means that the record will remain in the table but this doesn't matter much for if the user runs the same procedure on the same order, the record will be deleted at the beginning of the procedure.

Wednesday, 21 January 2026

The single character bug returns

This morning I was tasked with retrieving the location of inventory within a specific warehouse. I wrote the following code that is part of a larger procedure that creates a warehouse transfer document.

:LOC = '0'; /* default value */ SELECT WAREHOUSES.LOCNAME INTO :LOC FROM WAREHOUSES, WARHSBAL WHERE WAREHOUSES.WARHS = WARHSBAL.WARHS AND WARHSBAL.PART = :PART AND WAREHOUSES.WARHSNAME = 'Main';

When the procedure containing this code was run, there was a complaint that location R does not exist for warehouse Main. This error was doubly confusing - at first, I thought that the R might refer to the part's type, which would make no sense whatsoever. When I ran the snippet in Windbi, the result was something like R.1.2, which was when I understood what was happening.

:LOC is initially set to the single character '0', for if the retrieval fails (for example, if the part has no inventory) then at least :LOC will have a value that is guaranteed to exist. Unfortunately, as I discovered several years ago1, initialising a string with a single character confuses Priority who thinks that this is a character variable, not a string, and so saving further values into this variable will only save the first character.

The solution is to have an initialisation line at the beginning, like :LOC = '00000000' (I imagine that the length doesn't matter too much, as long as it's longer than one character. Once this line had been added, the addition to the procedure worked flawlessly. 

I notice that I wrote in the earlier blog that this issue had been fixed in version 21. I'm not sure with which version I was working today (it was for an external client), but it was web-based and probably not less than version 23.

Internal links
[1] 51

Tuesday, 20 January 2026

WARNINGYES

It's very easy to define a business rule: if such and such happens, then either: display an error message, display a warning message, send an email, send an SMS, open a task or send a push message (whatever that is). A warning message displays some text along with two buttons, "continue" or "cancel". I've never noticed until now that the default button is "continue". Thus it's very easy to consider the scenario whereby a user does something that causes a rule to fire, but the user ignores the warning message and simply presses "continue", thus basically negating the whole point of the warning.

I discovered (very belatedly) that there is a system constant, WARNINGYES, that controls which button is the default; 1, which is the predefined value, means that the "continue" button is the default, whereas 0 means that "cancel" is the default. I changed the value of the constant to 0 and now hopefully users will actually read any message displayed.

This is to a certain extent shooting myself in the foot: once every couple of days I have to create delivery notes from packing lists (don't ask), and when I paste an order number into the delivery notes screen, two or three messages are displayed. I don't read them and simply press enter to continue. Now I'm going to have to press an arrow key and then enter. The message are generally "Packing list X is connected to this order" and some text referring to the customer. Because I belong to the 'tabula' users group, I can't execute the privileges program, otherwise I would turn these warning messages off. Such are the tribulations of a system manager.

I made that change 20 minutes ago and no one has written to me about it yet. Normally a change like that gets noticed very quickly.

Wednesday, 17 December 2025

Injecting an English title for a report sent by TTS

I wrote1 several months ago about the tedious process of adding English titles to reports and forms. That kind of work has settled down, but instead I've had to face some rather strange problems. The latest example is a report that is sent by email via TTS; the data is prepared by a procedure that sends the report with the EXECUTE WINACTIV command. The problem, as one might guess, is that when it is sent based on Indian data, the report is in Hebrew.

The immediate problem about the body of the report was fixed quite easily as the form for a TTS task has a 'language' field. When this was set to English, the body of the report was indeed sent in English (and left to right) but the title of the report stayed in Hebrew, despite my having defined a translation of the report title. When the report is run from the menu by a user, this English title appears.

I thought at first that there might be an undocumented flag for WINACTIV that would force an English environment; notionally such a flag would be -e, but this character is already used to signify that the report should be sent to an email address. I tried saving the output to a file (then send it on via MAILMSG) to see whether this would make any difference, but it didn't. There is no possibility of assigning a value to SQL.LANGUAGE; in comparison SQL.ENV holds the name of the current environment that can be changed by invoking the ENV function.

I'm not sure where the inspiration for my solution came from, but it occurred to me that all I needed to do was to update the title of the report in the EXEC table. So I added the following code towards the end of the procedure, after the data has been collected but before the report is sent.
GOTO 10 WHERE SQL.COUNTRY = 'ISR'; /* India */ :MSGNUM = 10; :GROUP = 'INDIAGROUP'; GOTO 20; LABEL 10; /* Israel */ :MSGNUM = 20; :GROUP = 'ISRAELGROUP'; LABEL 20; GOSUB 900; EXECUTE WINACTIV '$', SQL.TMPFILE, 'STACK', :$.STK, '-g', :GROUP; GOTO 99 WHERE :MSGNUM = 20; :MSGNUM = 20; GOSUB 900; LABEL 99; /* End */ /***********************************************/ SUB 900; SELECT ENTMESSAGE ('$', 'P', :MSGNUM) INTO :PAR1 FROM DUMMY; UPDATE EXEC SET TITLE = :PAR1 WHERE ENAME = '$' AND TYPE = 'R'; RETURN

Message 10 holds the English title whereas message 20 holds the Hebrew title. There's no point in resetting the title after the report has been created if it has already been set to Hebrew. This hack only works because the report is being sent by a spawned process; if it were run from the menu, the report is effectively already loaded into memory when the procedure starts and so run-time changes do not take effect (unless one is using the documented methods which aren't applicable here).

Internal links
[1] 119

Wednesday, 10 December 2025

The LOADFNC external program

This program takes the lines that are in the Interim Table-Journal Entries form and transforms them into journal entries. I needed to deal with this program as the next step in the long journey that began with the recursive FILELIST1. There's one subtle pitfall with this program which I'll describe.

My procedure takes the results from the FILELIST program, connects to a given environment and loads a file found by FILEIST into the interim table. The sequence of commands is as follows

:TOFILE = STRCAT (SYSPATH ('LOAD', 0), :COMPANY, '\loadfnc.txt'); SELECT MESSAGE INTO :FN FROM STACK_ERR WHERE LINE > 0 AND STRPIECE (MESSAGE, '/', 1, 1) = :COMPANY AND INTDATA2 > 30; /* size of file - exclude dummy.txt */ :FNAME = STRCAT (:CHKDIR, '/', :FN); EXECUTE MOVEFILE :FNAME, :TOFILE; EXECUTE DBLOAD '-L', 'loadfnc.txt', '-E', 'loadfnc.err', SQL.TMPFILE;

The above is actually run in the framework of a cursor, but for simplicity, I am assuming that there is a single file. TOFILE is the name of the destination file; its name has to be the same as the name of the interface. The documentation says that such a file can either be in the system/load directory or in a subdirectory of system/load according to company/environment name. Once the file is found, FNAME restores the directory structure of the filename that was removed initially prior to the FILELIST command, and then FNAME is copied to TOFILE. Once copied, this file can then be uploaded via the DBLOAD program.

This first half of the program went well but the second half had me stumped. One can find a menu option that runs LOADFNC and there is the trigger LoadAccAE2 of the 'func' table that runs LOADFNC. There are three parameters that appear to be a message, the environment's name and a linked table of users. The last parameter is probably the easiest to create. The command in LoadAccAE2 is EXECUTE LOADFNC SQL.TMPFILE, :DNAME, :USERLNK. The first and third parameters were easy to figure out but the second parameter seemed problematic. DNAME is one of the fields in the ENVIRONMENT table so I thought that this was the name of the current environment. There was some code in the menu option LOADFNC2 which I didn't initially understand that would have helped me.

In order to figure out what the parameters to LOADFNC needed to be, I manually loaded some data into the interim table then ran the menu option. I received an error message saying that 'test' (the value that I had stored in the the interim table's 'source program' field) had not been defined in the Definition of Load Parameters form. Once I added 'test' to this table, I was able to run LOADFNC successfully. At this time, the code in the menu option became clear: DNAME is not the name of the environment, but the value stored in the 'source program' field. Not only that: this value has to be the same in all the lines being loaded. This was the pitfall of which I was not aware.

My test data had created a temporary journal entry that would have to have its status changed to 'final'. In order to find the entry, I would have to read the value of a certain field in the interim table to find this value in the journal entry, but successful loading of lines into journal entries removes the lines from the interim table. I thought at first that I would have to copy the interim table before loading then iterate over it, finding the required values, but I discovered that one of the flags in the Definition of Load Parameters form causes the newly entered journal entry to be finalised automatically. So there was no need to copy the interim table.

I have to figure out what to do if a file cannot be loaded successfully into the interim table or data cannot be turned into journal entries. Supposedly DBLOAD can create a file with data that did not get loaded, but if this program is running in a loop, that error file is going to be overwritten. Similarly, if LOADFNC can't turn data into a journal entry, the data will be left in the interim table, but this again will be overwritten with the data from the next file. I could attach the contents of the table to an email, but the recipient would have to enter the data manually into the interim table.

Internal links
[1] 122

Tuesday, 25 November 2025

More about the FILELIST program

The normal way of executing this program is as follows (STACK_ERR because I'm using the -R flag)

SELECT SQL.TMPFILE INTO :ST6 FROM DUMMY; :CHKDIR = SYSPATH ('LOAD', 0); EXECUTE FILELIST :CHKDIR, :ST6, '-R', '-d', SQL.TMPFILE; LINK STACK_ERR TO :ST6; SELECT * FROM STACK_ERR FORMAT; /* this is in Windbi */

If one omits the final parameter (SQL.TMPFILE), then the following is displayed

the option '-msg_file' is required but missing Positional arguments: --dirname arg directory name --linked_stack_file arg link to STACK6 or STACK_ERR for results --msg_file arg text file for return messages General arguments: --u names lowercased (result in STACK6 by default) --l names uppercased (result in STACK6 by default) --f forces resuts to be written to STACK_ERR --R performes subdirectories scan (result in STACK_ERR) --d put directory names in result --A

The option '-msg_file' presumably refers to SQL.TMPFILE that I omitted from the procedure call. I have no idea where else this technique of omitting 'the text file for return messages' can be used in order to get a program's help. COPYFILE and MOVEFILE don't have such a parameter, although they do allow an optional STACK parameter that can be checked for program success or failure.

Thursday, 20 November 2025

Continuing the recursive FILELIST procedure

Four months ago, I wrote1 about the recursive FILELIST and the fact that two essential flags were not documented. Over the past few days, I've been working with the results of that command; I had to copy files to a location in system/mail, then attach them to financial journal entries, then delete the files. Nothing worked ... or rather, everything that was not connected to copying and deleting the files worked properly, but the files were not being accessed.

Eventually there was no option left but to call for help from Priority Software, so today we had a grand debugging session. The programmer from PS at first was at a loss as to why the commands weren't workng. The reason only became clear when he used a program ('filezila' ?) to access the system/sync directory from the external side; it turns out that the company that was placing files in this directory had created a subdirectory system/sync in the sync directory, thus explaining why we couldn't access any of the files. 

To make this clear: the output from EXECUTE FILELIST looked like this

system/sync/dataplus/a191016/ system/sync/dataplus/a191016/dummy.txt system/sync/dataplus/a191016/5031266_25034.pdf ...

I had assumed that the program was showing the complete path, but in reality the filename with path was ../../system/sync/system/sync/dataplus/a191016/5031266_25034.pdf. Once this had been taken into account on the internal side, my code worked perfectly.

Here are a few things that I picked up during the work session. At one stage, the command EXECUTE FILELIST was executed without any parameters (I think); this caused the program to show its help - the various flags that can be passed. I discovered that the mysterious -d parameter means "put directory names in result". Secondly, there are two integer fields in STACK_ERR (the table that holds the directory results) that can be useful: INTDATA1 holds the creation date of each file, and INTDATA2 holds the file size. I didn't need to use the creation date but this might have been useful. INTDATA2 was definitely useful as I could exclude accessing 'files' that had a size less than 30 bytes - the first two lines in the example that I quoted above have such file sizes. Using this field makes the cursor simpler. Finally, one doesn't have to prefix the file path with ../../system/sync: it's cleaner to do this with SYSPATH ('SYNC', 0) - actually I had done this at the beginning of the procedure in order to get the files.

So part of the final code is as follows

SELECT SQL.TMPFILE INTO :ST6 FROM DUMMY; :CHKDIR = SYSPATH ('SYNC', 0); EXECUTE FILELIST :CHKDIR, :ST6, '-R', '-d', SQL.TMPFILE; LINK STACK_ERR TO :ST6; /* First get the directories */ DECLARE C1 CURSOR FOR SELECT DISTINCT STRPIECE (MESSAGE, '/', 4, 1) FROM STACK_ERR WHERE LINE > 0 AND STRPIECE (MESSAGE, '/', 4, 1) <> '' AND EXISTS (SELECT 1 FROM ENVIRONMENT WHERE DNAME = STRPIECE (MESSAGE, '/', 4, 1)); OPEN C1; GOTO 300 WHERE :RETVAL <= 0; LABEL 100; FETCH C1 INTO :COMPANY; GOTO 200 WHERE :RETVAL <= 0; ENV :COMPANY; /* switch to current company */ LINK GENERALLOAD TO :$.GEN; :LINE = 0; /* Get files */ DECLARE C2 CURSOR FOR SELECT MESSAGE, STRPIECE (MESSAGE, '/', 5, 1) FROM STACK_ERR WHERE LINE > 0 AND STRPIECE (MESSAGE, '/', 4, 1) = :COMPANY AND INTDATA2 > 30; /* size of file - exclude dummy.txt */ OPEN C2; LOOP 100 WHERE :RETVAL <= 0; LABEL 110; FETCH C2 INTO :BIGFNAME, :GNAME; GOTO 150 WHERE :RETVAL <= 0; ... /* copy file */ :HNAME = STRCAT (SYSPATH ('SYNC', 1), :BIGFNAME); SELECT NEWATTACH (:GNAME) INTO :FOUT FROM DUMMY; EXECUTE MOVEFILE :HNAME, :FOUT; ...

Apparently no one uses SFTP in order to place files from an external source into the system/sync directory, so in a sense, I and the implementor who hired my services, are pioneers with this file handling code.

Friday, 17 October 2025

Running COSTING automatically - continued

Today was the first day that COSTING ran automatically using my procedure to execute it, and it did so perfectly. After it finished, I thought of a new wrinkle. I had originally added my procedure to the TTS and marked it inactive; after running the accumulator last night, I marked it as active. It was my intention to mark it as inactive again until the next accumulator.

Then I thought: why not check whether the COSTFLAG is set for the most recent accumulator? If it is set, then don't run COSTING. As this external program is in a separate procedural step, I could issue an ERRMSG call that would halt the procedure, but I thought it better to set a flag that sets a :$.GO variable - and a GOTO step would skip over the COSTING stage if necessary. 

So now the procedure is in the TTS and will run every Friday, but most weeks it will do nothing.

In stage 10, SQLI: /* My stuff */ :CURDATE = 01/01/88; SELECT MAX (CURDATE) INTO :CURDATE FROM ACCDATES; :FLAG = '\0'; SELECT COSTFLAG INTO :FLAG FROM ACCDATES WHERE CURDATE = :CURDATE; GOTO 99 WHERE :FLAG = 'Y'; LINK ACCDATES TO :$.DAT; ... LABEL 99; :$.GO = (:FLAG = 'Y' ? 50 : 30); /* End of stage 10 */
Stage/parameterName/type
10SQLI
DATLINE
GOINT
20GOTO
GOINT
30COSTING
ARGINT
MSGASCII
DATLINE
40SQLI
50END

Remember that ARG must be 1. Stage 40 sends me an email

Sunday, 28 September 2025

Running COSTING automatically

From a financial/inventory point of view, the external program COSTING is one of the most important programs under the Priority umbrella. For those that don't know, this program calculates the value of every part stored in a given 'accumulator', that in itself stores the inventory of each part in every warehouse at a given date (the program can also calculate for previous accumulators). Unlike the daily costing program, this program saves its data forever and so is extremely useful when wanting to know the value of inventory at a given date (normally the last day of a month). 

There is a parameter (ARG) passed to COSTING that determines whether it will be a daily run (deleting previous values) or a monthly run (saving values). In the first case, ARG will equal 4, whereas in the second case, ARG will equal 1.

Until not so very long ago, one could schedule COSTING and it would run automatically, using the most current accumulator. Unfortunately, this behaviour changed a few versions ago, when a scheduled run would fail because it didn't have a date for the accumulator. As this program runs for 14-16 hours on my server, this meant that I would have to get up at 4:15 on a Friday morning, stumble to the computer, connect to the server and start the program.

Eventually I got fed up with this and tried to determine what would be needed to run COSTING automatically on the last accumulator only. Basically a two step procedure is required, but I added a third step to send me email that the procedure has completed. This code does indeed run COSTING automatically, creating a monthly costing.

Stage 10 - SQLI /* TEST_COSTINGACC - No'am, 25/09/25 A cut-down version of COSTINGACC that calculates for the last tzovar only - automatically. Parameters: DAT, type LINE */ :LASTDATE = 0; #include func/FashionCosting SELECT CURDATE INTO :LASTDATE FROM ACCDATES ORIG WHERE COSTFLAG <> 'Y' AND NOT EXISTS (SELECT 'X' FROM ACCDATES ORIG2 WHERE ORIG2.COSTFLAG = 'Y' AND ORIG2.CURDATE > ORIG.CURDATE) ORDER BY CURDATE DESC; /* My stuff */ :CURDATE = 01/01/88; SELECT MAX (CURDATE) INTO :CURDATE FROM ACCDATES; LINK ACCDATES TO :$.DAT; GENMSG 1001 WHERE :RETVAL <= 0; INSERT INTO ACCDATES SELECT * FROM ACCDATES ORIG WHERE CURDATE = :CURDATE; UNLINK ACCDATES; /* Input stage */ SELECT VALUE INTO :STARTDATE FROM LASTS WHERE NAME='COSTSTARTDATE'; :CURDATE = :FIRSTDATE = 0; :COUNT = 0; LINK ACCDATES TO :$.DAT; SELECT MIN(CURDATE), COUNT(*) INTO :CURDATE, :COUNT FROM ACCDATES WHERE CURDATE > 0; UNLINK ACCDATES; SELECT CURDATE INTO :FIRSTDATE FROM ACCDATES WHERE CURDATE > 0 AND COSTFLAG = 'Y' ORDER BY CURDATE; SELECT DTOA(0+:STARTDATE, 'XX/XX/XX') INTO :PAR1 FROM DUMMY; ERRMSG 1 WHERE :FIRSTDATE = 0 AND 0+:STARTDATE <> :CURDATE AND :STARTDATE <> 0; :ENAME = 'COSTINGACC'; :DATE = 0 + :CURDATE; #include func/CostingLog ----------------------------------------------------------------- STAGE 20: COSTING Parameters: ARG, type INT, value 1 MSG, type ASCII DAT, type LINE /* The parameters have to be in this order */ ----------------------------------------------------------------- STAGE 30: SQLI #include func/CostingMpart :PAR1 = '$'; MAILMSG 40 TO EMAIL 'tabula@gmail.com';

Thursday, 25 September 2025

Saving a part's picture by means of an interface

The scenario: a part is copied from company A to company B, along with various fields, including the path to the part's picture. The picture has a special status in the table PARTEXTFILE: the value of the field EXTFILENUM will be -1.

In the procedure that has been running for the past few years, the path to the picture has been obtained correctly, but was being stored as a regular entry in company B. The person responsible for company B had never brought this subject to my attention until now. When I saw that the picture was being saved as a regular entry, I tried to overcome this by passing the value -1 in the appropriate tuple in GENERALLOAD. This did not work.

My next attempt was a typical hack: find the value of EXTFILENUM for the part in company B and update it to -1. This worked, but obviously was not the correct way of solving the problem. Fortunately sanity was restored when I asked myself how regular saving of the picture works in the form LOGPART. This is when I discovered that the path is stored in the form variable EXTFILENAME, and that the trigger BUF11 causes this value to be stored as a picture in PARTEXTFILE (below is the standard code).

/* Insert, Update and Delete EXTFILENAME */ GOTO 111 WHERE :$.EXTFILENAME = :$1.EXTFILENAME ; /* clean previous record */ DELETE FROM PARTEXTFILE WHERE PART = :$.PART AND EXTFILENUM = -1 ; GOTO 112 WHERE :$1.EXTFILENAME = ''; EXECUTE DELATTACH '-I', :$1.EXTFILENAME; LABEL 112 ; /* insert new attachment */ GOTO 111 WHERE :$.EXTFILENAME = ''; INSERT INTO PARTEXTFILE (PART, EXTFILENUM, EXTFILENAME) VALUES (:$.PART, -1, :$.EXTFILENAME) ; LABEL 111;

Note the use of the procedure DELATTACH: this can cause the actual file to be deleted if it is not referenced anywhere. Saving the file parth in PARTEXTFILE is done by an insertion, thus enabling the value -1 to be set; doing this via an interface doesn't work.

Now I save the file path as the equivalent to :$.EXTFILENAME in the same tuple as the part name before passing it to the interface. And this works.

Wednesday, 6 August 2025

Some Priority internals (SCREENMSG)

I had developed a form for a client and I wanted to document it. There are reports that purport to do this but they don't always give all the information, and my improved report doesn't exist on the client's computer. So I used the WINDBI Dump capability; one can dump a form and one can dump a form's cursor; the difference between the two is that the latter only gives the SQL query that populates the form whereas the former also gives triggers.

Looking at some of this information, my eye was caught by this statement:
SELECT 192 INTO :SCREENMSG
FROM DUMMY WHERE :RETVAL = 0;

I wondered what this could mean. Could it be that :SCREENMSG is a special variable in the same way that :ERRMSG is? Storing a number in :ERRMSG will cause the corresponding error message (from the procedure) to be displayed. So presumably the above statement will cause message 192 to be displayed on the screen. But where is 192 coming from? There were similar statements but with different numbers.

Maybe, I thought, there is a table called SCREENMSG or SCREENMSGS from which the value is automagically retrieved. And so it is: the table is SCREENMSG and value 192 is (translated into English), 'Value does not exist in the table'. I looked at all the values in the table and have to admit that I could not imagine any personal use for them.

At least another internal has been explained.


Monday, 4 August 2025

Strange report bug 2

Several years ago, I wrote1 about a bug in a report: although there was a join to a specific table (STACK2), the report behaved as if this join was non-existent. When I dumped the report in WINDBI, indeed the join to STACK2 had disappeared. The fix was to add to the report another field from STACK2 (there are only two fields in this table; STACK2.ELEMENT was used to join with something, so obviously STACK2.TYPE had to be added).

Over the past few days, I've been helping another programmer write a somewhat complicated report that is supposed to show open invoices and payments for suppliers at a given date, where the prices have been converted to dollars. One of the complications of this report is that it has to use two instances of CURREGITEMS, one fixed to have its currency dollars (-2) and its date set to the parametric conversion date. The other instance is joined to the customer's currency: this allows for the conversion of a currency other than NIS or $ to be converted.

Although the join ACCOUNTS.CURRENCY = CURREGITEMS1.CURRENCY appears in the report, the report behaves as if this join does not exist when the report is run. Again, dumping the report shows that this join does not exist. I partially solved the problem by having only ACCOUNTS.CURRENCY appear as a report field, then adding = CURREGITEMS1.CURRENCY as its expression (i.e. in a son form of the report field). 

Why do I write 'partially'? Because when ACCOUNTS.CURRENCY is -1, i.e. NIS, there is no corresponding record in CURREGITEMS. Indeed, originally the join was a left join to solve the shekel problem. But how could I fake a left join when I'm using CURREGITEMS as an expression? My off-the-cuff solution was to use the following expression
= (ACCOUNTS.CURRENCY = -1 ?
ACCOUNTS.CURRENCY : CURREGITEMS.CURRENCY1)
but I'm not totally convinced that this is the correct solution. Indeed, the programmer that I was helping informed me that it worked, but that she had to add the 'distinct' flag to the report.

The original problem (see first paragraph) was solved by adding another field of STACK2. Here, other fields of CURREGITEMS1 are being used, but they're not being displayed. There is a field in CURREGITEMS called RREXCHANGE that appears to be zero all the time, so this field could be displayed but have the 'don't display if zero' option set.

Internal links
[1] 37

Thursday, 17 July 2025

Determining if the ouput of an HTML document is a printer, the screen or email

Due to legal requirements, delivery notes and invoices have a field 'Printed' that determines when the document is printed whether the word 'original' or 'copy' should appear in the output. Displaying the document on the screen always results in 'copy'. A client wanted a similar mechanism but for customer orders - they want to know if  the order has been printed at any stage.

In an HTML document, the first three stages are usually INPUT, HTMLCURSOR and SQLI. In that third stage, the code for determing which text should be added ('original'/'copy') can be seen, but there is nothing in the document that says whether the document is being printed or displayed. Looking though the SDK, there's nothing explicit on this topic, although there is a hint.

Saving a Certified Copy when Printing a Document

You can define a procedure so that, when the document is printed or sent by e-mail (:SENDOPTION = 'PRINT' or 'AMAIL'), a certified copy is saved that can be printed later (using the Create Certified Copy program).

In order to save a copy:

  • Include an HTMLEXTFILES step which goes over the attached files.
  • In the first INPUT step, set the :HTMLPRINTORIG variable to 1.
  • In the SQLI step after the HTMLCURSOR step, set the :SAVECOPY variable to 1.

These variables are updated during runtime, so that if several documents are being printed together, the variables will be set for each individual document, depending on its status.

So it would seem that the undocumented system variable :SENDOPTION is the key to the client's request. I added a little code to the SQLI stage of the procedure WWWSHOWORDER that would save the value of :SENDOPTION in a table of private constants. From this I was able to ascertain that when printing, the value is 'PRINT' and when displaying on the screen, it's 'DISPLAY'. Presumably when sending email the value is 'AMAIL' and for fax (??) it's probably 'FAX', although I don't need to know this.

For my purposes (or for the client), I have to update the private field 'PRINTED' in the ORDERS table if SENDOPTION is not equal to 'DISPLAY'. I think it better to save a username and date/time than just the fact that the order has been printed, but that's only my opinion.

I can't imagine any further use for this undocumented variable, but I vaguely recall that once I would have liked to have known its value, although for what, I do not remember.

Tuesday, 1 July 2025

Recursive FILELIST

The SDK documents under heading Browsing the Contents of a Folder (page 233 in my copy of the V23 document) how to obtain a list of files in a folder. 

:DIR = '../../tmpDir'; SELECT SQL.TMPFILE INTO :ST6 FROM DUMMY; SELECT SQL.TMPFILE INTO :MSG FROM DUMMY; EXECUTE FILELIST :DIR,:ST6,:MSG; /* In the linked file of the STACK6 table, you will find all files and folders under the input directory :DIR. */ LINK STACK6 TO :ST6; GOTO 99 WHERE :RETVAL <= 0; DECLARE NEWFILES CURSOR FOR SELECT TOLOWER(NAME) FROM STACK6 WHERE TOLOWER(NAME) LIKE ' loadorder*'; OPEN NEWFILES; ...

I've written code based on this to poll a specific directory; if a file is found, then the procedure performs some action based on this file (e.g. reads the file and uses it as input for an interface). At the end, the procedure deletes the file so that it won't be found again.

This is all well and good when Priority is hosted on a company server, but is problematic when the web interface is used and Priority is hosted 'in the cloud'. The new online SDK discusses this scenario and states that The system/sync folder is a special folder available in Priority installations on the public cloud. It provides a location, accessible by SFTP, where users can upload files from an external source. The folder behaves a bit differently than regular Priority folders. I am working with a client for whom some external company is uploading files to a subfolder of system/sync; the client wants that these files (or rather, a reference to these files) be stored as an attachment to a financial journal entry. I tried using the 'standard' FILELIST code as written above but this was not bringing me any joy.

After approaching Priority Software, an undocumented feature of FILELIST was revealed: in order to traverse system/sync, one has to perform a recursive search by means of the flag '-R'. There is also another flag whose meaning escapes me at the moment, '-d'. Most importantly, instead of linking STACK6 to the results, one links STACK_ERR. It's not clear to me why this change is required: STACK6 has the fields NUM (equivalent to STACK_ERR.LINE), TYPE (equivalent to CHARDATA) and NAME (equivalent to MESSAGE), where the length of NAME is 100 characters. But the proof is in the pudding, and a recursive search does not work with STACK6.

Here is the required code when using system/sync as written for WINDBI as a test:

SELECT SQL.TMPFILE INTO :ST6 FROM DUMMY; :CHKDIR = SYSPATH ('SYNC', 0); EXECUTE FILELIST :CHKDIR, :ST6, '-R', '-d', SQL.TMPFILE; LINK STACK_ERR TO :ST6; SELECT LINE, CHARDATA, MESSAGE FROM STACK_ERR FORMAT; UNLINK STACK_ERR;

This code does provide the names of files in the target directory.

While writing these words, I've had a problematic insight: if a procedure traverses the given folder and find files, it will attach them to journal entries. The next time the procedure runs, the same files will be found - it seems possible that one can attach the same file twice to some entity! The keys of the EXTFILE table are IV, TYPE and EXTFILENUM, and a quick test shows that indeed one can attach the same file more than once to the same entity! Obviously I will have to implement some form of check to prevent this; as opposed to the polling code that deletes the file after handling it, here the files have to remain 'forever'. No date information seems to be passed so one can't use this as a filter.

Wednesday, 18 June 2025

Converting minutes into days:hours:minutes

One of my clients wanted a report that was based on order statuses (as we know now, TODOLIST), displaying the time spent in each status in the format DD:mm:hh. The field TODOLIST.DURATION holds this time in minutes and the task is to convert it.

One might ask, what is the problem with using DTOA to convert the number? It seems that if one does not display a month, then the number of days in the current month is used. For 51936, a number that I had, the expression DTOA (51936, 'DD:hh:mm') gives the result 06:01:36, because there aren't more than 30 days in the current month (the expression is equivalent to 36 days and some change).

The form DOCTODOLISTLOG purports to give the number of days and number of hours in separate columns but this isn't what the client wants and it's also not exact (a fraction of hours is given, not the number of the minutes). The number of days is defined as (0.0 + TODOLIST.DURATION)/ 1440.0. I don't understand why the number is promoted to a real, as TODOLIST.DURATION / 1440 gives the same answer (36). But I'm not taking a chance, so I will use the same expression. As I want an integral number of days, I need to use ITOA, but this won't work on a real, so first I have to round the number, giving the expression (ITOA (ROUND((0.0 + TODOLIST.DURATION)/ 1440.0), 2). The final 2 is to ensure that the result will have at least two digits, where leading zeroes are added as necessary. If I had kept the number as a real and used RTOA, then I could have chopped off any remainder but there would be no leading zeroes.

What about the hours and minutes? The way that I did this in a report was first to define a value that is not displayed: TODOLIST.DURATION MOD 1440. This is done in order to simplify the remaining calculations. Using 51936 as input, this expression results in 96; clearly this is equivalent to 1 hour and 36 minutes, or #207 / 60 and #207 mod 60, where #207 is the number of the expression.

Thus the final string is given by the following formula
STRCAT (ITOA (ROUND((0.0 + TODOLIST.DURATION)/ 1440.0), 2), ':',
ITOA (#207 / 60, 2), ':', ITOA (#207 MOD 60, 2))

If TODOLIST.DURATION is 51936, then this monstrosity results in the string 36:01:36  (yes, the client wanted that if the duration were over 12 days then it would appear in red - but that's another story).

Monday, 16 June 2025

Showing 'info only' parts in BOMs

I spent an hour banging my head against a wall today, trying to find the correct invocation to SONRAW that would result in showing all sons, including those that are marked as 'info only'. The basic form of the command is

EXECUTE SONRAW :$.PRT, SQL.DATE8, :$.ARC, :$.OPT, :$.REV
where: :$.PRT is a linked file of parts that one wants to 'explode'
SQL.DATE8 is of course, today - this may have some function with trees that have revisions
:$.ARC is the result, a linked file of tuples of PARTARC
:$.OPT is the desired option, the raison d'etre of this blog
:$.REV is apparently something to do with revisions ... or not.

If one dissects the various values of :$.OPT as in the PARTTREE procedure, 0 = regular, 2 = including phantoms, 4 = including info only parts. These values can be combined as they form a bit mask. The value of :$.REV had me scratching my head: if :$.OPT is 0 then :$.REV can be empty and one does get some form of tree, but it seems best that :$.REV always be set to '-N'. I wasted a great deal of time trying to find the right invocation to include info-only sons where :$.REV was empty.
  • So the invocation for a 'normal' BOM would be
    EXECUTE SONRAW :$.PRT, SQL.DATE8, :$.ARC, 0, '-N'
  • For a 'normal' BOM with phantoms would be
    EXECUTE SONRAW :$.PRT, SQL.DATE8, :$.ARC, 2, '-N'
  • And for a 'normal' BOM with info-only sons would be
    EXECUTE SONRAW :$.PRT, SQL.DATE8, :$.ARC, 4, '-N'
Cut this out and store it somewhere.

Sunday, 8 June 2025

Converting procedures and reports to English

I am in the middle of converting many procedures and reports to English for a client who is starting operations in India. This process is fairly straight-forward and boring, but there are some problems that have to be overcome.

The process can be divided into two: translating labels (of the procedure/report itself, procedural parameters, field titles in reports) and handling fields that have an English value as well as a Hebrew value. Translating labels is straight-forward; the only point worth noting is that as labels are (generally) limited to 20 characters, one should enter the translated label for language 3 (American English) first then copy the label to language 2 (British English). It seems that the check for length occurs only for language 3.

One place that requires slightly special handling is reports that have a different title to the given title. Normally, the translated title of the report goes in the 'translation of entity title' sub-form, but a specific title that can be longer goes in the 'output title' sub-form. The first would be displayed in a menu whereas the second is only for output. This sub-form has its own sub-form, 'Translation of Output Title'.

Handling fields with a separate English value is more involved. There is only one table, DOCSTATUSES, that has both STATDES and ESTATDES fields, and choosing which to display is very simple:

(SQL.LANGUAGE = 1 ? DOCSTATUSES.STATDES : DOCSTATUSES.ESTATDES)

But most other tables have the English value in a separate table, eg. CUSTOMERS.CUSTDES and CUSTOMERSA.ECUSTDES. This is slightly more involved, as one has to write both the conditional statement as well as adding a left join between CUSTOMERS and CUSTOMERSA.

The real problems start when this field is a parameter: the client has a propensity for including the part status in reports, where it is frequently a parameter. In this case, one has to add a CHOOSE-FIELD trigger for the field that in itself is very interesting as it displays how Priority manages a UNION ALL.

SELECT PARTSTATS.STATDES, '' FROM PARTSTATS WHERE PARTSTAT <> 0 AND INACTIVEFLAG <> 'Y' AND SQL.LANGUAGE = 1; SELECT /* AND STOP */ DOCSTATUSES.ESTATDES, '' FROM DOCSTATUSES WHERE DOCSTATUS <> 0 AND TYPE = '4' AND SQL.LANGUAGE > 1;

The (currently) insolvable problem is with part status being a parameter to an INPUT or SQLI stage. One can give the parameter an English title and define the above CHOOSE-FIELD trigger, but into which table is the value stored, PARTSTATS or DOCSTATUSES? One, somewhat clumsy, solution is to use different stages for the different languages, viz.

[STAGE 10] :$.GO = (SQL.LANGUAGE = 1 ? 30 : 50; [STAGE 20]: GOTO; {$.GO} [STAGE 30]; /* Hebrew */ LINK PARTSTATS TO :$.STA; /* parameter */ .... [STAGE 40] GOTO; {60} [STAGE 50] /* English */ LINK DOCSTATUSES TO :$.STD; /* parameter */ ... [STAGE 60] Report

For a moment, I thought that I could write a CHOOSE-FIELD trigger similar to the above for the procedural parameter that would choose either PARTSTATS.PARTSTAT or DOCSTATUSES.DOCSTAT, but there are two problems with this: (a) the parameter has to a character value, not numerical (and using ITOA doesn't solve the problem; (b) the form that Priority uses from which a value will be chosen is dependent on the table linked to the parameter that is in this case PARTSTATS. So it looks like I'm stuck with the GOTO solution. 

One possible improvement to this would be in stage 50 - instead of duplicating whatever is in stage 30 but using DOCSTATUSES, the appropriate values could be entered into a linked instance of PARTSTATS. Similarly stage 30 only gets values for PARTSTATS. Then there need be only one 'real' SQLI stage, 60, that does whatever is necessary for the report.

[Update] The GOTO solution won't work either. Stage 50 has the linked file DOCSTATUSES, and so the form that will be displayed or from which values will be extracted is also DOCSTATUSES. Any attempt to run this form results in the error message 'This form is reserved for internal use by the system'. To get around this problem, I defined a new form that is based on DOCSTATUSES, shows only ESTATDES and SORT and is defined as Q. In the 'continuation' sub-form of the procedural parameter, I defined the target form to be my private form.

This works! But it's still clumsy.

Monday, 19 May 2025

Program logging

I was asked this morning how often user X had run procedure Y in the past few months. I explained that Priority saves (to the best of my knowledge) such data in the form of saved reports for a few days and then I delete those reports. Of course, I had deleted the reports this morning so I couldn't give any data.

What I could do was add logging procedure usage to Priority. First I defined a table TEST_PROGUSERS:

FieldTypeWidthTitle
PROGINT13> Procedure
USERINT13> User
CURDATEDATE14Date
DETAILSRCHAR48Details

Then I built a read-only form based on this table; this form is a son form of EPROG, the procedure generator. In the procedures who usage I want to track I added the following statement

INSERT INTO TEST_PROGUSERS (PROG, USER, CURDATE, DETAILS) SELECT EXEC.EXEC, SQL.USER, SQL.DATE, STRCAT (DTOA (:$.FDT, 'DD/MM/YY'), ',', DTOA (:$.TDT, 'DD/MM/YY')) FROM EXEC WHERE EXEC.ENAME = '$' AND EXEC.TYPE = 'P';

The 'details' field is supposed to contain the values of the parameters passed to the procedure. 

Now I can see at a glance who has run a given procedure and when, without having to rely on saved reports (which even if they exist are inaccurate as someone could send the output to Excel, thus preventing the report instance being saved).