InventoryServiceImpl.java
/*
* Copyright 2020 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.gringlobal.service.impl;
import static org.gringlobal.model.community.CommunityAppSettings.BARCODE_INVENTORY;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import javax.persistence.EntityManager;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import com.blazebit.persistence.CriteriaBuilderFactory;
import com.blazebit.persistence.querydsl.BlazeJPAQuery;
import com.querydsl.core.Tuple;
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.PathBuilder;
import com.querydsl.jpa.JPQLQuery;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.genesys.blocks.model.filters.NumberFilter;
import org.genesys.blocks.security.SecurityContextUtil;
import org.genesys.filerepository.InvalidRepositoryFileDataException;
import org.genesys.filerepository.InvalidRepositoryPathException;
import org.gringlobal.api.FilteredPage;
import org.gringlobal.api.Pagination;
import org.gringlobal.api.exception.InvalidApiUsageException;
import org.gringlobal.api.exception.NotFoundElement;
import org.gringlobal.application.config.GGCESecurityConfig;
import org.gringlobal.component.GGCE;
import org.gringlobal.custom.elasticsearch.SearchException;
import org.gringlobal.model.Accession;
import org.gringlobal.model.AccessionInvAttach;
import org.gringlobal.model.AppSetting;
import org.gringlobal.model.Cooperator;
import org.gringlobal.model.DateVersionEntityId.EntityIdAndModifiedDate;
import org.gringlobal.model.Geography;
import org.gringlobal.model.Inventory;
import org.gringlobal.model.InventoryAction;
import org.gringlobal.model.InventoryExtra;
import org.gringlobal.model.InventoryMaintenancePolicy;
import org.gringlobal.model.InventoryQualityStatus;
import org.gringlobal.model.LazyLoading;
import org.gringlobal.model.OrderRequest;
import org.gringlobal.model.OrderRequestItem;
import org.gringlobal.model.QAccession;
import org.gringlobal.model.QAccessionInvAnnotation;
import org.gringlobal.model.QAccessionInvAttach;
import org.gringlobal.model.QAccessionInvGroupMap;
import org.gringlobal.model.QAccessionInvName;
import org.gringlobal.model.QCropTraitObservation;
import org.gringlobal.model.QCropTraitObservationData;
import org.gringlobal.model.QInventory;
import org.gringlobal.model.QInventoryAction;
import org.gringlobal.model.QInventoryViability;
import org.gringlobal.model.QInventoryViabilityAction;
import org.gringlobal.model.QInventoryViabilityAttach;
import org.gringlobal.model.QInventoryViabilityData;
import org.gringlobal.model.QInventoryViabilityDataEnvironmentMap;
import org.gringlobal.model.QOrderRequestItem;
import org.gringlobal.model.QSiteCounter;
import org.gringlobal.model.SeedInventoryExtra;
import org.gringlobal.model.Site;
import org.gringlobal.model.TaxonomyGenus;
import org.gringlobal.model.TaxonomySpecies;
import org.gringlobal.model.TissueCultureExtra;
import org.gringlobal.model.AbstractAction.ActionState;
import org.gringlobal.model.community.CommunityCodeValues;
import org.gringlobal.model.community.SecurityAction;
import org.gringlobal.model.community.CommunityCodeValues.CodeValueDef;
import org.gringlobal.model.community.InventoryQualityStatusAttach;
import org.gringlobal.model.community.QInventoryQualityStatusAttach;
import org.gringlobal.model.workflow.WorkflowActionStep;
import org.gringlobal.persistence.AccessionInvAnnotationRepository;
import org.gringlobal.persistence.AccessionInvAttachRepository;
import org.gringlobal.persistence.AccessionInvGroupMapRepository;
import org.gringlobal.persistence.AccessionInvNameRepository;
import org.gringlobal.persistence.AccessionRepository;
import org.gringlobal.persistence.CropTraitObservationDataRepository;
import org.gringlobal.persistence.CropTraitObservationRepository;
import org.gringlobal.persistence.InventoryActionRepository;
import org.gringlobal.persistence.InventoryExtraRepository;
import org.gringlobal.persistence.InventoryMaintenancePolicyRepository;
import org.gringlobal.persistence.InventoryQualityStatusRepository;
import org.gringlobal.persistence.InventoryRepository;
import org.gringlobal.persistence.InventoryRepositoryCustom;
import org.gringlobal.persistence.InventoryViabilityActionRepository;
import org.gringlobal.persistence.InventoryViabilityAttachRepository;
import org.gringlobal.persistence.InventoryViabilityDataEnvMapRepository;
import org.gringlobal.persistence.InventoryViabilityDataRepository;
import org.gringlobal.persistence.InventoryViabilityRepository;
import org.gringlobal.persistence.OrderRequestItemRepository;
import org.gringlobal.persistence.SiteRepository;
import org.gringlobal.service.AccessionInvGroupService;
import org.gringlobal.service.AppSettingsService;
import org.gringlobal.service.InventoryActionService;
import org.gringlobal.service.InventoryActionService.InventoryActionRequest;
import org.gringlobal.service.InventoryActionService.InventoryActionScheduleFilter;
import org.gringlobal.service.InventoryAttachmentService;
import org.gringlobal.service.InventoryAttachmentService.InventoryAttachmentRequest;
import org.gringlobal.service.InventoryExtraService;
import org.gringlobal.service.InventoryQualityStatusAttachmentService;
import org.gringlobal.service.InventoryQualityStatusService;
import org.gringlobal.service.InventoryService;
import org.gringlobal.service.InvitroInventoryService;
import org.gringlobal.service.MethodService;
import org.gringlobal.service.OrderRequestService;
import org.gringlobal.service.TemplatingService;
import org.gringlobal.service.filter.AccessionFilter;
import org.gringlobal.service.filter.AccessionInvAttachFilter;
import org.gringlobal.service.filter.InventoryActionFilter;
import org.gringlobal.service.filter.InventoryFilter;
import org.gringlobal.service.filter.InventoryQualityStatusFilter;
import org.gringlobal.service.filter.SiteFilter;
import org.genesys.blocks.util.TransactionHelper;
import org.hibernate.Hibernate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.support.Querydsl;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.multipart.MultipartFile;
import com.google.common.collect.Lists;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.EntityPath;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.NumberPath;
import com.querydsl.jpa.impl.JPAQuery;
@Service
@Transactional(readOnly = true)
@Validated
@Slf4j
public class InventoryServiceImpl extends FilteredCRUDService2Impl<Inventory, InventoryFilter, InventoryRepository> implements InventoryService, InvitroInventoryService {
private static final String[] BOOST_FIELDS = { "inventoryNumber", "names.plantName", "accession.accessionNumber", "accession.taxonomySpecies.name" };
@Autowired
private InventoryMaintenancePolicyRepository inventoryMaintPolicyRepo;
@Autowired
private AccessionInvNameRepository accessionInvNameRepository;
@Autowired
private AccessionInvGroupService accessionInvGroupService;
@Autowired
private AccessionInvAttachRepository attachRepository;
@Autowired
private InventoryActionRepository inventoryActionRepository;
@Autowired
private AccessionRepository accessionRepository;
@Autowired
private OverviewHelper overviewHelper;
@Autowired
private AppSettingsService appSettingsService;
@Autowired
private TemplatingService templatingService;
@Autowired
private InventoryActionService inventoryActionService;
@Autowired
QuerydslPredicateExecutor<InventoryAction> actionFinder;
@Autowired
private EntityManager em;
@Autowired
private TransactionHelper transactionHelper;
@Autowired
private InventoryExtraService inventoryExtraService;
@Autowired
@Lazy
private OrderRequestService orderRequestService;
@Autowired
private GGCESecurityConfig.GgceSec ggceSec;
@Autowired
private InventoryAttachmentService attachmentService;
@Autowired
private SiteRepository siteRepository;
@Autowired
private CriteriaBuilderFactory criteriaBuilderFactory;
@Autowired
private InventoryQualityStatusRepository inventoryQualityStatusRepository;
@Autowired
private AccessionInvGroupMapRepository accessionInvGroupMapRepository;
@Autowired
private AccessionInvAnnotationRepository accessionInvAnnotationRepository;
@Autowired
private CropTraitObservationDataRepository cropTraitObservationDataRepository;
@Autowired
private CropTraitObservationRepository cropTraitObservationRepository;
@Autowired
private InventoryViabilityRepository inventoryViabilityRepository;
@Autowired
private InventoryViabilityDataRepository inventoryViabilityDataRepository;
@Autowired
private InventoryViabilityActionRepository inventoryViabilityActionRepository;
@Autowired
private InventoryViabilityAttachRepository inventoryViabilityAttachRepository;
@Autowired
private InventoryViabilityDataEnvMapRepository viabilityDataEnvMapRepository;
@Autowired
private OrderRequestItemRepository orderRequestItemRepository;
@Autowired
private AccessionInvAttachRepository accessionInvAttachRepository;
/** Units of measure representing weight in grams */
public static final Set<String> GRAM_UNITS = Set.of(CommunityCodeValues.UNIT_OF_QUANTITY_GRAM.value);
/** Units of measure representing seed */
public static final Set<String> SEED_UNITS = Set.of(CommunityCodeValues.UNIT_OF_QUANTITY_SEED.value, CommunityCodeValues.UNIT_OF_QUANTITY_COUNT.value);
@Component
protected static class AttachmentSupport extends BaseAttachmentFilteredSupport<Inventory, AccessionInvAttach, InventoryAttachmentRequest, AccessionInvAttachFilter> implements InventoryAttachmentService {
@Autowired
protected GGCESecurityConfig.GgceSec ggceSec;
public AttachmentSupport() {
super(QAccessionInvAttach.accessionInvAttach.inventory().id, QAccessionInvAttach.accessionInvAttach.id);
}
// Limit to sites with READ permission
@Override
protected AccessionInvAttachFilter adjustFilter(AccessionInvAttachFilter filter) {
var siteIds = ggceSec.getSiteIds(SecurityAction.InventoryAttachment.name(), "READ");
if (siteIds != null) {
log.debug("Forcing Site IDs: {}", siteIds);
var attachFilter = new AccessionInvAttachFilter();
attachFilter.inventory = new InventoryFilter().includeSystem(null);
attachFilter.inventory.site(new SiteFilter());
attachFilter.inventory.site.id(siteIds);
attachFilter.AND = filter;
return attachFilter;
} else {
// No change
return filter;
}
}
/**
* Based on the original `inventory_attach_wizard_get_filepath` dataview.
* <ul>
* <li>AIA/</li>
* <li>${accession.taxonomySpecies.taxonomyGenus.genusName}</li>
* <li>Conditional: last 2 digits of accession ID: ${accession.id % 100} if there are over 2000 accessions of that `TaxonomyGenus`</li>
* <li>${accession.id}</li>
* </ul>
*
* @param inventory
* @return repository path for attachments
*/
@Override
public Path createRepositoryPath(Inventory inventory) {
inventory = owningEntityRepository.getReferenceById(inventory.getId());
Hibernate.initialize(inventory.getAccession());
Accession accession = inventory.getAccession();
Hibernate.initialize(accession.getTaxonomySpecies());
TaxonomySpecies taxonomySpecies = accession.getTaxonomySpecies();
Hibernate.initialize(taxonomySpecies.getTaxonomyGenus());
TaxonomyGenus taxonomyGenus = taxonomySpecies.getTaxonomyGenus();
StringBuilder builder = new StringBuilder("/").append("AIA").append("/")
.append(taxonomyGenus.getGenusName()).append("/")
.append(accession.getId() % 100).append("/") // last 2 digits
.append(accession.getId()).append("/")
.append(inventory.getId());
return Paths.get(builder.toString());
}
@Override
@Transactional
@PostAuthorize("hasAuthority('GROUP_ADMINS') or @ggceSec.actionAllowed('InventoryAttachment', 'CREATE', returnObject.inventory.site)")
protected AccessionInvAttach createAttach(Inventory entity, AccessionInvAttach source) {
AccessionInvAttach attach = new AccessionInvAttach();
attach.apply(source);
attach.setVirtualPath(source.getVirtualPath()); // SOAP uses this to create the record
attach.setInventory(entity);
return attach;
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
@PreAuthorize("hasAuthority('GROUP_ADMINS') or @ggceSec.actionAllowed('InventoryAttachment', 'CREATE', #entity.site)")
public AccessionInvAttach uploadFile(Inventory entity, MultipartFile file, InventoryAttachmentRequest metadata) throws IOException, InvalidRepositoryPathException,
InvalidRepositoryFileDataException {
return super.uploadFile(entity, file, metadata);
}
@Override
@Transactional
@PreAuthorize("hasAuthority('GROUP_ADMINS') or @ggceSec.actionAllowed('InventoryAttachment', 'DELETE', #entity.site)")
public AccessionInvAttach removeFile(Inventory entity, Long attachmentId) {
return super.removeFile(entity, attachmentId);
}
@Override
@Transactional
@PostAuthorize("hasAuthority('GROUP_ADMINS') or @ggceSec.actionAllowed('InventoryAttachment', 'CREATE', returnObject.inventory.site)")
public AccessionInvAttach create(AccessionInvAttach source) {
return _lazyLoad(createFast(source));
}
@Override
@Transactional
@PostAuthorize("hasAuthority('GROUP_ADMINS') or @ggceSec.actionAllowed('InventoryAttachment', 'CREATE', returnObject.inventory.site)")
public AccessionInvAttach createFast(AccessionInvAttach source) {
var inventory = owningEntityRepository.getReferenceById(source.getInventory().getId());
Hibernate.initialize(inventory.getSite()); // For permissions
var attach = createAttach(inventory, source);
var savedAttach = repository.save(attach);
return savedAttach;
}
@Override
@Transactional
@PreAuthorize("hasAuthority('GROUP_ADMINS') or @ggceSec.actionAllowed('InventoryAttachment', 'WRITE', #target.inventory.site)")
public AccessionInvAttach update(AccessionInvAttach updated, AccessionInvAttach target) {
return _lazyLoad(updateFast(updated, target));
}
@Override
@Transactional
@PreAuthorize("hasAuthority('GROUP_ADMINS') or @ggceSec.actionAllowed('InventoryAttachment', 'WRITE', #target.inventory.site)")
public AccessionInvAttach updateFast(AccessionInvAttach updated, AccessionInvAttach target) {
target.apply(updated);
updateRepositoryFileMetadata(updated, target);
return repository.save(target);
}
@Override
@Transactional
@PreAuthorize("hasAuthority('GROUP_ADMINS') or @ggceSec.actionAllowed('InventoryAttachment', 'DELETE', #entity.inventory.site)")
public AccessionInvAttach remove(AccessionInvAttach entity) {
return super.remove(entity);
}
/**
* Utility to find attachments belonging to an inventory.
* Used by SOAP FilesEndpoint.
*/
@Override
public AccessionInvAttach findAttachment(Inventory inventory, Path filePath) {
return attachFinder.findOne(
QAccessionInvAttach.accessionInvAttach.inventory().eq(inventory)
.and(QAccessionInvAttach.accessionInvAttach.virtualPath.eq(filePath.toString()))
).orElse(null);
}
@Override
@Transactional
@PreAuthorize("@ggceSec.actionAllowed('InventoryAttachment', 'WRITE', #source.inventory.site)")
public void shareAttachment(AccessionInvAttach source, List<Inventory> inventories) {
List<AccessionInvAttach> createdAttachments = new ArrayList<>(inventories.size());
for (Inventory inventory : inventories) {
var attach = createAttach(inventory, source);
attach.setContentType(source.getContentType());
attach.setTitle(source.getTitle());
attach.setVirtualPath(source.getVirtualPath());
attach.setRepositoryFile(source.getRepositoryFile());
createdAttachments.add(attach);
}
attachRepository.saveAll(createdAttachments);
}
}
@Component
protected static class ActionSupport extends BaseActionSupport<Inventory, InventoryAction, InventoryActionFilter, InventoryActionRepository, InventoryActionRequest, InventoryActionScheduleFilter>
implements InventoryActionService {
@Autowired
private InventoryRepository inventoryRepository;
@Autowired
private MethodService methodService;
@Override
protected EntityPath<Inventory> getOwningEntityPath() {
return QInventoryAction.inventoryAction.inventory();
}
@Override
protected void initializeActionDetails(List<InventoryAction> actions) {
actions.forEach(action -> {
Hibernate.initialize(action.getInventory());
action.getInventory().lazyLoad();
});
}
protected InventoryFilter adjustFilter(InventoryFilter filter) {
var siteIds = ggceSec.getSiteIds(SecurityAction.InventoryData.name(), "READ");
if (siteIds != null) {
log.debug("Forcing Site IDs: {}", siteIds);
var siteFilter = new InventoryFilter().includeSystem(null);
siteFilter.site(new SiteFilter());
siteFilter.site.id(siteIds);
siteFilter.AND = filter;
return siteFilter;
} else {
// No change
return filter;
}
}
@Override
protected InventoryActionFilter adjustFilter(InventoryActionFilter filter) {
var siteIds = ggceSec.getSiteIds(SecurityAction.InventoryData.name(), "READ");
if (siteIds != null) {
log.debug("Forcing Site IDs: {}", siteIds);
var siteFilter = new InventoryActionFilter();
siteFilter.inventory = new InventoryFilter().includeSystem(null);
siteFilter.inventory.site(new SiteFilter());
siteFilter.inventory.site.id(siteIds);
siteFilter.AND = filter;
return siteFilter;
} else {
// No change
return filter;
}
}
@Override
protected void applyOwningEntityFilter(InventoryActionScheduleFilter filter, String owningEntityAlias, List<Predicate> predicates) {
QInventory qInventory = new QInventory(owningEntityAlias);
if (predicates != null) {
filter.inventory = adjustFilter(filter.inventory);
if (filter.inventory != null) {
predicates.addAll(filter.inventory.collectPredicates(qInventory));
}
}
}
@Override
protected InventoryAction createAction(Inventory owningEntity) {
InventoryAction action = new InventoryAction();
action.setInventory(owningEntity);
return action;
}
@Override
protected void updateAction(InventoryAction action, InventoryActionRequest request) {
action.setQuantity(request.quantity);
action.setQuantityUnitCode(request.quantityUnitCode);
action.setFormCode(request.formCode);
if (request.method != null && !request.method.isNew()) {
action.setMethod(methodService.get(request.method.getId()));
}
}
@Override
protected InventoryAction prepareNextWorkflowStepAction(WorkflowActionStep nextStep, InventoryAction completedAction) {
InventoryAction nextAction = new InventoryAction();
nextAction.setInventory(new Inventory(completedAction.getInventory().getId()));
return nextAction;
}
@Override
@Transactional
@PostAuthorize("@ggceSec.actionAllowed('InventoryData', 'ADMINISTRATION', returnObject.inventory.site)")
public InventoryAction createFast(InventoryAction source) {
log.debug("Create InventoryAction. Input data {}", source);
InventoryAction inventoryAction = new InventoryAction();
inventoryAction.apply(source);
InventoryAction saved = repository.save(inventoryAction);
if (saved.getState() == ActionState.COMPLETED) {
if (CommunityCodeValues.INVENTORY_ACTION_WITHDRAW.value.equals(saved.getActionNameCode())) {
// Automatically reduce inventory quantity
var updatedInventory = reduceInventory(saved, inventoryRepository.getReferenceById(saved.getInventory().getId()));
saved.setInventory(updatedInventory);
} else if (CommunityCodeValues.INVENTORY_ACTION_HARVEST.value.equals(saved.getActionNameCode())) {
// Set quantity on hand to 0 when HARVEST is completed
var plantedInventory = inventoryRepository.getReferenceById(saved.getInventory().getId());
if (Objects.equals("Y", plantedInventory.getIsAutoDeducted())) {
// If auto-deducted, then set quantity to 0 to mark it as finished
plantedInventory.setQuantityOnHand(0.0d);
plantedInventory = inventoryRepository.save(plantedInventory);
}
saved.setInventory(plantedInventory);
}
}
return saved;
}
@Override
@Transactional
@PostAuthorize("@ggceSec.actionAllowed('InventoryData', 'ADMINISTRATION', returnObject.inventory.site)")
public InventoryAction create(InventoryAction source) {
return _lazyLoad(createFast(source));
}
@Override
@Transactional
@PostAuthorize("@ggceSec.actionAllowed('InventoryData', 'ADMINISTRATION', returnObject.inventory.site)")
public InventoryAction update(InventoryAction updated) {
return super.update(updated);
}
@Override
@Transactional
@PostAuthorize("@ggceSec.actionAllowed('InventoryData', 'ADMINISTRATION', returnObject.inventory.site)")
public InventoryAction remove(InventoryAction entity) {
return super.remove(entity);
}
@Override
@Transactional
@PostAuthorize("@ggceSec.actionAllowed('InventoryData', 'ADMINISTRATION', returnObject.inventory.site)")
public InventoryAction updateFast(InventoryAction updated, InventoryAction target) {
var canBeClosed = target.getState() != ActionState.COMPLETED;
var saved = super.update(updated, target);
if (canBeClosed && saved.getState() == ActionState.COMPLETED) {
if (CommunityCodeValues.INVENTORY_ACTION_WITHDRAW.value.equals(saved.getActionNameCode())) {
// Automatically reduce inventory quantity
var updatedInventory = reduceInventory(saved, inventoryRepository.getReferenceById(saved.getInventory().getId()));
saved.setInventory(updatedInventory);
} else if (CommunityCodeValues.INVENTORY_ACTION_HARVEST.value.equals(saved.getActionNameCode())) {
// Set quantity on hand to 0 when HARVEST is completed
var plantedInventory = inventoryRepository.getReferenceById(saved.getInventory().getId());
if (Objects.equals("Y", plantedInventory.getIsAutoDeducted())) {
// If auto-deducted, then set quantity to 0 to mark it as finished
plantedInventory.setQuantityOnHand(0.0d);
plantedInventory = inventoryRepository.save(plantedInventory);
}
saved.setInventory(plantedInventory);
}
}
return saved;
}
@Override
@Transactional
@PostAuthorize("@ggceSec.actionAllowed('InventoryData', 'ADMINISTRATION', returnObject.inventory.site)")
public InventoryAction update(InventoryAction updated, InventoryAction target) {
return _lazyLoad(updateFast(updated, target));
}
@Override
protected Iterable<Inventory> findOwningEntities(Set<Long> id) {
return inventoryRepository.findAll(QInventory.inventory.id.in(id));
}
private Inventory reduceInventory(InventoryAction inventoryAction, Inventory inventory) {
assert (CommunityCodeValues.INVENTORY_ACTION_WITHDRAW.value.equals(inventoryAction.getActionNameCode()));
// if flagged as auto-deducted, reduce quantity on hand
if (!"Y".equals(inventory.getIsAutoDeducted())) {
return inventory;
} else {
if (!inventory.getQuantityOnHandUnitCode().equals(inventoryAction.getQuantityUnitCode())) {
throw new InvalidApiUsageException("Quantity Unit Code of the action does not match the unit code of inventory item.");
}
if (inventory.getQuantityOnHand() < inventoryAction.getQuantity()) {
throw new InvalidApiUsageException("Insufficient quantity on hand.");
} else {
// update quantity
inventory.setQuantityOnHand(inventory.getQuantityOnHand() - inventoryAction.getQuantity());
return inventoryRepository.save(inventory);
}
}
}
}
@Service
@Transactional(readOnly = true)
@Validated
protected static class InventoryQualityStatusServiceImpl extends FilteredCRUDService2Impl<InventoryQualityStatus, InventoryQualityStatusFilter, InventoryQualityStatusRepository>
implements InventoryQualityStatusService {
@Override
@Transactional
@PostAuthorize("hasAuthority('GROUP_ADMINS') or @ggceSec.actionAllowed('InventoryData', 'WRITE', returnObject.inventory.site)")
public InventoryQualityStatus create(InventoryQualityStatus source) {
return _lazyLoad(createFast(source));
}
@Override
@Transactional
@PostAuthorize("hasAuthority('GROUP_ADMINS') or @ggceSec.actionAllowed('InventoryData', 'WRITE', returnObject.inventory.site)")
public InventoryQualityStatus createFast(InventoryQualityStatus source) {
InventoryQualityStatus statusForSave = new InventoryQualityStatus();
statusForSave.apply(source);
return repository.save(statusForSave);
}
@Override
@Transactional
@PreAuthorize("hasAuthority('GROUP_ADMINS') or @ggceSec.actionAllowed('InventoryData', 'WRITE', #target.inventory.site)")
public InventoryQualityStatus update(InventoryQualityStatus updated, InventoryQualityStatus target) {
return _lazyLoad(updateFast(updated, target));
}
@Override
@Transactional
@PreAuthorize("hasAuthority('GROUP_ADMINS') or @ggceSec.actionAllowed('InventoryData', 'WRITE', #target.inventory.site)")
public InventoryQualityStatus updateFast(InventoryQualityStatus updated, InventoryQualityStatus target) {
target.apply(updated);
return repository.save(target);
}
@Component
protected static class AttachmentSupport extends BaseAttachmentSupport<InventoryQualityStatus, InventoryQualityStatusAttach, InventoryQualityStatusAttachmentService.InventoryQualityStatusAttachmentRequest>
implements InventoryQualityStatusAttachmentService {
public AttachmentSupport() {
super(QInventoryQualityStatusAttach.inventoryQualityStatusAttach.inventoryQualityStatus().id, QInventoryQualityStatusAttach.inventoryQualityStatusAttach.id);
}
@Override
@PreAuthorize("hasAuthority('GROUP_ADMINS') or @ggceSec.actionAllowed('InventoryData', 'WRITE', #entity.inventory.site)")
@Transactional
public InventoryQualityStatusAttach uploadFile(InventoryQualityStatus entity, MultipartFile file, InventoryQualityStatusAttachmentService.InventoryQualityStatusAttachmentRequest metadata) throws IOException, InvalidRepositoryPathException, InvalidRepositoryFileDataException {
return super.uploadFile(entity, file, metadata);
}
@Override
@PreAuthorize("hasAuthority('GROUP_ADMINS') or @ggceSec.actionAllowed('InventoryData', 'WRITE', #entity.inventory.site)")
@Transactional
public InventoryQualityStatusAttach removeFile(InventoryQualityStatus entity, Long attachmentId) {
return super.removeFile(entity, attachmentId);
}
@Override
public Path createRepositoryPath(InventoryQualityStatus qualityStatus) {
qualityStatus = owningEntityRepository.getReferenceById(qualityStatus.getId());
return Paths.get("/quality-status/" + qualityStatus.getId());
}
@Override
@PreAuthorize("hasAuthority('GROUP_ADMINS') or @ggceSec.actionAllowed('InventoryData', 'WRITE', #entity.inventory.site)")
protected InventoryQualityStatusAttach createAttach(InventoryQualityStatus entity, InventoryQualityStatusAttach source) {
InventoryQualityStatusAttach attach = new InventoryQualityStatusAttach();
attach.apply(source);
attach.setInventoryQualityStatus(entity);
return attach;
}
@Override
public InventoryQualityStatusAttach create(InventoryQualityStatusAttach source) {
throw new UnsupportedOperationException("Create attachments by uploading a file.");
}
@Override
@PreAuthorize("hasAuthority('GROUP_ADMINS') or @ggceSec.actionAllowed('InventoryData', 'WRITE', #entity.inventory.site)")
@Transactional
public InventoryQualityStatusAttach update(InventoryQualityStatusAttach updated, InventoryQualityStatusAttach target) {
target.apply(updated);
return _lazyLoad(repository.save(target));
}
@Override
@PreAuthorize("hasAuthority('GROUP_ADMINS') or @ggceSec.actionAllowed('InventoryData', 'WRITE', #entity.inventory.site)")
@Transactional
public InventoryQualityStatusAttach remove(InventoryQualityStatusAttach entity) {
return super.remove(entity);
}
}
}
@Transactional
@PreAuthorize("hasAuthority('GROUP_ADMINS')")
@Override
public int ensureSystemInventories() {
var accessionPath = QAccession.accession;
var subQuery = jpaQueryFactory.selectDistinct(accessionPath.id).from(accessionPath)
.where(accessionPath.inventories.any().formTypeCode.eq(Inventory.SYSTEM_INVENTORY_FTC));
var accessionsWithoutSystemInv = jpaQueryFactory.select(accessionPath.id).from(accessionPath)
.where(accessionPath.id.notIn(subQuery))
.fetch();
accessionRepository.findAllById(accessionsWithoutSystemInv).forEach(this::assureSystemInventory);
return accessionsWithoutSystemInv.size();
}
@Override
@PostAuthorize("@ggceSec.actionAllowed('InventoryData', 'READ', returnObject.site)")
public Inventory get(long id) {
return super.get(id);
}
@Override
@Transactional
@PreAuthorize("@ggceSec.actionAllowed('InventoryData', 'CREATE', #source.site)")
public Inventory createFast(Inventory source) {
return create(source);
}
@Override
@Transactional
@PreAuthorize("@ggceSec.actionAllowed('InventoryData', 'CREATE', #source.site)")
public Inventory create(Inventory source) {
if (Objects.nonNull(source.getDoi()) && !SecurityContextUtil.hasRole("ADMINISTRATOR")) {
throw new AccessDeniedException("Only administrators can change the DOI of an inventory.");
}
Inventory saved = new Inventory();
saved.apply(source);
saved = repository.save(saved);
if (saved.getDoi() != null && saved.getDoi().equals(saved.getAccession().getDoi())) {
throw new InvalidApiUsageException("Inventory DOI must be not the same as accession DOI");
}
if (!saved.isSystemInventory() && StringUtils.isBlank(saved.getBarcode())) {
mintBarcode(saved);
saved = repository.save(saved);
}
if (source.getExtra() != null) {
var extra = source.getExtra();
extra.setInventory(saved);
var createdExtra = inventoryExtraService.create(extra);
saved.setExtra(createdExtra);
}
// Save
return saved;
}
@Override
@Transactional
@PreAuthorize("@ggceSec.actionAllowed('InventoryData', 'WRITE', #target.site)")
public Inventory update(Inventory input, Inventory target) {
if (target.isSystemInventory()) {
throw new InvalidApiUsageException("The system inventory cannot be modified.");
}
if (target.equals(input.getParentInventory())) {
throw new InvalidApiUsageException("Cannot set a parent inventory, a cyclical dependency was detected.");
}
if (!Objects.equals(target.getDoi(), input.getDoi()) && !SecurityContextUtil.hasRole("ADMINISTRATOR")) {
throw new AccessDeniedException("Only administrators can change the DOI of an inventory.");
}
target.apply(input);
if (target.getDoi() != null && target.getDoi().equals(target.getAccession().getDoi())) {
throw new InvalidApiUsageException("Inventory DOI must be not the same as accession DOI");
}
if (input.getExtra() != null) {
if (target.getExtra() != null) {
target.getExtra().getId();
target.setExtra(inventoryExtraService.update(target.getExtra().apply(input.getExtra())));
} else {
var inputExtra = input.getExtra();
inputExtra.setInventory(target);
target.setExtra(inventoryExtraService.create(inputExtra));
};
}
return _lazyLoad(repository.save(target));
}
@Override
@Transactional
@PreAuthorize("@ggceSec.actionAllowed('InventoryData', 'WRITE', #target.site)")
public Inventory updateFast(@NotNull @Valid Inventory updated, Inventory target) {
if (target.isSystemInventory()) {
throw new InvalidApiUsageException("The system inventory cannot be modified.");
}
if (target.equals(updated.getParentInventory())) {
throw new InvalidApiUsageException("Cannot set a parent inventory, a cyclical dependency was detected.");
}
if (!Objects.equals(target.getDoi(), updated.getDoi()) && !SecurityContextUtil.hasRole("ADMINISTRATOR")) {
throw new AccessDeniedException("Only administrators can change the DOI of an inventory.");
}
target.apply(updated);
if (target.getDoi() != null && target.getDoi().equals(target.getAccession().getDoi())) {
throw new InvalidApiUsageException("Inventory DOI must be not the same as accession DOI");
}
if (updated.getExtra() != null) {
if (target.getExtra() != null) {
target.getExtra().getId();
target.setExtra(inventoryExtraService.update(target.getExtra().apply(updated.getExtra())));
} else {
var inputExtra = updated.getExtra();
inputExtra.setInventory(target);
target.setExtra(inventoryExtraService.create(inputExtra));
};
}
return repository.save(target);
}
@Override
@Transactional
@PreAuthorize("@ggceSec.actionAllowed('InventoryData', 'DELETE', #entity.site)")
public Inventory remove(Inventory entity) {
entity = reload(entity);
if (entity == null) {
return null;
}
if (entity.isSystemInventory()) {
throw new InvalidApiUsageException("System inventory cannot be removed");
}
inventoryQualityStatusRepository.deleteAll(entity.getQuality());
inventoryActionRepository.deleteAll(entity.getActions());
accessionInvGroupMapRepository.deleteAll(accessionInvGroupMapRepository.findAll(QAccessionInvGroupMap.accessionInvGroupMap.inventory().eq(entity)));
accessionInvNameRepository.deleteAll(accessionInvNameRepository.findAll(QAccessionInvName.accessionInvName.inventory().eq(entity)));
accessionInvAnnotationRepository.deleteAll(accessionInvAnnotationRepository.findAll(QAccessionInvAnnotation.accessionInvAnnotation.inventory().eq(entity)));
accessionInvAttachRepository.deleteAll(entity.getAttachments());
cropTraitObservationDataRepository.deleteAll(cropTraitObservationDataRepository.findAll(QCropTraitObservationData.cropTraitObservationData.inventory().eq(entity)));
var observations = Lists.newArrayList(cropTraitObservationRepository.findAll(QCropTraitObservation.cropTraitObservation.inventory().eq(entity)));
if (!observations.isEmpty()) {
cropTraitObservationDataRepository.deleteAll(cropTraitObservationDataRepository.findAll(QCropTraitObservationData.cropTraitObservationData.cropTraitObservation().in(observations)));
cropTraitObservationRepository.deleteAll(observations);
}
var viabilityList = Lists.newArrayList(inventoryViabilityRepository.findAll(QInventoryViability.inventoryViability.inventory().eq(entity)));
if (!viabilityList.isEmpty()) {
inventoryViabilityActionRepository.deleteAll(inventoryViabilityActionRepository.findAll(QInventoryViabilityAction.inventoryViabilityAction.inventoryViability().in(viabilityList)));
var viabilityDatas = Lists.newArrayList(inventoryViabilityDataRepository.findAll(QInventoryViabilityData.inventoryViabilityData.inventoryViability().in(viabilityList)));
if (!viabilityDatas.isEmpty()) {
viabilityDataEnvMapRepository.deleteAll(
viabilityDataEnvMapRepository.findAll(QInventoryViabilityDataEnvironmentMap.inventoryViabilityDataEnvironmentMap.inventoryViabilityData().in(viabilityDatas))
);
inventoryViabilityDataRepository.deleteAll(viabilityDatas);
}
inventoryViabilityAttachRepository.deleteAll(
inventoryViabilityAttachRepository.findAll(QInventoryViabilityAttach.inventoryViabilityAttach.inventoryViability().in(viabilityList))
);
inventoryViabilityRepository.deleteAll(viabilityList);
}
var orderRequestItems = Lists.newArrayList(orderRequestItemRepository.findAll(QOrderRequestItem.orderRequestItem.inventory().eq(entity)));
if (!orderRequestItems.isEmpty()) {
throw new InvalidApiUsageException("Inventory " + entity.getId() + " is used in an order request item and cannot be deleted.");
// orderRequestItemActionRepository.deleteAll(orderRequestItemActionRepository.findAll(QOrderRequestItemAction.orderRequestItemAction.orderRequestItem().in(orderRequestItems)));
// orderRequestItemRepository.deleteAll(orderRequestItems);
}
var itemsWithdrawnInv = orderRequestItemRepository.findAll(QOrderRequestItem.orderRequestItem.withdrawnInventory().eq(entity));
itemsWithdrawnInv.forEach(orderItem -> orderItem.setWithdrawnInventory(null));
orderRequestItemRepository.saveAllAndFlush(itemsWithdrawnInv);
entity = repository.getReferenceById(entity.getId());
repository.delete(entity);
return entity;
}
@Override
@Transactional
@PostAuthorize("@ggceSec.actionAllowed('InventoryData', 'WRITE', returnObject.site)")
public Inventory setInventoryQuantity(InventoryQuantityRequest inventoryQuantity) {
Inventory inventory = get(inventoryQuantity.id);
if (inventory.isSystemInventory()) {
throw new InvalidApiUsageException("Cannot set quantity for a SYSTEM inventory");
}
// Only update 100 seed weight if not null
if (inventoryQuantity.hundredSeedWeight != null) {
if (inventoryQuantity.hundredSeedWeight == 0 && inventory.getHundredSeedWeight() != null) {
// If 0, clear it
completeInventoryAction(inventory, CommunityCodeValues.INVENTORY_ACTION_100SEEDWEIGHT, "Cleared. Was " + inventory.getHundredSeedWeight());
inventory.setHundredSeedWeight(null);
} else if (inventoryQuantity.hundredSeedWeight != 0 && (inventory.getHundredSeedWeight() == null || BigDecimal.valueOf(inventory.getHundredSeedWeight()).compareTo(BigDecimal.valueOf(inventoryQuantity.hundredSeedWeight)) != 0)) {
// Otherwise update
completeInventoryAction(inventory, CommunityCodeValues.INVENTORY_ACTION_100SEEDWEIGHT, "100 seed weight set to " + inventoryQuantity.hundredSeedWeight + ". Was " + inventory.getHundredSeedWeight());
inventory.setHundredSeedWeight(inventoryQuantity.hundredSeedWeight);
}
}
inventory.setQuantityOnHand(inventoryQuantity.quantityOnHand);
inventory.setQuantityOnHandUnitCode(inventoryQuantity.quantityOnHandUnitCode);
inventory = repository.save(inventory);
completeInventoryAction(inventory, CommunityCodeValues.INVENTORY_ACTION_QUANTITYSET, inventoryQuantity.note);
return _lazyLoad(inventory);
}
@Override
@Transactional
@PreAuthorize("@ggceSec.actionAllowed('InventoryData', 'WRITE', #inventory.site)")
public String assignBarcode(Inventory inventory) {
inventory = get(inventory);
if (StringUtils.isNotBlank(inventory.getBarcode()))
return inventory.getBarcode(); // return existing barcode
mintBarcode(inventory);
repository.save(inventory);
return inventory.getBarcode();
}
/**
* Utility to mint barcode without loading or saving the inventory
*/
private void mintBarcode(Inventory inventory) {
if (inventory.isSystemInventory()) {
throw new InvalidApiUsageException("System inventories do not have barcodes");
}
AppSetting setting = appSettingsService.getSetting(BARCODE_INVENTORY.categoryTag, BARCODE_INVENTORY.name);
inventory.setBarcode(StringUtils.stripToNull(templatingService.fillTemplate(setting.getValue(), Map.of("inventory", inventory, "randomUUID", UUID.randomUUID()))));
}
@Override
@Transactional
public void discardMaterial(Map<Long, Integer> discardQuantities) {
for (var entry : discardQuantities.entrySet()) {
Inventory inventory = get(entry.getKey());
if (inventory.isSystemInventory())
throw new InvalidApiUsageException("Cannot update quantity for a SYSTEM inventory");
int discardQuantity = entry.getValue();
if (inventory.getQuantityOnHand() < discardQuantity)
throw new InvalidApiUsageException("Refusing to discard " + discardQuantity + " items, the current quantity on hand is " + inventory.getQuantityOnHand());
inventory.setQuantityOnHand(inventory.getQuantityOnHand() - discardQuantity);
var savedInventory = repository.save(inventory);
addCompletedInventoryAction(savedInventory, CommunityCodeValues.INVENTORY_ACTION_DISCARD, (action) -> {
action.setQuantity((double) discardQuantity);
action.setQuantityUnitCode(savedInventory.getQuantityOnHandUnitCode());
action.setFormCode(savedInventory.getFormTypeCode());
action.setNote("Discarded " + discardQuantity + " " + inventory.getQuantityOnHandUnitCode());
});
}
}
@Override
public Page<InventoryRepositoryCustom.AggregatedInventoryQuantity> aggregateQuantity(InventoryFilter filter, Pageable page) {
if (filter.isFulltextQuery())
throw new InvalidApiUsageException("Elasticsearch filters not supported.");
var resultPage = repository.aggregateQuantity(adjustFilter(filter).buildPredicate(), page);
resultPage.stream().forEach(e -> e.accession = accessionRepository.findById(e.accession.getId()).orElseThrow(() -> {
// should not happen
throw new NotFoundElement("No such accession");
}));
return resultPage;
}
@Override
@Transactional
public List<Inventory> splitInventory(SplitInventoryRequest splitInventoryRequest) {
var splitSource = splitInventoryRequest.sourceSplitInventory;
Inventory source = get(splitSource.id, splitSource.modifiedDate);
if (!ggceSec.actionAllowed(SecurityAction.InventoryData.name(), "WRITE", source.getSite())) {
throw new AccessDeniedException("Don't have permission to split inventory on this site");
}
if (source.isSystemInventory()) {
throw new InvalidApiUsageException("Cannot split a system inventory");
}
if (splitSource.containerTypeCode != null) {
source.setContainerTypeCode(splitSource.containerTypeCode);
source = update(source); // updated source inventory
}
if (splitSource.quantityOnHand != null) {
var quantityUpdate = new InventoryQuantityRequest();
quantityUpdate.id = source.getId();
quantityUpdate.quantityOnHand = splitSource.quantityOnHand.doubleValue();
quantityUpdate.quantityOnHandUnitCode = splitSource.quantityOnHandUnitCode;
quantityUpdate.note = splitSource.note;
source = setInventoryQuantity(quantityUpdate);
}
// New inventories
var parentInventory = source;
var parentExtra = parentInventory.getExtra();
List<Inventory> inventoriesForSave = new ArrayList<>();
splitInventoryRequest.splits.forEach(splitRequest -> {
var splitInventory = new Inventory();
splitInventory.apply(parentInventory); // Copy from source
splitInventory.setBarcode(null);
splitInventory.setStorageLocationPart1(null);
splitInventory.setStorageLocationPart2(null);
splitInventory.setStorageLocationPart3(null);
splitInventory.setStorageLocationPart4(null);
splitInventory.setBackupInventory(null);
if (splitRequest.inventoryMaintenancePolicy != null) {
if (splitRequest.inventoryMaintenancePolicy.id == null) {
throw new InvalidApiUsageException("InventoryMaintenancePolicy.id not specified");
}
splitInventory.applyInventoryMaintenancePolicy(inventoryMaintPolicyRepo.getReferenceById(splitRequest.inventoryMaintenancePolicy.id));
} else {
splitInventory.applyInventoryMaintenancePolicy(parentInventory.getInventoryMaintenancePolicy());
}
splitInventory.setParentInventory(parentInventory); // Set parent
splitInventory.setQuantityOnHand(splitRequest.quantityOnHand); // Optional quantity
splitInventory.setQuantityOnHandUnitCode(StringUtils.defaultIfBlank(splitRequest.quantityOnHandUnitCode, parentInventory.getQuantityOnHandUnitCode())); // Set unit code
splitInventory.setContainerTypeCode(splitRequest.containerTypeCode);
splitInventory.setInventoryNumberPart1(splitRequest.inventoryNumberPart1);
splitInventory.setInventoryNumberPart2(splitRequest.inventoryNumberPart2);
splitInventory.setInventoryNumberPart3(splitRequest.inventoryNumberPart3);
splitInventory.setNote(splitRequest.note);
splitInventory.setAvailabilityStatusCode(CommunityCodeValues.INVENTORY_AVAILABILITY_NOTSET.value);
var savedSplit = repository.save(splitInventory);
if (parentExtra != null) {
var splitInventoryExtra = parentExtra.copy();
if (splitInventoryExtra != null) { // We have a copy
splitInventoryExtra.setInventory(splitInventory);
inventoryExtraService.create(splitInventoryExtra);
}
}
inventoriesForSave.add(savedSplit);
});
// Assign barcodes to new inventories
inventoriesForSave.forEach(this::mintBarcode);
// Record the SPLIT action on source inventory
completeInventoryAction(parentInventory, CommunityCodeValues.INVENTORY_ACTION_SPLIT, splitSource.note);
return repository.saveAll(inventoriesForSave); // Do a final save (for barcodes)
}
@Override
@Transactional
public List<Inventory> assignLocation(AssignLocationRequest assignLocationRequest) {
var location = assignLocationRequest.location;
if (location == null || location.siteId == null) {
throw new InvalidApiUsageException("Location must be provided");
}
if (CollectionUtils.isEmpty(assignLocationRequest.inventories)) {
throw new InvalidApiUsageException("Inventories must be provided");
}
var site = siteRepository.findById(location.siteId).orElseThrow(() -> new NotFoundElement("No site with id=" + location.siteId));
if (!ggceSec.actionAllowed(SecurityAction.InventoryData.name(), "WRITE", site)) {
throw new AccessDeniedException("Don't have permission to assign inventories to this site");
}
var inventoriesForAssign = findByIdAndVersion(assignLocationRequest.inventories);
if (inventoriesForAssign.size() < assignLocationRequest.inventories.size()) {
throw new NotFoundElement("Some inventories not found");
}
var hasUnavailableSite = getHasUnavailableSite(inventoriesForAssign, SecurityAction.InventoryData, "WRITE");
if (hasUnavailableSite) {
throw new AccessDeniedException("Don't have permission to change location of the selected inventories");
}
for (Inventory inventory : inventoriesForAssign) {
inventory.setSite(site);
inventory.setStorageLocationPart1(location.storageLocationPart1);
inventory.setStorageLocationPart2(location.storageLocationPart2);
inventory.setStorageLocationPart3(location.storageLocationPart3);
inventory.setStorageLocationPart4(location.storageLocationPart4);
}
return repository.saveAll(inventoriesForAssign);
}
@Override
@Transactional
public List<Inventory> assignLocations(List<AssignLocationRequest> assignLocationRequests) {
return assignLocationRequests.stream()
.map(this::assignLocation)
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
@Override
@PostAuthorize("@ggceSec.actionAllowed('InventoryData', 'READ', returnObject.site)")
public Inventory getByBarcode(String barcode) {
return repository.findOne(QInventory.inventory.barcode.eq(barcode)).orElseThrow(() -> new NotFoundElement("Inventory not found by barcode: " + barcode));
}
private Collection<Inventory> findByIdAndVersion(Set<EntityIdAndModifiedDate> inventories) {
assert (CollectionUtils.isNotEmpty(inventories));
BooleanBuilder builder = new BooleanBuilder();
for (var i : inventories) {
builder.orAllOf(QInventory.inventory.id.eq(i.id), QInventory.inventory.modifiedDate.eq(i.modifiedDate));
}
return (Collection<Inventory>) repository.findAll(builder);
}
private void completeInventoryAction(Inventory inventory, CodeValueDef actionCodeValue, String note) {
addCompletedInventoryAction(inventory, actionCodeValue, (action) -> {
action.setNote(note);
// Update action with inventory data
action.setFormCode(inventory.getFormTypeCode());
action.setQuantity(inventory.getQuantityOnHand());
action.setQuantityUnitCode(inventory.getQuantityOnHandUnitCode());
});
}
private void addCompletedInventoryAction(Inventory inventory, CodeValueDef actionCodeValue, Consumer<InventoryAction> customizer) {
var nowDate = Instant.now();
BooleanExpression expression = QInventoryAction.inventoryAction.inventory().id.in(inventory.getId())
.and(QInventoryAction.inventoryAction.completedDate.isNull())
.and(QInventoryAction.inventoryAction.notBeforeDate.isNull().or(QInventoryAction.inventoryAction.notBeforeDate.loe(nowDate)))
.and(QInventoryAction.inventoryAction.actionNameCode.eq(actionCodeValue.value));
var pendingActions = StreamSupport.stream(actionFinder.findAll(expression).spliterator(), false)
.peek(action -> {
action.setCompletedDate(nowDate);
action.setCompletedDateCode(CommunityCodeValues.DATE_FORMAT_DATETIME.value);
if (customizer != null) {
customizer.accept(action);
}
}).collect(Collectors.toList());
var result = pendingActions.stream().map(inventoryActionService::update).collect(Collectors.toList());
if (result.size() > 0) {
return;
}
InventoryAction quantityAction = new InventoryAction();
quantityAction.setActionNameCode(actionCodeValue.value);
quantityAction.setInventory(inventory);
quantityAction.setCompletedDate(nowDate);
quantityAction.setCompletedDateCode(CommunityCodeValues.DATE_FORMAT_DATETIME.value);
quantityAction.setStartedDate(quantityAction.getCompletedDate());
quantityAction.setStartedDateCode(quantityAction.getCompletedDateCode());
if (customizer != null) {
customizer.accept(quantityAction);
}
inventoryActionRepository.save(quantityAction);
}
//
// @Override
// public Page<InventoryAction> listInventoriesWithAction(InventoryActionFilter actionFilter, InventoryFilter inventoryFilter, Pageable page) {
//
// var qInvA = QInventoryAction.inventoryAction;
// QInventory qInv = new QInventory("inventory");
// BooleanBuilder predicate = new BooleanBuilder();
//
// if (actionFilter != null) {
// predicate.and(ExpressionUtils.allOf(actionFilter.collectPredicates(qInvA)));
// }
// if (inventoryFilter != null) {
// predicate.and(ExpressionUtils.allOf(inventoryFilter.collectPredicates(qInv)));
// }
//
// var query = jpaQueryFactory.selectFrom(qInvA)
// // join
// .innerJoin(qInvA.inventory, qInv).fetchJoin();
//
// query = joinDetails(query, qInv)
// // where
// .where(predicate);
//
// query.select(qInvA);
//
// // get total elements
// var totalElements = query.fetchCount();
//
// // apply pagination
// Querydsl querydsl = new Querydsl(entityManager, new PathBuilder<>(qInvA.getType(), qInvA.getMetadata()));
// querydsl.applyPagination(page, query);
//
// var content = query.fetch();
//
// return new PageImpl<>(content, page, totalElements);
// }
@Override
protected InventoryFilter adjustFilter(InventoryFilter filter) {
var siteIds = ggceSec.getSiteIds(SecurityAction.InventoryData.name(), "READ");
if (siteIds != null) {
log.debug("Forcing Site IDs: {}", siteIds);
var siteFilter = new InventoryFilter().includeSystem(null);
siteFilter.site(new SiteFilter());
siteFilter.site.id(siteIds);
siteFilter.AND = filter;
if (filter != null) {
siteFilter._text(filter._text()); // A workaround for triggering ES querying
}
return siteFilter;
} else {
// No change
return filter;
}
}
@Override
public Page<Inventory> list(InventoryFilter filter, Pageable page) throws SearchException {
return list(Inventory.class, adjustFilter(filter), page, BOOST_FIELDS); // This is just to include the boosted fields
}
@Override
protected Page<Inventory> list(Class<Inventory> clazz, InventoryFilter filter, Pageable page, String... boostFields) throws SearchException {
page = Pagination.addSortByParams(page, idSortParams);
// if (ftf.isFulltextQuery() && elasticsearchService != null) { // This could be quietly ignoring full-test search
if (filter != null && filter.isFulltextQuery()) {
return elasticsearchService.findAll(clazz, filter, null, page, (entityIds) -> list(entityIds), boostFields);
}
BooleanBuilder predicate = new BooleanBuilder();
if (filter != null) {
predicate.and(filter.buildPredicate());
}
if (entityListQuery() != null) {
JPAQuery<Inventory> query = entityListQuery().where(predicate);
var inventoryExtraProp = QInventory.inventory.extra().getMetadata().getName().concat(".");
var hasExtraSorts = page.getSort().stream().anyMatch(sort -> sort.getProperty().startsWith(inventoryExtraProp));
if (hasExtraSorts) {
var queryDsl = new Querydsl(entityManager, new PathBuilder<Inventory>(QInventory.inventory.getType(), QInventory.inventory.getMetadata()));
query.offset(page.getOffset());
query.limit(page.getPageSize());
applyOrderWithExtra(query, page, inventoryExtraProp, queryDsl);
Long total = query.fetchCount();
return PageableExecutionUtils.getPage(query.fetch(), page, total::longValue);
}
return repository.findAll(query, page);
} else {
// default implementation without custom loading
return repository.findAll(predicate, page);
}
}
private void applyOrderWithExtra(JPQLQuery<Inventory> query, Pageable page, String inventoryExtraProp, Querydsl queryDsl) {
for (Sort.Order order : page.getSort()) {
if (order.getProperty().startsWith(inventoryExtraProp)) {
var extraOrderPath = Expressions.path(Comparable.class, QInventory.inventory, order.getProperty());
query.orderBy(new OrderSpecifier<Comparable>(
order.isAscending() ? com.querydsl.core.types.Order.ASC : com.querydsl.core.types.Order.DESC, extraOrderPath)
);
} else {
queryDsl.applySorting(Sort.by(order), query);
}
}
}
@Override
protected JPAQuery<Inventory> entityListQuery() {
return joinDetails(jpaQueryFactory.selectFrom(QInventory.inventory), QInventory.inventory);
}
private <T> JPAQuery<T> joinDetails(JPAQuery<T> query, QInventory inventory) {
QAccession aliasAccession = new QAccession("a");
return query
// site
.join(inventory.site()).fetchJoin()
// acce
.join(inventory.accession(), aliasAccession).fetchJoin() // Needs alias because of fetchJoin
// maint pol
.join(inventory.inventoryMaintenancePolicy()).fetchJoin()
// species
.join(aliasAccession.taxonomySpecies()).fetchJoin()
// extra
.leftJoin(inventory.extra()).fetchJoin()
// production location geography
.leftJoin(inventory.productionLocationGeography()).fetchJoin()
;
}
@Override
protected NumberPath<Long> entityIdPredicate() {
return QInventory.inventory.id;
}
@Override
public Inventory getSystemInventory(String instituteCode, String accessionNumber) {
return repository.getSystemInventory(instituteCode, accessionNumber);
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
@PreAuthorize("@ggceSec.actionAllowed('PassportData', 'CREATE', #accession.site)")
public Inventory assureSystemInventory(Accession accession) {
log.debug("Assuring a SYSTEM inventory for new accession {}", accession.getId());
final Inventory inventory = new Inventory();
inventory.setAccession(accession);
inventory.setInventoryNumberPart1(accession.getAccessionNumberPart1());
inventory.setInventoryNumberPart2(accession.getAccessionNumberPart2());
inventory.setInventoryNumberPart3(accession.getAccessionNumberPart3());
inventory.setFormTypeCode(Inventory.SYSTEM_INVENTORY_FTC);
inventory.setSite(accession.getSite());
final InventoryMaintenancePolicy inventoryMaintenancePolicy = inventoryMaintPolicyRepo.getSystemMaintenancePolicy();
inventory.setInventoryMaintenancePolicy(inventoryMaintenancePolicy);
inventory.setIsDistributable("N");
inventory.setIsAvailable("N");
inventory.setAvailabilityStatusCode(CommunityCodeValues.INVENTORY_AVAILABILITY_NOTSET.value);
inventory.setIsAutoDeducted("N");
inventory.setNote("Default Association Record for Accession -> Inventory");
final var now = Instant.now();
inventory.setOwnedBy(accession.getOwnedBy());
inventory.setOwnedDate(now);
inventory.setSite(accession.getSite());
// inventory.setCreatedBy(accession.getCreatedBy());
// inventory.setCreatedDate(now);
return repository.save(inventory);
}
@Override
@PostAuthorize("@ggceSec.actionAllowed('InventoryData', 'READ', returnObject.inventory.site)")
public InventoryDetails getInventoryDetails(Inventory inventory) {
if (inventory == null) {
throw new NotFoundElement();
}
inventory = this.reload(inventory);
inventory.lazyLoad();
// initialize lazy data
Hibernate.initialize(inventory.getActions());
Hibernate.initialize(inventory.getViability());
Hibernate.initialize(inventory.getQuality());
Hibernate.initialize(inventory.getInventoryMaintenancePolicy());
Hibernate.initialize(inventory.getParentInventory());
Hibernate.initialize(inventory.getProductionLocationGeography());
InventoryDetails inventoryDetails = new InventoryDetails();
inventoryDetails.inventory = inventory;
inventoryDetails.actions = inventory.getActions();
inventoryDetails.viability = inventory.getViability();
if (inventoryDetails.viability != null) {
inventoryDetails.viability.forEach((viab) -> {
if (viab.getInventoryViabilityRule() != null)
viab.getInventoryViabilityRule().getId();
if (viab.getMedium() != null) {
viab.getMedium().getId();
}
});
}
inventoryDetails.qualityStatus = inventory.getQuality();
if (inventoryDetails.qualityStatus != null) {
inventoryDetails.qualityStatus.forEach(LazyLoading::lazyLoad);
}
inventoryDetails.names = accessionInvNameRepository.findInventoryNames(inventory);
inventoryDetails.attachments = attachRepository.findInventoryAttachments(inventory);
inventoryDetails.attachments.forEach(accessionInvAttach -> {
if (accessionInvAttach.getAttachCooperator() != null) {
accessionInvAttach.getAttachCooperator().getId();
}
});
inventoryDetails.groups = accessionInvGroupService.listInventoryGroups(inventory);
return inventoryDetails;
}
@Override
public Map<Object, Number> inventoryOverview(String groupBy, InventoryFilter filter) {
return overviewHelper.getOverview(Inventory.class, QInventory.inventory, QInventory.inventory.id.countDistinct(), groupBy, adjustFilter(filter));
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR')")
public void recalculateAllInventoryNumbers() {
boolean lastPage = false;
int pageNumber = 0;
do {
Page<Inventory> page = repository.findAll(PageRequest.of(pageNumber, 1000));
lastPage = page.isLast();
pageNumber += 1;
log.warn("Updating inventoryNumber for {} records, page {}", page.getNumberOfElements(), pageNumber);
Lists.partition(page.getContent(), 100).forEach(batch -> transactionHelper.executeInTransaction(false, () -> {
batch.forEach(i -> repository.setInventoryNumber(i.getId(), GGCE.inventoryNumber(i)));
return true;
}));
// Clear anything cached in the entity manager
em.clear();
} while (!lastPage);
log.info("Done.");
}
@Override
@PreAuthorize("hasRole('ADMINISTRATOR')")
public void assignMissingInventoryNumbers() {
boolean lastPage = false;
int pageNumber = 0;
do {
Page<Inventory> page = repository.findAll(QInventory.inventory.inventoryNumber.isNull(), PageRequest.of(0, 1000));
lastPage = page.isLast();
pageNumber++;
log.warn("Assigning inventoryNumber for {} records, page {}", page.getNumberOfElements(), pageNumber);
Lists.partition(page.getContent(), 100).forEach(batch -> transactionHelper.executeInTransaction(false, () -> {
batch.forEach(a -> repository.setInventoryNumber(a.getId(), GGCE.inventoryNumber(a)));
return true;
}));
// Clear anything cached in the entity manager
em.clear();
} while (!lastPage && pageNumber < 5000); // at most 5000 loops
log.info("Done.");
}
@Service
@Transactional(readOnly = true)
@Validated
protected static class InventoryExtraServiceImpl extends CRUDServiceImpl<InventoryExtra, InventoryExtraRepository>
implements InventoryExtraService {
@Override
public InventoryExtra create(InventoryExtra source) {
return make(source);
}
@Override
public <T extends InventoryExtra> T make(T extra) {
var inventory = extra.getInventory();
if (inventory == null || inventory.getId() == null) {
throw new InvalidApiUsageException("Inventory must be provided");
}
return repository.save(extra);
}
@Override
public InventoryExtra update(InventoryExtra updated, InventoryExtra target) {
return repository.save(target.apply(updated));
}
}
@Override
@Transactional
@PreAuthorize("@ggceSec.actionAllowed('Invitro', 'CREATE', #request.sourceInventory.site)")
public List<Inventory> invitroStart(IVMultiplicationRequest request) {
assert (request.multiplicationItems != null && !request.multiplicationItems.isEmpty());
request.sourceInventory = get(request.sourceInventory.getId());
if (!Objects.equals("Y", request.sourceInventory.getIsAutoDeducted())) {
// Do not deduct from source (grin-global/grin-global-server#488)
} else if (request.sourceInventory.getQuantityOnHand() == null || request.sourceInventory.getQuantityOnHand() == 0 || request.sourceInventory.getQuantityOnHand() < request.quantityToDeduct) {
throw new InvalidApiUsageException("Source inventory does not have sufficient quantity"); // Require quantity
}
var createdList = new ArrayList<Inventory>(request.multiplicationItems.size());
for (IVMultiplicationItem item : request.multiplicationItems) {
if (item.inventoryMaintenancePolicy == null || item.inventoryMaintenancePolicy.getId() == null) {
throw new InvalidApiUsageException("Inventory maintenance policy id is empty");
}
item.inventoryMaintenancePolicy = inventoryMaintPolicyRepo.getReferenceById(item.inventoryMaintenancePolicy.getId());
var inv = new Inventory();
inv.setParentInventory(request.sourceInventory);
inv.setAccession(request.sourceInventory.getAccession());
inv.setFormTypeCode(StringUtils.defaultIfBlank(item.formTypeCode, CommunityCodeValues.GERMPLASM_FORM_INVITRO.value));
inv.setInventoryMaintenancePolicy(item.inventoryMaintenancePolicy);
inv.setInventoryNumberPart1(StringUtils.isBlank(item.inventoryNumberPart1) ? inv.getAccession().getAccessionNumber() : item.inventoryNumberPart1);
inv.setInventoryNumberPart2(-1L); // Auto-increment
inv.setInventoryNumberPart3(item.inventoryNumberPart3); // Not sure about this one...
inv.setQuantityOnHand(item.quantityOnHand == null ? 1.0d : item.quantityOnHand);
inv.setQuantityOnHandUnitCode(item.quantityOnHandUnitCode);
inv.setIsAutoDeducted(StringUtils.defaultIfBlank(item.isAutoDeducted, "Y")); // Make IV inventory auto-deducted when doing first introduction
inv.setContainerTypeCode(item.containerTypeCode);
inv.setAvailabilityStatusCode(CommunityCodeValues.INVENTORY_AVAILABILITY_NOTSET.value);
inv.setSite(request.sourceInventory.getSite());
inv.setPropagationDate(new Date());
inv.setPropagationDateCode(CommunityCodeValues.DATE_FORMAT_DATE.value);
inv.setAvailabilityReasonCode(item.availabilityReasonCode);
inv.setNote(item.note);
inv.setPathogenStatusCode(item.pathogenStatusCode != null ? item.pathogenStatusCode : request.sourceInventory.getPathogenStatusCode()); // Inherit from the source if not present
inv.setGeneration(request.sourceInventory.nextGeneration()); // Increment generation
if (inv.getGeneration() == null) inv.setGeneration(1L); // Set 1st generation
var created = create(inv);
applyTCExtraFromRequest(created, item, null /* Do not copy from source */);
createdList.add(created);
}
if (Objects.equals("Y", request.sourceInventory.getIsAutoDeducted())) {
// Consume source inventory
request.sourceInventory.setQuantityOnHand(request.sourceInventory.getQuantityOnHand() - request.quantityToDeduct);
request.sourceInventory = update(request.sourceInventory);
}
return createdList;
}
@Override
@Transactional
@PreAuthorize("@ggceSec.actionAllowed('Invitro', 'CREATE', #request.sourceInventory.site)")
public List<Inventory> invitroMultiply(IVMultiplicationRequest request) {
assert (request.multiplicationItems != null && !request.multiplicationItems.isEmpty());
request.sourceInventory = get(request.sourceInventory.getId());
if (!Objects.equals("Y", request.sourceInventory.getIsAutoDeducted())) {
// Do not deduct from source (grin-global/grin-global-server#488)
} else if (request.sourceInventory.getQuantityOnHand() == null || request.sourceInventory.getQuantityOnHand() == 0 || request.sourceInventory.getQuantityOnHand() < request.quantityToDeduct) {
throw new InvalidApiUsageException("Source inventory does not have sufficient quantity"); // Require quantity
}
var createdList = new ArrayList<Inventory>();
for (IVMultiplicationItem item : request.multiplicationItems) {
item.formTypeCode = item.formTypeCode != null ? item.formTypeCode : request.sourceInventory.getFormTypeCode();
item.inventoryMaintenancePolicy = item.inventoryMaintenancePolicy != null && item.inventoryMaintenancePolicy.getId() != null ?
inventoryMaintPolicyRepo.getReferenceById(item.inventoryMaintenancePolicy.getId()) : request.sourceInventory.getInventoryMaintenancePolicy();
item.inventoryNumberPart3 = item.inventoryNumberPart3 != null ? item.inventoryNumberPart3 : request.sourceInventory.getInventoryNumberPart3();
item.quantityOnHandUnitCode = item.quantityOnHandUnitCode != null ? item.quantityOnHandUnitCode : request.sourceInventory.getQuantityOnHandUnitCode();
item.containerTypeCode = item.containerTypeCode != null ? item.containerTypeCode : request.sourceInventory.getContainerTypeCode();
var inv = new Inventory();
inv.setParentInventory(request.sourceInventory);
inv.setAccession(request.sourceInventory.getAccession());
inv.setFormTypeCode(StringUtils.defaultIfBlank(item.formTypeCode, CommunityCodeValues.GERMPLASM_FORM_INVITRO.value));
inv.setInventoryMaintenancePolicy(item.inventoryMaintenancePolicy);
inv.setInventoryNumberPart1(StringUtils.isBlank(item.inventoryNumberPart1) ?
StringUtils.joinWith(".", request.sourceInventory.getInventoryNumberPart1(), request.sourceInventory.getInventoryNumberPart2())
: item.inventoryNumberPart1);
inv.setInventoryNumberPart2(-1L); // Auto-increment
inv.setInventoryNumberPart3(item.inventoryNumberPart3); // Not sure about this one...
inv.setQuantityOnHand(item.quantityOnHand == null ? 1.0d : item.quantityOnHand);
inv.setQuantityOnHandUnitCode(item.quantityOnHandUnitCode);
inv.setIsAutoDeducted(StringUtils.defaultIfBlank(item.isAutoDeducted, request.sourceInventory.getIsAutoDeducted())); // Use parent auto-deducted in multiplication
inv.setContainerTypeCode(item.containerTypeCode);
inv.setAvailabilityStatusCode(CommunityCodeValues.INVENTORY_AVAILABILITY_NOTSET.value);
inv.setSite(request.sourceInventory.getSite());
inv.setPropagationDate(new Date());
inv.setPropagationDateCode(CommunityCodeValues.DATE_FORMAT_DATE.value);
inv.setAvailabilityReasonCode(item.availabilityReasonCode);
inv.setNote(item.note);
inv.setPathogenStatusCode(item.pathogenStatusCode != null ? item.pathogenStatusCode : request.sourceInventory.getPathogenStatusCode()); // Inherit from the source if not present
inv.setGeneration(request.sourceInventory.nextGeneration()); // Increment generation
var created = create(inv);
applyTCExtraFromRequest(created, item, request.sourceInventory);
createdList.add(created);
}
if (Objects.equals("Y", request.sourceInventory.getIsAutoDeducted())) {
// Consume source inventory
request.sourceInventory.setQuantityOnHand(request.sourceInventory.getQuantityOnHand() - request.quantityToDeduct);
request.sourceInventory = update(request.sourceInventory);
}
return createdList;
}
/**
* Create and populate {@link TissueCultureExtra}
*
* @param inventory
* @param request
* @param sourceInventory {@code null} for introduction, {@code not-null} for multiplication
*/
private void applyTCExtraFromRequest(Inventory inventory, IVMultiplicationItem request, Inventory sourceInventory) {
TissueCultureExtra extra = new TissueCultureExtra();
// Copy introduction date
if (sourceInventory != null && sourceInventory.getExtra() instanceof TissueCultureExtra) {
TissueCultureExtra sourceExtra = (TissueCultureExtra) sourceInventory.getExtra();
if (sourceExtra.getIntroductionDate() != null) {
extra.setIntroductionDate(sourceExtra.getIntroductionDate());
extra.setIntroductionDateCode(sourceExtra.getIntroductionDateCode());
}
}
// Apply medium
if (request.medium != null) {
extra.setMedium(request.medium);
}
// Apply introduction date
if (request.introductionDate != null) {
extra.setIntroductionDate(request.introductionDate);
extra.setIntroductionDateCode(request.introductionDateCode);
} else if (sourceInventory == null) {
// Set current date as introduction date
extra.setIntroductionDate(new Date());
extra.setIntroductionDateCode(CommunityCodeValues.DATE_FORMAT_DATE.value);
}
extra.setInventory(inventory);
var createdExtra = inventoryExtraService.create(extra);
inventory.setExtra(createdExtra);
}
@Override
@Transactional
@PreAuthorize("@ggceSec.actionAllowed('InventoryData', 'ADMINISTRATION', #site)")
public OrderRequest multiplicationOrder(Site site, Set<Long> inventoryIds, Cooperator multiplicationCooperator, String orderType, String intendedUseCode) {
var inventories = Lists.newArrayList(repository.findAll(QInventory.inventory.id.in(inventoryIds)));
final var orderRequestItems = new ArrayList<OrderRequestItem>(inventories.size());
var sequenceNumber = new AtomicInteger(1);
inventories.forEach(inventory -> {
OrderRequestItem ori = new OrderRequestItem();
ori.setInventory(inventory);
ori.setSequenceNumber(sequenceNumber.getAndIncrement());
ori.setQuantityShippedUnitCode(inventory.getDistributionUnitCode());
ori.setDistributionFormCode(inventory.getDistributionDefaultFormCode());
ori.setQuantityShipped(inventory.getDistributionDefaultQuantity());
orderRequestItems.add(ori);
// Start the CommunityCodeValues.INVENTORY_ACTION_MULTIPLICATION action by setting the start date
InventoryActionService.InventoryActionRequest iar = InventoryActionService.InventoryActionRequest.builder()
.actionNameCode(CommunityCodeValues.INVENTORY_ACTION_MULTIPLICATION.value)
.quantity(null)
.quantityUnitCode(CommunityCodeValues.UNIT_OF_QUANTITY_SEED.value)
.formCode(CommunityCodeValues.GERMPLASM_FORM_SEED.value)
.id(Set.of(inventory.getId()))
.build();
var startedActions = inventoryActionService.startAction(iar);
log.info("Started {} action(s) for {}", startedActions.size(), iar.actionNameCode);
});
var orderRequest = new OrderRequest();
orderRequest.setOrderTypeCode(orderType);
orderRequest.setIntendedUseCode(intendedUseCode);
orderRequest.setRequestorCooperator(multiplicationCooperator);
orderRequest.setShipToCooperator(multiplicationCooperator);
orderRequest.setFinalRecipientCooperator(multiplicationCooperator);
orderRequest = orderRequestService.create(orderRequest, orderRequestItems);
return orderRequest;
}
@Override
@Transactional
public void shareAttachment(Long attachId, List<Long> inventoryIds) {
var inventories = repository.findAll(QInventory.inventory.id.in(inventoryIds), Pageable.unpaged()).getContent();
var hasUnavailableSite = getHasUnavailableSite(inventories, SecurityAction.InventoryAttachment, "CREATE");
if (hasUnavailableSite) {
throw new AccessDeniedException("Don't have permission to create attachments for the selected inventories");
}
var attachment = attachRepository.findById(attachId).orElseThrow(() -> new NotFoundElement("No such attach for provided id"));
attachmentService.shareAttachment(attachment, inventories);
}
/**
* Find sites where permission is not granted.
*/
private boolean getHasUnavailableSite(Collection<Inventory> inventories, SecurityAction action, String permission) {
var hasUnavailableSite = inventories.stream()
.map(Inventory::getSite)
.distinct()
.anyMatch(site -> !ggceSec.actionAllowed(action.name(), permission, site));
return hasUnavailableSite;
}
@Override
@Transactional
@PostAuthorize("@ggceSec.actionAllowed('InventoryData', 'WRITE', returnObject.inventory.site)")
public SeedInventoryExtra updateMoistureContent(SeedInventoryExtra extra, MoistureContentRequest request) {
assert (extra != null && extra.getId() != null);
SeedInventoryExtra target = (SeedInventoryExtra) inventoryExtraService.get(extra.getId());
target.setMoistureContent(request.moistureContent);
target.setMoistureContentDate(request.moistureContentDate);
target.setMoistureContentDateCode(request.moistureContentDateCode);
return (SeedInventoryExtra) inventoryExtraService.update(target);
}
@Override
public Page<ComparedSitesResponse> compareSites(AccessionFilter filter, Map<Long, NumberFilter<Long>> siteItems, Pageable page) {
// (Site, NumberFilter) entries
var siteAndFilters = siteRepository.findAllById(siteItems.keySet()).stream()
.map(site -> Map.entry(site, siteItems.get(site.getId())))
.collect(Collectors.toList());
if (siteAndFilters.size() != siteItems.size()) {
throw new IllegalArgumentException("Some of the provided sites are missing");
}
var accession = QAccession.accession;
List<Expression<?>> selectExpressions = new ArrayList<>();
selectExpressions.add(accession.id);
selectExpressions.add(accession.accessionNumber);
var bigQuery = new BlazeJPAQuery<>(entityManager, criteriaBuilderFactory).from(accession)
.where(filter != null ? filter.buildPredicate() : new AccessionFilter().buildPredicate())
.orderBy(accession.accessionNumberPart1.asc(), accession.accessionNumberPart2.asc(), accession.accessionNumberPart3.asc());
// Filter for accessions on my sites
var readSiteIds = ggceSec.getSiteIds(SecurityAction.PassportData.name(), "READ");
if (readSiteIds != null) {
bigQuery.where(accession.site().id.in(readSiteIds));
}
var fetchCountQuery = new BlazeJPAQuery<>(entityManager, criteriaBuilderFactory)
.from(accession)
.select(accession.id.count())
.where(filter != null ? filter.buildPredicate() : new AccessionFilter().buildPredicate());
for (var site : siteAndFilters) {
if (!ggceSec.actionAllowed(SecurityAction.InventoryData.name(), "READ", site.getKey())) {
throw new AccessDeniedException("Missing read permission for InventoryData on site");
}
QSiteCounter siteCounter = new QSiteCounter("CTEFor" + site.getKey().getId()); // CTE (accessionId, count(inventories))
var siteInv = new QInventory("subQuerySite" + site.getKey().getId());
var siteCountQuery = new BlazeJPAQuery<>(entityManager, criteriaBuilderFactory).from(siteInv).select(siteCounter)
.bind(siteCounter.siteCount, siteInv.id.count())
.bind(siteCounter.accessionId, siteInv.accession().id)
.where(
siteInv.site().eq(site.getKey())
.and(siteInv.accession().eq(accession))
.and(siteInv.formTypeCode.ne(Inventory.SYSTEM_INVENTORY_FTC))
.and(siteInv.quantityOnHand.isNull().or(siteInv.quantityOnHand.gt(0)))
)
.groupBy(siteInv.accession().id);
bigQuery.leftJoin(siteCountQuery, siteCounter).on(siteCounter.accessionId.eq(accession.id));
selectExpressions.add(siteCounter.siteCount.coalesce(0L));
// support filtering by count at this site
if (site.getValue() != null && !site.getValue().isEmpty()) {
var siteCounterPredicate = site.getValue().buildQuery(siteCounter.siteCount.coalesce(0L)).getValue();
bigQuery.where(siteCounterPredicate);
// add join to the fetch count query only if it has filter
fetchCountQuery.leftJoin(siteCountQuery, siteCounter).on(siteCounter.accessionId.eq(accession.id));
fetchCountQuery.where(siteCounterPredicate);
}
}
bigQuery.select(selectExpressions.toArray(new Expression[0]));
var total = fetchCountQuery.fetchOne();
if (total == 0) {
return new PageImpl<>(new ArrayList<>(), page, total);
}
// Apply pagination
bigQuery.offset(page.getOffset());
bigQuery.limit(page.getPageSize());
var responseItems = bigQuery.fetch().stream().map(result -> {
var response = new ComparedSitesResponse();
response.setAccessionNumber(((Tuple) result).get(accession.accessionNumber));
response.setAccessionId(((Tuple) result).get(accession.id));
Map<String, Long> siteInvCounter = new HashMap<>();
for (int siteIndex = 0, resultIndex = siteIndex + 2; siteIndex < siteAndFilters.size(); siteIndex++, resultIndex++) {
siteInvCounter.put(
siteAndFilters.get(siteIndex).getKey().getSiteShortName(),
((Tuple) result).get(resultIndex, Long.class)
);
}
response.setSiteInventories(siteInvCounter);
return response;
}).collect(Collectors.toList());
return new PageImpl<>(responseItems, page, total);
}
@Override
protected void prepareLabelContext(Map<String, Object> context, Inventory entity) {
context.put("inventory", entity);
context.put("accession", entity.getAccession());
context.put("taxon", entity.getAccession().getTaxonomySpecies().getName());
}
@Override
@Transactional(readOnly = true)
public FilteredPage<InventoryHarvest, InventoryActionFilter> listInventoryHarvest(
InventoryActionFilter filter,
Pageable page
) {
if (filter == null) {
filter = new InventoryActionFilter();
filter.states = Set.of(ActionState.INPROGRESS, ActionState.PENDING, ActionState.SCHEDULED);
}
filter.actionNameCode = Set.of(CommunityCodeValues.INVENTORY_ACTION_HARVEST.value);
var effectiveFilter = new InventoryActionFilter();
// Limit to accessible sites
effectiveFilter.inventory = adjustFilter(null);
// Limit to HARVEST action
effectiveFilter.actionNameCode = Set.of(CommunityCodeValues.INVENTORY_ACTION_HARVEST.value);
effectiveFilter.AND = filter;
var inventoryActions = inventoryActionService.listActions(effectiveFilter, page);
var data = inventoryActions.stream().map(InventoryHarvest::new).collect(Collectors.toList());
// Get all child inventories
var allChildInventories = (List<Inventory>) repository.findAll(
QInventory.inventory.parentInventory().in(data.stream().map(InventoryHarvest::getPlantedInventory).collect(Collectors.toSet()))
);
// Find the harvested inventory for each planted inventory
data.forEach(inventoryHarvest -> {
var plantedInventory = inventoryHarvest.getPlantedInventory();
var harvested = allChildInventories.stream()
// Find child inventories
.filter(candidate -> Objects.equals(plantedInventory.getId(), candidate.getParentInventory().getId()))
// Find the ones with generation = generation+1 OR generation=1 if parent has no generation
.filter(candidate -> candidate.getGeneration() != null
&& (plantedInventory.getGeneration() == null ? candidate.getGeneration().equals(2L) : Objects.equals(plantedInventory.getGeneration() + 1, candidate.getGeneration())))
// Get first one
.findFirst();
inventoryHarvest.setHarvestedInventory(harvested.orElse(null));
});
return new FilteredPage<>(filter, new PageImpl<>(data, page, inventoryActions.getTotalElements()));
}
@Override
@Transactional
@PreAuthorize("@ggceSec.actionAllowed('InventoryData', 'CREATE', #site)")
public InventoryHarvest createHarvestedInventory(
Inventory plantedInventory,
Site site,
InventoryMaintenancePolicy harvestedInventoryMaintenancePolicy,
String inventoryNumberPart1,
String inventoryNumberPart3,
Double harvestedQuantity,
String harvestedQuantityUnitCode,
String containerTypeCode,
Geography productionLocation,
String storageLocationPart1, String storageLocationPart2, String storageLocationPart3, String storageLocationPart4,
boolean isFinalBatch
) {
var harvestFilter = new InventoryActionFilter();
harvestFilter.actionNameCode = Set.of(CommunityCodeValues.INVENTORY_ACTION_HARVEST.value);
harvestFilter.states = Set.of(ActionState.INPROGRESS, ActionState.PENDING, ActionState.SCHEDULED);
harvestFilter.inventory(new InventoryFilter());
harvestFilter.inventory.id(Set.of(plantedInventory.getId()));
var inventoryHarvest = listInventoryHarvest(harvestFilter, Pageable.ofSize(1));
if (inventoryHarvest.page.getTotalElements() != 1) {
throw new InvalidApiUsageException("No such inventory ready for harvest");
}
var plantedInventoryForHarvest = inventoryHarvest.page.getContent().get(0);
var harvestAction = plantedInventoryForHarvest.getHarvestAction();
assert (plantedInventory.getQuantityOnHand() == null || 0d != plantedInventory.getQuantityOnHand().doubleValue());
assert (plantedInventory.getId() != null);
plantedInventory = get(plantedInventoryForHarvest.getPlantedInventory().getId());
plantedInventory = Hibernate.unproxy(plantedInventory, Inventory.class);
var harvestedInventory = plantedInventoryForHarvest.getHarvestedInventory();
if (harvestedInventory != null) { // We allow for updating the harvested inventory
harvestedInventory.setContainerTypeCode(containerTypeCode);
harvestedInventory.setProductionLocationGeography(productionLocation);
// Add to existing quantity
Double newQuantityOnHand = harvestedInventory.getQuantityOnHand();
newQuantityOnHand = newQuantityOnHand == null ? harvestedQuantity : harvestedQuantity == null ? newQuantityOnHand : newQuantityOnHand + harvestedQuantity;
harvestedInventory.setQuantityOnHand(newQuantityOnHand);
harvestedInventory.setQuantityOnHandUnitCode(harvestedQuantityUnitCode);
// Set inventory location
harvestedInventory.setStorageLocationPart1(storageLocationPart1);
harvestedInventory.setStorageLocationPart2(storageLocationPart2);
harvestedInventory.setStorageLocationPart3(storageLocationPart3);
harvestedInventory.setStorageLocationPart4(storageLocationPart4);
harvestedInventory = repository.save(harvestedInventory);
} else { // Register a new inventory!
harvestedInventoryMaintenancePolicy = inventoryMaintPolicyRepo.getReferenceById(harvestedInventoryMaintenancePolicy.getId());
harvestedInventory = new Inventory();
harvestedInventory.apply(plantedInventory);
// Clear
harvestedInventory.setId(null);
harvestedInventory.setBarcode(null);
harvestedInventory.setPathogenStatusCode(null);
harvestedInventory.setBackupInventory(null);
harvestedInventory.setNote(null);
harvestedInventory.setWebAvailabilityNote(null);
// Set
harvestedInventory.setParentInventory(plantedInventory);
harvestedInventory.setSite(site);
harvestedInventory.setGeneration(plantedInventory.getGeneration() == null ? 2L : plantedInventory.getGeneration() + 1);
harvestedInventory.setAvailabilityStatusCode(CommunityCodeValues.INVENTORY_AVAILABILITY_NOTSET.value);
harvestedInventory.applyInventoryMaintenancePolicy(harvestedInventoryMaintenancePolicy);
harvestedInventory.setQuantityOnHand(harvestedQuantity);
harvestedInventory.setQuantityOnHandUnitCode(harvestedQuantityUnitCode);
// Name the inventory
if (StringUtils.isNotBlank(inventoryNumberPart1)) {
harvestedInventory.setInventoryNumberPart1(inventoryNumberPart1); // Apply custom prefix
}
if (StringUtils.isNotBlank(inventoryNumberPart3)) {
harvestedInventory.setInventoryNumberPart3(inventoryNumberPart3); // Apply custom suffix
}
harvestedInventory.setInventoryNumberPart2(-1L); // Auto-renumber using the same name
harvestedInventory.setContainerTypeCode(containerTypeCode);
// Propagation date
harvestedInventory.setPropagationDate(new Date());
harvestedInventory.setPropagationDateCode(CommunityCodeValues.DATE_FORMAT_DATE.value);
// Set inventory location
harvestedInventory.setStorageLocationPart1(storageLocationPart1);
harvestedInventory.setStorageLocationPart2(storageLocationPart2);
harvestedInventory.setStorageLocationPart3(storageLocationPart3);
harvestedInventory.setStorageLocationPart4(storageLocationPart4);
// Set production geography
harvestedInventory.setProductionLocationGeography(productionLocation);
// Barcode the thing!
harvestedInventory.setBarcode(null);
harvestedInventory = repository.save(harvestedInventory);
mintBarcode(harvestedInventory);
harvestedInventory = repository.save(harvestedInventory);
}
var now = Instant.now();
{
// Log information about this harvested batch as action!
var hia = new InventoryAction();
hia.setInventory(harvestedInventory);
hia.setIsDone("Y");
hia.setCompletedDate(now);
hia.setCompletedDateCode(CommunityCodeValues.DATE_FORMAT_DATETIME.value);
hia.setActionNameCode(CommunityCodeValues.INVENTORY_ACTION_LOG.value);
hia.setQuantity(harvestedQuantity);
hia.setQuantityUnitCode(harvestedQuantityUnitCode);
hia.setFormCode(harvestedInventory.getFormTypeCode());
hia.setNote("Increased quantity through harvest");
inventoryActionService.createFast(hia);
}
// Start the action if necessary
if (harvestAction.getStartedDate() == null) {
harvestAction.setStartedDate(now);
harvestAction.setStartedDateCode(CommunityCodeValues.DATE_FORMAT_DATETIME.value);
}
if (isFinalBatch) {
// This will set the quantity of planted material to 0
harvestAction.setIsDone("Y");
harvestAction.setCompletedDate(now);
harvestAction.setCompletedDateCode(CommunityCodeValues.DATE_FORMAT_DATETIME.value);
} else {
// The action is IN PROGRESS and must be closed separately
}
harvestAction.setQuantity(harvestedInventory.getQuantityOnHand());
harvestAction.setQuantityUnitCode(harvestedInventory.getQuantityOnHandUnitCode());
harvestAction = inventoryActionService.updateFast(harvestAction);
plantedInventory = harvestAction.getInventory(); // Action update may update the inventory
// Return planted inventory with harvested inventory and action
plantedInventoryForHarvest.setPlantedInventory(plantedInventory);
plantedInventoryForHarvest.setHarvestedInventory(harvestedInventory);
plantedInventoryForHarvest.setHarvestAction(harvestAction);
return plantedInventoryForHarvest;
}
/**
* Checks if automatic conversion is possible between unitCode1 and unitCode2
*/
@Override
public boolean areCompatibleUnits(String unitCode1, String unitCode2, Double hundredSeedWeight) {
if (unitCode1 == null || unitCode2 == null) return false;
if (StringUtils.equals(unitCode1, unitCode2)) return true;
if (SEED_UNITS.contains(unitCode1)) {
if (SEED_UNITS.contains(unitCode2)) return true;
if (GRAM_UNITS.contains(unitCode2) && hundredSeedWeight != null && hundredSeedWeight != 0d) return true;
} else if (GRAM_UNITS.contains(unitCode1)) {
if (GRAM_UNITS.contains(unitCode2)) return true;
if (SEED_UNITS.contains(unitCode2) && hundredSeedWeight != null && hundredSeedWeight != 0d) return true;
}
return false;
}
/**
* Checks if automatic conversion is possible between unitCode1 and unitCode2
*/
@Override
public Double convertQuantity(String fromUnit, Double quantity, String toUnit, Double hundredSeedWeight) {
if (quantity == null) return null;
if (fromUnit == null || toUnit == null) throw new RuntimeException("Units not specified");
if (StringUtils.equals(fromUnit, toUnit)) return quantity;
if (SEED_UNITS.contains(fromUnit)) { // have seed
if (SEED_UNITS.contains(toUnit)) return quantity; // same
if (GRAM_UNITS.contains(toUnit)) { // want grams
if (hundredSeedWeight == null || hundredSeedWeight == 0d) throw new RuntimeException("Hundred seed weight is not available");
return quantity * hundredSeedWeight / 100d;
}
} else if (GRAM_UNITS.contains(fromUnit)) {
if (SEED_UNITS.contains(toUnit)) {
if (hundredSeedWeight == null || hundredSeedWeight == 0d) throw new RuntimeException("Hundred seed weight is not available");
return (double) Math.round(quantity * 100d / hundredSeedWeight); // Generate round number
}
}
throw new RuntimeException("Cannot convert from " + fromUnit + " to " + toUnit);
}
}