HomeDeveloper ReferenceTally Definition LanguageUser Defined Fields and Validation Controls

 

Explore Categories

 

 PDF

User Defined Fields, Validations and Controls

In the topic on object manipulation, we have covered the concept of creating and updating Internal objects and persisting the data/information as per the existing structure of the object. When an object needs to be manipulated with a particular data/information, it needs to be reflected against the predefined storage name associated with it.

The storage name is same as the method name available with the object. In real life scenarios, as per business requirements the data storage requirements may not be limited only to the methods already available within the Objects. The Tally user may require additional fields on the screen apart from the ones available in the Default Tally. For example, while entering a Sales Voucher, the dispatch details should store Vehicle details also. In such scenarios, the need to store or persist additional information as a part of existing Internal Object becomes mandatory.

TDL provides the capability to store additional information as a part of the existing internal object hierarchy in the form of User Defined Fields. This chapter will completely focus on defining and storing User Defined Fields. Later, in the topic we will also cover Validations and Control which are the most important requirements to maintain
integrity of data especially when additional functionalities are incorporated apart from the ones provided in default.

User Defined Fields

We have already seen that whenever the data and information entered in the field needs to be stored into Internal Objects, the attribute storage of the field specifies the method/storage name into which the corresponding data is reflected.
When additional information needs to be stored within the existing internal objects and persisted into the Tally Database, User Defined Fields(UDF) are created. User Defined Fields have a storage component defined by the user. All the valid datatypes available in TDL are applicable for UDF’s also.  A user defined field can be of data type such as Strings, Amount, Quantity, Rate, Number, Logical and Date. For usage and implementation of UDF’s, the following points need to be taken care of:

  • The UDF must be defined i.e. a storage component needs to be defined with a specific data type. At this point the storage does not have a correlation with an Internal Object.
  • The field associated with the UDF needs to be in the context of a Data Object. If the data is to be stored in a sub-object in the existing hierarchy of Internal Object, then the field associated with UDF also needs to be in the same sub-object.

UDF Definition

The UDFs are defined under the definition [System: UDF]. The datatype and index number must be specified while creating the UDF.

Syntax

[System : UDF]

<Name of UDF>: <Data Type>: <Index Number>

Where,

< Name of UDF > Identifies the UDF. Ideally, it should describe the purpose for which it has been created.

< Data Type > is any of the Tally data types or ‘Aggregate’.

< Index Number > can be any number between 1 and 65536.

Numbers falling between 1 to 9999 and 20001 to 65536 are opened for customisation, and those between 10000 to 20000 are allotted for common development in TSPL. The user can create 65536 UDFs of each data type.

The index numbers 1 to 29 are already used for Default TDL and are as follows:

1 – 29 of data type String

1 – 3 of data type Date and

1 – 2 of data type Number

Example

[System : UDF]

MyUDF 1 : String : 20003

MyUDF 2 : Date   : 20003

The advantage of UDF in Tally is that it automatically attaches with the current object. No specific declaration is required for object association, when the UDF is defined within system definition.

In the example, above the UDF MyUDF1 is defined with a String Datatype and MyUDF2 is defined with a Date Datatype. A UDF does not come into existence until some value is stored into it and is attached with an Internal Object. A UDF value can be stored along with an object already existing in the Tally database or to a new object being created for a specific object type. Once the value is stored, it can be accessed and used from the specified level just like an ordinary method.

Storing Values into a UDF

As we understand based on object association methodologies, an interface object always exists in context of a data object at any level in the existing data object hierarchy. The placeholder for any data/information is the field Interface object. A field is used to enter or display the values pertaining to a method/storage from a particular
level of the data object to which it is associated. The attribute Storage of field definition is used to specify the
UDF/storage component and attach it at the data object level to which the field is associated i.e. the field value is stored in the context of current object.

The attribute Storage is used to store the value entered in the field, in the current object context.

Syntax

[Field : <Field Name>]

Storage : <Default Storage/Name of UDF>

Where,

< Field Name> is the name of the field whose value is to be stored in the UDF.

< Name of UDF > identifies the UDF. Ideally, it should describe the purpose for which it is created.

Example

[Field : NewField]

Use     : NameField

Storage : MyUDF

Retrieving Values From UDF

As discussed, a UDF is attached to an Internal Object at a particular level in the existing hierarchy structure. Once it is stored, it can be accessed in the same way as an existing internal method.

In the context of the current object, the value of a UDF can be accessed by prefixing $ to the UDF name.

Syntax

$<Name of UDF>

Example

[Field: NewField]

Use    : NameField

Set As : $MyUDF

Retrieving Original UDF Index Number of a Data Within Associated Objects

A UDF can be used to store additional information into the Tally database. UDFs are stored in the current object context. Whenever a UDF is created and used in an already existing report, the data is stored in the context of the current object, i.e., it is always associated to the object to which the report is associated (the object in context).
Previously, if the TDL or the TCP was lost or corrupted, then there was no way by which we could know the UDF details like the UDF Number, and hence, the retrieval of data related to the UDF was quite difficult.

Now, an XML attribute ‘Index’, within the UDF ‘List Tag’, has been introduced to help retrieve the original UDF number corresponding to the data available within the Objects associated with it. This UDF number will be available in the Index attribute in the UDF List Tag, even when the TDL is not attached or is unavailable.

Example

<UDF:TESTUDFNO.LIST DESC=”‘Test UDF No’” ISLIST=”YES” TYPE=”String” INDEX=”1010″>

<UDF:TESTUDFNO DESC=”‘Test UDF No’”>Raam</UDF:TESTUDFNO>

</UDF:TESTUDFNO.LIST>

Here, the UDF number (1010) is displayed under the ‘Index’ attribute in the UDF List Tag.

Classification of UDFs

UDFs are created with the primary purpose of storing additional information apart from the ones available as predefined methods and collection in the existing Object hierarchy. The data storage requirements vary from storing a single/multiple values of a specific datatype to multiple composite values of varied datatypes. In view of
that, UDFs can be classified as given below:

  • Simple UDF
  • Complex/Compound/Aggregate UDF

Simple UDF

A simple UDF is used when a single or multiple values of a specific data type needs to be stored along with the Object specified. A UDF storing a single value of a specific data type can be correlated to a method. For example,  $closingbalance and a UDF storing multiple values of the same data type can be correlated to a simple collection. For example name and address collection.

It can store one or more values of a single data type. A UDF used for storage, stores the values in the context of the object associated at Line/Report level, by default. Only one value is stored in this case.

Store Single Value

When a single value pertaining to the value entered in the field needs to be stored, the field must exist in context
of the data object to which it is associated. Let us consider the example below to understand the storage and
retrieval for the same.

The following example code snippet demonstrates how a UDF can be made use of to store a single value:

Example

[Report : CompanyVehicles]

Object : Company

.

.

.

[Field : CVeh]

Use     : Name Field

Storage : Vehicle

Unique  : Yes

[System : UDF]

Vehicle : String : 700

In the example above the Report is in context of the object Company. The field CVeh is also in the context of Report object at the first level only. So, the storage specification at the field stores the value into the Single non repeat UDF Vehicle. Thereafter, the value thus stored can be retrieved in a field which is in context of the company object
using $vehicle.

The object is associated at the Report Level. The value stored in a UDF is in the context of Company Object in this case. The UDF Vehicle stores a single string value.

Store Multiple Values – Repeat UDF

Multiple values can be entered into a field when the line containing it is repeated in the part over the specified UDF. The storage in the field also specifies the name of the UDF. The implementation and usage of this UDF is exactly like a simple collection.

Syntax

[Part : <Part Name>]

Repeat : <Line Name> : <Name of UDF>

Where,

< Part Name > is the name of the part

< Line Name > is the name of the line to be repeated.

< Name of UDF > identifies the name of the UDF to store multiple values. The example in the section “UDF to store single value” can be modified to store multiple values.

Example

 Let us consider the example below to understand the storage and retrieval for the same. Since the implementation of a Simple UDF storing multiple values is exactly like a Simple Collection, the repeat attribute of Part definition in this case will be as follows: 

[Part : CompVeh]

Line     : CompVeh

Repeat   : CompVeh  : Vehicle

Break On : $$IsEmpty:$Vehicle

Scroll   : Vertical

[Line: CompVeh]

Field : CVeh

[Field: CVeh]

Use    : Name Field
Storage: Vehicle

In this scenario, multiple values of type String can be stored under the object Company. In the example the line containing the field CVeh is repeated over the simple UDF Vehicle. The data entry is repeated till the user enters an
empty value. All the values entered are stored in the UDF Vehicle and are attached to the Company object associated to the report. Thereafter the values stored in the UDF can be retrieved by using $vehicle in the field contained in the line repeated over the UDF Vehicle.

Aggregate UDF

A simple UDF can store single or multiple values of a specific datatype i.e., it contains single or repeated values of the same data type. In real life business scenarios, this does not suffice the data storage requirements. In order to store composite values of discrete datatypes repeating itself once or multiple times, aggregate UDF can be used.

An aggregate UDF can contain multiple Simple UDFs of different datatypes where the Simple UDF can either be Single or Repeat. It can also contain other aggregate UDFs within it and this nesting can continue upto infinity. This can be correlated with compound collections.

Aggregate UDF – Definition

Aggregate UDFs are defined in the same way as Simple UDFs inside the System:UDF definition. The data type to be specified here is Aggregate. The UDF defined using the keyword Aggregate is actually the container for the subcomponents defined thereafter. The subcomponents can be a Simple UDF or another aggregate UDF.

Syntax

[System: UDF]

Name of Aggr UDF       : Aggregate  : <Index Number>

Component UDF 1       : DataType  : <Index Number>

Component UDF 2      : DataType  : <Index Number>

|

|

Component Aggr UDF: Aggregate : <Index Number>

Where,

 < Name of aggr UDF> identifies the UDF and ideally it should describe the purpose for which it is created.

< Index Number> is any number falling between 1 and 65536. The number falling between 1 to 9999 and 20001 to 65536 can be used for customisation.

< Component UDF> 1-N are the components of the aggregate UDF which are Simple or aggregate UDFs.

Example

A company wants to create and store multiple details of company vehicles. The details required are: Vehicle Number, Brand, Year of Mfg., Purchase Cost, Type of Vehicle, Currently in Service, Sold On date and Sold for Amount.

[System : UDF]

Company Vehicles      : Aggregate : 1000

VVehicle Number       : String : 1000

VBrand                : String : 1001

VYear of Mfg          : Number : 1000

VPurchase Cost        : Amount : 1000

VType of Vehicle      : String : 1002

VCurrently in Service : Logical : 1000

VSold On date         : Date : 1000

VSold for             : Amount : 1001

;; Same index number can be given to different storage datatypes

To store the required details, simple UDFs are defined and to store them as one entity , a UDF of type Aggregate is defined, as shown in the example.

Storing Values – Aggregate UDF

Multiple values of discrete data types can be entered in different fields contained in a line. This line will be repeated over the aggregate UDF and the storages in the fields specify the component UDF’s. Aggregate UDF definition does not associate each component field with the aggregate UDF. The association will take place only when the line is repeated over aggregate UDF and the fields within that stores value into the component UDFs. Since the implementation of Aggregate UDF is exactly like a Compound collection, the repeat attribute of Part definition in this case will be as follows:

Syntax

Repeat : <Line Name>: <Name of Aggregate UDF>

Where,

< Line Name> is the name of line to be repeated.

< Name of Aggregate UDF> is the name of UDF defined with aggregate data type.

Example

[Part : Comp Vehicle]

Line     : Comp VehLn

Repeat   : Comp VehLn : Company Vehicles

BreakOn  : $$IsEmpty:$VBrand

.

.

.

[Field : CMP VBrand]

Use     : Short Name Field

Storage : VBrand

In the example,  the line containing the field CmpVBrand and CmpVVchNo is repeated over the aggregate UDF Company Vehicles. The data entry is repeated till the user enters an empty value in the field CmpVBrand. All the values entered in the fields are stored into the corresponding component UDFs of the aggregate UDF Company
Vehicles and are attached to the Company Object associated to the Report.
Thereafter, the values stored in the individual UDF’s can be retrieved by using $VBrand, $VVehicleNumber and so on in the fields contained in the line repeated over the aggregate UDF Company Vehicles. The Line is repeated over the Aggregate UDF and the Simple UDFs are entered in the fields.

Using Aggregate UDF in a Sub-Form

SubForm is an attribute that is used within a Field definition. It relates to a report (not Form) and can be invoked by a field. This attribute is useful to activate a report within a report, perform the necessary action and return to the report used to invoke the Subform. There is no limit on the number of subforms that can be used at the field level.

Syntax

[Field : Field Name]

Sub Form : <Report Name> : <Condition>

Where,

< Report Name > is the name of the Report to be displayed.

< Condition > could be any expression, which evaluates to a logical value. The report will be displayed only when the condition is True.

A Sub Form is not associated to the Object at the Report level. An Object associated to the Field in which the Sub Form is defined, gets associated to the Sub Form. A Sub Form will inherit the info object from the Field which appears as a pop-up.

The Bill-wise Details is an example of a Sub Form attribute. This screen is displayed as soon as an amount is entered for a ledger whose Bill-wise Details feature has been activated.

Example

The following code snippet uses a Sub Form to enter the details of bills when the Bill Collection ledger is selected, while entering a Voucher. The values entered in the Sub Form are stored in an Aggregate UDF. This UDF is attached to the object to which the field displaying the Sub Form is associated. Here, it is the Object of a Ledger Entries Collection.

The following code is used to associate a Sub Form to the default Field in a voucher.

[#Field : ACLSLed]

Sub Form : BillDetail : ##SVVoucherType = “Receipt” and $LedgerName = “Bill Collection”

The Name Report for the Subform uses an Aggregate UDF to store the data. A Line is repeated over the Aggregate UDF at the Part level.

[Part : BillDetails]

Scroll      : Vertical

Line        : BillDetailsH, BillDetailsD

Repeat      : BillDetailsD : BAggre

Break After : $$Line=2

The attribute Storage is used for all the fields.

[Field: CustName1]

Use     : Name Field

Storage : CustName

The UDF is defined as follows:

[System : UDF]

CustName : String : 1000

BillNo   : String : 1001

BillAmt  : Amount : 1001

EPrint1  : String : 1002

BAggre   : Aggregate : 1000

Usage – As a Table

The data stored in the Repeat UDFs and Aggregate UDFs are analogous to the Objects in the Collection. This data can be displayed as a table. In order to use the data stored in the UDFs as a table a collection needs to be constructed.

Collection Using UDF Stored at the Primary Object Level

Since, the UDF will always be attached to an existing internal object, the type specification will contain reference to the primary object.

Syntax

[Collection: <Collection Name>]

Type        : <UDF Name> : <Primary Object Name>

Child Of  : < Object Identifier>

Format   : $<UDF Name>, 20

Example:

[Collection: CMP Vehicles]

Type     : Vehicle : Company
Child Of : ##SVCurrentCompany
Format   : $Vehicle, 20
Title    : “Company Vehicles”

We have seen in previous examples that the Repeat UDF “Vehicle” stores multiple values of the same data type and is associated with the Company Object. The collection CMP Vehicles is constructed by specifying the type as Vehicle of a Company Object.

The Child of specifies the Company Object identifier which is the current company. Once the collection is defined it can be used in the Table attribute of field definition. So when the cursor is in the defined field the values stored in the UDF will be displayed as popup table.

[Field: EI Vehicles Det]

Use         : Short Name Field
Table       : CMP Vehicles, Not Applicable
Show Table  : Always

Collection Using UDF Stored at Any Level in the Object Hierarchy

As we know a UDF can be stored at any level in the existing Object hierarchy. In those cases, referring to the UDF data and construction of the collection using the referencing method as above is not possible. In those cases the data corresponding to the UDFs can be gathered only by traversing to the desired level in the hierarchy. The Walk attribute of the collection will be used for the same.

Example

Refer to the example used in using Subforms where the aggregate UDF “BAggre” with components BillNo, BillAmt, etc. are attached at the Ledger Entries level. The source collection is constructed using Vouchers of type “Receipt”

[Collection: Src Bills]

Type     : Vouchers : Voucher Type
Child Of : $$VchTypeReceipt

The BillTable collection walks over the Ledger Entries and then over BAggre UDF and then fetches the methods “BillNo” and “BillAmt”. Format is specified for the methods to be displayed in the Table.

[Collection: BillTable]

Source Collection : SrcBills

Walk              : LedgerEntries, BAggre

Fetch             : BillNo, BillAmt

Format            : $BillNo, 10

Format            : $BillAmt, 20

;;The above table is attached to the field “VchNarration”


[#Field: VchNarration]

Table             : BillTable

Validation and Controls

The validation can be applied at both Platform as well as TDL level. Platform always enforces core database integrity constraints. For example, a Voucher cannot be saved unless Debit and Credit amount is matched. Second level validations can be done at the TDL level.
This can be well utilized by the application developers to enforce business requirements. The validation concept can be used for different purposes like

  • Each business will have unique organizational structure. Naturally this needs to be reflected in the usage of Tally application. For example, the restricting access of Reports to the Data Entry Person or restricting Data Entry person to create Masters.
  • To assist the Data Entry operator to enter meaningful information. For Example PF Date of Joining should not be less than Date of Joining.
  • To enforce the integrity constraints. For example Vouchers having manual numbering with ‘prevent duplicates’, duplication of Voucher numbers are not allowed.
  • Customized reports can be brought under default security control

The following section discusses about developing TDL level validation with the help of definitions, attributes and built-in functions.

Field Level Attributes

Validate

The attribute Validate checks if the given condition is satisfied. Unless the given condition is satisfied, the user cannot move further. In other words, if the given condition for Validate is not satisfied, the cursor remains placed on the current field without moving to the subsequent field. It does not display any error message.

Syntax

Validate : < Logical Formula>

Where,

< Logical Formula> returns True/False based on which the cursor movement is decided.

Example

[Field: CMP Name]

Use      : Name Field

Validate : NOT $$IsEmpty:$$Value

Storage  : Name

Style    : Large Bold

In the above code snippet,

  • The field CMP Name is a field in Default TDL which is used to create/ alter a Company.
  • Validate stops the cursor from moving forward, unless some value is entered in the current field.
  • The function, IsEmpty returns a logical value as True, only if the parameter passed to it contains NULL.
  • The function, Value returns the value entered in the current field.
  • Thus, the attribute Validate used in the current field, controls the user from leaving the field blank and forces a user input.

Unique

This attribute takes a logical value. If it is set to Yes, then the values keyed in the field have to be unique. If the entries are duplicated, an error message, Duplicate Entry pops up. This attribute is useful when a Line is repeated over UDF/Collection, in order to avoid a repetition of values.

Syntax

Unique: <Yes / No>

Example

[!Field: VCHPHYSStockItem]

Table  : Unique Stock Item : $$Line = 1

Table  : Unique Stock Item, EndofList

Unique : Yes

In this code snippet, the field, VCHPHYSStockItem is an optional field in DefTDL which is used in a Physical Stock Voucher. The attribute, Unique avoids the repetition of Stock Item names.

Notify

This attribute is similar to the attribute Validate. The only difference is that it flashes a warning message and the cursor moves to the subsequent field. A System Formula is added to display the warning message.

Syntax

Notify : < String Formula> :  < Logical Formula>

Where,

< String Formula> is the name of the System Formula which is used to display as message in message box.

< Logical Formula> returns True/False based on this condition message box will trigger.

Example

[!Field: VCH NrmlBilledQty]

Notify : NegativeStock : ##VCFGNegativeStock AND @@IsOutwardType AND $$InCreateMode AND +

         $$IsNegative:@@FinalStockTotal

 
In this code snippet, VCH NrmlBilledQty is a default optional field in DefTDL used in a Voucher. The Notify attribute pops up as a warning message, if the entered quantity for a stock item is more than the available stock and the cursor moves to the subsequent field.

Control

The attribute Control is similar to Notify. The only difference is that it does not allow the user to proceed further after displaying a message. The cursor does not move to the subsequent field.

Syntax

Control : < String Formula >  : < Logical Formula>

Where,
< String Formula> is the name of the System Formula which is used to display as message in message box.
< Logical Formula> returns True/False. Based on this condition, message box will be triggered.

Example

[Field: Employee PFDateOfJoining]

Use       : Uni Date Field

Set As    : If $$IsEmpty:$PFAccountNumber AND $$IsEmpty:$FPFAccountNumber Then “” Else If NOT +

            $$FieldEdited OR $$IsEmpty:$PFJoiningDate Then $DateofJoin Else $$Value
Storage   : PFJoiningDate
Width     : 20
Align     : Left
Style     : Small Bold
Control   : PFJoiningDateBelowJoinDate:If $$IsEmpty:$PFAccountNumber AND $$IsEmpty: + 

            $FPFAccountNumber Then No Else $$Value < #EmployeeDateOfJoin
Set Always: Yes

In this code snippet, the field, ‘Employee PFDateOfJoining’ is a default field. The control always makes sure that Date of Joining for a Employee is always less than the Date Provident Fund joining Date.

The difference between the field attributes, Validate, Notify and Control are:

Field Attributes Displays Message Curser Movement
Validate No Restricted
Notify Yes Not Restricted
Control Yes Restricted

Form Level Attributes

Control

When the contents of a Form needs to be validated before accepting them, then Form level validation can be applied by using attribute Control. This attribute achieves a higher level of control on the contents of a Form over other controls used at the Lower levels of the Form.
If the condition specified with Control is not satisfied, then the Form displays an error message while trying to save. The Form cannot be saved until the condition in the attribute Control is fulfilled.

Syntax

Control: < String Formula>  : < Logical Formula>

Where,
< String Formula> is the name of the System Formula which is used to display as message in message box.
< Logical Formula> returns True/False based on this condition message box will trigger.

Example

[Form: Voucher]

Control : DateBelowBooksFrom :$Date < $BooksFrom:Company:##SVCurrentCompany
Control : DateBelowFromDate : $Date < $$SystemPeriodFrom
Control : DateBeyondToDate : $Date > $$SystemPeriodTo

In the example, Voucher is a default Form. While creating a voucher, the attribute, Control does not accept dates beyond the financial period or before beginning of the books.

Control

The attribute, Control restricts the appearance of Menu Items, based on the given condition.

Syntax

Control: < Key Item Name>  : < Logical condition>

Where,
< Key Item Name> is a string represents the Label of the Key Item.
< Logical condition> returns True/False based this appearance of Menu Item is decided.

Example

[Menu: Quick Setup]

Key Item  : @@locExciseForManufacturer: M:Display: ExciseMfgr QuickSetUp

Control   : @@locExciseForManufacturer: @@IsIndian AND $$IsInventoryOn

In this code snippet, the Menu, Quick Setup is a default definition. The Menu Item, Excise for Manufacturer, will be displayed only if the selected company is having statutory compliance for India Inventory module enabled.

Report Level Attributes

Multi Objects

This is a ‘Report’ level attribute which is required to be specified, in case Multiple Objects of the same collection are being added/modified in a Report. It is required specifically in case of multi master creation or alteration.

Syntax

MultiObjects: <Edit Collection>

Where,

<Edit Collection> is the name of the Collection for which modifications are to be done.

Example

[Report: Multi Ledger]

Multi Objects: Ledger Under MGroup

Family

The report level attribute Family is useful when the Security Control is enabled for the company. A Report can be made accessible for only a set of user(s) by setting proper rights at security levels.

For this name of the Report needs to be brought under default Collection ‘Report List’. The value specified with the attribute, Family is automatically added to the security list as a pop-up while assigning the rights under Security Control Menu.

Syntax

[Report: <Report Name>]

Family : <String Value>

Where,

<String Value> is a string which will appear under Security Control

Example

[Report:Balance Sheet]

Family : $$Translate:”Balance Sheet”

In this code snippet, the Balance Sheet is added to the Security list. The users having rights to display Balance sheet can only view the Report.

Function – $$Allow

This built-in function checks the permissions for the currently logged in user. This function can be effectively used to enable or disable an interface based on the permissions for the currently logged in user.

Syntax

$$Allow: < Literal Value 1 >: < Literal Value 2 >

Where,

< Literal Value 1> is a string which decides the type of access.

< Literal Value 2> is a string as well as the name of Report which pops under default collection ‘Report List’.

Example

[!Menu: Gateway of Tally]

Key Item: @@locAccountsInfo : A : Menu : Accounts Info. : NOT $$IsEmpty:$$SelectedCmps

Control : @@locAccountsInfo :$$Allow:Create:AccountsMasters OR $$Allow:Alter:AccountsMasters +

          OR $$Allow:Display:AccountsMasters

In this code snippet, the Menu, ‘Gateway of Tally’ is a default optional menu. The Menu Item ‘Account Info.’ will be displayed only if the given condition is satisfied. The function ‘Allow’ checks whether the current user has the rights to access the report displayed under the current Menu item. The value ‘Accounts Masters’ has been derived from the
attribute Family at the report definition.

TallyHelpwhatsAppbanner
Is this information useful?
YesNo
Helpful?
/* */