/**
  * the singleton pattern
  * @return Sales_Controller_InvoicePosition
  */
 public static function getInstance()
 {
     if (self::$_instance === NULL) {
         self::$_instance = new self();
     }
     return self::$_instance;
 }
 /**
  * if no productaggregates are defined for a contract, but 
  * accountables are related, use default billing Info from accountable
  * (product will be created if it does not exist - is needed in the invoice position)
  */
 public function testDefaultAutobillInterval()
 {
     $startDate = clone $this->_referenceDate;
     $startDate->subYear(1);
     $this->_createCustomers(1);
     $this->_createCostCenters();
     $this->_createTimeaccounts(array(array('title' => 'TA', 'description' => 'blabla', 'is_open' => 1, 'status' => 'to bill', 'budget' => 100)));
     $addressId = $this->_addressRecords->filter('customer_id', $this->_customerRecords->filter('name', 'Customer1')->getFirstRecord()->getId())->filter('type', 'billing')->getFirstRecord()->getId();
     // this contract begins 6 months before the first invoice will be created
     $this->_createContracts(array(array('number' => 100, 'title' => 'MyContract', 'description' => 'unittest', 'container_id' => $this->_sharedContractsContainerId, 'billing_point' => 'begin', 'billing_address_id' => $addressId, 'start_date' => $startDate, 'end_date' => NULL)));
     $startDate = clone $this->_referenceDate;
     $startDate->subMonth(1);
     $startDate = clone $this->_referenceDate;
     $startDate->addDay(5);
     $result = $this->_invoiceController->createAutoInvoices($startDate);
     $this->assertEquals(1, $result['created_count']);
     $filter = new Sales_Model_ProductFilter(array());
     $filter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'accountable', 'operator' => 'equals', 'value' => 'Timetracker_Model_Timeaccount')));
     $products = Sales_Controller_Product::getInstance()->search($filter);
     $this->assertEquals(1, $products->count());
     $this->assertEquals('Timetracker_Model_Timeaccount', $products->getFirstRecord()->accountable);
     $filter = new Sales_Model_InvoicePositionFilter(array());
     $filter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'invoice_id', 'operator' => 'equals', 'value' => $result['created'][0])));
     $invoicePositions = Sales_Controller_InvoicePosition::getInstance()->search($filter);
     $this->assertEquals(1, $invoicePositions->count());
 }
 /**
  * inspects delete action
  *
  * @param array $_ids
  * @return array of ids to actually delete
  */
 protected function _inspectDelete(array $_ids)
 {
     $records = $this->_backend->getMultiple($_ids);
     $records->setTimezone(Tinebase_Core::getUserTimezone());
     $invoicePositionController = Sales_Controller_InvoicePosition::getInstance();
     $contractController = Sales_Controller_Contract::getInstance();
     foreach ($records as $record) {
         if (!$record->is_auto) {
             continue;
         }
         if ($record->cleared == 'CLEARED') {
             // cleared invoices must not be deleted
             throw new Sales_Exception_InvoiceAlreadyClearedDelete();
         } else {
             // try to find a invoice after this one
             // there should be a contract
             $contractRelation = Tinebase_Relations::getInstance()->getRelations('Sales_Model_Invoice', 'Sql', $record->getId(), NULL, array(), TRUE, array('Sales_Model_Contract'))->getFirstRecord();
             if ($contractRelation) {
                 $contract = $contractRelation->related_record;
                 $contract->setTimezone(Tinebase_Core::getUserTimezone());
                 // get all invoices related to this contract. throw exception if a follwing invoice has been found
                 $invoiceRelations = Tinebase_Relations::getInstance()->getRelations('Sales_Model_Contract', 'Sql', $contract->getId(), NULL, array(), TRUE, array('Sales_Model_Invoice'));
                 foreach ($invoiceRelations as $invoiceRelation) {
                     $invoiceRelation->related_record->setTimezone(Tinebase_Core::getUserTimezone());
                     if ($record->getId() !== $invoiceRelation->related_record->getId() && $record->creation_time < $invoiceRelation->related_record->creation_time) {
                         throw new Sales_Exception_DeletePreviousInvoice();
                     }
                 }
                 $this->_currentBillingContract = $contract;
                 $productAggregates = $this->_findProductAggregates();
             } else {
                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) {
                     Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Could not find contract relation -> skip contract handling');
                 }
                 $contract = null;
                 $productAggregates = array();
             }
             // remove invoice_id from billables
             $filter = new Sales_Model_InvoicePositionFilter(array());
             $filter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'invoice_id', 'operator' => 'equals', 'value' => $record->getId())));
             $invoicePositions = $invoicePositionController->search($filter);
             $allModels = array_unique($invoicePositions->model);
             foreach ($allModels as $model) {
                 if ($model == 'Sales_Model_ProductAggregate') {
                     continue;
                 }
                 $filteredInvoicePositions = $invoicePositions->filter('model', $model);
                 $billableControllerName = $model::getBillableControllerName();
                 $billableFilterName = $model::getBillableFilterName();
                 $filterInstance = new $billableFilterName(array());
                 $filterInstance->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'invoice_id', 'operator' => 'equals', 'value' => $record->getId())));
                 $billableControllerName::getInstance()->updateMultiple($filterInstance, array('invoice_id' => NULL));
                 // set invoice ids of the timeaccounts
                 if ($model == 'Timetracker_Model_Timeaccount') {
                     $filterInstance = new Timetracker_Model_TimeaccountFilter(array());
                     $filterInstance->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'invoice_id', 'operator' => 'equals', 'value' => $record->getId())));
                     Timetracker_Controller_Timeaccount::getInstance()->updateMultiple($filterInstance, array('invoice_id' => NULL));
                 }
             }
             // delete invoice positions
             $invoicePositionController->delete($invoicePositions->getId());
             // set last_autobill a period back
             if ($contract) {
                 //find the month of each productAggregate we have to set it back to
                 $undoProductAggregates = array();
                 $paController = Sales_Controller_ProductAggregate::getInstance();
                 foreach ($invoicePositions as $inPos) {
                     if ($inPos->model != 'Sales_Model_ProductAggregate') {
                         continue;
                     }
                     //if we didnt find a month for the productAggreagte yet or if the month found is greater than the one we have at hands
                     if (!isset($undoProductAggregates[$inPos->accountable_id]) || strcmp($undoProductAggregates[$inPos->accountable_id], $inPos->month) > 0) {
                         $undoProductAggregates[$inPos->accountable_id] = $inPos->month;
                     }
                 }
                 foreach ($productAggregates as $productAggregate) {
                     if (!$productAggregate->last_autobill) {
                         continue;
                     }
                     if (!isset($undoProductAggregates[$productAggregate->id])) {
                         $product = $this->_cachedProducts->getById($productAggregate->product_id);
                         if (!$product) {
                             $product = Sales_Controller_Product::getInstance()->get($productAggregate->product_id);
                             $this->_cachedProducts->addRecord($product);
                         }
                         if ($product->accountable == 'Sales_Model_Product' || $record->date != null && $record->date->isLater($productAggregate->last_autobill)) {
                             continue;
                         }
                         $productAggregate->last_autobill->subMonth($productAggregate->interval);
                     } else {
                         $productAggregate->last_autobill = new Tinebase_DateTime($undoProductAggregates[$productAggregate->id] . '-01 00:00:00', Tinebase_Core::getUserTimezone());
                         if ($productAggregate->billing_point == 'begin') {
                             $productAggregate->last_autobill->subMonth($productAggregate->interval);
                         }
                         if ($productAggregate->start_date && $productAggregate->last_autobill < $productAggregate->start_date) {
                             $tmp = clone $productAggregate->start_date;
                             $tmp->setTimezone(Tinebase_Core::getUserTimezone());
                             $tmp->setDate($tmp->format('Y'), $tmp->format('m'), 1);
                             $tmp->setTime(0, 0, 0);
                             if ($productAggregate->last_autobill < $tmp || $productAggregate->billing_point == 'end' && $productAggregate->last_autobill == $tmp) {
                                 $productAggregate->last_autobill = NULL;
                             }
                         }
                     }
                     $productAggregate->setTimezone('UTC');
                     $paController->update($productAggregate);
                     $productAggregate->setTimezone(Tinebase_Core::getUserTimezone());
                 }
             }
         }
     }
     return $_ids;
 }
 /**
  * processUpdateBillingInformation
  * 
  * @param Tinebase_Record_RecordSet $contracts
  */
 public function processUpdateLastAutobillOfProductAggregates(Tinebase_Record_RecordSet $contracts)
 {
     $now = Tinebase_DateTime::now();
     $billingPoints = array('Timetracker_Model_Timeaccount' => 'end', 'Sales_Model_Product' => 'end', 'WebAccounting_Model_BackupPath' => 'end', 'WebAccounting_Model_StoragePath' => 'end', 'WebAccounting_Model_MailAccount' => 'end', 'WebAccounting_Model_DReg' => 'begin', 'WebAccounting_Model_CertificateDomain' => 'begin', 'WebAccounting_Model_IPNet' => 'end', '' => 'end', 'Sales_Model_ProductAgregate' => 'end');
     foreach ($contracts as $contract) {
         if ($contract->end_date && $contract->end_date < $now) {
             continue;
         }
         // find product aggregates for this contract
         $filter = new Sales_Model_ProductAggregateFilter(array());
         $filter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'contract_id', 'operator' => 'equals', 'value' => $contract->getId())));
         $productAggregates = Sales_Controller_ProductAggregate::getInstance()->search($filter);
         foreach ($productAggregates as $pa) {
             // find all invoices for the contract
             $filter = new Sales_Model_InvoiceFilter(array(array('field' => 'contract', 'operator' => 'AND', 'value' => array(array('field' => ':id', 'operator' => 'equals', 'value' => $contract->getId())))));
             $invoices = Sales_Controller_Invoice::getInstance()->search($filter);
             // find last invoice position for this aggregate
             $filter = new Sales_Model_InvoicePositionFilter();
             $filter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'invoice_id', 'operator' => 'in', 'value' => $invoices->getArrayOfIds())));
             $pagination = new Tinebase_Model_Pagination(array('limit' => 1, 'sort' => 'month', 'dir' => 'DESC'));
             $lastInvoicePosition = Sales_Controller_InvoicePosition::getInstance()->search($filter, $pagination)->getFirstRecord();
             // set billing_point, if none given
             if (!$pa->billing_point) {
                 $pa->billing_point = $billingPoints[$lastInvoicePosition->model];
             }
             if (!$lastInvoicePosition) {
                 // if no invoice position has been found, this is a new contract, so set start_date to the first day of the month of the contracts start_date
                 $date = clone $contract->start_date;
                 $date->setTimezone(Tinebase_Core::getUserTimezone());
                 $date->setTime(0, 0, 0);
                 $date->setDate($date->format('Y'), $date->format('m'), 1);
                 $date->setTimezone('UTC');
                 $startDate = clone $date;
                 $labDate = NULL;
             } else {
                 $split = explode('-', $lastInvoicePosition->month);
                 $date = Tinebase_DateTime::now();
                 $date->setTimezone(Tinebase_Core::getUserTimezone());
                 $date->setTime(0, 0, 0);
                 $date->setDate($split[0], $split[1], 1);
                 // set to next billing date
                 $date->addMonth(1);
                 // if the billing point is at the begin of the interval, set date back one interval
                 if ($pa->billing_point == 'begin') {
                     $date->subMonth($pa->interval);
                 }
                 $date->setTimezone('UTC');
                 $labDate = clone $date;
                 // find first invoice position to calculate start_date
                 $pagination = new Tinebase_Model_Pagination(array('limit' => 1, 'sort' => 'month', 'dir' => 'ASC'));
                 $firstInvoicePosition = Sales_Controller_InvoicePosition::getInstance()->search($filter, $pagination)->getFirstRecord();
                 $split = explode('-', $firstInvoicePosition->month);
                 $startDate = Tinebase_DateTime::now()->setTimezone(Tinebase_Core::getUserTimezone());
                 $startDate->setTime(0, 0, 0);
                 $startDate->setDate($split[0], $split[1], 1);
                 $startDate->setTimezone('UTC');
             }
             $pa->start_date = $startDate;
             $pa->last_autobill = $labDate;
             Sales_Controller_ProductAggregate::getInstance()->update($pa);
         }
     }
 }
 /**
  * tests auto invoice creation
  *
  * TODO should be refactored/fixed:  line 97: $this->assertEquals(6, $half); // $half is completely random
  */
 public function testExportInvoice()
 {
     $this->markTestSkipped('FIXME: this test currently produces random results');
     $this->_createFullFixtures();
     $date = clone $this->_referenceDate;
     $i = 0;
     // until 1.7
     while ($i < 8) {
         $date->addMonth(1);
         $this->_invoiceController->createAutoInvoices($date);
         $i++;
     }
     $all = $this->_invoiceController->getAll();
     $cc3 = $this->_costcenterRecords->filter('remark', 'unittest3')->getFirstRecord();
     $cc4 = $this->_costcenterRecords->filter('remark', 'unittest4')->getFirstRecord();
     $all->setTimezone(Tinebase_Core::getUserTimezone());
     $customer3Invoices = $all->filter('costcenter_id', $cc3->getId())->sort('start_date');
     $customer4Invoices = $all->filter('costcenter_id', $cc4->getId())->sort('start_date');
     // there are timesheets in 2 intervals, so no empty invoice should be generated
     $this->assertEquals(1, $customer3Invoices->count(), 'Customer 3 must have 1 invoice!');
     // there are 2 products, interval 3,6 -> so every quarter in this and the first quarter of next year must be found
     $this->assertEquals(3, $customer4Invoices->count(), 'Customer 4 must have 3 invoices!');
     // test products export
     $definition = dirname(dirname(dirname(dirname(__FILE__)))) . '/tine20/Sales/Export/definitions/invoiceposition_default_ods.xml';
     $filter = new Sales_Model_InvoicePositionFilter(array());
     $filter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'invoice_id', 'operator' => 'equals', 'value' => $customer4Invoices->getFirstRecord()->getId())));
     $filter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'model', 'operator' => 'equals', 'value' => 'Sales_Model_ProductAggregate')));
     $exporter = new Sales_Export_Ods_InvoicePosition($filter, Sales_Controller_InvoicePosition::getInstance(), array('definitionFilename' => $definition));
     $doc = $exporter->generate();
     $xml = $this->_getContentXML($doc);
     $ns = $xml->getNamespaces(true);
     $spreadsheetXml = $xml->children($ns['office'])->{'body'}->{'spreadsheet'};
     // the product should be found here
     $half = 0;
     $quarter = 0;
     $i = 2;
     while ($i < 11) {
         $value = (string) $spreadsheetXml->children($ns['table'])->{'table'}->{'table-row'}->{$i}->children($ns['table'])->{'table-cell'}->{0}->children($ns['text'])->{0};
         $this->assertTrue(in_array($value, array('billhalfyearly', 'billeachquarter')), $value);
         if ($value == 'billhalfyearly') {
             $half++;
         } else {
             $quarter++;
         }
         $i++;
     }
     $this->assertEquals(6, $half);
     $this->assertEquals(3, $quarter);
     unlink($doc);
     // test timesheets export
     $definition = dirname(dirname(dirname(dirname(__FILE__)))) . '/tine20/Timetracker/Export/definitions/ts_default_ods.xml';
     $filter = new Timetracker_Model_TimesheetFilter(array());
     $filter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'invoice_id', 'operator' => 'equals', 'value' => $customer3Invoices->getFirstRecord()->getId())));
     $exporter = new Timetracker_Export_Ods_Timesheet($filter, Timetracker_Controller_Timesheet::getInstance(), array('definitionFilename' => $definition));
     $doc = $exporter->generate();
     $xml = $this->_getContentXML($doc);
     $spreadsheetXml = $xml->children($ns['office'])->{'body'}->{'spreadsheet'};
     $firstContentRow = $spreadsheetXml->children($ns['table'])->{'table'}->{'table-row'}->{2};
     // the timesheet should be found here
     $this->assertEquals($this->_referenceYear . '-05-06', (string) $firstContentRow->children($ns['table'])->{'table-cell'}->{0}->children($ns['text'])->{0});
     $this->assertEquals('ts from ' . $this->_referenceYear . '-05-06 00:00:00', (string) $firstContentRow->children($ns['table'])->{'table-cell'}->{1}->children($ns['text'])->{0});
     $this->assertEquals('TA-for-Customer3', (string) $firstContentRow->children($ns['table'])->{'table-cell'}->{3}->children($ns['text'])->{0});
     $this->assertEquals('1.75', (string) $firstContentRow->children($ns['table'])->{'table-cell'}->{5}->children($ns['text'])->{0});
     unlink($doc);
 }
 /**
  * export product aggregates
  *
  * This is not a default Export! This exports the invoice positions holding product aggregates.
  *
  * @param string $filter JSON encoded string with ids for multi export or filter
  * @param string $options format or export definition id
  */
 public function exportProductAggregates($filter, $options)
 {
     $decodedFilter = Zend_Json::decode($filter);
     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
         Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Export filter: ' . print_r($decodedFilter, TRUE));
     }
     if (!is_array($decodedFilter)) {
         $decodedFilter = array(array('field' => 'id', 'operator' => 'equals', 'value' => $decodedFilter));
     }
     $filter = new Sales_Model_InvoicePositionFilter($decodedFilter);
     parent::_export($filter, Zend_Json::decode($options), Sales_Controller_InvoicePosition::getInstance());
 }
 /**
  * inspects delete action
  *
  * @param array $_ids
  * @return array of ids to actually delete
  */
 protected function _inspectDelete(array $_ids)
 {
     $records = $this->_backend->getMultiple($_ids);
     $records->setTimezone(Tinebase_Core::getUserTimezone());
     $invoicePositionController = Sales_Controller_InvoicePosition::getInstance();
     $contractController = Sales_Controller_Contract::getInstance();
     foreach ($records as $record) {
         if (!$record->is_auto) {
             continue;
         }
         if ($record->cleared == 'CLEARED') {
             // cleared invoices must not be deleted
             throw new Sales_Exception_InvoiceAlreadyClearedDelete();
         } else {
             // try to find a invoice after this one
             // there should be a contract
             $contractRelation = Tinebase_Relations::getInstance()->getRelations('Sales_Model_Invoice', 'Sql', $record->getId(), NULL, array(), TRUE, array('Sales_Model_Contract'))->getFirstRecord();
             if ($contractRelation) {
                 $contract = $contractRelation->related_record;
                 $contract->setTimezone(Tinebase_Core::getUserTimezone());
                 // get all invoices related to this contract. throw exception if a follwing invoice has been found
                 $invoiceRelations = Tinebase_Relations::getInstance()->getRelations('Sales_Model_Contract', 'Sql', $contract->getId(), NULL, array(), TRUE, array('Sales_Model_Invoice'));
                 foreach ($invoiceRelations as $invoiceRelation) {
                     $invoiceRelation->related_record->setTimezone(Tinebase_Core::getUserTimezone());
                     if ($record->getId() !== $invoiceRelation->related_record->getId() && $record->creation_time < $invoiceRelation->related_record->creation_time) {
                         throw new Sales_Exception_DeletePreviousInvoice();
                     }
                 }
             } else {
                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) {
                     Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Could not find contract relation -> skip contract handling');
                 }
                 $contract = null;
             }
             // remove invoice_id from billables
             $filter = new Sales_Model_InvoicePositionFilter(array());
             $filter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'invoice_id', 'operator' => 'equals', 'value' => $record->getId())));
             $invoicePositions = $invoicePositionController->search($filter);
             $allModels = array_unique($invoicePositions->model);
             foreach ($allModels as $model) {
                 if ($model == 'Sales_Model_ProductAggregate') {
                     continue;
                 }
                 $filteredInvoicePositions = $invoicePositions->filter('model', $model);
                 $billableControllerName = $model::getBillableControllerName();
                 $billableFilterName = $model::getBillableFilterName();
                 $filterInstance = new $billableFilterName(array());
                 $filterInstance->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'invoice_id', 'operator' => 'equals', 'value' => $record->getId())));
                 $billableControllerName::getInstance()->updateMultiple($filterInstance, array('invoice_id' => NULL));
                 // set invoice ids of the timeaccounts
                 if ($model == 'Timetracker_Model_Timeaccount') {
                     $filterInstance = new Timetracker_Model_TimeaccountFilter(array());
                     $filterInstance->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'invoice_id', 'operator' => 'equals', 'value' => $record->getId())));
                     Timetracker_Controller_Timeaccount::getInstance()->updateMultiple($filterInstance, array('invoice_id' => NULL));
                 }
             }
             // delete invoice positions
             $invoicePositionController->delete($invoicePositions->getId());
             // set last_autobill a period back
             if ($contract) {
                 // check product aggregates
                 $filter = new Sales_Model_ProductAggregateFilter(array());
                 $filter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'contract_id', 'operator' => 'equals', 'value' => $contract->getId())));
                 $paController = Sales_Controller_ProductAggregate::getInstance();
                 $productAggregates = $paController->search($filter);
                 $productAggregates->setTimezone(Tinebase_Core::getUserTimezone());
                 foreach ($productAggregates as $productAggregate) {
                     if ($productAggregate->last_autobill) {
                         $lab = clone $productAggregate->last_autobill;
                         $add = 0 - (int) $productAggregate->interval;
                         $productAggregate->last_autobill = $lab->addMonth($add);
                         $productAggregate->last_autobill->setTime(0, 0, 0);
                         // last_autobill may not be before aggregate starts (may run into this case if interval has been resized)
                         if (!$productAggregate->start_date || $productAggregate->last_autobill < $productAggregate->start_date) {
                             $productAggregate->last_autobill = NULL;
                         }
                     }
                     $productAggregate->setTimezone('UTC');
                     $paController->update($productAggregate);
                 }
             }
         }
     }
     return $_ids;
 }