/**
   * Removes first the inoutLines with {@link I_M_InOutLine#getQtyEntered()} = 0 and afterwards the
   * inouts that have no more lines.
   */
  public void purgeLinesEmpty() {
    int rmInOuts = 0, rmInOutLines = 0;
    // removing empty inOutLines
    for (final I_M_InOut inOut : getCandidates()) {
      for (final I_M_InOutLine inOutLine : getLines(inOut)) {
        if (inOutLine.getMovementQty().signum() <= 0) {
          removeLine(inOutLine);
          rmInOutLines++;
        }
      }
    }

    // removing empty inOuts
    for (final I_M_InOut inOut : getCandidates()) {
      if (hasNoLines(inOut)) {
        final ArrayKey key =
            Util.mkKey(
                inOut.getBPartnerAddress(), inOut.getM_Warehouse_ID(), inOut.getM_Shipper_ID());
        shipperKey2Candidate.remove(key);
        inOut2Line.remove(inOut);
        orderedCandidates.remove(inOut);
        rmInOuts++;
      }
    }
    logger.info(
        "Removed " + rmInOuts + " MInOut instances and " + rmInOutLines + " MInOutLine instances");
  }
  public void removeLine(final I_M_InOutLine inOutLine) {
    if (inOutLine == null) {
      throw new NullPointerException("inOutLine");
    }

    final MInOutLine inOutLinePO = getPO(inOutLine);
    final MInOut inOutPO = line2InOut.remove(inOutLinePO);
    if (inOutPO == null) {
      throw new IllegalStateException("inOutLine wasn't in line2InOut");
    }

    boolean success = inOut2Line.get(inOutPO).remove(inOutLine);
    if (!success) {
      throw new IllegalStateException("inOutLine wasn't in inOut2Line");
    }

    final int orderLineId = inOutLine.getC_OrderLine_ID();
    success = orderLineId2InOutLine.remove(orderLineId) != null;
    if (!success) {
      throw new IllegalStateException(
          "inOutLine wasn't in orderLineId2InOutLine."
              + "\n orderLineId2InOutLine: "
              + orderLineId2InOutLine);
    }

    success = line2sched.remove(inOutLinePO) != null;
    if (!success) {
      throw new IllegalStateException(
          "inOutLine wasn't in line2sched" + "\n line2sched: " + line2sched);
    }
  }
  /**
   * Updated all inoutLines' PostageFreeStatus according to the respective bPartner's postage free
   * amount.
   *
   * @param ignorePostageFreeAmt if true, the lines qty is not set to {@link BigDecimal#ZERO}, even
   *     if we are below the customer's postage free amount. However, the status is still set.
   */
  @Override
  public void updatePostageFreeStatus(final boolean ignorePostageFreeAmt) {
    for (final I_M_InOut shipmentCandidate : getCandidates()) {
      final I_C_BPartner bPartner =
          InterfaceWrapperHelper.create(shipmentCandidate.getC_BPartner(), I_C_BPartner.class);

      final BigDecimal postageFree = (BigDecimal) bPartner.getPostageFreeAmt();

      BigDecimal shipmentValue = BigDecimal.ZERO;

      // if ignorePostageFreeAmount is false and a postage free amount
      // is set, we need to check if the value of this shipment is
      // enough to make shipping profitable
      boolean sufficiantValue = postageFree == null;

      if (!sufficiantValue) {
        for (final I_M_InOutLine inOutLine : getLines(shipmentCandidate)) {

          // access orderLineCache instead of loading the line
          // from DB
          final MOrderLine orderLinePO = this.orderLineCache.get(inOutLine.getC_OrderLine_ID());
          final BigDecimal lineValue =
              orderLinePO.getPriceActual().multiply(inOutLine.getQtyEntered());

          shipmentValue = shipmentValue.add(lineValue);

          if (shipmentValue.compareTo(postageFree) >= 0) {
            sufficiantValue = true;
            break;
          }
        }
      }
      for (final I_M_InOutLine inOutLine : getLines(shipmentCandidate)) {
        final PostageFreeStatus status;

        if (sufficiantValue) {
          status = PostageFreeStatus.OK;
        } else {
          if (!ignorePostageFreeAmt) {
            inOutLine.setQtyEntered(BigDecimal.ZERO);
            inOutLine.setMovementQty(BigDecimal.ZERO);
          }

          status = PostageFreeStatus.BELOW_POSTAGEFREE_AMT;

          if (logger.isInfoEnabled()) {
            logger.info(
                "Shipment "
                    + shipmentCandidate.getDocumentNo()
                    + " has an insufficient value of "
                    + shipmentValue.toPlainString()
                    + " (minimum value is "
                    + postageFree.toPlainString()
                    + ")");
          }
        }

        final MInOutLine inOutLinePO = getPO(inOutLine);
        line2PostageFreeStatus.put(inOutLinePO, status);
      }
    }
  }
  @Override
  public void addLine(
      final I_M_InOut inOut,
      final I_M_InOutLine inOutLine,
      final I_M_ShipmentSchedule sched,
      final CompleteStatus completeStatus,
      final I_C_Order order) {
    Check.assumeNotNull(inOut, "inOut not null");
    Check.assumeNotNull(inOutLine, "inOutLine not null");
    Check.assumeNotNull(sched, "sched not null");
    Check.assumeNotNull(completeStatus, "completeStatus not null");

    if (!orderedCandidates.contains(inOut)) {
      throw new IllegalStateException(
          "inOut needs to be added using 'addInOut' first"
              + "\n InOut: "
              + inOut
              + "\n orderedCandidates: "
              + orderedCandidates);
    }
    if (inOutLine.getC_OrderLine_ID() < 1) {
      throw new IllegalStateException(
          "inOutLine needs to have a C_OrderLine_ID > 0" + "\n inOutLine: " + inOutLine);
    }
    if (CompleteStatus.INCOMPLETE_ORDER.equals(completeStatus)) {
      throw new IllegalArgumentException(
          "completeStatus may not be "
              + CompleteStatus.INCOMPLETE_ORDER
              + " (this will be figured out later by this class)");
    }

    final MInOutLine inOutLinePO = getPO(inOutLine);
    final MInOut inOutPO = getPO(inOut);

    inOut2Line.get(inOutPO).add(inOutLinePO);
    line2InOut.put(inOutLinePO, inOutPO);

    // store the inoutLine's orderId and status as well to support our
    // later purge
    line2CompleteStatus.put(inOutLinePO, completeStatus);
    line2PostageFreeStatus.put(inOutLinePO, PostageFreeStatus.OK);

    final MOrder orderPO = InterfaceWrapperHelper.getPO(order);

    Set<MInOutLine> inOutLines = order2InOutLine.get(orderPO);
    if (inOutLines == null) {
      inOutLines = new HashSet<MInOutLine>();
      order2InOutLine.put(orderPO, inOutLines);
    }
    inOutLines.add(inOutLinePO);

    //
    // C_OrderLine_ID to M_InOutLine mapping
    {
      final MInOutLine inOutLinePO_Old =
          orderLineId2InOutLine.put(inOutLine.getC_OrderLine_ID(), inOutLinePO);
      if (inOutLinePO_Old != null && inOutLinePO_Old != inOutLinePO) {
        throw new IllegalArgumentException(
            "An InOutLine was already set for order line in orderLineId2InOutLine mapping"
                + "\n InOutLine: "
                + inOutLinePO
                + "\n InOutLine (old): "
                + inOutLinePO_Old
                + "\n orderLineId2InOutLine (after change): "
                + orderLineId2InOutLine);
      }
    }

    line2sched.put(inOutLinePO, sched);
  }