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