/*
* Copyright (C) 2014 GG-Net GmbH - Oliver Günther
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.ggnet.dwoss.receipt;
import eu.ggnet.dwoss.rules.ReceiptOperation;
import eu.ggnet.dwoss.rules.PositionType;
import eu.ggnet.dwoss.redtape.entity.Dossier;
import eu.ggnet.dwoss.redtape.entity.Position;
import eu.ggnet.dwoss.redtape.entity.PositionBuilder;
import eu.ggnet.dwoss.redtape.entity.Document;
import eu.ggnet.dwoss.stock.entity.StockTransactionStatus;
import eu.ggnet.dwoss.stock.entity.StockUnit;
import eu.ggnet.dwoss.stock.eao.StockTransactionEao;
import eu.ggnet.dwoss.stock.eao.LogicTransactionEao;
import eu.ggnet.dwoss.stock.entity.StockTransactionParticipationType;
import eu.ggnet.dwoss.stock.entity.StockTransactionType;
import eu.ggnet.dwoss.stock.entity.Stock;
import eu.ggnet.dwoss.stock.emo.StockLocationDiscoverer;
import eu.ggnet.dwoss.stock.emo.LogicTransactionEmo;
import eu.ggnet.dwoss.stock.eao.StockUnitEao;
import eu.ggnet.dwoss.stock.entity.StockTransactionStatusType;
import eu.ggnet.dwoss.stock.entity.Shipment;
import eu.ggnet.dwoss.stock.entity.StockTransaction;
import eu.ggnet.dwoss.stock.entity.StockTransactionPosition;
import eu.ggnet.dwoss.stock.entity.LogicTransaction;
import eu.ggnet.dwoss.stock.entity.StockTransactionParticipation;
import eu.ggnet.dwoss.uniqueunit.eao.UniqueUnitEao;
import eu.ggnet.dwoss.uniqueunit.entity.Product;
import eu.ggnet.dwoss.uniqueunit.format.UniqueUnitFormater;
import eu.ggnet.dwoss.uniqueunit.entity.UniqueUnitHistory;
import eu.ggnet.dwoss.uniqueunit.eao.ProductEao;
import eu.ggnet.dwoss.uniqueunit.entity.UniqueUnit;
import eu.ggnet.dwoss.uniqueunit.format.ProductFormater;
import java.sql.*;
import java.util.*;
import java.util.Date;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.persistence.*;
import javax.sql.DataSource;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.*;
import eu.ggnet.dwoss.mandator.api.value.PostLedger;
import eu.ggnet.dwoss.mandator.api.value.ReceiptCustomers;
import eu.ggnet.dwoss.redtape.assist.RedTapes;
import eu.ggnet.dwoss.redtape.eao.DossierEao;
import eu.ggnet.dwoss.redtape.emo.DossierEmo;
import eu.ggnet.dwoss.stock.assist.Stocks;
import eu.ggnet.dwoss.uniqueunit.assist.UniqueUnits;
import eu.ggnet.dwoss.uniqueunit.entity.UniqueUnit.Identifier;
import eu.ggnet.dwoss.util.UserInfoException;
import eu.ggnet.dwoss.util.persistence.eao.DefaultEao;
import static eu.ggnet.dwoss.uniqueunit.entity.UniqueUnit.Identifier.*;
/**
* Receipt Operation for Units.
*
* @author oliver.guenther
*/
@Stateless
public class UnitProcessorOperation implements UnitProcessor {
private final static Logger L = LoggerFactory.getLogger(UnitProcessorOperation.class);
@Inject
@UniqueUnits
private EntityManager uuEm;
@Inject
@Stocks
private EntityManager stockEm;
@Inject
@RedTapes
private DataSource redTapeSource;
@Inject
@RedTapes
private EntityManager redTapeEm;
@Inject
private ReceiptCustomers receiptCustomers;
@Inject
private PostLedger postLedger;
/**
* Receipts a new Unit.
* Multiphase Process:
* <ol>
* <li>Validation and throw IllegalArgumentException if
* <ul>
* <li>Supplied UniqueUnit is already persistent</li>
* <li>RefurbishedId is already taken</li>
* <li>Serial is already taken and Unit is in Stock [UniqueUnit & Stock]</li>
* </ul>
* </li>
* <li>If serial is taken update existing Unit else persist Unit[UniqueUnit]<br />
* (At this point it implies that the Unit is not in Stock)</li>
* <li>Create, weak reference StockUnit and prepare for rollIn [Stock]</li>
* </ol>
* <p/>
* @param shipment the shipment
* @param recieptUnit the UniqueUnit to be receipt, must not be null
* @param operation the Operation to do
* @param transaction
* @param arranger
* @param operationComment
* @throws IllegalArgumentException if validation fails
*/
@Override
public void receipt(UniqueUnit recieptUnit, Product product, Shipment shipment,
StockTransaction transaction, ReceiptOperation operation, String operationComment, String arranger) throws IllegalArgumentException {
L.info("Receiping Unit(refurbishId={},name={}) on StockTransaction(id={}) with {} by {}",
recieptUnit.getRefurbishId(), ProductFormater.toNameWithPartNo(product), transaction.getId(), operation, arranger);
validateReceipt(recieptUnit);
UniqueUnit uniqueUnit = receiptUniqueUnit(recieptUnit, Objects.requireNonNull(product, "Product == null, not allowed"), shipment);
StockUnit stockUnit = receiptAndAddStockUnit(uniqueUnit, transaction);
if ( operation == ReceiptOperation.SALEABLE ) return; // Nothing to do
executeOperation(uniqueUnit, stockUnit, operation, operationComment, arranger);
}
/**
* Updates the UniqueUnit.
* Multiphase Process.
* <ol>
* <li>Validate all input data.</li>
* <li>Merge UniqueUnit and set Product</li>
* <li>Overwrite StockUnit</li>
* <li>Overwrite SopoUnit</li>
* <li>If SopoUnit is on "Process equivalent Customer", remove it there.</li>
* <li>Execute Operation [Sopo]:
* <ul>
* <li>If Operation == Sales, nothing more to do</li>
* <li>Else findByTypeAndStatus the KundenId from Contractor and Operation -> create or reuse a Auftrag and add the SopoUnit</li>
* </ul>
* </li>
* </ol>
*
* @param uniqueUnit the UniqueUnit, must not be null
* @param product the Product, must not be null
* @param updateOperation the operation, must not be null
* @param operationComment the comment of the operation, may be null
* @param arranger
*/
@Override
public void update(UniqueUnit uniqueUnit, Product product, ReceiptOperation updateOperation, String operationComment, String arranger) throws IllegalArgumentException {
L.info("Updateing UniqueUnit(id={},refurbishId={},name={}) with {} by {}", uniqueUnit.getId(),
uniqueUnit.getRefurbishId(), ProductFormater.toNameWithPartNo(product), updateOperation, arranger);
uniqueUnit = updateUniqueUnit(uniqueUnit, product);
StockUnit stockUnit = optionalUpdateStockUnit(uniqueUnit);
// These two operations define, that nothing in LT/RedTape/Sopo may be changed.
if ( updateOperation == ReceiptOperation.IN_SALE || stockUnit == null ) return;
boolean executeNextOperation = cleanUpOldOperation(uniqueUnit, stockUnit, updateOperation, operationComment, arranger);
if ( executeNextOperation ) executeOperation(uniqueUnit, stockUnit, updateOperation, operationComment, arranger);
}
/**
* Transfers a UniqueUnits StockUnit to the supplied Stock.
*
* <ul>
* <li>Validate, if a StockUnit for the UniqueUnit exists, and this StockUnit is in Stock</li>
* <li>Transfer StockUnit via {@link StockTransactionType#EXTERNAL_TRANSFER}</li>
* <li>Update the SopoUnit</li>
* </ul>
* <p/>
* @param uniqueUnit the uniqueUnit
* @param stockId the stockId
* @param arranger
* @return
*/
// TODO: Use StockTransactionEmo.resquestExternalTransfer and completeExternalTransfer
@Override
public UniqueUnit transfer(UniqueUnit uniqueUnit, int stockId, String arranger) {
StockUnit stockUnit = new StockUnitEao(stockEm).findByUniqueUnitId(uniqueUnit.getId());
if ( stockUnit == null ) throw new IllegalArgumentException("No StockUnit for " + uniqueUnit);
if ( stockUnit.isInTransaction() ) throw new IllegalArgumentException(stockUnit + " is on Transaction");
Stock destination = new DefaultEao<>(Stock.class, stockEm).findById(stockId);
StockTransaction st = new StockTransaction(StockTransactionType.EXTERNAL_TRANSFER);
StockTransactionStatus status = new StockTransactionStatus(StockTransactionStatusType.COMPLETED, new Date());
status.addParticipation(new StockTransactionParticipation(StockTransactionParticipationType.ARRANGER, arranger));
st.addStatus(status);
st.setSource(stockUnit.getStock());
st.setDestination(destination);
st.addPosition(new StockTransactionPosition(stockUnit));
stockEm.persist(st);
stockUnit.setPosition(null);
new StockLocationDiscoverer(stockEm).discoverAndSetLocation(stockUnit, destination);
uniqueUnit = new UniqueUnitEao(uuEm).findById(uniqueUnit.getId());
uniqueUnit.addHistory("External Stock change from " + stockUnit.getStock() + " to " + destination + " by " + arranger);
uniqueUnit.fetchEager();
return uniqueUnit;
}
/**
* Returns a editable UniqueUnit.
* An Exception is thrown if:
* <ul>
* <li>No UniqueUnit with refurbishedId</li>
* <li>No StockUnit for UniqueUnit</li>
* <li>StockUnit is on Transaction</li>
* <li>No SopoUnit with refurbishedId</li>
* <li>No SopoUnit UniqueUnit miss match</li>
* </ul>
* The Operation is discovert via:
* <ul>
* <li>If on an AlphaAcount, and operation is allowed, returns appropriated operation</li>
* <li>If on no Auftrag, returns Sales</li>
* <li>If on any other Auftrag, returns null</li>
* </ul>
*
* @param refurbishedIdOrSerial the refurbishedId or the serial, both are tried
* @return a EditableUnit with, the editable UniqueUnit, the refrencing StockUnit, the Operation it is in, and the PartNo
* @throws UserInfoException if refurbishedId is not ok.
*/
@Override
public EditableUnit findEditableUnit(String refurbishedIdOrSerial) throws UserInfoException {
if ( StringUtils.isBlank(refurbishedIdOrSerial) ) throw new UserInfoException("Keine SopoNr/Seriennummer eingegeben");
UniqueUnitEao uniqueUnitEao = new UniqueUnitEao(uuEm);
UniqueUnit uniqueUnit = uniqueUnitEao.findByIdentifier(REFURBISHED_ID, refurbishedIdOrSerial);
if ( uniqueUnit == null ) uniqueUnit = uniqueUnitEao.findByIdentifier(SERIAL, refurbishedIdOrSerial);
if ( uniqueUnit == null )
throw new UserInfoException("Keine Gerät mit SopoNr/Seriennummer " + refurbishedIdOrSerial + " gefunden");
StockUnit stockUnit;
ReceiptOperation operation = ReceiptOperation.SALEABLE;
stockUnit = new StockUnitEao(stockEm).findByUniqueUnitId(uniqueUnit.getId());
if ( stockUnit == null )
throw new UserInfoException("Keine Lagergerät für SopoNr " + uniqueUnit.getIdentifier(REFURBISHED_ID) + " gefunden, bearbeitung unzulässig");
LogicTransaction lt = new LogicTransactionEao(stockEm).findByUniqueUnitId(uniqueUnit.getId());
if ( lt != null ) {
operation = receiptCustomers
.getOperation(new DossierEao(redTapeEm).findById(lt.getDossierId()).getCustomerId())
.orElse(ReceiptOperation.IN_SALE);
}
// Lazyinit
uniqueUnit.fetchEager();
return new EditableUnit(uniqueUnit, stockUnit, operation, uniqueUnit.getProduct() == null ? "" : uniqueUnit.getProduct().getPartNo());
}
private Position toPosition(UniqueUnit uniqueUnit, String operationComment) {
return new PositionBuilder().setType(PositionType.UNIT).setBookingAccount(postLedger.get(PositionType.UNIT).orElse(-1))
.setDescription(UniqueUnitFormater.toDetailedDiscriptionLine(uniqueUnit) + ", Aufnahme: " + operationComment)
.setName(UniqueUnitFormater.toPositionName(uniqueUnit))
.setUniqueUnitId(uniqueUnit.getId())
.setUniqueUnitProductId(uniqueUnit.getProduct().getId()).createPosition();
}
private void validateReceipt(UniqueUnit receiptUnit) throws IllegalArgumentException {
if ( receiptUnit.getId() > 0 ) throw new IllegalArgumentException("UniqueUnit has already been persisted " + receiptUnit);
UniqueUnitEao uniqueUnitEao = new UniqueUnitEao(uuEm);
UniqueUnit uniqueUnit = uniqueUnitEao.findByIdentifier(Identifier.REFURBISHED_ID, receiptUnit.getIdentifier(Identifier.REFURBISHED_ID));
if ( uniqueUnit != null ) throw new IllegalArgumentException(
"UniqueUnit(refurbishedId=" + receiptUnit.getIdentifier(Identifier.REFURBISHED_ID) + ") exists: " + uniqueUnit);
StockUnit stockUnit = new StockUnitEao(stockEm).findByRefurbishId(receiptUnit.getRefurbishId());
if ( stockUnit != null ) throw new IllegalArgumentException(stockUnit + " exists");
}
private UniqueUnit receiptUniqueUnit(UniqueUnit recieptUnit, Product product, Shipment shipment) {
UniqueUnit uniqueUnit = new UniqueUnitEao(uuEm).findByIdentifier(UniqueUnit.Identifier.SERIAL, recieptUnit.getIdentifier(UniqueUnit.Identifier.SERIAL));
product = new ProductEao(uuEm).findById(product.getId());
if ( uniqueUnit == null ) {
recieptUnit.setProduct(product);
L.debug("persisting {}", recieptUnit);
// TODO: Update in UI.
recieptUnit.setShipmentId(shipment.getId());
recieptUnit.setShipmentLabel(shipment.getShipmentId());
recieptUnit.setInputDate(new Date());
uuEm.persist(recieptUnit);
uniqueUnit = recieptUnit;
} else {
uniqueUnit.setIdentifier(UniqueUnit.Identifier.REFURBISHED_ID, recieptUnit.getIdentifier(UniqueUnit.Identifier.REFURBISHED_ID));
uniqueUnit.setComment(recieptUnit.getComment());
uniqueUnit.setComments(recieptUnit.getComments());
uniqueUnit.setCondition(recieptUnit.getCondition());
uniqueUnit.setInternalComment(recieptUnit.getInternalComment());
uniqueUnit.setInternalComments(recieptUnit.getInternalComments());
uniqueUnit.setEquipments(recieptUnit.getEquipments());
uniqueUnit.setProduct(product);
uniqueUnit.setContractor(shipment.getContractor());
uniqueUnit.setShipmentId(shipment.getId());
uniqueUnit.setShipmentLabel(shipment.getShipmentId());
uniqueUnit.setWarranty(recieptUnit.getWarranty());
uniqueUnit.setWarrentyValid(recieptUnit.getWarrentyValid());
uniqueUnit.setInputDate(new Date()); // Allways set the InputDate on receipt.
L.debug("updating {}", recieptUnit);
}
return uniqueUnit;
}
private UniqueUnit updateUniqueUnit(UniqueUnit uniqueUnit, Product product) {
product = new ProductEao(uuEm).findById(product.getId());
uniqueUnit = uuEm.merge(uniqueUnit);
uniqueUnit.setProduct(product);
L.debug("updating {}", uniqueUnit);
return uniqueUnit;
}
private StockUnit receiptAndAddStockUnit(UniqueUnit uniqueUnit, StockTransaction transaction) {
StockUnit stockUnit = new StockUnit();
stockUnit.setUniqueUnitId(uniqueUnit.getId());
stockUnit.setRefurbishId(uniqueUnit.getIdentifier(UniqueUnit.Identifier.REFURBISHED_ID));
stockUnit.setName(uniqueUnit.getProduct().getTradeName().getName() + " " + uniqueUnit.getProduct().getName());
transaction = new StockTransactionEao(stockEm).findById(transaction.getId());
transaction.addPosition(new StockTransactionPosition(stockUnit));
L.debug("adding {} to {}", stockUnit, transaction); // implies persist on transaction commit
return stockUnit;
}
private StockUnit optionalUpdateStockUnit(UniqueUnit uniqueUnit) {
StockUnit stockUnit = new StockUnitEao(stockEm).findByUniqueUnitId(uniqueUnit.getId());
if ( stockUnit == null ) return null;
stockUnit.setRefurbishId(uniqueUnit.getIdentifier(Identifier.REFURBISHED_ID));
stockUnit.setName(uniqueUnit.getProduct().getName());
L.debug("updating {}", stockUnit);
return stockUnit;
}
private void executeOperation(UniqueUnit uniqueUnit, StockUnit stockUnit, ReceiptOperation operation, String operationComment, String arranger) {
long customerId = receiptCustomers.getCustomerId(uniqueUnit.getContractor(), operation);
Document doc = new DossierEmo(redTapeEm)
.requestActiveDocumentBlock((int)customerId, "Blockaddresse KundenId " + customerId, "Erzeugung durch " + operation, arranger);
L.debug("requestActiveDocumentBlock = {} with dossier = {}", doc, doc.getDossier());
redTapeEm.flush();
redTapeEm.refresh(doc, LockModeType.PESSIMISTIC_FORCE_INCREMENT);
L.debug("Refreshed requestActiveDocumentBlock to = {} with dossier = {}", doc, doc.getDossier());
if ( !doc.isActive() )
throw new RuntimeException("The Document(id={}) has changed to inactive while locking, this was very unlikely, inform Administrator");
int directCount = countPositionsDirect(doc);
if ( doc.getPositions().size() != directCount ) {
L.warn("Using Workaround for UniqueUnit(id=" + uniqueUnit.getId() + ",refurbishId=" + uniqueUnit.getRefurbishId()
+ " cause Document(id=" + doc.getId() + ").position.size=" + doc.getPositions().size() + " != directCount=" + directCount);
doc.append(directCount + 1, toPosition(uniqueUnit, operationComment));
} else {
doc.append(toPosition(uniqueUnit, operationComment));
}
LogicTransaction lt = new LogicTransactionEmo(stockEm).request(doc.getDossier().getId(), LockModeType.PESSIMISTIC_FORCE_INCREMENT);
lt.add(stockUnit); // Implicit removes it from an existing LogicTransaction
L.debug("Executed Operation {} for uniqueUnit(id={},refurbishId={}), added to LogicTransaction({}) and Dossier({})",
operation, uniqueUnit.getId(), uniqueUnit.getRefurbishId(), lt.getId(), doc.getDossier().getIdentifier());
uniqueUnit.addHistory(UniqueUnitHistory.Type.STOCK, "RecepitOeration:" + operation + ", " + operationComment + " by " + arranger);
}
private boolean cleanUpOldOperation(UniqueUnit uniqueUnit, StockUnit stockUnit, ReceiptOperation updateOperation, String operationComment, String arranger) {
LogicTransaction oldLogicTransaction = stockUnit.getLogicTransaction();
if ( oldLogicTransaction != null ) {
Dossier oldDossier = new DossierEao(redTapeEm).findById(oldLogicTransaction.getDossierId());
ReceiptOperation oldOperation = receiptCustomers.getOperation(oldDossier.getCustomerId()).orElse(null);
Document oldDocument = oldDossier.getActiveDocuments().get(0);
redTapeEm.flush();
redTapeEm.refresh(oldDocument, LockModeType.PESSIMISTIC_FORCE_INCREMENT);
if ( !oldDocument.isActive() ) throw new RuntimeException(
"The Document(id={}) has changed to inactive while locking, this was very unlikely, inform Administrator");
Position oldPosition = oldDocument.getPositionByUniqueUnitId(uniqueUnit.getId());
if ( oldOperation == updateOperation ) {
oldPosition.setDescription(oldPosition.getDescription() + ", Aufnahme: " + operationComment);
L.debug("old operation and update operation are {}, nothing more to do", updateOperation);
return false;
}
// cleanUp old Block and Auftrag
convertToComment(oldPosition, updateOperation);
L.debug("Old Operation cleanup, removed uniqueUnit(id={},refurbishId={}) from Dossier({})",
new Object[]{uniqueUnit.getId(), uniqueUnit.getRefurbishId(), oldDossier.getIdentifier()});
}
if ( updateOperation == ReceiptOperation.SALEABLE ) {
if ( oldLogicTransaction != null ) oldLogicTransaction.remove(stockUnit);
uniqueUnit.addHistory("Released for Sale by " + arranger);
L.debug("update operation is {}, nothing more to do", updateOperation);
return false;
}
return true;
}
private void convertToComment(Position position, ReceiptOperation operation) {
position.setType(PositionType.COMMENT);
position.setUniqueUnitId(0);
position.setUniqueUnitProductId(0);
position.setPrice(0);
position.setAfterTaxPrice(0);
position.setDescription("Entfernt durch " + operation + ", war: " + position.getName());
position.setName("Entfernt durch " + operation);
}
/**
* This is maximum crap.
* We need to verify the very seldom case, that another transaction did in fact modify the Document
* by adding positions in the same phase, this em was active.
*
* @param doc theDocument
* @return the amount of Positions directly from the Databases.
*/
// FIXME: Repair this asp, after we switched to the EJB Server.
private int countPositionsDirect(Document doc) {
try {
try (Connection con = redTapeSource.getConnection();
Statement stm = con.createStatement();
ResultSet rs = stm.executeQuery("SELECT COUNT(id) FROM Position WHERE Document_id = " + doc.getId());) {
rs.next();
return rs.getInt(1);
}
} catch (SQLException ex) {
throw new RuntimeException(ex);
}
}
}