| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.ofbiz.order.requirement; |
| |
| import java.util.*; |
| import java.math.BigDecimal; |
| import java.sql.Timestamp; |
| |
| import org.apache.ofbiz.base.util.*; |
| import org.apache.ofbiz.entity.condition.*; |
| import org.apache.ofbiz.entity.Delegator; |
| import org.apache.ofbiz.entity.GenericEntityException; |
| import org.apache.ofbiz.entity.GenericValue; |
| import org.apache.ofbiz.entity.util.EntityQuery; |
| import org.apache.ofbiz.entity.util.EntityUtil; |
| import org.apache.ofbiz.order.order.OrderReadHelper; |
| import org.apache.ofbiz.service.DispatchContext; |
| import org.apache.ofbiz.service.GenericServiceException; |
| import org.apache.ofbiz.service.LocalDispatcher; |
| import org.apache.ofbiz.service.ServiceUtil; |
| |
| /** |
| * Requirement Services |
| */ |
| |
| public class RequirementServices { |
| |
| public static final String module = RequirementServices.class.getName(); |
| public static final String resource_error = "OrderErrorUiLabels"; |
| |
| public static Map<String, Object> getRequirementsForSupplier(DispatchContext ctx, Map<String, ? extends Object> context) { |
| Delegator delegator = ctx.getDelegator(); |
| LocalDispatcher dispatcher = ctx.getDispatcher(); |
| Locale locale = (Locale) context.get("locale"); |
| |
| EntityCondition requirementConditions = (EntityCondition) context.get("requirementConditions"); |
| String partyId = (String) context.get("partyId"); |
| String unassignedRequirements = (String) context.get("unassignedRequirements"); |
| List<String> statusIds = UtilGenerics.checkList(context.get("statusIds")); |
| //TODO currencyUomId still not used |
| try { |
| List<EntityCondition> conditions = UtilMisc.toList( |
| EntityCondition.makeCondition("requirementTypeId", EntityOperator.EQUALS, "PRODUCT_REQUIREMENT"), |
| EntityUtil.getFilterByDateExpr() |
| ); |
| if (UtilValidate.isNotEmpty(statusIds)) { |
| conditions.add(EntityCondition.makeCondition("statusId", EntityOperator.IN, statusIds)); |
| } else { |
| conditions.add(EntityCondition.makeCondition("statusId", EntityOperator.EQUALS, "REQ_APPROVED")); |
| } |
| if (requirementConditions != null) conditions.add(requirementConditions); |
| |
| // we're either getting the requirements for a given supplier, unassigned requirements, or requirements for all suppliers |
| if (UtilValidate.isNotEmpty(partyId)) { |
| conditions.add(EntityCondition.makeCondition("partyId", EntityOperator.EQUALS, partyId)); |
| conditions.add(EntityCondition.makeCondition("roleTypeId", EntityOperator.EQUALS, "SUPPLIER")); |
| } else if (UtilValidate.isNotEmpty(unassignedRequirements)) { |
| conditions.add(EntityCondition.makeCondition("partyId", EntityOperator.EQUALS, null)); |
| } else { |
| conditions.add(EntityCondition.makeCondition("roleTypeId", EntityOperator.EQUALS, "SUPPLIER")); |
| } |
| |
| List<GenericValue> requirementAndRoles = EntityQuery.use(delegator).from("RequirementAndRole") |
| .where(conditions) |
| .orderBy("partyId", "requirementId") |
| .queryList(); |
| |
| // maps to cache the associated suppliers and products data, so we don't do redundant DB and service requests |
| Map<String, GenericValue> suppliers = new HashMap<String, GenericValue>(); |
| Map<String, GenericValue> gids = new HashMap<String, GenericValue>(); |
| Map<String, Map<String, Object>> inventories = new HashMap<String, Map<String,Object>>(); |
| Map<String, BigDecimal> productsSold = new HashMap<String, BigDecimal>(); |
| |
| // to count quantity, running total, and distinct products in list |
| BigDecimal quantity = BigDecimal.ZERO; |
| BigDecimal amountTotal = BigDecimal.ZERO; |
| Set<String> products = new HashSet<String>(); |
| |
| // time period to count products ordered from, six months ago and the 1st of that month |
| Timestamp timePeriodStart = UtilDateTime.getMonthStart(UtilDateTime.nowTimestamp(), 0, -6); |
| |
| // join in fields with extra data about the suppliers and products |
| List<Map<String, Object>> requirements = new LinkedList<Map<String,Object>>(); |
| for (GenericValue requirement : requirementAndRoles) { |
| Map<String, Object> union = new HashMap<String, Object>(); |
| String productId = requirement.getString("productId"); |
| partyId = requirement.getString("partyId"); |
| String facilityId = requirement.getString("facilityId"); |
| BigDecimal requiredQuantity = requirement.getBigDecimal("quantity"); |
| |
| // get an available supplier product, preferably the one with the smallest minimum quantity to order, followed by price |
| String supplierKey = partyId + "^" + productId; |
| GenericValue supplierProduct = suppliers.get(supplierKey); |
| if (supplierProduct == null) { |
| // TODO: it is possible to restrict to quantity > minimumOrderQuantity, but then the entire requirement must be skipped |
| supplierProduct = EntityQuery.use(delegator).from("SupplierProduct") |
| .where("partyId", partyId, "productId", productId) |
| .orderBy("minimumOrderQuantity", "lastPrice") |
| .filterByDate("availableFromDate", "availableThruDate") |
| .queryFirst(); |
| suppliers.put(supplierKey, supplierProduct); |
| } |
| |
| // add our supplier product and cost of this line to the data |
| if (supplierProduct != null) { |
| union.putAll(supplierProduct.getAllFields()); |
| BigDecimal lastPrice = supplierProduct.getBigDecimal("lastPrice"); |
| amountTotal = amountTotal.add(lastPrice.multiply(requiredQuantity)); |
| } |
| |
| // for good identification, get the UPCA type (UPC code) |
| GenericValue gid = gids.get(productId); |
| if (gid == null) { |
| gid = EntityQuery.use(delegator).from("GoodIdentification").where("goodIdentificationTypeId", "UPCA", "productId", requirement.get("productId")).queryOne(); |
| gids.put(productId, gid); |
| } |
| if (gid != null) union.put("idValue", gid.get("idValue")); |
| |
| // the ATP and QOH quantities |
| if (UtilValidate.isNotEmpty(facilityId)) { |
| String inventoryKey = facilityId + "^" + productId; |
| Map<String, Object> inventory = inventories.get(inventoryKey); |
| if (inventory == null) { |
| inventory = dispatcher.runSync("getInventoryAvailableByFacility", UtilMisc.toMap("productId", productId, "facilityId", facilityId)); |
| if (ServiceUtil.isError(inventory)) { |
| return inventory; |
| } |
| inventories.put(inventoryKey, inventory); |
| } |
| if (inventory != null) { |
| union.put("qoh", inventory.get("quantityOnHandTotal")); |
| union.put("atp", inventory.get("availableToPromiseTotal")); |
| } |
| } |
| |
| // how many of the products were sold (note this is for a fixed time period across all product stores) |
| BigDecimal sold = productsSold.get(productId); |
| if (sold == null) { |
| EntityCondition prodConditions = EntityCondition.makeCondition(UtilMisc.toList( |
| EntityCondition.makeCondition("productId", EntityOperator.EQUALS, productId), |
| EntityCondition.makeCondition("orderTypeId", EntityOperator.EQUALS, "SALES_ORDER"), |
| EntityCondition.makeCondition("orderStatusId", EntityOperator.NOT_IN, UtilMisc.toList("ORDER_REJECTED", "ORDER_CANCELLED")), |
| EntityCondition.makeCondition("orderItemStatusId", EntityOperator.NOT_IN, UtilMisc.toList("ITEM_REJECTED", "ITEM_CANCELLED")), |
| EntityCondition.makeCondition("orderDate", EntityOperator.GREATER_THAN_EQUAL_TO, timePeriodStart) |
| ), EntityOperator.AND); |
| GenericValue count = EntityQuery.use(delegator).select("quantityOrdered").from("OrderItemQuantityReportGroupByProduct").where(prodConditions).queryFirst(); |
| if (count != null) { |
| sold = count.getBigDecimal("quantityOrdered"); |
| if (sold != null) productsSold.put(productId, sold); |
| } |
| } |
| if (sold != null) { |
| union.put("qtySold", sold); |
| } |
| |
| // keep a running total of distinct products and quantity to order |
| if (requirement.getBigDecimal("quantity") == null) requirement.put("quantity", BigDecimal.ONE); // default quantity = 1 |
| quantity = quantity.add(requiredQuantity); |
| products.add(productId); |
| |
| // add all the requirement fields last, to overwrite any conflicting fields |
| union.putAll(requirement.getAllFields()); |
| requirements.add(union); |
| } |
| |
| Map<String, Object> results = ServiceUtil.returnSuccess(); |
| results.put("requirementsForSupplier", requirements); |
| results.put("distinctProductCount", Integer.valueOf(products.size())); |
| results.put("quantityTotal", quantity); |
| results.put("amountTotal", amountTotal); |
| return results; |
| } catch (GenericServiceException e) { |
| Debug.logError(e, module); |
| return ServiceUtil.returnError(UtilProperties.getMessage(resource_error, "OrderServiceExceptionSeeLogs", locale)); |
| } catch (GenericEntityException e) { |
| Debug.logError(e, module); |
| return ServiceUtil.returnError(UtilProperties.getMessage(resource_error, "OrderEntityExceptionSeeLogs", locale)); |
| } |
| } |
| |
| // note that this service is designed to work only when a sales order status changes from CREATED -> APPROVED because HOLD -> APPROVED is too complex |
| public static Map<String, Object> createAutoRequirementsForOrder(DispatchContext ctx, Map<String, ? extends Object> context) { |
| Delegator delegator = ctx.getDelegator(); |
| LocalDispatcher dispatcher = ctx.getDispatcher(); |
| GenericValue userLogin = (GenericValue) context.get("userLogin"); |
| |
| String orderId = (String) context.get("orderId"); |
| try { |
| GenericValue order = EntityQuery.use(delegator).from("OrderHeader").where("orderId", orderId).queryOne(); |
| GenericValue productStore = order.getRelatedOne("ProductStore", true); |
| if (productStore == null) { |
| Debug.logInfo("ProductStore for order ID " + orderId + " not found, requirements not created", module); |
| return ServiceUtil.returnSuccess(); |
| } |
| String facilityId = productStore.getString("inventoryFacilityId"); |
| List<GenericValue> orderItems = order.getRelated("OrderItem", null, null, false); |
| for (GenericValue item : orderItems) { |
| GenericValue product = item.getRelatedOne("Product", false); |
| if (product == null) continue; |
| if ((!"PRODRQM_AUTO".equals(product.get("requirementMethodEnumId")) && |
| !"PRODRQM_AUTO".equals(productStore.get("requirementMethodEnumId"))) || |
| (product.get("requirementMethodEnumId") == null && |
| !"PRODRQM_AUTO".equals(productStore.get("requirementMethodEnumId")))) continue; |
| BigDecimal quantity = item.getBigDecimal("quantity"); |
| BigDecimal cancelQuantity = item.getBigDecimal("cancelQuantity"); |
| BigDecimal required = quantity.subtract(cancelQuantity == null ? BigDecimal.ZERO : cancelQuantity); |
| if (required.compareTo(BigDecimal.ZERO) <= 0) continue; |
| |
| Map<String, Object> input = UtilMisc.toMap("userLogin", userLogin, "facilityId", facilityId, "productId", product.get("productId"), "quantity", required, "requirementTypeId", "PRODUCT_REQUIREMENT"); |
| Map<String, Object> results = dispatcher.runSync("createRequirement", input); |
| if (ServiceUtil.isError(results)) return results; |
| String requirementId = (String) results.get("requirementId"); |
| |
| input = UtilMisc.toMap("userLogin", userLogin, "orderId", order.get("orderId"), "orderItemSeqId", item.get("orderItemSeqId"), "requirementId", requirementId, "quantity", required); |
| results = dispatcher.runSync("createOrderRequirementCommitment", input); |
| if (ServiceUtil.isError(results)) return results; |
| } |
| } catch (GenericEntityException e) { |
| Debug.logError(e, module); |
| } catch (GenericServiceException e) { |
| Debug.logError(e, module); |
| } |
| return ServiceUtil.returnSuccess(); |
| } |
| |
| // note that this service is designed to work only when a sales order status changes from CREATED -> APPROVED because HOLD -> APPROVED is too complex |
| public static Map<String, Object> createATPRequirementsForOrder(DispatchContext ctx, Map<String, ? extends Object> context) { |
| Delegator delegator = ctx.getDelegator(); |
| LocalDispatcher dispatcher = ctx.getDispatcher(); |
| GenericValue userLogin = (GenericValue) context.get("userLogin"); |
| |
| /* |
| * The strategy in this service is to begin making requirements when the product falls below the |
| * ProductFacility.minimumStock. Because the minimumStock is an upper bound, the quantity to be required |
| * is either that required to bring the ATP back up to the minimumStock level or the amount ordered, |
| * whichever is less. |
| * |
| * If there is a way to support reorderQuantity without losing the order item -> requirement association data, |
| * then this service should be updated. |
| * |
| * The result is that this service generates many small requirements when stock levels are low for a product, |
| * which is perfectly fine since the system is capable of creating POs in bulk from aggregate requirements. |
| * The only concern would be a UI to manage numerous requirements with ease, preferrably by aggregating |
| * on productId. |
| */ |
| String orderId = (String) context.get("orderId"); |
| try { |
| GenericValue order = EntityQuery.use(delegator).from("OrderHeader").where("orderId", orderId).queryOne(); |
| GenericValue productStore = order.getRelatedOne("ProductStore", true); |
| if (productStore == null) { |
| Debug.logInfo("ProductStore for order ID " + orderId + " not found, ATP requirements not created", module); |
| return ServiceUtil.returnSuccess(); |
| } |
| String facilityId = productStore.getString("inventoryFacilityId"); |
| List<GenericValue> orderItems = order.getRelated("OrderItem", null, null, false); |
| for (GenericValue item : orderItems) { |
| GenericValue product = item.getRelatedOne("Product", false); |
| if (product == null) continue; |
| |
| if (!("PRODRQM_ATP".equals(product.get("requirementMethodEnumId")) || |
| ("PRODRQM_ATP".equals(productStore.get("requirementMethodEnumId")) && product.get("requirementMethodEnumId") == null))) continue; |
| |
| BigDecimal quantity = item.getBigDecimal("quantity"); |
| BigDecimal cancelQuantity = item.getBigDecimal("cancelQuantity"); |
| BigDecimal ordered = quantity.subtract(cancelQuantity == null ? BigDecimal.ZERO : cancelQuantity); |
| if (ordered.compareTo(BigDecimal.ZERO) <= 0) continue; |
| |
| // get the minimum stock for this facility (if not configured assume a minimum of zero, ie create requirements when it goes into backorder) |
| GenericValue productFacility = EntityQuery.use(delegator).from("ProductFacility").where("facilityId", facilityId, "productId", product.get("productId")).queryOne(); |
| BigDecimal minimumStock = BigDecimal.ZERO; |
| if (productFacility != null && productFacility.get("minimumStock") != null) { |
| minimumStock = productFacility.getBigDecimal("minimumStock"); |
| } |
| |
| // get the facility ATP for product, which should be updated for this item's reservation |
| Map<String, Object> results = dispatcher.runSync("getInventoryAvailableByFacility", UtilMisc.toMap("userLogin", userLogin, "productId", product.get("productId"), "facilityId", facilityId)); |
| if (ServiceUtil.isError(results)) return results; |
| BigDecimal atp = ((BigDecimal) results.get("availableToPromiseTotal")); // safe since this is a required OUT param |
| |
| // count all current requirements for this product |
| BigDecimal pendingRequirements = BigDecimal.ZERO; |
| EntityConditionList<EntityExpr> ecl = EntityCondition.makeCondition(UtilMisc.toList( |
| EntityCondition.makeCondition("facilityId", EntityOperator.EQUALS, facilityId), |
| EntityCondition.makeCondition("productId", EntityOperator.EQUALS, product.get("productId")), |
| EntityCondition.makeCondition("requirementTypeId", EntityOperator.EQUALS, "PRODUCT_REQUIREMENT"), |
| EntityCondition.makeCondition("statusId", EntityOperator.NOT_EQUAL, "REQ_ORDERED"), |
| EntityCondition.makeCondition("statusId", EntityOperator.NOT_EQUAL, "REQ_REJECTED")), |
| EntityOperator.AND); |
| List<GenericValue> requirements = EntityQuery.use(delegator).from("Requirement").where(ecl).queryList(); |
| for (GenericValue requirement : requirements) { |
| pendingRequirements = pendingRequirements.add(requirement.get("quantity") == null ? BigDecimal.ZERO : requirement.getBigDecimal("quantity")); |
| } |
| |
| // the minimum stock is an upper bound, therefore we either require up to the minimum stock or the input required quantity, whichever is less |
| BigDecimal shortfall = minimumStock.subtract(atp).subtract(pendingRequirements); |
| BigDecimal required = ordered.compareTo(shortfall) < 0 ? ordered : shortfall; |
| if (required.compareTo(BigDecimal.ZERO) <= 0) continue; |
| |
| Map<String, Object> input = UtilMisc.toMap("userLogin", userLogin, "facilityId", facilityId, "productId", product.get("productId"), "quantity", required, "requirementTypeId", "PRODUCT_REQUIREMENT"); |
| results = dispatcher.runSync("createRequirement", input); |
| if (ServiceUtil.isError(results)) return results; |
| String requirementId = (String) results.get("requirementId"); |
| |
| input = UtilMisc.toMap("userLogin", userLogin, "orderId", order.get("orderId"), "orderItemSeqId", item.get("orderItemSeqId"), "requirementId", requirementId, "quantity", required); |
| results = dispatcher.runSync("createOrderRequirementCommitment", input); |
| if (ServiceUtil.isError(results)) return results; |
| } |
| } catch (GenericEntityException e) { |
| Debug.logError(e, module); |
| } catch (GenericServiceException e) { |
| Debug.logError(e, module); |
| } |
| return ServiceUtil.returnSuccess(); |
| } |
| |
| public static Map<String, Object> updateRequirementsToOrdered (DispatchContext ctx, Map<String, ? extends Object> context) { |
| Delegator delegator = ctx.getDelegator(); |
| LocalDispatcher dispatcher = ctx.getDispatcher(); |
| GenericValue userLogin = (GenericValue) context.get("userLogin"); |
| String orderId = (String) context.get("orderId"); |
| OrderReadHelper orh = new OrderReadHelper(delegator, orderId); |
| try { |
| for(GenericValue orderItem: orh.getOrderItems()){ |
| GenericValue orderRequirementCommitment = EntityQuery.use(delegator).from("OrderRequirementCommitment") |
| .where(UtilMisc.toMap("orderId", orderId, "orderItemSeqId", orderItem.getString("orderItemSeqId"))) |
| .queryFirst(); |
| if (orderRequirementCommitment != null) { |
| String requirementId = orderRequirementCommitment.getString("requirementId"); |
| /* Change status of requirement to ordered */ |
| Map<String, Object> inputMap = UtilMisc.<String, Object>toMap("userLogin", userLogin, "requirementId", requirementId, "statusId", "REQ_ORDERED", "quantity", orderItem.getBigDecimal("quantity")); |
| // TODO: check service result for an error return |
| dispatcher.runSync("updateRequirement", inputMap); |
| } |
| } |
| } catch(GenericEntityException e){ |
| Debug.logError(e, module); |
| } catch(GenericServiceException e){ |
| Debug.logError(e, module); |
| } |
| return ServiceUtil.returnSuccess(); |
| } |
| } |
| |