package com.qcadoo.mes.materialFlowResources; import java.math.BigDecimal; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.dao.DataAccessException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.stereotype.Service; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.collect.Maps; import com.qcadoo.localization.api.TranslationService; import com.qcadoo.mes.basic.BasicException; import com.qcadoo.mes.basic.controllers.dataProvider.dto.AbstractDTO; import com.qcadoo.mes.basic.controllers.dataProvider.responses.DataResponse; import com.qcadoo.mes.materialFlowResources.constants.DocumentState; import com.qcadoo.mes.materialFlowResources.constants.DocumentType; import com.qcadoo.mes.materialFlowResources.constants.WarehouseAlgorithm; @Service public class DocumentPositionValidator { @Autowired private NamedParameterJdbcTemplate jdbcTemplate; @Autowired private TranslationService translationService; @Autowired private DocumentPositionService documentPositionService; public Map<String, Object> validateAndTryMapBeforeCreate(DocumentPositionDTO documentPositionDTO) { return validateAndMap(documentPositionDTO); } public Map<String, Object> validateAndTryMapBeforeUpdate(DocumentPositionDTO documentPositionDTO) { return validateAndMap(documentPositionDTO); } public void validateBeforeDelete(Long id) { } private Map<String, Object> validateAndMap(DocumentPositionDTO position) { Preconditions.checkNotNull(position, "documentGrid.required.documentPosition"); Preconditions.checkNotNull(position.getDocument(), "documentGrid.required.documentPosition.document"); DocumentDTO document = jdbcTemplate .queryForObject("SELECT * FROM materialflowresources_document WHERE id = :id", Collections.singletonMap("id", position.getDocument()), new BeanPropertyRowMapper<DocumentDTO>(DocumentDTO.class)); List<String> errors = new ArrayList<>(); Map<String, Object> params = null; if (isGridReadOnly(document)) { errors.add("documentGrid.error.position.documentAccepted"); } else { if (Strings.isNullOrEmpty(position.getProduct())) { errors.add("documentGrid.error.position.product.required"); } if (Strings.isNullOrEmpty(position.getUnit())) { errors.add("documentGrid.error.position.unit.required"); } errors.addAll(validateConversion(position)); errors.addAll(validateAdditionalCode(position)); errors.addAll(validatePrice(position)); errors.addAll(validateQuantity(position)); errors.addAll(validateGivenquantity(position)); errors.addAll(validateDates(position)); errors.addAll(checkAttributesRequirement(position, document)); errors.addAll(validateResources(position, document)); errors.addAll(validatePallet(position, document)); if (errors.isEmpty()) { errors.addAll(validateAvailableQuantity(position, document, errors)); } params = tryMapDocumentPositionVOToParams(position, errors); } if (!errors.isEmpty()) { throw new BasicException(errors.stream().collect(Collectors.joining("\n"))); } return params; } private boolean isGridReadOnly(DocumentDTO document) { return DocumentState.parseString(document.getState()) == DocumentState.ACCEPTED; } private List<String> checkAttributesRequirement(final DocumentPositionDTO position, final DocumentDTO document) { DocumentType documentType = DocumentType.parseString(document.getType()); if (documentType == DocumentType.RECEIPT || documentType == DocumentType.INTERNAL_INBOUND) { LocationDTO warehouseTo = getWarehouseById(document.getLocationTo_id()); return validatePositionAttributes(position, warehouseTo.isRequirePrice(), warehouseTo.isRequirebatch(), warehouseTo.isRequirEproductionDate(), warehouseTo.isRequirEexpirationDate()); } return Arrays.asList(); } private List<String> validateAvailableQuantity(DocumentPositionDTO position, DocumentDTO document, List<String> errors) { String type = document.getType(); String query = "SELECT draftmakesreservation FROM materialflowresources_documentpositionparameters LIMIT 1"; Boolean enabled = jdbcTemplate.queryForObject(query, new HashMap<String, Object>() { }, Boolean.class); if (enabled && DocumentType.isOutbound(type) && !document.getInBuffer()) { BigDecimal availableQuantity = getAvailableQuantityForProductAndLocation(position, tryGetProductIdByNumber(position.getProduct(), errors), document.getLocationFrom_id()); BigDecimal quantity = position.getQuantity(); if (availableQuantity == null || quantity.compareTo(availableQuantity) > 0) { errors.add("documentGrid.error.position.quantity.notEnoughResources"); } else { if (!StringUtils.isEmpty(position.getResource())) { BigDecimal resourceAvailableQuantity = getAvailableQuantityForResource(position, tryGetProductIdByNumber(position.getProduct(), errors), document.getLocationFrom_id()); if (resourceAvailableQuantity == null || quantity.compareTo(resourceAvailableQuantity) > 0) { errors.add("documentGrid.error.position.quantity.notEnoughResources"); } } } } return Arrays.asList(); } private BigDecimal getAvailableQuantityForResource(DocumentPositionDTO position, Long productId, Long locationId) { Long positionId = 0L; if (position != null) { positionId = position.getId(); } Long resourceId = null; if (position != null && !StringUtils.isEmpty(position.getResource())) { ResourceDTO resource = documentPositionService.getResourceByNumber(position.getResource()); if (resource != null) { resourceId = resource.getId(); } } if (resourceId == null) { return BigDecimal.ZERO; } String query = "SELECT availableQuantity FROM materialflowresources_resource WHERE id = :resource_id"; Map<String, Object> params = Maps.newHashMap(); params.put("product_id", productId); params.put("location_id", locationId); params.put("position_id", positionId); params.put("resource_id", resourceId); BigDecimal availableQuantity = jdbcTemplate.query(query, params, new ResultSetExtractor<BigDecimal>() { @Override public BigDecimal extractData(ResultSet rs) throws SQLException, DataAccessException { return rs.next() ? rs.getBigDecimal("availableQuantity") : BigDecimal.ZERO; } }); if (positionId != null && positionId != 0L) { String queryForOld = "SELECT product_id, quantity, resource_id FROM materialflowresources_position WHERE id = :position_id"; Map<String, Object> oldPosition = jdbcTemplate.query(queryForOld, params, new ResultSetExtractor<Map<String, Object>>() { @Override public Map<String, Object> extractData(ResultSet rs) throws SQLException, DataAccessException { Map<String, Object> result = Maps.newHashMap(); if (rs.next()) { result.put("product_id", rs.getLong("product_id")); result.put("quantity", rs.getBigDecimal("quantity")); result.put("resource_id", rs.getLong("resource_id")); } return result; } }); Long oldResource = (Long) oldPosition.get("resource_id"); if (oldResource != null) { if (oldResource.compareTo(resourceId) == 0) { availableQuantity = ((BigDecimal) oldPosition.get("quantity")).add(availableQuantity); } } } return availableQuantity; } private BigDecimal getAvailableQuantityForProductAndLocation(DocumentPositionDTO position, Long productId, Long locationId) { Long positionId = 0L; if (position != null) { positionId = position.getId(); } Long resourceId = null; if (position != null && !StringUtils.isEmpty(position.getResource())) { ResourceDTO resource = documentPositionService.getResourceByNumber(position.getResource()); if (resource != null) { resourceId = resource.getId(); } } String query = "SELECT availableQuantity FROM materialflowresources_resourcestock " + "WHERE product_id = :product_id AND location_id = :location_id"; Map<String, Object> params = Maps.newHashMap(); params.put("product_id", productId); params.put("location_id", locationId); params.put("position_id", positionId); params.put("resource_id", resourceId); BigDecimal availableQuantity = jdbcTemplate.query(query, params, new ResultSetExtractor<BigDecimal>() { @Override public BigDecimal extractData(ResultSet rs) throws SQLException, DataAccessException { return rs.next() ? rs.getBigDecimal("availableQuantity") : BigDecimal.ZERO; } }); if (positionId != null && positionId != 0L) { String queryForOld = "SELECT product_id, quantity, resource_id FROM materialflowresources_position WHERE id = :position_id"; Map<String, Object> oldPosition = jdbcTemplate.query(queryForOld, params, new ResultSetExtractor<Map<String, Object>>() { @Override public Map<String, Object> extractData(ResultSet rs) throws SQLException, DataAccessException { Map<String, Object> result = Maps.newHashMap(); if (rs.next()) { result.put("product_id", rs.getLong("product_id")); result.put("quantity", rs.getBigDecimal("quantity")); result.put("resource_id", rs.getLong("resource_id")); } return result; } }); if (productId.compareTo((Long) oldPosition.get("product_id")) == 0) { availableQuantity = ((BigDecimal) oldPosition.get("quantity")).add(availableQuantity); } } return availableQuantity; } private List<String> validatePositionAttributes(DocumentPositionDTO position, boolean requirePrice, boolean requireBatch, boolean requireProductionDate, boolean requireExpirationDate) { List<String> errors = new ArrayList<>(); if (requirePrice && (position.getPrice() == null || BigDecimal.ZERO.compareTo(position.getPrice()) == 0)) { errors.add("documentGrid.error.position.price.required"); } if (requireBatch && (position.getBatch() == null || position.getBatch().trim().isEmpty())) { errors.add("documentGrid.error.position.batch.required"); } if (requireProductionDate && position.getProductionDate() == null) { errors.add("documentGrid.error.position.productionDate.required"); } if (requireExpirationDate && position.getExpirationDate() == null) { errors.add("documentGrid.error.position.expirationDate.required"); } return errors; } private List<String> validateResources(final DocumentPositionDTO position, final DocumentDTO document) { if (DocumentState.parseString(document.getState()).compareTo(DocumentState.ACCEPTED) == 0) { return Arrays.asList(); } DocumentType type = DocumentType.parseString(document.getType()); if (DocumentType.TRANSFER.equals(type) || DocumentType.RELEASE.equals(type) || DocumentType.INTERNAL_OUTBOUND.equals(type)) { LocationDTO warehouseFrom = getWarehouseById(document.getLocationFrom_id()); String algorithm = warehouseFrom.getAlgorithm(); if (WarehouseAlgorithm.MANUAL.getStringValue().compareTo(algorithm) == 0) { boolean isValid = position.getResource() != null; if (!isValid) { return Arrays.asList("documentGrid.error.position.resource.required"); } } } if (!Strings.isNullOrEmpty(position.getResource())) { boolean find = false; DataResponse resourcesResponse = documentPositionService.getResourcesResponse(position.getDocument(), "", position.getProduct(), position.getConversion(), position.getAdditionalCode()); List<AbstractDTO> resources = resourcesResponse.getEntities(); for (AbstractDTO abstractDTO : resources) { ResourceDTO resourceDTO = (ResourceDTO) abstractDTO; if (position.getResource().equals(resourceDTO.getNumber())) { find = true; break; } } if (!find) { position.setResource(null); return Arrays.asList("documentGrid.error.position.resource.invalid"); } } return Arrays.asList(); } private List<String> validateDates(final DocumentPositionDTO position) { Date productionDate = position.getProductionDate(); Date expirationDate = position.getExpirationDate(); if (productionDate != null && expirationDate != null && expirationDate.compareTo(productionDate) < 0) { return Arrays.asList("documentGrid.error.position.expirationDate.lessThenProductionDate"); } return Arrays.asList(); } private LocationDTO getWarehouseById(Long id) { BeanPropertyRowMapper<LocationDTO> x = new BeanPropertyRowMapper<>(LocationDTO.class); x.setPrimitivesDefaultedForNullValue(true); return jdbcTemplate.queryForObject("SELECT * FROM materialflow_location WHERE id = :id", Collections.singletonMap("id", id), x); } private Collection<? extends String> validateQuantity(DocumentPositionDTO position) { if (position.getQuantity() == null) { return Arrays.asList("documentGrid.error.position.quantity.required"); } else if (BigDecimal.ZERO.compareTo(position.getQuantity()) >= 0) { return Arrays.asList("documentGrid.error.position.quantity.invalid"); } return validateBigDecimal(position.getQuantity(), "quantity", 5, 9); } private Collection<? extends String> validateGivenquantity(DocumentPositionDTO position) { if (position.getGivenquantity() == null) { return Arrays.asList("documentGrid.error.position.givenquantity.required"); } else if (BigDecimal.ZERO.compareTo(position.getGivenquantity()) >= 0) { return Arrays.asList("documentGrid.error.position.givenquantity.invalid"); } return validateBigDecimal(position.getGivenquantity(), "givenquantity", 5, 9); } private Collection<? extends String> validatePrice(DocumentPositionDTO position) { if (position.getPrice() != null && BigDecimal.ZERO.compareTo(position.getPrice()) > 0) { return Arrays.asList("documentGrid.error.position.price.invalid"); } if (position.getPrice() != null) { return validateBigDecimal(position.getPrice(), "price", 5, 7); } return Arrays.asList(); } private Collection<? extends String> validateConversion(DocumentPositionDTO position) { if (position.getConversion() == null) { return Arrays.asList("documentGrid.error.position.conversion.required"); } else { if (BigDecimal.ZERO.compareTo(position.getConversion()) >= 0) { return Arrays.asList("documentGrid.error.position.conversion.invalid"); } return validateBigDecimal(position.getConversion(), "conversion", 5, 7); } } private Collection<? extends String> validateAdditionalCode(DocumentPositionDTO position) { String additionalCode = position.getAdditionalCode(); if (!StringUtils.isEmpty(additionalCode)) { try { Map<String, Object> filters = new HashMap<>(); filters.put("code", additionalCode); filters.put("productNumber", position.getProduct()); Long additionalCodeId = jdbcTemplate .queryForObject( "SELECT additionalcode.id FROM basic_additionalcode additionalcode WHERE additionalcode.code = :code " + "AND additionalcode.product_id IN (SELECT id FROM basic_product WHERE number = :productNumber)", filters, Long.class); } catch (EmptyResultDataAccessException e) { return Arrays.asList("documentGrid.error.position.additionalCode.doesntMatch"); } } return Arrays.asList(); } private Map<String, Object> tryMapDocumentPositionVOToParams(DocumentPositionDTO vo, List<String> errors) { Map<String, Object> params = new HashMap<>(); params.put("id", vo.getId()); params.put("product_id", tryGetProductIdByNumber(vo.getProduct(), errors)); params.put("additionalcode_id", tryGetAdditionalCodeIdByCode(vo.getAdditionalCode(), errors)); params.put("quantity", vo.getQuantity()); params.put("givenquantity", vo.getGivenquantity()); params.put("givenunit", vo.getGivenunit()); params.put("conversion", vo.getUnit().equals(vo.getGivenunit()) ? 1 : vo.getConversion()); params.put("expirationDate", vo.getExpirationDate()); params.put("palletnumber_id", tryGetPalletNumberIdByNumber(vo.getPalletNumber(), errors)); params.put("typeofpallet", vo.getTypeOfPallet()); params.put("storagelocation_id", tryGetStorageLocationIdByNumber(vo.getStorageLocation(), errors)); params.put("document_id", vo.getDocument()); params.put("productionDate", vo.getProductionDate()); params.put("price", vo.getPrice()); params.put("resource_id", tryGetResourceIdByNumber(vo.getResource(), errors)); params.put("batch", vo.getBatch().trim()); params.put("waste", vo.isWaste()); return params; } private Long tryGetProductIdByNumber(String productNumber, List<String> errors) { if (Strings.isNullOrEmpty(productNumber)) { return null; } try { Long productId = jdbcTemplate.queryForObject( "SELECT product.id FROM basic_product product WHERE product.number = :number", Collections.singletonMap("number", productNumber), Long.class); return productId; } catch (EmptyResultDataAccessException e) { errors.add(String.format("Nie znaleziono takiego produktu: '%s'.", productNumber)); return null; } } private Long tryGetAdditionalCodeIdByCode(String additionalCode, List<String> errors) { if (Strings.isNullOrEmpty(additionalCode)) { return null; } try { Long additionalCodeId = jdbcTemplate.queryForObject( "SELECT additionalcode.id FROM basic_additionalcode additionalcode WHERE additionalcode.code = :code", Collections.singletonMap("code", additionalCode), Long.class); return additionalCodeId; } catch (EmptyResultDataAccessException e) { errors.add(String.format("Nie znaleziono takiego dodatkowego kodu: '%s'.", additionalCode)); return null; } } private Long tryGetPalletNumberIdByNumber(String palletNumber, List<String> errors) { if (Strings.isNullOrEmpty(palletNumber)) { return null; } try { Long palletNumberId = jdbcTemplate.queryForObject( "SELECT palletnumber.id FROM basic_palletnumber palletnumber WHERE palletnumber.number = :number", Collections.singletonMap("number", palletNumber), Long.class); return palletNumberId; } catch (EmptyResultDataAccessException e) { errors.add(String.format("Nie znaleziono takiego numeru palety: '%s'.", palletNumber)); return null; } } private Long tryGetStorageLocationIdByNumber(String storageLocationNumber, List<String> errors) { if (Strings.isNullOrEmpty(storageLocationNumber)) { return null; } try { Long storageLocationId = jdbcTemplate .queryForObject( "SELECT storagelocation.id FROM materialflowresources_storagelocation storagelocation WHERE storagelocation.number = :number", Collections.singletonMap("number", storageLocationNumber), Long.class); return storageLocationId; } catch (EmptyResultDataAccessException e) { errors.add(String.format("Nie znaleziono takiego miejsca składowania: '%s'.", storageLocationNumber)); return null; } } private Object tryGetResourceIdByNumber(String resource, List<String> errors) { if (Strings.isNullOrEmpty(resource)) { return null; } try { Long resourceId = jdbcTemplate.queryForObject("SELECT id FROM materialflowresources_resource WHERE number = :number", Collections.singletonMap("number", resource), Long.class); return resourceId; } catch (EmptyResultDataAccessException e) { errors.add(String.format("Nie znaleziono takiego zasobu: '%s'.", resource)); return null; } } private List<String> validateBigDecimal(BigDecimal value, String field, int maxScale, int maxPrecision) { List<String> errors = new ArrayList<>(); BigDecimal noZero = value.stripTrailingZeros(); int scale = noZero.scale(); int precision = noZero.precision(); if (scale < 0) { precision -= scale; scale = 0; } String fieldName = translationService.translate("documentGrid.gridColumn." + field, LocaleContextHolder.getLocale()); if (scale > maxScale) { errors.add(String.format( translationService.translate("documentGrid.error.position.bigdecimal.invalidScale", LocaleContextHolder.getLocale()), fieldName, maxScale)); } if ((precision - scale) > maxPrecision) { errors.add(String.format( translationService.translate("documentGrid.error.position.bigdecimal.invalidPrecision", LocaleContextHolder.getLocale()), fieldName, maxPrecision)); } return errors; } private Collection<? extends String> validatePallet(DocumentPositionDTO position, DocumentDTO document) { List<String> errors = new ArrayList<>(); if (isInDocument(document)) { if (existsNotMatchingResourceForPalletNumber(position, document)) { errors.add(translationService.translate( "documentGrid.error.position.existsOtherResourceForPalletAndStorageLocation", LocaleContextHolder.getLocale())); } else if (existsNotMatchingPositionForPalletNumber(position, document)) { errors.add(translationService.translate( "documentGrid.error.position.existsOtherPositionForPalletAndStorageLocation", LocaleContextHolder.getLocale())); } else if (existsNotMatchingDeliveredProductForPalletNumber(position, document)) { errors.add(translationService.translate( "documentGrid.error.position.existsOtherDeliveredProductForPalletAndStorageLocation", LocaleContextHolder.getLocale())); } } return errors; } private boolean isInDocument(DocumentDTO document) { DocumentType type = DocumentType.parseString(document.getType()); return type == DocumentType.RECEIPT || type == DocumentType.INTERNAL_INBOUND || type == DocumentType.TRANSFER; } private boolean existsNotMatchingResourceForPalletNumber(DocumentPositionDTO position, DocumentDTO document) { if (Strings.isNullOrEmpty(position.getPalletNumber())) { return false; } StringBuilder query = new StringBuilder(); query.append("SELECT count(*) FROM materialflowresources_resource resource "); query.append("JOIN basic_palletnumber pallet ON (resource.palletnumber_id = pallet.id) "); query.append("LEFT JOIN materialflowresources_storagelocation storage ON (resource.storagelocation_id = storage.id) "); query.append("WHERE pallet.number = :palletNumber AND (storage.number <> :storageNumber OR resource.typeofpallet <> :typeOfPallet) "); query.append("AND resource.location_id = :locationId"); Map<String, Object> params = new HashMap<>(); params.put("palletNumber", position.getPalletNumber()); params.put("storageNumber", position.getStorageLocation()); params.put("typeOfPallet", position.getTypeOfPallet()); params.put("locationId", document.getLocationTo_id()); Long count = jdbcTemplate.queryForObject(query.toString(), params, Long.class); return count > 0; } private boolean existsNotMatchingPositionForPalletNumber(DocumentPositionDTO position, DocumentDTO document) { if (Strings.isNullOrEmpty(position.getPalletNumber())) { return false; } StringBuilder query = new StringBuilder(); query.append("SELECT count(*) FROM materialflowresources_position position "); query.append("JOIN basic_palletnumber pallet ON (position.palletnumber_id = pallet.id) "); query.append("LEFT JOIN materialflowresources_storagelocation storage ON (position.storagelocation_id = storage.id) "); query.append("WHERE pallet.number = :palletNumber AND (storage.number <> :storageNumber OR position.typeofpallet <> :typeOfPallet) "); query.append("AND position.document_id = :documentId AND position.id <> :positionId "); Map<String, Object> params = new HashMap<>(); params.put("palletNumber", position.getPalletNumber()); params.put("storageNumber", position.getStorageLocation()); params.put("typeOfPallet", position.getTypeOfPallet()); params.put("documentId", position.getDocument()); params.put("positionId", position.getId()); Long count = jdbcTemplate.queryForObject(query.toString(), params, Long.class); return count > 0; } private boolean existsNotMatchingDeliveredProductForPalletNumber(DocumentPositionDTO position, DocumentDTO document) { if (Strings.isNullOrEmpty(position.getPalletNumber())) { return false; } StringBuilder query = new StringBuilder(); query.append("SELECT count(*) FROM deliveries_deliveredproduct position "); query.append("JOIN basic_palletnumber pallet ON (position.palletnumber_id = pallet.id) "); query.append("JOIN deliveries_delivery delivery ON position.delivery_id = delivery.id "); query.append("LEFT JOIN materialflowresources_storagelocation storage ON (position.storagelocation_id = storage.id) "); query.append("WHERE pallet.number = :palletNumber AND (storage.number <> :storageNumber OR position.pallettype <> :typeOfPallet) "); query.append("AND delivery.location_id = :locationId"); Map<String, Object> params = new HashMap<>(); params.put("palletNumber", position.getPalletNumber()); params.put("storageNumber", position.getStorageLocation()); params.put("typeOfPallet", position.getTypeOfPallet()); params.put("locationId", document.getLocationTo_id()); Long count = jdbcTemplate.queryForObject(query.toString(), params, Long.class); return count > 0; } }