package org.openlmis.stockmanagement.controller; /* * This program is part of the OpenLMIS logistics management information system platform software. * Copyright © 2013 VillageReach * * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. *   * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details. * You should have received a copy of the GNU Affero General Public License along with this program.  If not, see http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.  */ import com.wordnik.swagger.annotations.Api; import com.wordnik.swagger.annotations.ApiOperation; import lombok.NoArgsConstructor; import org.apache.log4j.Logger; import org.openlmis.core.domain.*; import org.openlmis.core.domain.StockAdjustmentReason; import org.openlmis.core.repository.FacilityRepository; import org.openlmis.core.repository.StockAdjustmentReasonRepository; import org.openlmis.core.service.*; import org.openlmis.core.web.OpenLmisResponse; import org.openlmis.core.web.controller.BaseController; import org.openlmis.stockmanagement.domain.*; import org.openlmis.stockmanagement.dto.StockEvent; import org.openlmis.stockmanagement.dto.StockEventType; import org.openlmis.stockmanagement.repository.LotRepository; import org.openlmis.stockmanagement.repository.StockCardRepository; import org.openlmis.stockmanagement.service.StockCardService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import javax.servlet.http.HttpServletRequest; import java.util.*; import static com.google.common.collect.Iterables.any; import static org.openlmis.core.utils.RightUtil.with; import static org.springframework.web.bind.annotation.RequestMethod.GET; import static org.springframework.web.bind.annotation.RequestMethod.POST; /** * This controller provides GET, POST, and DELETE endpoints related to stock cards. */ @Controller @Api(value = "Stock Cards", description = "Track the stock cards (stock on hand) at various facilities.") @RequestMapping(value = "/api/v2/") @NoArgsConstructor public class StockCardController extends BaseController { private static Logger logger = Logger.getLogger(StockCardController.class); @Autowired private FacilityRepository facilityRepository; @Autowired private ProductService productService; @Autowired private StockCardRepository stockCardRepository; @Autowired private StockAdjustmentReasonRepository stockAdjustmentReasonRepository; @Autowired private LotRepository lotRepository; @Autowired private ProgramProductService programProductService; @Autowired private ProgramService programService; @Autowired private RoleRightsService roleRightsService; @Autowired private StockCardService service; StockCardController(MessageService messageService, FacilityRepository facilityRepository, ProductService productService, StockAdjustmentReasonRepository stockAdjustmentReasonRepository, StockCardRepository stockCardRepository, LotRepository lotRepository, ProgramProductService programProductService, ProgramService programService, RoleRightsService roleRightsService, StockCardService service) { this.messageService = Objects.requireNonNull(messageService); this.facilityRepository = Objects.requireNonNull(facilityRepository); this.productService = Objects.requireNonNull(productService); this.stockCardRepository = Objects.requireNonNull(stockCardRepository); this.stockAdjustmentReasonRepository = Objects.requireNonNull(stockAdjustmentReasonRepository); this.lotRepository = Objects.requireNonNull(lotRepository); this.programProductService = Objects.requireNonNull(programProductService); this.programService = Objects.requireNonNull(programService); this.roleRightsService = Objects.requireNonNull(roleRightsService); this.service = Objects.requireNonNull(service); } @RequestMapping(value = "facilities/{facilityId}/products/{productCode}/stockCard", method = GET, headers = ACCEPT_JSON) @ApiOperation(value = "Get information about the stock card for the specified facility and product.", notes = "Gets stock card information, by facility and product." + "<p>If no view permissions are found for this stock card, will return 403 Forbidden." + "<p>" + "<p>Path parameters (required):" + "<ul>" + "<li><strong>facilityId</strong> (Long) - facility for the stock card.</li>" + "<li><strong>productCode</strong> (String) - product for the stock card.</li>" + "</ul>" + "<p>" + "<p>Request parameters:" + "<ul>" + "<li><strong>entries</strong> (Integer, optional, default = 1) - Number of stock card entries to " + "get in the result.</li>" + "</ul>") public ResponseEntity getStockCard(@PathVariable Long facilityId, @PathVariable String productCode, @RequestParam(value = "entries", defaultValue = "1")Integer entries, @RequestParam(value = "includeEmptyLots", required = false, defaultValue = "false") boolean includeEmptyLots, HttpServletRequest request) { // Check permissions Long userId = loggedInUserId(request); List<Right> rights = roleRightsService.getRightsForUserFacilityAndProductCode(userId, facilityId, productCode); if (!any(rights, with("VIEW_STOCK_ON_HAND"))) { return OpenLmisResponse.error(messageService.message("error.permission.stock.card.view"), HttpStatus.FORBIDDEN); } StockCard stockCard = stockCardRepository.getStockCardByFacilityAndProduct(facilityId, productCode); if (stockCard != null) { filterEntries(stockCard, entries, includeEmptyLots); return OpenLmisResponse.response(stockCard); } else { return OpenLmisResponse.error("The specified stock card does not exist." , HttpStatus.NOT_FOUND); } } @RequestMapping(value = "facilities/{facilityId}/stockCards/{stockCardId}", method = GET, headers = ACCEPT_JSON) @ApiOperation(value = "Get information about the specified stock card for the specified facility.", notes = "Gets stock card information, by facility and stock card id." + "<p>If facility does not have specified stock card id, will return 404 Not Found." + "<p>If no view permissions are found for this stock card, will return 403 Forbidden." + "<p>" + "<p>Path parameters (required):" + "<ul>" + "<li><strong>facilityId</strong> (Long) - facility for the stock card.</li>" + "<li><strong>stockCardId</strong> (Long) - the specified stock card.</li>" + "</ul>" + "<p>" + "<p>Request parameters:" + "<ul>" + "<li><strong>entries</strong> (Integer, optional, default = 1) - Number of stock card entries to " + "get in the result.</li>" + "</ul>") public ResponseEntity getStockCardById(@PathVariable Long facilityId, @PathVariable Long stockCardId, @RequestParam(value = "entries", defaultValue = "1")Integer entries, @RequestParam(value = "includeEmptyLots", required = false, defaultValue = "false") boolean includeEmptyLots, HttpServletRequest request) { Long userId = loggedInUserId(request); Product product = stockCardRepository.getProductByStockCardId(stockCardId); List<Right> rights = roleRightsService.getRightsForUserFacilityAndProductCode(userId, facilityId, product.getCode()); if (!any(rights, with("VIEW_STOCK_ON_HAND"))) { return OpenLmisResponse.error(messageService.message("error.permission.stock.card.view"), HttpStatus.FORBIDDEN); } StockCard stockCard = service.getStockCardById(facilityId, stockCardId); if (stockCard != null) { filterEntries(stockCard, entries, includeEmptyLots); return OpenLmisResponse.response(stockCard); } else { return OpenLmisResponse.error("The specified stock card does not exist." , HttpStatus.NOT_FOUND); } } @RequestMapping(value = "facilities/{facilityId}/stockCards", method = GET, headers = ACCEPT_JSON) @ApiOperation(value = "Get information about all stock cards for the specified facility.", notes = "Gets all stock card information, by facility." + "<p>If no stock cards exist, will return 404 Not Found." + "<p>If no view permissions are found for any existing stock card, will return an empty list." + "<p>" + "<p>Path parameters (required):" + "<ul>" + "<li><strong>facilityId</strong> (Long) - facility for the stock cards.</li>" + "</ul>" + "<p>" + "<p>Request parameters:" + "<ul>" + "<li><strong>entries</strong> (Integer, optional, default = 1) - Number of stock card entries to " + "get in the result.</li>" + "<li><strong>countOnly</strong> (Boolean, optional, default = false) - Get only the count of " + "stock cards.</li>" + "</ul>") public ResponseEntity getStockCards(@PathVariable Long facilityId, @RequestParam(value = "entries", defaultValue = "1") Integer entries, @RequestParam(value = "countOnly", defaultValue = "false") Boolean countOnly, @RequestParam(value = "includeEmptyLots", required = false, defaultValue = "false") boolean includeEmptyLots, HttpServletRequest request) { Long userId = loggedInUserId(request); List<StockCard> stockCards = service.getStockCards(facilityId); if (stockCards != null) { // Filter stock cards based on permission, put into permitted stock cards List<StockCard> permittedStockCards = new ArrayList<>(); for (StockCard stockCard : stockCards) { List<Right> rights = roleRightsService.getRightsForUserFacilityAndProductCode(userId, facilityId, stockCard.getProduct().getCode()); if (any(rights, with("VIEW_STOCK_ON_HAND"))) { permittedStockCards.add(stockCard); } } // If countOnly specified, then only return count of permitted stock cards if (countOnly) { return OpenLmisResponse.response("count", permittedStockCards.size()); } // Filter the permitted stock cards based on other criteria for (StockCard stockCard : permittedStockCards) { filterEntries(stockCard, entries, includeEmptyLots); } return OpenLmisResponse.response("stockCards", permittedStockCards); } else { return OpenLmisResponse.error("The specified stock cards do not exist." , HttpStatus.NOT_FOUND); } } @RequestMapping(value = "facilities/{facilityId}/stockCards", method = POST, headers = ACCEPT_JSON) @ApiOperation(value="Update stock cards at a facility.", notes = "Updates stock cards at a facility. This is done by providing a list of stock events." + "<p>Path parameters (required):" + "<ul>" + "<li><strong>facilityId</strong> (Long) - facility for the stock cards in which to update.</li>" + "</ul>" + "<p>" + "<p>Body parameters (required):" + "<ul>" + "<li>" + "<strong>stock events</strong> (Array of stock event objects) - a list of stock events to " + "process for update." + "<p>" + "<p>Stock event properties" + "<ul>" + "<li><strong>type</strong> (String, required) - type code of stock event (choices are ISSUE, " + "RECEIPT, ADJUSTMENT).</li>" + "<li><strong>facilityId</strong> (Long, required for ISSUE/RECEIPT types) - facility id where" + "stock is going to/coming from.</li>" + "<li><strong>productCode</strong> (String, required) - product code of the stock being " + "processed.</li>" + "<li><strong>quantity</strong> (Long, required) - quantity of stock being processed. Specify as a " + "positive number. For ISSUE, this amount is decremented, for RECEIPT, this amount is incremented, " + "for ADJUSTMENT, it depends on the adjustment reason.</li>" + "<li><strong>reasonName</strong> (String, required for ADJUSTMENT types) - reason code for the " + "adjustment.</li>" + "<li><strong>lotId</strong> (Long, optional) - lot id of a particular lot that will be processed. " + "This and lot are optional; if lotId is specified, lot is ignored.</li>" + "<li><strong>lot</strong> (Object, optional) - lot object of a particular lot that will be " + "processed. If lot with the specified code, manufacturerName, and expirationDate do not exist, a " + "lot will be created.</li>" + "<li><strong>customProps</strong> (Object, optional) - an object of custom properties (keys and " + "values) to specify custom fields for the stock event." + "</ul>" + "</li>" + "</ul>" + "<p>" + "<p>Example stock event list JSON:" + "<pre><code>" + "[\n" + " {\n" + " \"type\": \"ADJUSTMENT\",\n" + " \"productCode\": \"V001\",\n" + " \"quantity\": 50,\n" + " \"reasonName\": \"TRANSFER_IN\",\n" + " \"lotId\": 1\n" + " },\n" + " {\n" + " \"type\": \"ISSUE\",\n" + " \"facilityId\": 19077,\n" + " \"productCode\": \"V001\",\n" + " \"quantity\": 50,\n" + " \"customProps\": {\n" + " \"occurred\": \"2015-10-01\"\n" + " }\n" + " },\n" + " {\n" + " \"type\": \"RECEIPT\",\n" + " \"facilityId\": 19074,\n" + " \"productCode\": \"V001\",\n" + " \"quantity\": 50,\n" + " \"lot\": {\n" + " \"lotCode\": \"C1\",\n" + " \"manufacturerName\": \"Manufacturer 3\",\n" + " \"expirationDate\": \"2016-07-01\"\n" + " },\n" + " \"customProps\": {\n" + " \"vvmStatus\": \"1\"\n" + " }\n" + " }\n" + "]\n" + "</code></pre>") @Transactional public ResponseEntity processStock(@PathVariable long facilityId, @RequestBody(required = true) List<StockEvent> events, HttpServletRequest request) { // verify we have something to do and facility exists if(null == events || 0 >= events.size()) return OpenLmisResponse.success(messageService.message("success.stock.event.none")); if(null == facilityRepository.getById(facilityId)) return OpenLmisResponse.error(messageService.message("error.facility.unknown"), HttpStatus.BAD_REQUEST); // convert events to entries Long userId = loggedInUserId(request); List<StockCardEntry> entries = new ArrayList<>(); for(StockEvent event : events) { logger.debug("Processing event: " + event); // validate event if(!event.isValid()) return OpenLmisResponse.error(messageService.message("error.stock.event.invalid"), HttpStatus.BAD_REQUEST); // validate product String productCode = event.getProductCode(); if(null == productService.getByCode(productCode)) return OpenLmisResponse.error(messageService.message("error.product.unknown"), HttpStatus.BAD_REQUEST); // validate reason StockAdjustmentReason reason = null; if (StockEventType.ADJUSTMENT == event.getType()) { reason = stockAdjustmentReasonRepository.getAdjustmentReasonByName( event.getReasonName()); if(null == reason) return OpenLmisResponse.error(messageService.message("error.stockadjustmentreason.unknown"), HttpStatus.BAD_REQUEST); } // validate permissions List<Right> rights = roleRightsService.getRightsForUserFacilityAndProductCode(userId, facilityId, productCode); if (!any(rights, with("MANAGE_STOCK"))) { return OpenLmisResponse.error(messageService.message("error.permission.stock.card.manage"), HttpStatus.FORBIDDEN); } // get or create stock card StockCard card = service.getOrCreateStockCard(facilityId, productCode); if(null == card) return OpenLmisResponse.error(messageService.message("error.stock.card.get"), HttpStatus.INTERNAL_SERVER_ERROR); // get or create lot, if lot is being used StringBuilder str = new StringBuilder(); Long lotId = event.getLotId(); Lot lotObj = event.getLot(); LotOnHand lotOnHand = service.getLotOnHand(lotId, lotObj, productCode, card, str); if (!str.toString().equals("")) { return OpenLmisResponse.error(messageService.message(str.toString()), HttpStatus.BAD_REQUEST); } // create entry from event long quantity = event.getPositiveOrNegativeQuantity(reason); StockCardEntryType entryType = StockCardEntryType.ADJUSTMENT; switch (event.getType()) { case ISSUE: entryType = StockCardEntryType.DEBIT; break; case RECEIPT: entryType = StockCardEntryType.CREDIT; break; case ADJUSTMENT: entryType = StockCardEntryType.ADJUSTMENT; break; default: break; } Long onHand = (null != lotObj) ? lotOnHand.getQuantityOnHand() : card.getTotalQuantityOnHand(); if (!event.isValidIssueQuantity(onHand)) { return OpenLmisResponse.error(messageService.message("error.stock.quantity.invalid"), HttpStatus.INTERNAL_SERVER_ERROR); } Date occurred = event.getOccurred(); String referenceNumber = event.getReferenceNumber(); StockCardEntry entry = new StockCardEntry(card, entryType, quantity, occurred, referenceNumber); entry.setAdjustmentReason(reason); entry.setLotOnHand(lotOnHand); Map<String, String> customProps = event.getCustomProps(); if (null != customProps) { for (String k : customProps.keySet()) { entry.addKeyValue(k, customProps.get(k)); } } entry.setCreatedBy(userId); entry.setModifiedBy(userId); entries.add(entry); } service.addStockCardEntries(entries); return OpenLmisResponse.success(messageService.message("success.stock.adjusted")); } //Calls filterEntries() for each specified stockCard private void filterEntries(List<StockCard> stockCards, Integer entryCount, boolean includeEmptyLots) { for (StockCard stockCard : stockCards) { filterEntries(stockCard, entryCount, includeEmptyLots); } } //Convenience method, calling truncateStockCardEntries() and removeEmptyLotsFromStockCard() private void filterEntries(StockCard stockCard, Integer entryCount, boolean includeEmptyLots) { truncateStockCardEntries(stockCard, entryCount); if(!includeEmptyLots) removeEmptyLotsFromStockCard(stockCard); } //Filter stockCard.entries such that it contains only the first entryCount number of items private void truncateStockCardEntries(StockCard stockCard, Integer entryCount) { List<StockCardEntry> entries = stockCard.getEntries(); if (entries != null) { if (entryCount < 0) { stockCard.setEntries(entries.subList(0, 1)); } else if (entryCount < entries.size()) { stockCard.setEntries(entries.subList(0, entryCount)); } } } //Filter stockCard such that only contains Lots that have a positive quantityOnHand private void removeEmptyLotsFromStockCard(StockCard stockCard) { //Data validation List<LotOnHand> originalLotsOnHand = stockCard.getLotsOnHand(); if(originalLotsOnHand == null) return; //Build a list of nonEmptyLots... List<LotOnHand> nonEmptyLots = new LinkedList<LotOnHand>(); for (LotOnHand lot : originalLotsOnHand) { if(lot.getQuantityOnHand() > 0) nonEmptyLots.add(lot); } //...and associate it with our StockCard stockCard.setLotsOnHand(nonEmptyLots); } }