package crmdna.inventory;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.cmd.Query;
import com.googlecode.objectify.cmd.QueryKeys;
import crmdna.client.Client;
import crmdna.common.UnitUtils;
import crmdna.common.UnitUtils.PhysicalQuantity;
import crmdna.common.UnitUtils.ReportingUnit;
import crmdna.common.Utils;
import crmdna.common.Utils.Currency;
import crmdna.common.api.APIException;
import crmdna.common.api.APIResponse.Status;
import crmdna.sequence.Sequence;
import crmdna.sequence.Sequence.SequenceType;
import java.util.*;
import static crmdna.common.AssertUtils.ensure;
import static crmdna.common.AssertUtils.ensureNotNull;
import static crmdna.common.OfyService.ofy;
public class InventoryItemCore {
static InventoryItemProp create(String namespace, long groupId, long inventoryItemTypeId,
String displayName, PhysicalQuantity physicalQuantity, ReportingUnit reportingUnit) {
ensureNotNull(displayName, "Display name cannot be null");
ensure(displayName.length() > 0, "Display name cannot be empty");
// first character should be an alphabet
char c = displayName.toLowerCase().charAt(0);
ensure(c >= 'a' && c <= 'z', "First character should be an alphabet");
UnitUtils.ensureValidReportingUnit(physicalQuantity, reportingUnit);
String name = Utils.removeSpaceUnderscoreBracketAndHyphen(displayName.toLowerCase());
List<Key<InventoryItemEntity>> keys =
ofy(namespace).load().type(InventoryItemEntity.class).filter("name", name)
.filter("groupId", groupId).keys().list();
if (keys.size() != 0)
throw new APIException().status(Status.ERROR_RESOURCE_ALREADY_EXISTS).message(
"There is already an inventory item with the same name for group [" + groupId + "]");
String key = getUniqueKey(namespace, groupId, name);
long val = MemcacheServiceFactory.getMemcacheService().increment(key, 1, (long) 0);
if (val != 1)
throw new APIException().status(Status.ERROR_RESOURCE_ALREADY_EXISTS).message(
"There is already a inventory item with the same name (in cache) for group [" + groupId
+ "]");
InventoryItemEntity entity = new InventoryItemEntity();
entity.inventoryItemId = Sequence.getNext(namespace, SequenceType.INVENTORY_ITEM);
entity.groupId = groupId;
entity.displayName = displayName;
entity.physicalQuantity = physicalQuantity;
entity.inventoryItemTypeId = inventoryItemTypeId;
entity.groupId = groupId;
entity.reportingUnit = reportingUnit;
// populate dependents
entity.name = name;
entity.firstChar = entity.name.substring(0, 1);
ofy(namespace).save().entity(entity).now();
return entity.toProp();
}
static InventoryItemProp update(String namespace, long inventoryItemId,
Long newInventoryItemTypeId, String newDisplayName, ReportingUnit newReportingUnit) {
InventoryItemEntity entity = safeGet(namespace, inventoryItemId);
String name = null;
if (newDisplayName != null) {
ensure(newDisplayName.length() > 0, "Display name cannot be empty");
char c = newDisplayName.toLowerCase().charAt(0);
ensure(c >= 'a' && c <= 'z', "First character of display name should be an alphabet");
name = Utils.removeSpaceUnderscoreBracketAndHyphen(newDisplayName.toLowerCase());
if (!entity.name.equals(name)) {
// ensure name doesn't clash with another existing entity in the
// same group
List<Key<InventoryItemEntity>> keys =
ofy(namespace).load().type(InventoryItemEntity.class).filter("name", name)
.filter("groupId", entity.groupId).keys().list();
if (keys.size() != 0)
throw new APIException().status(Status.ERROR_RESOURCE_ALREADY_EXISTS).message(
"There is already an inventory item with the same name for group [" + entity.groupId
+ "]");
String key = getUniqueKey(namespace, entity.groupId, name);
long val = MemcacheServiceFactory.getMemcacheService().increment(key, 1, (long) 0);
if (val != 1) {
throw new APIException().status(Status.ERROR_RESOURCE_ALREADY_EXISTS).message(
"There is already a inventory item with the same name for group [" + entity.groupId
+ "]");
}
}
}
if (newInventoryItemTypeId != null) {
InventoryItemType.safeGet(namespace, newInventoryItemTypeId);
}
if (newReportingUnit != null) {
UnitUtils.ensureValidReportingUnit(entity.physicalQuantity, newReportingUnit);
}
// all ok. populate and save
if (newDisplayName != null) {
entity.displayName = newDisplayName;
entity.name = name;
entity.firstChar = entity.name.substring(0, 1);
}
if (newInventoryItemTypeId != null)
entity.inventoryItemTypeId = newInventoryItemTypeId;
if (newReportingUnit != null)
entity.reportingUnit = newReportingUnit;
ofy(namespace).save().entity(entity).now();
return entity.toProp();
}
private static String getUniqueKey(String namespace, long groupId, String name) {
return namespace + "_" + SequenceType.INVENTORY_ITEM + "_" + groupId + "_" + name;
}
static InventoryItemEntity safeGet(String namespace, long inventoryItemId) {
InventoryItemEntity entity =
ofy(namespace).load().type(InventoryItemEntity.class).id(inventoryItemId).now();
if (null == entity)
throw new APIException().status(Status.ERROR_RESOURCE_NOT_FOUND).message(
"Inventory item id [" + inventoryItemId + "] does not exist");
return entity;
}
static InventoryItemEntity safeGetByName(String namespace, String name) {
ensureNotNull(name);
name = Utils.removeSpaceUnderscoreBracketAndHyphen(name.toLowerCase());
List<InventoryItemEntity> entities =
ofy(namespace).load().type(InventoryItemEntity.class).filter("name", name).list();
if (entities.size() == 0)
throw new APIException().status(Status.ERROR_RESOURCE_NOT_FOUND).message(
"Inventory item [" + name + "] does not exist");
if (entities.size() > 1)
throw new APIException().status(Status.ERROR_RESOURCE_INCORRECT).message(
"Found [" + entities.size() + "] matches for inventory item [" + name
+ "]. Please specify Id");
return entities.get(0);
}
static Map<Long, InventoryItemEntity> get(String namespace, Set<Long> inventoryItemIds) {
Map<Long, InventoryItemEntity> map =
ofy(namespace).load().type(InventoryItemEntity.class).ids(inventoryItemIds);
return map;
}
static List<InventoryItemProp> query(String namespace, InventoryItemQueryCondition qc) {
List<Key<InventoryItemEntity>> keys = queryKeys(namespace, qc).list();
ensureNotNull(keys);
Collection<InventoryItemEntity> entities = ofy(namespace).load().keys(keys).values();
List<InventoryItemProp> props = new ArrayList<>(keys.size());
for (InventoryItemEntity entity : entities) {
props.add(entity.toProp());
}
InventoryItemProp.populateDependents(namespace, props);
Collections.sort(props);
return props;
}
static QueryKeys<InventoryItemEntity> queryKeys(String namespace, InventoryItemQueryCondition qc) {
ensureNotNull(qc);
Query<InventoryItemEntity> query = ofy(namespace).load().type(InventoryItemEntity.class);
if (qc.groupId != null) {
query = query.filter("groupId", qc.groupId);
}
if ((qc.inventoryItemTypeIds != null) && !qc.inventoryItemTypeIds.isEmpty()) {
query = query.filter("inventoryItemTypeId in", qc.inventoryItemTypeIds);
}
if ((qc.firstChars != null) && !qc.firstChars.isEmpty()) {
query = query.filter("firstChar in", qc.firstChars);
}
return query.keys();
}
static Map<Long, InventoryItemEntity> queryEntities(String namespace,
InventoryItemQueryCondition qc) {
List<Key<InventoryItemEntity>> keys = InventoryItemCore.queryKeys(namespace, qc).list();
ensureNotNull(keys);
Set<Long> ids = new HashSet<>();
for (Key<InventoryItemEntity> key : keys) {
long id = key.getId();
ids.add(id);
}
Map<Long, InventoryItemEntity> map =
ofy(namespace).load().type(InventoryItemEntity.class).ids(ids);
return map;
}
static InventoryCheckInProp checkIn(String namespace, long inventoryItemId, Date date,
double qtyInReportingUnit, ReportingUnit reportingUnit, double pricePerReportingUnit,
Currency ccy, String changeDescription, String login) {
InventoryItemEntity inventoryItemEntity = InventoryItemCore.safeGet(namespace, inventoryItemId);
ensure(qtyInReportingUnit > 0, "invalid quantityInReporting [" + qtyInReportingUnit + "]");
double qtyInDefaultUnit =
UnitUtils.safeGetQtyInDefaultUnit(inventoryItemEntity.physicalQuantity, qtyInReportingUnit,
reportingUnit);
ensure(pricePerReportingUnit >= 0, "invalid pricePerReportingUnit [" + pricePerReportingUnit
+ "]");
InventoryCheckInEntity inventoryCheckInEntity = new InventoryCheckInEntity();
inventoryCheckInEntity.checkInId = Sequence.getNext(namespace, SequenceType.INVENTORY_CHECKIN);
if (date == null)
inventoryCheckInEntity.ms = new Date().getTime();
else
inventoryCheckInEntity.ms = date.getTime();
inventoryCheckInEntity.availableQtyInDefaultUnit = qtyInDefaultUnit;
inventoryCheckInEntity.qtyInDefaultUnit = qtyInDefaultUnit;
inventoryCheckInEntity.available = true;
inventoryCheckInEntity.ccy = ccy;
inventoryCheckInEntity.inventoryItemId = inventoryItemId;
inventoryCheckInEntity.pricePerDefaultUnit =
UnitUtils.safeGetPricePerDefaultUnit(inventoryItemEntity.physicalQuantity,
pricePerReportingUnit, reportingUnit);
inventoryCheckInEntity.login = login;
ofy(namespace).save().entity(inventoryCheckInEntity).now();
return inventoryCheckInEntity.toProp(UnitUtils
.getDefaultUnit(inventoryItemEntity.physicalQuantity));
}
private static Map<Long, Double> getAvailableQtyInDefaultUnit(
List<InventoryCheckInEntity> entities) {
HashMap<Long, Double> map = new HashMap<Long, Double>();
for (InventoryCheckInEntity entity : entities) {
if (!map.containsKey(entity.inventoryItemId))
map.put(entity.inventoryItemId, 0.0);
double value = map.get(entity.inventoryItemId);
value += entity.availableQtyInDefaultUnit;
map.put(entity.inventoryItemId, value);
}
return map;
}
static Map<Long, QuantityPriceProp> getAvailableQtyAndAvgPrice(String namespace,
Set<Long> inventoryItemIds) {
HashMap<Long, QuantityPriceProp> map = new HashMap<>();
List<InventoryCheckInEntity> checkIns =
getCheckInsWithAvailableQtyFIFO(namespace, inventoryItemIds);
Map<Long, List<InventoryCheckInEntity>> entityMap = new HashMap<>();
for (InventoryCheckInEntity entity : checkIns) {
long key = entity.inventoryItemId;
if (!entityMap.containsKey(key))
entityMap.put(key, new ArrayList<InventoryCheckInEntity>());
List<InventoryCheckInEntity> list = entityMap.get(key);
ensureNotNull(list);
list.add(entity);
}
for (Long inventoryItemId : entityMap.keySet()) {
QuantityPriceProp prop = new QuantityPriceProp();
List<Double> prices = new ArrayList<>();
List<Double> quantities = new ArrayList<>();
List<InventoryCheckInEntity> list = entityMap.get(inventoryItemId);
for (InventoryCheckInEntity entity : list) {
prices.add(entity.pricePerDefaultUnit);
quantities.add(entity.availableQtyInDefaultUnit);
prop.availableQtyInDefaultUnit += entity.availableQtyInDefaultUnit;
// TODO: handle case where currencies are different
prop.ccy = entity.ccy;
}
prop.avgPricePerDefaultUnit = Utils.getWeightedAvg(prices, quantities);
map.put(inventoryItemId, prop);
}
return map;
}
private static List<InventoryCheckInEntity> getCheckInsWithAvailableQtyFIFO(String namespace,
Set<Long> inventoryItemIds) {
if ((inventoryItemIds == null) || inventoryItemIds.isEmpty())
return new ArrayList<>();
List<InventoryCheckInEntity> list =
ofy(namespace).load().type(InventoryCheckInEntity.class).filter("available", true)
.filter("inventoryItemId in", inventoryItemIds).list();
Collections.sort(list);
return list;
}
static InventoryCheckOutProp checkOut(String namespace, final long inventoryItemId, Date date,
double qtyInReportingUnit, final ReportingUnit reportingUnit, Double pricePerReportingUnit,
Currency ccy, String comment, Set<String> tags, String login) {
InventoryItemEntity inventoryItemEntity = safeGet(namespace, inventoryItemId);
ensureNotNull(reportingUnit, "reportingUnit is null");
ensure(qtyInReportingUnit > 0, "qtyInReportingUnit [" + qtyInReportingUnit
+ "] is negative or zero");
List<InventoryCheckInEntity> checkIns =
getCheckInsWithAvailableQtyFIFO(namespace, Utils.getSet(inventoryItemId));
Map<Long, Double> map = getAvailableQtyInDefaultUnit(checkIns);
if (!map.containsKey(inventoryItemId))
throw new APIException().status(Status.ERROR_RESOURCE_NOT_FOUND).message(
"Nothing available to checkout");
double availableQtyInReportingUnit =
UnitUtils.safeGetQtyInReportingUnit(inventoryItemEntity.physicalQuantity,
map.get(inventoryItemId), reportingUnit);
if (qtyInReportingUnit > availableQtyInReportingUnit) {
throw new APIException().status(Status.ERROR_RESOURCE_NOT_FOUND).message(
"Only [" + availableQtyInReportingUnit + "] " + reportingUnit + " available");
}
double qtyInDefaultUnit =
UnitUtils.safeGetQtyInDefaultUnit(inventoryItemEntity.physicalQuantity, qtyInReportingUnit,
reportingUnit);
List<InventoryCheckInEntity> toSave = new ArrayList<>();
List<Double> prices = new ArrayList<>();
List<Double> quantities = new ArrayList<>();
InventoryCheckOutEntity checkOut = new InventoryCheckOutEntity();
checkOut.checkOutId = Sequence.getNext(namespace, SequenceType.INVENTORY_CHECKOUT);
if (date == null)
checkOut.ms = new Date().getTime();
else
checkOut.ms = date.getTime();
checkOut.qtyInDefaultUnit = qtyInDefaultUnit;
checkOut.ccy = ccy;
checkOut.inventoryItemId = inventoryItemId;
checkOut.login = login;
if ((tags != null) && !tags.isEmpty())
checkOut.tags = tags;
for (InventoryCheckInEntity checkIn : checkIns) {
ensure(qtyInDefaultUnit >= 0, "qtyInDefaultUnit [" + qtyInDefaultUnit + "] is less than 0");
if (qtyInDefaultUnit == 0) // break out of loop
break;
double qtyCheckedOutInDefaultUnit =
Math.min(qtyInDefaultUnit, checkIn.availableQtyInDefaultUnit);
ensure(qtyCheckedOutInDefaultUnit > 0, "qtyCheckedOutInDefaultUnit ["
+ qtyCheckedOutInDefaultUnit + "] is less than 0");
prices.add(checkIn.pricePerDefaultUnit);
quantities.add(qtyCheckedOutInDefaultUnit);
checkIn.availableQtyInDefaultUnit -= qtyCheckedOutInDefaultUnit;
ensure(checkIn.availableQtyInDefaultUnit >= 0, "checkIn.availableQtyInDefaultUnit ["
+ checkIn.availableQtyInDefaultUnit + "] is less than 0");
if (checkIn.availableQtyInDefaultUnit == 0)
checkIn.available = false;
qtyInDefaultUnit -= qtyCheckedOutInDefaultUnit;
toSave.add(checkIn);
CheckOutDetail checkOutDetail = new CheckOutDetail();
checkOutDetail.checkInPricePerDefaultUnit = checkIn.pricePerDefaultUnit;
checkOutDetail.checkInCcy = checkIn.ccy;
checkOutDetail.checkOutCcy = ccy;
checkOutDetail.qtyInDefaultUnit = qtyCheckedOutInDefaultUnit;
if (pricePerReportingUnit != null)
checkOutDetail.checkOutPricePerDefaultUnit = pricePerReportingUnit;
else {
checkOutDetail.checkOutPricePerDefaultUnit = checkIn.pricePerDefaultUnit;
}
checkOut.checkOutDetails.add(checkOutDetail);
}
// find weighted average of price
if (pricePerReportingUnit == null) {
// checkout at checkin price
checkOut.avgPricePerDefaultUnit = Utils.getWeightedAvg(prices, quantities);
} else {
checkOut.avgPricePerDefaultUnit =
UnitUtils.safeGetPricePerDefaultUnit(inventoryItemEntity.physicalQuantity,
pricePerReportingUnit, reportingUnit);
}
ofy(namespace).save().entities(toSave);
ofy(namespace).save().entity(checkOut);
return checkOut.toProp(inventoryItemEntity.physicalQuantity, inventoryItemEntity.reportingUnit);
}
static void delete(String namespace, long inventoryItemId) {
Client.ensureValid(namespace);
InventoryItemEntity entity = safeGet(namespace, inventoryItemId);
ofy(namespace).delete().entity(entity).now();
}
public enum CheckInOrOut {
CHECK_IN, CHECK_OUT
}
}