How do I require a field to be mandatory on a NAV page? - dynamics-nav

It seems that the underlying nature of NAV is to resist requiring the populating of a field to be mandatory. In the case of our business logic, certain fields must be populated in order for the data to be valid. For example, a customer record must have at least a name and phone number. I've searched a number of places but have not found a suitable solution. So how can this be accomplished?

After struggling to find a succinct way to require certain fields on a card to be populated, I have come up with the following and it (so far) is working for me. I started to sense that NAV was not meant to have mandatory fields, but I need them for our business logic. Anyways, here we go...
Step One:
- We have a codeunit for various validation logic, in which I've added the function to read a custom table listing the tables and their fields that are mandatory. This function takes the table number, key field, and a "create mode". It returns a text "completion status" value. I find the table for the record I am validating. I loop through the mandatory fields, if the field is not populated, I add it to a list of incomplete fields. If the list of incomplete fields is empty, the completion status is "done". If the list of incomplete fields is populated, a message is displayed indicating the missing fields and allows the user an option to cancel the create of a new record or to stay on the (new or existing) record and enter the missing data, and the completion status is set to "delete" to cancel a create, or "return" to stay on the record. Logic follows:
CheckMadatoryFields(TableNumber : Integer;KeyField : Code[10];CreateMode : Boolean) Completion Status : Text[30]
// Read the 'LockoutFields' table to find the manditory fields for the table passed in.
LockoutFields.RESET;
LockoutFields.SETRANGE("Table No.", TableNumber);
LockoutFields.SETFILTER("Filter ID", 'MANDITORY_FIELD');
// Get a record reference for the table passed in
RecRef.OPEN(TableNumber);
RecRef.SETVIEW('WHERE("No." = FILTER(' + KeyField + '))');
// Set this to done, i.e. data is complete (don't delete by mistake).
CompletionStatus := 'done';
IF RecRef.FINDFIRST THEN BEGIN
// Check the record's manditory field(s) listed in the 'LockoutFields' table to see if they're blank.
IF LockoutFields.FINDSET THEN BEGIN
REPEAT
FldRef := RecRef.FIELD(LockoutFields."Field No.");
IF FORMAT(FldRef.VALUE) = '' THEN
FldList := FldList + ' - ' + FldRef.CAPTION + '\';
UNTIL LockoutFields.NEXT = 0;
END;
IF FldList <> '' THEN BEGIN
// If creating the record, add the 'Cancel Create' message, otherwise don't.
IF CreateMode THEN
DeleteRecord := CONFIRM(Text_ManditoryField + '\' + FldList + '\' + Text_CancelCreate, FALSE)
ELSE BEGIN
DeleteRecord := FALSE;
MESSAGE(Text_ManditoryField + '\' + FldList, FALSE);
END;
// Return a 'delete' status when deleting, or a 'return' status to stay on the record.
IF DeleteRecord THEN
CompletionStatus := 'delete'
ELSE
CompletionStatus := 'return';
END;
END;
RecRef.CLOSE;`
Step 2:
- On the card you want to check for mandatory fields, in my case the Customer card, I added a function to call the validation function in the codeunit described above. I also added logic to the
OnQueryClosePage trigger to call my local function. This all worked fine, as the user could not close the Customer card without completing the mandatory fields or cancelling the create of the customer, except if the user were to use Ctrl+PgUp or Ctrl+PgDn, which took them off the record. The trick was putting the the correct logic in the OnNextRecord trigger so that the validation was executed and the Ctrl+PgUp or Ctrl+PgDn still functioned (note: I found this bit somewhere on mibuso, many thanks!). Logic follows:
OnNextRecord(...)
IF CheckManditoryFields = TRUE THEN BEGIN
Customer := Rec;
CurrentSteps := Customer.NEXT(Steps);
IF CurrentSteps <> 0 THEN
Rec := Customer;
EXIT(CurrentSteps);
END;
OnQueryClosePage(...)
EXIT(CheckManditoryFields);
CheckMandatoryFields() ExitValue : Boolean
// Check for manditory fields on this table. If there are missing manditory
// fields, the user can cancel this create, in which case, a 'TRUE' value is
// returned and we'll delete this record.
ExitValue := TRUE;
IF Rec."No." <> '' THEN BEGIN // This is blank if user quits immediately after page starts.
CompletionStatus := HHValidation.CheckManditoryFields(18,Rec."No.",CreateMode);
IF (CompletionStatus = 'delete')
AND (CreateMode = TRUE) THEN // User cancelled create (not edit), delete the record and exit.
Rec.DELETE(TRUE)
ELSE
IF CompletionStatus = 'done' THEN // User completed manditory fields, OK to exit.
ExitValue := TRUE
ELSE
ExitValue := FALSE; //User did not complete manditory fields and wants to return and add them.
END;
I think that's about it. The details of the custom table are really up to how you want to code it. The validation code unit can be what you want it to be. Using a table allows the mandatory fields to added or removed without changing any logic, and this "generic" validation logic could be put on any page. The key is the two triggers on the card and having a common validation routine to call.

I used this code on a form (classic client) but there is one error in the OnNextRecord code.
OnNextRecord(...)
IF CheckManditoryFields = TRUE THEN BEGIN
Customer := Rec;
CurrentSteps := Customer.NEXT(Steps);
IF CurrentSteps <> 0 THEN
Rec := Customer;
EXIT(CurrentSteps);
END;
You should also cover the situation where CheckMandatoryFields returns FALSE. Otherwise when I request the next record it shows me an empty record.
It should be like this:
OnNextRecord(...)
IF CheckManditoryFields = TRUE THEN BEGIN
Customer := Rec;
CurrentSteps := Customer.NEXT(Steps);
IF CurrentSteps <> 0 THEN
Rec := Customer;
EXIT(CurrentSteps);
END ELSE
EXIT(Steps);

There is another error in the OnNextRecord function:
OnNextRecord(...)
IF CheckManditoryFields = TRUE THEN BEGIN
Customer := Rec;
CurrentSteps := Customer.NEXT(Steps);
IF CurrentSteps 0 THEN
Rec := Customer;
EXIT(CurrentSteps);
END ELSE
EXIT(Steps);
The filters set on the sourcetable of the page (or form) are not copied to the record on which the steps are taken. So you can navigate to a record that is not in your filterset.
Instead assigning the Rec to the Customer, you should copy it:
OnNextRecord(...)
IF CheckManditoryFields THEN BEGIN
Customer.COPY(Rec);
CurrentSteps := Customer.NEXT(Steps);
IF CurrentSteps 0 THEN
Rec := Customer;
EXIT(CurrentSteps);
END ELSE
EXIT(Steps);

Try using this code from the page trigger
OnQueryClosePage(CloseAction : Action None) : Boolean
Var
Begin
Rec.TESTFIELD(FieldName);
End;

Related

Getting database field value not working

I am using the ado connection a adoquery and dsr and adotable
I have a database with bookingnumbers as a field in the table client.
I would like to get the last bookingnumber in the field and store it in a variable.
The booking number is saved as text on access.
So far I have:
Var
sNum : string;
....
sNum := Datamodule1.tblClient['BookingNumber'].Last;
but it is not working.
please help?
It is not a good idea to try and find the maximum value of a field by navigating the dataset, especially if the dataset isn't necessarily ordered by the field in question. Try something like this instead:
function TForm1.GetMaxBookingNumber : Integer;
var
Q : TAdoQuery;
begin
Q := TAdoQuery.Create(Nil);
Q.Connection := DataModule1.AdoConnection1; // or whatever the name of your connection is
try
Q.SQL.Text := 'SELECT MAX(BookingNumber) FROM CLIENT';
Q.Open;
// the `not IsNull` in the following allows for the table being empty
if not Q.Fields[0].IsNull then
Result := Q.Fields[0].AsInteger
else
Result := -1;
finally
Q.Free;
end;
end;
So here is what i wanted :
var
sNum : string;
procedure button click
begin
with Datamodule1 do
begin
qryClient.SQL.Add('SELECT BookingNumber FROM Client');
qryClient.Open;
qryClient.last;
sNum := qryClient['BookingNumber'];
end;
end;

What code is needed to conditionally provide a value to a field in a beforeinsert trigger?

I need to store a string value in a field in a table, specifically in its Subcategory VarChar(50) column.
The value of Subcategory prior to this post processing is either 0 or 1; I need to change that to a more human-friendly value.
I haven't created a database trigger in decades and need some help with the code. This is my pseudo SQL (a hodgepodge of SQL and VB):
CREATE OR REPLACE TRIGGER tr_CustomerCategoryLog_BeforeInsert
BEFORE INSERT ON CustomerCategoryLog FOR EACH ROW
DECLARE _Category = :new.Category;
DECLARE _Subcategory = :new.Subcategory;
BEGIN
If _Category = "New"
If _Subcategory = 0
:new.Subcategory := 'New';
End If
Else If _Subcategory = 1
:new.Subcategory := 'Assumed';
End If
End If
If _Category = "Existing"
If _Subcategory = 0
:new.Subcategory := 'Existing';
End If
Else If _Subcategory = 1
:new.Subcategory := 'Organic'
End If
End If
Return "Unknown"
End Function
END;
If the logic isn't clear, in semi-plain English it is:
If the value of the Category field is "New", set the Subcategory field value also to "New" if the value of Subcategory is currently 0; otherwise, set it to "Assumed"
If the value of the Category field is "Existing", set the Subcategory field value also to "Existing" if the value of Subcategory is currently 0; otherwise, set it to "Organic"
Maybe I need to give Steely Dan's album "Trigger Logic" a listen.
UPDATE
I think the answer will work, but it's not complete enough for me.
Since I apparently have Oracle code mixed up in the pseudoSQL above, what would the complete code need to look like (to create a BeforeInsert trigger on the CustomerCategoryLog table)?
Is this more like it:
CREATE TRIGGER tr_CustomerCategoryLog_BeforeInsert
ON CustomerCategoryLog
INSTEAD OF INSERT
AS
BEGIN
SELECT
CASE
WHEN #Category = 'New'
THEN CHOOSE(#Subcategory + 1, 'New', 'Assumed')
WHEN #Category = 'Existing'
THEN CHOOSE(#Subcategory + 1, 'Existing', 'Organic')
ELSE 'Unknown'
END
END
?
I tend to avoid triggers (perhaps a character flaw... I also don't like mashed potatoes), but the following illustration could simplify your logic
Declare #Category varchar(50) = 'Existing'
Declare #Subcategory int = 1 -- works if BIT
Select case when #Category = 'New' then choose(#Subcategory+1,'New','Assumed')
when #Category = 'Existing' then choose(#Subcategory+1,'Existing','Organic')
else 'Unknown' end
Returns
Organic

Oracle Issue with retrieving single row when multiple rows returned

I have a problem that i've spent about 3 days on.
I have a table(CDKEY) with 6 columns: CDKEYSEQ, Userseq,Banned, Communityseq, cdkey, Email.
Banned is always 0 (at this point), Userseq is NULL unless someone logged on/registered with the cdkey and email is NULL until the cdkey is registered.
Basically Userseq doesn't get filled in until a user logs in. So there will always be an email value before a user sequence value.
NOW The issue:
I'm trying to create a stored procedure that gets called when someone wants a cdkey (which they provide an email for).
The procedure first checks a table called community to make sure the Community exists.
Then if the Community exists The procedure is supposed to check the CDKEY Table for a key that has the correct community sequence AND AlSO has a NULL Value for both USERSEQ and EMAIL.
Obviously using just a select query doesnt work because there are multiple rows that are returned that match those conditions.
I tried using cursors, which got me a little further.
The problem with the cursors is that when I had two conditions after the WHERE clause, it didnt return anything.
Here is my current Procedure Code:
create or replace PROCEDURE KEYREGISTRATION(
PRODUCT_IN IN VARCHAR2 ,
in_CPUID IN LONG ,
in_MACID IN LONG ,
in_MACID2 IN LONG ,
in_HDID IN LONG ,
in_PCCores IN LONG ,
in_PCName IN VARCHAR2 ,
in_Email IN VARCHAR2 ,
out_cdkey OUT VARCHAR2 ,
returncode OUT NUMBER )
AS
CodeSuccess CONSTANT NUMBER := 0;
CoreError CONSTANT NUMBER := 2;
CodeAlreadyExists CONSTANT NUMBER := 3;
CodeBadProduct CONSTANT NUMBER := 4;
new_cdkey VARCHAR2(50);
old_cdkey VARCHAR2(50);
acommunitySeq NUMBER;
BEGIN
acommunitySeq := 0;
new_cdkey := '';
old_cdkey := '';
SELECT COMMUNITYSEQ INTO acommunityseq FROM COMMUNITY WHERE NAME = PRODUCT_IN;
returncode := CodeSuccess;
/*EXCEPTION
WHEN NO_DATA_FOUND THEN
returncode := CodeBadProduct; */
IF returncode = CodeSuccess THEN
BEGIN
SELECT CDKEY INTO old_cdkey FROM CDKEY WHERE EMAIL = in_email;
returncode := CodeBadProduct;
out_cdkey := old_cdkey;
RETURN;
EXCEPTION
WHEN NO_DATA_FOUND THEN
returncode := CodeSuccess;
END;
END IF;
IF returncode = CodeSuccess THEN
/*SELECT CDKEY into new_cdkey FROM CDKEY WHERE EMAIL = NULL AND COMMUNITYSEQ = acommunityseq; */
DECLARE
CURSOR c1
IS
SELECT CDKEY FROM CDKEY WHERE COMMUNITYSEQ = acommunityseq AND EMAIL = NULL;
BEGIN
OPEN c1;
FETCH c1 INTO new_cdkey;
IF ( c1%notfound ) THEN
returncode := CoreError;
END IF;
UPDATE cdkey SET EMAIL = in_email WHERE CDKEY = new_cdkey;
INSERT INTO user_hw VALUES( EMAIL = in_email, CPUID = in_cpuid,
MACID = in_macid, MACID2 = in_macid2, CPUCORES = in_pccores, PCNAME = in_pcname;
out_cdkey := new_cdkey;
returncode := CodeSuccess;
COMMIT;
END;
ELSE
returncode := CoreError;
ROLLBACK;
END IF;
END KEYREGISTRATION;
You think a query will not work - and your reasoning is "because a query will return too many rows." That is incorrect. Add a WHERE clause (or add to the filters you already have), with the condition ROWNUM = 1 - this will return the first row that meets all the other conditions, the processing will end, and you will get just this row and nothing else.

What was picked within a columns.picklist?

I wasted a bit of time trying to work out something I figured would be simple.
I've got a database with multiple tables (MySQL). One table containing "Components" and another containing "Products". Products are built using Components, for example; Product ABC might be made up of 3 x Screws, 4 x bolts, 1 kilogram of fresh air... etc! Am I making sense so far?
The components are displayed in a DBGrid. If the user makes a mistake and wants to add another "Component" to a "Product" a Picklist appears listing all Components (from a different table) for them to select from.
Now, here's my problem! When something is selected from the column[i].picklist (this is part of a DBGrid) how do I know what was selected. I thought there would be an event fired, but there doesn't seem to be.
I need to know which item was selected so I can retrieve an appropriate description for the next field.
There are 3 fields, they are COMPONENT, DESCRIPTION, QUANTITY. Only COMPONENT and QUANTITY can be edited by the user.
I hope I'm making some sense here.
Here is the code I'm using now (as messy as it is);
procedure TForm1.CompletePolesSourceStateChange(Sender: TObject);
var
loop: Integer;
Tmp: string;
begin
case CompletePolesSource.state of
dsInsert:
begin
CompVals.Clear; // Is a tstringlist created elsewhere
CompVals.Delimiter := '|';
CompVals.QuoteChar := '"';
PoleComponentsGrid.Columns[0].readonly := false; // Is readonly when not in DSInsert
PoleComponentsGrid.Columns[0].PickList.Clear; // Clear other crap
{
Now add Parts to a Name / Value list (CODE / DESCRIPTION) so I can later
get the description without looking it up in the other table.
}
for loop := 1 to componentstable.RecordCount do // Get CODE from other table
begin
componentstable.RecNo := loop;
tmp := componentstable.Fieldbyname('CODE').asstring + '=' + componentstable.Fieldbyname('ITEM').asstring;
CompVals.Add(tmp);
PoleComponentsGrid.Columns.Items[0].PickList.Add(tmp);
end;
PoleComponentsGrid.Columns.Items[0].readonly := true;
end;
end;
end;
This will show the data of the selected rows of the DBGrid
procedure TFrmPrincipal.btnShowSelectedRowsClick(Sender: TObject);
var
i: Integer;
aux: string;
begin
for i := 0 to DBGrid1.SelectedRows.Count - 1 do
begin
ClientDataSet1.GotoBookmark(pointer(DBGrid1.SelectedRows.Items[i]));
aux := aux + IntToStr(ClientDataSet1.RecNo) + ' - ' +
ClientDataSet1.FieldByName('CUSTOMER').AsString + #13;
end;
ShowMessage(‘Selected Rows: ‘ + #13 + aux);
end;

Problems with writing to a MS Access Database (Delphi)

I'm trying to write bits of code to a Microsoft access database from Delphi. I'm getting data from a TStringGrid. The first column has the ItemID, and the 2nd column has the Quantity. I'd like it to loop through the TStringGrid and save each row as a reperate row in my database and also save the Order ID with it on every column (The order ID stays the same for each order so that doesn't need to change) .
I'm getting an error when running which says
"Project Heatmat.exe raised an exception class EVarientInvalidArgError with message 'Invalid Argument'. Process Stopped."
I can't figure out why it's giving me this error, and as you can probably see i'm not very good at coding yet. Any help would be appreciated!
Thank you.
procedure TCreateNewOrder.btnSaveClick(Sender: TObject);
var
intNumber, count : integer;
begin
Count:= 0;
if messagedlg ('Are you sure?', mtWarning, [mbyes, mbno], 0) = mryes then
begin
with HeatmatConnection.HeatmatDatabase do
begin
intNumber:= TBLOrder.RecordCount;
TBLOrder.Append;
TBLOrder['CustomerID']:= CompanyName.ItemIndex+1;
TBLOrder['OrderID']:= intNumber +1;
for count:= 1 to StringGrid1.RowCount-1 do
begin
TBLOrderedItem.Append;
TBLOrderedItem['OrderID']:= intNumber+1;
TBLOrderedItem['ItemID']:= StringGrid1.Cells[1, count];
TBLOrderedItem['Quantity']:= StringGrid1.Cells[2, count];
TBLOrderedItem.Post;
end;
end;
end;
end;
TStringGrid cells are strings. trying to assign a string directly to a numeric field will raise an Exception.
So a good practice is to assign values to database fields via AsString, AsInteger, AsBoolean etc... this will make the correct conversion.
In your code use:
TBLOrderedItem.FieldByName('ItemID').AsString := StringGrid1.Cells[1, count];
The same is true for Quantity.
To assign an Integer value use:
TBLOrderedItem.FieldByName('OrderID').AsInteger := intNumber + 1;
BTW, you are forgetting TBLOrder.Post i.e:
....
TBLOrder.Append;
TBLOrder.FieldByName('CustomerID').AsInteger := CompanyName.ItemIndex + 1;
TBLOrder.FieldByName('OrderID').AsInteger := intNumber + 1;
TBLOrder.Post;
...
Finally, I would also suggest to rename TBLOrder to tblOrder so that it's name wont imply that it is a Type.

Resources