001: /*
002: * $Id: InventoryServices.java,v 1.4 2004/02/23 15:36:15 jonesde Exp $
003: *
004: * Copyright (c) 2001, 2002 The Open For Business Project - www.ofbiz.org
005: *
006: * Permission is hereby granted, free of charge, to any person obtaining a
007: * copy of this software and associated documentation files (the "Software"),
008: * to deal in the Software without restriction, including without limitation
009: * the rights to use, copy, modify, merge, publish, distribute, sublicense,
010: * and/or sell copies of the Software, and to permit persons to whom the
011: * Software is furnished to do so, subject to the following conditions:
012: *
013: * The above copyright notice and this permission notice shall be included
014: * in all copies or substantial portions of the Software.
015: *
016: * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
017: * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
018: * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
019: * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
020: * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
021: * OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
022: * THE USE OR OTHER DEALINGS IN THE SOFTWARE.
023: */
024: package org.ofbiz.product.inventory;
025:
026: import java.sql.Timestamp;
027: import java.util.ArrayList;
028: import java.util.Calendar;
029: import java.util.HashMap;
030: import java.util.Iterator;
031: import java.util.List;
032: import java.util.Map;
033: import java.util.Set;
034:
035: import org.ofbiz.base.util.Debug;
036: import org.ofbiz.base.util.UtilDateTime;
037: import org.ofbiz.base.util.UtilMisc;
038: import org.ofbiz.entity.GenericDelegator;
039: import org.ofbiz.entity.GenericEntityException;
040: import org.ofbiz.entity.GenericValue;
041: import org.ofbiz.entity.condition.EntityExpr;
042: import org.ofbiz.entity.condition.EntityOperator;
043: import org.ofbiz.service.DispatchContext;
044: import org.ofbiz.service.GenericServiceException;
045: import org.ofbiz.service.LocalDispatcher;
046: import org.ofbiz.service.ServiceUtil;
047:
048: /**
049: * Inventory Services
050: *
051: * @author <a href="mailto:jaz@ofbiz.org">Andy Zeneski</a>
052: * @author <a href="mailto:jonesde@ofbiz.org">David E. Jones</a>
053: * @author <a href="mailto:tiz@sastau.it">Jacopo Cappellato</a>
054: * @version $Revision: 1.4 $
055: * @since 2.0
056: */
057: public class InventoryServices {
058:
059: public final static String module = InventoryServices.class
060: .getName();
061:
062: public static Map prepareInventoryTransfer(DispatchContext dctx,
063: Map context) {
064: GenericDelegator delegator = dctx.getDelegator();
065: String inventoryItemId = (String) context
066: .get("inventoryItemId");
067: Double xferQty = (Double) context.get("xferQty");
068: GenericValue inventoryItem = null;
069: GenericValue newItem = null;
070:
071: try {
072: inventoryItem = delegator.findByPrimaryKey("InventoryItem",
073: UtilMisc.toMap("inventoryItemId", inventoryItemId));
074: } catch (GenericEntityException e) {
075: return ServiceUtil
076: .returnError("Inventory item lookup problem ["
077: + e.getMessage() + "]");
078: }
079:
080: if (inventoryItem == null) {
081: return ServiceUtil
082: .returnError("Cannot locate inventory item.");
083: }
084:
085: String inventoryType = inventoryItem
086: .getString("inventoryItemTypeId");
087: if (inventoryType.equals("NON_SERIAL_INV_ITEM")) {
088: Double atp = inventoryItem.getDouble("availableToPromise");
089: Double qoh = inventoryItem.getDouble("quantityOnHand");
090:
091: if (atp == null) {
092: return ServiceUtil
093: .returnError("The request transfer amount is not available, there is no available to promise on the Inventory Item with ID "
094: + inventoryItem
095: .getString("inventoryItemId"));
096: }
097: if (qoh == null) {
098: qoh = atp;
099: }
100:
101: // first make sure we have enough to cover the request transfer amount
102: if (xferQty.doubleValue() > atp.doubleValue()) {
103: return ServiceUtil
104: .returnError("The request transfer amount is not available, the available to promise ["
105: + atp
106: + "] is not sufficient for the desired transfer quantity ["
107: + xferQty
108: + "] on the Inventory Item with ID "
109: + inventoryItem
110: .getString("inventoryItemId"));
111: }
112:
113: /*
114: * atp < qoh - split and save the qoh - atp
115: * xferQty < atp - split and save atp - xferQty
116: * atp < qoh && xferQty < atp - split and save qoh - atp + atp - xferQty
117: */
118:
119: // at this point we have already made sure that the xferQty is less than or equals to the atp, so if less that just create a new inventory record for the quantity to be moved
120: // NOTE: atp should always be <= qoh, so if xfer < atp, then xfer < qoh, so no need to check/handle that
121: // however, if atp < qoh && atp == xferQty, then we still need to split; oh, but no need to check atp == xferQty in the second part because if it isn't greater and isn't less, then it is equal
122: if (xferQty.doubleValue() < atp.doubleValue()
123: || atp.doubleValue() < qoh.doubleValue()) {
124:
125: newItem = new GenericValue(inventoryItem);
126: newItem.set("availableToPromise", xferQty);
127: newItem.set("quantityOnHand", xferQty);
128:
129: inventoryItem.set("availableToPromise", new Double(atp
130: .doubleValue()
131: - xferQty.doubleValue()));
132: inventoryItem.set("quantityOnHand", new Double(qoh
133: .doubleValue()
134: - xferQty.doubleValue()));
135: }
136: } else if (inventoryType.equals("SERIALIZED_INV_ITEM")) {
137: if (!inventoryItem.getString("statusId").equals(
138: "INV_AVAILABLE")) {
139: return ServiceUtil
140: .returnError("Serialized inventory is not available for transfer.");
141: }
142: }
143:
144: // setup values so that no one will grab the inventory during the move
145: // if newItem is not null, it is the item to be moved, otherwise the original inventoryItem is the one to be moved
146: if (inventoryType.equals("NON_SERIAL_INV_ITEM")) {
147: // set the transfered inventory item's atp to 0 and the qoh to the xferQty; at this point atp and qoh will always be the same, so we can safely zero the atp for now
148: if (newItem != null) {
149: newItem.set("availableToPromise", new Double(0.0));
150: } else {
151: inventoryItem
152: .set("availableToPromise", new Double(0.0));
153: }
154: } else if (inventoryType.equals("SERIALIZED_INV_ITEM")) {
155: // set the status to avoid re-moving or something
156: if (newItem != null) {
157: newItem.set("statusId", "INV_BEING_TRANSFERED");
158: } else {
159: inventoryItem.set("statusId", "INV_BEING_TRANSFERED");
160: }
161: }
162:
163: try {
164: Map results = ServiceUtil.returnSuccess();
165:
166: inventoryItem.store();
167: if (newItem != null) {
168: Long newSeqId = delegator.getNextSeqId("InventoryItem");
169: if (newSeqId == null) {
170: return ServiceUtil
171: .returnError("ERROR: Could not get next sequence id for InventoryItem, cannot create item.");
172: }
173:
174: newItem.set("inventoryItemId", newSeqId.toString());
175: newItem.create();
176:
177: results.put("inventoryItemId", newItem
178: .get("inventoryItemId"));
179: } else {
180: results.put("inventoryItemId", inventoryItem
181: .get("inventoryItemId"));
182: }
183: return results;
184: } catch (GenericEntityException e) {
185: return ServiceUtil
186: .returnError("Inventory store/create problem ["
187: + e.getMessage() + "]");
188: }
189: }
190:
191: public static Map completeInventoryTransfer(DispatchContext dctx,
192: Map context) {
193: GenericDelegator delegator = dctx.getDelegator();
194: String inventoryTransferId = (String) context
195: .get("inventoryTransferId");
196: GenericValue inventoryTransfer = null;
197: GenericValue inventoryItem = null;
198:
199: try {
200: inventoryTransfer = delegator
201: .findByPrimaryKey("InventoryTransfer", UtilMisc
202: .toMap("inventoryTransferId",
203: inventoryTransferId));
204: inventoryItem = inventoryTransfer
205: .getRelatedOne("InventoryItem");
206: } catch (GenericEntityException e) {
207: return ServiceUtil
208: .returnError("Inventory Item/Transfer lookup problem ["
209: + e.getMessage() + "]");
210: }
211:
212: if (inventoryTransfer == null || inventoryItem == null)
213: return ServiceUtil
214: .returnError("ERROR: Lookup of InventoryTransfer and/or InventoryItem failed!");
215:
216: String inventoryType = inventoryItem
217: .getString("inventoryItemTypeId");
218:
219: // set the fields on the transfer record
220: if (inventoryTransfer.get("receiveDate") == null)
221: inventoryTransfer.set("receiveDate", UtilDateTime
222: .nowTimestamp());
223:
224: // set the fields on the item
225: inventoryItem.set("facilityId", inventoryTransfer
226: .get("facilityIdTo"));
227: inventoryItem.set("containerId", inventoryTransfer
228: .get("containerIdTo"));
229: inventoryItem.set("locationSeqId", inventoryTransfer
230: .get("locationSeqIdTo"));
231:
232: if (inventoryType.equals("NON_SERIAL_INV_ITEM"))
233: inventoryItem.set("availableToPromise", inventoryItem
234: .get("quantityOnHand"));
235: else if (inventoryType.equals("SERIALIZED_INV_ITEM"))
236: inventoryItem.set("statusId", "INV_AVAILABLE");
237:
238: // store the entities
239: try {
240: inventoryTransfer.store();
241: inventoryItem.store();
242: } catch (GenericEntityException e) {
243: return ServiceUtil.returnError("Inventory store problem ["
244: + e.getMessage() + "]");
245: }
246:
247: return ServiceUtil.returnSuccess();
248: }
249:
250: public static Map cancelInventoryTransfer(DispatchContext dctx,
251: Map context) {
252: GenericDelegator delegator = dctx.getDelegator();
253: String inventoryTransferId = (String) context
254: .get("inventoryTransferId");
255: GenericValue inventoryTransfer = null;
256: GenericValue inventoryItem = null;
257:
258: try {
259: inventoryTransfer = delegator
260: .findByPrimaryKey("InventoryTransfer", UtilMisc
261: .toMap("inventoryTransferId",
262: inventoryTransferId));
263: inventoryItem = inventoryTransfer
264: .getRelatedOne("InventoryItem");
265: } catch (GenericEntityException e) {
266: return ServiceUtil
267: .returnError("Inventory Item/Transfer lookup problem ["
268: + e.getMessage() + "]");
269: }
270:
271: if (inventoryTransfer == null || inventoryItem == null)
272: return ServiceUtil
273: .returnError("ERROR: Lookup of InventoryTransfer and/or InventoryItem failed!");
274:
275: String inventoryType = inventoryItem
276: .getString("inventoryItemTypeId");
277:
278: // re-set the fields on the item
279: if (inventoryType.equals("NON_SERIAL_INV_ITEM"))
280: inventoryItem.set("availableToPromise", inventoryItem
281: .get("quantityOnHand"));
282: else if (inventoryType.equals("SERIALIZED_INV_ITEM"))
283: inventoryItem.set("statusId", "INV_AVAILABLE");
284:
285: // store the entity
286: try {
287: inventoryItem.store();
288: } catch (GenericEntityException e) {
289: return ServiceUtil
290: .returnError("Inventory item store problem ["
291: + e.getMessage() + "]");
292: }
293:
294: return ServiceUtil.returnSuccess();
295: }
296:
297: public static Map checkInventoryAvailability(DispatchContext dctx,
298: Map context) {
299: GenericDelegator delegator = dctx.getDelegator();
300: LocalDispatcher dispatcher = dctx.getDispatcher();
301: GenericValue userLogin = (GenericValue) context
302: .get("userLogin");
303:
304: Map ordersToUpdate = new HashMap();
305: Map ordersToCancel = new HashMap();
306:
307: // find all inventory items w/ a negative ATP
308: List inventoryItems = null;
309: try {
310: List exprs = UtilMisc.toList(new EntityExpr(
311: "availableToPromise", EntityOperator.LESS_THAN,
312: new Double(0)));
313: inventoryItems = delegator
314: .findByAnd("InventoryItem", exprs);
315: } catch (GenericEntityException e) {
316: Debug
317: .logError(e, "Trouble getting inventory items",
318: module);
319: return ServiceUtil
320: .returnError("Problem getting InventoryItem records");
321: }
322:
323: if (inventoryItems == null) {
324: Debug
325: .logInfo(
326: "No items out of stock; no backorders to worry about",
327: module);
328: return ServiceUtil.returnSuccess();
329: }
330:
331: Debug.log("OOS Inventory Items: " + inventoryItems.size(),
332: module);
333:
334: Iterator itemsIter = inventoryItems.iterator();
335: while (itemsIter.hasNext()) {
336: GenericValue inventoryItem = (GenericValue) itemsIter
337: .next();
338:
339: // get the incomming shipment information for the item
340: List shipmentAndItems = null;
341: try {
342: List exprs = new ArrayList();
343: exprs.add(new EntityExpr("productId",
344: EntityOperator.EQUALS, inventoryItem
345: .get("productId")));
346: exprs.add(new EntityExpr("destinationFacilityId",
347: EntityOperator.EQUALS, inventoryItem
348: .get("facilityId")));
349: exprs
350: .add(new EntityExpr("statusId",
351: EntityOperator.NOT_EQUAL,
352: "SHIPMENT_DELIVERED"));
353: exprs
354: .add(new EntityExpr("statusId",
355: EntityOperator.NOT_EQUAL,
356: "SHIPMENT_CANCELLED"));
357: shipmentAndItems = delegator.findByAnd(
358: "ShipmentAndItem", exprs, UtilMisc
359: .toList("estimatedArrivalDate"));
360: } catch (GenericEntityException e) {
361: Debug.logError(e,
362: "Problem getting ShipmentAndItem records",
363: module);
364: return ServiceUtil
365: .returnError("Problem getting ShipmentAndItem records");
366: }
367:
368: // get the reservations in order of newest first
369: List reservations = null;
370: try {
371: reservations = inventoryItem.getRelated(
372: "OrderItemInventoryRes", null, UtilMisc
373: .toList("-reservedDatetime"));
374: } catch (GenericEntityException e) {
375: Debug.logError(e,
376: "Problem getting related reservations", module);
377: return ServiceUtil
378: .returnError("Problem getting related reservations");
379: }
380:
381: if (reservations == null) {
382: Debug
383: .logWarning(
384: "No outstanding reservations for this inventory item, why is it negative then?",
385: module);
386: continue;
387: }
388:
389: Debug.log("Reservations for item: " + reservations.size(),
390: module);
391:
392: // available at the time of order
393: double availableBeforeReserved = inventoryItem.getDouble(
394: "availableToPromise").doubleValue();
395:
396: // go through all the reservations in order
397: Iterator ri = reservations.iterator();
398: while (ri.hasNext()) {
399: GenericValue reservation = (GenericValue) ri.next();
400: String orderId = reservation.getString("orderId");
401: String orderItemSeqId = reservation
402: .getString("orderItemSeqId");
403: Timestamp promisedDate = reservation
404: .getTimestamp("promisedDatetime");
405: Timestamp currentPromiseDate = reservation
406: .getTimestamp("currentPromisedDate");
407: Timestamp actualPromiseDate = currentPromiseDate;
408: if (actualPromiseDate == null) {
409: actualPromiseDate = promisedDate;
410: }
411:
412: Debug
413: .log("Promised Date: " + actualPromiseDate,
414: module);
415:
416: // find the next possible ship date
417: Timestamp nextShipDate = null;
418: double availableAtTime = 0.00;
419: Iterator si = shipmentAndItems.iterator();
420: while (si.hasNext()) {
421: GenericValue shipmentItem = (GenericValue) si
422: .next();
423: availableAtTime += shipmentItem.getDouble(
424: "quantity").doubleValue();
425: if (availableAtTime >= availableBeforeReserved) {
426: nextShipDate = shipmentItem
427: .getTimestamp("estimatedArrivalDate");
428: break;
429: }
430: }
431:
432: Debug.log("Next Ship Date: " + nextShipDate, module);
433:
434: // create a modified promise date (promise date - 1 day)
435: Calendar pCal = Calendar.getInstance();
436: pCal.setTimeInMillis(actualPromiseDate.getTime());
437: pCal.add(Calendar.DAY_OF_YEAR, -1);
438: Timestamp modifiedPromisedDate = new Timestamp(pCal
439: .getTimeInMillis());
440: Timestamp now = UtilDateTime.nowTimestamp();
441:
442: Debug.log("Promised Date + 1: " + modifiedPromisedDate,
443: module);
444: Debug.log("Now: " + now, module);
445:
446: // check the promised date vs the next ship date
447: if (nextShipDate == null
448: || nextShipDate.after(actualPromiseDate)) {
449: if (nextShipDate == null
450: && modifiedPromisedDate.after(now)) {
451: // do nothing; we are okay to assume it will be shipped on time
452: Debug
453: .log(
454: "No ship date known yet, but promised date hasn't approached, assuming it will be here on time",
455: module);
456: } else {
457: // we cannot ship by the promised date; need to notify the customer
458: Debug
459: .log(
460: "We won't ship on time, getting notification info",
461: module);
462: Map notifyItems = (Map) ordersToUpdate
463: .get(orderId);
464: if (notifyItems == null) {
465: notifyItems = new HashMap();
466: }
467: notifyItems.put(orderItemSeqId, nextShipDate);
468: ordersToUpdate.put(orderId, notifyItems);
469:
470: // need to know if nextShipDate is more then 30 days after promised
471: Calendar sCal = Calendar.getInstance();
472: sCal.setTimeInMillis(actualPromiseDate
473: .getTime());
474: sCal.add(Calendar.DAY_OF_YEAR, 30);
475: Timestamp farPastPromised = new Timestamp(sCal
476: .getTimeInMillis());
477:
478: // check to see if this is >30 days or second run, if so flag to cancel
479: boolean needToCancel = false;
480: if (nextShipDate == null
481: || nextShipDate.after(farPastPromised)) {
482: // we cannot ship until >30 days after promised; using cancel rule
483: Debug
484: .log(
485: "Ship date is >30 past the promised date",
486: module);
487: needToCancel = true;
488: }
489: if (currentPromiseDate != null
490: && actualPromiseDate
491: .equals(currentPromiseDate)) {
492: // this is the second notification; using cancel rule
493: needToCancel = true;
494: }
495:
496: // add the info to the cancel map if we need to schedule a cancel
497: if (needToCancel) {
498: // queue the item to be cancelled
499: Debug.log(
500: "Flagging the item to auto-cancel",
501: module);
502: Map cancelItems = (Map) ordersToCancel
503: .get(orderId);
504: if (cancelItems == null) {
505: cancelItems = new HashMap();
506: }
507: cancelItems.put(orderItemSeqId,
508: farPastPromised);
509: ordersToCancel.put(orderId, cancelItems);
510: }
511:
512: // store the updated promiseDate as the nextShipDate
513: try {
514: reservation.set("currentPromisedDate",
515: nextShipDate);
516: reservation.store();
517: } catch (GenericEntityException e) {
518: Debug.logError(e,
519: "Problem storing reservation : "
520: + reservation, module);
521: }
522: }
523: }
524:
525: // subtract our qty from reserved to get the next value
526: availableBeforeReserved -= reservation.getDouble(
527: "quantity").doubleValue();
528: }
529: }
530:
531: // all items to cancel will also be in the notify list so start with that
532: List ordersToNotify = new ArrayList();
533: Set orderSet = ordersToUpdate.keySet();
534: Iterator orderIter = orderSet.iterator();
535: while (orderIter.hasNext()) {
536: String orderId = (String) orderIter.next();
537: Map backOrderedItems = (Map) ordersToUpdate.get(orderId);
538: Map cancelItems = (Map) ordersToCancel.get(orderId);
539:
540: GenericValue orderShipPref = null;
541: List orderItems = null;
542: try {
543: orderShipPref = delegator.findByPrimaryKey(
544: "OrderShipmentPreference", UtilMisc.toMap(
545: "orderId", orderId, "orderItemSeqId",
546: "_NA_"));
547: orderItems = delegator.findByAnd("OrderItem", UtilMisc
548: .toMap("orderId", orderId));
549: } catch (GenericEntityException e) {
550: Debug
551: .logError(
552: e,
553: "Cannot get order shipment preference or items",
554: module);
555: }
556:
557: // check the split pref
558: boolean maySplit = false;
559: if (orderShipPref != null
560: && orderShipPref.get("maySplit") != null) {
561: maySplit = orderShipPref.getBoolean("maySplit")
562: .booleanValue();
563: }
564:
565: // figure out if we must cancel all items
566: boolean cancelAll = false;
567: Timestamp cancelAllTime = null;
568: if (!maySplit && cancelItems != null) {
569: cancelAll = true;
570: Set cancelSet = cancelItems.keySet();
571: cancelAllTime = (Timestamp) cancelItems.get(cancelSet
572: .iterator().next());
573: }
574:
575: // if there are none to cancel just create an empty map
576: if (cancelItems == null) {
577: cancelItems = new HashMap();
578: }
579:
580: if (orderItems != null) {
581: List toBeStored = new ArrayList();
582: Iterator orderItemsIter = orderItems.iterator();
583: while (orderItemsIter.hasNext()) {
584: GenericValue orderItem = (GenericValue) orderItemsIter
585: .next();
586: String orderItemSeqId = orderItem
587: .getString("orderItemSeqId");
588: Timestamp shipDate = (Timestamp) backOrderedItems
589: .get(orderItemSeqId);
590: Timestamp cancelDate = (Timestamp) cancelItems
591: .get(orderItemSeqId);
592: Timestamp currentCancelDate = (Timestamp) orderItem
593: .getTimestamp("autoCancelDate");
594:
595: if (backOrderedItems.containsKey(orderItemSeqId)) {
596: orderItem.set("estimatedShipDate", shipDate);
597:
598: if (currentCancelDate == null) {
599: if (cancelAll || cancelDate != null) {
600: if (orderItem
601: .get("dontCancelSetUserLogin") == null
602: && orderItem
603: .get("dontCancelSetDate") == null) {
604: if (cancelAllTime != null) {
605: orderItem.set("autoCancelDate",
606: cancelAllTime);
607: } else {
608: orderItem.set("autoCancelDate",
609: cancelDate);
610: }
611: }
612: }
613: // only notify orders which have not already sent the final notice
614: ordersToNotify.add(orderId);
615: }
616: toBeStored.add(orderItem);
617: }
618: }
619: if (toBeStored.size() > 0) {
620: try {
621: delegator.storeAll(toBeStored);
622: } catch (GenericEntityException e) {
623: Debug.logError(e,
624: "Problem storing order items", module);
625: }
626: }
627: }
628: }
629:
630: // send off a notification for each order
631: Iterator orderNotifyIter = ordersToNotify.iterator();
632: while (orderNotifyIter.hasNext()) {
633: String orderId = (String) orderNotifyIter.next();
634:
635: try {
636: dispatcher.runAsync("sendOrderBackorderNotification",
637: UtilMisc.toMap("orderId", orderId, "userLogin",
638: userLogin));
639: } catch (GenericServiceException e) {
640: Debug
641: .logError(
642: e,
643: "Problems sending off the notification",
644: module);
645: continue;
646: }
647: }
648:
649: return ServiceUtil.returnSuccess();
650: }
651: }
|