001// -------------------------------------------------------------------------------- 002// Copyright 2002-2024 Echo Three, LLC 003// 004// Licensed under the Apache License, Version 2.0 (the "License"); 005// you may not use this file except in compliance with the License. 006// You may obtain a copy of the License at 007// 008// http://www.apache.org/licenses/LICENSE-2.0 009// 010// Unless required by applicable law or agreed to in writing, software 011// distributed under the License is distributed on an "AS IS" BASIS, 012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013// See the License for the specific language governing permissions and 014// limitations under the License. 015// -------------------------------------------------------------------------------- 016 017package com.echothree.model.control.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.OrderLineLogic; 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.ExecutionErrorAccumulator; 071import com.echothree.util.server.persistence.Session; 072 073public class SalesOrderLineLogic 074 extends OrderLineLogic { 075 076 private SalesOrderLineLogic() { 077 super(); 078 } 079 080 private static class LogicHolder { 081 static SalesOrderLineLogic instance = new SalesOrderLineLogic(); 082 } 083 084 public static SalesOrderLineLogic getInstance() { 085 return LogicHolder.instance; 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 orderShipmentGroup = salesOrderLogic.getDefaultOrderShipmentGroup(order, itemDeliveryType); 152 153 if(orderShipmentGroup == null) { 154 var holdUntilComplete = order.getLastDetail().getHoldUntilComplete(); 155 var orderShipToParty = salesOrderLogic.getOrderShipToParty(order, true, createdByPartyPK); 156 157 // If partyContactMechanism is null, attempt to get from SHIP_TO party for the order. 158 // If no SHIP_TO party exists, try to copy from the BILL_TO party. 159 160 // TODO. 161 162 // Select an appropriate partyContactMechanism for the itemDeliveryType. 163 164 // TODO. 165 166 orderShipmentGroup = salesOrderShipmentGroupLogic.createSalesOrderShipmentGroup(session, eea, order, 167 orderShipmentGroupSequence, itemDeliveryType, Boolean.TRUE, partyContactMechanism, 168 shippingMethod, holdUntilComplete, createdByPartyPK); 169 } else { 170 var orderShipmentGroupDetail = orderShipmentGroup.getLastDetail(); 171 172 partyContactMechanism = orderShipmentGroupDetail.getPartyContactMechanism(); 173 shippingMethod = orderShipmentGroupDetail.getShippingMethod(); 174 } 175 } 176 177 // If shippingMethod was specified, check to see if it can be used for this item and partyContactMechanism. 178 179 // Check InventoryCondition. 180 if(inventoryCondition == null) { 181 var inventoryControl = Session.getModelController(InventoryControl.class); 182 183 inventoryCondition = inventoryControl.getDefaultInventoryCondition(); 184 185 if(inventoryControl == null) { 186 handleExecutionError(UnknownDefaultInventoryConditionException.class, eea, ExecutionErrors.UnknownDefaultInventoryCondition.name()); 187 } 188 } 189 190 // Check UnitOfMeasureType. 191 if(unitOfMeasureType == null) { 192 var itemUnitOfMeasureType = itemControl.getDefaultItemUnitOfMeasureType(item); 193 194 if(itemUnitOfMeasureType == null) { 195 handleExecutionError(UnknownDefaultItemUnitOfMeasureTypeException.class, eea, ExecutionErrors.UnknownDefaultItemUnitOfMeasureType.name()); 196 } else { 197 unitOfMeasureType = itemUnitOfMeasureType.getUnitOfMeasureType(); 198 } 199 } 200 201 // Verify the OfferItem exists. 202 var offerUse = source == null ? null : source.getLastDetail().getOfferUse(); 203 if(offerUse == null) { 204 var salesOrder = salesOrderControl.getSalesOrder(order); 205 206 offerUse = salesOrder.getOfferUse(); 207 } 208 209 var offerItem = OfferItemLogic.getInstance().getOfferItem(eea, offerUse.getLastDetail().getOffer(), item); 210 211 // Verify unitAmount. 212 if(offerItem != null) { 213 var offerItemControl = Session.getModelController(OfferItemControl.class); 214 var offerItemPrice = offerItemControl.getOfferItemPrice(offerItem, inventoryCondition, unitOfMeasureType, currency); 215 216 if(offerItemPrice == null) { 217 handleExecutionError(UnknownOfferItemPriceException.class, eea, ExecutionErrors.UnknownOfferItemPrice.name()); 218 } else { 219 var itemPriceTypeName = itemDetail.getItemPriceType().getItemPriceTypeName(); 220 221 if(itemPriceTypeName.equals(ItemPriceTypes.FIXED.name())) { 222 // We'll accept the supplied unitAmount as long as it passes the limit checks later on. Any enforcement of 223 // security should come in the UC. 224 if(unitAmount == null) { 225 var offerItemFixedPrice = offerItemControl.getOfferItemFixedPrice(offerItemPrice); 226 227 unitAmount = offerItemFixedPrice.getUnitPrice(); 228 } 229 } else if(itemPriceTypeName.equals(ItemPriceTypes.VARIABLE.name())) { 230 if(unitAmount == null) { 231 handleExecutionError(UnitAmountRequiredException.class, eea, ExecutionErrors.UnitAmountRequired.name()); 232 } else { 233 var offerItemVariablePrice = offerItemControl.getOfferItemVariablePrice(offerItemPrice); 234 235 if(unitAmount < offerItemVariablePrice.getMinimumUnitPrice()) { 236 handleExecutionError(UnitAmountBelowMinimumUnitPriceException.class, eea, ExecutionErrors.UnitAmountBelowMinimumUnitPrice.name()); 237 } 238 239 if(unitAmount > offerItemVariablePrice.getMaximumUnitPrice()) { 240 handleExecutionError(UnitAmountAboveMaximumUnitPriceException.class, eea, ExecutionErrors.UnitAmountAboveMaximumUnitPrice.name()); 241 } 242 243 if(unitAmount % offerItemVariablePrice.getUnitPriceIncrement() != 0) { 244 handleExecutionError(UnitAmountNotMultipleOfUnitPriceIncrementException.class, eea, ExecutionErrors.UnitAmountNotMultipleOfUnitPriceIncrement.name()); 245 } 246 } 247 } else { 248 handleExecutionError(UnknownOfferItemPriceException.class, eea, ExecutionErrors.UnknownItemPriceType.name(), itemPriceTypeName); 249 } 250 } 251 } 252 253 // Check ItemUnitPriceLimits. 254 if(unitAmount != null) { 255 var itemUnitPriceLimit = itemControl.getItemUnitPriceLimit(item, inventoryCondition, unitOfMeasureType, currency); 256 257 // This isn't required. If it is missing, no check is performed. 258 if(itemUnitPriceLimit != null) { 259 var minimumUnitPrice = itemUnitPriceLimit.getMinimumUnitPrice(); 260 var maximumUnitPrice = itemUnitPriceLimit.getMaximumUnitPrice(); 261 262 if(minimumUnitPrice != null && unitAmount < minimumUnitPrice) { 263 handleExecutionError(UnitAmountBelowMinimumItemUnitPriceLimitException.class, eea, ExecutionErrors.UnitAmountBelowMinimumItemUnitPriceLimit.name()); 264 } 265 266 if(maximumUnitPrice != null && unitAmount > maximumUnitPrice) { 267 handleExecutionError(UnitAmountAboveMaximumItemUnitPriceLimitException.class, eea, ExecutionErrors.UnitAmountAboveMaximumItemUnitPriceLimit.name()); 268 } 269 } 270 } 271 272 // Check quantity being ordered and make sure that it's within acceptible limits. Both ItemUnitLimits and ItemUnitCustomerTypeLimits. 273 if(inventoryCondition != null && unitOfMeasureType != null) { 274 var itemUnitLimit = itemControl.getItemUnitLimit(item, inventoryCondition, unitOfMeasureType); 275 276 if(itemUnitLimit != null) { 277 var minimumQuantity = itemUnitLimit.getMinimumQuantity(); 278 var maximumQuantity = itemUnitLimit.getMaximumQuantity(); 279 280 if(minimumQuantity != null && quantity < minimumQuantity) { 281 handleExecutionError(QuantityBelowMinimumItemUnitLimitException.class, eea, ExecutionErrors.QuantityBelowMinimumItemUnitLimit.name()); 282 } 283 284 if(maximumQuantity != null && quantity > maximumQuantity) { 285 handleExecutionError(QuantityAboveMaximumItemUnitLimitException.class, eea, ExecutionErrors.QuantityAboveMaximumItemUnitLimit.name()); 286 } 287 } 288 289 if(customerType != null) { 290 var itemUnitCustomerTypeLimit = itemControl.getItemUnitCustomerTypeLimit(item, inventoryCondition, unitOfMeasureType, customerType); 291 292 if(itemUnitCustomerTypeLimit != null) { 293 var minimumQuantity = itemUnitCustomerTypeLimit.getMinimumQuantity(); 294 var maximumQuantity = itemUnitCustomerTypeLimit.getMaximumQuantity(); 295 296 if(minimumQuantity != null && quantity < minimumQuantity) { 297 handleExecutionError(QuantityBelowMinimumItemUnitCustomerTypeLimitException.class, eea, ExecutionErrors.QuantityBelowMinimumItemUnitCustomerTypeLimit.name()); 298 } 299 300 if(maximumQuantity != null && quantity > maximumQuantity) { 301 handleExecutionError(QuantityAboveMaximumItemUnitCustomerTypeLimitException.class, eea, ExecutionErrors.QuantityAboveMaximumItemUnitCustomerTypeLimit.name()); 302 } 303 } 304 } 305 } 306 307 // Check Item's SalesOrderStartTime and SalesOrderEndTime. 308 var salesOrderStartTime = itemDetail.getSalesOrderStartTime(); 309 if(salesOrderStartTime != null && session.START_TIME < salesOrderStartTime) { 310 handleExecutionError(CurrentTimeBeforeSalesOrderStartTimeException.class, eea, ExecutionErrors.CurrentTimeBeforeSalesOrderStartTime.name()); 311 } 312 313 Long salesOrderEndTime = itemDetail.getSalesOrderEndTime(); 314 if(salesOrderEndTime != null && session.START_TIME > salesOrderEndTime) { 315 handleExecutionError(CurrentTimeAfterSalesOrderEndTimeException.class, eea, ExecutionErrors.CurrentTimeAfterSalesOrderEndTime.name()); 316 } 317 318 // Check Item's status. 319 if(!WorkflowStepLogic.getInstance().isEntityInWorkflowSteps(eea, ItemStatusConstants.Workflow_ITEM_STATUS, item, 320 ItemStatusConstants.WorkflowStep_ITEM_STATUS_DISCONTINUED).isEmpty()) { 321 handleExecutionError(ItemDiscontinuedException.class, eea, ExecutionErrors.ItemDiscontinued.name(), item.getLastDetail().getItemName()); 322 } 323 324 // Create the line. 325 if(eea == null || !eea.hasExecutionErrors()) { 326 // If a specific CancellationPolicy was specified, use that. Otherwise, try to use the one for the Item. 327 // If that's null, we'll leave it null for the OrderLine, which indicates that we should fall back to the 328 // one on the Order if it's ever needed. 329 if(cancellationPolicy == null) { 330 cancellationPolicy = itemDetail.getCancellationPolicy(); 331 } 332 333 // If a specific ReturnPolicy was specified, use that. Otherwise, try to use the one for the Item. 334 // If that's null, we'll leave it null for the OrderLine, which indicates that we should fall back to the 335 // one on the Order if it's ever needed. 336 if(returnPolicy == null) { 337 returnPolicy = itemDetail.getReturnPolicy(); 338 } 339 340 // If there was no taxable flag passed in, then get the taxable value from the Item. If that is true, 341 // the taxable flag from the Order will override it. 342 if(taxable == null) { 343 // taxable = itemDetail.getTaxable(); 344 taxable = Boolean.TRUE; // TODO: This needs to consider the GeoCode-aware taxing system. 345 346 if(taxable) { 347 taxable = orderDetail.getTaxable(); 348 } 349 } 350 351 orderLine = createOrderLine(session, eea, order, orderLineSequence, parentOrderLine, orderShipmentGroup, item, inventoryCondition, 352 unitOfMeasureType, quantity, unitAmount, description, cancellationPolicy, returnPolicy, taxable, createdByPartyPK); 353 354 if(eea == null || !eea.hasExecutionErrors()) { 355 salesOrderControl.createSalesOrderLine(orderLine, offerUse, associateReferral, createdByPartyPK); 356 } 357 } 358 } 359 } 360 361 return orderLine; 362 } 363 364 public OrderLine createOrderLine(final Session session, final ExecutionErrorAccumulator eea, final UserVisit userVisit, 365 final String orderName, final String itemName, final String inventoryConditionName, final String cancellationPolicyName, 366 final String returnPolicyName, final String unitOfMeasureTypeName, final String sourceName, 367 final String strOrderLineSequence, final String strQuantity, final String strUnitAmount, final String description, 368 final String strTaxable, final Party createdByParty) { 369 var order = orderName == null ? null : SalesOrderLogic.getInstance().getOrderByName(eea, orderName); 370 var item = ItemLogic.getInstance().getItemByNameThenAlias(eea, itemName); 371 var inventoryCondition = inventoryConditionName == null ? null : InventoryConditionLogic.getInstance().getInventoryConditionByName(eea, inventoryConditionName); 372 var cancellationPolicy = cancellationPolicyName == null ? null : CancellationPolicyLogic.getInstance().getCancellationPolicyByName(eea, CancellationKinds.CUSTOMER_CANCELLATION.name(), cancellationPolicyName); 373 var returnPolicy = returnPolicyName == null ? null : ReturnPolicyLogic.getInstance().getReturnPolicyByName(eea, ReturnKinds.CUSTOMER_RETURN.name(), returnPolicyName); 374 var source = sourceName == null ? null : SourceLogic.getInstance().getSourceByName(eea, sourceName); 375 OrderLine orderLine = null; 376 377 if(!eea.hasExecutionErrors()) { 378 var itemDetail = item.getLastDetail(); 379 var unitOfMeasureKind = itemDetail.getUnitOfMeasureKind(); 380 var unitOfMeasureType = unitOfMeasureTypeName == null ? null : UnitOfMeasureTypeLogic.getInstance().getUnitOfMeasureTypeByName(eea, unitOfMeasureKind, unitOfMeasureTypeName); 381 382 if(!eea.hasExecutionErrors()) { 383 var orderLineSequence = strOrderLineSequence == null ? null : Integer.valueOf(strOrderLineSequence); 384 var quantity = Long.valueOf(strQuantity); 385 var unitAmount = strUnitAmount == null ? null : Long.valueOf(strUnitAmount); 386 var taxable = strTaxable == null ? null : Boolean.valueOf(strTaxable); 387 var associateReferral = AssociateReferralLogic.getInstance().getAssociateReferral(session, userVisit); 388 389 orderLine = createSalesOrderLine(session, eea, userVisit, order, null, 390 null, orderLineSequence, null, null, null, item, inventoryCondition, unitOfMeasureType, quantity, 391 unitAmount, description, cancellationPolicy, returnPolicy, taxable, source, associateReferral, 392 createdByParty); 393 } 394 } 395 396 return orderLine; 397 } 398 399 public OrderLine getOrderLineByName(final ExecutionErrorAccumulator eea, final String orderName, final String orderLineSequence) { 400 return getOrderLineByName(eea, OrderTypes.SALES_ORDER.name(), orderName, orderLineSequence); 401 } 402 403 public OrderLine getOrderLineByNameForUpdate(final ExecutionErrorAccumulator eea, final String orderName, final String orderLineSequence) { 404 return getOrderLineByNameForUpdate(eea, OrderTypes.SALES_ORDER.name(), orderName, orderLineSequence); 405 } 406 407}