0012188: add copyOmitFields to modelconfig
[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                 'copyOmit'              => true,
163             ),
164             'invoice_id'        => array(
165                 'label'                 => 'Invoice', // _('Invoice')
166                 'type'                  => 'record',
167                 'inputFilters'          => array('Zend_Filter_Empty' => NULL),
168                 'config'                => array(
169                     'appName'               => 'Sales',
170                     'modelName'             => 'Invoice',
171                     'idProperty'            => 'id'
172                 ),
173                 'validators'            => array(Zend_Filter_Input::ALLOW_EMPTY => true),
174                 'copyOmit'              => true,
175             ),
176             'status'            => array(
177                 // is_open = Status, status = Billed
178                 'label'                 => 'Billed', //_('Billed')
179                 'type'                  => 'string',
180                 'filterDefinition'      => array(
181                     'filter'                => 'Tinebase_Model_Filter_Text',
182                     'jsConfig'              => array('filtertype' => 'timetracker.timeaccountbilled')
183                 ),
184                 'validators'            => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => 'not yet billed'),
185                 'copyOmit'              => true,
186             ),
187             'cleared_at'        => array(
188                 'label'                 => "Cleared at", // _("Cleared at")
189                 'type'                  => 'datetime',
190                 'validators'            => array(Zend_Filter_Input::ALLOW_EMPTY => true),
191                 'copyOmit'              => true,
192             ),
193             'deadline'          => array(
194                 'label'                 => 'Booking deadline', // _('Booking deadline')
195                 'type'                  => 'string',
196                 'validators'            => array(
197                                             Zend_Filter_Input::ALLOW_EMPTY      => true,
198                                             Zend_Filter_Input::DEFAULT_VALUE    => self::DEADLINE_NONE,
199                                                 array('InArray', array(self::DEADLINE_NONE, self::DEADLINE_LASTWEEK)),
200                                             )
201             ),
202             'grants'            => array(
203                 'label'                 => NULL,
204                 'type'                  => 'records',
205                 'config'                => array(
206                     'appName'               => 'Timetracker',
207                     'modelName'             => 'TimeaccountGrants',
208                     'idProperty'            => 'id'
209                 ),
210                 'validators'            => array(Zend_Filter_Input::ALLOW_EMPTY => true),
211             ),
212             'responsible'       => array(
213                 'type'                  => 'virtual',
214                 'config'                => array(
215                     'type'                  => 'relation',
216                     'label'                 => 'Responsible',    // _('Responsible')
217                     'config'                => array(
218                         'appName'               => 'Addressbook',
219                         'modelName'             => 'Contact',
220                         'type'                  => 'RESPONSIBLE'
221                     )
222                 )
223             ),
224
225         )
226     );
227
228     /**
229      * @see Tinebase_Record_Abstract
230      */
231     protected static $_relatableConfig = array(
232         array('relatedApp' => 'Sales', 'relatedModel' => 'CostCenter', 'config' => array(
233             array('type' => 'COST_CENTER', 'degree' => 'sibling', 'text' => 'Cost Center', 'max' => '1:0'), // _('Cost Center')
234             )
235         ),
236         array('relatedApp' => 'Addressbook', 'relatedModel' => 'Contact', 'config' => array(
237             array('type' => 'RESPONSIBLE', 'degree' => 'sibling', 'text' => 'Responsible Person', 'max' => '1:0'), // _('Responsible Person')
238         )
239         )
240     );
241     
242     /**
243      * if foreign Id fields should be resolved on search and get from json
244      * should have this format:
245      *     array('Calendar_Model_Contact' => 'contact_id', ...)
246      * or for more fields:
247      *     array('Calendar_Model_Contact' => array('contact_id', 'customer_id), ...)
248      * (e.g. resolves contact_id with the corresponding Model)
249      *
250      * @var array
251      */
252     protected static $_resolveForeignIdFields = array(
253         'Sales_Model_Invoice' => array('invoice_id'),
254     );
255     
256     /**
257      * relation type: contract
258      *
259      */
260     const RELATION_TYPE_CONTRACT = 'CONTRACT';
261     
262     /**
263      * deadline type: none
264      * = no deadline for timesheets
265      */
266     const DEADLINE_NONE = 'none';
267     
268     /**
269      * deadline type: last week
270      * = booking timesheets allowed until monday midnight for the last week
271      */
272     const DEADLINE_LASTWEEK = 'lastweek';
273
274     /**
275      * set from array data
276      *
277      * @param array $_data
278      * @return void
279      */
280     public function setFromArray(array $_data)
281     {
282         parent::setFromArray($_data);
283         
284         if (isset($_data['grants']) && !empty($_data['grants'])) {
285             $this->grants = new Tinebase_Record_RecordSet('Timetracker_Model_TimeaccountGrants', $_data['grants']);
286         }  else {
287             $this->grants = new Tinebase_Record_RecordSet('Timetracker_Model_TimeaccountGrants');
288         }
289     }
290     
291     /**
292      * returns the timesheet filter 
293      * 
294      * @param Tinebase_DateTime $date
295      * @param Sales_Model_Contract
296      * @return Timetracker_Model_TimesheetFilter
297      */
298     protected function _getBillableTimesheetsFilter(Tinebase_DateTime $date, Sales_Model_Contract $contract = NULL)
299     {
300         $endDate = clone $date;
301         $endDate->setDate($endDate->format('Y'), $endDate->format('n'), 1);
302         $endDate->setTime(0,0,0);
303         $endDate->subSecond(1);
304         
305         if (! $contract) {
306             $contract = $this->_referenceContract;
307         }
308         
309         $csdt = clone $contract->start_date;
310         
311         $csdt->setTimezone('UTC');
312         $endDate->setTimezone('UTC');
313         
314         $border = new Tinebase_DateTime(Sales_Config::getInstance()->get(Sales_Config::IGNORE_BILLABLES_BEFORE));
315         
316         // if this is not budgeted, show for timesheets in this period
317         $filter = new Timetracker_Model_TimesheetFilter(array(
318             array('field' => 'start_date', 'operator' => 'before_or_equals', 'value' => $endDate),
319             array('field' => 'start_date', 'operator' => 'after_or_equals', 'value' => $csdt),
320             array('field' => 'start_date', 'operator' => 'after_or_equals', 'value' => $border),
321             array('field' => 'is_cleared', 'operator' => 'equals', 'value' => FALSE),
322             array('field' => 'is_billable', 'operator' => 'equals', 'value' => TRUE),
323         ), 'AND');
324         
325         if (! is_null($contract->end_date)) {
326             $ced = clone $contract->end_date;
327             $ced->setTimezone('UTC');
328             
329             $filter->addFilter(new Tinebase_Model_Filter_Date(
330                 array('field' => 'start_date', 'operator' => 'before_or_equals', 'value' => $ced)
331             ));
332         }
333
334         $filter->addFilter(new Tinebase_Model_Filter_Text(
335             array('field' => 'invoice_id', 'operator' => 'equals', 'value' => '')
336         ));
337
338         $filter->addFilter(new Tinebase_Model_Filter_Text(
339             array('field' => 'timeaccount_id', 'operator' => 'equals', 'value' => $this->getId())
340         ));
341         
342         return $filter;
343     }
344     
345     /**
346      * returns the max interval of all billables
347      *
348      * @param Tinebase_DateTime $date
349      * @return array
350      */
351     public function getInterval(Tinebase_DateTime $date = NULL)
352     {
353         if (! $date) {
354             $date = $this->_referenceDate;
355         }
356         
357         // if this is a timeaccount with a budget, the timeaccount is the billable
358         if (intval($this->budget > 0)) {
359             
360             $startDate = clone $date;
361             $startDate->setDate($date->format('Y'), $date->format('n'), 1);
362             $endDate = clone $startDate;
363             $endDate->addMonth(1)->subSecond(1);
364             
365             $interval = array($startDate, $endDate);
366         } else {
367             $interval = parent::getInterval($date);
368         }
369         
370         return $interval;
371     }
372     
373     /**
374      * returns the quantity of this billable
375      *
376      * @return float
377      */
378     public function getQuantity()
379     {
380         return (float) $this->budget;
381     }
382     
383     /**
384      * loads billables for this record
385      *
386      * @param Tinebase_DateTime $date
387      * @param Sales_Model_ProductAggregate $productAggregate
388      * @return void
389     */
390     public function loadBillables(Tinebase_DateTime $date, Sales_Model_ProductAggregate $productAggregate)
391     {
392         $this->_referenceDate = $date;
393         $this->_billables = array();
394         
395         if (intval($this->budget) > 0) {
396             
397             $month = $date->format('Y-m');
398             
399             $this->_billables[$month] = array($this);
400             
401         } else {
402             if ($productAggregate !== null && $productAggregate->billing_point == 'end') {
403                 $enddate = $this->_getEndDate($productAggregate);
404             } else {
405                 $enddate = null;
406             }
407             
408             $filter = $this->_getBillableTimesheetsFilter($enddate !== null ? $enddate : $date);
409             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
410                 . ' TS Filter: ' . print_r($filter->toArray(), true));
411             $timesheets = Timetracker_Controller_Timesheet::getInstance()->search($filter);
412             
413             foreach($timesheets as $timesheet) {
414                 $month = new Tinebase_DateTime($timesheet->start_date);
415                 $month = $month->format('Y-m');
416                 
417                 if (! isset($this->_billables[$month])) {
418                     $this->_billables[$month] = array();
419                 }
420                 
421                 $this->_billables[$month][] = $timesheet;
422             }
423         }
424     }
425     
426     /**
427      * returns true if this record should be billed for the specified date
428      *
429      * @param Tinebase_DateTime $date
430      * @param Sales_Model_Contract $contract
431      * @param Sales_Model_ProductAggregate $productAggregate
432      * @return boolean
433     */
434     public function isBillable(Tinebase_DateTime $date, Sales_Model_Contract $contract = NULL, Sales_Model_ProductAggregate $productAggregate = NULL)
435     {
436         $this->_referenceDate = clone $date;
437         $this->_referenceContract = $contract;
438         
439         if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($this->toArray(), true));
440         
441         if (! $this->is_open || $this->status == 'billed' || $this->cleared_at || $this->invoice_id) {
442             return FALSE;
443         }
444         
445         if (intval($this->budget) > 0) {
446              if ($this->status == 'to bill' && $this->invoice_id == NULL) {
447                 // if there is a budget, this timeaccount should be billed and there is no invoice linked, bill it
448                 return TRUE;
449              } else {
450                  return FALSE;
451              }
452         } else {
453             
454             if (! $this->is_billable) {
455                 return FALSE;
456             }
457             
458             if ($productAggregate !== null && $productAggregate->billing_point == 'end') {
459                 $enddate = $this->_getEndDate($productAggregate);
460             } else {
461                 $enddate = null;
462             }
463             
464             $pagination = new Tinebase_Model_Pagination(array('limit' => 1));
465             $filter = $this->_getBillableTimesheetsFilter($enddate !== null ? $enddate : $date, $contract);
466
467             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) {
468                 Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
469                     . ' Use filter in "isBillable"-Method of Timetracker_Model_Timeaccount: '
470                     . print_r($filter->toArray(), 1));
471             }
472             
473             $timesheets = Timetracker_Controller_Timesheet::getInstance()->search($filter, $pagination, FALSE, /* $_onlyIds = */ TRUE);
474             
475             if (! empty($timesheets))  {
476                 return TRUE;
477             }
478         }
479         
480         // no match, not billable
481         return FALSE;
482     }
483     
484     /**
485      * returns the end date to look for timesheets
486      * 
487      * @param Sales_Model_ProductAggregate $productAggregate
488      * 
489      * @return Tinebase_DateTime
490      */
491     protected function _getEndDate(Sales_Model_ProductAggregate $productAggregate)
492     {
493         if ($productAggregate->last_autobill) {
494             $enddate = clone $productAggregate->last_autobill;
495         } else {
496             $enddate = clone ( ($productAggregate->start_date && $productAggregate->start_date->isLaterOrEquals($this->_referenceContract->start_date)) ? $productAggregate->start_date : $this->_referenceContract->start_date );  
497         }
498         while ($enddate->isEarlier($this->_referenceDate)) {
499             $enddate->addMonth($productAggregate->interval);
500         }
501         if ($enddate->isLater($this->_referenceDate)) {
502             $enddate->subMonth($productAggregate->interval);
503         }
504         
505         return $enddate;
506     }
507     
508     /**
509      * returns the name of the billable controller
510      *
511      * @return string
512      */
513     public static function getBillableControllerName() {
514         return 'Timetracker_Controller_Timesheet';
515     }
516     
517     /**
518      * returns the name of the billable filter
519      *
520      * @return string
521     */
522     public static function getBillableFilterName() {
523         return 'Timetracker_Model_TimesheetFilter';
524     }
525     
526     /**
527      * returns the name of the billable model
528      *
529      * @return string
530     */
531     public static function getBillableModelName() {
532         return 'Timetracker_Model_Timesheet';
533     }
534     
535     /**
536      * the invoice_id - field of all billables of this accountable gets the id of this invoice
537      *
538      * @param Sales_Model_Invoice $invoice
539      */
540     public function conjunctInvoiceWithBillables($invoice)
541     {
542         $tsController = Timetracker_Controller_Timesheet::getInstance();
543         $this->_disableTimesheetChecks($tsController);
544         
545         if (intval($this->budget) > 0) {
546             // set this ta billed
547             $this->invoice_id = $invoice->getId();
548             Timetracker_Controller_Timeaccount::getInstance()->update($this, FALSE);
549
550             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
551                 . ' TA got budget: set all unbilled TS of this TA billed');
552             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
553                 . ' TA:' . print_r($this->toArray(), true));
554
555             $filter = new Timetracker_Model_TimesheetFilter(array(
556                 array('field' => 'is_cleared', 'operator' => 'equals', 'value' => FALSE),
557                 array('field' => 'is_billable', 'operator' => 'equals', 'value' => TRUE),
558             ), 'AND');
559             // NOTE: using text filter here for id (operator equals is not defined in default timeaccount_id filter)
560             $filter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'timeaccount_id', 'operator' => 'equals', 'value' => $this->getId())));
561             $tsController->updateMultiple($filter, array('invoice_id' => $invoice->getId()));
562         } else {
563             $ids = $this->_getIdsOfBillables();
564             
565             if (! empty($ids)) {
566                 $filter = new Timetracker_Model_TimesheetFilter(array());
567                 $filter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'id', 'operator' => 'in', 'value' => $ids)));
568
569                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
570                     . ' Bill ' . count($ids) . ' TS');
571
572                 $tsController->updateMultiple($filter, array('invoice_id' => $invoice->getId()));
573             }
574         }
575         
576         $this->_enableTimesheetChecks($tsController);
577     }
578     
579     /**
580      * disable ts checks
581      * 
582      * @param Timetracker_Controller_Timesheet $tsController
583      */
584     protected function _disableTimesheetChecks($tsController)
585     {
586         $tsController->doCheckDeadLine(false);
587         $tsController->doRightChecks(false);
588         $tsController->doRelationUpdate(false);
589         $tsController->setRequestContext(array('skipClosedCheck' => true));
590     }
591     
592     /**
593      * enable ts checks
594      * 
595      * @param Timetracker_Controller_Timesheet $tsController
596      */
597     protected function _enableTimesheetChecks($tsController)
598     {
599         $tsController->doCheckDeadLine(true);
600         $tsController->doRightChecks(true);
601         $tsController->doRelationUpdate(true);
602         $tsController->setRequestContext(array('skipClosedCheck' => false));
603     }
604     
605     /**
606      * returns the unit of this billable
607      *
608      * @return string
609      */
610     public function getUnit()
611     {
612         return 'hour'; // _('hour')
613     }
614     
615     /**
616      * set each billable of this accountable billed
617      *
618      * @param Sales_Model_Invoice $invoice
619      */
620     public function clearBillables(Sales_Model_Invoice $invoice)
621     {
622         $tsController = Timetracker_Controller_Timesheet::getInstance();
623         $this->_disableTimesheetChecks($tsController);
624         
625         $filter = new Timetracker_Model_TimesheetFilter(array(), 'AND');
626         $filter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'is_cleared', 'operator' => 'equals', 'value' => 0)));
627         $filter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'timeaccount_id', 'operator' => 'equals', 'value' => $this->getId())));
628         $filter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'invoice_id', 'operator' => 'equals', 'value' => $invoice->getId())));
629         
630         // if this timeaccount has a budget, close and bill this and set cleared at date
631         if (intval($this->budget) > 0) {
632             $this->is_open    = 0;
633             $this->status     = 'billed';
634             $this->cleared_at = Tinebase_DateTime::now();
635             
636             Timetracker_Controller_Timeaccount::getInstance()->update($this);
637             // also clear all timesheets belonging to this invoice and timeaccount
638             $tsController->updateMultiple($filter, array('is_cleared' => 1));
639         } else {
640             // otherwise clear all timesheets of this invoice
641             $tsController->updateMultiple($filter, array('is_cleared' => 1));
642         }
643         
644         $this->_enableTimesheetChecks($tsController);
645     }
646
647     /**
648      * returns true if this invoice needs to be recreated because data changed
649      *
650      * @param Tinebase_DateTime $date
651      * @param Sales_Model_ProductAggregate $productAggregate
652      * @param Sales_Model_Invoice $invoice
653      * @param Sales_Model_Contract $contract
654      * @return boolean
655      */
656     public function needsInvoiceRecreation(Tinebase_DateTime $date, Sales_Model_ProductAggregate $productAggregate, Sales_Model_Invoice $invoice, Sales_Model_Contract $contract)
657     {
658         $filter = new Timetracker_Model_TimesheetFilter(array(), 'AND');
659         $filter->addFilter(new Tinebase_Model_Filter_Text(
660             array('field' => 'invoice_id', 'operator' => 'equals', 'value' => $invoice->getId())
661         ));
662         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
663             . ' TS Filter: ' . print_r($filter->toArray(), true));
664         $timesheets = Timetracker_Controller_Timesheet::getInstance()->search($filter);
665         $timesheets->setTimezone(Tinebase_Core::getUserTimezone());
666         foreach($timesheets as $timesheet)
667         {
668             if ($timesheet->last_modified_time && $timesheet->last_modified_time->isLater($invoice->creation_time)) {
669                 return true;
670             }
671         }
672
673         return false;
674     }
675 }