/**
  * prepares the relations and finds all billable accountables for the invoice
  * 
  * @param Tinebase_Record_RecordSet $productAggregates
  * @return array
  */
 protected function _prepareInvoiceRelationsAndFindBillableAccountables($productAggregates)
 {
     $modelsToBill = array();
     $billedRelations = array();
     $simpleProductsToBill = array();
     $modelsToSkip = array();
     // this holds all relations for the invoice
     $relations = array();
     $billableAccountables = array();
     // iterate product aggregates to get the billing definition for the models
     foreach ($productAggregates as $productAggregate) {
         // is null, if this is the first time to bill the product aggregate
         $lastBilled = $productAggregate->last_autobill == NULL ? NULL : clone $productAggregate->last_autobill;
         $productEnded = false;
         $endDate = NULL;
         if (NULL != $productAggregate->end_date) {
             $endDate = clone $productAggregate->end_date;
         }
         if ($this->_currentBillingContract->end_date != NULL && (NULL === $endDate || $endDate->isLater($this->_currentBillingContract->end_date))) {
             $endDate = clone $this->_currentBillingContract->end_date;
         }
         // if the product has been billed already, add the interval
         if ($lastBilled) {
             $nextBill = $lastBilled;
             $nextBill->setDate($nextBill->format('Y'), $nextBill->format('m'), 1);
             $nextBill->addMonth($productAggregate->interval);
             if (NULL !== $endDate && $endDate->isEarlier($nextBill)) {
                 if ($productAggregate->billing_point == 'end') {
                     if ($productAggregate->last_autobill->isEarlier($endDate)) {
                         // ok, fix nextBill to be close to endDate
                         $nextBill = $endDate;
                         $nextBill->setDate($nextBill->format('Y'), $nextBill->format('m'), 1);
                         $nextBill->addMonth(1);
                     } else {
                         // not ok, ignore
                         $productEnded = true;
                     }
                 } else {
                     // not ok, ignore
                     $productEnded = true;
                 }
             }
         } else {
             // it hasn't been billed already
             if (NULL != $productAggregate->start_date && $productAggregate->start_date->isLaterOrEquals($this->_currentBillingContract->start_date)) {
                 $nextBill = clone $productAggregate->start_date;
             } else {
                 $nextBill = clone $this->_currentBillingContract->start_date;
             }
             $nextBill->setDate($nextBill->format('Y'), $nextBill->format('m'), 1);
             // add interval, if the billing point is at the end of the interval
             if ($productAggregate->billing_point == 'end') {
                 $nextBill->addMonth($productAggregate->interval);
                 if (NULL !== $endDate && $endDate->isEarlier($nextBill)) {
                     // ok, fix nextBill to be close to endDate
                     $nextBill = $endDate;
                     $nextBill->setDate($nextBill->format('Y'), $nextBill->format('m'), 1);
                     $nextBill->addMonth(1);
                 }
             }
         }
         $nextBill->setTime(0, 0, 0);
         $product = $this->_cachedProducts->getById($productAggregate->product_id);
         if (!$product) {
             $product = Sales_Controller_Product::getInstance()->get($productAggregate->product_id);
             $this->_cachedProducts->addRecord($product);
         }
         // find out if this model has to be billed or skipped
         if (!$productEnded && $this->_currentMonthToBill->isLaterOrEquals($nextBill)) {
             if ($product->accountable == 'Sales_Model_Product' || $product->accountable == '') {
                 $simpleProductsToBill[] = array('pa' => $productAggregate, 'ac' => $productAggregate);
             } else {
                 if ($productAggregate->json_attributes && isset($productAggregate->json_attributes['assignedAccountables']) && is_array($productAggregate->json_attributes['assignedAccountables']) && count($productAggregate->json_attributes['assignedAccountables'])) {
                     foreach ($productAggregate->json_attributes['assignedAccountables'] as $relationId) {
                         $billedRelations[$relationId] = true;
                         $relation = $this->_currentBillingContract->relations->getById($relationId);
                         if (false === $relation || $product->accountable != $relation->related_model) {
                             throw new Tinebase_Exception_UnexpectedValue('couldnt resolved assignedAccountables');
                         }
                         $relations[] = array_merge(array('related_model' => $relation->related_model, 'related_id' => $relation->related_id, 'related_record' => $relation->related_record->toArray()), $this->_getRelationDefaults());
                         $billableAccountables[] = array('ac' => $relation->related_record, 'pa' => $productAggregate);
                     }
                 } else {
                     $modelsToBill[$product->accountable] = $productAggregate;
                 }
             }
         } else {
             $modelsToSkip[] = $product->accountable;
         }
     }
     // iterate relations, look for accountables, prepare relations
     foreach ($this->_currentBillingContract->relations as $relation) {
         if (isset($billedRelations[$relation->id])) {
             continue;
         }
         // use productaggregate definition, if it has been found
         if (isset($modelsToBill[$relation->related_model]) && !in_array($relation->related_model, $modelsToSkip)) {
             $relations[] = array_merge(array('related_model' => $relation->related_model, 'related_id' => $relation->related_id, 'related_record' => $relation->related_record->toArray()), $this->_getRelationDefaults());
             $billableAccountables[] = array('ac' => $relation->related_record, 'pa' => $modelsToBill[$relation->related_model]);
         } elseif (!in_array($relation->related_model, $modelsToSkip) && in_array('Sales_Model_Accountable_Interface', class_implements($relation->related_model))) {
             // no product aggregate definition has been found -> use default values
             $relations[] = array_merge(array('related_model' => $relation->related_model, 'related_id' => $relation->related_id, 'related_record' => $relation->related_record->toArray()), $this->_getRelationDefaults());
             $billableAccountables[] = array('ac' => $relation->related_record, 'pa' => $relation->related_record->getDefaultProductAggregate($this->_currentBillingContract));
         }
     }
     foreach ($simpleProductsToBill as $product) {
         $relations[] = array_merge(array('related_model' => 'Sales_Model_ProductAggregate', 'related_id' => $product['pa']->getId(), 'related_record' => $product['pa']->toArray()), $this->_getRelationDefaults());
         $billableAccountables[] = $product;
     }
     // add contract relation
     $relations[] = array('own_model' => 'Sales_Model_Invoice', 'own_backend' => Tasks_Backend_Factory::SQL, 'own_id' => NULL, 'related_degree' => Tinebase_Model_Relation::DEGREE_SIBLING, 'related_model' => 'Sales_Model_Contract', 'related_backend' => Tasks_Backend_Factory::SQL, 'related_id' => $this->_currentBillingContract->getId(), 'related_record' => $this->_currentBillingContract->toArray(), 'type' => 'CONTRACT');
     // add customer relation
     $relations[] = array('own_model' => 'Sales_Model_Invoice', 'own_backend' => Tasks_Backend_Factory::SQL, 'own_id' => NULL, 'related_degree' => Tinebase_Model_Relation::DEGREE_SIBLING, 'related_model' => 'Sales_Model_Customer', 'related_backend' => Tasks_Backend_Factory::SQL, 'related_id' => $this->_currentBillingCustomer['id'], 'related_record' => $this->_currentBillingCustomer, 'type' => 'CUSTOMER');
     return array($relations, $billableAccountables);
 }
 /**
  * returns true if this record should be billed for the specified date
  * 
  * @param Tinebase_DateTime $date
  * @param Sales_Model_Contract $contract
  * @param Sales_Model_ProductAggregate $productAggregate
  * @return boolean
  */
 public function isBillable(Tinebase_DateTime $date, Sales_Model_Contract $contract = NULL, Sales_Model_ProductAggregate $productAggregate = NULL)
 {
     $this->_referenceContract = $contract;
     if (!$this->last_autobill) {
         if (!$this->start_date) {
             $nextBill = clone $contract->start_date;
         } else {
             $nextBill = clone $this->start_date;
         }
         $nextBill->setDate($nextBill->format('Y'), $nextBill->format('m'), 1);
         if ($this->billing_point == 'end') {
             $nextBill->addMonth($this->interval);
         }
     } else {
         $nextBill = clone $this->last_autobill;
         $nextBill->setDate($nextBill->format('Y'), $nextBill->format('m'), 1);
         $nextBill->addMonth($this->interval);
     }
     // if we are not already in user timezone we are in deep shit, add assertation rather instead or something
     $nextBill->setTimeZone(Tinebase_Core::getUserTimezone());
     $nextBill->setTime(0, 0, 0);
     return $date->isLaterOrEquals($nextBill);
 }