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.module.purap.document.service.impl;
017
018 import java.math.BigDecimal;
019 import java.util.ArrayList;
020 import java.util.HashMap;
021 import java.util.Iterator;
022 import java.util.List;
023 import java.util.Map;
024
025 import org.apache.commons.lang.StringUtils;
026 import org.kuali.kfs.integration.cab.CapitalAssetBuilderModuleService;
027 import org.kuali.kfs.integration.purap.CapitalAssetSystem;
028 import org.kuali.kfs.module.purap.PurapConstants;
029 import org.kuali.kfs.module.purap.PurapPropertyConstants;
030 import org.kuali.kfs.module.purap.PurapRuleConstants;
031 import org.kuali.kfs.module.purap.businessobject.PurApAccountingLine;
032 import org.kuali.kfs.module.purap.businessobject.PurApItem;
033 import org.kuali.kfs.module.purap.businessobject.PurchasingCapitalAssetItem;
034 import org.kuali.kfs.module.purap.businessobject.RequisitionCapitalAssetItem;
035 import org.kuali.kfs.module.purap.businessobject.RequisitionCapitalAssetSystem;
036 import org.kuali.kfs.module.purap.businessobject.RequisitionItem;
037 import org.kuali.kfs.module.purap.document.PurchaseOrderDocument;
038 import org.kuali.kfs.module.purap.document.PurchasingDocument;
039 import org.kuali.kfs.module.purap.document.RequisitionDocument;
040 import org.kuali.kfs.module.purap.document.dataaccess.RequisitionDao;
041 import org.kuali.kfs.module.purap.document.service.PurapService;
042 import org.kuali.kfs.module.purap.document.service.RequisitionService;
043 import org.kuali.kfs.sys.context.SpringContext;
044 import org.kuali.kfs.sys.service.PostalCodeValidationService;
045 import org.kuali.kfs.sys.service.UniversityDateService;
046 import org.kuali.kfs.vnd.businessobject.VendorCommodityCode;
047 import org.kuali.kfs.vnd.businessobject.VendorContract;
048 import org.kuali.kfs.vnd.businessobject.VendorDetail;
049 import org.kuali.kfs.vnd.document.service.VendorService;
050 import org.kuali.rice.kew.exception.WorkflowException;
051 import org.kuali.rice.kim.bo.Person;
052 import org.kuali.rice.kim.service.PersonService;
053 import org.kuali.rice.kns.bo.Note;
054 import org.kuali.rice.kns.service.BusinessObjectService;
055 import org.kuali.rice.kns.service.DateTimeService;
056 import org.kuali.rice.kns.service.DocumentService;
057 import org.kuali.rice.kns.service.KualiConfigurationService;
058 import org.kuali.rice.kns.service.KualiRuleService;
059 import org.kuali.rice.kns.service.ParameterService;
060 import org.kuali.rice.kns.util.KualiDecimal;
061 import org.kuali.rice.kns.util.ObjectUtils;
062 import org.springframework.transaction.annotation.Transactional;
063
064
065
066 /**
067 * Implementation of RequisitionService
068 */
069 @Transactional
070 public class RequisitionServiceImpl implements RequisitionService {
071 private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(RequisitionServiceImpl.class);
072
073 private BusinessObjectService businessObjectService;
074 private CapitalAssetBuilderModuleService capitalAssetBuilderModuleService;
075 private DateTimeService dateTimeService;
076 private DocumentService documentService;
077 private KualiRuleService ruleService;
078 private KualiConfigurationService kualiConfigurationService;
079 private ParameterService parameterService;
080 private PersonService<Person> personService;
081 private PostalCodeValidationService postalCodeValidationService;
082 private PurapService purapService;
083 private RequisitionDao requisitionDao;
084 private UniversityDateService universityDateService;
085 private VendorService vendorService;
086
087 public PurchasingCapitalAssetItem createCamsItem(PurchasingDocument purDoc, PurApItem purapItem) {
088 PurchasingCapitalAssetItem camsItem = new RequisitionCapitalAssetItem();
089 camsItem.setItemIdentifier(purapItem.getItemIdentifier());
090 // If the system type is INDIVIDUAL then for each of the capital asset items, we need a system attached to it.
091 if (purDoc.getCapitalAssetSystemTypeCode().equals(PurapConstants.CapitalAssetTabStrings.INDIVIDUAL_ASSETS)) {
092 CapitalAssetSystem resultSystem = new RequisitionCapitalAssetSystem();
093 camsItem.setPurchasingCapitalAssetSystem(resultSystem);
094 }
095 camsItem.setPurchasingDocument(purDoc);
096
097 return camsItem;
098 }
099
100 public CapitalAssetSystem createCapitalAssetSystem() {
101 CapitalAssetSystem resultSystem = new RequisitionCapitalAssetSystem();
102 return resultSystem;
103 }
104
105 /**
106 * @see org.kuali.kfs.module.purap.document.service.RequisitionService#getRequisitionById(java.lang.Integer)
107 */
108 public RequisitionDocument getRequisitionById(Integer id) {
109 String documentNumber = requisitionDao.getDocumentNumberForRequisitionId(id);
110 if (ObjectUtils.isNotNull(documentNumber)) {
111 try {
112 RequisitionDocument doc = (RequisitionDocument) documentService.getByDocumentHeaderId(documentNumber);
113
114 return doc;
115 }
116 catch (WorkflowException e) {
117 String errorMessage = "Error getting requisition document from document service";
118 LOG.error("getRequisitionById() " + errorMessage, e);
119 throw new RuntimeException(errorMessage, e);
120 }
121 }
122
123 return null;
124 }
125
126 /**
127 * @see org.kuali.kfs.module.purap.document.service.RequisitionService#isAutomaticPurchaseOrderAllowed(org.kuali.kfs.module.purap.document.RequisitionDocument)
128 */
129 public boolean isAutomaticPurchaseOrderAllowed(RequisitionDocument requisition) {
130 LOG.debug("isAutomaticPurchaseOrderAllowed() started");
131
132 /*
133 * The private checkAutomaticPurchaseOrderRules method contains rules to check if a requisition can become an APO (Automatic
134 * Purchase Order). The method returns a string containing the reason why this method should return false. Save the reason
135 * as a note on the requisition.
136 */
137 String note = checkAutomaticPurchaseOrderRules(requisition);
138 if (StringUtils.isNotEmpty(note)) {
139 note = PurapConstants.REQ_REASON_NOT_APO + note;
140 try {
141 Note apoNote = documentService.createNoteFromDocument(requisition, note);
142 documentService.addNoteToDocument(requisition, apoNote);
143 }
144 catch (Exception e) {
145 throw new RuntimeException(PurapConstants.REQ_UNABLE_TO_CREATE_NOTE + " " + e);
146 }
147
148 LOG.debug("isAutomaticPurchaseOrderAllowed() return false; " + note);
149 return false;
150 }
151
152 LOG.debug("isAutomaticPurchaseOrderAllowed() You made it! Your REQ can become an APO; return true.");
153 return true;
154 }
155
156 /**
157 * Checks the rule for Automatic Purchase Order eligibility of the requisition and return a String containing the reason why the
158 * requisition was not eligible to become an APO if it was not eligible, or return an empty String if the requisition is
159 * eligible to become an APO
160 *
161 * @param requisition the requisition document to be checked for APO eligibility.
162 * @return String containing the reason why the requisition was not eligible to become an APO if it was not eligible, or an
163 * empty String if the requisition is eligible to become an APO.
164 */
165 protected String checkAutomaticPurchaseOrderRules(RequisitionDocument requisition) {
166 String requisitionSource = requisition.getRequisitionSourceCode();
167 KualiDecimal reqTotal = requisition.getTotalDollarAmount();
168 KualiDecimal apoLimit = purapService.getApoLimit(requisition.getVendorContractGeneratedIdentifier(), requisition.getChartOfAccountsCode(), requisition.getOrganizationCode());
169 requisition.setOrganizationAutomaticPurchaseOrderLimit(apoLimit);
170
171 LOG.debug("isAPO() reqId = " + requisition.getPurapDocumentIdentifier() + "; apoLimit = " + apoLimit + "; reqTotal = " + reqTotal);
172 if (apoLimit == null) {
173 return "APO limit is empty.";
174 }
175 else {
176 if (reqTotal.compareTo(apoLimit) == 1) {
177 return "Requisition total is greater than the APO limit.";
178 }
179 }
180
181 if (reqTotal.compareTo(KualiDecimal.ZERO) <= 0) {
182 return "Requisition total is not greater than zero.";
183 }
184
185 LOG.debug("isAPO() vendor #" + requisition.getVendorHeaderGeneratedIdentifier() + "-" + requisition.getVendorDetailAssignedIdentifier());
186 if (requisition.getVendorHeaderGeneratedIdentifier() == null || requisition.getVendorDetailAssignedIdentifier() == null) {
187 return "Vendor was not selected from the vendor database.";
188 }
189 else {
190 VendorDetail vendorDetail = vendorService.getVendorDetail(requisition.getVendorHeaderGeneratedIdentifier(), requisition.getVendorDetailAssignedIdentifier());
191 if (vendorDetail == null) {
192 return "Error retrieving vendor from the database.";
193 }
194 if ( StringUtils.isBlank(requisition.getVendorLine1Address()) ||
195 StringUtils.isBlank(requisition.getVendorCityName()) ||
196 StringUtils.isBlank(requisition.getVendorCountryCode())) {
197 return "Requisition does not have all of the vendor address fields that are required for Purchase Order.";
198 }
199 requisition.setVendorRestrictedIndicator(vendorDetail.getVendorRestrictedIndicator());
200 if (requisition.getVendorRestrictedIndicator() != null && requisition.getVendorRestrictedIndicator()) {
201 return "Selected vendor is marked as restricted.";
202 }
203 requisition.setVendorDetail(vendorDetail);
204
205 if ((!PurapConstants.RequisitionSources.B2B.equals(requisitionSource)) && ObjectUtils.isNull(requisition.getVendorContractGeneratedIdentifier())) {
206 Person initiator = getPersonService().getPerson(requisition.getDocumentHeader().getWorkflowDocument().getInitiatorPrincipalId());
207 VendorContract b2bContract = vendorService.getVendorB2BContract(vendorDetail, initiator.getCampusCode());
208 if (b2bContract != null) {
209 return "Standard requisition with no contract selected but a B2B contract exists for the selected vendor.";
210 }
211 }
212 }
213
214 //if vendor address isn't complete, no APO
215 if (StringUtils.isBlank(requisition.getVendorLine1Address()) ||
216 StringUtils.isBlank(requisition.getVendorCityName()) ||
217 StringUtils.isBlank(requisition.getVendorCountryCode()) ||
218 !postalCodeValidationService.validateAddress(requisition.getVendorCountryCode(), requisition.getVendorStateCode(), requisition.getVendorPostalCode(), "", "")) {
219 return "Requistion does not contain a complete vendor address";
220 }
221
222 // These are needed for commodity codes. They are put in here so that
223 // we don't have to loop through items too many times.
224 String purchaseOrderRequiresCommodityCode = parameterService.getParameterValue(PurchaseOrderDocument.class, PurapRuleConstants.ITEMS_REQUIRE_COMMODITY_CODE_IND);
225 boolean commodityCodeRequired = purchaseOrderRequiresCommodityCode.equals("Y");
226
227 for (Iterator iter = requisition.getItems().iterator(); iter.hasNext();) {
228 RequisitionItem item = (RequisitionItem) iter.next();
229 if (item.isItemRestrictedIndicator()) {
230 return "Requisition contains an item that is marked as restricted.";
231 }
232
233 //We only need to check the commodity codes if this is an above the line item.
234 if (item.getItemType().isLineItemIndicator()) {
235 String commodityCodesReason = "";
236 List<VendorCommodityCode> vendorCommodityCodes = commodityCodeRequired ? requisition.getVendorDetail().getVendorCommodities() : null;
237 commodityCodesReason = checkAPORulesPerItemForCommodityCodes(item, vendorCommodityCodes, commodityCodeRequired);
238 if (StringUtils.isNotBlank(commodityCodesReason)) {
239 return commodityCodesReason;
240 }
241 }
242
243 if (PurapConstants.ItemTypeCodes.ITEM_TYPE_ORDER_DISCOUNT_CODE.equals(item.getItemType().getItemTypeCode()) || PurapConstants.ItemTypeCodes.ITEM_TYPE_TRADE_IN_CODE.equals(item.getItemType().getItemTypeCode())) {
244 if ((item.getItemUnitPrice() != null) && ((BigDecimal.ZERO.compareTo(item.getItemUnitPrice())) != 0)) {
245 // discount or trade-in item has unit price that is not empty or zero
246 return "Requisition contains a " + item.getItemType().getItemTypeDescription() + " item, so it does not qualify as an APO.";
247 }
248 }
249
250 if (!PurapConstants.RequisitionSources.B2B.equals(requisitionSource)) {
251 for (PurApAccountingLine accountingLine : item.getSourceAccountingLines()) {
252 if (capitalAssetBuilderModuleService.doesAccountingLineFailAutomaticPurchaseOrderRules(accountingLine)) {
253 return "Requisition contains accounting line with capital object level";
254 }
255 }
256 }
257
258 }// endfor items
259
260 if (capitalAssetBuilderModuleService.doesDocumentFailAutomaticPurchaseOrderRules(requisition)) {
261 return "Requisition contains capital asset items.";
262 }
263
264 if (StringUtils.isNotEmpty(requisition.getRecurringPaymentTypeCode())) {
265 return "Payment type is marked as recurring.";
266 }
267
268 if ((requisition.getPurchaseOrderTotalLimit() != null) && (KualiDecimal.ZERO.compareTo(requisition.getPurchaseOrderTotalLimit()) != 0)) {
269 LOG.debug("isAPO() po total limit is not null and not equal to zero; return false.");
270 return "The 'PO not to exceed' amount has been entered.";
271 }
272
273 if (StringUtils.isNotEmpty(requisition.getAlternate1VendorName()) || StringUtils.isNotEmpty(requisition.getAlternate2VendorName()) || StringUtils.isNotEmpty(requisition.getAlternate3VendorName()) || StringUtils.isNotEmpty(requisition.getAlternate4VendorName()) || StringUtils.isNotEmpty(requisition.getAlternate5VendorName())) {
274 LOG.debug("isAPO() alternate vendor name exists; return false.");
275 return "Requisition contains additional suggested vendor names.";
276 }
277
278 if (requisition.isPostingYearNext() && !purapService.isTodayWithinApoAllowedRange()) {
279 return "Requisition is set to encumber next fiscal year and approval is not within APO allowed date range.";
280 }
281
282 return "";
283 }
284
285 /**
286 * Checks the APO rules for Commodity Codes.
287 * The rules are as follow:
288 * 1. If an institution does not require a commodity code on a requisition but
289 * does require a commodity code on a purchase order:
290 * a. If the requisition qualifies for an APO and the commodity code is blank
291 * on any line item then the system should use the default commodity code
292 * for the vendor.
293 * b. If there is not a default commodity code for the vendor then the
294 * requisition is not eligible to become an APO.
295 * 2. The commodity codes where the restricted indicator is Y should disallow
296 * the requisition from becoming an APO.
297 * 3. If the commodity code is Inactive when the requisition is finally approved
298 * do not allow the requisition to become an APO.
299 *
300 * @param purItem
301 * @param vendorCommodityCodes
302 * @param commodityCodeRequired
303 * @return
304 */
305 protected String checkAPORulesPerItemForCommodityCodes(RequisitionItem purItem, List<VendorCommodityCode>vendorCommodityCodes, boolean commodityCodeRequired) {
306 // If the commodity code is blank on any line item and a commodity code is required,
307 // then the system should use the default commodity code for the vendor
308 if (purItem.getCommodityCode() == null && commodityCodeRequired) {
309 for (VendorCommodityCode vcc : vendorCommodityCodes) {
310 if (vcc.isCommodityDefaultIndicator()) {
311 purItem.setCommodityCode(vcc.getCommodityCode());
312 purItem.setPurchasingCommodityCode(vcc.getPurchasingCommodityCode());
313 }
314 }
315 }
316 if (purItem.getCommodityCode() == null) {
317 // If there is not a default commodity code for the vendor then the requisition is not eligible to become an APO.
318 if (commodityCodeRequired)
319 return "There are missing commodity code(s).";
320 }
321 else if (!purItem.getCommodityCode().isActive()) {
322 return "Requisition contains inactive commodity codes.";
323 }
324 else if (purItem.getCommodityCode().isRestrictedItemsIndicator()) {
325 return "Requisition contains an item with a restricted commodity code.";
326 }
327 return "";
328 }
329
330 /**
331 * @see org.kuali.kfs.module.purap.document.service.RequisitionService#getRequisitionsAwaitingContractManagerAssignment()
332 */
333 public List<RequisitionDocument> getRequisitionsAwaitingContractManagerAssignment() {
334 Map fieldValues = new HashMap();
335 fieldValues.put(PurapPropertyConstants.STATUS_CODE, PurapConstants.RequisitionStatuses.AWAIT_CONTRACT_MANAGER_ASSGN);
336 List<RequisitionDocument> unassignedRequisitions = new ArrayList(SpringContext.getBean(BusinessObjectService.class).findMatchingOrderBy(RequisitionDocument.class, fieldValues, PurapPropertyConstants.PURAP_DOC_ID, true));
337 return unassignedRequisitions;
338 }
339
340 /**
341 * @see org.kuali.kfs.module.purap.document.service.RequisitionService#getCountOfRequisitionsAwaitingContractManagerAssignment()
342 */
343 public int getCountOfRequisitionsAwaitingContractManagerAssignment() {
344 List<RequisitionDocument> unassignedRequisitions = getRequisitionsAwaitingContractManagerAssignment();
345 if (ObjectUtils.isNotNull(unassignedRequisitions)) {
346 return unassignedRequisitions.size();
347 }
348 else {
349 return 0;
350 }
351 }
352
353 public void setBusinessObjectService(BusinessObjectService boService) {
354 this.businessObjectService = boService;
355 }
356
357 public void setDocumentService(DocumentService documentService) {
358 this.documentService = documentService;
359 }
360
361 public void setRequisitionDao(RequisitionDao requisitionDao) {
362 this.requisitionDao = requisitionDao;
363 }
364
365 public void setPurapService(PurapService purapService) {
366 this.purapService = purapService;
367 }
368
369 public KualiRuleService getRuleService() {
370 return ruleService;
371 }
372
373 public void setRuleService(KualiRuleService ruleService) {
374 this.ruleService = ruleService;
375 }
376
377 public void setParameterService(ParameterService parameterService) {
378 this.parameterService = parameterService;
379 }
380
381 public void setDateTimeService(DateTimeService dateTimeService) {
382 this.dateTimeService = dateTimeService;
383 }
384
385 public void setUniversityDateService(UniversityDateService universityDateService) {
386 this.universityDateService = universityDateService;
387 }
388
389 public void setVendorService(VendorService vendorService) {
390 this.vendorService = vendorService;
391 }
392
393 public void setKualiConfigurationService(KualiConfigurationService kualiConfigurationService) {
394 this.kualiConfigurationService = kualiConfigurationService;
395 }
396
397 public void setCapitalAssetBuilderModuleService(CapitalAssetBuilderModuleService capitalAssetBuilderModuleService) {
398 this.capitalAssetBuilderModuleService = capitalAssetBuilderModuleService;
399 }
400
401 public void setPostalCodeValidationService(PostalCodeValidationService postalCodeValidationService) {
402 this.postalCodeValidationService = postalCodeValidationService;
403 }
404
405 /**
406 * @return Returns the personService.
407 */
408 protected PersonService<Person> getPersonService() {
409 if(personService==null)
410 personService = SpringContext.getBean(PersonService.class);
411 return personService;
412 }
413
414 }