/** * 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); }