001// --------------------------------------------------------------------------------
002// Copyright 2002-2025 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.sales.server.logic;
018
019import com.echothree.model.control.accounting.common.exception.InvalidCurrencyException;
020import com.echothree.model.control.accounting.server.logic.CurrencyLogic;
021import com.echothree.model.control.associate.server.logic.AssociateReferralLogic;
022import com.echothree.model.control.batch.server.logic.BatchLogic;
023import com.echothree.model.control.cancellationpolicy.common.CancellationKinds;
024import com.echothree.model.control.cancellationpolicy.server.logic.CancellationPolicyLogic;
025import com.echothree.model.control.core.server.control.EntityInstanceControl;
026import com.echothree.model.control.customer.common.exception.MissingDefaultCustomerTypeException;
027import com.echothree.model.control.customer.server.control.CustomerControl;
028import com.echothree.model.control.offer.common.exception.MissingDefaultSourceException;
029import com.echothree.model.control.offer.server.control.OfferControl;
030import com.echothree.model.control.offer.server.control.SourceControl;
031import com.echothree.model.control.offer.server.logic.SourceLogic;
032import com.echothree.model.control.order.common.OrderRoleTypes;
033import com.echothree.model.control.order.common.OrderTypes;
034import com.echothree.model.control.order.common.exception.MissingDefaultOrderPriorityException;
035import com.echothree.model.control.order.common.exception.MissingRequiredBillToPartyException;
036import com.echothree.model.control.order.server.control.OrderBatchControl;
037import com.echothree.model.control.order.server.control.OrderControl;
038import com.echothree.model.control.order.server.control.OrderPriorityControl;
039import com.echothree.model.control.order.server.control.OrderRoleControl;
040import com.echothree.model.control.order.server.logic.BaseOrderLogic;
041import com.echothree.model.control.party.common.PartyTypes;
042import com.echothree.model.control.party.server.logic.PartyLogic;
043import com.echothree.model.control.returnpolicy.common.ReturnKinds;
044import com.echothree.model.control.returnpolicy.server.logic.ReturnPolicyLogic;
045import com.echothree.model.control.sales.common.choice.SalesOrderStatusChoicesBean;
046import com.echothree.model.control.sales.common.exception.InvalidSalesOrderBatchStatusException;
047import com.echothree.model.control.sales.common.exception.InvalidSalesOrderReferenceException;
048import com.echothree.model.control.sales.common.exception.InvalidSalesOrderStatusException;
049import com.echothree.model.control.sales.common.exception.SalesOrderDuplicateReferenceException;
050import com.echothree.model.control.sales.common.exception.SalesOrderReferenceRequiredException;
051import com.echothree.model.control.sales.common.exception.UnknownSalesOrderStatusChoiceException;
052import com.echothree.model.control.sales.common.workflow.SalesOrderStatusConstants;
053import com.echothree.model.control.sales.server.control.SalesOrderControl;
054import com.echothree.model.control.shipment.server.control.PartyFreeOnBoardControl;
055import com.echothree.model.control.shipment.server.logic.FreeOnBoardLogic;
056import com.echothree.model.control.term.server.control.TermControl;
057import com.echothree.model.control.term.server.logic.TermLogic;
058import com.echothree.model.control.user.server.control.UserControl;
059import com.echothree.model.control.workflow.server.control.WorkflowControl;
060import com.echothree.model.control.workflow.server.logic.WorkflowDestinationLogic;
061import com.echothree.model.control.workflow.server.logic.WorkflowLogic;
062import com.echothree.model.control.workflow.server.logic.WorkflowStepLogic;
063import com.echothree.model.data.accounting.server.entity.Currency;
064import com.echothree.model.data.batch.server.entity.Batch;
065import com.echothree.model.data.cancellationpolicy.server.entity.CancellationPolicy;
066import com.echothree.model.data.core.server.entity.EntityInstance;
067import com.echothree.model.data.customer.server.entity.Customer;
068import com.echothree.model.data.customer.server.entity.CustomerType;
069import com.echothree.model.data.offer.server.entity.Offer;
070import com.echothree.model.data.offer.server.entity.Source;
071import com.echothree.model.data.order.server.entity.Order;
072import com.echothree.model.data.order.server.entity.OrderPriority;
073import com.echothree.model.data.party.common.pk.PartyPK;
074import com.echothree.model.data.party.server.entity.Language;
075import com.echothree.model.data.party.server.entity.Party;
076import com.echothree.model.data.returnpolicy.server.entity.ReturnPolicy;
077import com.echothree.model.data.shipment.server.entity.FreeOnBoard;
078import com.echothree.model.data.term.server.entity.Term;
079import com.echothree.model.data.user.server.entity.UserVisit;
080import com.echothree.util.common.message.ExecutionErrors;
081import com.echothree.util.common.persistence.BasePK;
082import com.echothree.util.server.message.ExecutionErrorAccumulator;
083import com.echothree.util.server.persistence.Session;
084import javax.enterprise.context.ApplicationScoped;
085import javax.enterprise.inject.spi.CDI;
086
087@ApplicationScoped
088public class SalesOrderLogic
089        extends BaseOrderLogic {
090
091    protected SalesOrderLogic() {
092        super();
093    }
094
095    public static SalesOrderLogic getInstance() {
096        return CDI.current().select(SalesOrderLogic.class).get();
097    }
098    
099    final static long AllocatedInventoryTimeout = 5 * 60 * 1000; // 5 Minutes
100
101    public CustomerType getCustomerType(final ExecutionErrorAccumulator eea, final Offer offer, final Customer customer) {
102        var customerControl = Session.getModelController(CustomerControl.class);
103        CustomerType customerType = null;
104
105        // 1) Try to get it from the customer, if one was supplied.
106        if(customer != null) {
107            customerType = customer.getCustomerType();
108        }
109
110        // 2) Try to get it from the offer, if one was supplied.
111        if(customerType == null && offer != null) {
112            var offerControl = Session.getModelController(OfferControl.class);
113            var offerCustomerType = offerControl.getDefaultOfferCustomerType(offer);
114
115            if(offerCustomerType != null) {
116                customerType = offerCustomerType.getCustomerType();
117            }
118        }
119
120        // 3) Try to get the default CustomerType, error if it isn't available.
121        if(customerType == null) {
122            customerType = customerControl.getDefaultCustomerType();
123
124            if(customerType == null) {
125                handleExecutionError(MissingDefaultCustomerTypeException.class, eea, ExecutionErrors.MissingDefaultCustomerType.name());
126            }
127        }
128
129        return customerType;
130    }
131
132    public void validateSalesOrderReference(final ExecutionErrorAccumulator eea, final String reference, final CustomerType customerType, final Customer billToCustomer) {
133        Boolean requireReference = null;
134        Boolean allowReferenceDuplicates = null;
135        String referenceValidationPattern = null;
136
137        if(billToCustomer != null) {
138            requireReference = billToCustomer.getRequireReference();
139            allowReferenceDuplicates = billToCustomer.getAllowReferenceDuplicates();
140            referenceValidationPattern = billToCustomer.getReferenceValidationPattern();
141        } else if(customerType != null) {
142            var customerTypeDetail = customerType.getLastDetail();
143
144            requireReference = customerTypeDetail.getDefaultRequireReference();
145            allowReferenceDuplicates = customerTypeDetail.getDefaultAllowReferenceDuplicates();
146            referenceValidationPattern = customerTypeDetail.getDefaultReferenceValidationPattern();
147        }
148
149        if(requireReference != null) {
150            if(requireReference && reference == null) {
151                handleExecutionError(SalesOrderReferenceRequiredException.class, eea, ExecutionErrors.SalesOrderReferenceRequired.name());
152            } else if(reference != null) {
153                var orderControl = Session.getModelController(OrderControl.class);
154
155                if(!allowReferenceDuplicates) {
156                    if(billToCustomer == null) {
157                        handleExecutionError(MissingRequiredBillToPartyException.class, eea, ExecutionErrors.MissingRequiredBillToParty.name());
158                    } else if(orderControl.countOrdersByBillToAndReference(billToCustomer.getParty(), reference) != 0) {
159                        handleExecutionError(SalesOrderDuplicateReferenceException.class, eea, ExecutionErrors.SalesOrderDuplicateReference.name());
160                    }
161                }
162
163                if(referenceValidationPattern != null && !reference.matches(referenceValidationPattern)) {
164                    handleExecutionError(InvalidSalesOrderReferenceException.class, eea, ExecutionErrors.InvalidSalesOrderReference.name());
165                }
166            }
167        }
168    }
169
170    public CancellationPolicy getCancellationPolicy(final ExecutionErrorAccumulator eea, final CustomerType customerType, final Customer billToCustomer) {
171        return CancellationPolicyLogic.getInstance().getDefaultCancellationPolicyByKind(eea, CancellationKinds.CUSTOMER_CANCELLATION.name(),
172                new CancellationPolicy[]{
173                    billToCustomer == null ? null : billToCustomer.getCancellationPolicy(),
174                    customerType.getLastDetail().getDefaultCancellationPolicy()
175                });
176    }
177
178    public ReturnPolicy getReturnPolicy(final ExecutionErrorAccumulator eea, final CustomerType customerType, final Customer billToCustomer) {
179        return ReturnPolicyLogic.getInstance().getDefaultReturnPolicyByKind(eea, ReturnKinds.CUSTOMER_RETURN.name(),
180                new ReturnPolicy[]{
181                    billToCustomer == null ? null : billToCustomer.getReturnPolicy(),
182                    customerType.getLastDetail().getDefaultReturnPolicy()
183                });
184    }
185
186    /**
187     * Create a new Sales Order given a set of parameters. Default values will be determined as best as possible when
188     * a parameter is marked as Optional. If a previous preference has not been set then it may not be possible to
189     * to determine appropriate defaults.
190     *
191     * @param session Required.
192     * @param eea Required.
193     * @param userVisit Required.
194     * @param source Optional.
195     * @param billToParty Optional.
196     * @param orderPriority Optional.
197     * @param currency Optional.
198     * @param holdUntilComplete Optional.
199     * @param allowBackorders Optional.
200     * @param allowSubstitutions Optional.
201     * @param allowCombiningShipments Optional.
202     * @param reference Optional.
203     * @param term Optional.
204     * @param taxable Optional.
205     * @param workflowEntranceName Optional.
206     * @param createdByParty Required.
207     * @return The newly created Order, or null if there was an error.
208     */
209    public Order createSalesOrder(final Session session, final ExecutionErrorAccumulator eea, final UserVisit userVisit, final Batch batch, Source source,
210            final Party billToParty, OrderPriority orderPriority, Currency currency, Boolean holdUntilComplete, Boolean allowBackorders, Boolean allowSubstitutions,
211            Boolean allowCombiningShipments, final String reference, Term term, FreeOnBoard freeOnBoard, Boolean taxable, final String workflowEntranceName, final Party createdByParty) {
212        var orderType = getOrderTypeByName(eea, OrderTypes.SALES_ORDER.name());
213        var billToOrderRoleType = getOrderRoleTypeByName(eea, OrderRoleTypes.BILL_TO.name());
214        var placingOrderRoleType = getOrderRoleTypeByName(eea, OrderRoleTypes.PLACING.name());
215        Order order = null;
216
217        if(batch != null) {
218            if(SalesOrderBatchLogic.getInstance().checkBatchAvailableForEntry(eea, batch)) {
219                var orderBatchControl = Session.getModelController(OrderBatchControl.class);
220                var orderBatchCurrency = orderBatchControl.getOrderBatch(batch).getCurrency();
221
222                if(currency == null) {
223                    currency = orderBatchCurrency;
224                } else {
225                    if(!currency.equals(orderBatchCurrency)) {
226                        handleExecutionError(InvalidCurrencyException.class, eea, ExecutionErrors.InvalidCurrency.name(), currency.getCurrencyIsoName(),
227                                orderBatchCurrency.getCurrencyIsoName());
228                    }
229                }
230            } else {
231                handleExecutionError(InvalidSalesOrderBatchStatusException.class, eea, ExecutionErrors.InvalidSalesOrderBatchStatus.name(),
232                        batch.getLastDetail().getBatchName());
233            }
234        }
235
236        if(eea == null || !eea.hasExecutionErrors()) {
237            if(source == null) {
238                var sourceControl = Session.getModelController(SourceControl.class);
239
240                source = sourceControl.getDefaultSource();
241
242                if(source == null) {
243                    handleExecutionError(MissingDefaultSourceException.class, eea, ExecutionErrors.MissingDefaultSource.name());
244                }
245            }
246
247            if(orderPriority == null) {
248                var orderPriorityControl = Session.getModelController(OrderPriorityControl.class);
249
250                orderPriority = orderPriorityControl.getDefaultOrderPriority(orderType);
251
252                if(orderPriority == null) {
253                    handleExecutionError(MissingDefaultOrderPriorityException.class, eea, ExecutionErrors.MissingDefaultOrderPriority.name(), OrderTypes.SALES_ORDER.name());
254                }
255            }
256
257            if(currency == null) {
258                var userControl = Session.getModelController(UserControl.class);
259
260                if(billToParty != null) {
261                    currency = userControl.getPreferredCurrencyFromParty(billToParty);
262                }
263
264                if(currency == null && userVisit != null) {
265                    currency = userControl.getPreferredCurrencyFromUserVisit(userVisit);
266                }
267
268                if(currency == null) {
269                    currency = CurrencyLogic.getInstance().getDefaultCurrency(eea);
270                }
271            }
272
273            if(billToParty != null) {
274                PartyLogic.getInstance().checkPartyType(eea, billToParty, PartyTypes.CUSTOMER.name());
275            }
276
277            if(eea == null || !eea.hasExecutionErrors()) {
278                var customerControl = Session.getModelController(CustomerControl.class);
279                var billToCustomer = billToParty == null ? null : customerControl.getCustomer(billToParty);
280                var offerUse = source.getLastDetail().getOfferUse();
281                var customerType = getCustomerType(eea, offerUse.getLastDetail().getOffer(), billToCustomer);
282
283                if(eea == null || !eea.hasExecutionErrors()) {
284                    var cancellationPolicy = getCancellationPolicy(eea, customerType, billToCustomer);
285                    var returnPolicy = getReturnPolicy(eea, customerType, billToCustomer);
286                    
287                    if(billToCustomer != null) {
288                        validateSalesOrderReference(eea, reference, customerType, billToCustomer);
289                    }
290
291                    if(eea == null || !eea.hasExecutionErrors()) {
292                        var customerTypeDetail = customerType.getLastDetail();
293                        var sequence = offerUse.getLastDetail().getSalesOrderSequence();
294                        var createdBy = createdByParty == null ? null : createdByParty.getPrimaryKey();
295
296                        // If term or taxable were not set, then try to come up with sensible defaults, first from a PartyTerm if it
297                        // was available, and then falling back on the CustomerType.
298                        if(term == null || taxable == null) {
299                            var termControl = Session.getModelController(TermControl.class);
300                            var partyTerm = billToParty == null ? null : termControl.getPartyTerm(billToParty);
301
302                            if(partyTerm == null) {
303                                if(term == null) {
304                                    term = customerTypeDetail.getDefaultTerm();
305                                }
306
307                                if(taxable == null) {
308                                    taxable = customerTypeDetail.getDefaultTaxable();
309                                }
310                            } else {
311                                if(term == null) {
312                                    term = partyTerm.getTerm();
313                                }
314
315                                if(taxable == null) {
316                                    taxable = partyTerm.getTaxable();
317                                }
318                            }
319
320                            // If no better answer was found, use the default Term.
321                            if(term == null) {
322                                termControl.getDefaultTerm();
323                            }
324
325                            // If no better answer was found, the order is taxable.
326                            if(taxable == null) {
327                                taxable = true;
328                            }
329                        }
330
331                        // If FreeOnBoard wasn't specified, using the bill to Party, fir look for a preference for the Party,
332                        // and then check the CustomerType.
333                        if(freeOnBoard == null) {
334                            var partFreeOnBoardControl = Session.getModelController(PartyFreeOnBoardControl.class);
335                            var partyFreeOnBoard = billToParty == null ? null : partFreeOnBoardControl.getPartyFreeOnBoard(billToParty);
336
337                            if(partyFreeOnBoard == null) {
338                                if(freeOnBoard == null) {
339                                    freeOnBoard = customerTypeDetail.getDefaultFreeOnBoard();
340                                }
341                            } else {
342                                if(freeOnBoard == null) {
343                                    freeOnBoard = partyFreeOnBoard.getFreeOnBoard();
344                                }
345                            }
346
347                            // If no better answer was found, use the default FreeOnBoard.
348                            if(freeOnBoard == null) {
349                                freeOnBoard = FreeOnBoardLogic.getInstance().getDefaultFreeOnBoard(eea);
350                            }
351                        }
352
353                        // If any of these flags were not set, try to get them from either the Customer, or the CustomerType.
354                        if(holdUntilComplete == null || allowBackorders == null || allowSubstitutions == null || allowCombiningShipments == null) {
355                            if(billToCustomer == null) {
356                                    if(holdUntilComplete == null) {
357                                        holdUntilComplete = customerTypeDetail.getDefaultHoldUntilComplete();
358                                    }
359
360                                    if(allowBackorders == null) {
361                                        allowBackorders = customerTypeDetail.getDefaultAllowBackorders();
362                                    }
363
364                                    if(allowSubstitutions == null) {
365                                        allowSubstitutions = customerTypeDetail.getDefaultAllowSubstitutions();
366                                    }
367
368                                    if(allowCombiningShipments == null) {
369                                        allowCombiningShipments = customerTypeDetail.getDefaultAllowCombiningShipments();
370                                    }
371                            } else {
372                                if(holdUntilComplete == null) {
373                                    holdUntilComplete = billToCustomer.getHoldUntilComplete();
374                                }
375
376                                if(allowBackorders == null) {
377                                    allowBackorders = billToCustomer.getAllowBackorders();
378                                }
379
380                                if(allowSubstitutions == null) {
381                                    allowSubstitutions = billToCustomer.getAllowSubstitutions();
382                                }
383
384                                if(allowCombiningShipments == null) {
385                                    allowCombiningShipments = billToCustomer.getAllowCombiningShipments();
386                                }
387                            }
388                        }
389
390                        order = createOrder(eea, orderType, sequence, orderPriority, currency, holdUntilComplete, allowBackorders,
391                                allowSubstitutions, allowCombiningShipments, term, freeOnBoard, reference, null,
392                                cancellationPolicy, returnPolicy, taxable, createdBy);
393
394                        if(eea == null || !eea.hasExecutionErrors()) {
395                            var entityInstanceControl = Session.getModelController(EntityInstanceControl.class);
396                            var orderControl = Session.getModelController(OrderControl.class);
397                            var orderRoleControl = Session.getModelController(OrderRoleControl.class);
398                            var salesOrderControl = Session.getModelController(SalesOrderControl.class);
399                            var workflowControl = Session.getModelController(WorkflowControl.class);
400                            var associateReferral = AssociateReferralLogic.getInstance().getAssociateReferral(session, userVisit);
401                            var entityInstance = entityInstanceControl.getEntityInstanceByBasePK(order.getPrimaryKey());
402
403                            salesOrderControl.createSalesOrder(order, offerUse, associateReferral, createdBy);
404
405                            orderControl.createOrderUserVisit(order, userVisit);
406                            
407                            workflowControl.addEntityToWorkflowUsingNames(null, SalesOrderStatusConstants.Workflow_SALES_ORDER_STATUS, workflowEntranceName,
408                                    entityInstance, null, session.START_TIME + AllocatedInventoryTimeout, createdBy);
409                            
410                            if(billToParty != null) {
411                                orderRoleControl.createOrderRole(order, billToParty, billToOrderRoleType, createdBy);
412                            }
413
414                            if(createdByParty != null) {
415                                orderRoleControl.createOrderRole(order, createdByParty, placingOrderRoleType, createdBy);
416                            }
417                            
418                            if(batch != null) {
419                                // eea is passed in as null to createBatchEntity(...) so that an Exception will be thrown is something
420                                // goes wrong. No real way to back out at this point if it does, except by Exception.
421                                BatchLogic.getInstance().createBatchEntity(null, entityInstance, batch, createdBy);
422                            }
423                        }
424                    }
425                }
426            }
427        }
428
429        return order;
430    }
431
432    public Order createSalesOrder(final Session session, final ExecutionErrorAccumulator eea, final UserVisit userVisit,
433            final String batchName, final String sourceName, final String billToPartyName, final String orderPriorityName,
434            final String currencyIsoName, final String termName, final String strHoldUntilComplete, final String strAllowBackorders,
435            final String strAllowSubstitutions, final String strAllowCombiningShipments, final String reference, final String freeOnBoardName,
436            final String strTaxable, final String workflowEntranceName, final Party createdByParty) {
437        var batch = batchName == null ? null : SalesOrderBatchLogic.getInstance().getBatchByName(eea, batchName);
438        var source = sourceName == null ? null : SourceLogic.getInstance().getSourceByName(eea, sourceName);
439        var billToParty = billToPartyName == null ? null : PartyLogic.getInstance().getPartyByName(eea, billToPartyName, PartyTypes.CUSTOMER.name());
440        var orderPriority = orderPriorityName == null ? null : SalesOrderLogic.getInstance().getOrderPriorityByName(eea, orderPriorityName);
441        var currency = currencyIsoName == null ? null : CurrencyLogic.getInstance().getCurrencyByName(eea, currencyIsoName);
442        var term = termName == null ? null : TermLogic.getInstance().getTermByName(eea, termName);
443        var freeOnBoard = freeOnBoardName == null ? null : FreeOnBoardLogic.getInstance().getFreeOnBoardByName(eea, freeOnBoardName);
444        Order order = null;
445
446        if(!eea.hasExecutionErrors()) {
447            var holdUntilComplete = strHoldUntilComplete == null ? null : Boolean.valueOf(strHoldUntilComplete);
448            var allowBackorders = strAllowBackorders == null ? null : Boolean.valueOf(strAllowBackorders);
449            var allowSubstitutions = strAllowSubstitutions == null ? null : Boolean.valueOf(strAllowSubstitutions);
450            var allowCombiningShipments = strAllowCombiningShipments == null ? null : Boolean.valueOf(strAllowCombiningShipments);
451            var taxable = strTaxable == null ? null : Boolean.valueOf(strTaxable);
452
453            order = createSalesOrder(session, eea, userVisit, batch, source, billToParty, orderPriority, currency,
454                    holdUntilComplete, allowBackorders, allowSubstitutions, allowCombiningShipments, reference, term,
455                    freeOnBoard, taxable, workflowEntranceName, createdByParty);
456
457        }
458
459        return order;
460    }
461
462    public boolean isOrderInWorkflowSteps(final ExecutionErrorAccumulator eea, final Order order, final String... workflowStepNames) {
463        return isOrderInWorkflowSteps(eea, getEntityInstanceByBaseEntity(order), workflowStepNames);
464    }
465
466    public boolean isOrderInWorkflowSteps(final ExecutionErrorAccumulator eea, final EntityInstance entityInstance, final String... workflowStepNames) {
467        return !WorkflowStepLogic.getInstance().isEntityInWorkflowSteps(eea, SalesOrderStatusConstants.Workflow_SALES_ORDER_STATUS, entityInstance,
468                workflowStepNames).isEmpty();
469    }
470
471    public Order getOrderByName(final ExecutionErrorAccumulator eea, final String orderName) {
472        return getOrderByName(eea, OrderTypes.SALES_ORDER.name(), orderName);
473    }
474
475    public Order getOrderByNameForUpdate(final ExecutionErrorAccumulator eea, final String orderName) {
476        return getOrderByNameForUpdate(eea, OrderTypes.SALES_ORDER.name(), orderName);
477    }
478
479    public OrderPriority getOrderPriorityByName(final ExecutionErrorAccumulator eea, final String orderPriorityName) {
480        return SalesOrderPriorityLogic.getInstance().getOrderPriorityByName(eea, OrderTypes.SALES_ORDER.name(), orderPriorityName);
481    }
482
483    public OrderPriority getOrderPriorityByNameForUpdate(final ExecutionErrorAccumulator eea, final String orderPriorityName) {
484        return SalesOrderPriorityLogic.getInstance().getOrderPriorityByNameForUpdate(eea, OrderTypes.SALES_ORDER.name(), orderPriorityName);
485    }
486
487    public SalesOrderStatusChoicesBean getSalesOrderStatusChoices(final String defaultOrderStatusChoice, final Language language, final boolean allowNullChoice,
488            final Order order, final PartyPK partyPK) {
489        var workflowControl = Session.getModelController(WorkflowControl.class);
490        var salesOrderStatusChoicesBean = new SalesOrderStatusChoicesBean();
491
492        if(order == null) {
493            workflowControl.getWorkflowEntranceChoices(salesOrderStatusChoicesBean, defaultOrderStatusChoice, language, allowNullChoice,
494                    workflowControl.getWorkflowByName(SalesOrderStatusConstants.Workflow_SALES_ORDER_STATUS), partyPK);
495        } else {
496            var entityInstanceControl = Session.getModelController(EntityInstanceControl.class);
497            var entityInstance = entityInstanceControl.getEntityInstanceByBasePK(order.getPrimaryKey());
498            var workflowEntityStatus = workflowControl.getWorkflowEntityStatusByEntityInstanceUsingNames(SalesOrderStatusConstants.Workflow_SALES_ORDER_STATUS, entityInstance);
499
500            workflowControl.getWorkflowDestinationChoices(salesOrderStatusChoicesBean, defaultOrderStatusChoice, language, allowNullChoice, workflowEntityStatus.getWorkflowStep(), partyPK);
501        }
502
503        return salesOrderStatusChoicesBean;
504    }
505
506    public void setSalesOrderStatus(final Session session, final ExecutionErrorAccumulator eea, final Order order, final String orderStatusChoice, final PartyPK modifiedBy) {
507        var entityInstanceControl = Session.getModelController(EntityInstanceControl.class);
508        var workflowControl = Session.getModelController(WorkflowControl.class);
509        var workflow = WorkflowLogic.getInstance().getWorkflowByName(eea, SalesOrderStatusConstants.Workflow_SALES_ORDER_STATUS);
510        var entityInstance = entityInstanceControl.getEntityInstanceByBasePK(order.getPrimaryKey());
511        var workflowEntityStatus = workflowControl.getWorkflowEntityStatusByEntityInstanceForUpdate(workflow, entityInstance);
512        var workflowDestination = orderStatusChoice == null? null: workflowControl.getWorkflowDestinationByName(workflowEntityStatus.getWorkflowStep(), orderStatusChoice);
513
514        if(workflowDestination != null || orderStatusChoice == null) {
515            var workflowDestinationLogic = WorkflowDestinationLogic.getInstance();
516            var currentWorkflowStepName = workflowEntityStatus.getWorkflowStep().getLastDetail().getWorkflowStepName();
517            var map = workflowDestinationLogic.getWorkflowDestinationsAsMap(workflowDestination);
518            var handled = false;
519            Long triggerTime = null;
520            
521            if(currentWorkflowStepName.equals(SalesOrderStatusConstants.WorkflowStep_ENTRY_ALLOCATED)) {
522                if(workflowDestinationLogic.workflowDestinationMapContainsStep(map, SalesOrderStatusConstants.Workflow_SALES_ORDER_STATUS, SalesOrderStatusConstants.WorkflowStep_ENTRY_UNALLOCATED)) {
523                    // TODO: Unallocate inventory.
524                    handled = true;
525                } else if(workflowDestinationLogic.workflowDestinationMapContainsStep(map, SalesOrderStatusConstants.Workflow_SALES_ORDER_STATUS, SalesOrderStatusConstants.WorkflowStep_ENTRY_COMPLETE)) {
526                    // TODO: Verify the order is not part of a batch.
527                    // TODO: Verify all aspects of the order are valid.
528                    handled = true;
529                } else if(workflowDestinationLogic.workflowDestinationMapContainsStep(map, SalesOrderStatusConstants.Workflow_SALES_ORDER_STATUS, SalesOrderStatusConstants.WorkflowStep_BATCH_AUDIT)) {
530                    // TODO: Verify the order is part of a batch.
531                    // TODO: Verify all aspects of the order are valid.
532                    handled = true;
533                }
534            } else if(currentWorkflowStepName.equals(SalesOrderStatusConstants.WorkflowStep_ENTRY_UNALLOCATED)) {
535                if(workflowDestinationLogic.workflowDestinationMapContainsStep(map, SalesOrderStatusConstants.Workflow_SALES_ORDER_STATUS, SalesOrderStatusConstants.WorkflowStep_ENTRY_ALLOCATED)) {
536                    // TODO: Allocate inventory.
537                    
538                    triggerTime = session.START_TIME + AllocatedInventoryTimeout;
539                    handled = true;
540                }
541            } else if(currentWorkflowStepName.equals(SalesOrderStatusConstants.WorkflowStep_BATCH_AUDIT)) {
542                if(workflowDestinationLogic.workflowDestinationMapContainsStep(map, SalesOrderStatusConstants.Workflow_SALES_ORDER_STATUS, SalesOrderStatusConstants.WorkflowStep_ENTRY_COMPLETE)) {
543                    handled = true;
544                }
545            }
546            
547            if(eea == null || !eea.hasExecutionErrors()) {
548                if(handled) {
549                    workflowControl.transitionEntityInWorkflow(eea, workflowEntityStatus, workflowDestination, triggerTime, modifiedBy);
550                } else {
551                    // TODO: An error of some sort.
552                }
553            }
554        } else {
555            handleExecutionError(UnknownSalesOrderStatusChoiceException.class, eea, ExecutionErrors.UnknownSalesOrderStatusChoice.name(), orderStatusChoice);
556        }
557    }
558
559    /** Check to see if an Order is available for modification, and if it isn't, send back an error.
560     * 
561     * @param session Required.
562     * @param eea Required.
563     * @param order Required.
564     * @param modifiedBy Required.
565     */
566    public void checkOrderAvailableForModification(final Session session, final ExecutionErrorAccumulator eea, final Order order, final PartyPK modifiedBy) {
567        var workflowControl = Session.getModelController(WorkflowControl.class);
568        var workflowEntityStatus = workflowControl.getWorkflowEntityStatusByEntityInstanceForUpdateUsingNames(SalesOrderStatusConstants.Workflow_SALES_ORDER_STATUS, getEntityInstanceByBaseEntity(order));
569        var workflowStepName = workflowEntityStatus.getWorkflowStep().getLastDetail().getWorkflowStepName();
570        
571        if(workflowStepName.equals(SalesOrderStatusConstants.WorkflowEntrance_ENTRY_ALLOCATED)) {
572            var workflowTrigger = workflowControl.getWorkflowTriggerForUpdate(workflowEntityStatus);
573            
574            workflowTrigger.setTriggerTime(session.START_TIME + AllocatedInventoryTimeout);
575        } else if(workflowStepName.equals(SalesOrderStatusConstants.WorkflowEntrance_ENTRY_UNALLOCATED)) {
576            setSalesOrderStatus(session, eea, order, SalesOrderStatusConstants.WorkflowDestination_ENTRY_UNALLOCATED_TO_ALLOCATED, modifiedBy);
577        } else {
578            handleExecutionError(InvalidSalesOrderStatusException.class, eea, ExecutionErrors.InvalidSalesOrderStatus.name(), order.getLastDetail().getOrderName(), workflowStepName);
579        }
580    }
581
582    /** Find the BILL_TO Party for a given Order.
583     * 
584     * @param order Required.
585     * @return The Party used for the BILL_TO OrderRoleType. May be null.
586     */
587    public Party getOrderBillToParty(final Order order) {
588        var orderRoleControl = Session.getModelController(OrderRoleControl.class);
589        var billToOrderRole = orderRoleControl.getOrderRoleByOrderAndOrderRoleTypeUsingNames(order, OrderRoleTypes.BILL_TO.name());
590        Party party = null;
591        
592        if(billToOrderRole != null) {
593            party = billToOrderRole.getParty();
594        }
595        
596        return party;
597    }
598    
599    /** Find the BILL_TO Party for a given Order.
600     * 
601     * @param party Optional.
602     * @return The CustomerType for the Party. May be null.
603     */
604    public CustomerType getCustomerTypeFromParty(final Party party) {
605        var customerControl = Session.getModelController(CustomerControl.class);
606        var customer = party == null ? null : customerControl.getCustomer(party);
607        CustomerType customerType = null;
608        
609        if(customer != null) {
610            customerType = customer.getCustomerType();
611        }
612        
613        return customerType;
614    }
615    
616    /** Find the BILL_TO Party for a given Order.
617     * 
618     * @param order Required.
619     * @return The CustomerType for the BILL_TO Party. May be null.
620     */
621    public CustomerType getOrderBillToCustomerType(final Order order) {
622        return getCustomerTypeFromParty(getOrderBillToParty(order));
623    }
624    
625    /** Attempt to find a SHIP_TO Party for the Order. If none is found, and billToFallback is set to true, then
626     * attempt to find the BILL_TO Party. If a BILL_TO Party is found, copy it to the SHIP_TO.
627     * 
628     * @param order Required.
629     * @param billToFallback Required.
630     * @param createdBy Required if billToFallback is true.
631     * @return The Party that is to be used for the SHIP_TO OrderRoleType. May be null.
632     */
633    public Party getOrderShipToParty(final Order order, final boolean billToFallback, final BasePK createdBy) {
634        var orderRoleControl = Session.getModelController(OrderRoleControl.class);
635        var shipToOrderRole = orderRoleControl.getOrderRoleByOrderAndOrderRoleTypeUsingNames(order, OrderRoleTypes.SHIP_TO.name());
636        
637        if(shipToOrderRole == null && billToFallback) {
638            shipToOrderRole = orderRoleControl.getOrderRoleByOrderAndOrderRoleTypeUsingNames(order, OrderRoleTypes.BILL_TO.name());
639            
640            if(shipToOrderRole != null) {
641                orderRoleControl.createOrderRoleUsingNames(order, shipToOrderRole.getParty(), OrderRoleTypes.SHIP_TO.name(), createdBy);
642            }
643        }
644        
645        return shipToOrderRole == null ? null : shipToOrderRole.getParty();
646    }
647    
648}