Accounting Documents
The vast majority of transactional documents which come packaged with KFS are "Accounting Documents": documents that, in some way, create accounting lines as an essential part of the document. (the Labor Expense Transfer documents, Labor Journal Voucher document, and several Purchasing/Accounts Payable documents also include accounting lines, and they build on the Accounting Document framework as well. This document will concentrate, though, upon Financial Documents as the easiest starting off point.) Given this, it makes sense that KFS provides a lot of functionality for dealing with documents with accounting lines. Implementing new financial documents is made much easier by having a full idea of what functionality exists for us to build upon.
Accounting Documents
What specific features does an accounting document support? To answer that question, let's take a look at org.kuali.kfs.sys.document.AccountingDocument. Traditionally, documents in the document hierarchy are declared as interfaces, with the default implementation in the same package, with the name {name of document interface}Base. This is just a long way of saying we need to take a look at org.kuali.kfs.sys.document.AccountingDocumentBase as well.
The most basic functionality that implementations of the AccountingDocument interface must perform is that of tracking accounting lines. AccountingDocument automatically keeps track of two sets of accounting lines - the source accounting lines and target accounting lines. Therefore, for each of those two groups there are methods to:
- get all of the accounting lines in the group as a List: getSourceAccountingLines() and getTargetAccountingLines()
- set a group of accounting lines to those lines in a List: setSourceAccountingLines() and setTargetAccountingLines()
- add a new accounting line to the List of accounting lines in the group: addSourceAccountingLine() and addTargetAccountingLine()
- return the accounting line at a given index in the list: getSourceAccountingLine() and getTargetAccountingLine()
- gets the sum monetary total for all the accounting lines in a group: getSourceTotal() and getTargetTotal()
- and finally, getters and setters for the next index for the source or target accounting lines
That leaves but one last method in the AccountingDocument interface: getAccountingLineParser(). We'll take a closer look at in the section on Accounting Line Parsers.
What if my accounting document only has one set of accounting lines?
Traditionally, documents with only one set of accounting lines - such as the documents in the Disbursement Voucher or Journal Voucher - use the source accounting lines and don't display the target accounting lines; we'll take a look at how to do that when we look at the HTML side of this framework.
Document Hierarchy
The AccountingDocument interface extends several other interfaces, each of which adds functionality to the document. Let's concentrate on org.kuali.kfs.sys.document.LedgerPostingDocument, which extends TransactionalDocument and therefore is based on KNS functionality, and org.kuali.kfs.sys.document.GeneralLedgerPostingDocument, which extends LedgerPostingDocument. AccountingDocument, in turn, extends GeneralLedgerPostingDocument.
LedgerPostingDocument defines some basic methods relating to posting. It defines six simple methods - three getter/setter pairs - for the properties that all posting would need to know: the posting year, the posting period code, and then the related AccountingPeriod record, which uses the posting period code and posting year as keys. That's it. However, all entries that post to a ledger need to have information relating to when they were posted, so these are obviously important.
LedgerPostingDocument is then extended to post to the general ledger: GeneralLedgerPostingDocument to post to the general ledger. (There is also a org.kuali.kfs.module.ld.document.LaborLedgerPostingDocument.) GeneralLedgerPostingDocument has several methods related to generating pending entries for the document, including getting a list of pending entries, determining if bank cash offsets are required, and sending sufficient funds checks back a list of pending entries related to the document. This is, of course, critical functionality: each accounting line document should generate pending entries to send to the scrubber and poster.
What are accounting lines?
Accounting lines come in many, many different varieties - just take a look at any of the financial processing documents. Basically, though, an accounting line simply represents a charge to an account, which can then be turned into an explicit and offset general or labor ledger entry. Every accounting line requires a chart, an account, an object code (which may be either an income, expense, asset or liability object code, therefore changing how the amount of the line would be interpreted in the context of the document - whether it's a debit or a credit), and an amount; accounts can also be associated with sub-accounts and object codes can be associated with sub-object codes. Accounting lines also contain a Project code and Org Ref ID which can provide additional information about the accounting line. Some documents include additional fields, such as Line Desc, Ref Origin Code, Ref Number which further expands information contained in the accounting line. The Journal Voucher allows a huge amount of freedom in entering information; for instance, a user can set the object type and balance type for an accounting line, which gives an unbelievable amount of control over the ledger and is consequently only available for use by a couple of people. Most financial processing documents, though, follow very basic accounting rules and are simply easier ways to enter accounting information.
Extending AccountingLine
Several accounting documents need more information than is stored by AccountingLine. For instance, BudgetAdjustmentDocument needs to know what accounting periods certain adjustments hit in. LaborJournalVoucherDocument needs to keep a lot of extra information about its accounting lines. For that reason, these documents extend SourceAccountingLine and TargetAccountingLine to hold the extra information. (And, for the curious, there may be other reasons to extend accounting lines as well; take a look at the comments in GECSourceAccountingLine.)
Of course, extending the accounting line classes aren't cost free. There's a number of things that need to be done to get the document to use the new accounting line types and otherwise get the new accounting lines to work:
- Each of the new accounting lines need a brand new OJB mapping. That involves a lot of copying an pasting, so that you're certain that all the previously existing attributes in AccountingLine still work.
- The document needs to use the new accounting line class in its own OJB mappings, for the sourceAccountingLines and targetAccountingLines mappings.
- Finally, the accounting document has two methods: getSourceAccountingLineClass() and getTargetAccountingLineClass(). Obviously, the document with the extended accounting line tags needs to return the extended source accounting line class and the extended target accounting line class.
This need also caused some interesting issues to pop up in workflow document type defining; we'll cover why that was below.
Document Totaling
Since each accounting line has a monetary amount associated with it, we often think in terms of the entire document having a total. Furthermore, it's often useful to search for accounting documents based on its total amount. For that reason, the org.kuali.kfs.sys.document.AmountTotaling interface is defined in KNS. That interface declares one method, getTotalDollarAmount(), which will return the total dollar amount for the accounting document.
AccountingDocumentBase defines a default getTotalDollarAmount implementation, based on the assumption - true in the majority of accounting documents - that the total of the source lines should be equal to the total of the target lines, and therefore, if it returns one of those totals, it will be the total amount of the document.
Not the sum!
AccountingDocumentBase returns either the source or the target amount, but not the sum of them. This is because on most documents, that sum would be zero - ie, the source and target lines would cancel each other out - and therefore, one side alone determines the total of the document, though which side doesn't really matter, as they're going to equal out.
AccountingDocumentBase does not implement AmountTotaling...it just defines the single method that AmountTotaling requires of implementations. Therefore, documents which inherit from AccountingDocumentBase can define themselves as AmountTotaling documents and simply inherit that functionality from AccountingDocumentBase. Or they can avoid defining themselves as AmountTotaling, and not be picked up by workflow as a document with a total (though, in current KFS practice, this is a very rare situation). Naturally, documents inheriting from AccountingDocumentBase can override that definition and define a document specific version of getTotalDollarAmount() - JournalVoucher, for example, does this.
Accounting Line Parsers
Most Accounting Documents allow the uploading of accounting lines into either the source or the target section. This way, an entire file of accounting lines can be uploaded at once, which makes filling out documents much easier. To accomplish this, AccountingDocument returns implementations of org.kuali.kfs.sys.businessobject.AccountingLineParser to actually parse the documents.
Currently, .CSV files are the only files that AccountingLineParser implementations can parse (though org.kuali.kfs.sys.businessobject.AccountingLineParserBase could be extended to deal with, say, Excel spreadsheets or some other sort of text delimited files). Basically, an implementation of AccountingLineParserBase sets up a format of the imported data - basically, which accounting line fields are in which column of the .CSV file. In AccountingLineParserBase, for instance, the fields are assumed to be in the order: chart code, account number, sub account number, financial object code, financial sub-object code, project code, organization reference id, and amount. This format - an array of String field names - is returned by the getSourceAccountingLineFormat() and getTargetAccountingLineFormat() methods. Therefore, to change the default behavior of the AccountingLineParser, one need only extend AccountingLineParserBase and then return different arrays from the getSourceAccountingLineFormat() and getTargetAccountingLineFormat(). In effect, this is what BudgetAdjustmentAccountingLineParser does. AccountingLineParserBase also defines two methods, performCustomSourceAccountingLinePopulation and performCustomTargetAccountingLinePopulation, which allow classes which extend AccountingLineParserBase to parse the accounting lines in whatever way they see fit.
Setting the accounting line parser for a document is done through the data dictionary, through the importedLineParserClass property. Here's how the BudgetAdjustmentDocument sets the accounting line parser.
<bean id="BudgetAdjustmentDocument" parent="BudgetAdjustmentDocument-parentBean"/> <bean id="BudgetAdjustmentDocument-parentBean" abstract="true" parent="AccountingDocumentEntry"> <property name="documentTypeName" value="BA"/> <property name="documentClass" value="org.kuali.kfs.fp.document.BudgetAdjustmentDocument"/> <property name="importedLineParserClass" value="org.kuali.kfs.fp.businessobject.BudgetAdjustmentAccountingLineParser" /> . . . </bean>
Accounting Document Rules
First of all, and most basically, accounting documents allow the user to add accounting lines (at least source accounting lines, but likely both source and target accounting lines. We note, though, that none of the rule methods in the accounting document rule framework differentiate between source and target accounting lines. Thankfully, the AccountingLine interface declares two methods: isSourceAccountingLine() and isTargetAccountingLine() which allow rules to differentiate the two.
If we can add accounting lines, there's a decent change we'll input bad data and want to wipe out an entire accounting line. Therefore, we've got a delete accounting line event as well. And too, we may not delete the whole line but rather correct the fields that are in error. Therefore, we've also got an update accounting line event. There also exists a "review" event, which is generated for accounting lines that were not updated. This allows rules to be processed on the accounting lines that may be in error because of events that occurred within the rest of the document.
A lot of important business logic within KFS is wrapped up in Accounting Documents. A major push was made in KFS to make this business logic more easily changed by implementing institutions. This new validation system builds on the concepts in the traditional KNS validation system, but leverages Spring to treat validations more interchangeably.
It should be noted that while this functionality was developed specifically for Accounting Documents, it can be used on any TransactionalDocument.
Steps for validating a documentÂ
Before we begin in earnest, here's a check list of the steps we need to do, in order, to set up validations for an accounting document. Each of these steps is covered both in more detail and with an augmented sense of narrative flair in the other sections.
- First, create validation beans. This is covered in detail later in this document.
- If the document's module does not have a module/{module name}/document/validation/configuration/{module name but nicely upper cased}ValidatorDefinitions.xml, one needs to be created. This Spring XML configuration will be the repository of the definitions for the validation beans we just created. All of these beans, though, should be abstract until we tie them into our event-based validations.
- Create in module/{module name}/document/validation/configuration a Spring XML configuration named {document name}Validation.xml. This is where we set up the validations as they relate to the events they are validated against.
- In the document's module's spring-{module name}.xml file, we'll write import statements for all the new Spring files created so far.
- In the data dictionary configuration for the document, we'll do the following:
- Create a validation map that maps event classes to the names of the beans that should validate that event
- Set the validationMap property of the main document configuration bean to have a ref to the validation map bean we just created
- And finally, set the businessRulesClass to org.kuali.kfs.rules.AccountingRuleEngineRuleBase.
That's it! Let's take a look at the details.
Creating validation beansÂ
In the KNS rules model, rules are associated with events. Every event has a corresponding interface which a rules class must implement to be able to handle events of that type. So, for instance, the RouteDocumentEvent is associated with the RouteDocumentRule and when fired, the event calls processRouteDocument - the method required by the RouteDocumentRule method - on the rules class. This meant that all the business logic for the rule was called, at least, from a single, monolithic point, and while developers tried to break up rules in a way that they could be overridden, there was no good way to add rules at a top level without inheriting a rules implementation and extending the rules method.
The new Accounting Document Rules Engine takes a different tack. It splits rules into very simple validations. These simple validations are then declaratively configured via Spring. If an implementing institution wants to change the rules, they need only write the extra rulettes they're interested in and change the declared configuration. While there's a lot of extra configuration involved in this process, we've gained a huge amount of flexibility in the process.
What are the rules for creating a new validation class?
- The class implements the org.kuali.kfs.validation.Validation interface (though it's typically easier to simply extend org.kuali.kfs.validation.GenericValidation)
- The class mplements the public boolean validate(AttributedDocumentEvent event) method. This method should contain all of the business logic for the validation: the business logic test, the setting of errors, and anything else the Validation needs to do.
- The class has setter properties for any information they need to be given from the triggering event to complete the validation. For instance, most Validation classes contain a "setAccountingDocumentForValidation" method.
Let's take a look at a real Validation, org.kuali.kfs.sys.validation.impl.BusinessObjectDataDictionaryValidation, which takes any business object and performs the standard data dictionary validations against it.
/** * A validation to have the data dictionary perform its validations upon a business object */ 1. public class BusinessObjectDataDictionaryValidation extends GenericValidation { 2. private DictionaryValidationService dictionaryValidationService; 3. private PersistableBusinessObject businessObjectForValidation; /** * Validates a business object against the data dictionary * @see org.kuali.kfs.validation.GenericValidation#validate(java.lang.Object[]) */ 4. public boolean validate(AttributedDocumentEvent event) { 5. return getDictionaryValidationService().isBusinessObjectValid(businessObjectForValidation, ""); 6. } /** * Gets the dictionaryValidationService attribute. * @return Returns the dictionaryValidationService. */ 7. public DictionaryValidationService getDictionaryValidationService() { 8. return dictionaryValidationService; 9. } /** * Sets the dictionaryValidationService attribute value. * @param dictionaryValidationService The dictionaryValidationService to set. */ 10. public void setDictionaryValidationService(DictionaryValidationService dictionaryValidationService) { 11. this.dictionaryValidationService = dictionaryValidationService; 12. } /** * Gets the businessObjectForValidation attribute. * @return Returns the businessObjectForValidation. */ 13. public PersistableBusinessObject getBusinessObjectForValidation() { 14. return businessObjectForValidation; 15. } /** * Sets the businessObjectForValidation attribute value. * @param businessObjectForValidation The businessObjectForValidation to set. */ 16. public void setBusinessObjectForValidation(PersistableBusinessObject businessObjectForValidation) { 17. this.businessObjectForValidation = businessObjectForValidation; 18. } 19. }
This is pretty easy stuff: without the javadoc comments, a mere 19 lines of code! (Of course, we're helped by having a service do a lot of the validation for us in this particular case). We can see, too, that this class does everything we said above a Validation needs to do.
- It extends org.kuali.kfs.sys.validation.impl.GenericValidation - that means that it does indeed implement the org.kuali.kfs.validation.Validation interface.
- It overrides the public boolean validate(AttributedDocumentEvent event) method for its own unique logic - in this case, deferring to the DictionaryValidationService to do the validation
- It has a setter for the business object to be validated - in this case, the businessObjectForValidation property. As a way of differentiating properties to validate from other properties on the Validation, the suffix "ForValidation" is added to the names of these properties. We advise implementing institutions to continue this practice.
By passing values into the Validation by setters, we gain flexibility. We could conceivably cast the event argument passed into the validate method to a given event or event interface - but if we do that, then we're tied to a certain event. BusinessObjectDataDictionaryValidation, since it uses a setter, is not tied to any single event, which means it can be used to validate against practically any kind of event. Given that, the use of setting properties instead of pulling information directly from the passed event is highly recommended.
Next: let's see what the configuration looks like.
Configuring validation beans in Spring FilesÂ
Because BusinessObjectDataDictionaryValidation is used in a lot of contexts, we'll cover its configuration in a little bit. First, let's look at a simpler validation: the org.kuali.kfs.sys.validation.impl.DebitsAndCreditsBalanceValidation. This validation is part of the basic accounting document routing validation - it checks that the debits and the credits of the general ledger pending entries on the document balance each other. It takes in one validation property: accountingDocumentForValidation.
The easiest way for us to leverage existing libraries to help with the configuration of these Validations was to use Spring. Each Validation is defined as a Spring bean. Let's look at the configuration for DebitsAndCreditsBalanceValidaton, from org/kuali/kfs/sys/validation/configuration/FinancialSystemValidators.xml:
<bean id="AccountingDocument-debitsAndCreditsBalanceValidation" class="org.kuali.kfs.sys.validation.impl.DebitsAndCreditsBalanceValidation" abstract="true" />
We note that this declaration makes the bean abstract. Since these Validation beans typically hold some kind of state, they have to be declared as prototypes - thus our original bean declaration is only half of the work. We need a concrete child of the validation parent, such as the example below.
1. <bean parent="AccountingDocument-debitsAndCreditsBalanceValidation" scope="prototype"> 2. <property name="parameterProperties"> 3. <list> 4. <bean parent="validationFieldConversion"> 5. <property name="sourceEventProperty" value="document" /> 6. <property name="targetValidationProperty" value="accountingDocumentForValidation" /> 7. </bean> 8. </list> 9. </property> 10. </bean>
What's different here? The setting of the parameterProperties. GenericValidation has getters and setters for a property called "parameterProperties". This is a list of org.kuali.kfs.sys.validation.impl.ValidationFieldConvertible implementations (the generic implementation is named org.kuali.kfs.validation.ValidationFieldConversion and has been created as an abstract bean named "validationFieldConversion"). This is what we use to get properties of the event triggering the validation which populate the validation bean. In this case, we have a sourceEventProperty named document - so we expect that there's a "getDocument()" property on the event that triggers this validation. We then populate that using the targetValidationProperty, "accountingDocumentForValidation" - which we knew was the validation property. All non-abstract versions of the bean must populate the Validation class's properties like this.
Since certain validationFieldConversion configurations are used over and over again, we simplified this a bit:
<bean id="accountingDocumentFieldConversion" abstract="true" parent="validationFieldConversion"> <property name="sourceEventProperty" value="document" /> <property name="targetValidationProperty" value="accountingDocumentForValidation" /> </bean> <bean parent="AccountingDocument-debitsAndCreditsBalanceValidation" scope="prototype"> <property name="parameterProperties"> <list> <bean parent="accountingDocumentFieldConversion" /> </list> </property> </bean>
There are also preset validationFieldConversions for accountingLine to accountingLineForValidation, updatedAccountingLine to updatedAccountingLineForValidation, and others.
Note one other thing: this definition of the bean is scoped as "prototype" (ie, it is not a singleton bean). Since we set state on the bean, we need to set the scope to prototype. Spring will create a new bean every time the validation is run, and any state set on it will not affect any other beans.
Adding many validations together with CompositeValidationsÂ
In routing our accounting document, we want to validate more than whether the debits and credits balance. But, of course, we want to break the Validations so that each Validations covers only one small and distinct fragment of validation logic. That means that we need to create validation beans that are actually many validation beans in one.
CompositeValidation implements the Validation interface. Its main job is to hold a list of other validations. When validate is called on CompositeValidation, it simply walks through the Validations in its validations property, validating for each. Let's look a full example, once again declared in FinancialSystemValidators.xml
1. <bean id="AccountingDocument-RouteDocument-DefaultValidation" class="CompositeValidation" scope="prototype" abstract="true"> 2. <property name="validations"> 3. <list> 4. <bean parent="AccountingDocument-requiredAccountingLinesCountValidation" scope="prototype"> 5. <property name="accountingLineGroupName" value="source" /> 6. <property name="minimumNumber" value="1" /> 7. <property name="parameterProperties"> 8. <list> 9. <bean parent="accountingDocumentFieldConversion" /> 10. </list> 11. </property> 12. </bean> 13. <bean parent="AccountingDocument-requiredAccountingLinesCountValidation" scope="prototype"> 14. <property name="accountingLineGroupName" value="target" /> 15. <property name="minimumNumber" value="1" /> 16. <property name="parameterProperties"> 17. <list> 18. <bean parent="accountingDocumentFieldConversion" /> 19. </list> 20. </property> 21. </bean> 22. <bean parent="AccountingDocument-debitsAndCreditsBalanceValidation" scope="prototype"> 23. <property name="parameterProperties"> 24. <list> 25. <bean parent="accountingDocumentFieldConversion" /> 26. </list> 27. </property> 28. </bean> 29. </list> 30. </property> 31. </bean>
How does our composite validation work? In line 1 of the code sample, we can see that a bean is declared that uses as its parent the bean "CompositeValidation", which is a convenient declaration of org.kuali.kfs.sys.validation.impl.CompositeValidation. This class has one property set: a list of child validations, called validations (lines 2 and). In that list, we simply list the validation beans that we want validated. CompositeValidation goes through the list in the order specified in the configuration. Note, though, that CompositeValidation does not have its own parameterProperties - each sub-validation must declare its own parameterProperties as it needs.
What if, in this validation, we didn't want to check any other rulette if the first rulette failed - in this case, if the document didn't have the required number of source lines, we didn't want to validate the rulettes that checked if there were the required number of target lines or that the debits and credits balanced? In that case, we would have added one line to our configuration, like so:
1. <bean id="AccountingDocument-RouteDocument-DefaultValidation" class="CompositeValidation" scope="prototype" abstract="true"> 2. <property name="validations"> 3. <list> 4. <bean parent="AccountingDocument-requiredAccountingLinesCountValidation" scope="prototype"> 5. <property name="accountingLineGroupName" value="source" /> 6. <property name="minimumNumber" value="1" /> 7. <property name="parameterProperties"> 8. <list> 9. <bean parent="accountingDocumentFieldConversion" /> 10. </list> 11. </property> 12. <property name="quitOnFail" value="true" /> 13. </bean> . . . 31. </bean>
Line 15 specifies the property "quitOnFail". If the CompositeValidation has a Validation that has failed and which has its quitOnFail property set to true, then it quits right there, and does not perform the rest of the validations in the list.
Finally, note CompositeValidation can have any validation listed within its validation property, including other CompositeValidations any level deep of nesting needed.
Easier adding validations together with hutches
Sometimes, a number of homogeneous validations are preformed at once. For instance, whenever an accounting line is added, updated, or reviewed on a document, there's a number of "value allowed" validations which check if certain fields on the accounting line have values that aren't allowed by the specific document. Since there are several similar validations, they were added to a "hutch" class, org.kuali.kfs.sys.document.validation.impl.AccountingLineValuesAllowedValidationHutch. This class has a couple of unique features. First of all, each validation within the hutch is given a name - a hole for the validation to live in (which is why it got called a "hutch"). Secondly, the hutch only takes in the parameter property names for the properties on the event to populate. This is because the validations are considered to be heterogeneous - that is, it expects all the targetValidationProperties to be the same for all the validations in the hutch. This class provided some convenience in writing these validations, since it allows a mechanism sort of like overriding methods to work. The curious should take a look.
Validating every item in a collection with CollectionValidationsÂ
What about those cases when we want to validate every item in a collection on a document the same way? CompositeValidations are no good there, because they loop through a list of Validations - not a list of items on a document. But...no worries. Another Validation - org.kuali.kfs.sys.document.validation.impl.CollectionValidation - comes to the rescue.
Let's take an example from a new source: org/kuali/kfs/fp/document/validation/configuration/InternalBillingValidation.xml (and note where our document-for-document configurations go...more on that later):
1. <bean id="InternalBilling-routeDocumentValidation-parentBean" class="CompositeValidation" scope="prototype" abstract="true"> 2. <property name="validations"> 3. <list> 4. <bean parent="AccountingDocument-RouteDocument-DefaultValidation" scope="prototype" /> 5. <bean class="org.kuali.kfs.validation.CollectionValidation" scope="prototype"> 6. <property name="collectionProperty" value="document.items" /> 7. <property name="validations"> 8. <list> 9. <bean parent="AccountingDocument-businessObjectDataDictionaryValidation" scope="prototype"> 10. <property name="parameterProperties"> 11. <list> 12. <bean parent="validationFieldConversion"> 13. <property name="sourceEventProperty" value="iterationSubject" /> 14. <property name="targetValidationProperty" value="businessObjectForValidation" /> 15. </bean> 16. </list> 17. </property> 18. </bean> 19. </list> 20. </property> 21. </bean> 22. </list> 23. </property> 24. </bean>
Here we see lots of interesting things. First, we've got a specialized composite bean to handle the validations for Internal Billing's route document event. This event actually uses all of the default validations declared above, and so on line 4, we declare a bean that has the default validation as a parent. Then, on line 5, we add a validation - a CollectionValidation.
As we can see in lines 7 through 20, CollectionValidation acts like CompositeValidation, in that it has a list of Validation classes to validate through (CollectionValidation actually extends CompositeValidation, so these rules work precisely the same as they do in CompositeValidation). On line 6, we have an extra property though: collectionProperty. This is the property of the event to iterate over and apply all the sub-validations to, in this case the List of Items on the InternalBillingDocument.
How do we declare the property conversions for this, though? As CollectionValidation iterates through the collection, it sets a property on the passed in event named iterationSubject. When we need the current subject of the iteration over the collection to send to the validation on line 13, we specify the sourceEventProperty named "iterationSubject".
Note, though, that iterationSubject is defined only within the scope of its nearest CollectionValidation. Obviously, this means that iterationSubject will be null outside the CollectionValidation. It also means that if a CollectionValidation is nested within a parent CollectionValidation, the inner CollectonValidation has access only to the iterationSubject of its own iteration - not the iterationSubject of its parent.
Running mutually exclusive validations with BranchingValidationsÂ
There's one last specialized validation class - org.kuali.kfs.sys.document.validation.impl.BranchingValidation. This is for documents where, based on the state of the document, mutually exclusive validations must take place. A classic case is the DisbursementVoucher, where, based on whether the Payee is a Vendor or an Employee, two different sets of Validations needs to be performed. There are also several examples of BranchingValidations in Accounts Receivable's Customer Invoice document and several Purchasing/Accounts Payable documents.
org.kuali.kfs.sys.document.validation.impl.BranchingValidation has one abstract method - protected abstract String determineBranch(AttributedDocumentEvent event). Unlike CompositeValidation and CollectionValidation - neither of which should ever need to be extended - to get a BranchingValidation to work, you need to extend the class and override the determineBranch method. This method returns the name of a branch.
BranchingValidation also holds a Map property called "branchMap". This Map associates String names with a Validation bean (and that Validation bean can be, of course, a CompositeValidation). When a BranchingValidation is validated, it calls determineBranch. If determineBranch returns a null or empty String, the validation simply returns true. In any other case, the String that the method returns is used to lookup a Validation on the map, and then that Validation is validated.
Associating events with validationsÂ
Validations have been created to validate against the events occurring to our document and we've got the Spring configuration ready. How do we get our document to trigger those Validations when certain events occur? To do that, there's four easy steps:
- Create a Spring configuration file for each document, this one only for validations
- Add that configuration to the main Spring file for the module the document lives in
- Set up the validation to event map in the document's data dictionary
- Use the right rules class in the document's data dictionary
Let's look at each step in detail.
Create a Spring configuration file for each document, this one only for validations
As you can no doubt imagine the code and configuration needs for validations can get large very quickly. To keep things manageable, there's a number of best practices about where that code and configuration goes.
First, all the code for Validation belongs in the package org.kuali.kfs.{your module}.document.validation.impl We've tried to set up the practice that if a Validation exists only for one document, it will be named "{DocumentName}{WhatTheRuletteChecks}Validation". Beans are correspondingly named "{DocumentName}-{whatTheRuletteChecks}Validation" and we use the parentBean pattern pioneered by the Spring configuration for data dictionaries.
The Spring configuration for one document's validations can run up to several hundred lines, and therefore, we set up the configuration in its own Spring definition file, which exists in the directory org/kuali/kfs/{your module}/document/validation/configuration. This means that the Spring configuration file for the module as a whole tends to stay pretty clear. As a best practice, the module has a configuration where all the parent beans are created, and then each document's configuration simply tells what validations should be run for each event.
Add that configuration to the main Spring file for your module
Of course, our main Spring file still needs to have a way to access these beans. How does that work? Well, as we can see from this example taken from spring-fp.xml, it's pretty easy:
1. <!-- validations --> 2. <import resource="document/validation/configuration/FinancialProcessingValidators.xml" /> 3. <import resource="document/validation/configuration/InternalBillingValidation.xml" /> 4. <import resource="document/validation/configuration/BudgetAdjustmentValidation.xml" /> 5. <import resource="document/validation/configuration/TransferOfFundsValidation.xml" /> 6. <import resource="document/validation/configuration/AuxiliaryVoucherValidation.xml" /> 7. <import resource="document/validation/configuration/CashReceiptValidation.xml" />
Each configuration file is imported as a resource into the main Spring file. Easy, but sadly, also easy to forget....
Set up the Validation event map
We still want to associate validations with events; for instance, we want the InternalBilling-routeDocumentValidation to run when an InternalBilling document has a route event triggered against it. That's easily set up in the data dictionary. Let's check out org/kuali/kfs/fp/document/datadictionary/InternalBillingDocument.xml:
1. <bean id="InternalBillingDocument-validations" parent="InternalBillingDocument-validations-parentBean" /> 2. <bean id="InternalBillingDocument-validations-parentBean" class="org.springframework.beans.factory.config.MapFactoryBean"> 3. <property name="sourceMap"> 4. <map> 5. <entry> 6. <key><value>org.kuali.kfs.sys.document.validation.event.AttributedRouteDocumentEvent</value></key> 7. <value>InternalBilling-routeDocumentValidation</value> 8. </entry> 9. <entry> 10. <key><value>org.kuali.kfs.sys.document.validation.event.AttributedApproveDocumentEvent</value></key> 11. <value>InternalBilling-approveDocumentValidation</value> 12. </entry> 13. <entry> 14. <key><value>org.kuali.kfs.sys.document.validation.event.AttributedBlanketApproveDocumentEvent</value></key> 15. <value>InternalBilling-blanketApproveDocumentValidation</value> 16. </entry> 17. <entry> 18. <key><value>org.kuali.kfs.sys.document.validation.event.AddAccountingLineEvent</value></key> 19. <value>InternalBilling-addAccountingLineValidation</value> 20. </entry> 21. <entry> 22. <key><value>org.kuali.kfs.sys.document.validation.event.DeleteAccountingLineEvent</value></key> 23. <value>InternalBilling-deleteAccountingLineValidation</value> 24. </entry> 25. <entry> 26. <key><value>org.kuali.kfs.sys.document.validation.event.UpdateAccountingLineEvent</value></key> 27. <value>InternalBilling-updateAccountingLineValidation</value> 28. </entry> 29. <entry> 30. <key><value>org.kuali.kfs.sys.document.validation.event.ReviewAccountingLineEvent</value></key> 31. <value>InternalBilling-reviewAccountingLineValidation</value> 32. </entry> 33. </map> 34. </property> 35. </bean>
Here, we simply associate a class of an event with the name of the bean that we want validated when the event occurs. When an AttributedRouteDocumentEvent occurs to an Internal Billing document, the bean InternalBilling-routeDocumentValidation will handle the validation.
Note that we're not using <ref> declarations to access the beans, just names. This is because the data dictionary Spring context is separate from the Spring context for the rest of the application (ie, where we set up all our validation beans). Therefore, we simply specify the name; the rules engine will get the actual Spring bean for the Validation from the main application SpringContext.
Second, note that we're using "AttributedRouteDocumentEvent" - not org.kuali.kfs.sys.document.validation.event.AttributedRouteDocumentEvent, which is what really gets thrown by KNS. What's up with that? Well, RouteDocumentEvent demands a different interface than the simple interface needed by our rules class (which we'll get to presently). While accounting document centric events in KFS will eventually just expect to call their rule against the AccountingRuleEngineRuleBase class, the KNS ones will never move. This has two interesting side effects. First: we've got to map our validations to the wrapped event, ie AttributedRouteDocumentEvent. Also, all the validation that KNS does for documents will always occur anyway - we can't prevent it from happening. That's typically a good thing, though - it would be really atrocious if we had to redeclare all of KNS's document validations within our configuration too!
We do not need to specify a Validation to respond to every event that could happen on a document. Several documents, for instance, do no Validations when a SaveDocumentEvent occurs - and therefore, they do not have an associated Validation. When the document is being validated and the rules engine can't find the event in the specified event map, then it assumes that validation passes for that and returns true.
Finally, there's two different validations for ApproveDocumentEvent and BlanketApproveDocumentEvent. However, as we all know, BlanketApproveDocumentEvent extends ApproveDocumentEvent - doesn't that mean that ApproveDocumentEvent validations will get automatically approved whenever BlanketApproveDocumentEvents get thrown? Not with the Accouting Documents validation. The rules engine looks for precisely the event class in the map which it is validating for; it doesn't try to figure out inheritance rules for those classes. Therefore, if a validation needs to run for both ApproveDocumentEvents and BlanketApproveDocumentEvents, it must be configured for both events.
One final line of code - we need to tell our document's data dictionary definition that this is its rule map. Once again, we consult org/kuali/kfs/fp/datadictionary/docs/InternalBillingDocument.xml:
1. <bean id="InternalBillingDocument-parentBean" abstract="true" parent="TransactionalDocumentEntry"> 2. <property name="label" value="Internal Billing" /> 3. <property name="documentTypeName" value="InternalBillingDocument" /> 4. <property name="documentClass" value="org.kuali.module.financial.document.InternalBillingDocument" /> 5. <property name="businessRulesClass" value="org.kuali.kfs.sys.document.validation.impl.AccountingRuleEngineRuleBase" /> . . . 6. <property name="validationMap" ref="InternalBillingDocument-validations" /> 7. </bean>
On line 6, you'll see that we set the validationMap property on the TransactionalDocumentEntry for the InternalBillingDocument-parent to be the map that we just declared.
Use the right rules class
We actually already did that: check out line 5 of the definition above. Our rules class for every document using this engine will be org.kuali.kfs.sys.document.validation.impl.AccountingRuleEngineRuleBase.
Permissions and AccountingLineAuthorizers
Allowing editing of accounting lines occurs in two places: in KIM permissions and in special AccountingLineAuthorizers, which take the place of DocumentPresentationControllers, but only at the accounting line level.
KIM Permissions for accounting lines
Whole lines or specific fields of lines can be made editable for members of certain roles using permissions templated from the KFS-SYS Modify Accounting Lines permission. Each Modify Accounting Lines permission requires three attribute records, one to specify what the propertyName of the accounting line is, one of the document type of the affected document (and document type inheritance is considered here), and finally the route level that the accounting lines are editable at. For instance, there's a KFS-FP Accounting Lines Permission (standard permission 238) which has as its property "sourceAccountingLines", the document type as "FP" and the route level as "PreRoute" - a permission which allows editing for any field on any FP document before it has been routed. There is a similar permission, standard permission 190, which has the route level as "Account" - roles for this permission are set by default to only let KFS-SYS Fiscal Officers the ability to modify accounting lines at this level. The name of the accounting line collection, such as "sourceAccountingLines" or "items.sourceAccountingLines" allows all fields in an accounting line to be edited. One can also specify a property of, for instance, "sourceAccountingLines.amount" which would only let the amount field be edited at a certain route level.
AccountingLineAuthorizers
AccountingLineAuthorizers do the KIM checks described above, but they also allow for:
- setting blocks on a line as unviewable (so those blocks will not be rendered)
- which actions appear on a given line
- whether to render a new accounting line for an accounting line group or not
- whether the entirety of every field on every accounting line in an entire group is editable
If extending org.kuali.kfs.sys.document.authorization.AccountingLineAuthorizerBase - which is the suggested practice - there are two other methods which allow "presentation controller" type logic for whether a whole accounting line or a field on an accounting line are editable (determineEditPermissionOnLine and determineEditPermissionByFieldName).
AccountingLineAuthorizer implementations are set in the data dictionary entry for a document's accounting line group, as we'll see below.
Putting an Accounting Document on the Web
Obviously, our accounting document needs to go on the web, and once again, it works just like the entire document processing framework in the rest of Kuali. We've got actions and forms, and the accounting document framework adds some actions (such as adding accounting lines, deleting them, and so forth) and changes the form in interesting ways; our accounting documents will simply extend the form (org.kuali.kfs.web.struts.form.KualiAccountingDocumentFormBase) and the action class (org.kuali.kfs.web.struts.action.KualiAccountingDocumentActionBase) as necessary. Let's look, then, at how the accounting framework extends the document framework in the web layer.
KualiAccountingDocumentFormBase
The form for accounting documents holds some very obvious data, and it also does some fairly non-obvious things. Let's take a tour of how KualiAccountingDocumentFormBase extends the form base.
- Since the view layer needs to be able to get information created by the document authorizer for our accounting documents - what accounts are editable for the current user and so on - so those Maps are actually generated by the action and set in the form before the form is passed to the view layer.
- The form also includes a "newSourceAccountingLine" and "newTargetAccountingLine", since new source and target accounting lines have not been added yet to the document (naturally, just as with any form in KFS, the accounting document is already encapsulated in the form), and there are getters and setters associated with these properties.
- There are methods to "populate" accounting lines. These methods set the primary keys for child objects within accounting lines. For instance, let's say that we've entered an accounting line with a sub-object code. Sub-object code BO's have an extensive primary key - not only the sub-object code, but also the parent object code, the chart of the parent object code, and the fiscal year that the sub-object code is valid for. All of these can be derived from the document or the accounting line, and the populate methods fill out all of that information on the sub-object BO before it is saved, so that the O/RM layer can correctly generate the relationship between the accounting line and the BO in the database.
- If source or target accounting lines were uploaded for the document, which was discussed in the section on accounting line parsers, the form needs to hold on to the uploaded forms.
- The form contains accounting line decorators. These are very simple objects that tell the user interface whether or not the given accounting line is revertible or not. This based on whether there is a previous version of the accounting line - a "baseline accounting line" - which is different from the current version of the accounting line that the user can edit. For instance, when we first add an accounting line to a document, it's not revertible - there's no previous version. But then if we change the account on that added accounting line, we've got a previous version, and we can revert to it. Therefore, the decorator for that accounting line has its "revertible" property set to true. Every accounting line has one.
KualiAccountingDocumentActionBase
KualiAccountingDocumentActionBase extends the KualiTransactionalDocumentActionBase to allow users to actually act on the accounting lines in an accounting document. Most of the methods look pretty familiar by now - we've met some of them through the events that were added for the rule framework, and some of them through the accounting line parsers and baseline accounting lines. For most of the new action methods we've got, there's a version for source lines and target lines, both of which end up calling a central method which does all the work. For instance, we see that there's a revertSourceLine and revertTargetLine, both of which do some processing and then call revertAccountingLine. We'll concentrate on the "accounting line" methods.
- insertAccountingLine is called when we add a new accounting line to the document. It validates the accounting line, and then creates corresponding baseline accounting lines and accounting line decorators (though the line is obviously not revertible at this point).
- deleteAccountingLine is called when an old accounting line is removed from a document. It validates the removal and if that works, it removes the baseline accounting line and the decorator.
- revertAccountingLine is called when the user reverts an updated accounting line to the baseline version of the line. No validation occurs at this point, though if the baseline accounting line needed sales tax associated with it, then those fields will be shown.
- uploadAccountingLines is called when a new file of accounting lines are uploaded into the document. It parsers the file using an accounting line parser, and then actually defers to insertAccountingLine to put the accounting lines into the file
- performBalanceInquiryForAccountingLine is called when a user checks the balance on an accounting line. This allows users to conveniently check the balances on the accounts given in the accounting line.
- hideDetails and showDetails hide or show the accounting lines on the form.
There are two other areas of responsibility for the action: figuring out if the account entered on an accounting line needs to be overridden and figuring out if the accounting line is subject to sales tax.
Sales Tax
Some accounts are also subject to sales tax. When a user enters a line subject to sales tax, the accounting line will return with extra fields to fill in for sales tax information.
Two parameters control whether an accounting line is subject to sales tax: KFS-FP / Document / SALES_TAX_APPLICABLE_DOCUMENT_TYPES which is a list of document types that sales tax will be applied to, and KFS-FP / Document / SALES_TAX_APPLICABLE_ACCOUNTS_AND_OBJECT_CODES, which lists the account numbers and object codes of lines that should have sales tax applied to them.
For instance, let's say that we're creating a Disbursement of Income and Expense document using the account BL-9612706 and object code 9015, and that these combinations are listed in the parameters above. When we try to add this accounting line, the line will pop back, requiring sales tax information. Those fields are the chart and account of the account to take the sales tax from, the gross taxable amount, the taxable sales amount, and the sales date. Given this information, when general ledger pending entries are generated, they'll calculate the appropriate sales tax and generate extra entries for that.
Most of the logic for determining what lines need sales tax and how to show those lines exists in KualiAccountingDocumentActionBase, but because the major logic - which accounting lines should have sales tax associated with them - is set in parameters, very few accounting documents are going to have to override any of those methods.
Accounting Line tags
The accounting lines are rendered using a system based on the framework which renders maintenance documents. This makes changing rendering for accounting lines incredibly simple. To create a custom rendering for accounting lines, we simply describe the rendering in the data dictionary and then add the right tags to the JSP page for the accounting document.
Transactional documents in KFS now have a data dictionary property called accountingLineGroups and we can see from InternalBillingDocument.xml:
1. <bean id="InternalBillingDocument" parent="InternalBillingDocument-parentBean" /> 2. <bean id="InternalBillingDocument-parentBean" abstract="true" parent="AccountingDocumentEntry"> . . . 3. <property name="accountingLineGroups"> 4. <map> 5. <entry> 6. <key><value>source</value></key> 7. <ref bean="InternalBilling-sourceAccountingLineGroup" parent="AccountingLineGroup" /> 8. </entry> 9. <entry> 10. <key><value>target</value></key> 11. <ref bean="InternalBilling-targetAccountingLineGroup" parent="AccountingLineGroup" /> 12. </entry> 13. </map> 14. </property> 15. </bean>
The accountingLineGroups property takes in a map that matches String keys to AccountingLineGroup beans. What do these AccountingLineGroup beans look like? Let's take a look at InternalBillingDocument.xml's InternalBilling-sourceAccountingLineGroup bean:
1. <bean id="InternalBilling-sourceAccountingLineGroup" parent="InternalBilling-sourceAccountingLineGroup-parentBean" /> 2. <bean id="InternalBilling-sourceAccountingLineGroup-parentBean" parent="AccountingLineGroup" abstract="true"> 3. <property name="accountingLineView" ref="FinancialProcessing-SalesTax-accountingLineView" /> 4. <property name="accountingLineClass" value="org.kuali.kfs.sys.businessobject.SourceAccountingLine" /> 5. <property name="groupLabel" value="Income" /> 6. <property name="accountingLineAuthorizerClass" value="org.kuali.kfs.fp.document.authorization.FinancialProcessingAccountingLineAuthorizer" /> 7. <property name="importedLinePropertyPrefix" value="source" /> 8. <property name="totals" ref="AccountingDocument-sourceGroupTotals" /> 9. <property name="errorKey" value="document.sourceAccounting*,sourceAccountingLines,newSourceLine*" /> 10. </bean>
Let's break this down, one line at a time.
On line 2, we see that these beans are going to have, as a parent bean, the AccountingLineGroup bean.
Line is really the important thing: the accountingLineView bean. This is the bean that specifies how the accounting line is going to look. We'll examine this in detail, as soon as we finish looking through the rest of this definition.
On line 4, we specify the accounting line class for accounting lines within this group. This can be controlled on the document itself, but the default implementation uses the data dictionary, making it much easier to customize.
Line 5 allows us to set the label for the group. Again, this used to be specified on the document itself but now we've got a lot more flexibility.
Line 6 specifies the AccountingLineAuthorizer for this group. The org.kuali.kfs.sys.document.authorization.AccountingLineAuthorizer interface specifies methods that authorize "blocks" within accounting lines. We'll take a much closer look at these in the reference section.
Line 7 specifies the importLinePropertyPrefix. This basically specifies that imported accounting lines will be moved into the property "document.sourceAccountingLines". If the importLinePropertyPrefix is not specified, then there will be no button rendered to allow for the importing of accounting lines.
Line 8 specifies how totals are supposed to look for this group. AccountingDocument-sourceGroupTotals will display a currency-formatted total for the sourceAccountingLine collection of the document; there's a similar bean for targetGroupTotals. It should be noted though that these totals are very flexible - multiple totals can be specified and creating new totals is pretty simple.
Finally in Line 9, we specify the error keys that this accounting line group should catch and display errors for.
The next step is how the accounting line view has been rendered. Here's the definition of the bean FinancialProcessing-SalesTax-accountingLineView, which can be found in FinancialProcessingAccountingDocumentDefinitions.xml:
1. <bean id="FinancialProcessing-SalesTax-accountingLineView" parent="FinancialProcessing-SalesTax-accountingLineView-parent" /> 2. <bean id="FinancialProcessing-SalesTax-accountingLineView-parent" parent="AccountingLineView" abstract="true"> 3. <property name="elements"> 4. <list> 5. <bean parent="AccountingLineView-sequenceNumber" /> 6. <bean parent="AccountingLineView-lines"> 7. <property name="lines"> 8. <list> 9. <bean parent="AccountingLineView-line"> 10. <property name="elementName" value="accountingInformation" /> 11. <property name="fields"> 12. <list> 13. <bean parent="AccountingLineView-field" p:name="postingYear" p:hidden="true" /> 14. <bean parent="AccountingLineView-field" p:name="overrideCode" p:hidden="true" /> 15. <bean parent="AccountingLineView-field" p:name="versionNumber" p:hidden="true" /> 16. <bean parent="AccountingLineView-field" p:name="objectId" p:hidden="true" /> 17. <bean parent="AccountingLineView-field" p:name="documentNumber" p:hidden="true" /> 18. <bean parent="AccountingLineView-field" p:name="chartOfAccountsCode" p:required="true" p:webUILeaveFieldFunction="loadChartInfo" p:dynamicLabelProperty="chart.finChartOfAccountDescription" /> 19. <bean parent="AccountingLineView-field" p:name="accountNumber" p:required="true" p:webUILeaveFieldFunction="loadAccountInfo" p:dynamicLabelProperty="account.accountName"> 20. <property name="overrideFields"> 21. <list> 22. <bean parent="AccountingLineView-overrideField" p:name="accountExpiredOverride" /> 23. <bean parent="AccountingLineView-overrideField" p:name="nonFringeAccountOverride" /> 24. </list> 25. </property> 26. </bean> 27. <bean parent="AccountingLineView-field" p:name="subAccountNumber" p:webUILeaveFieldFunction="loadSubAccountInfo" p:dynamicLabelProperty="subAccount.subAccountName" /> 28. <bean parent="AccountingLineView-field" p:name="financialObjectCode" p:required="true" p:dynamicNameLabelGeneratorBeanName="objectCodeDynamicNameLabelGenerator"> 29. <property name="overrideFields"> 30. <list> 31. <bean parent="AccountingLineView-overrideField" p:name="objectBudgetOverride" /> 32. </list> 33. </property> 34. </bean> 35. <bean parent="AccountingLineView-field" p:name="financialSubObjectCode" p:webUILeaveFieldFunction="loadSubObjectInfo" p:dynamicLabelProperty="subObjectCode.financialSubObjectCodeName" /> 36. <bean parent="AccountingLineView-field" p:name="projectCode" p:webUILeaveFieldFunction="loadProjectInfo" p:dynamicLabelProperty="project.projectDescription" /> 37. <bean parent="AccountingLineView-field" p:name="organizationReferenceId" /> 38. </list> 39. </property> 40. </bean> 41. <bean parent="AccountingLineView-line"> 42. <property name="elementName" value="salesTaxInformation" /> 43. <property name="fields"> 44. <list> 45. <bean parent="AccountingLineView-field" p:name="salesTax.versionNumber" p:hidden="true" /> 46. <bean parent="AccountingLineView-field" p:name="salesTax.objectId" p:hidden="true" /> 47. <bean parent="AccountingLineView-field" p:name="salesTax.chartOfAccountsCode" p:required="true" p:webUILeaveFieldFunction="updateChartName" p:dynamicLabelProperty="salesTax.chartOfAccounts.finChartOfAccountDescription" /> 48. <bean parent="AccountingLineView-field" p:name="salesTax.accountNumber" p:required="true" p:webUILeaveFieldFunction="updateAccountNumber" p:dynamicLabelProperty="salesTax.account.accountName" /> 49. <bean parent="AccountingLineView-field" p:name="salesTax.financialDocumentGrossSalesAmount" p:required="true" /> 50. <bean parent="AccountingLineView-field" p:name="salesTax.financialDocumentTaxableSalesAmount" p:required="true" /> 51. <bean parent="AccountingLineView-field" p:name="salesTax.financialDocumentSaleDate" p:required="true" /> 52. </list> 53. </property> 54. </bean> 55. </list> 56. </property> 58. </bean> 59. <bean parent="AccountingLineView-field" p:name="amount" p:required="true" /> 60. <bean parent="AccountingLineView-actions" /> 61. </list> 62. </property> 63. </bean>
Layout Elements
Layout in maintenance docs is simple - in every sense of the word. It's easy to do and it is as well primitive: headers in one column, fields in the other. In the first two releases of KFS, on the other hand, accounting lines has total flexibility. Changing rendering to something more sophisticated required merely changing the tags that accomplished the rendering. Sadly, with the new accounting line engine, some of that flexibility had to be sacrificed. However, layoutElements still provide a lot of flexibility with how accounting lines get rendered.
There are three basic layout elements that cover most of layout operations: the lines element, the line element, and the field element. There are also some less used layout elements, one of which we will cover here: the show/hide block. All of these elements can be named (for instance, in the example above, where elementName is set to "accountingInformation"). Their job is to figure out how to join the table that will be rendered to HTML.
Lines elements contain "line filling" elements - typically, that's a line element, though it can also be a show/hide block element. A line joins a table by making sure that the table will have enough rows for its child lines and then adding each line to its own row.
Line elements contain fields. These elements won't ever take more than two table rows (one for the header, one for the input cells) and it puts each child field into its own table cell.
The behavior of fields is dynamically chosen. If a field is a child of a line element, they'll fit into their own table cells. In the example above, chartOfAccountsCode and accountNumber each fit into one table cell (plus one table cell for the header). However...there are some fields that are not children of line elements, such as the amount field, AccountingLineView-actions and AccountingLineView-sequenceNumber (both AccountingLineView-actions and AccountingLineView-sequenceNumber act like fields when joining tables). If a field layout element is sibling to a lines layout element, it will take up one row for the header and then span the rest of the rows taken up by the lines layout element.
Finally, the show/hide layout element (AccountingLineView-showHideLines) has to be a child of a lines layout element. It, in turn, acts like a lines layout element in that it has children who are lines. These rows will be rendered inside a div with a button that allows the showing or hiding of the div. The show/hide layout element will span all cols for the given line.
Override fields
Fields, as we noticed, are really the bread and butter of what we're attempting to render; just like in maintenance documents, fields are what the user enters data into. In fact, all accounting line view data dictionary field definitions extend maintenance document field definitions so a lot of the same functionality - default value finders, for instance - are available in this framework; the only major exception to this is field masking. Also, certain fields in accounting lines are a bit more complex than their maintenance document counterparts. For an example, let's look at lines 19-26 of our second example again:
19. <bean parent="AccountingLineView-field" p:name="accountNumber" p:required="true" p:webUILeaveFieldFunction="loadAccountInfo" p:dynamicLabelProperty="account.accountName"> 20. <property name="overrideFields"> 21. <list> 22. <bean parent="AccountingLineView-overrideField" p:name="accountExpiredOverride" /> 23. <bean parent="AccountingLineView-overrideField" p:name="nonFringeAccountOverride" /> 24. </list> 25. </property> 26. </bean>
The accountNumber field has two override fields. Override fields get turned on by logic (found in KualiAccountingDocumentActionBase); for instance, if an account is expired, then the Action would figure that out and show a checkbox that allows the user to override the fact that the account is expired and use the account on the line anyway. These override fields are simply added to the overrideFields property of the field where the checkbox should show up.
Extra field properties
As we noted, AccountingLineView-field extends MaintenanceField and therefore, a lot of functionality is gained from the Maintenance Document framework. However, there are a couple new properties that were added to fields, so let's take a look at those:
- overrideFields: As seen above, these fields allow the value in the field to override the fact that, for some reason or another, it normally wouldn't be used.
- hidden: true or false, this forces the field to be rendered as a hidden value
- dynamicLabelProperty: the maintenance field definition includes two properties, webUILeaveFieldFunction and webUILeaveFieldCallbackFunction, which allow labels to be added through AJAX - ie, once a user enters the account number, the name of the account shows up. That's also available for AccountingLineView-field but one of two extra properties must be added: either dynamicLabelProperty or dynamicNameLabelGeneratorBeanName. The dynamicLabelProperty property works withe webUILeaveFieldFunction and allows the setting of what field should be displayed as the dynamic label (account.name, for instance, is the label for accountNumber).
- dynamicNameLabelGeneratorBeanName: some AJAX functions - notably Object Code's - cannot be called through simple webUILeaveFieldFunctions with dynamicLabelProperty properties set. For this instance, one simply specifies the name of an implementation of org.kuali.kfs.sys.document.service.DynamicNameLabelGenerator exposed through Spring. This interface provides all the methods needed so the accounting line renderer can construct the AJAX call properly; webUILeaveFieldFunction, webUILeaveFieldFunctionCallback, and dynamicLabelProperty are no longer needed.
Using the accounting lines from the JSP
Finally, let's take a look at InternalBilling.jsp to see what changes we made there to use the accounting line group:
1. <kul:tab tabTitle="Accounting Lines" defaultOpen="true" tabErrorKey="${KFSConstants.ACCOUNTING_LINE_ERRORS}"> 2. <sys:accountingLines> 3. <sys:accountingLineGroup newLinePropertyName="newSourceLine" collectionPropertyName="document.sourceAccountingLines" collectionItemPropertyName="document.sourceAccountingLine" attributeGroupName="source" /> 4. <sys:accountingLineGroup newLinePropertyName="newTargetLine" collectionPropertyName="document.targetAccountingLines" collectionItemPropertyName="document.targetAccountingLine" attributeGroupName="target"/> 5. </sys:accountingLines> 6. </kul:tab>
As a first note, unlike the fin:accountingLines tag, the sys:accountingLines tag does not draw the tab for the accounting lines - that has to be done separately.
On line 2, we see the <sys:accountingLines> tag - it takes in no properties and acts mostly to group together all of the accountingLineGroups into a visual cohesive whole.
On lines and 4, we see that as children to <sys:accountingLines>, we've got <sys:accountingLineGroup> tags. These are the tags which force an accounting line group to be rendered. There are four attributes we need to concern ourselves about here:
- newLinePropertyName - the property of the form where data for a new accounting line goes. If not specified, the group tag will not render any new lines.
- collectionPropertyName - the property of the form where data for existing (on the document) accounting lines goes. This is a required property.
- collectionItemPropertyName - the property of the form to find accounting lines one at a time - for instance, in this case, the 4th existing accounting line for the source group will be sought at form property document.sourceAccountingLine3. This isn't required; if not present, the tag will attempt to figure out the name of the single collection instance itself by removing the final "s" from the collectionPropertyName.
- attributeGroupName - finally, we've eaten our tail like an ourobous! This specifies what the key of the group bean definition in the data dictionary that we should be using to render this group.
We can specify as many accounting line groups as we want - though the <sys:accountingLines> tag generally won't produce favorable results if no child groups are rendered.
Conclusion
As we've seen, to build all of the various documents with accounting lines in KFS, a large amount of highly reusable and customizable functionality was created, and as we create new accounting documents, we can easily build on top of the model and functionality KFS already has. This makes a lot of sense: KFS was really built to support accounting documents. We've seen how we can build documents, create rules and document authorizers, put together the proper data dictionary, customize the controller and view layers, and finally create the right workflow route for accounting documents. The ability to put this all together with a minimum of big changes means that the accounting document framework provides KFS with a lot of power for each of our institutions.
Kuali documentation is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 2.5 License.
Kuali software is licensed for use pursuant to the Affero General Public License, version 3.
Copyright © 2014 Kuali, Inc. All rights reserved.
Portions of Kuali are copyrighted by other parties as described in the Acknowledgments screen.
Kuali ® is a registered trademark of the Trustees of Indiana University.