001    /*
002     * Copyright 2011 The Kuali Foundation.
003     * 
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     * 
008     * http://www.opensource.org/licenses/ecl2.php
009     * 
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.kfs.coa.document.validation.impl;
017    
018    import java.util.Collection;
019    import java.util.HashMap;
020    import java.util.Map;
021    
022    import org.apache.commons.lang.StringUtils;
023    import org.kuali.kfs.coa.businessobject.A21SubAccount;
024    import org.kuali.kfs.coa.businessobject.IndirectCostRecoveryRateDetail;
025    import org.kuali.kfs.coa.businessobject.SubAccount;
026    import org.kuali.kfs.coa.service.SubFundGroupService;
027    import org.kuali.kfs.sys.KFSConstants;
028    import org.kuali.kfs.sys.KFSKeyConstants;
029    import org.kuali.kfs.sys.KFSPropertyConstants;
030    import org.kuali.kfs.sys.context.SpringContext;
031    import org.kuali.kfs.sys.service.UniversityDateService;
032    import org.kuali.rice.kns.document.MaintenanceDocument;
033    import org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase;
034    import org.kuali.rice.kns.service.DataDictionaryService;
035    import org.kuali.rice.kns.util.ObjectUtils;
036    
037    /**
038     * This class implements the business rules specific to the {@link SubAccount} Maintenance Document.
039     */
040    public class SubAccountRule extends MaintenanceDocumentRuleBase {
041    
042        protected static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(SubAccountRule.class);
043    
044        protected SubAccount oldSubAccount;
045        protected SubAccount newSubAccount;
046    
047        /**
048         * This performs rules checks on document approve
049         * <ul>
050         * <li>{@link SubAccountRule#setCgAuthorized(boolean)}</li>
051         * <li>{@link SubAccountRule#checkForPartiallyEnteredReportingFields()}</li>
052         * <li>{@link SubAccountRule#checkCgRules(MaintenanceDocument)}</li>
053         * </ul>
054         * This rule fails on business rule failures
055         * 
056         * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomApproveDocumentBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument)
057         */
058        protected boolean processCustomApproveDocumentBusinessRules(MaintenanceDocument document) {
059            LOG.info("Entering processCustomApproveDocumentBusinessRules()");
060    
061            // check that all sub-objects whose keys are specified have matching objects in the db
062            boolean success = checkForPartiallyEnteredReportingFields();
063    
064            // process CG rules if appropriate
065            success &= checkCgRules(document);
066    
067            return success;
068        }
069    
070        /**
071         * This performs rules checks on document route
072         * <ul>
073         * <li>{@link SubAccountRule#setCgAuthorized(boolean)}</li>
074         * <li>{@link SubAccountRule#checkForPartiallyEnteredReportingFields()}</li>
075         * <li>{@link SubAccountRule#checkCgRules(MaintenanceDocument)}</li>
076         * </ul>
077         * This rule fails on business rule failures
078         * 
079         * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomRouteDocumentBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument)
080         */
081        protected boolean processCustomRouteDocumentBusinessRules(MaintenanceDocument document) {
082            LOG.info("Entering processCustomRouteDocumentBusinessRules()");
083    
084            boolean success = true;
085    
086            // check that all sub-objects whose keys are specified have matching objects in the db
087            success &= checkForPartiallyEnteredReportingFields();
088    
089            // process CG rules if appropriate
090            success &= checkCgRules(document);
091    
092            return success;
093        }
094    
095        /**
096         * This performs rules checks on document save
097         * <ul>
098         * <li>{@link SubAccountRule#setCgAuthorized(boolean)}</li>
099         * <li>{@link SubAccountRule#checkForPartiallyEnteredReportingFields()}</li>
100         * <li>{@link SubAccountRule#checkCgRules(MaintenanceDocument)}</li>
101         * </ul>
102         * This rule does not fail on business rule failures
103         * 
104         * @see org.kuali.rice.kns.maintenance.rules.MaintenanceDocumentRuleBase#processCustomSaveDocumentBusinessRules(org.kuali.rice.kns.document.MaintenanceDocument)
105         */
106        protected boolean processCustomSaveDocumentBusinessRules(MaintenanceDocument document) {
107    
108            boolean success = true;
109    
110            LOG.info("Entering processCustomSaveDocumentBusinessRules()");
111    
112            // check that all sub-objects whose keys are specified have matching objects in the db
113            success &= checkForPartiallyEnteredReportingFields();
114    
115            // process CG rules if appropriate
116            success &= checkCgRules(document);
117    
118            return success;
119        }
120    
121        /**
122         * This method sets the convenience objects like newAccount and oldAccount, so you have short and easy handles to the new and
123         * old objects contained in the maintenance document. It also calls the BusinessObjectBase.refresh(), which will attempt to load
124         * all sub-objects from the DB by their primary keys, if available.
125         * 
126         * @param document - the maintenanceDocument being evaluated
127         */
128        public void setupConvenienceObjects() {
129    
130            // setup oldAccount convenience objects, make sure all possible sub-objects are populated
131            oldSubAccount = (SubAccount) super.getOldBo();
132    
133            // setup newAccount convenience objects, make sure all possible sub-objects are populated
134            newSubAccount = (SubAccount) super.getNewBo();
135        }
136    
137        /**
138         * This checks that the reporting fields are entered altogether or none at all
139         * 
140         * @return false if only one reporting field filled out and not all of them, true otherwise
141         */
142        protected boolean checkForPartiallyEnteredReportingFields() {
143    
144            LOG.info("Entering checkExistenceAndActive()");
145    
146            boolean success = true;
147            boolean allReportingFieldsEntered = false;
148            boolean anyReportingFieldsEntered = false;
149    
150            // set a flag if all three reporting fields are filled (this is separated just for readability)
151            if (StringUtils.isNotEmpty(newSubAccount.getFinancialReportChartCode()) && StringUtils.isNotEmpty(newSubAccount.getFinReportOrganizationCode()) && StringUtils.isNotEmpty(newSubAccount.getFinancialReportingCode())) {
152                allReportingFieldsEntered = true;
153            }
154    
155            // set a flag if any of the three reporting fields are filled (this is separated just for readability)
156            if (StringUtils.isNotEmpty(newSubAccount.getFinancialReportChartCode()) || StringUtils.isNotEmpty(newSubAccount.getFinReportOrganizationCode()) || StringUtils.isNotEmpty(newSubAccount.getFinancialReportingCode())) {
157                anyReportingFieldsEntered = true;
158            }
159    
160            // if any of the three reporting code fields are filled out, all three must be, or none
161            // if any of the three are entered
162            if (anyReportingFieldsEntered && !allReportingFieldsEntered) {
163                putGlobalError(KFSKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_RPTCODE_ALL_FIELDS_IF_ANY_FIELDS);
164                success &= false;
165            }
166    
167            return success;
168        }
169    
170        /**
171         * This checks to make sure that if cgAuthorized is false it succeeds immediately, otherwise it checks that all the information
172         * for CG is correctly entered and identified including:
173         * <ul>
174         * <li>If the {@link SubFundGroup} isn't for Contracts and Grants then check to make sure that the cost share and ICR fields are
175         * not empty</li>
176         * <li>If it isn't a child of CG, then the SubAccount must be of type ICR</li>
177         * </ul>
178         * 
179         * @param document
180         * @return true if the user is not authorized to change CG fields, otherwise it checks the above conditions
181         */
182        protected boolean checkCgRules(MaintenanceDocument document) {
183    
184            boolean success = true;
185    
186            // short circuit if the parent account is NOT part of a CG fund group
187            boolean a21SubAccountRefreshed = false;
188            if (ObjectUtils.isNotNull(newSubAccount.getAccount())) {
189                if (ObjectUtils.isNotNull(newSubAccount.getAccount().getSubFundGroup())) {
190    
191                    // compare them, exit if the account isn't for contracts and grants
192                    if (!SpringContext.getBean(SubFundGroupService.class).isForContractsAndGrants(newSubAccount.getAccount().getSubFundGroup())) {
193    
194                        // KULCOA-1116 - Check if CG CS and CG ICR are empty, if not throw an error
195                        if (checkCgCostSharingIsEmpty() == false) {
196                            putFieldError("a21SubAccount.costShareChartOfAccountCode", KFSKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_NON_FUNDED_ACCT_CS_INVALID, new String[] { SpringContext.getBean(SubFundGroupService.class).getContractsAndGrantsDenotingAttributeLabel(), SpringContext.getBean(SubFundGroupService.class).getContractsAndGrantsDenotingValueForMessage() });
197                            success = false;
198                        }
199    
200                        if (checkCgIcrIsEmpty() == false) {
201                            putFieldError("a21SubAccount.indirectCostRecoveryTypeCode", KFSKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_NON_FUNDED_ACCT_ICR_INVALID, new String[] { SpringContext.getBean(SubFundGroupService.class).getContractsAndGrantsDenotingAttributeLabel(), SpringContext.getBean(SubFundGroupService.class).getContractsAndGrantsDenotingValueForMessage() });
202                            success = false;
203                        }
204    
205                        // KULRNE-4660 - this isn't the child of a CG account; sub account must be ICR type
206                        if (!ObjectUtils.isNull(newSubAccount.getA21SubAccount())) {
207                            // KFSMI-798 - refresh() changed to refreshNonUpdateableReferences()
208                            // All references for A21SubAccount are non-updatable
209                            newSubAccount.getA21SubAccount().refreshNonUpdateableReferences();
210                            a21SubAccountRefreshed = true;
211                            if (StringUtils.isEmpty(newSubAccount.getA21SubAccount().getSubAccountTypeCode()) || !newSubAccount.getA21SubAccount().getSubAccountTypeCode().equals(KFSConstants.SubAccountType.EXPENSE)) {
212                                putFieldError("a21SubAccount.subAccountTypeCode", KFSKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_NON_FUNDED_ACCT_SUB_ACCT_TYPE_CODE_INVALID, new String[] { SpringContext.getBean(SubFundGroupService.class).getContractsAndGrantsDenotingAttributeLabel(), SpringContext.getBean(SubFundGroupService.class).getContractsAndGrantsDenotingValueForMessage() });
213                                success = false;
214                            }
215                        }
216    
217                        return success;
218                    }
219                }
220            }
221    
222            // short circuit if there is no A21SubAccount object at all (ie, null)
223            if (ObjectUtils.isNull(newSubAccount.getA21SubAccount())) {
224                return success;
225            }
226    
227            // FROM HERE ON IN WE CAN ASSUME THERE IS A VALID A21 SUBACCOUNT OBJECT
228    
229            // manually refresh the a21SubAccount object, as it wont have been
230            // refreshed by the parent, as its updateable
231            // though only refresh if we didn't refresh in the checks above
232            if (!a21SubAccountRefreshed) {
233                newSubAccount.getA21SubAccount().refresh();
234            }
235    
236            // C&G A21 Type field must be in the allowed values
237            if (!KFSConstants.SubAccountType.ELIGIBLE_SUB_ACCOUNT_TYPE_CODES.contains(newSubAccount.getA21SubAccount().getSubAccountTypeCode())) {
238                putFieldError("a21SubAccount.subAccountTypeCode", KFSKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_INVALI_SUBACCOUNT_TYPE_CODES, KFSConstants.SubAccountType.ELIGIBLE_SUB_ACCOUNT_TYPE_CODES.toString());
239                success &= false;
240            }
241    
242            // get a convenience reference to this code
243            String cgA21TypeCode = newSubAccount.getA21SubAccount().getSubAccountTypeCode();
244    
245            // if this is a Cost Sharing SubAccount, run the Cost Sharing rules
246            if (KFSConstants.SubAccountType.COST_SHARE.trim().equalsIgnoreCase(StringUtils.trim(cgA21TypeCode))) {
247                success &= checkCgCostSharingRules();
248            }
249    
250            // if this is an ICR subaccount, run the ICR rules
251            if (KFSConstants.SubAccountType.EXPENSE.trim().equals(StringUtils.trim(cgA21TypeCode))) {
252                success &= checkCgIcrRules();
253            }
254    
255            return success;
256        }
257    
258        /**
259         * This checks that if the cost share information is filled out that it is valid and exists, or if fields are missing (such as
260         * the chart of accounts code and account number) an error is recorded
261         * 
262         * @return true if all cost share fields filled out correctly, false if the chart of accounts code and account number for cost
263         *         share are missing
264         */
265        protected boolean checkCgCostSharingRules() {
266    
267            boolean success = true;
268            boolean allFieldsSet = false;
269    
270            A21SubAccount a21 = newSubAccount.getA21SubAccount();
271    
272            // check to see if all required fields are set
273            if (StringUtils.isNotEmpty(a21.getCostShareChartOfAccountCode()) && StringUtils.isNotEmpty(a21.getCostShareSourceAccountNumber())) {
274                allFieldsSet = true;
275            }
276    
277            // Cost Sharing COA Code and Cost Sharing Account Number are required
278            success &= checkEmptyBOField("a21SubAccount.costShareChartOfAccountCode", a21.getCostShareChartOfAccountCode(), "Cost Share Chart of Accounts Code");
279            success &= checkEmptyBOField("a21SubAccount.costShareSourceAccountNumber", a21.getCostShareSourceAccountNumber(), "Cost Share AccountNumber");
280    
281            // existence test on Cost Share Account
282            if (allFieldsSet) {
283                if (ObjectUtils.isNull(a21.getCostShareAccount())) {
284                    putFieldError("a21SubAccount.costShareSourceAccountNumber", KFSKeyConstants.ERROR_EXISTENCE, getDisplayName("a21SubAccount.costShareSourceAccountNumber"));
285                    success &= false;
286                }
287            }
288    
289            // existence test on Cost Share SubAccount
290            if (allFieldsSet && StringUtils.isNotBlank(a21.getCostShareSourceSubAccountNumber())) {
291                if (ObjectUtils.isNull(a21.getCostShareSourceSubAccount())) {
292                    putFieldError("a21SubAccount.costShareSourceSubAccountNumber", KFSKeyConstants.ERROR_EXISTENCE, getDisplayName("a21SubAccount.costShareSourceSubAccountNumber"));
293                    success &= false;
294                }
295            }
296    
297            // Cost Sharing Account may not be for contracts and grants
298            if (ObjectUtils.isNotNull(a21.getCostShareAccount())) {
299                if (ObjectUtils.isNotNull(a21.getCostShareAccount().getSubFundGroup())) {
300                    if (a21.getCostShareAccount().isForContractsAndGrants()) {
301                        putFieldError("a21SubAccount.costShareSourceAccountNumber", KFSKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_COST_SHARE_ACCOUNT_MAY_NOT_BE_CG_FUNDGROUP, new String[] { SpringContext.getBean(SubFundGroupService.class).getContractsAndGrantsDenotingAttributeLabel(), SpringContext.getBean(SubFundGroupService.class).getContractsAndGrantsDenotingValueForMessage() });
302                        success &= false;
303                    }
304                }
305            }
306    
307            // The ICR fields must be empty if the sub-account type code is for cost sharing
308            if (checkCgIcrIsEmpty() == false) {
309                putFieldError("a21SubAccount.indirectCostRecoveryTypeCode", KFSKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_ICR_SECTION_INVALID, a21.getSubAccountTypeCode());
310                success &= false;
311            }
312    
313            return success;
314        }
315    
316        /**
317         * This checks that if the ICR information is entered that it is valid for this fiscal year and that all of its fields are valid
318         * as well (such as account)
319         * 
320         * @return true if the ICR information is filled in and it is valid
321         */
322        protected boolean checkCgIcrRules() {
323            A21SubAccount a21 = newSubAccount.getA21SubAccount();
324            if(ObjectUtils.isNull(a21)) {
325                return true;
326            }
327    
328            boolean success = true;
329            
330            // existence check for ICR Type Code
331            if (StringUtils.isNotEmpty(a21.getIndirectCostRecoveryTypeCode())) {
332                if (ObjectUtils.isNull(a21.getIndirectCostRecoveryType())) {
333                    putFieldError("a21SubAccount.indirectCostRecoveryTypeCode", KFSKeyConstants.ERROR_EXISTENCE, "ICR Type Code: " + a21.getIndirectCostRecoveryTypeCode());
334                    success = false;
335                }
336            }
337    
338            // existence check for Financial Series ID
339            if (StringUtils.isNotEmpty(a21.getFinancialIcrSeriesIdentifier())) {            
340                String fiscalYear = StringUtils.EMPTY + SpringContext.getBean(UniversityDateService.class).getCurrentFiscalYear();
341                String icrSeriesId = a21.getFinancialIcrSeriesIdentifier();
342                
343                Map<String, String> pkMap = new HashMap<String, String>();
344                pkMap.put(KFSPropertyConstants.UNIVERSITY_FISCAL_YEAR, fiscalYear);
345                pkMap.put(KFSPropertyConstants.FINANCIAL_ICR_SERIES_IDENTIFIER, icrSeriesId);
346                Collection<IndirectCostRecoveryRateDetail> icrRateDetails = getBoService().findMatching(IndirectCostRecoveryRateDetail.class, pkMap);
347                
348                if (ObjectUtils.isNull(icrRateDetails) || icrRateDetails.isEmpty()) {
349                    String label = SpringContext.getBean(DataDictionaryService.class).getAttributeLabel(A21SubAccount.class, KFSPropertyConstants.FINANCIAL_ICR_SERIES_IDENTIFIER);
350                    putFieldError(KFSPropertyConstants.A21_SUB_ACCOUNT + "." + KFSPropertyConstants.FINANCIAL_ICR_SERIES_IDENTIFIER, KFSKeyConstants.ERROR_EXISTENCE, label + " (" + icrSeriesId + ")");
351                    success = false;
352                }
353                else {
354                    for(IndirectCostRecoveryRateDetail icrRateDetail : icrRateDetails) {
355                        if(ObjectUtils.isNull(icrRateDetail.getIndirectCostRecoveryRate())){                                
356                            putFieldError(KFSPropertyConstants.A21_SUB_ACCOUNT + "." + KFSPropertyConstants.FINANCIAL_ICR_SERIES_IDENTIFIER, KFSKeyConstants.IndirectCostRecovery.ERROR_DOCUMENT_ICR_RATE_NOT_FOUND, new String[]{fiscalYear, icrSeriesId});
357                            success = false;
358                            break;
359                        }
360                    }
361                }            
362            }
363    
364            // existence check for ICR Account
365            if (StringUtils.isNotEmpty(a21.getIndirectCostRcvyFinCoaCode()) && StringUtils.isNotEmpty(a21.getIndirectCostRecoveryAcctNbr())) {
366                if (ObjectUtils.isNull(a21.getIndirectCostRecoveryAcct())) {
367                    putFieldError("a21SubAccount.indirectCostRecoveryAcctNbr", KFSKeyConstants.ERROR_EXISTENCE, "ICR Account: " + a21.getIndirectCostRcvyFinCoaCode() + "-" + a21.getIndirectCostRecoveryAcctNbr());
368                    
369                    success = false;
370                }
371            }
372    
373            // The cost sharing fields must be empty if the sub-account type code is for ICR
374            if (checkCgCostSharingIsEmpty() == false) {
375                putFieldError("a21SubAccount.costShareChartOfAccountCode", KFSKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_COST_SHARE_SECTION_INVALID, a21.getSubAccountTypeCode());
376    
377                success &= false;
378            }
379    
380            return success;
381        }
382    
383        /**
384         * This method tests if all fields in the Cost Sharing section are empty.
385         * 
386         * @return true if the cost sharing values passed in are empty, otherwise false.
387         */
388        protected boolean checkCgCostSharingIsEmpty() {
389            boolean success = true;
390    
391            A21SubAccount newA21SubAccount = newSubAccount.getA21SubAccount();
392            if (ObjectUtils.isNotNull(newA21SubAccount)) {
393                success &= StringUtils.isEmpty(newA21SubAccount.getCostShareChartOfAccountCode());
394                success &= StringUtils.isEmpty(newA21SubAccount.getCostShareSourceAccountNumber());
395                success &= StringUtils.isEmpty(newA21SubAccount.getCostShareSourceSubAccountNumber());
396            }
397    
398            return success;
399        }
400    
401        /**
402         * This method tests if all fields in the ICR section are empty.
403         * 
404         * @return true if the ICR values passed in are empty, otherwise false.
405         */
406        protected boolean checkCgIcrIsEmpty() {
407            boolean success = true;
408            
409            A21SubAccount newA21SubAccount = newSubAccount.getA21SubAccount();
410            if (ObjectUtils.isNotNull(newA21SubAccount)) {
411                success &= StringUtils.isEmpty(newA21SubAccount.getFinancialIcrSeriesIdentifier());
412                success &= StringUtils.isEmpty(newA21SubAccount.getIndirectCostRcvyFinCoaCode());
413                success &= StringUtils.isEmpty(newA21SubAccount.getIndirectCostRecoveryAcctNbr());
414                success &= StringUtils.isEmpty(newA21SubAccount.getIndirectCostRecoveryTypeCode());
415                // this is a boolean, so create any value if set to true, meaning a user checked the box, otherwise assume it's empty
416                success &= StringUtils.isEmpty(newA21SubAccount.getOffCampusCode() ? "1" : "");
417            }
418    
419            return success;
420        }
421    
422        /**
423         * This method tests the value entered, and if there is anything there it logs a new error, and returns false.
424         * 
425         * @param value - String value to be tested
426         * @param fieldName - name of the field being tested
427         * @return false if there is any value in value, otherwise true
428         */
429        protected boolean disallowAnyValues(String value, String fieldName) {
430            if (StringUtils.isNotEmpty(value)) {
431                putFieldError(fieldName, KFSKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_NOT_AUTHORIZED_ENTER_CG_FIELDS, getDisplayName(fieldName));
432                return false;
433            }
434            return true;
435        }
436    
437        /**
438         * This method tests the two values entered, and if there is any change between the two, it logs an error, and returns false.
439         * Note that the comparison is done after trimming both leading and trailing whitespace from both strings, and then doing a
440         * case-insensitive comparison.
441         * 
442         * @param oldValue - the original String value of the field
443         * @param newValue - the new String value of the field
444         * @param fieldName - name of the field being tested
445         * @return false if there is any difference between the old and new, true otherwise
446         */
447        protected boolean disallowChangedValues(String oldValue, String newValue, String fieldName) {
448    
449            if (isFieldValueChanged(oldValue, newValue)) {
450                putFieldError(fieldName, KFSKeyConstants.ERROR_DOCUMENT_SUBACCTMAINT_NOT_AUTHORIZED_CHANGE_CG_FIELDS, getDisplayName(fieldName));
451                return false;
452            }
453            return true;
454        }
455    
456        /**
457         * This compares two string values to see if the newValue has changed from the oldValue
458         * 
459         * @param oldValue - original value
460         * @param newValue - new value
461         * @return true if the two fields are different from each other
462         */
463        protected boolean isFieldValueChanged(String oldValue, String newValue) {
464    
465            if (StringUtils.isBlank(oldValue) && StringUtils.isBlank(newValue)) {
466                return false;
467            }
468    
469            if (StringUtils.isBlank(oldValue) && StringUtils.isNotBlank(newValue)) {
470                return true;
471            }
472    
473            if (StringUtils.isNotBlank(oldValue) && StringUtils.isBlank(newValue)) {
474                return true;
475            }
476    
477            if (!oldValue.trim().equalsIgnoreCase(newValue.trim())) {
478                return true;
479            }
480    
481            return false;
482        }
483    
484    
485        /**
486         * This method retrieves the label name for a specific property
487         * 
488         * @param propertyName - property to retrieve label for (from the DD)
489         * @return the label
490         */
491        protected String getDisplayName(String propertyName) {
492            return getDdService().getAttributeLabel(SubAccount.class, propertyName);
493        }
494    
495    }