/** * gets the personal calendar of given user * * @param string $_userId * @return Tinebase_Model_Container */ protected function _getPersonalCalendar($_userId) { if (!array_key_exists($_userId, $this->_personalCalendarCache)) { // get calendar by preference to ensure its the default personal $defaultCalendarId = Tinebase_Core::getPreference('Calendar')->getValueForUser(Calendar_Preference::DEFAULTCALENDAR, $_userId, Tinebase_Acl_Rights::ACCOUNT_TYPE_USER); $calendar = Tinebase_Container::getInstance()->getContainerById($defaultCalendarId); // detect if container just got created $isNewContainer = false; if ($calendar->creation_time instanceof DateTime) { $isNewContainer = $this->_migrationStartTime->isEarlier($calendar->creation_time); } if ($isNewContainer && $this->_config->setPersonalCalendarGrants || $this->_config->forcePersonalCalendarGrants) { // resolve grants based on user/groupmemberships $grants = $this->getGrantsByOwner('Calendar', $_userId); Tinebase_Container::getInstance()->setGrants($calendar->getId(), $grants, TRUE); } $this->_personalCalendarCache[$_userId] = $calendar; } return $this->_personalCalendarCache[$_userId]; }
/** * returns next occurrence _ignoring exceptions_ or NULL if there is none/not computable * * NOTE: an ongoing event during $from [start, end[ is considered as next * NOTE: for previous events on ongoing event is considered as previous * * NOTE: computing the next occurrence of an open end rrule can be dangerous, as it might result * in a endless loop. Therefore we only make a limited number of attempts before giving up. * * @param Calendar_Model_Event $_event * @param Tinebase_Record_RecordSet $_exceptions * @param Tinebase_DateTime $_from * @param Int $_which * @return Calendar_Model_Event|NULL */ public static function computeNextOccurrence($_event, $_exceptions, $_from, $_which = 1) { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' $from = ' . $_from->toString()); } if ($_which === 0 || $_event->dtstart >= $_from && $_event->dtend > $_from) { return $_event; } $freqMap = array(self::FREQ_DAILY => Tinebase_DateTime::MODIFIER_DAY, self::FREQ_WEEKLY => Tinebase_DateTime::MODIFIER_WEEK, self::FREQ_MONTHLY => Tinebase_DateTime::MODIFIER_MONTH, self::FREQ_YEARLY => Tinebase_DateTime::MODIFIER_YEAR); $rrule = new Calendar_Model_Rrule(NULL, TRUE); $rrule->setFromString($_event->rrule); $from = clone $_from; $until = clone $from; $interval = $_which * $rrule->interval; // we don't want to compute ourself $ownEvent = clone $_event; $ownEvent->setRecurId($_event->getId()); $exceptions = clone $_exceptions; $exceptions->addRecord($ownEvent); $recurSet = new Tinebase_Record_RecordSet('Calendar_Model_Event'); if ($_from->isEarlier($_event->dtstart)) { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' from is ealier dtstart -> given event is next occurrence'); } return $_event; } $rangeDate = $_which > 0 ? $until : $from; if (!isset($freqMap[$rrule->freq])) { if (Tinebase_Core::isLogLevel(Zend_Log::ERR)) { Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' Invalid RRULE:' . print_r($rrule->toArray(), true)); } throw new Calendar_Exception('Invalid freq in RRULE: ' . $rrule->freq); } $rangeDate->add($interval, $freqMap[$rrule->freq]); $attempts = 0; if ($_event->rrule_until instanceof DateTime && Tinebase_Core::isLogLevel(Zend_Log::TRACE)) { Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' Event rrule_until: ' . $_event->rrule_until->toString()); } while (TRUE) { if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) { Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ . ' trying to find next occurrence from ' . $from->toString()); } if ($_event->rrule_until instanceof DateTime && $from->isLater($_event->rrule_until)) { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' passed rrule_until -> no further occurrences'); } return NULL; } $until = $_event->rrule_until instanceof DateTime && $until->isLater($_event->rrule_until) ? clone $_event->rrule_until : $until; $recurSet->merge(self::computeRecurrenceSet($_event, $exceptions, $from, $until)); $attempts++; // NOTE: computeRecurrenceSet also returns events during $from in some cases, but we need // to events later than $from. $recurSet = $recurSet->filter(function ($event) use($from) { return $event->dtstart >= $from; }); if (count($recurSet) >= abs($_which)) { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " found next occurrence after {$attempts} attempt(s)"); } break; } if ($attempts > count($exceptions) + 5) { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " could not find the next occurrence after {$attempts} attempts, giving up"); } return NULL; } $from->add($interval, $freqMap[$rrule->freq]); $until->add($interval, $freqMap[$rrule->freq]); } $recurSet->sort('dtstart', $_which > 0 ? 'ASC' : 'DESC'); $nextOccurrence = $recurSet[abs($_which) - 1]; if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' $nextOccurrence->dtstart = ' . $nextOccurrence->dtstart->toString()); } return $nextOccurrence; }
/** * create auto invoices for one contract * * @param Sales_Model_Contract $contract * @param Tinebase_DateTime $currentDate * @param boolean $merge */ protected function _createAutoInvoicesForContract(Sales_Model_Contract $contract, Tinebase_DateTime $currentDate, $merge = false) { // set this current billing date (user timezone) $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)) { return false; } if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Processing contract "' . $this->_currentBillingContract->number . '"'); } // fire event to allow other applications do some work before billing $this->_firePrebillEvent(); // find product aggregates of the current contract $productAggregates = $this->_findProductAggregates(); // 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; } } } // 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; } } 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()); } } $_addMonth = true; if (null == $this->_currentMonthToBill) { $this->_currentMonthToBill = clone $contract->start_date; $_addMonth = false; } $this->_currentMonthToBill->setTimezone(Tinebase_Core::getUserTimezone()); $this->_currentMonthToBill->setDate($this->_currentMonthToBill->format('Y'), $this->_currentMonthToBill->format('m'), 1); $this->_currentMonthToBill->setTime(0, 0, 0); if ($_addMonth) { $this->_currentMonthToBill->addMonth(1); } $doSleep = false; if (($merge || $contract->merge_invoices) && $this->_currentMonthToBill->isEarlier($this->_currentBillingDate)) { $this->_currentMonthToBill = clone $this->_currentBillingDate; } 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 . ' ' . $productAggregate->interval); } } //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 (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()); } } // 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' => clone $this->_currentMonthToBill, 'sales_tax' => 19)); $invoice->relations = $relations; $invoice->setTimezone('UTC', TRUE); // create invoice $invoice = $this->create($invoice); $this->_autoInvoiceIterationResults[] = $invoice->getId(); $this->_autoInvoiceIterationDetailResults[] = $invoice; $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; } elseif (Tinebase_Core::isLogLevel(Zend_Log::INFO)) { Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' $invoicePositions->count() == false'); } $this->_currentMonthToBill->addMonth(1); } }
/** * add calendar owner as attendee if not already set * * @param string $calendarId * @param Tinebase_DateTime $from * @param Tinebase_DateTime $until * @param boolean $dry run * * @return number of updated events */ public function repairAttendee($calendarId, $from, $until, $dry = false) { $container = Tinebase_Container::getInstance()->getContainerById($calendarId); if ($container->type !== Tinebase_Model_Container::TYPE_PERSONAL) { throw new Calendar_Exception('Only allowed for personal containers!'); } if ($container->owner_id !== Tinebase_Core::getUser()->getId()) { throw new Calendar_Exception('Only allowed for own containers!'); } $updateCount = 0; while ($from->isEarlier($until)) { $endWeek = $from->getClone()->addWeek(1); if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) { Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Repairing period ' . $from . ' - ' . $endWeek); } // TODO we need to detect events with DECLINED/DELETED attendee $events = $this->_getEventsForPeriodAndCalendar($calendarId, $from, $endWeek); $from->addWeek(1); if (count($events) == 0) { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' No events found'); } continue; } foreach ($events as $event) { // add attendee if not already set if ($event->isRecurInstance()) { // TODO get base event if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Skip recur instance ' . $event->toShortString()); } continue; } $ownAttender = Calendar_Model_Attender::getOwnAttender($event->attendee); if (!$ownAttender) { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Add missing attender to event ' . $event->toShortString()); } $attender = new Calendar_Model_Attender(array('user_type' => Calendar_Model_Attender::USERTYPE_USER, 'user_id' => Tinebase_Core::getUser()->contact_id, 'status' => Calendar_Model_Attender::STATUS_ACCEPTED)); $event->attendee->addRecord($attender); if (!$dry) { $this->update($event); } $updateCount++; } } } return $updateCount; }
/** * returns next occurrence _ignoring exceptions_ or NULL if there is none/not computable * * NOTE: computing the next occurrence of an open end rrule can be dangoures, as it might result * in a endless loop. Therefore we only make a limited number of attempts before giving up. * * @param Calendar_Model_Event $_event * @param Tinebase_Record_RecordSet $_exceptions * @param Tinebase_DateTime $_from * @param Int $_which * @return Calendar_Model_Event */ public static function computeNextOccurrence($_event, $_exceptions, $_from, $_which = 1) { $freqMap = array(self::FREQ_DAILY => Tinebase_DateTime::MODIFIER_DAY, self::FREQ_WEEKLY => Tinebase_DateTime::MODIFIER_WEEK, self::FREQ_MONTHLY => Tinebase_DateTime::MODIFIER_MONTH, self::FREQ_YEARLY => Tinebase_DateTime::MODIFIER_YEAR); $rrule = new Calendar_Model_Rrule(NULL, TRUE); $rrule->setFromString($_event->rrule); $from = clone $_from; $until = clone $from; $interval = $_which * $rrule->interval; // we don't want to compute ourself $ownEvent = clone $_event; $ownEvent->setRecurId(); $_exceptions->addRecord($ownEvent); $recurSet = new Tinebase_Record_RecordSet('Calendar_Model_Event'); if ($_from->isEarlier($_event->dtstart)) { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' from is ealier dtstart -> given event is next occurrence'); } return $_event; } $until->add($interval, $freqMap[$rrule->freq]); $attempts = 0; while (TRUE) { if ($_event->rrule_until instanceof DateTime && $from->isLater($_event->rrule_until)) { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' passed rrule_until -> no furthor occurrences'); } return NULL; } $until = $_event->rrule_until instanceof DateTime && $until->isLater($_event->rrule_until) ? $_event->rrule_until : $until; $recurSet->merge(self::computeRecurrenceSet($_event, $_exceptions, $from, $until)); $attempts++; if (count($recurSet) >= $_which) { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " found next occurrence after {$attempts} attempt(s)"); } break; } if ($attempts > count($_exceptions) + 5) { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) { Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " could not find the next occurrence after {$attempts} attempts, giving up"); } return NULL; } $from->add($interval, $freqMap[$rrule->freq]); $until->add($interval, $freqMap[$rrule->freq]); } $recurSet->sort('dtstart', 'ASC'); return $recurSet[$_which - 1]; }