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