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