0009768: Use ModelConfig for Timetracker models
[tine20] / tine20 / Timetracker / Model / Timeaccount.php
1 <?php
2 /**
3  * class to hold Timeaccount data
4  * 
5  * @package     Timetracker
6  * @subpackage  Model
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Philipp Schüle <p.schuele@metaways.de>
9  * @copyright   Copyright (c) 2007-2016 Metaways Infosystems GmbH (http://www.metaways.de)
10  * 
11  * @todo        update validators (default values, mandatory fields)
12  * @todo        add setFromJson with relation handling
13  */
14
15 /**
16  * class to hold Timeaccount data
17  * 
18  * @package     Timetracker
19  * @subpackage  Model
20  */
21 class Timetracker_Model_Timeaccount extends Sales_Model_Accountable_Abstract
22 {
23     /**
24      * key in $_validators/$_properties array for the filed which 
25      * represents the identifier
26      * 
27      * @var string
28      */    
29     protected $_identifier = 'id';
30     
31     /**
32      * application the record belongs to
33      *
34      * @var string
35      */
36     protected $_application = 'Timetracker';
37
38     /**
39      * holds the configuration object (must be declared in the concrete class)
40      *
41      * @var Tinebase_ModelConfiguration
42      */
43     protected static $_configurationObject = NULL;
44
45     /**
46      * Holds the model configuration (must be assigned in the concrete class)
47      *
48      * @var array
49      */
50     protected static $_modelConfiguration = array(
51         'containerName'     => 'Timeaccount',
52         'containersName'    => 'Timeaccounts',
53         'recordName'        => 'Timeaccount',
54         'recordsName'       => 'Timeaccounts', // ngettext('Timeaccount', 'Timeaccounts', n)
55         'hasRelations'      => TRUE,
56         'hasCustomFields'   => TRUE,
57         'hasNotes'          => TRUE,
58         'hasTags'           => TRUE,
59         'modlogActive'      => TRUE,
60         'hasAttachments'    => TRUE,
61         'createModule'      => TRUE,
62         'containerProperty' => 'container_id',
63         'multipleEdit'      => TRUE,
64         'requiredRight'     => 'manage',
65
66         'titleProperty'     => 'title',
67         'appName'           => 'Timetracker',
68         'modelName'         => 'Timeaccount',
69
70         'filterModel'       => array(
71             'contract'          => array(
72                 'filter'            => 'Tinebase_Model_Filter_ExplicitRelatedRecord',
73                 'title'             => 'Contract', // _('Contract')
74                 'options'           => array(
75                     'controller'        => 'Sales_Controller_Contract',
76                     'filtergroup'       => 'Sales_Model_ContractFilter',
77                     'own_filtergroup'   => 'Timetracker_Model_TimeaccountFilter',
78                     'own_controller'    => 'Timetracker_Controller_Timeaccount',
79                     'related_model'     => 'Sales_Model_Contract',
80                 ),
81                 'jsConfig'          => array('filtertype' => 'timetracker.timeaccountcontract')
82             ),
83             'responsible'       => array(
84                 'filter'            => 'Tinebase_Model_Filter_ExplicitRelatedRecord',
85                 'title'             => 'Responsible',
86                 'options'           => array(
87                     'controller'        => 'Addressbook_Controller_Contact',
88                     'filtergroup'       => 'Addressbook_Model_ContactFilter',
89                     'own_filtergroup'   => 'Timetracker_Model_TimeaccountFilter',
90                     'own_controller'    => 'Timetracker_Controller_Timeaccount',
91                     'related_model'     => 'Addressbook_Model_Contact',
92                 ),
93                 'jsConfig'          => array('filtertype' => 'timetracker.timeaccountresponsible')
94             )
95         ),
96
97         'fields'            => array(
98             'account_grants'    => array(
99                 'label'                 => NULL,
100                 'validators'            => array(Zend_Filter_Input::ALLOW_EMPTY => true),
101             ),
102             'title'             => array(
103                 'label'                 => 'Title', //_('Title')
104                 'duplicateCheckGroup'   => 'title',
105                 'queryFilter'           => TRUE,
106                 'showInDetailsPanel'    => TRUE,
107                 'validators'            => array(Zend_Filter_Input::ALLOW_EMPTY => false, 'presence'=>'required'),
108             ),
109             'number'            => array(
110                 'label'                 => 'Number', //_('Number')
111                 'duplicateCheckGroup'   => 'number',
112                 'queryFilter'           => TRUE,
113                 'showInDetailsPanel'    => TRUE,
114                 'validators'            => array(Zend_Filter_Input::ALLOW_EMPTY => true),
115             ),
116             'description'       => array(
117                 'label'                 => 'Description', // _('Description')
118                 'type'                  => 'text',
119                 'showInDetailsPanel'    => TRUE,
120                 'validators'            => array(Zend_Filter_Input::ALLOW_EMPTY => true),
121             ),
122             'budget'            => array(
123                 'type'                  => 'float',
124                 'inputFilters'          => array('Zend_Filter_Digits', 'Zend_Filter_Empty' => NULL),
125                 'validators'            => array(Zend_Filter_Input::ALLOW_EMPTY => true),
126             ),
127             'budget_unit'       => array(
128                 'shy'                   => TRUE,
129                 'default'               => 'hours',
130                 'validators'            => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => 'hours'),
131             ),
132             'price'             => array(
133                 'type'                  => 'integer',
134                 'inputFilters'          => array('Zend_Filter_PregReplace' => array('/,/', '.'), 'Zend_Filter_Empty' => NULL),
135                 'validators'            => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => 0),
136             ),
137             'price_unit'        => array(
138                 'shy'                   => TRUE,
139                 'default'               => 'hours',
140                 'validators'            => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => 'hours'),
141             ),
142             'is_open'           => array(
143                 // is_open = Status, status = Billed
144                 'label'                 => 'Status', //_('Status')
145                 'type'                  => 'boolean',
146                 'default'               => 1,
147                 'inputFilters'          => array('Zend_Filter_Empty' => 0),
148                 'filterDefinition'      => array(
149                     'filter'                => 'Tinebase_Model_Filter_Bool',
150                     'jsConfig'              => array('filtertype' => 'timetracker.timeaccountstatus')
151                 ),
152                 'validators'            => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => 1),
153             ),
154             'is_billable'       => array(
155                 'type'                  => 'boolean',
156                 'default'               => TRUE,
157                 'validators'            => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => 1),
158             ),
159             'billed_in'         => array(
160                 'label'                 => "Cleared in", // _("Cleared in"),
161                 'validators'            => array(Zend_Filter_Input::ALLOW_EMPTY => true),
162             ),
163             'invoice_id'        => array(
164                 'label'                 => 'Invoice', // _('Invoice')
165                 'type'                  => 'record',
166                 'inputFilters'          => array('Zend_Filter_Empty' => NULL),
167                 'config'                => array(
168                     'appName'               => 'Sales',
169                     'modelName'             => 'Invoice',
170                     'idProperty'            => 'id'
171                 ),
172                 'validators'            => array(Zend_Filter_Input::ALLOW_EMPTY => true),
173             ),
174             'status'            => array(
175                 // is_open = Status, status = Billed
176                 'label'                 => 'Billed', //_('Billed')
177                 'type'                  => 'string',
178                 'filterDefinition'      => array(
179                     'filter'                => 'Tinebase_Model_Filter_Text',
180                     'jsConfig'              => array('filtertype' => 'timetracker.timeaccountbilled')
181                 ),
182                 'validators'            => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => 'not yet billed'),
183             ),
184             'cleared_at'        => array(
185                 'label'                 => "Cleared at", // _("Cleared at")
186                 'type'                  => 'datetime',
187                 'validators'            => array(Zend_Filter_Input::ALLOW_EMPTY => true),
188             ),
189             'deadline'          => array(
190                 'label'                 => 'Booking deadline', // _('Booking deadline')
191                 'type'                  => 'string',
192                 'validators'            => array(
193                                             Zend_Filter_Input::ALLOW_EMPTY      => true,
194                                             Zend_Filter_Input::DEFAULT_VALUE    => self::DEADLINE_NONE,
195                                                 array('InArray', array(self::DEADLINE_NONE, self::DEADLINE_LASTWEEK)),
196                                             )
197             ),
198             'grants'            => array(
199                 'label'                 => NULL,
200                 'type'                  => 'records',
201                 'config'                => array(
202                     'appName'               => 'Timetracker',
203                     'modelName'             => 'TimeaccountGrants',
204                     'idProperty'            => 'id'
205                 ),
206                 'validators'            => array(Zend_Filter_Input::ALLOW_EMPTY => true),
207             ),
208             'responsible'       => array(
209                 'type'                  => 'virtual',
210                 'config'                => array(
211                     'type'                  => 'relation',
212                     'label'                 => 'Responsible',    // _('Responsible')
213                     'config'                => array(
214                         'appName'               => 'Addressbook',
215                         'modelName'             => 'Contact',
216                         'type'                  => 'RESPONSIBLE'
217                     )
218                 )
219             ),
220
221         )
222     );
223
224     /**
225      * @see Tinebase_Record_Abstract
226      */
227     protected static $_relatableConfig = array(
228         array('relatedApp' => 'Sales', 'relatedModel' => 'CostCenter', 'config' => array(
229             array('type' => 'COST_CENTER', 'degree' => 'sibling', 'text' => 'Cost Center', 'max' => '1:0'), // _('Cost Center')
230             )
231         ),
232         array('relatedApp' => 'Addressbook', 'relatedModel' => 'Contact', 'config' => array(
233             array('type' => 'RESPONSIBLE', 'degree' => 'sibling', 'text' => 'Responsible Person', 'max' => '1:0'), // _('Responsible Person')
234         )
235         )
236     );
237     
238     /**
239      * if foreign Id fields should be resolved on search and get from json
240      * should have this format:
241      *     array('Calendar_Model_Contact' => 'contact_id', ...)
242      * or for more fields:
243      *     array('Calendar_Model_Contact' => array('contact_id', 'customer_id), ...)
244      * (e.g. resolves contact_id with the corresponding Model)
245      *
246      * @var array
247      */
248     protected static $_resolveForeignIdFields = array(
249         'Sales_Model_Invoice' => array('invoice_id'),
250     );
251     
252     /**
253      * relation type: contract
254      *
255      */
256     const RELATION_TYPE_CONTRACT = 'CONTRACT';
257     
258     /**
259      * deadline type: none
260      * = no deadline for timesheets
261      */
262     const DEADLINE_NONE = 'none';
263     
264     /**
265      * deadline type: last week
266      * = booking timesheets allowed until monday midnight for the last week
267      */
268     const DEADLINE_LASTWEEK = 'lastweek';
269
270     /**
271      * set from array data
272      *
273      * @param array $_data
274      * @return void
275      */
276     public function setFromArray(array $_data)
277     {
278         parent::setFromArray($_data);
279         
280         if (isset($_data['grants']) && !empty($_data['grants'])) {
281             $this->grants = new Tinebase_Record_RecordSet('Timetracker_Model_TimeaccountGrants', $_data['grants']);
282         }  else {
283             $this->grants = new Tinebase_Record_RecordSet('Timetracker_Model_TimeaccountGrants');
284         }
285     }
286     
287     /**
288      * returns the timesheet filter 
289      * 
290      * @param Tinebase_DateTime $date
291      * @param Sales_Model_Contract
292      * @return Timetracker_Model_TimesheetFilter
293      */
294     protected function _getBillableTimesheetsFilter(Tinebase_DateTime $date, Sales_Model_Contract $contract = NULL)
295     {
296         $endDate = clone $date;
297         $endDate->setDate($endDate->format('Y'), $endDate->format('n'), 1);
298         $endDate->setTime(0,0,0);
299         $endDate->subSecond(1);
300         
301         if (! $contract) {
302             $contract = $this->_referenceContract;
303         }
304         
305         $csdt = clone $contract->start_date;
306         
307         $csdt->setTimezone('UTC');
308         $endDate->setTimezone('UTC');
309         
310         $border = new Tinebase_DateTime(Sales_Config::getInstance()->get(Sales_Config::IGNORE_BILLABLES_BEFORE));
311         
312         // if this is not budgeted, show for timesheets in this period
313         $filter = new Timetracker_Model_TimesheetFilter(array(
314             array('field' => 'start_date', 'operator' => 'before_or_equals', 'value' => $endDate),
315             array('field' => 'start_date', 'operator' => 'after_or_equals', 'value' => $csdt),
316             array('field' => 'start_date', 'operator' => 'after_or_equals', 'value' => $border),
317             array('field' => 'is_cleared', 'operator' => 'equals', 'value' => FALSE),
318             array('field' => 'is_billable', 'operator' => 'equals', 'value' => TRUE),
319         ), 'AND');
320         
321         if (! is_null($contract->end_date)) {
322             $ced = clone $contract->end_date;
323             $ced->setTimezone('UTC');
324             
325             $filter->addFilter(new Tinebase_Model_Filter_Date(
326                 array('field' => 'start_date', 'operator' => 'before_or_equals', 'value' => $ced)
327             ));
328         }
329
330         $filter->addFilter(new Tinebase_Model_Filter_Text(
331             array('field' => 'invoice_id', 'operator' => 'equals', 'value' => '')
332         ));
333
334         $filter->addFilter(new Tinebase_Model_Filter_Text(
335             array('field' => 'timeaccount_id', 'operator' => 'equals', 'value' => $this->getId())
336         ));
337         
338         return $filter;
339     }
340     
341     /**
342      * returns the max interval of all billables
343      *
344      * @param Tinebase_DateTime $date
345      * @return array
346      */
347     public function getInterval(Tinebase_DateTime $date = NULL)
348     {
349         if (! $date) {
350             $date = $this->_referenceDate;
351         }
352         
353         // if this is a timeaccount with a budget, the timeaccount is the billable
354         if (intval($this->budget > 0)) {
355             
356             $startDate = clone $date;
357             $startDate->setDate($date->format('Y'), $date->format('n'), 1);
358             $endDate = clone $startDate;
359             $endDate->addMonth(1)->subSecond(1);
360             
361             $interval = array($startDate, $endDate);
362         } else {
363             $interval = parent::getInterval($date);
364         }
365         
366         return $interval;
367     }
368     
369     /**
370      * returns the quantity of this billable
371      *
372      * @return float
373      */
374     public function getQuantity()
375     {
376         return (float) $this->budget;
377     }
378     
379     /**
380      * loads billables for this record
381      *
382      * @param Tinebase_DateTime $date
383      * @param Sales_Model_ProductAggregate $productAggregate
384      * @return void
385     */
386     public function loadBillables(Tinebase_DateTime $date, Sales_Model_ProductAggregate $productAggregate)
387     {
388         $this->_referenceDate = $date;
389         $this->_billables = array();
390         
391         if (intval($this->budget) > 0) {
392             
393             $month = $date->format('Y-m');
394             
395             $this->_billables[$month] = array($this);
396             
397         } else {
398             if ($productAggregate !== null && $productAggregate->billing_point == 'end') {
399                 $enddate = $this->_getEndDate($productAggregate);
400             } else {
401                 $enddate = null;
402             }
403             
404             $filter = $this->_getBillableTimesheetsFilter($enddate !== null ? $enddate : $date);
405             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
406                 . ' TS Filter: ' . print_r($filter->toArray(), true));
407             $timesheets = Timetracker_Controller_Timesheet::getInstance()->search($filter);
408             
409             foreach($timesheets as $timesheet) {
410                 $month = new Tinebase_DateTime($timesheet->start_date);
411                 $month = $month->format('Y-m');
412                 
413                 if (! isset($this->_billables[$month])) {
414                     $this->_billables[$month] = array();
415                 }
416                 
417                 $this->_billables[$month][] = $timesheet;
418             }
419         }
420     }
421     
422     /**
423      * returns true if this record should be billed for the specified date
424      *
425      * @param Tinebase_DateTime $date
426      * @param Sales_Model_Contract $contract
427      * @param Sales_Model_ProductAggregate $productAggregate
428      * @return boolean
429     */
430     public function isBillable(Tinebase_DateTime $date, Sales_Model_Contract $contract = NULL, Sales_Model_ProductAggregate $productAggregate = NULL)
431     {
432         $this->_referenceDate = clone $date;
433         $this->_referenceContract = $contract;
434         
435         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($this->toArray(), true));
436         
437         if (! $this->is_open || $this->status == 'billed' || $this->cleared_at || $this->invoice_id) {
438             return FALSE;
439         }
440         
441         if (intval($this->budget) > 0) {
442              if ($this->status == 'to bill' && $this->invoice_id == NULL) {
443                 // if there is a budget, this timeaccount should be billed and there is no invoice linked, bill it
444                 return TRUE;
445              } else {
446                  return FALSE;
447              }
448         } else {
449             
450             if (! $this->is_billable) {
451                 return FALSE;
452             }
453             
454             if ($productAggregate !== null && $productAggregate->billing_point == 'end') {
455                 $enddate = $this->_getEndDate($productAggregate);
456             } else {
457                 $enddate = null;
458             }
459             
460             $pagination = new Tinebase_Model_Pagination(array('limit' => 1));
461             $filter = $this->_getBillableTimesheetsFilter($enddate !== null ? $enddate : $date, $contract);
462
463             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
464                 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
465                     . ' Use filter in "isBillable"-Method of Timetracker_Model_Timeaccount: '
466                     . print_r($filter->toArray(), 1));
467             }
468             
469             $timesheets = Timetracker_Controller_Timesheet::getInstance()->search($filter, $pagination, FALSE, /* $_onlyIds = */ TRUE);
470             
471             if (! empty($timesheets))  {
472                 return TRUE;
473             }
474         }
475         
476         // no match, not billable
477         return FALSE;
478     }
479     
480     /**
481      * returns the end date to look for timesheets
482      * 
483      * @param Sales_Model_ProductAggregate $productAggregate
484      * 
485      * @return Tinebase_DateTime
486      */
487     protected function _getEndDate(Sales_Model_ProductAggregate $productAggregate)
488     {
489         if ($productAggregate->last_autobill) {
490             $enddate = clone $productAggregate->last_autobill;
491         } else {
492             $enddate = clone ( ($productAggregate->start_date && $productAggregate->start_date->isLaterOrEquals($this->_referenceContract->start_date)) ? $productAggregate->start_date : $this->_referenceContract->start_date );  
493         }
494         while ($enddate->isEarlier($this->_referenceDate)) {
495             $enddate->addMonth($productAggregate->interval);
496         }
497         if ($enddate->isLater($this->_referenceDate)) {
498             $enddate->subMonth($productAggregate->interval);
499         }
500         
501         return $enddate;
502     }
503     
504     /**
505      * returns the name of the billable controller
506      *
507      * @return string
508      */
509     public static function getBillableControllerName() {
510         return 'Timetracker_Controller_Timesheet';
511     }
512     
513     /**
514      * returns the name of the billable filter
515      *
516      * @return string
517     */
518     public static function getBillableFilterName() {
519         return 'Timetracker_Model_TimesheetFilter';
520     }
521     
522     /**
523      * returns the name of the billable model
524      *
525      * @return string
526     */
527     public static function getBillableModelName() {
528         return 'Timetracker_Model_Timesheet';
529     }
530     
531     /**
532      * the invoice_id - field of all billables of this accountable gets the id of this invoice
533      *
534      * @param Sales_Model_Invoice $invoice
535      */
536     public function conjunctInvoiceWithBillables($invoice)
537     {
538         $tsController = Timetracker_Controller_Timesheet::getInstance();
539         $this->_disableTimesheetChecks($tsController);
540         
541         if (intval($this->budget) > 0) {
542             // set this ta billed
543             $this->invoice_id = $invoice->getId();
544             Timetracker_Controller_Timeaccount::getInstance()->update($this, FALSE);
545
546             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
547                 . ' TA got budget: set all unbilled TS of this TA billed');
548             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
549                 . ' TA:' . print_r($this->toArray(), true));
550
551             $filter = new Timetracker_Model_TimesheetFilter(array(
552                 array('field' => 'is_cleared', 'operator' => 'equals', 'value' => FALSE),
553                 array('field' => 'is_billable', 'operator' => 'equals', 'value' => TRUE),
554             ), 'AND');
555             // NOTE: using text filter here for id (operator equals is not defined in default timeaccount_id filter)
556             $filter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'timeaccount_id', 'operator' => 'equals', 'value' => $this->getId())));
557             $tsController->updateMultiple($filter, array('invoice_id' => $invoice->getId()));
558         } else {
559             $ids = $this->_getIdsOfBillables();
560             
561             if (! empty($ids)) {
562                 $filter = new Timetracker_Model_TimesheetFilter(array());
563                 $filter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'id', 'operator' => 'in', 'value' => $ids)));
564
565                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
566                     . ' Bill ' . count($ids) . ' TS');
567
568                 $tsController->updateMultiple($filter, array('invoice_id' => $invoice->getId()));
569             }
570         }
571         
572         $this->_enableTimesheetChecks($tsController);
573     }
574     
575     /**
576      * disable ts checks
577      * 
578      * @param Timetracker_Controller_Timesheet $tsController
579      */
580     protected function _disableTimesheetChecks($tsController)
581     {
582         $tsController->doCheckDeadLine(false);
583         $tsController->doContainerACLChecks(false);
584         $tsController->doRightChecks(false);
585         $tsController->doRelationUpdate(false);
586     }
587     
588     /**
589      * enable ts checks
590      * 
591      * @param Timetracker_Controller_Timesheet $tsController
592      */
593     protected function _enableTimesheetChecks($tsController)
594     {
595         $tsController->doCheckDeadLine(true);
596         $tsController->doContainerACLChecks(true);
597         $tsController->doRightChecks(true);
598         $tsController->doRelationUpdate(true);
599     }
600     
601     /**
602      * returns the unit of this billable
603      *
604      * @return string
605      */
606     public function getUnit()
607     {
608         return 'hour'; // _('hour')
609     }
610     
611     /**
612      * set each billable of this accountable billed
613      *
614      * @param Sales_Model_Invoice $invoice
615      */
616     public function clearBillables(Sales_Model_Invoice $invoice)
617     {
618         $tsController = Timetracker_Controller_Timesheet::getInstance();
619         $this->_disableTimesheetChecks($tsController);
620         
621         $filter = new Timetracker_Model_TimesheetFilter(array(), 'AND');
622         $filter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'is_cleared', 'operator' => 'equals', 'value' => 0)));
623         $filter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'timeaccount_id', 'operator' => 'equals', 'value' => $this->getId())));
624         $filter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'invoice_id', 'operator' => 'equals', 'value' => $invoice->getId())));
625         
626         // if this timeaccount has a budget, close and bill this and set cleared at date
627         if (intval($this->budget) > 0) {
628             $this->is_open    = 0;
629             $this->status     = 'billed';
630             $this->cleared_at = Tinebase_DateTime::now();
631             
632             Timetracker_Controller_Timeaccount::getInstance()->update($this);
633             // also clear all timesheets belonging to this invoice and timeaccount
634             $tsController->updateMultiple($filter, array('is_cleared' => 1));
635         } else {
636             // otherwise clear all timesheets of this invoice
637             $tsController->updateMultiple($filter, array('is_cleared' => 1));
638         }
639         
640         $this->_enableTimesheetChecks($tsController);
641     }
642
643     /**
644      * returns true if this invoice needs to be recreated because data changed
645      *
646      * @param Tinebase_DateTime $date
647      * @param Sales_Model_ProductAggregate $productAggregate
648      * @param Sales_Model_Invoice $invoice
649      * @param Sales_Model_Contract $contract
650      * @return boolean
651      */
652     public function needsInvoiceRecreation(Tinebase_DateTime $date, Sales_Model_ProductAggregate $productAggregate, Sales_Model_Invoice $invoice, Sales_Model_Contract $contract)
653     {
654         $filter = new Timetracker_Model_TimesheetFilter(array(), 'AND');
655         $filter->addFilter(new Tinebase_Model_Filter_Text(
656             array('field' => 'invoice_id', 'operator' => 'equals', 'value' => $invoice->getId())
657         ));
658         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
659             . ' TS Filter: ' . print_r($filter->toArray(), true));
660         $timesheets = Timetracker_Controller_Timesheet::getInstance()->search($filter);
661         $timesheets->setTimezone(Tinebase_Core::getUserTimezone());
662         foreach($timesheets as $timesheet)
663         {
664             if ($timesheet->last_modified_time && $timesheet->last_modified_time->isLater($invoice->creation_time)) {
665                 return true;
666             }
667         }
668
669         return false;
670     }
671 }