001// --------------------------------------------------------------------------------
002// Copyright 2002-2024 Echo Three, LLC
003//
004// Licensed under the Apache License, Version 2.0 (the "License");
005// you may not use this file except in compliance with the License.
006// You may obtain a copy of the License at
007//
008//     http://www.apache.org/licenses/LICENSE-2.0
009//
010// Unless required by applicable law or agreed to in writing, software
011// distributed under the License is distributed on an "AS IS" BASIS,
012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013// See the License for the specific language governing permissions and
014// limitations under the License.
015// --------------------------------------------------------------------------------
016
017package com.echothree.model.control.purchase.server.logic;
018
019import com.echothree.model.control.cancellationpolicy.common.CancellationKinds;
020import com.echothree.model.control.cancellationpolicy.server.logic.CancellationPolicyLogic;
021import com.echothree.model.control.core.server.control.CoreControl;
022import com.echothree.model.control.order.common.OrderRoleTypes;
023import com.echothree.model.control.order.common.OrderTypes;
024import com.echothree.model.control.order.server.control.OrderControl;
025import com.echothree.model.control.order.server.control.OrderRoleControl;
026import com.echothree.model.control.order.server.logic.OrderLogic;
027import com.echothree.model.control.purchase.common.choice.PurchaseOrderStatusChoicesBean;
028import com.echothree.model.control.purchase.common.exception.DuplicateHandlingInPurchaseOrderStatusTransitionException;
029import com.echothree.model.control.purchase.common.exception.InvalidPurchaseOrderReferenceException;
030import com.echothree.model.control.purchase.common.exception.InvalidPurchaseOrderStatusException;
031import com.echothree.model.control.purchase.common.exception.PurchaseOrderDuplicateReferenceException;
032import com.echothree.model.control.purchase.common.exception.PurchaseOrderReferenceRequiredException;
033import com.echothree.model.control.purchase.common.exception.UnhandledPurchaseOrderStatusTransitionException;
034import com.echothree.model.control.purchase.common.exception.UnknownPurchaseOrderStatusChoiceException;
035import com.echothree.model.control.purchase.common.workflow.PurchaseOrderStatusConstants;
036import com.echothree.model.control.returnpolicy.common.ReturnKinds;
037import com.echothree.model.control.returnpolicy.server.logic.ReturnPolicyLogic;
038import com.echothree.model.control.sequence.common.SequenceTypes;
039import com.echothree.model.control.sequence.server.logic.SequenceGeneratorLogic;
040import com.echothree.model.control.shipment.server.control.PartyFreeOnBoardControl;
041import com.echothree.model.control.shipment.server.logic.FreeOnBoardLogic;
042import com.echothree.model.control.term.server.control.TermControl;
043import com.echothree.model.control.term.server.logic.TermLogic;
044import com.echothree.model.control.user.server.control.UserControl;
045import com.echothree.model.control.vendor.server.control.VendorControl;
046import com.echothree.model.control.vendor.server.logic.VendorLogic;
047import com.echothree.model.control.workflow.server.control.WorkflowControl;
048import com.echothree.model.control.workflow.server.logic.WorkflowDestinationLogic;
049import com.echothree.model.control.workflow.server.logic.WorkflowLogic;
050import com.echothree.model.control.workflow.server.logic.WorkflowStepLogic;
051import com.echothree.model.data.cancellationpolicy.server.entity.CancellationPolicy;
052import com.echothree.model.data.core.server.entity.EntityInstance;
053import com.echothree.model.data.order.server.entity.Order;
054import com.echothree.model.data.order.server.entity.OrderPriority;
055import com.echothree.model.data.party.common.pk.PartyPK;
056import com.echothree.model.data.party.server.entity.Language;
057import com.echothree.model.data.party.server.entity.Party;
058import com.echothree.model.data.returnpolicy.server.entity.ReturnPolicy;
059import com.echothree.model.data.shipment.server.entity.FreeOnBoard;
060import com.echothree.model.data.term.server.entity.Term;
061import com.echothree.model.data.user.server.entity.UserVisit;
062import com.echothree.model.data.vendor.server.entity.Vendor;
063import com.echothree.model.data.vendor.server.entity.VendorType;
064import com.echothree.util.common.message.ExecutionErrors;
065import com.echothree.util.common.persistence.BasePK;
066import com.echothree.util.server.message.ExecutionErrorAccumulator;
067import com.echothree.util.server.persistence.Session;
068
069public class PurchaseOrderLogic
070        extends OrderLogic {
071
072    private PurchaseOrderLogic() {
073        super();
074    }
075
076    private static class LogicHolder {
077        static PurchaseOrderLogic instance = new PurchaseOrderLogic();
078    }
079
080    public static PurchaseOrderLogic getInstance() {
081        return LogicHolder.instance;
082    }
083    
084    public void validatePurchaseOrderReference(final ExecutionErrorAccumulator eea, final String reference,
085            final Vendor vendor) {
086        var requireReference = vendor.getRequireReference();
087
088        if(requireReference != null) {
089            var allowReferenceDuplicates = vendor.getAllowReferenceDuplicates();
090            var referenceValidationPattern = vendor.getReferenceValidationPattern();
091
092            if(requireReference && reference == null) {
093                handleExecutionError(PurchaseOrderReferenceRequiredException.class, eea, ExecutionErrors.PurchaseOrderReferenceRequired.name());
094            } else if(reference != null) {
095                var orderControl = Session.getModelController(OrderControl.class);
096
097                if(!allowReferenceDuplicates && orderControl.countOrdersByBillToAndReference(vendor.getParty(), reference) != 0) {
098                    handleExecutionError(PurchaseOrderDuplicateReferenceException.class, eea, ExecutionErrors.PurchaseOrderDuplicateReference.name());
099                }
100
101                if(referenceValidationPattern != null && !reference.matches(referenceValidationPattern)) {
102                    handleExecutionError(InvalidPurchaseOrderReferenceException.class, eea, ExecutionErrors.InvalidPurchaseOrderReference.name());
103                }
104            }
105        }
106    }
107
108    public CancellationPolicy getCancellationPolicy(final ExecutionErrorAccumulator eea, final VendorType vendorType, final Vendor billToVendor) {
109        return CancellationPolicyLogic.getInstance().getDefaultCancellationPolicyByKind(eea, CancellationKinds.VENDOR_CANCELLATION.name(),
110                new CancellationPolicy[]{
111                    billToVendor == null ? null : billToVendor.getCancellationPolicy(),
112                    vendorType.getLastDetail().getDefaultCancellationPolicy()
113                });
114    }
115
116    public ReturnPolicy getReturnPolicy(final ExecutionErrorAccumulator eea, final VendorType vendorType, final Vendor billToVendor) {
117        return ReturnPolicyLogic.getInstance().getDefaultReturnPolicyByKind(eea, ReturnKinds.VENDOR_RETURN.name(),
118                new ReturnPolicy[]{
119                    billToVendor == null ? null : billToVendor.getReturnPolicy(),
120                    vendorType.getLastDetail().getDefaultReturnPolicy()
121                });
122    }
123
124    /**
125     * Create a new Purchase Order using appropriate defaults where possible for Optional parameters.
126     *
127     * @param session Required.
128     * @param eea Required.
129     * @param userVisit Required.
130     * @param vendorParty Required.
131     * @param holdUntilComplete Optional.
132     * @param allowBackorders Optional.
133     * @param allowSubstitutions Optional.
134     * @param allowCombiningShipments Optional.
135     * @param reference Optional.
136     * @param term Optional.
137     * @param workflowEntranceName Optional.
138     * @param createdByParty Required.
139     * @return The newly created Order, or null if there was an error.
140     */
141    public Order createPurchaseOrder(final Session session, final ExecutionErrorAccumulator eea, final UserVisit userVisit,
142            final Party vendorParty, Boolean holdUntilComplete, Boolean allowBackorders, Boolean allowSubstitutions,
143            Boolean allowCombiningShipments, final String reference, Term term, FreeOnBoard freeOnBoard,
144            final String workflowEntranceName, final Party createdByParty) {
145        var orderType = getOrderTypeByName(eea, OrderTypes.PURCHASE_ORDER.name());
146        var billToOrderRoleType = getOrderRoleTypeByName(eea, OrderRoleTypes.BILL_TO.name());
147        var placingOrderRoleType = getOrderRoleTypeByName(eea, OrderRoleTypes.PLACING.name());
148        Order order = null;
149
150        if(eea == null || !eea.hasExecutionErrors()) {
151            var orderControl = Session.getModelController(OrderControl.class);
152            var partyFreeOnBoardControl = Session.getModelController(PartyFreeOnBoardControl.class);
153            var termControl = Session.getModelController(TermControl.class);
154            var userControl = Session.getModelController(UserControl.class);
155            var vendorControl = Session.getModelController(VendorControl.class);
156            var currency = userControl.getPreferredCurrencyFromParty(vendorParty);
157            var vendor = vendorControl.getVendor(vendorParty);
158            var vendorType = vendor.getVendorType();
159
160            holdUntilComplete = holdUntilComplete == null ? vendor.getHoldUntilComplete() : holdUntilComplete;
161            allowBackorders = allowBackorders == null ? vendor.getAllowBackorders() : allowBackorders;
162            allowSubstitutions = allowSubstitutions == null ? vendor.getAllowSubstitutions() : allowSubstitutions;
163            allowCombiningShipments = allowCombiningShipments == null ? vendor.getAllowCombiningShipments() : allowCombiningShipments;
164
165            term = term == null ? termControl.getPartyTerm(vendorParty).getTerm() : term;
166            freeOnBoard = freeOnBoard == null ? partyFreeOnBoardControl.getPartyFreeOnBoard(vendorParty).getFreeOnBoard() : freeOnBoard;
167
168            var cancellationPolicy = getCancellationPolicy(eea, vendorType, vendor);
169            var returnPolicy = getReturnPolicy(eea, vendorType, vendor);
170
171            validatePurchaseOrderReference(eea, reference, vendor);
172
173            var sequence = SequenceGeneratorLogic.getInstance().getDefaultSequence(eea, SequenceTypes.PURCHASE_ORDER.name());
174
175            if(eea == null || !eea.hasExecutionErrors()) {
176                var coreControl = Session.getModelController(CoreControl.class);
177                var orderRoleControl = Session.getModelController(OrderRoleControl.class);
178                var workflowControl = Session.getModelController(WorkflowControl.class);
179                var userSesson = userControl.getUserSessionByUserVisit(userVisit);;
180                var createdByPartyPK = createdByParty.getPrimaryKey();
181
182                order = createOrder(eea, orderType, sequence, null, currency, holdUntilComplete, allowBackorders,
183                        allowSubstitutions, allowCombiningShipments, term, freeOnBoard, reference, null,
184                        cancellationPolicy, returnPolicy, null, createdByPartyPK);
185
186                orderControl.createOrderUserVisit(order, userVisit);
187
188                var entityInstance = coreControl.getEntityInstanceByBasePK(order.getPrimaryKey());
189                workflowControl.addEntityToWorkflowUsingNames(null, PurchaseOrderStatusConstants.Workflow_PURCHASE_ORDER_STATUS,
190                        workflowEntranceName, entityInstance, null, null, createdByPartyPK);
191
192                orderRoleControl.createOrderRole(order, userSesson.getPartyRelationship().getFromParty(), billToOrderRoleType, createdByPartyPK);
193
194                orderRoleControl.createOrderRole(order, createdByParty, placingOrderRoleType, createdByPartyPK);
195            }
196        }
197
198        return order;
199    }
200
201    public Order createPurchaseOrder(final Session session, final ExecutionErrorAccumulator eea, final UserVisit userVisit,
202            final String vendorName, final String termName, final String strHoldUntilComplete, final String strAllowBackorders,
203            final String strAllowSubstitutions, final String strAllowCombiningShipments, final String reference, final String freeOnBoardName,
204            final String workflowEntranceName, final Party createdByParty) {
205        var vendor = VendorLogic.getInstance().getVendorByName(eea, vendorName, null, null);
206        var term = termName == null ? null : TermLogic.getInstance().getTermByName(eea, termName);
207        var freeOnBoard = freeOnBoardName == null ? null : FreeOnBoardLogic.getInstance().getFreeOnBoardByName(eea, freeOnBoardName);
208        Order order = null;
209
210        if(!eea.hasExecutionErrors()) {
211            var holdUntilComplete = strHoldUntilComplete == null ? null : Boolean.valueOf(strHoldUntilComplete);
212            var allowBackorders = strAllowBackorders == null ? null : Boolean.valueOf(strAllowBackorders);
213            var allowSubstitutions = strAllowSubstitutions == null ? null : Boolean.valueOf(strAllowSubstitutions);
214            var allowCombiningShipments = strAllowCombiningShipments == null ? null : Boolean.valueOf(strAllowCombiningShipments);
215
216            order = createPurchaseOrder(session, eea, userVisit, vendor.getParty(), holdUntilComplete, allowBackorders,
217                allowSubstitutions, allowCombiningShipments, reference, term, freeOnBoard, workflowEntranceName,
218                createdByParty);
219        }
220
221        return order;
222    }
223
224    public boolean isOrderInWorkflowSteps(final ExecutionErrorAccumulator eea, final Order order, final String... workflowStepNames) {
225        return isOrderInWorkflowSteps(eea, getEntityInstanceByBaseEntity(order), workflowStepNames);
226    }
227
228    public boolean isOrderInWorkflowSteps(final ExecutionErrorAccumulator eea, final EntityInstance entityInstance, final String... workflowStepNames) {
229        return !WorkflowStepLogic.getInstance().isEntityInWorkflowSteps(eea, PurchaseOrderStatusConstants.Workflow_PURCHASE_ORDER_STATUS, entityInstance,
230                workflowStepNames).isEmpty();
231    }
232
233    public Order getOrderByName(final ExecutionErrorAccumulator eea, final String orderName) {
234        return getOrderByName(eea, OrderTypes.PURCHASE_ORDER.name(), orderName);
235    }
236
237    public Order getOrderByNameForUpdate(final ExecutionErrorAccumulator eea, final String orderName) {
238        return getOrderByNameForUpdate(eea, OrderTypes.PURCHASE_ORDER.name(), orderName);
239    }
240
241    public OrderPriority getOrderPriorityByName(final ExecutionErrorAccumulator eea, final String orderPriorityName) {
242        return getOrderPriorityByName(eea, OrderTypes.PURCHASE_ORDER.name(), orderPriorityName);
243    }
244
245    public OrderPriority getOrderPriorityByNameForUpdate(final ExecutionErrorAccumulator eea, final String orderPriorityName) {
246        return getOrderPriorityByNameForUpdate(eea, OrderTypes.PURCHASE_ORDER.name(), orderPriorityName);
247    }
248
249    public PurchaseOrderStatusChoicesBean getPurchaseOrderStatusChoices(final String defaultOrderStatusChoice, final Language language, final boolean allowNullChoice,
250            final Order order, final PartyPK partyPK) {
251        var workflowControl = Session.getModelController(WorkflowControl.class);
252        var purchaseOrderStatusChoicesBean = new PurchaseOrderStatusChoicesBean();
253
254        if(order == null) {
255            workflowControl.getWorkflowEntranceChoices(purchaseOrderStatusChoicesBean, defaultOrderStatusChoice, language, allowNullChoice,
256                    workflowControl.getWorkflowByName(PurchaseOrderStatusConstants.Workflow_PURCHASE_ORDER_STATUS), partyPK);
257        } else {
258            var coreControl = Session.getModelController(CoreControl.class);
259            var entityInstance = coreControl.getEntityInstanceByBasePK(order.getPrimaryKey());
260            var workflowEntityStatus = workflowControl.getWorkflowEntityStatusByEntityInstanceUsingNames(PurchaseOrderStatusConstants.Workflow_PURCHASE_ORDER_STATUS, entityInstance);
261
262            workflowControl.getWorkflowDestinationChoices(purchaseOrderStatusChoicesBean, defaultOrderStatusChoice, language, allowNullChoice, workflowEntityStatus.getWorkflowStep(), partyPK);
263        }
264
265        return purchaseOrderStatusChoicesBean;
266    }
267
268    public void setPurchaseOrderStatus(final Session session, final ExecutionErrorAccumulator eea, final Order order, final String orderStatusChoice, final PartyPK modifiedBy) {
269        var coreControl = Session.getModelController(CoreControl.class);
270        var workflowControl = Session.getModelController(WorkflowControl.class);
271        var workflow = WorkflowLogic.getInstance().getWorkflowByName(eea, PurchaseOrderStatusConstants.Workflow_PURCHASE_ORDER_STATUS);
272        var entityInstance = coreControl.getEntityInstanceByBasePK(order.getPrimaryKey());
273        var workflowEntityStatus = workflowControl.getWorkflowEntityStatusByEntityInstanceForUpdate(workflow, entityInstance);
274        var workflowDestination = orderStatusChoice == null? null: workflowControl.getWorkflowDestinationByName(workflowEntityStatus.getWorkflowStep(), orderStatusChoice);
275
276        if(workflowDestination != null || orderStatusChoice == null) {
277            var workflowDestinationLogic = WorkflowDestinationLogic.getInstance();
278            var currentWorkflowStepName = workflowEntityStatus.getWorkflowStep().getLastDetail().getWorkflowStepName();
279            var map = workflowDestinationLogic.getWorkflowDestinationsAsMap(workflowDestination);
280            var handled = false;
281            Long triggerTime = null;
282
283            if(currentWorkflowStepName.equals(PurchaseOrderStatusConstants.WorkflowStep_ENTRY)) {
284                if(workflowDestinationLogic.workflowDestinationMapContainsStep(map, PurchaseOrderStatusConstants.Workflow_PURCHASE_ORDER_STATUS, PurchaseOrderStatusConstants.WorkflowStep_ENTRY_COMPLETE)) {
285                    // TODO: What happens moving from ENTRY to ENTRY_COMPLETE?
286                    handled = true;
287                }
288
289                if(workflowDestinationLogic.workflowDestinationMapContainsStep(map, PurchaseOrderStatusConstants.Workflow_PURCHASE_ORDER_STATUS, PurchaseOrderStatusConstants.WorkflowStep_CANCELED)) {
290                    if(!handled) {
291                        // TODO: What happens moving from ENTRY to CANCELED?
292                        // TODO: Purchase Order Lines need to be canceled as well.
293                        handled = true;
294                    } else {
295                        handleExecutionError(DuplicateHandlingInPurchaseOrderStatusTransitionException.class, eea, ExecutionErrors.DuplicateHandlingInPurchaseOrderStatusTransition.name(), orderStatusChoice);
296                    }
297                }
298            }
299
300            if(eea == null || !eea.hasExecutionErrors()) {
301                if(handled) {
302                    workflowControl.transitionEntityInWorkflow(eea, workflowEntityStatus, workflowDestination, triggerTime, modifiedBy);
303                } else {
304                    handleExecutionError(UnhandledPurchaseOrderStatusTransitionException.class, eea, ExecutionErrors.UnhandledPurchaseOrderStatusTransition.name(), orderStatusChoice);
305                }
306            }
307        } else {
308            handleExecutionError(UnknownPurchaseOrderStatusChoiceException.class, eea, ExecutionErrors.UnknownPurchaseOrderStatusChoice.name(), orderStatusChoice);
309        }
310    }
311
312    /** Check to see if an Order is available for modification, and if it isn't, send back an error.
313     *
314     * @param session Required.
315     * @param eea Required.
316     * @param order Required.
317     * @param modifiedBy Required.
318     */
319    public void checkOrderAvailableForModification(final Session session, final ExecutionErrorAccumulator eea, final Order order, final PartyPK modifiedBy) {
320        var workflowControl = Session.getModelController(WorkflowControl.class);
321        var workflowEntityStatus = workflowControl.getWorkflowEntityStatusByEntityInstanceForUpdateUsingNames(PurchaseOrderStatusConstants.Workflow_PURCHASE_ORDER_STATUS, getEntityInstanceByBaseEntity(order));
322        var workflowStepName = workflowEntityStatus.getWorkflowStep().getLastDetail().getWorkflowStepName();
323
324        if(!workflowStepName.equals(PurchaseOrderStatusConstants.WorkflowStep_ENTRY)) {
325            handleExecutionError(InvalidPurchaseOrderStatusException.class, eea, ExecutionErrors.InvalidPurchaseOrderStatus.name(), order.getLastDetail().getOrderName(), workflowStepName);
326        }
327    }
328
329    /** Find the BILL_TO Party for a given Order.
330     * 
331     * @param order Required.
332     * @return The Party used for the BILL_TO OrderRoleType. May be null.
333     */
334    public Party getOrderBillToParty(final Order order) {
335        var orderRoleControl = Session.getModelController(OrderRoleControl.class);
336        var billToOrderRole = orderRoleControl.getOrderRoleByOrderAndOrderRoleTypeUsingNames(order, OrderRoleTypes.BILL_TO.name());
337        Party party = null;
338        
339        if(billToOrderRole != null) {
340            party = billToOrderRole.getParty();
341        }
342        
343        return party;
344    }
345    
346    /** Find the VendorType for a given Party.
347     * 
348     * @param party Optional.
349     * @return The VendorType for the Party. May be null.
350     */
351    public VendorType getVendorTypeFromParty(final Party party) {
352        var vendorControl = Session.getModelController(VendorControl.class);
353        var vendor = party == null ? null : vendorControl.getVendor(party);
354        VendorType vendorType = null;
355        
356        if(vendor != null) {
357            vendorType = vendor.getVendorType();
358        }
359        
360        return vendorType;
361    }
362    
363    /** Find the VendorType for the BILL_TO Party for a given Order.
364     * 
365     * @param order Required.
366     * @return The VendorType for the BILL_TO Party. May be null.
367     */
368    public VendorType getOrderBillToVendorType(final Order order) {
369        return getVendorTypeFromParty(getOrderBillToParty(order));
370    }
371    
372    /** Attempt to find a SHIP_TO Party for the Order. If none is found, and billToFallback is set to true, then
373     * attempt to find the BILL_TO Party. If a BILL_TO Party is found, copy it to the SHIP_TO.
374     * 
375     * @param order Required.
376     * @param billToFallback Required.
377     * @param createdBy Required if billToFallback is true.
378     * @return The Party that is to be used for the SHIP_TO OrderRoleType. May be null.
379     */
380    public Party getOrderShipToParty(final Order order, final boolean billToFallback, final BasePK createdBy) {
381        var orderRoleControl = Session.getModelController(OrderRoleControl.class);
382        var shipToOrderRole = orderRoleControl.getOrderRoleByOrderAndOrderRoleTypeUsingNames(order, OrderRoleTypes.SHIP_TO.name());
383        
384        if(shipToOrderRole == null && billToFallback) {
385            shipToOrderRole = orderRoleControl.getOrderRoleByOrderAndOrderRoleTypeUsingNames(order, OrderRoleTypes.BILL_TO.name());
386            
387            if(shipToOrderRole != null) {
388                orderRoleControl.createOrderRoleUsingNames(order, shipToOrderRole.getParty(), OrderRoleTypes.SHIP_TO.name(), createdBy);
389            }
390        }
391        
392        return shipToOrderRole == null ? null : shipToOrderRole.getParty();
393    }
394    
395}