Invoice deletion now based on the actual invoice position
authorPaul Mehrer <p.mehrer@metaways.de>
Thu, 12 Mar 2015 13:52:45 +0000 (14:52 +0100)
committerPhilipp Schüle <p.schuele@metaways.de>
Thu, 12 Mar 2015 14:41:13 +0000 (15:41 +0100)
Sales_Controller_Invoice::_inspectDelete():
The product aggregates last_autobill is set back by the actually existing
(and now to be deleted) invoice positions. That is a real undo.
Before the reset of the last_autobill was based on speculation and thus error prone.

Invoice creation now iterates over the months from the first date that needs to be
billed until the current billing month and creates multiple invoices in one go as needed.

Change-Id: I9481d6ad603e18aef92aec076f3ea73ad471f303
Reviewed-on: http://gerrit.tine20.com/customers/1718
Reviewed-by: Philipp Schüle <p.schuele@metaways.de>
Tested-by: Jenkins CI (http://ci.tine20.com/)
tests/tine20/Sales/InvoiceControllerTests.php
tests/tine20/Sales/InvoiceExportTests.php
tests/tine20/Sales/InvoiceJsonTests.php
tine20/Sales/Controller/Invoice.php
tine20/Sales/Model/ProductAggregate.php
tine20/Timetracker/Model/Timeaccount.php

index 480fbbd..2f3c0be 100644 (file)
@@ -270,14 +270,9 @@ class Sales_InvoiceControllerTests extends Sales_InvoiceTestCase
         $this->_createFullFixtures();
         
         $date = clone $this->_referenceDate;
-        $i = 0;
+        $date->addMonth(12);
         
-        // the whole year, 12 months
-        while ($i < 12) {
-            $result = $this->_invoiceController->createAutoInvoices($date);
-            $date->addMonth(1);
-            $i++;
-        }
+        $this->_invoiceController->createAutoInvoices($date);
         
         $paController = Sales_Controller_ProductAggregate::getInstance();
         $productAggregates = $paController->getAll();
@@ -520,24 +515,18 @@ class Sales_InvoiceControllerTests extends Sales_InvoiceTestCase
         )));
         
         $startDate = clone $this->_referenceDate;
-        // the whole year, 12 months
-        $i=0;
-        
         $startDate->addDay(5);
+        $startDate->addMonth(24);
         
-        while ($i < 24) {
-            $startDate->addMonth(1);
-            $result = $this->_invoiceController->createAutoInvoices($startDate);
-            $this->assertEquals(1, $result['created_count']);
-            $i++;
-        }
+        $result = $this->_invoiceController->createAutoInvoices($startDate);
+        $this->assertEquals(25, $result['created_count']);
         
         $invoices = $this->_invoiceController->getAll('start_date');
         $firstInvoice = $invoices->getFirstRecord();
         $this->assertInstanceOf('Tinebase_DateTime', $firstInvoice->start_date);
         $this->assertEquals('0101', $firstInvoice->start_date->format('md'));
         
-        $this->assertEquals(24, $invoices->count());
+        $this->assertEquals(25, $invoices->count());
         
         $filter = new Sales_Model_InvoicePositionFilter(array());
         $filter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'invoice_id', 'operator' => 'in', 'value' => $invoices->getArrayOfIds())));
@@ -561,13 +550,13 @@ class Sales_InvoiceControllerTests extends Sales_InvoiceTestCase
             $this->assertEquals($invoice->end_date->format('Y-m'), $pos->month);
         }
         
-        $this->assertEquals(24, $invoicePositions->count());
+        $this->assertEquals(25, $invoicePositions->count());
         
         $this->assertEquals($this->_referenceYear . '-01', $invoicePositions->getFirstRecord()->month);
         
         $invoicePositions->sort('month', 'DESC');
         
-        $this->assertEquals($this->_referenceYear + 1 . '-12', $invoicePositions->getFirstRecord()->month);
+        $this->assertEquals($this->_referenceYear + 2 . '-01', $invoicePositions->getFirstRecord()->month);
     }
     
     /**
@@ -673,12 +662,10 @@ class Sales_InvoiceControllerTests extends Sales_InvoiceTestCase
             $this->_invoiceController->delete($invoice);
         }
         
-        $testDate = clone $this->_referenceDate;
-        
         $productAggregate = $paController->get($productAggregate->getId());
         $productAggregate->setTimezone(Tinebase_Core::getUserTimezone());
         
-        $this->assertEquals(NULL, $productAggregate->last_autobill);
+        $this->assertEquals($this->_referenceDate, $productAggregate->last_autobill);
         
         // create 6 invoices again - each month one invoice - last autobill must be increased each month
         for ($i = 1; $i < 7; $i++) {
@@ -1023,7 +1010,7 @@ class Sales_InvoiceControllerTests extends Sales_InvoiceTestCase
     
         $result = $this->_invoiceController->createAutoInvoices($date);
     
-        $this->assertEquals(3, count($result['created']));
+        $this->assertEquals(5, count($result['created']));
         
         $tsController = Timetracker_Controller_Timesheet::getInstance();
     
@@ -1146,7 +1133,7 @@ class Sales_InvoiceControllerTests extends Sales_InvoiceTestCase
     
         $result = $this->_invoiceController->createAutoInvoices($date);
     
-        $this->assertEquals(3, count($result['created']));
+        $this->assertEquals(5, count($result['created']));
         
         $invoice = $this->_invoiceController->get($result['created'][0]);
         $invoice->cleared = 'CLEARED';
index 1a0b699..4f4e669 100644 (file)
@@ -4,7 +4,7 @@
  * 
  * @package     Sales
  * @license     http://www.gnu.org/licenses/agpl.html
- * @copyright   Copyright (c) 2014 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @copyright   Copyright (c) 2014-2015 Metaways Infosystems GmbH (http://www.metaways.de)
  * @author      Alexander Stintzing <a.stintzing@metaways.de>
  * 
  */
@@ -28,9 +28,13 @@ class Sales_InvoiceExportTests extends Sales_InvoiceTestCase
     
     /**
      * 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;
         
@@ -38,8 +42,8 @@ class Sales_InvoiceExportTests extends Sales_InvoiceTestCase
         
         // until 1.7
         while ($i < 8) {
-            $result = $this->_invoiceController->createAutoInvoices($date);
             $date->addMonth(1);
+            $this->_invoiceController->createAutoInvoices($date);
             $i++;
         }
         
@@ -132,7 +136,7 @@ class Sales_InvoiceExportTests extends Sales_InvoiceTestCase
     
         // until 1.7
         while ($i < 8) {
-            $result = $this->_invoiceController->createAutoInvoices($date);
+            $this->_invoiceController->createAutoInvoices($date);
             $date->addMonth(1);
             $i++;
         }
@@ -149,9 +153,13 @@ class Sales_InvoiceExportTests extends Sales_InvoiceTestCase
         $spreadsheetXml = $xml->children($ns['office'])->{'body'}->{'spreadsheet'};
     
         // the product should be found here
-        $half = 0;
-        $quarter = 0;
-    
-        $this->assertEquals('Debitor', (string) $spreadsheetXml->children($ns['table'])->{'table'}->{'table-row'}->{1}->children($ns['table'])->{'table-cell'}->{3}->children($ns['text'])->{0});
+        $this->assertEquals('Debitor', (string) $spreadsheetXml->children(
+                $ns['table']
+            )->{'table'}->{'table-row'}->{1}->children(
+                $ns['table']
+            )->{'table-cell'}->{3}->children(
+                $ns['text']
+            )->{0}
+        );
     }
 }
index bf5d001..4f6b9dc 100644 (file)
@@ -79,6 +79,7 @@ class Sales_InvoiceJsonTests extends Sales_InvoiceTestCase
         
         $date = clone $this->_referenceDate;
         $date->addHour(3);
+        $date->addMonth(1);
         
         $this->_invoiceController->createAutoInvoices($date);
         
@@ -106,7 +107,7 @@ class Sales_InvoiceJsonTests extends Sales_InvoiceTestCase
         // first invoice for customer 4
         $invoice = $json->getInvoice($c4Invoice['id']);
         
-        $this->assertEquals(9, count($invoice['positions']));
+        $this->assertEquals(18, count($invoice['positions']));
         
         foreach($invoice['relations'] as $relation) {
             switch ($relation['type']) {
@@ -148,14 +149,9 @@ class Sales_InvoiceJsonTests extends Sales_InvoiceTestCase
         $this->_createFullFixtures();
         
         // the whole year, 12 months
-        $i = 0;
         $date = clone $this->_referenceDate;
-        
-        while ($i < 12) {
-            $result = $this->_invoiceController->createAutoInvoices($date);
-            $date->addMonth(1);
-            $i++;
-        }
+        $date->addMonth(12);
+        $this->_invoiceController->createAutoInvoices($date);
         
         $json = new Sales_Frontend_Json();
         
@@ -275,6 +271,7 @@ class Sales_InvoiceJsonTests extends Sales_InvoiceTestCase
         $i = 0;
         $date = clone $this->_referenceDate;
         $date->addHour(3);
+        $date->addMonth(1);
         
         $result = $this->_invoiceController->createAutoInvoices($date);
         
index 70af96a..ffd8763 100644 (file)
@@ -49,6 +49,12 @@ class Sales_Controller_Invoice extends Sales_Controller_NumberableAbstract
     protected $_currentBillingCustomer = NULL;
     
     /**
+     * the date of the month that needs to be billed next for the current contract
+     * 
+     * @var Tinebase_DateTime
+     */
+    protected $_currentMonthToBill = NULL;
+    /**
      * holds the limit the iterator should have
      * 
      * @var integer
@@ -300,37 +306,54 @@ class Sales_Controller_Invoice extends Sales_Controller_NumberableAbstract
         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;
+            
             // if the product has been billed already, add the interval
             if ($lastBilled) {
-                $nextBill = clone $lastBilled;
-                $nextBill->addMonth($productAggregate->interval);
+                if (NULL != $productAggregate->end_date && $lastBilled->isLaterOrEquals($productAggregate->end_date)) {
+                    $productEnded = true;
+                } else {
+                    $nextBill = $lastBilled;
+                    $nextBill->setDate($nextBill->format('Y'), $nextBill->format('m'), 1);
+                    $nextBill->addMonth($productAggregate->interval);
+                }
             } else {
-                // it hasn't been billed already, so take the start_date of the contract as date
-                $nextBill = clone $this->_currentBillingContract->start_date;
-        
+                // 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);
                 }
             }
-        
-            // assure creating the last bill if a contract has been terminated
-            if (($this->_currentBillingContract->end_date !== NULL) && $nextBill->isLater($this->_currentBillingContract->end_date)) {
-                $nextBill = clone $this->_currentBillingContract->end_date;
-            }
-        
-            $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);
+            
+            if (!$productEnded) {
+                if (NULL != $productAggregate->end_date && $nextBill->isLater($productAggregate->end_date)) {
+                    $nextBill = clone $productAggregate->end_date;
+                }
+                
+                // assure creating the last bill if a contract has been terminated
+                if (($this->_currentBillingContract->end_date !== NULL) && $nextBill->isLater($this->_currentBillingContract->end_date)) {
+                    $nextBill = clone $this->_currentBillingContract->end_date;
+                }
+                
+                $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 ($this->_currentBillingDate->isLaterOrEquals($nextBill)) {
+            if (! $productEnded && $this->_currentMonthToBill->isLaterOrEquals($nextBill)) {
                 if (($product->accountable == 'Sales_Model_Product') || ($product->accountable == '')) {
                     $simpleProductsToBill[] = array('pa' => $productAggregate, 'ac' => $productAggregate);
                 } else {
@@ -425,12 +448,12 @@ class Sales_Controller_Invoice extends Sales_Controller_NumberableAbstract
         $earliestStartDate = $latestEndDate = NULL;
         
         foreach ($billableAccountables as $ba) {
-            if (! $ba['ac']->isBillable($this->_currentBillingDate, $this->_currentBillingContract, $ba['pa'])) {
+            if (! $ba['ac']->isBillable($this->_currentMonthToBill, $this->_currentBillingContract, $ba['pa'])) {
                 continue;
             }
         
-            $ba['ac']->loadBillables($this->_currentBillingDate, $ba['pa']);
-            $billables = $ba['ac']->getBillables($this->_currentBillingDate, $ba['pa']);
+            $ba['ac']->loadBillables($this->_currentMonthToBill, $ba['pa']);
+            $billables = $ba['ac']->getBillables($this->_currentMonthToBill, $ba['pa']);
         
             if (empty($billables)) {
                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
@@ -467,7 +490,9 @@ class Sales_Controller_Invoice extends Sales_Controller_NumberableAbstract
     protected function _createAutoInvoicesForContract(Sales_Model_Contract $contract, Tinebase_DateTime $currentDate)
     {
         // set this current billing date (user timezone)
-        $this->_currentBillingDate     = $currentDate;
+        $this->_currentBillingDate = clone $currentDate;
+        $this->_currentBillingDate->setDate($this->_currentBillingDate->format('Y'), $this->_currentBillingDate->format('m'), 1);
+        $this->_currentBillingDate->setTime(0,0,0);
         
         // check all prerequisites needed for billing of the contract
         if (! $this->_validateContract($contract)) {
@@ -484,80 +509,160 @@ class Sales_Controller_Invoice extends Sales_Controller_NumberableAbstract
         // find product aggregates of the current contract
         $productAggregates = $this->_findProductAggregates();
         
-        // prepare relations and find all billable accountables of the current contract
-        list($relations, $billableAccountables) = $this->_prepareInvoiceRelationsAndFindBillableAccountables($productAggregates);
-        
-        // find invoice positions and the first start date and last end date of all billables
-        list($invoicePositions, $earliestStartDate, $latestEndDate) = $this->_findInvoicePositionsAndInvoiceInterval($billableAccountables);
-        
-        // if there are no positions, no bill will be created, but the last_autobill info is set, if the current date is later 
-        if ($invoicePositions->count() == 0) {
+        // find month that needs to be billed next (note: _currentMonthToBill is the 01-01 00:00:00 of the next month, its the border, like last_autobill)
+        $this->_currentMonthToBill = null;
+        foreach ($productAggregates as $productAggregate) {
+            if ( null != $productAggregate->last_autobill ) {
+                $tmp = clone $productAggregate->last_autobill;
+                $tmp->setDate($tmp->format('Y'), $tmp->format('m'), 1);
+                $tmp->setTime(0,0,0);
+                if ( null == $this->_currentMonthToBill || $tmp->isLater($this->_currentMonthToBill) ) {
+                    $this->_currentMonthToBill = $tmp;
+                }
+            }
+        }
         
-            if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
-                Tinebase_Core::getLogger()->log(__METHOD__ . '::' . __LINE__ . ' No efforts for the contract "' . $this->_currentBillingContract->title . '" could be found.', Zend_Log::INFO);
+        // this contract has no productAggregates, maybe just time accounts? use last invoice to find already billed month
+        if ( null == $this->_currentMonthToBill ) {
+            // find newest invoice of contract (probably can be done more efficient!)
+            $invoiceRelations = Tinebase_Relations::getInstance()->getRelations('Sales_Model_Contract', 'Sql', $contract->getId(), NULL, array(), TRUE, array('Sales_Model_Invoice'));
+            // do not modify $newestInvoiceTime!!!! it does NOT get cloned!
+            $newestInvoiceTime = null;
+            $newestInvoice = null;
+            foreach($invoiceRelations as $invoiceRelation) {
+                $invoiceRelation->related_record->setTimezone(Tinebase_Core::getUserTimezone());
+                if ( null == $newestInvoiceTime || $invoiceRelation->related_record->creation_time->isLater($newestInvoiceTime) ) {
+                    $newestInvoiceTime = $invoiceRelation->related_record->creation_time;
+                    $newestInvoice = $invoiceRelation->related_record;
+                }
             }
             
-            return false;
+            if ( null != $newestInvoice ) {
+                // we can only take the end_date because there are no product aggregates (that have a last_autobill set) in this contract, otherwise it might be one interval ahead!
+                $this->_currentMonthToBill = clone $newestInvoice->end_date;
+                $this->_currentMonthToBill->addDay(4);
+                $this->_currentMonthToBill->subMonth(1);
+                //$this->_currentMonthToBill->setTimezone(Tinebase_Core::getUserTimezone());
+            }
         }
         
-        // prepare invoice
-        $invoice = new Sales_Model_Invoice(array(
-            'is_auto'       => TRUE,
-            'description'   => $this->_currentBillingContract->title . ' (' . $this->_currentBillingDate->toString() . ')',
-            'type'          => 'INVOICE',
-            'address_id'    => $this->_currentBillingContract->billing_address_id,
-            'credit_term'   => $this->_currentBillingCustomer['credit_term'],
-            'customer_id'   => $this->_currentBillingCustomer['id'],
-            'costcenter_id' => $this->_currentBillingCostCenter->getId(),
-            'start_date'    => $earliestStartDate,
-            'end_date'      => $latestEndDate,
-            'positions'     => $invoicePositions->toArray(),
-            'date'          => NULL,
-            'sales_tax'     => 19
-        ));
-        
-        $invoice->relations = $relations;
-        
-        $invoice->setTimezone('UTC', TRUE);
-
-        // create invoice
-        $invoice = $this->create($invoice);
-        $this->_autoInvoiceIterationResults[] = $invoice->getId();
+        if ( null == $this->_currentMonthToBill ) {
+            $this->_currentMonthToBill = clone $contract->start_date;
+        }
+        $this->_currentMonthToBill->setTimezone(Tinebase_Core::getUserTimezone());
+        $this->_currentMonthToBill->setDate($this->_currentMonthToBill->format('Y'), $this->_currentMonthToBill->format('m'), 1);
+        $this->_currentMonthToBill->setTime(0,0,0);
+        $this->_currentMonthToBill->addMonth(1);
         
-        $paToUpdate = array();
+        $doSleep = false;
         
-        // conjunct billables with invoice, find out which productaggregates to update
-        foreach($billableAccountables as $ba) {
-            $ba['ac']->conjunctInvoiceWithBillables($invoice);
-            if ($ba['pa']->getId()) {
-                $paToUpdate[$ba['pa']->getId()] = $ba['pa'];
+        while ( $this->_currentMonthToBill->isEarlierOrEquals($this->_currentBillingDate) ) {
+            
+            if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
+                Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' $this->_currentMonthToBill: ' . $this->_currentMonthToBill
+                    . ' $this->_currentBillingDate ' . $this->_currentBillingDate);
+                foreach ($productAggregates as $productAggregate) {
+                    Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . $productAggregate->id . ' ' . $productAggregate->last_autobill);
+                }
             }
-        }
-        
-        foreach($paToUpdate as $paId => $productAggregate) {
-            $firstBill = (! $productAggregate->last_autobill);
             
-            $lab = $productAggregate->last_autobill ? clone $productAggregate->last_autobill : ($productAggregate->start_date ? clone $productAggregate->start_date : clone $this->_currentBillingContract->start_date);
-            $lab->setTimezone(Tinebase_Core::getUserTimezone());
-            $lab->setTime(0,0,0);
+            //required to have one sec difference in the invoice creation_time, can be optimized to look for milliseconds
+            if ( $doSleep ) {
+                sleep(1);
+                $doSleep = false;
+            }
+            // prepare relations and find all billable accountables of the current contract
+            list($relations, $billableAccountables) = $this->_prepareInvoiceRelationsAndFindBillableAccountables($productAggregates);
             
-            if (! $firstBill) {
-                $lab->addMonth($productAggregate->interval);
-            } else {
-                if ($productAggregate->billing_point == 'end') {
-                    // if first bill, add interval on billing_point end
-                    $lab->addMonth($productAggregate->interval);
+            if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
+                Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' count $billableAccountables: ' . count($billableAccountables));
+                foreach ($billableAccountables as $ba) {
+                    Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' accountable: ' . get_class($ba['ac']) . ' id: ' . $ba['ac']->getId());
                 }
             }
-
-            $productAggregate->last_autobill = $lab;
-            $productAggregate->setTimezone('UTC');
             
-            if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
-                Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Updating last_autobill of "' . $productAggregate->getId() . '": ' . $lab->__toString());
+            // find invoice positions and the first start date and last end date of all billables
+            list($invoicePositions, $earliestStartDate, $latestEndDate) = $this->_findInvoicePositionsAndInvoiceInterval($billableAccountables);
+            
+            /**** TODO ****/
+            // if there are no positions, no more bills need to be created,
+            // but the last_autobill info is set, if the current date is later
+            if ($invoicePositions->count() > 0 ) {
+                
+                // prepare invoice
+                $invoice = new Sales_Model_Invoice(array(
+                    'is_auto'       => TRUE,
+                    'description'   => $this->_currentBillingContract->title . ' (' . $this->_currentMonthToBill->toString() . ')',
+                    'type'          => 'INVOICE',
+                    'address_id'    => $this->_currentBillingContract->billing_address_id,
+                    'credit_term'   => $this->_currentBillingCustomer['credit_term'],
+                    'customer_id'   => $this->_currentBillingCustomer['id'],
+                    'costcenter_id' => $this->_currentBillingCostCenter->getId(),
+                    'start_date'    => $earliestStartDate,
+                    'end_date'      => $latestEndDate,
+                    'positions'     => $invoicePositions->toArray(),
+                    'date'          => NULL,
+                    'sales_tax'     => 19
+                ));
+                
+                $invoice->relations = $relations;
+                
+                $invoice->setTimezone('UTC', TRUE);
+        
+                // create invoice
+                $invoice = $this->create($invoice);
+                $this->_autoInvoiceIterationResults[] = $invoice->getId();
+                
+                $paToUpdate = array();
+                
+                // conjunct billables with invoice, find out which productaggregates to update
+                foreach($billableAccountables as $ba) {
+                    $ba['ac']->conjunctInvoiceWithBillables($invoice);
+                    if ($ba['pa']->getId()) {
+                        $paToUpdate[$ba['pa']->getId()] = $ba['pa'];
+                    }
+                }
+                
+                foreach($paToUpdate as $paId => $productAggregate) {
+                    $firstBill = (! $productAggregate->last_autobill);
+                    
+                    $lab = $productAggregate->last_autobill ? clone $productAggregate->last_autobill : ($productAggregate->start_date ? clone $productAggregate->start_date : clone $this->_currentBillingContract->start_date);
+                    $lab->setTimezone(Tinebase_Core::getUserTimezone());
+                    $lab->setDate($lab->format('Y'), $lab->format('m'), 1);
+                    $lab->setTime(0,0,0);
+                    
+                    if (! $firstBill) {
+                        $lab->addMonth($productAggregate->interval);
+                    } else {
+                        if ($productAggregate->billing_point == 'end') {
+                            // if first bill, add interval on billing_point end
+                            $lab->addMonth($productAggregate->interval);
+                        }
+                    }
+                    
+                    while ($this->_currentMonthToBill->isLater($lab)) {
+                        $lab->addMonth($productAggregate->interval);
+                    }
+                    if ($lab->isLater($this->_currentMonthToBill)) {
+                        $lab->subMonth($productAggregate->interval);
+                    }
+                    
+                    $productAggregate->last_autobill = $lab;
+                    $productAggregate->setTimezone('UTC');
+                    
+                    if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
+                        Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Updating last_autobill of "' . $productAggregate->getId() . '": ' . $lab->__toString());
+                    }
+                    
+                    Sales_Controller_ProductAggregate::getInstance()->update($productAggregate);
+                    
+                    $productAggregate->setTimezone(Tinebase_Core::getUserTimezone());
+                }
+                
+                $doSleep = true;
             }
             
-            Sales_Controller_ProductAggregate::getInstance()->update($productAggregate);
+            $this->_currentMonthToBill->addMonth(1);
         }
     }
     
@@ -827,10 +932,13 @@ class Sales_Controller_Invoice extends Sales_Controller_NumberableAbstract
                             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
@@ -839,7 +947,7 @@ class Sales_Controller_Invoice extends Sales_Controller_NumberableAbstract
                 $invoicePositions = $invoicePositionController->search($filter);
                 
                 $allModels = array_unique($invoicePositions->model);
-
+                
                 foreach($allModels as $model) {
                     
                     if ($model == 'Sales_Model_ProductAggregate') {
@@ -872,31 +980,56 @@ class Sales_Controller_Invoice extends Sales_Controller_NumberableAbstract
                 
                 // 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())
-                    ));
-                    
+                    //find the month of each productAggregate we have to set it back to
+                    $undoProductAggregates = array();
                     $paController = Sales_Controller_ProductAggregate::getInstance();
-                    $productAggregates = $paController->search($filter);
-                    $productAggregates->setTimezone(Tinebase_Core::getUserTimezone());
+                    
+                    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) {
-                            $lab = clone $productAggregate->last_autobill;
-                            $add = 0 - (int) $productAggregate->interval;
-                            $productAggregate->last_autobill = $lab->addMonth($add);
-                            $productAggregate->last_autobill->setTime(0,0,0);
+                        
+                        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_ProductAggregate')
+                                continue;
+                            
+                            $productAggregate->last_autobill->subMonth($productAggregate->interval);
+                        } else {
                             
-                            // 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->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());
                     }
                 }
             }
index 88837ad..8305be3 100644 (file)
@@ -130,13 +130,27 @@ class Sales_Model_ProductAggregate extends Sales_Model_Accountable_Abstract
      */
     public function getInterval(Tinebase_DateTime $date = NULL)
     {
+        if ($date != NULL) {
+            $date = clone $date;
+        } elseif($this->_referenceDate != NULL) {
+            $date = clone $this->_referenceDate;
+        }
+        if ($date != NULL) {
+            $date->setDate($date->format('Y'), $date->format('m'), 1);
+            // if we are not already in user timezone we are in deep shit, add assertation rather instead or something
+            $date->setTimezone(Tinebase_Core::getUserTimezone());
+            $date->setTime(0,0,0);
+            if ($this->billing_point == 'begin') {
+                $date->addMonth($this->interval);
+            }
+        }
+        
         if (! $this->last_autobill) {
             if (! $this->start_date) {
                 $from = clone $this->_referenceContract->start_date;
             } else {
                 $from = clone $this->start_date;
             }
-             
         } else {
             $from = clone $this->last_autobill;
             if ($this->billing_point == 'begin') {
@@ -145,11 +159,17 @@ class Sales_Model_ProductAggregate extends Sales_Model_Accountable_Abstract
         }
         
         $from->setDate($from->format('Y'), $from->format('m'), 1);
+        // if we are not already in user timezone we are in deep shit, add assertation rather instead or something
         $from->setTimezone(Tinebase_Core::getUserTimezone());
         $from->setTime(0,0,0);
         
         $to = clone $from;
-        $to->addMonth($this->interval);
+        do {
+            $to->addMonth($this->interval);
+        } while($date != NULL && $to->isEarlier($date)) ;
+        if ($this->billing_point == 'end' && $to->isLater($date)) {
+            $to->subMonth($this->interval);
+        }
         $to->subSecond(1);
         
         return array($from, $to);
@@ -169,10 +189,12 @@ class Sales_Model_ProductAggregate extends Sales_Model_Accountable_Abstract
         list($from, $to) = $this->getInterval();
         $this->_billables = array();
         
+        // if we are not already in user timezone we are in deep shit, add assertation rather instead or something
         $this->setTimezone(Tinebase_Core::getUserTimezone());
         
         while($from < $to) {
             $this->_billables[$from->format('Y-m')] = array(clone $this);
+            // 1 or interval?!? should show up every month as a position, so 1! NOT interval
             $from->addMonth(1);
         }
     }
@@ -204,18 +226,22 @@ class Sales_Model_ProductAggregate extends Sales_Model_Accountable_Abstract
              } 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 $nextBill < $date;
+         return $date->isLaterOrEquals($nextBill);
      }
      
      /**
@@ -280,9 +306,6 @@ class Sales_Model_ProductAggregate extends Sales_Model_Accountable_Abstract
      */
     protected function _setFromJson(array &$_data)
     {
-        if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
-            . ' json data: ' . print_r($_data, true));
-        
         // sanitize product id if it is an array
         if (is_array($_data['product_id']) && isset($_data['product_id']['id']) ) {
             $_data['product_id'] = $_data['product_id']['id'];
index 87c4bd6..fa7626f 100644 (file)
@@ -416,7 +416,6 @@ class Timetracker_Model_Timeaccount extends Sales_Model_Accountable_Abstract
             ), 'AND');
             // NOTE: using text filter here for id (operator equals is not defined in default timeaccount_id filter)
             $filter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'timeaccount_id', 'operator' => 'equals', 'value' => $this->getId())));
-
             $tsController->updateMultiple($filter, array('invoice_id' => $invoice->getId()));
         } else {
             $ids = $this->_getIdsOfBillables();