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 }