3 * class to hold Timeaccount data
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)
11 * @todo update validators (default values, mandatory fields)
12 * @todo add setFromJson with relation handling
16 * class to hold Timeaccount data
18 * @package Timetracker
21 class Timetracker_Model_Timeaccount extends Sales_Model_Accountable_Abstract
24 * key in $_validators/$_properties array for the filed which
25 * represents the identifier
29 protected $_identifier = 'id';
32 * application the record belongs to
36 protected $_application = 'Timetracker';
39 * holds the configuration object (must be declared in the concrete class)
41 * @var Tinebase_ModelConfiguration
43 protected static $_configurationObject = NULL;
46 * Holds the model configuration (must be assigned in the concrete class)
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,
59 'modlogActive' => TRUE,
60 'hasAttachments' => TRUE,
61 'createModule' => TRUE,
62 'containerProperty' => 'container_id',
63 'multipleEdit' => TRUE,
64 'requiredRight' => 'manage',
66 'titleProperty' => 'title',
67 'appName' => 'Timetracker',
68 'modelName' => 'Timeaccount',
70 'filterModel' => array(
72 'filter' => 'Tinebase_Model_Filter_ExplicitRelatedRecord',
73 'title' => 'Contract', // _('Contract')
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',
81 'jsConfig' => array('filtertype' => 'timetracker.timeaccountcontract')
83 'responsible' => array(
84 'filter' => 'Tinebase_Model_Filter_ExplicitRelatedRecord',
85 'title' => 'Responsible',
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',
93 'jsConfig' => array('filtertype' => 'timetracker.timeaccountresponsible')
98 'account_grants' => array(
100 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
103 'label' => 'Title', //_('Title')
104 'duplicateCheckGroup' => 'title',
105 'queryFilter' => TRUE,
106 'showInDetailsPanel' => TRUE,
107 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => false, 'presence'=>'required'),
110 'label' => 'Number', //_('Number')
111 'duplicateCheckGroup' => 'number',
112 'queryFilter' => TRUE,
113 'showInDetailsPanel' => TRUE,
114 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
116 'description' => array(
117 'label' => 'Description', // _('Description')
119 'showInDetailsPanel' => TRUE,
120 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
124 'inputFilters' => array('Zend_Filter_Digits', 'Zend_Filter_Empty' => NULL),
125 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
127 'budget_unit' => array(
129 'default' => 'hours',
130 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => 'hours'),
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),
137 'price_unit' => array(
139 'default' => 'hours',
140 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => 'hours'),
143 // is_open = Status, status = Billed
144 'label' => 'Status', //_('Status')
147 'inputFilters' => array('Zend_Filter_Empty' => 0),
148 'filterDefinition' => array(
149 'filter' => 'Tinebase_Model_Filter_Bool',
150 'jsConfig' => array('filtertype' => 'timetracker.timeaccountstatus')
152 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => 1),
154 'is_billable' => array(
157 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => 1),
159 'billed_in' => array(
160 'label' => "Cleared in", // _("Cleared in"),
161 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
164 'invoice_id' => array(
165 'label' => 'Invoice', // _('Invoice')
167 'inputFilters' => array('Zend_Filter_Empty' => NULL),
169 'appName' => 'Sales',
170 'modelName' => 'Invoice',
173 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
177 // is_open = Status, status = Billed
178 'label' => 'Billed', //_('Billed')
180 'filterDefinition' => array(
181 'filter' => 'Tinebase_Model_Filter_Text',
182 'jsConfig' => array('filtertype' => 'timetracker.timeaccountbilled')
184 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => 'not yet billed'),
187 'cleared_at' => array(
188 'label' => "Cleared at", // _("Cleared at")
189 'type' => 'datetime',
190 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
194 'label' => 'Booking deadline', // _('Booking deadline')
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)),
206 'appName' => 'Timetracker',
207 'modelName' => 'TimeaccountGrants',
210 'validators' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
212 'responsible' => array(
215 'type' => 'relation',
216 'label' => 'Responsible', // _('Responsible')
218 'appName' => 'Addressbook',
219 'modelName' => 'Contact',
220 'type' => 'RESPONSIBLE'
229 * @see Tinebase_Record_Abstract
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')
236 array('relatedApp' => 'Addressbook', 'relatedModel' => 'Contact', 'config' => array(
237 array('type' => 'RESPONSIBLE', 'degree' => 'sibling', 'text' => 'Responsible Person', 'max' => '1:0'), // _('Responsible Person')
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)
252 protected static $_resolveForeignIdFields = array(
253 'Sales_Model_Invoice' => array('invoice_id'),
257 * relation type: contract
260 const RELATION_TYPE_CONTRACT = 'CONTRACT';
263 * deadline type: none
264 * = no deadline for timesheets
266 const DEADLINE_NONE = 'none';
269 * deadline type: last week
270 * = booking timesheets allowed until monday midnight for the last week
272 const DEADLINE_LASTWEEK = 'lastweek';
275 * set from array data
277 * @param array $_data
280 public function setFromArray(array $_data)
282 parent::setFromArray($_data);
284 if (isset($_data['grants']) && !empty($_data['grants'])) {
285 $this->grants = new Tinebase_Record_RecordSet('Timetracker_Model_TimeaccountGrants', $_data['grants']);
287 $this->grants = new Tinebase_Record_RecordSet('Timetracker_Model_TimeaccountGrants');
292 * returns the timesheet filter
294 * @param Tinebase_DateTime $date
295 * @param Sales_Model_Contract
296 * @return Timetracker_Model_TimesheetFilter
298 protected function _getBillableTimesheetsFilter(Tinebase_DateTime $date, Sales_Model_Contract $contract = NULL)
300 $endDate = clone $date;
301 $endDate->setDate($endDate->format('Y'), $endDate->format('n'), 1);
302 $endDate->setTime(0,0,0);
303 $endDate->subSecond(1);
306 $contract = $this->_referenceContract;
309 $csdt = clone $contract->start_date;
311 $csdt->setTimezone('UTC');
312 $endDate->setTimezone('UTC');
314 $border = new Tinebase_DateTime(Sales_Config::getInstance()->get(Sales_Config::IGNORE_BILLABLES_BEFORE));
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),
325 if (! is_null($contract->end_date)) {
326 $ced = clone $contract->end_date;
327 $ced->setTimezone('UTC');
329 $filter->addFilter(new Tinebase_Model_Filter_Date(
330 array('field' => 'start_date', 'operator' => 'before_or_equals', 'value' => $ced)
334 $filter->addFilter(new Tinebase_Model_Filter_Text(
335 array('field' => 'invoice_id', 'operator' => 'equals', 'value' => '')
338 $filter->addFilter(new Tinebase_Model_Filter_Text(
339 array('field' => 'timeaccount_id', 'operator' => 'equals', 'value' => $this->getId())
346 * returns the max interval of all billables
348 * @param Tinebase_DateTime $date
351 public function getInterval(Tinebase_DateTime $date = NULL)
354 $date = $this->_referenceDate;
357 // if this is a timeaccount with a budget, the timeaccount is the billable
358 if (intval($this->budget > 0)) {
360 $startDate = clone $date;
361 $startDate->setDate($date->format('Y'), $date->format('n'), 1);
362 $endDate = clone $startDate;
363 $endDate->addMonth(1)->subSecond(1);
365 $interval = array($startDate, $endDate);
367 $interval = parent::getInterval($date);
374 * returns the quantity of this billable
378 public function getQuantity()
380 return (float) $this->budget;
384 * loads billables for this record
386 * @param Tinebase_DateTime $date
387 * @param Sales_Model_ProductAggregate $productAggregate
390 public function loadBillables(Tinebase_DateTime $date, Sales_Model_ProductAggregate $productAggregate)
392 $this->_referenceDate = $date;
393 $this->_billables = array();
395 if (intval($this->budget) > 0) {
397 $month = $date->format('Y-m');
399 $this->_billables[$month] = array($this);
402 if ($productAggregate !== null && $productAggregate->billing_point == 'end') {
403 $enddate = $this->_getEndDate($productAggregate);
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);
413 foreach($timesheets as $timesheet) {
414 $month = new Tinebase_DateTime($timesheet->start_date);
415 $month = $month->format('Y-m');
417 if (! isset($this->_billables[$month])) {
418 $this->_billables[$month] = array();
421 $this->_billables[$month][] = $timesheet;
427 * returns true if this record should be billed for the specified date
429 * @param Tinebase_DateTime $date
430 * @param Sales_Model_Contract $contract
431 * @param Sales_Model_ProductAggregate $productAggregate
434 public function isBillable(Tinebase_DateTime $date, Sales_Model_Contract $contract = NULL, Sales_Model_ProductAggregate $productAggregate = NULL)
436 $this->_referenceDate = clone $date;
437 $this->_referenceContract = $contract;
439 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' ' . print_r($this->toArray(), true));
441 if (! $this->is_open || $this->status == 'billed' || $this->cleared_at || $this->invoice_id) {
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
454 if (! $this->is_billable) {
458 if ($productAggregate !== null && $productAggregate->billing_point == 'end') {
459 $enddate = $this->_getEndDate($productAggregate);
464 $pagination = new Tinebase_Model_Pagination(array('limit' => 1));
465 $filter = $this->_getBillableTimesheetsFilter($enddate !== null ? $enddate : $date, $contract);
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));
473 $timesheets = Timetracker_Controller_Timesheet::getInstance()->search($filter, $pagination, FALSE, /* $_onlyIds = */ TRUE);
475 if (! empty($timesheets)) {
480 // no match, not billable
485 * returns the end date to look for timesheets
487 * @param Sales_Model_ProductAggregate $productAggregate
489 * @return Tinebase_DateTime
491 protected function _getEndDate(Sales_Model_ProductAggregate $productAggregate)
493 if ($productAggregate->last_autobill) {
494 $enddate = clone $productAggregate->last_autobill;
496 $enddate = clone ( ($productAggregate->start_date && $productAggregate->start_date->isLaterOrEquals($this->_referenceContract->start_date)) ? $productAggregate->start_date : $this->_referenceContract->start_date );
498 while ($enddate->isEarlier($this->_referenceDate)) {
499 $enddate->addMonth($productAggregate->interval);
501 if ($enddate->isLater($this->_referenceDate)) {
502 $enddate->subMonth($productAggregate->interval);
509 * returns the name of the billable controller
513 public static function getBillableControllerName() {
514 return 'Timetracker_Controller_Timesheet';
518 * returns the name of the billable filter
522 public static function getBillableFilterName() {
523 return 'Timetracker_Model_TimesheetFilter';
527 * returns the name of the billable model
531 public static function getBillableModelName() {
532 return 'Timetracker_Model_Timesheet';
536 * the invoice_id - field of all billables of this accountable gets the id of this invoice
538 * @param Sales_Model_Invoice $invoice
540 public function conjunctInvoiceWithBillables($invoice)
542 $tsController = Timetracker_Controller_Timesheet::getInstance();
543 $this->_disableTimesheetChecks($tsController);
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);
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));
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),
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()));
563 $ids = $this->_getIdsOfBillables();
566 $filter = new Timetracker_Model_TimesheetFilter(array());
567 $filter->addFilter(new Tinebase_Model_Filter_Text(array('field' => 'id', 'operator' => 'in', 'value' => $ids)));
569 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
570 . ' Bill ' . count($ids) . ' TS');
572 $tsController->updateMultiple($filter, array('invoice_id' => $invoice->getId()));
576 $this->_enableTimesheetChecks($tsController);
582 * @param Timetracker_Controller_Timesheet $tsController
584 protected function _disableTimesheetChecks($tsController)
586 $tsController->doCheckDeadLine(false);
587 $tsController->doRightChecks(false);
588 $tsController->doRelationUpdate(false);
589 $tsController->setRequestContext(array('skipClosedCheck' => true));
595 * @param Timetracker_Controller_Timesheet $tsController
597 protected function _enableTimesheetChecks($tsController)
599 $tsController->doCheckDeadLine(true);
600 $tsController->doRightChecks(true);
601 $tsController->doRelationUpdate(true);
602 $tsController->setRequestContext(array('skipClosedCheck' => false));
606 * returns the unit of this billable
610 public function getUnit()
612 return 'hour'; // _('hour')
616 * set each billable of this accountable billed
618 * @param Sales_Model_Invoice $invoice
620 public function clearBillables(Sales_Model_Invoice $invoice)
622 $tsController = Timetracker_Controller_Timesheet::getInstance();
623 $this->_disableTimesheetChecks($tsController);
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())));
630 // if this timeaccount has a budget, close and bill this and set cleared at date
631 if (intval($this->budget) > 0) {
633 $this->status = 'billed';
634 $this->cleared_at = Tinebase_DateTime::now();
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));
640 // otherwise clear all timesheets of this invoice
641 $tsController->updateMultiple($filter, array('is_cleared' => 1));
644 $this->_enableTimesheetChecks($tsController);
648 * returns true if this invoice needs to be recreated because data changed
650 * @param Tinebase_DateTime $date
651 * @param Sales_Model_ProductAggregate $productAggregate
652 * @param Sales_Model_Invoice $invoice
653 * @param Sales_Model_Contract $contract
656 public function needsInvoiceRecreation(Tinebase_DateTime $date, Sales_Model_ProductAggregate $productAggregate, Sales_Model_Invoice $invoice, Sales_Model_Contract $contract)
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())
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)
668 if ($timesheet->last_modified_time && $timesheet->last_modified_time->isLater($invoice->creation_time)) {