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.associate.server.logic.AssociateReferralLogic; 020import com.echothree.model.control.cancellationpolicy.common.CancellationKinds; 021import com.echothree.model.control.cancellationpolicy.server.logic.CancellationPolicyLogic; 022import com.echothree.model.control.inventory.common.exception.UnknownDefaultInventoryConditionException; 023import com.echothree.model.control.inventory.server.control.InventoryControl; 024import com.echothree.model.control.inventory.server.logic.InventoryConditionLogic; 025import com.echothree.model.control.item.common.ItemPriceTypes; 026import com.echothree.model.control.item.common.exception.UnknownDefaultItemUnitOfMeasureTypeException; 027import com.echothree.model.control.item.common.workflow.ItemStatusConstants; 028import com.echothree.model.control.item.server.control.ItemControl; 029import com.echothree.model.control.item.server.logic.ItemLogic; 030import com.echothree.model.control.offer.common.exception.UnknownOfferItemPriceException; 031import com.echothree.model.control.offer.server.control.OfferItemControl; 032import com.echothree.model.control.offer.server.logic.OfferItemLogic; 033import com.echothree.model.control.offer.server.logic.SourceLogic; 034import com.echothree.model.control.order.common.OrderTypes; 035import com.echothree.model.control.order.server.logic.BaseOrderLineLogic; 036import com.echothree.model.control.returnpolicy.common.ReturnKinds; 037import com.echothree.model.control.returnpolicy.server.logic.ReturnPolicyLogic; 038import com.echothree.model.control.sales.common.exception.CurrentTimeAfterSalesOrderEndTimeException; 039import com.echothree.model.control.sales.common.exception.CurrentTimeBeforeSalesOrderStartTimeException; 040import com.echothree.model.control.sales.common.exception.ItemDiscontinuedException; 041import com.echothree.model.control.sales.common.exception.QuantityAboveMaximumItemUnitCustomerTypeLimitException; 042import com.echothree.model.control.sales.common.exception.QuantityAboveMaximumItemUnitLimitException; 043import com.echothree.model.control.sales.common.exception.QuantityBelowMinimumItemUnitCustomerTypeLimitException; 044import com.echothree.model.control.sales.common.exception.QuantityBelowMinimumItemUnitLimitException; 045import com.echothree.model.control.sales.common.exception.UnitAmountAboveMaximumItemUnitPriceLimitException; 046import com.echothree.model.control.sales.common.exception.UnitAmountAboveMaximumUnitPriceException; 047import com.echothree.model.control.sales.common.exception.UnitAmountBelowMinimumItemUnitPriceLimitException; 048import com.echothree.model.control.sales.common.exception.UnitAmountBelowMinimumUnitPriceException; 049import com.echothree.model.control.sales.common.exception.UnitAmountNotMultipleOfUnitPriceIncrementException; 050import com.echothree.model.control.sales.common.exception.UnitAmountRequiredException; 051import com.echothree.model.control.sales.server.control.SalesOrderControl; 052import com.echothree.model.control.uom.server.logic.UnitOfMeasureTypeLogic; 053import com.echothree.model.control.workflow.server.logic.WorkflowStepLogic; 054import com.echothree.model.data.associate.server.entity.AssociateReferral; 055import com.echothree.model.data.batch.server.entity.Batch; 056import com.echothree.model.data.cancellationpolicy.server.entity.CancellationPolicy; 057import com.echothree.model.data.contact.server.entity.PartyContactMechanism; 058import com.echothree.model.data.inventory.server.entity.InventoryCondition; 059import com.echothree.model.data.item.server.entity.Item; 060import com.echothree.model.data.offer.server.entity.Source; 061import com.echothree.model.data.order.server.entity.Order; 062import com.echothree.model.data.order.server.entity.OrderLine; 063import com.echothree.model.data.order.server.entity.OrderShipmentGroup; 064import com.echothree.model.data.party.server.entity.Party; 065import com.echothree.model.data.returnpolicy.server.entity.ReturnPolicy; 066import com.echothree.model.data.shipping.server.entity.ShippingMethod; 067import com.echothree.model.data.uom.server.entity.UnitOfMeasureType; 068import com.echothree.model.data.user.server.entity.UserVisit; 069import com.echothree.util.common.message.ExecutionErrors; 070import com.echothree.util.server.message.DummyExecutionErrorAccumulator; 071import com.echothree.util.server.message.ExecutionErrorAccumulator; 072import com.echothree.util.server.persistence.Session; 073import javax.enterprise.context.ApplicationScoped; 074import javax.enterprise.inject.spi.CDI; 075 076@ApplicationScoped 077public class SalesOrderLineLogic 078 extends BaseOrderLineLogic { 079 080 protected SalesOrderLineLogic() { 081 super(); 082 } 083 084 public static SalesOrderLineLogic getInstance() { 085 return CDI.current().select(SalesOrderLineLogic.class).get(); 086 } 087 088 /** 089 * Create a new Sales Order Line using appropriate defaults for Optional values when possible. 090 * 091 * @param session Required. 092 * @param eea Optional. 093 * @param userVisit Required. 094 * @param order Optional. 095 * @param orderShipmentGroup Optional. 096 * @param orderShipmentGroupSequence Optional. 097 * @param orderLineSequence Optional. 098 * @param parentOrderLine Optional. 099 * @param partyContactMechanism Optional. 100 * @param shippingMethod Optional. 101 * @param item Required. 102 * @param inventoryCondition Optional. 103 * @param unitOfMeasureType Optional. 104 * @param quantity Required. 105 * @param unitAmount Optional for Items with a FIXED ItemPriceType, Required for VARIABLE. 106 * @param description Optional. 107 * @param taxable Optional. 108 * @param source Optional. 109 * @param associateReferral Optional. 110 * @param createdByParty Required. 111 * @return The newly created OrderLine, otherwise null if there was an error. 112 */ 113 public OrderLine createSalesOrderLine(final Session session, final ExecutionErrorAccumulator eea, final UserVisit userVisit, 114 Order order, OrderShipmentGroup orderShipmentGroup, final Integer orderShipmentGroupSequence, Integer orderLineSequence, 115 final OrderLine parentOrderLine, PartyContactMechanism partyContactMechanism, ShippingMethod shippingMethod, final Item item, 116 InventoryCondition inventoryCondition, UnitOfMeasureType unitOfMeasureType, final Long quantity, Long unitAmount, 117 final String description, CancellationPolicy cancellationPolicy, ReturnPolicy returnPolicy, Boolean taxable, final Source source, 118 final AssociateReferral associateReferral, final Party createdByParty) { 119 var salesOrderLogic = SalesOrderLogic.getInstance(); 120 var createdByPartyPK = createdByParty.getPrimaryKey(); 121 OrderLine orderLine = null; 122 123 // Create a new Sales Order if there was not one supplied. Defaults will be used for nearly all options. 124 if(order == null) { 125 order = SalesOrderLogic.getInstance().createSalesOrder(session, eea, userVisit, (Batch)null, null, null, 126 null, null, null, null, null, null, null, null, null, null, null, createdByParty); 127 } 128 129 salesOrderLogic.checkOrderAvailableForModification(session, eea, order, createdByPartyPK); 130 131 if(eea == null || !eea.hasExecutionErrors()) { 132 var salesOrderShipmentGroupLogic = SalesOrderShipmentGroupLogic.getInstance(); 133 var itemControl = Session.getModelController(ItemControl.class); 134 var salesOrderControl = Session.getModelController(SalesOrderControl.class); 135 var orderDetail = order.getLastDetail(); 136 var itemDetail = item.getLastDetail(); 137 var itemDeliveryType = itemDetail.getItemDeliveryType(); 138 var currency = orderDetail.getCurrency(); 139 var customerType = salesOrderLogic.getOrderBillToCustomerType(order); 140 141 if(customerType != null && shippingMethod != null) { 142 salesOrderShipmentGroupLogic.checkCustomerTypeShippingMethod(eea, customerType, shippingMethod); 143 } 144 145 if(eea == null || !eea.hasExecutionErrors()) { 146 // ItemDeliveryType must be checked against the ContactMechanismType for the partyContactMechanism. 147 148 // Check to see if an orderShipmentGroup was supplied. If it wasn't, try to get a default one for this order and itemDeliveryType. 149 // If a default doesn't exist, then create one. 150 if(orderShipmentGroup == null) { 151 var dummyExecutionErrorAccumulator = new DummyExecutionErrorAccumulator(); // No Execution Errors, don't throw Exceptions 152 orderShipmentGroup = SalesOrderShipmentGroupLogic.getInstance().getDefaultOrderShipmentGroup(dummyExecutionErrorAccumulator, 153 order, itemDeliveryType); 154 155 if(orderShipmentGroup == null) { 156 var holdUntilComplete = order.getLastDetail().getHoldUntilComplete(); 157 var orderShipToParty = salesOrderLogic.getOrderShipToParty(order, true, createdByPartyPK); 158 159 // If partyContactMechanism is null, attempt to get from SHIP_TO party for the order. 160 // If no SHIP_TO party exists, try to copy from the BILL_TO party. 161 162 // TODO. 163 164 // Select an appropriate partyContactMechanism for the itemDeliveryType. 165 166 // TODO. 167 168 orderShipmentGroup = salesOrderShipmentGroupLogic.createSalesOrderShipmentGroup(session, eea, order, 169 orderShipmentGroupSequence, itemDeliveryType, true, partyContactMechanism, 170 shippingMethod, holdUntilComplete, createdByPartyPK); 171 } else { 172 var orderShipmentGroupDetail = orderShipmentGroup.getLastDetail(); 173 174 partyContactMechanism = orderShipmentGroupDetail.getPartyContactMechanism(); 175 shippingMethod = orderShipmentGroupDetail.getShippingMethod(); 176 } 177 } 178 179 // If shippingMethod was specified, check to see if it can be used for this item and partyContactMechanism. 180 181 // Check InventoryCondition. 182 if(inventoryCondition == null) { 183 var inventoryControl = Session.getModelController(InventoryControl.class); 184 185 inventoryCondition = inventoryControl.getDefaultInventoryCondition(); 186 187 if(inventoryControl == null) { 188 handleExecutionError(UnknownDefaultInventoryConditionException.class, eea, ExecutionErrors.UnknownDefaultInventoryCondition.name()); 189 } 190 } 191 192 // Check UnitOfMeasureType. 193 if(unitOfMeasureType == null) { 194 var itemUnitOfMeasureType = itemControl.getDefaultItemUnitOfMeasureType(item); 195 196 if(itemUnitOfMeasureType == null) { 197 handleExecutionError(UnknownDefaultItemUnitOfMeasureTypeException.class, eea, ExecutionErrors.UnknownDefaultItemUnitOfMeasureType.name()); 198 } else { 199 unitOfMeasureType = itemUnitOfMeasureType.getUnitOfMeasureType(); 200 } 201 } 202 203 // Verify the OfferItem exists. 204 var offerUse = source == null ? null : source.getLastDetail().getOfferUse(); 205 if(offerUse == null) { 206 var salesOrder = salesOrderControl.getSalesOrder(order); 207 208 offerUse = salesOrder.getOfferUse(); 209 } 210 211 var offerItem = OfferItemLogic.getInstance().getOfferItem(eea, offerUse.getLastDetail().getOffer(), item); 212 213 // Verify unitAmount. 214 if(offerItem != null) { 215 var offerItemControl = Session.getModelController(OfferItemControl.class); 216 var offerItemPrice = offerItemControl.getOfferItemPrice(offerItem, inventoryCondition, unitOfMeasureType, currency); 217 218 if(offerItemPrice == null) { 219 handleExecutionError(UnknownOfferItemPriceException.class, eea, ExecutionErrors.UnknownOfferItemPrice.name()); 220 } else { 221 var itemPriceTypeName = itemDetail.getItemPriceType().getItemPriceTypeName(); 222 223 if(itemPriceTypeName.equals(ItemPriceTypes.FIXED.name())) { 224 // We'll accept the supplied unitAmount as long as it passes the limit checks later on. Any enforcement of 225 // security should come in the UC. 226 if(unitAmount == null) { 227 var offerItemFixedPrice = offerItemControl.getOfferItemFixedPrice(offerItemPrice); 228 229 unitAmount = offerItemFixedPrice.getUnitPrice(); 230 } 231 } else if(itemPriceTypeName.equals(ItemPriceTypes.VARIABLE.name())) { 232 if(unitAmount == null) { 233 handleExecutionError(UnitAmountRequiredException.class, eea, ExecutionErrors.UnitAmountRequired.name()); 234 } else { 235 var offerItemVariablePrice = offerItemControl.getOfferItemVariablePrice(offerItemPrice); 236 237 if(unitAmount < offerItemVariablePrice.getMinimumUnitPrice()) { 238 handleExecutionError(UnitAmountBelowMinimumUnitPriceException.class, eea, ExecutionErrors.UnitAmountBelowMinimumUnitPrice.name()); 239 } 240 241 if(unitAmount > offerItemVariablePrice.getMaximumUnitPrice()) { 242 handleExecutionError(UnitAmountAboveMaximumUnitPriceException.class, eea, ExecutionErrors.UnitAmountAboveMaximumUnitPrice.name()); 243 } 244 245 if(unitAmount % offerItemVariablePrice.getUnitPriceIncrement() != 0) { 246 handleExecutionError(UnitAmountNotMultipleOfUnitPriceIncrementException.class, eea, ExecutionErrors.UnitAmountNotMultipleOfUnitPriceIncrement.name()); 247 } 248 } 249 } else { 250 handleExecutionError(UnknownOfferItemPriceException.class, eea, ExecutionErrors.UnknownItemPriceType.name(), itemPriceTypeName); 251 } 252 } 253 } 254 255 // Check ItemUnitPriceLimits. 256 if(unitAmount != null) { 257 var itemUnitPriceLimit = itemControl.getItemUnitPriceLimit(item, inventoryCondition, unitOfMeasureType, currency); 258 259 // This isn't required. If it is missing, no check is performed. 260 if(itemUnitPriceLimit != null) { 261 var minimumUnitPrice = itemUnitPriceLimit.getMinimumUnitPrice(); 262 var maximumUnitPrice = itemUnitPriceLimit.getMaximumUnitPrice(); 263 264 if(minimumUnitPrice != null && unitAmount < minimumUnitPrice) { 265 handleExecutionError(UnitAmountBelowMinimumItemUnitPriceLimitException.class, eea, ExecutionErrors.UnitAmountBelowMinimumItemUnitPriceLimit.name()); 266 } 267 268 if(maximumUnitPrice != null && unitAmount > maximumUnitPrice) { 269 handleExecutionError(UnitAmountAboveMaximumItemUnitPriceLimitException.class, eea, ExecutionErrors.UnitAmountAboveMaximumItemUnitPriceLimit.name()); 270 } 271 } 272 } 273 274 // Check quantity being ordered and make sure that it's within acceptible limits. Both ItemUnitLimits and ItemUnitCustomerTypeLimits. 275 if(inventoryCondition != null && unitOfMeasureType != null) { 276 var itemUnitLimit = itemControl.getItemUnitLimit(item, inventoryCondition, unitOfMeasureType); 277 278 if(itemUnitLimit != null) { 279 var minimumQuantity = itemUnitLimit.getMinimumQuantity(); 280 var maximumQuantity = itemUnitLimit.getMaximumQuantity(); 281 282 if(minimumQuantity != null && quantity < minimumQuantity) { 283 handleExecutionError(QuantityBelowMinimumItemUnitLimitException.class, eea, ExecutionErrors.QuantityBelowMinimumItemUnitLimit.name()); 284 } 285 286 if(maximumQuantity != null && quantity > maximumQuantity) { 287 handleExecutionError(QuantityAboveMaximumItemUnitLimitException.class, eea, ExecutionErrors.QuantityAboveMaximumItemUnitLimit.name()); 288 } 289 } 290 291 if(customerType != null) { 292 var itemUnitCustomerTypeLimit = itemControl.getItemUnitCustomerTypeLimit(item, inventoryCondition, unitOfMeasureType, customerType); 293 294 if(itemUnitCustomerTypeLimit != null) { 295 var minimumQuantity = itemUnitCustomerTypeLimit.getMinimumQuantity(); 296 var maximumQuantity = itemUnitCustomerTypeLimit.getMaximumQuantity(); 297 298 if(minimumQuantity != null && quantity < minimumQuantity) { 299 handleExecutionError(QuantityBelowMinimumItemUnitCustomerTypeLimitException.class, eea, ExecutionErrors.QuantityBelowMinimumItemUnitCustomerTypeLimit.name()); 300 } 301 302 if(maximumQuantity != null && quantity > maximumQuantity) { 303 handleExecutionError(QuantityAboveMaximumItemUnitCustomerTypeLimitException.class, eea, ExecutionErrors.QuantityAboveMaximumItemUnitCustomerTypeLimit.name()); 304 } 305 } 306 } 307 } 308 309 // Check Item's SalesOrderStartTime and SalesOrderEndTime. 310 var salesOrderStartTime = itemDetail.getSalesOrderStartTime(); 311 if(salesOrderStartTime != null && session.START_TIME < salesOrderStartTime) { 312 handleExecutionError(CurrentTimeBeforeSalesOrderStartTimeException.class, eea, ExecutionErrors.CurrentTimeBeforeSalesOrderStartTime.name()); 313 } 314 315 var salesOrderEndTime = itemDetail.getSalesOrderEndTime(); 316 if(salesOrderEndTime != null && session.START_TIME > salesOrderEndTime) { 317 handleExecutionError(CurrentTimeAfterSalesOrderEndTimeException.class, eea, ExecutionErrors.CurrentTimeAfterSalesOrderEndTime.name()); 318 } 319 320 // Check Item's status. 321 if(!WorkflowStepLogic.getInstance().isEntityInWorkflowSteps(eea, ItemStatusConstants.Workflow_ITEM_STATUS, item, 322 ItemStatusConstants.WorkflowStep_ITEM_STATUS_DISCONTINUED).isEmpty()) { 323 handleExecutionError(ItemDiscontinuedException.class, eea, ExecutionErrors.ItemDiscontinued.name(), item.getLastDetail().getItemName()); 324 } 325 326 // Create the line. 327 if(eea == null || !eea.hasExecutionErrors()) { 328 // If a specific CancellationPolicy was specified, use that. Otherwise, try to use the one for the Item. 329 // If that's null, we'll leave it null for the OrderLine, which indicates that we should fall back to the 330 // one on the Order if it's ever needed. 331 if(cancellationPolicy == null) { 332 cancellationPolicy = itemDetail.getCancellationPolicy(); 333 } 334 335 // If a specific ReturnPolicy was specified, use that. Otherwise, try to use the one for the Item. 336 // If that's null, we'll leave it null for the OrderLine, which indicates that we should fall back to the 337 // one on the Order if it's ever needed. 338 if(returnPolicy == null) { 339 returnPolicy = itemDetail.getReturnPolicy(); 340 } 341 342 // If there was no taxable flag passed in, then get the taxable value from the Item. If that is true, 343 // the taxable flag from the Order will override it. 344 if(taxable == null) { 345 // taxable = itemDetail.getTaxable(); 346 taxable = true; // TODO: This needs to consider the GeoCode-aware taxing system. 347 348 if(taxable) { 349 taxable = orderDetail.getTaxable(); 350 } 351 } 352 353 orderLine = createOrderLine(session, eea, order, orderLineSequence, parentOrderLine, orderShipmentGroup, item, inventoryCondition, 354 unitOfMeasureType, quantity, unitAmount, description, cancellationPolicy, returnPolicy, taxable, createdByPartyPK); 355 356 if(eea == null || !eea.hasExecutionErrors()) { 357 salesOrderControl.createSalesOrderLine(orderLine, offerUse, associateReferral, createdByPartyPK); 358 } 359 } 360 } 361 } 362 363 return orderLine; 364 } 365 366 public OrderLine createOrderLine(final Session session, final ExecutionErrorAccumulator eea, final UserVisit userVisit, 367 final String orderName, final String itemName, final String inventoryConditionName, final String cancellationPolicyName, 368 final String returnPolicyName, final String unitOfMeasureTypeName, final String sourceName, 369 final String strOrderLineSequence, final String strQuantity, final String strUnitAmount, final String description, 370 final String strTaxable, final Party createdByParty) { 371 var order = orderName == null ? null : SalesOrderLogic.getInstance().getOrderByName(eea, orderName); 372 var item = ItemLogic.getInstance().getItemByNameThenAlias(eea, itemName); 373 var inventoryCondition = inventoryConditionName == null ? null : InventoryConditionLogic.getInstance().getInventoryConditionByName(eea, inventoryConditionName); 374 var cancellationPolicy = cancellationPolicyName == null ? null : CancellationPolicyLogic.getInstance().getCancellationPolicyByName(eea, CancellationKinds.CUSTOMER_CANCELLATION.name(), cancellationPolicyName); 375 var returnPolicy = returnPolicyName == null ? null : ReturnPolicyLogic.getInstance().getReturnPolicyByName(eea, ReturnKinds.CUSTOMER_RETURN.name(), returnPolicyName); 376 var source = sourceName == null ? null : SourceLogic.getInstance().getSourceByName(eea, sourceName); 377 OrderLine orderLine = null; 378 379 if(!eea.hasExecutionErrors()) { 380 var itemDetail = item.getLastDetail(); 381 var unitOfMeasureKind = itemDetail.getUnitOfMeasureKind(); 382 var unitOfMeasureType = unitOfMeasureTypeName == null ? null : UnitOfMeasureTypeLogic.getInstance().getUnitOfMeasureTypeByName(eea, unitOfMeasureKind, unitOfMeasureTypeName); 383 384 if(!eea.hasExecutionErrors()) { 385 var orderLineSequence = strOrderLineSequence == null ? null : Integer.valueOf(strOrderLineSequence); 386 var quantity = Long.valueOf(strQuantity); 387 var unitAmount = strUnitAmount == null ? null : Long.valueOf(strUnitAmount); 388 var taxable = strTaxable == null ? null : Boolean.valueOf(strTaxable); 389 var associateReferral = AssociateReferralLogic.getInstance().getAssociateReferral(session, userVisit); 390 391 orderLine = createSalesOrderLine(session, eea, userVisit, order, null, 392 null, orderLineSequence, null, null, null, item, inventoryCondition, unitOfMeasureType, quantity, 393 unitAmount, description, cancellationPolicy, returnPolicy, taxable, source, associateReferral, 394 createdByParty); 395 } 396 } 397 398 return orderLine; 399 } 400 401 public OrderLine getOrderLineByName(final ExecutionErrorAccumulator eea, final String orderName, final String orderLineSequence) { 402 return getOrderLineByName(eea, OrderTypes.SALES_ORDER.name(), orderName, orderLineSequence); 403 } 404 405 public OrderLine getOrderLineByNameForUpdate(final ExecutionErrorAccumulator eea, final String orderName, final String orderLineSequence) { 406 return getOrderLineByNameForUpdate(eea, OrderTypes.SALES_ORDER.name(), orderName, orderLineSequence); 407 } 408 409}