/* * 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.redtape; import java.util.*; import javax.ejb.Stateless; import javax.inject.Inject; import javax.persistence.EntityManager; import org.slf4j.*; import eu.ggnet.dwoss.customer.api.AddressService; import eu.ggnet.dwoss.customer.api.CustomerService; import eu.ggnet.dwoss.event.AddressChange; import eu.ggnet.dwoss.mandator.api.value.Mandator; import eu.ggnet.dwoss.redtape.assist.RedTapes; import eu.ggnet.dwoss.redtape.eao.DocumentEao; import eu.ggnet.dwoss.redtape.eao.DossierEao; import eu.ggnet.dwoss.redtape.emo.AddressEmo; import eu.ggnet.dwoss.redtape.entity.*; import eu.ggnet.dwoss.redtape.format.DossierFormater; import eu.ggnet.dwoss.redtape.state.CustomerDocument; import eu.ggnet.dwoss.redtape.state.RedTapeStateMachine; import eu.ggnet.dwoss.redtape.workflow.*; import eu.ggnet.dwoss.rules.*; import eu.ggnet.dwoss.stock.assist.Stocks; import eu.ggnet.dwoss.stock.eao.StockUnitEao; import eu.ggnet.dwoss.stock.emo.LogicTransactionEmo; import eu.ggnet.dwoss.stock.entity.StockUnit; import eu.ggnet.dwoss.uniqueunit.assist.UniqueUnits; import eu.ggnet.dwoss.uniqueunit.eao.ProductEao; import eu.ggnet.dwoss.uniqueunit.entity.PriceType; import eu.ggnet.dwoss.uniqueunit.entity.Product; import eu.ggnet.dwoss.util.UserInfoException; import eu.ggnet.statemachine.StateTransition; import static eu.ggnet.dwoss.rules.DocumentType.*; /** * This class handles every operation between RedTape, UniqueUnit and Stock. * <u>For Dossier creation both:</u> * {@link RedTapeWorkerOperation#create(long, boolean, java.lang.String) } as well as * {@link RedTapeWorkerOperation#update(eu.ggnet.dwoss.redtape.entity.Document, java.lang.Integer, java.lang.String) } are needed. * create() will only handle RedTapeTransactions while update will handle all necessary SopoTransactions. * <p/> * @author pascal.perau */ @Stateless public class RedTapeWorkerOperation implements RedTapeWorker { private static final Logger L = LoggerFactory.getLogger(RedTapeWorkerOperation.class); @Inject @RedTapes private EntityManager redTapeEm; @Inject private AddressService addressService; @Inject private RedTapeUpdateRepaymentWorkflow repaymentWorkflow; @Inject private RedTapeCreateDossierWorkflow createDossierWorkflow; @Inject @Stocks private EntityManager stockEm; @Inject @UniqueUnits private EntityManager uuEm; @Inject private Mandator mandator; @Inject private CustomerService customerService; private final RedTapeStateMachine stateMachine = new RedTapeStateMachine(); @Override public List<StateTransition<CustomerDocument>> getPossibleTransitions(CustomerDocument cdoc) { return stateMachine.getPossibleTransitions(cdoc); } /** * Creates a new SalesProduct for the PartNo. * <p/> * @param partNo the partNo to use. * @return the new SalesProduct. * @throws UserInfoException if the PartNo does not exist. */ @Override public SalesProduct createSalesProduct(String partNo) throws UserInfoException { ProductEao productEao = new ProductEao(uuEm); Product findByPartNo = productEao.findByPartNo(partNo); if ( findByPartNo == null ) throw new UserInfoException("Part Nummer exestiert Nicht!"); SalesProduct salesProduct = new SalesProduct(partNo, findByPartNo.getName(), findByPartNo.getPrice(PriceType.SALE), findByPartNo.getId(), findByPartNo.getDescription()); redTapeEm.persist(salesProduct); return salesProduct; } /** * Creates a new, valid Dossier containing a document of the order type. * * See {@link RedTapeCreateDossierWorkflow} for implementation. * * @param customerId The customer associated to the new Dossier. * @param dispatch The dispatch state of the Dossier. * @param arranger The arranger of the new Dossier. * @return A new, valid, persisted Dossier. */ @Override public Dossier create(long customerId, boolean dispatch, String arranger) { return createDossierWorkflow.execute(customerId, dispatch, arranger); } /** * This method handles necessary cleanups if creation or update is canceled. * <p/> * If stock.LogicTransaction differs form Dossier.Document.Positions ⇒ change LogicTransaction. * <ul><li>Only LogicTransaction > Dossier.Document.Positions should be possible</li></ul> * If Dossier.isEmpty → delete. * If stock.LogicTransaction is empty → delete. */ @Override public Document revertCreate(Document detached) throws UserInfoException { Document original = new DocumentEao(redTapeEm).findById(detached.getId()); if ( original.isActive() != detached.isActive() ) throw new UserInfoException("Das Document wurde durch jemand anderen inzwischen geändert, bitte neu laden.\n" + "Hint: original(" + original.getId() + ").active=" + original.isActive() + ", detached(" + detached.getId() + ").active=" + detached.isActive()); if ( original.getOptLock() != detached.getOptLock() ) throw new UserInfoException("Das Document wurde durch jemand anderen inzwischen geändert, bitte neu laden.\n" + "Hint: original(" + original.getId() + ").optLock=" + original.getOptLock() + ", detached(" + detached.getId() + ").optLock=" + detached.getOptLock()); LogicTransactionEmo ltEmo = new LogicTransactionEmo(stockEm); if ( !detached.isClosed() ) ltEmo.equilibrate(original.getDossier().getId(), original.getPositionsUniqueUnitIds()); if ( original.getDossier().getDocuments().size() == 1 && original.getPositions().isEmpty() ) { redTapeEm.remove(original.getDossier()); return null; } return original; } /** * Gives a pair of {@link Address}es based on the Customer. * <p/> * If no Address could be found, new persisted enteties are created. * <p/> */ @Override public Addresses requestAdressesByCustomer(long customerId) { AddressEmo addressEmo = new AddressEmo(redTapeEm); Address invoice = addressEmo.request(addressService.defaultAddressLabel(customerId, AddressType.INVOICE)); Address shipping = addressEmo.request(addressService.defaultAddressLabel(customerId, AddressType.SHIPPING)); return new Addresses(invoice, shipping); } /** * Gives either an address out of the db or persist a new one if nothing is found. * <p/> * @param description * @return the found or new persisted Address */ @Override public Address requestAddressByDescription(String description) { return new AddressEmo(redTapeEm).request(description); } /** * Update Comments of the Dossier. * <p/> * @param dossier the dossier to update * @param comment the comment * @return returns the dossier * @throws UserInfoException if something is not ok. */ @Override public Dossier updateComment(Dossier dossier, String comment) throws UserInfoException { if ( Objects.equals(dossier.getComment(), comment) ) return dossier; long id = dossier.getId(); // This is some magic to find out if this is are SopoAuftrag. if ( id == 0 ) throw new UserInfoException("Dossier is not in Database, but updateComment is called"); dossier = new DossierEao(redTapeEm).findById(dossier.getId()); if ( dossier == null ) throw new UserInfoException("Dossier(id=" + id + ") is not in Database, but updateComment is called"); dossier.fetchEager(); dossier.setComment(comment); return dossier; } /** * Create a HTML formated String representing the detailed information from a {@link Dossier}. * <p/> * @param dossierId The Dossier * @return a HTML formated String representing the detailed information from a {@link Dossier}. */ @Override public String toDetailedHtml(long dossierId) { Dossier dos = redTapeEm.find(Dossier.class, dossierId); if ( dos == null ) return "<strong>No Dossier with Id: " + dossierId + " found</strong>"; String detailedHtmlCustomer = customerService.asHtmlHighDetailed(dos.getCustomerId()); String stockInfo = "StockUnits:<ul>"; for (StockUnit stockUnit : new StockUnitEao(stockEm).findByUniqueUnitIds(dos.getRelevantUniqueUnitIds())) stockInfo += "<li>" + stockUnit + "</li>"; stockInfo += "</ul>"; return "<html>" + detailedHtmlCustomer + "<br />" + DossierFormater.toHtmlDetailed(dos) + "<br />" + stockInfo + "</html>"; } /** * Deletes a {@link Dossier}, cleaning up the Stock. * <p/> * @param dos the Dossier to be deleted. */ @Override public void delete(Dossier dos) { new LogicTransactionEmo(stockEm).equilibrate(dos.getId(), new ArrayList<>()); Dossier attachedDossier = new DossierEao(redTapeEm).findById(dos.getId()); redTapeEm.remove(attachedDossier); } /** * Changes the {@link Address} of all active {@link Document} of {@link DocumentType#ORDER} and no Invoices or CreditMemos found from every {@link Dossier} * containing a specific customer id. * <p/> * If one of the addresses does not exist, it will be created. * <p/> * @param addressChange */ @Override public void updateAllDocumentAdresses(AddressChange addressChange) { AddressEmo addressEmo = new AddressEmo(redTapeEm); Address address = addressEmo.request(addressService.defaultAddressLabel(addressChange.getCustomerId(), addressChange.getType())); redTapeEm.detach(address); List<Dossier> dossiers = new DossierEao(redTapeEm).findByCustomerId(addressChange.getCustomerId()); for (Dossier dossier : dossiers) { if ( !dossier.getActiveDocuments(DocumentType.INVOICE).isEmpty() ) continue; for (Document document : new HashSet<>(dossier.getActiveDocuments(DocumentType.ORDER))) { if ( document.getConditions().contains(Document.Condition.CANCELED) ) continue; // May be fetchEager redTapeEm.detach(document); if ( addressChange.getType() == AddressType.INVOICE ) document.setInvoiceAddress(address); if ( addressChange.getType() == AddressType.SHIPPING ) document.setShippingAddress(address); redTapeEm.flush(); redTapeEm.clear(); internalUpdate(document, null, addressChange.getArranger()); } } } /** * Changes the State, of a Customer and Document based on the transition. * <p/> * @param cdoc the Document and Customer to take the change. * @param transition the transition to do. * @param arranger the arranger * @return the Document in the new state. */ @Override public Document stateChange(CustomerDocument cdoc, StateTransition<CustomerDocument> transition, String arranger) { EnumSet<CustomerFlag> customerFlags = EnumSet.noneOf(CustomerFlag.class); customerFlags.addAll(cdoc.getCustomerFlags()); L.info("stateChange with {} on {}", transition.getName(), cdoc); stateMachine.stateChange(cdoc, transition); // INFO: This is a stupid solution. if ( !customerFlags.equals(cdoc.getCustomerFlags()) ) customerService.updateCustomerFlags(cdoc.getDocument().getDossier().getCustomerId(), cdoc.getCustomerFlags()); return internalUpdate(cdoc.getDocument(), null, arranger); } /** * Update changes from a Document by looking up the original from the database. * <p/> * A document is not equal if {@link Document#equalsContent(Document) } is false * or Document.getDossier.paymentMethod or Document.getDossier.dispatch are different. * Every Document manipulation is done by this method and handling all necessary manipulations in the SopoSoft system as well. * <p/> * <u>Dossier Handling</u> * <ul> * <li>Changes to {@link Dossier#paymentMethod} and {@link Dossier#dispatch} are persisted</li> * </ul> * <u>Document Handling</u> * <ul> * <li>If the given Document has no changes it is returned right away</li> * <li>If unequal a {@link Document#partialClone() } is used and detached Entities are attached (Dossier, Addresses)</li> * <li>If the {@link Document#getType() } is INVOICE while the previous version is not, a new Invoice Identifier is set to the new Document.</li> * </ul> * <u>SopoAuftrag Handling</u> * <ul> * <li>If the Document is updated, the SopoAuftrag will be updated as well.<br /> * This is done by clearing all Positions and refill the whole SopoAuftrag.</li> * <li>If no SopoAuftrag exist, a new one is created.</li> * </ul> * <u>Stock Handling</u> * <ul> * <li>In any update process, the LogicTransaction will be cleared from its StockUnits</li> * <li>Does the new Document contain no Position of Position.Type.UNIT the LogicTransaction will be deleted.</li> * <li>Should there be any clash of StockUnit Transaction information, a Exception is thrown</li> * </ul> * <p/> * * @param doc The Document that will be equalised against the original * @param destination In the case of CreditMemo, the destination for the units. * @param arranger The recent user * @return A new persisted Document or the given if equal */ @Override public Document update(final Document doc, Integer destination, final String arranger) { return internalUpdate(doc, destination, arranger); } private Document internalUpdate(final Document doc, Integer destination, final String arranger) { RedTapeWorkflow workflow = null; switch (doc.getType()) { case BLOCK: workflow = new RedTapeUpdateBlockWorkflow( redTapeEm, uuEm, stockEm, doc, arranger, mandator); break; case ORDER: workflow = new RedTapeUpdateOrderWorkflow( redTapeEm, uuEm, stockEm, doc, arranger, mandator); break; case INVOICE: workflow = new RedTapeUpdateInvoiceWorkflow( redTapeEm, uuEm, stockEm, doc, arranger, mandator); break; case RETURNS: case CAPITAL_ASSET: workflow = new RedTapeUpdateCapitalAssetReturnsWorkflow( redTapeEm, uuEm, stockEm, doc, arranger, mandator); break; case COMPLAINT: workflow = new RedTapeUpdateComplaintWorkflow( redTapeEm, uuEm, stockEm, doc, arranger, mandator); break; case CREDIT_MEMO: case ANNULATION_INVOICE: return fetchEager(repaymentWorkflow.execute(doc, destination, arranger)); default: } Document result = workflow.execute(); // TODO: Make a better fetch eager result.getDossier().getDocuments().size(); return result; } private Document fetchEager(Document doc) { doc.getDossier().getDocuments().size(); return doc; } }