<?php

  /**
   * Invoice class
   *
   * @package activeCollab.modules.invoicing
   * @subpackage models
   */
  class Invoice extends BaseInvoice implements IRoutingContext, IObjectContext, IPayments, IConfigContext, IInvoice, INotifierContext, IActivityLogs, IHistory {
    
    // Invoice setting
    const INVOICE_SETTINGS_SUM_ALL = 'sum_all';
    const INVOICE_SETTINGS_SUM_ALL_BY_JOB_TYPE = 'sum_records_by_job_type';
    const INVOICE_SETTINGS_KEEP_AS_SEPARATE = 'keep_records_as_separate_invoice_items';
    
    /**
     * List of protected fields (can't be set using setAttributes() method)
     *
     * @var array
     */
  	protected $protect = array(
  	  'status',
  	  'issued_on',
  	  'issued_by_id',
  	  'issued_by_name',
  	  'issued_by_email',
  	  'closed_on',
  	  'closed_by_id',
  	  'closed_by_name',
  	  'closed_by_email',
  	  'created_on',
  	  'created_by_id',
  	  'created_by_name',
  	  'created_by_email'
  	);
  	
    /**
     * Cached inspector instance
     * 
     * @var IInvoiceInspectorImplementation
     */
    private $inspector = false;
    
    /**
     * Return inspector helper instance
     * 
     * @return IInvoiceInspectorImplementation
     */
    function inspector() {
      if($this->inspector === false) {
        $this->inspector = new IInvoiceInspectorImplementation($this);
      } // if
      
      return $this->inspector;
    } // inspector
  	
    
    /**
     * Return issued On Month
     * 
     * @return string
     */
    function getIssuedOnMonth() {
      if($this->getStatus() > 0) {
        return $this->getIssuedOn()->getMonth() . ', ' . $this->getIssuedOn()->getYear();
      }//if
      return 0;
    }//getIssuedOnMonth
    
     /**
      * Return due On Month
      *
      * @return string
      */
    function getDueOnMonth() {
      if($this->getStatus() > 0) {
        return $this->getDueOn()->getMonth() . ', ' . $this->getDueOn()->getYear();
      }//if
      return 0;
    }//getDueOnMonth
    
    
    /**
     * Return tax column text
     * 
     * @return string
     */
    function getTaxColumnText() {
      if($this->getTax() == 0) {
        $column_text = '';
      } else {
        $common_tax_rate = $this->commonTaxRate();
        
        if($common_tax_rate instanceof TaxRate) {
          $column_text = $common_tax_rate->getName() . " (" . $common_tax_rate->getVerbosePercentage() . ")";
        } else {
          $column_text = lang('Tax');
        }//if
      }//if
      
      return $column_text;
    } // getTaxColumnText

  	/**
     * Add items
     * 
     * @param $items
     */
    function addItems($items) {
      $counter = 0;
      
      if(is_foreachable($items)) {
        try {
          DB::beginWork('Add invoice items @ ' . __CLASS__);
          
          foreach($items as $item_data) {
        		$item = new InvoiceItem();
        		
        		$item->setAttributes($item_data);
        		$item->setInvoiceId($this->getId());
        		$item->setPosition($counter + 1);
        		$item->save();
        		$counter++;
        	} // foreach
          
          DB::commit('Invoice items added @ ' . __CLASS__);
        } catch(Exception $e) {
          DB::rollback('Failed to add invoice items @ ' . __CLASS__);
          throw $e;
        } // try
      } // if
      
      if($counter == 0) {
      	throw new ValidationErrors(array('invoice' => lang('Invoice items data is not valid. All descriptions are required and there need to be at least one unit with cost set per item!')));
      } // if
    } // addItems
    
    /**
     * Return invoice name
     *
     * @return string
     */
    function getName($short = false) {
      if ($this->getNumber()) {
        if ($short) {
          return $this->getNumber();
        } else {
        	return lang(':invoice_show_as #:invoice_num', array(
            'invoice_show_as' => Invoices::printInvoiceAs(),
            'invoice_num' => $this->getNumber()
          ));
        } // if
      } else {
        if ($short) {
          return lang('Draft #:invoice_num', array(
            'invoice_num' => $this->getId()
          ));
        } else {
          return lang(':invoice_show_as Draft #:invoice_num', array(
            'invoice_show_as' => Invoices::printInvoiceAs(),
            'invoice_num' => $this->getId()
          ));
        } // if
      } // if
    } // getName

    /**
     * Cached company value
     *
     * @var Company
     */
    private $company = false;

    /**
      * Return invoice company
      *
      * @return Company
      */
    function getCompany() {
      if($this->company === false) {
        $this->company = Companies::findById($this->getCompanyId());
      } // if
      return $this->company;
    } // getCompany
    
    /**
     * Get client company name
     * 
     * @return string
     */
    function getCompanyName() {
    	$company = $this->getCompany();
    	
    	if ($company instanceof Company) {
    		return $company->getName();
    	} // if
    	
    	return false;
    } // getCompanyName

    /**
     * Cached project instance
     *
     * @var Project
     */
    private $project = false;

    /**
     * Return project instance
     *
     * @return Project
     */
    function getProject() {
      if($this->project === false) {
        $this->project = DataObjectPool::get('Project', $this->getProjectId());
      } // if
      return $this->project;
    } // getProject

    /**
     * Cached currency instance
     *
     * @var Currency
     */
    private $currency = false;

    /**
     * Return invoice currency
     *
     * @return Currency
     */
    function getCurrency() {
      if($this->currency === false) {
        $this->currency = Currencies::findById($this->getCurrencyId());
      } // if
      return $this->currency;
    } // getCurrency

    /**
     * Return currency name
     *
     * @return string
     */
    function getCurrencyName() {
      return $this->getCurrency() instanceof Currency ? $this->getCurrency()->getName() : lang('Unknown Currency');
    } // getCurrencyName

    /**
     * Return currency code
     *
     * @return string
     */
    function getCurrencyCode() {
      return $this->getCurrency() instanceof Currency ? $this->getCurrency()->getCode() : lang('Unknown Currency');
    } // getCurrencyCode
    
    /**
     * Cached language value
     *
     * @var Language
     */
    private $language = false;

    /**
      * Return invoice company
      *
      * @return Company
      */
    function getLanguage() {
      if($this->language === false) {
        $this->language = Languages::findById($this->getLanguageId());
      } // if
      return $this->language;
    } // getLanguage
    
    /**
     * Cached issued by instance
     *
     * @var User
     */
    private $issued_by = false;
    
    /**
     * Return user object of person who issued this invoice
     *
     * @return User
     */
    function getIssuedBy() {
      if($this->issued_by === false) {
        $issued_by_id = $this->getIssuedById();
        if($issued_by_id) {
          $this->issued_by = Users::findById($issued_by_id);
        } // if
        
        if(!$this->issued_by instanceof IUser && is_valid_email($this->getIssuedByEmail())) {
          $this->issued_by = new AnonymousUser($this->getIssuedByName(), $this->getIssuedByEmail());
        } // if
      } // if
      return $this->issued_by;
    } // getIssuedBy
    
    /**
     * Cached issued to by instance
     *
     * @var User
     */
    private $issued_to = false;
    
    /**
     * Return user to which invoice is issued to
     *
     * @return User
     */
    function getIssuedTo() {
      if($this->issued_to === false) {
        $this->issued_to = Users::findById($this->getIssuedToId());
      } // if
      return $this->issued_to;
    } // getIssuedTo
    
    /**
     * Set issued to
     * 
     * @param User $value
     * @return User
     */
    function setIssuedTo($value) {
      if($value instanceof User) {
        $this->setIssuedToId($value->getId());
      } elseif($value === null) {
        $this->setIssuedToId(null);
      } else {
        throw new InvalidInstanceError('user', $value, '$user should be instance of User class or NULL');
      } // if
      
      $this->issued_to = $value;
      
      return $this->issued_to;
    } // setIssuedTo
    
    /**
     * Cached instance of person who closed this invoice
     *
     * @var User
     */
    private $closed_by = false;
    
    /**
     * Return user object of person who closed this invoice
     *
     * @return User
     */
    function getClosedBy() {
      if($this->closed_by === false) {
        $closed_by_id = $this->getClosedById();
        if($closed_by_id) {
          $this->closed_by = Users::findById($closed_by_id);
        } // if
        
        if(!($this->closed_by instanceof User)) {
          $this->closed_by = new AnonymousUser($this->getClosedByName(), $this->getClosedByEmail());
        } // if
      } // if
      return $this->closed_by;
    } // getClosedBy

    /**
     * Cached invoice items
     *
     * @var array
     */
    private $items = false;

    /**
     * Return invoice items
     *
     * @return array
     */
    function getItems($use_cache = false) {
      if($use_cache) {
        if($this->items === false) {
          $this->items = InvoiceItems::findByInvoice($this);
        } // if
        return $this->items;
      } else {
        return $this->items = InvoiceItems::findByInvoice($this);
      }//if
    } // getItems
    
    /**
     * Cached invoice total
     *
     * @var float
     */
    private $total = false;

    /**
     * Cached tax value
     *
     * @var float
     */
    private $tax = false;

    /**
     * Cached taxed total
     *
     * @var float
     */
    private $taxed_total = false;
    
    /**
     * Return invoice total
     *
     * @param boolean $cache
     * @return float
     */
    function getTotal($cache = true) {
      $this->calculateTotal($cache);
      return $this->total;
    } // getTotal

    /**
     * Return calculated tax
     *
     * @param boolean $cache
     * @return float
     */
    function getTax($cache = true) {
      $this->calculateTotal($cache);
      return $this->tax;
    } // getTax

    /**
     * Returned taxed total
     *
     * @param boolean $cache
     * @return float
     */
    function getTaxedTotal($cache = true) {
      $this->calculateTotal($cache);
      return $this->taxed_total;
    } // getTaxedTotal
    
    /**
     * Generates invoice id by invoice pattern
     *
     * @return string
     */
    function generateInvoiceId() {
      $pattern = ConfigOptions::getValue('invoicing_number_pattern');
      $padding = (integer) ConfigOptions::getValue('invoicing_number_counter_padding');
      
      // retrieve counters
      list($total_counter, $year_counter, $month_counter) = Invoices::getDateInvoiceCounters();
      $total_counter++; $year_counter++; $month_counter++;

      // Apply padding, if needed
      $prepared_total_counter = $padding ? str_pad($total_counter, $padding, '0', STR_PAD_LEFT) : $total_counter;
      $prepared_year_counter = $padding ? str_pad($year_counter, $padding, '0', STR_PAD_LEFT) : $year_counter;
      $prepared_month_counter = $padding ? str_pad($month_counter, $padding, '0', STR_PAD_LEFT) : $month_counter;

      // retrieve variables
      $variable_year = date('Y');
      $variable_month = date('n');
      $variable_month_short = date('M');
      $variable_month_long = date('F');
     
      $generated_invoice_id = str_ireplace(array(
        INVOICE_NUMBER_COUNTER_TOTAL,
        INVOICE_NUMBER_COUNTER_YEAR,
        INVOICE_NUMBER_COUNTER_MONTH,
        INVOICE_VARIABLE_CURRENT_YEAR,
        INVOICE_VARIABLE_CURRENT_MONTH,
        INVOICE_VARIABLE_CURRENT_MONTH_SHORT,
        INVOICE_VARIABLE_CURRENT_MONTH_LONG,
      ), array(
        $prepared_total_counter,
        $prepared_year_counter,
        $prepared_month_counter,
        $variable_year,
        $variable_month,
        $variable_month_short,
        $variable_month_long,
      ), $pattern);
      
      return $generated_invoice_id;
    } // function

    /**
     * Calculate total by walking through list of items
     */
    function calculateTotal($cache = true) {
      if($cache == false || $this->total === false || $this->tax === false || $this->taxed_total === false) {
        $this->total = 0;
        $this->tax = 0;

        if(is_foreachable($this->getItems())) {
          foreach($this->getItems() as $item) {
            $this->total += $item->getSubTotal();
            $this->tax   += $item->getTax();
          } // foreach
        } // if
        
        // now we have total tax for invoice, we need to round it up to 2 decimals
        $this->tax = round($this->tax, 2);
        $this->taxed_total = round($this->total + $this->tax,2);
      } // if
    } // calculateTotal

    /**
     * Cached amount paid
     *
     * @var float
     */
    private $paid_amount = false;

    /**
     * Return paid amount
     *
     * @param boolean $cache
     * @return float
     */
    function getPaidAmount($cache = true) {
      if($cache == false || $this->paid_amount === false) {
        $this->payments()->getPaidAmount();
      } // if
      return $this->paid_amount;
      
    } // getPaidAmount

    /**
     * Return % that was paid
     *
     * @param boolean $cache
     * @return integer
     */
    function getPercentPaid($cache = true) {
      return (float) number_format(($this->getPaidAmount($cache) * 100) / $this->getTaxedTotal($cache), 2, '.', '');
    } // getPercentPaid

    /**
     * Return max amount that can be paid with the next payment
     *
     * @param boolean $cache
     * @return float
     */
    function getMaxPayment($cache = true) {
      return $this->getTaxedTotal($cache) - $this->getPaidAmount($cache);
    } // getMaxPayment
    
    /**
     * Return Tax Rate if one tax rate is used for all items, else return false
     * 
     * @return mixed
     */
    function commonTaxRate() {
      if($this->getTax() == 0) {
        //no tax
        return false;
      } else {
        //tax exists
        $items = $this->getItems();
        if(is_foreachable($items)) {
          $tax_rate = null;
          
          foreach($items as $item) {
            if($item->getTaxRate() instanceof TaxRate) {
              $rate[] = $item->getTaxRate()->getId();
              $tax_rate = $item->getTaxRate();
            } else {
              $rate[] = 0;
            }
          }//foreach
          
          $tax_arr = array_unique($rate);
          if(count($tax_arr) == 1) {
            return $tax_rate;
          }
          return false;
        }//if
      }//if
      
      return false;
    }//commonTaxRate
    
    // ---------------------------------------------------
    //  Options
    // ---------------------------------------------------
    
    /**
     * Prepare list of options that $user can use
     *
     * @param IUser $user
     * @param NamedList $options
     * @param string $interface
     * @return NamedList
     */
    protected function prepareOptionsFor(IUser $user, NamedList $options, $interface = AngieApplication::INTERFACE_DEFAULT) {
      if($this->canView($user) && !$this->isCanceled()) {
        $options->add('view_pdf', array(
          'url' => $this->canEdit($user) ? $this->getPdfUrl() : $this->getCompanyPdfUrl(),
          'text' => $this->getStatus() == INVOICE_STATUS_DRAFT ? lang('Preview PDF') : lang('View PDF'),
        	'onclick' => new TargetBlankCallback(),
        	'icon' => AngieApplication::getImageUrl('icons/12x12/download-pdf.png', INVOICING_MODULE),
        	'important' => true
        ), true);
      } // if
      
    	if ($this->isIssued() && $this->canView($user) && $this->payments()->canMake($user) && ($user->isFinancialManager() || ($this->payments()->hasDefinedGateways()))) {
    		$options->add('make_a_payment', array(
    			'url' => $this->payments()->getAddUrl(),
    			'text' => lang('Make a Payment'),
    			'onclick' => new FlyoutFormCallback('payment_created'),
    			'icon' => AngieApplication::getImageUrl('icons/12x12/add-payment.png', INVOICING_MODULE),
    			'important' => true
    		));
    	} // if
      
    	if ($this->isIssued() && $this->canEdit($user)) {
    		$options->add('resend_email', array(
    			'url' => $this->getNotifyUrl(),
    			'text' => lang('Resend Email'),
    			'onclick' => new FlyoutFormCallback('resend_email', array(
            'width' => 400
          )),
    			'important' => $this->isOverdue()
    		));
    	} // if
    	
    	
      if($this->canIssue($user)) {
        $options->add('issue', array(
          'url' => $this->getIssueUrl(),
          'text' => lang('Issue'),
        	'onclick' => new FlyoutFormCallback($this->getUpdatedEventName(), array(
            'width' => 400,
          )),
        	'icon' => AngieApplication::getImageUrl('icons/12x12/issue-invoice.png', INVOICING_MODULE),
    			'important' => true
        ), true);
      } // if
      
      if($this->canCancel($user)) {
        $options->add('cancel', array(
          'url' => $this->getCancelUrl(),
          'text' => lang('Cancel'),
						'onclick' => new AsyncLinkCallback(array(
							'confirmation' => lang('Are you sure that you want to cancel this invoice? All existing payments associated with this invoice will be deleted!'), 
							'success_message' => lang('Invoice has been successfully canceled'), 
							'success_event' => $this->getUpdatedEventName()
						))
					), true);
      } // if
      
      if(Invoices::canAdd($user)) {
        $options->add('duplicate', array(
          'url' => $this->getDuplicateUrl(),
          'text' => lang('Duplicate'),
        	'onclick' => new FlyoutFormCallback('invoice_created'),
        	'icon' => AngieApplication::getImageUrl('icons/12x12/duplicate-invoice.png', INVOICING_MODULE),
        ), true);
      } // if
      
      if($this->canViewRelatedItems($user) && ($this->countTimeRecords() || $this->countExpenses())) {
        $options->add('time', array(
          'url' => $this->getTimeUrl(),
          'text' => lang('Items (:count)', array('count' => $this->countTimeRecords() + $this->countExpenses())),
        ), true);
      } // if
      
      
      if($this->canEdit($user)) {
        $options->add('edit', array(
          'url' => $this->getEditUrl(),
          'text' => lang('Edit'),
        	'onclick' => new FlyoutFormCallback('invoice_updated'),
        	'icon' => AngieApplication::getImageUrl('icons/12x12/edit.png', ENVIRONMENT_FRAMEWORK),
        	'important' => true
        ), true);
      } // if
      
      if($this->canDelete($user)) {
        $options->add('delete', array(
          'url' => $this->getDeleteUrl(),
          'text' => lang('Delete'),
					'onclick' => new AsyncLinkCallback(array(
					 'confirmation' => lang('Are you sure that you want to delete this invoice?'), 
					 'success_message' => lang('Invoice successfully deleted'), 
        	 'success_event' => 'invoice_deleted'
					)),
          'icon' => AngieApplication::getImageUrl('icons/12x12/delete.png', ENVIRONMENT_FRAMEWORK),
        ), true);
      } // if
            
      parent::prepareOptionsFor($user, $options, $interface);
    } // prepareOptionsFor
    
    // ---------------------------------------------------
    //  Time records
    // ---------------------------------------------------
    
    /**
     * Cached array of related time records
     *
     * @var array
     */
    private $time_record_ids = false;
    
    /**
     * Return array of related time record ID-s
     *
     * @param $cached
     * @return array
     */
    function getTimeRecordIds($cached = false) {
      if($this->time_record_ids === false || !$cached) {
        $rows = DB::execute('SELECT parent_id FROM ' . TABLE_PREFIX . 'invoice_related_records WHERE invoice_id = ? AND parent_type = ?', $this->getId(), 'TimeRecord');
        if(is_foreachable($rows)) {
          $this->time_record_ids = array();
          foreach($rows as $row) {
            $this->time_record_ids[] = (integer) $row['parent_id'];
          } // foreach
        } else {
          $this->time_record_ids = null;
        } // if
      } // if
      return $this->time_record_ids;
    } // getTimeRecordIds
    
    /**
     * Cached value of related time records count
     *
     * @var integer
     */
    private $time_records_count = false;
    
    /**
     * Return number of related time records
     *
     * @return integer
     */
    function countTimeRecords() {
      if($this->time_records_count === false) {
        $this->time_records_count = (integer) DB::executeFirstCell("SELECT COUNT(parent_id) AS 'row_count' FROM " . TABLE_PREFIX . 'invoice_related_records WHERE invoice_id = ? AND parent_type = ?', $this->getId(), 'TimeRecord');
      } // if
      return $this->time_records_count;
    } // countTimeRecords
    
    /**
     * Release time records
     *
     * @return boolean
     */
    function releaseTimeRecords() {
     $ids = $this->getTimeRecordIds();
     
      if($ids) {
        $this->setTimeRecordsStatus(BILLABLE_STATUS_BILLABLE, $ids);
        return DB::execute('DELETE FROM ' . TABLE_PREFIX . 'invoice_related_records WHERE invoice_id = ? AND parent_type = ? AND parent_id IN (?)', $this->getId(), 'TimeRecord', $ids);
      } // if
    } // releaseTimeRecords
    
    /**
     * Release time records
     *
     * @param array $ids
     * @return boolean
     */
    function releaseTimeRecordsByIds($ids) {
      $this->setTimeRecordsStatus(BILLABLE_STATUS_BILLABLE, $ids);
      return DB::execute('DELETE FROM ' . TABLE_PREFIX . 'invoice_related_records WHERE invoice_id = ? AND parent_type = ? AND parent_id IN (?)', $this->getId(), 'TimeRecord', $ids);
    } // releaseTimeRecords
    
    /**
     * Return related time records
     *
     * @param int $visibility
     * @return array
     */
    function getTimeRecords($visibility = VISIBILITY_NORMAL) {
      $ids = $this->getTimeRecordIds();
      return is_foreachable($ids) ? TimeRecords::findByIds($ids, STATE_VISIBLE) : null;
    } // getTimeRecords
    
    /**
     * Set status to related time records
     *
     * @param integer $new_status
     * @param array $ids
     * @return boolean
     */
    function setTimeRecordsStatus($new_status, $ids = false) {
      if(!$ids) {
        $ids = $this->getTimeRecordIds();
      }//if
      
      if(is_foreachable($ids)) {
        $update = DB::execute('UPDATE ' . TABLE_PREFIX . 'time_records SET billable_status = ? WHERE id IN (?)', $new_status, $ids);
        if($update) {
          cache_remove_by_pattern(TABLE_PREFIX . 'time_records_id_*');
          return true;
        } else {
          return $update;
        } // if
      } // if
      return true;
    } // setTimeRecordsStatus
    
    // ---------------------------------------------------
    //  Expenses
    // ---------------------------------------------------
    
    /**
     * Cached array of expenses records
     *
     * @var array
     */
    private $expense_ids = false;
    
    /**
     * Return array of related expense ID-s
     *
     * @param $cached
     * @return array
     */
    function getExpenseIds($cached = false) {
      if($this->expense_ids === false || !$cached) {
        $rows = DB::execute('SELECT parent_id FROM ' . TABLE_PREFIX . 'invoice_related_records WHERE invoice_id = ? AND parent_type = ?', $this->getId(), 'Expense');
        if(is_foreachable($rows)) {
          $this->expense_ids = array();
          foreach($rows as $row) {
            $this->expense_ids[] = (integer) $row['parent_id'];
          } // foreach
        } else {
          $this->expense_ids = null;
        } // if
      } // if
      return $this->expense_ids;
    } // getExpenseIds
    
    /**
     * Cached value of related expenses count
     *
     * @var integer
     */
    private $expenses_count = false;
    
    /**
     * Return number of related time records
     *
     * @return integer
     */
    function countExpenses() {
      if($this->expenses_count === false) {
        $this->expenses_count = (integer) DB::executeFirstCell("SELECT COUNT(parent_id) AS 'row_count' FROM " . TABLE_PREFIX . 'invoice_related_records WHERE invoice_id = ? AND parent_type = ?', $this->getId(), 'Expense');
      } // if
      return $this->expenses_count;
    } // countExpenses
    
    /**
     * Release expenses
     *
     * @return boolean
     */
    function releaseExpenses() {
      $ids = $this->getExpenseIds();
      if($ids) {
        $this->setExpensesStatus(BILLABLE_STATUS_BILLABLE,$ids);
        return DB::execute('DELETE FROM ' . TABLE_PREFIX . 'invoice_related_records WHERE invoice_id = ? AND parent_type = ? AND parent_id IN (?)', $this->getId(), 'Expense', $ids);
      }
      
    } // releaseExpenses
    
    /**
     * Release expenses
     *
     * @return boolean
     */
    function releaseExpensesByIds($ids) {
      $this->setExpensesStatus(BILLABLE_STATUS_BILLABLE,$ids);
      return DB::execute('DELETE FROM ' . TABLE_PREFIX . 'invoice_related_records WHERE invoice_id = ? AND parent_type = ? AND parent_id IN (?)', $this->getId(), 'Expense', $ids);
    } // releaseExpenses
    
    /**
     * Return related expenses
     *
     * @return array
     */
    function getExpenses($visibility = VISIBILITY_NORMAL) {
      $ids = $this->getExpenseIds();
      return is_foreachable($ids) ? Expenses::findByIds($ids, STATE_VISIBLE) : null;
    } // getExpenses
    
    /**
     * Set status to related expenses
     *
     * @param integer $new_status
     * @return boolean
     */
    function setExpensesStatus($new_status,$ids = false) {
      if(!$ids) {
        $ids = $this->getExpenseIds();
      }//if
      if(is_foreachable($ids)) {
        $update = DB::execute('UPDATE ' . TABLE_PREFIX . 'expenses SET billable_status = ? WHERE id IN (?)', $new_status, $ids);
        if($update) {
          cache_remove_by_pattern(TABLE_PREFIX . 'expenses_id_*');
          return true;
        } else {
          return $update;
        } // if
      } // if
      return true;
    } // setExpensesStatus
       
    // ---------------------------------------------------
    //  Status
    // ---------------------------------------------------

    /**
     * Return verbose invoice status
     *
     * @return string
     */
    function getVerboseStatus() {
      switch($this->getStatus()) {
        case INVOICE_STATUS_DRAFT:
          return lang('Draft');
        case INVOICE_STATUS_ISSUED:
          return lang('Issued');
        case INVOICE_STATUS_PAID:
          return lang('Paid');
        case INVOICE_STATUS_CANCELED:
          return lang('Canceled');
      } // switch
    } // getVerboseStatus
    
    /**
     * Change invoice status
     *
     * @param integer $status
     * @param User $by
     * @param DateValue $on
     */
    function setStatus($status, $by = null, $on = null) {
      $by = $by instanceof User ? $by : Authentication::getLoggedUser();
      $on = $on instanceof DateValue ? $on : new DateValue();
      
      switch($status) {
        
        // Mark invoice as draft
        case INVOICE_STATUS_DRAFT:
          parent::setStatus($status);
          
          $this->setIssuedOn(null);
          $this->setIssuedById(null);
          $this->setIssuedByName(null);
          $this->setIssuedByEmail(null);
          
          $this->setClosedOn(null);
          $this->setClosedById(null);
          $this->setClosedByName(null);
          $this->setClosedByEmail(null);
          break;
          
        // Mark invoice as issued
        case INVOICE_STATUS_ISSUED:
          parent::setStatus($status);
          
          if($on) {
            $this->setIssuedOn($on);
          } // if
          
          if($by) {
            $this->setIssuedById($by->getId());
            $this->setIssuedByName($by->getName());
            $this->setIssuedByEmail($by->getEmail());
          } // if
          
          $this->setClosedOn(null);
          $this->setClosedById(null);
          $this->setClosedByName(null);
          $this->setClosedByEmail(null);
          
          $this->setTimeRecordsStatus(BILLABLE_STATUS_PENDING_PAYMENT);
          $this->setExpensesStatus(BILLABLE_STATUS_PENDING_PAYMENT);
          break;
          
        // Mark invoice as paid
        case INVOICE_STATUS_PAID:
          parent::setStatus(INVOICE_STATUS_PAID);
          
          $this->setClosedOn($on);
          $this->setClosedById($by->getId());
          $this->setClosedByName($by->getName());
          $this->setClosedByEmail($by->getEmail());
          
          $this->setTimeRecordsStatus(BILLABLE_STATUS_PAID);
          $this->setExpensesStatus(BILLABLE_STATUS_PAID);
          
          // Sent notification invoice paid
          if($this->getIssuedTo() instanceof IUser && !$this->getIssuedTo()->isFinancialManager()) {
            ApplicationMailer::notifier()->notifyUsers($this->getIssuedTo(), $this, 'invoicing/paid', null, array(
              'attachments' => array($this->getPdfAttachmentPath() => 'invoice.pdf'),
              'method' => ApplicationMailer::SEND_INSTANTNLY,
            ));
          } // if

          // Notify financial managers (all or everyone execept manager making the payment)
          if($by->isFinancialManager()) {
            ApplicationMailer::notifier()->notifyFinancialManagers($this, 'invoicing/paid', null, array('exclude' => $by));
          } else {
            ApplicationMailer::notifier()->notifyFinancialManagers($this, 'invoicing/paid');
          } // if

          break;
          
        // Mark invoice as canceled
        case INVOICE_STATUS_CANCELED:
          parent::setStatus(INVOICE_STATUS_CANCELED);
          
          $this->setClosedOn($on);
          $this->setClosedById($by->getId());
          $this->setClosedByName($by->getName());
          $this->setClosedByEmail($by->getEmail());
          
          //InvoicePayments::deleteByInvoice($this);
          $this->payments()->delete();
          
          $this->setTimeRecordsStatus(BILLABLE_STATUS_BILLABLE);
          $this->releaseTimeRecords();
          $this->setExpensesStatus(BILLABLE_STATUS_BILLABLE);
          $this->releaseExpenses();
          break;
          
        default:
          throw new InvalidParamError('status', $status, '$status is not valid invoice status', true);
      } // switch
    } // setStatus
    
    /**
     * Mark this invoice as issued
     * 
     * @param User $issued_by
     * @param User $issued_to
     * @param DateValue $issued_on
     * @param DateValue $due_on
     */
    function markAsIssued(User $issued_by, $issued_to, DateValue $issued_on, DateValue $due_on) {
      if($this->getStatus() == INVOICE_STATUS_DRAFT) {
        try {
          DB::beginWork('Marking invoice as issued @ ' . __CLASS__);
          
          $this->setStatus(INVOICE_STATUS_ISSUED, $issued_by, $issued_on);
          $this->setDueOn($due_on);
          
          if($issued_to instanceof User) {
            $this->setIssuedTo($issued_to);
          } // if
          
          $autogenerated = false;
          
          // Generate invoice number?
	        if(!$this->getNumber()) {
	          $this->setNumber($this->generateInvoiceId());
	          $autogenerated = true;
	        } // if
	        
	        $this->save();
	        
	        $this->activityLogs()->logIssuing($issued_by);
	        
          if($autogenerated) {
            Invoices::incrementDateInvoiceCounters();  
          } // if
          
          DB::commit('Invoice marked as issued @ ' . __CLASS__);
        } catch(Exception $e) {
          DB::rollback('Failed to mark invoice as issued @ ' . __CLASS__);
          throw $e;
        } // try
      } else {
        throw new InvalidParamError('status', $this->getStatus(), 'Only draft invoices can be marked as issued');
      } // if
    } // markAsIssued
    
    /**
     * Mark invoice as paid
     * 
     * @param User $by
     * @param Payment $payment
     */
    function markAsPaid(User $by, Payment $payment) {
      if($this->getStatus() == INVOICE_STATUS_ISSUED) {
        try {
          DB::beginWork('Marking invoice as paid @ ' . __CLASS__);
          
          $this->setStatus(INVOICE_STATUS_PAID, $by, DateTimeValue::now());
          $this->save();
	        
	        $this->activityLogs()->logPaid($by);
          
          DB::commit('Invoice marked as paid @ ' . __CLASS__);
        } catch(Exception $e) {
          DB::rollback('Failed to mark invoice as paid @ ' . __CLASS__);
          throw $e;
        } // try
      } else {
        throw new InvalidParamError('status', $this->getStatus(), 'Only issued invoices can be marked as paid');
      } // if
    } // markAsPaid
    
    /**
     * Mark this invoice as canceled
     * 
     * @param User $by
     */
    function markAsCanceled(User $by) {
      if($this->getStatus() == INVOICE_STATUS_ISSUED || $this->getStatus() == INVOICE_STATUS_PAID) {
        try {
          DB::beginWork('Marking invoice as canceled @ ' . __CLASS__);
          
          $this->setStatus(INVOICE_STATUS_CANCELED, $by, DateTimeValue::now());
          $this->save();
	        
	        $this->activityLogs()->logCancelation($by);
          
          DB::commit('Invoice marked as canceled @ ' . __CLASS__);
        } catch(Exception $e) {
          DB::rollback('Failed to mark invoice as canceled @ ' . __CLASS__);
          throw $e;
        } // try
      } else {
        throw new InvalidParamError('status', $this->getStatus(), 'Only issued and paid invoices can be marked as canceled');
      } // if
    } // markAsCanceled
    
    /**
     * Check if invoice is draft
     * 
		 * @return boolean
     */
    function isDraft() {
    	return !($this->getIssuedOn() instanceof DateValue) && $this->getStatus() == INVOICE_STATUS_DRAFT;
    } // isDraft
    
    /**
     * Returns true if this invoice is issued
     *
     * @return boolean
     */
    function isIssued() {
      return $this->getIssuedOn() instanceof DateValue && $this->getStatus() == INVOICE_STATUS_ISSUED;
    } // isIssued
    
    /**
     * Returns true if this invoice is marked as paid
     *
     * @return boolean
     */
    function isPaid() {
      return $this->getClosedOn() instanceof DateValue && $this->getStatus() == INVOICE_STATUS_PAID;
    } // isPaid
    
    /**
     * Check if this invoice is overdue
     *
     * @return boolean
     */
    function isOverdue() {
      $today = new DateValue(time() + get_user_gmt_offset());
      $due_on = $this->getDueOn();
      return (boolean) ($this->isIssued() && !$this->isPaid() && !$this->isCanceled() && ($due_on instanceof DateValue && ($due_on->getTimestamp() < $today->getTimestamp())));
    } // isOverdue
    
    /**
     * Returns true if this invoice is canceled
     *
     * @return boolean
     */
    function isCanceled() {
      return $this->getClosedOn() instanceof DateValue && $this->getStatus() == INVOICE_STATUS_CANCELED;
    } // isCanceled
    
    /**
     * Check whether invoice is based on quote
     *
     * @return boolean
     */
    function isBasedOnQuote() {
    	return (boolean) $this->getBasedOnType() == 'Quote';
    } // isBasedOnQuote
    
    /**
     * Get based on quote
     *
     * @return Quote
     */
    function getBasedOnQuote() {
    	return Quotes::findById($this->getBasedOnId());
    } // getBasedOnQuote
    
    /**
     * Return based on object
     * 
     * @return IInvoiceBasedOn
     */
    function getBasedOn() {
      if($this->getBasedOnType() && $this->getBasedOnId()) {
        return DataObjectPool::get($this->getBasedOnType(), $this->getBasedOnId());
      } // if

      return null;
    } // getBasedOn

    /**
     * Set based on value
     *
     * @param IInvoiceBasedOn $based_on
     */
    function setBasedOn($based_on) {
      if($based_on instanceof IInvoiceBasedOn) {
        $this->setBasedOnType(get_class($based_on));
        $this->setBasedOnId($based_on->getId());
      } elseif($based_on === null) {
        $this->setBasedOnType(null);
        $this->setBasedOnId(null);
      } else {
        throw new InvalidInstanceError('based_on', $based_on, 'IInvoiceBasedOn');
      } // if
    } // setBasedOn
    
    // ---------------------------------------------------
    //  PDF
    // ---------------------------------------------------
    
    /**
     * Return PDF filename
     *
     * @return string
     */
    function getPdfFileName() {
      return $this->getName() . '.pdf';
    } // getPdfFileName
    
    /**
     * Return PDF path
     *
     * @return string
     */
    function getPdfFilePath() {
      return INVOICES_WORK_PATH . '/invoice-' . $this->getId() . '.pdf';
    } // getPdfFilePath
    
    /**
     * Create a new PDF file
     *
     * @return boolean
     */
    function generatePdf() {
      require_once ANGIE_PATH . '/fpdf/init.php';
    } // generatePdf

    /**
     * Make a PDF copy and prepare it to be emailed
     *
     * This function returns file path of the created PDF file
     *
     * @return string
     */
    function getPdfAttachmentPath() {
      $filename = WORK_PATH . '/invoice_' . $this->getId() . '.pdf';

      InvoicePDFGenerator::save($this, $filename);

      return $filename;
    } // getPdfAttachmentPath
    
    // ---------------------------------------------------
    //  Interface implementations
    // ---------------------------------------------------
    
     /**
     * Return email notification context ID
     *
     * @return string
     */
    function getNotifierContextId() {
      return 'INVOICE/' . $this->getId();
    } // getNotifierContextId
    
    /**
     * Return routing context name
     *
     * @return string
     */
    function getRoutingContext() {
      return 'invoice';
    } // getRoutingContext
    
    /**
     * Return routing context parameters
     *
     * @return mixed
     */
    function getRoutingContextParams() {
      return array('invoice_id' => $this->getId());
    } // getRoutingContextParams
    
    /**
     * Return object context domain
     * 
     * @return string
     */
    function getObjectContextDomain() {
      return 'invoices';
    } // getObjectContextDomain
    
    /**
     * Return object context path
     * 
     * @return string
     */
    function getObjectContextPath() {
      return 'invoices/' . $this->getId();
    } // getObjectContextPath
    
    /**
     * Payments helper
     *
     * @var IInvoicePaymentsImplementation
     */
    private $payments = false;
    
    /**
     * Return payments helper instance
     *
     * @return IInvoicePaymentsImplementation
     */
    function payments() {
      if($this->payments === false) {
        $this->payments = new IInvoicePaymentsImplementation($this);
      } // if
      return $this->payments;
    } // payments
    
    /**
     * Cached activity logs helper instance
     *
     * @var IInvoiceActivityLogsImplementation
     */
    private $activity_logs = false;
    
    /**
     * Return activity logs helper instance
     * 
     * @return IInvoiceActivityLogsImplementation
     */
    function activityLogs() {
      if($this->activity_logs === false) {
        $this->activity_logs = new IInvoiceActivityLogsImplementation($this);
      } // if
      
      return $this->activity_logs;
    } // activityLogs
    
    /**
     * Cached history implementation instance
     *
     * @var IHistoryImplementation
     */
    private $history = false;
    
    /**
     * Return history helper instance
     * 
     * @return IHistoryImplementation
     */
    function history() {
      if($this->history === false) {
        $this->history = new IHistoryImplementation($this, array('company_id', 'project_id', 'currency_id', 'language_id', 'number', 'company_address', 'comment', 'note', 'status', 'due_on', 'allow_payments'));
      } // if
      
      return $this->history;
    } // history
    
    // ---------------------------------------------------
    //  Permissions
    // ---------------------------------------------------
    
    /**
     * Returns true if $user can view invoice
     *
     * @param User $user
     * @return boolean
     */
    function canView($user) {
      return $user->isFinancialManager() || (Invoices::canAccessCompanyInvoices($user, $this->getCompany()) && !$this->isDraft());
    } // canView

    /**
     * Returns true if $user can see related invoice items
     *
     * @param User $user
     * @return boolean
     */
    function canViewRelatedItems($user) {
      return $user->isFinancialManager();
    } // canViewRelatedItems

    /**
     * Returns true if $user can edit this invoice
     *
     * @param User $user
     * @return boolean
     */
    function canEdit($user) {
      if($this->getStatus() == INVOICE_STATUS_DRAFT || $this->getStatus() == INVOICE_STATUS_ISSUED) {
        return $user->isFinancialManager();
      } else {
        return false;
      } // if
    } // canEdit

    /**
     * Returns true if $user can delete this invoice
     *
     * @param User $user
     * @return boolean
     */
    function canDelete($user) {
      if($this->getStatus() == INVOICE_STATUS_DRAFT) {
        return $user->isFinancialManager();
      } else {
        return false;
      } // if
    } // canDelete
    
    /**
     * Returns true if this invoice can be issue by $user
     *
     * @param User $user
     * @return boolean
     */
    function canIssue($user) {
      return $this->getStatus() == INVOICE_STATUS_DRAFT && $user->isFinancialManager();
    } // canIssue
    
    /**
     * Returns true if $user can cancel this invoice
     *
     * @param User $user
     * @return boolean
     */
    function canCancel($user) {
      if($this->getStatus() == INVOICE_STATUS_ISSUED || $this->getStatus() == INVOICE_STATUS_PAID) {
        return $user->isFinancialManager();
      } else {
        return false;
      } // if
    } // canCancel

    /**
     * Returns true if $user can add payment to this invoice
     *
     * @param User $user
     * @return boolean
     */
    function canAddPayment($user) {
      return $this->getStatus() == INVOICE_STATUS_ISSUED && $user->isFinancialManager();
    } // canAddPayment
    
    // ---------------------------------------------------
    //  URL-s
    // ---------------------------------------------------
    
    /**
     * Return PDF doc URL
     *
     * @return string
     */
    function getPdfUrl() {
      return Router::assemble('invoice_pdf', array(
      	'invoice_id' => $this->getId(),
     		'force' => true,
     		'disposition' => 'attachment'
      ));
    } // getPdfUrl
    
    /**
     * Return company view URL
     *
     * @return string
     */
    function getCompanyViewUrl() {
      return Router::assemble('people_company_invoice', array('invoice_id' => $this->getId(), 'company_id' => $this->getCompanyId()));
    } // getCompanyViewUrl
    
    /**
     * Return public PDF URL accessible from company invoices page
     *
     * @return string
     */
    function getCompanyPdfUrl() {
      return Router::assemble('people_company_invoice_pdf', array(
      	'invoice_id' => $this->getId(),
      	'company_id' => $this->getCompanyId(),
     		'force' => true,
     		'disposition' => 'attachment'
      ));
    } // getCompanyPdfUrl

    /**
     * Return send invoice URL
     *
     * @return string
     */
    function getIssueUrl() {
      return Router::assemble('invoice_issue', array('invoice_id' => $this->getId()));
    } // getIssueUrl
    
    /**
     * Return cancel invoice URL
     *
     * @return string
     */
    function getCancelUrl() {
      return Router::assemble('invoice_cancel', array('invoice_id' => $this->getId()));
    } // getCancelUrl

    /**
     * Return duplicate invoice URL
     *
     * @return string
     */
    function getDuplicateUrl() {
      return Router::assemble('invoices_add', array('duplicate_invoice_id' => $this->getId()));
    } // getDuplicateUrl

    /**
     * Return add payment URL
     *
     * @return string
     */
    function getAddPaymentUrl() {
      return Router::assemble('invoice_custom_payments_add_old', array('invoice_id' => $this->getId()));
    } // getAddPaymentUrl
     
    /**
     * Return invoice time URL
     *
     * @return string
     */
    function getTimeUrl() {
      return Router::assemble('invoice_time', array('invoice_id' => $this->getId()));
    } // getTimeUrl
    
    /**
     * Return invoice expense URL
     *
     * @return string
     */
    function getExpenseUrl() {
      return Router::assemble('invoice_expenses', array('invoice_id' => $this->getId()));
    } // getExpenseUrl
    
    /**
     * Return release URL
     *
     * @return string
     */
    function getReleaseUrl() {
      return Router::assemble('invoice_items_release', array('invoice_id' => $this->getId()));
    } // getReleaseTimeUrl
    
    /**
     * Return notify URL
     * 
     * @return string
     */
    function getNotifyUrl() {
       return Router::assemble('invoice_notify', array('invoice_id' => $this->getId()));
    } // getNotifyUrl
     
    // ---------------------------------------------------
    //  System
    // ---------------------------------------------------
    
		/**
     * Return array or property => value pairs that describes this object
     *
     * $user is an instance of user who requested description - it's used to get
     * only the data this user can see
     *
     * @param IUser $user
     * @param boolean $detailed
     * @param boolean $for_interface
     * @return array
     */
    function describe(IUser $user, $detailed = false, $for_interface = false) {
      $result = parent::describe($user, $detailed, $for_interface);
      
      $result['currency'] = array(
      	'id' => $this->getCurrencyId(),
      	'code' => $this->getCurrencyCode(),
      	'name' => $this->getCurrencyName()
      );
      
      $result['urls']['issue'] = $this->getIssueUrl();
      $result['urls']['cancel'] = $this->getCancelUrl();
      $result['urls']['duplicate'] = $this->getDuplicateUrl();      
      $result['urls']['pdf'] = $this->getPdfUrl();
      $result['urls']['delete'] = $this->getDeleteUrl();
      
      $result['state']['is_draft'] = $this->isDraft();
      $result['state']['is_issued'] = $this->isIssued();
      $result['state']['is_overdue'] = $this->isOverdue();
      $result['state']['is_paid'] = $this->isPaid();
      $result['state']['is_canceled'] = $this->isCanceled();
      
      $result['status'] = (int) $this->getStatus();
      
      $result['name'] = $this->getName();
      $result['short_name'] = $this->getName(true);
      
      $result['based_on'] = $this->getBasedOn();
      $result['created_by'] = $this->getCreatedBy();
      $result['created_on'] = $this->getCreatedOn();
      $result['issued_on'] = $this->getIssuedOn();
      $result['issued_on_month'] = $this->getIssuedOn() ? date('m-Y', $this->getIssuedOn()->getTimestamp()) : null;
      $result['due_on'] = $this->getDueOn();
      $result['due_on_month'] = $this->getDueOn() ? date('m-Y', $this->getDueOn()->getTimestamp()) : null;
      $result['paid_on'] = $this->isPaid() ? $this->getClosedOn() : null;
      $result['closed_on'] = $this->isCanceled() ? $this->getClosedOn() : null;
      
      $result['issued_by'] = $this->isIssued() || $this->getIssuedBy() instanceof IUser ? $this->getIssuedBy() : null;
      $result['paid_by'] = $this->isPaid() ? $this->getClosedBy() : null;
      $result['closed_by'] = $this->isCanceled() ? $this->getClosedBy() : null;
      
      
      $result['payments'] = array(
        'paid_amount' => $this->payments()->getPaidAmount(),
        'paid_amount_percentage' => $this->payments()->getPercentPaid(),
        'total_payments' => $this->payments()->getTotalPayments(),
        'permissions' => array(
          'can_view' => $this->payments()->canView($user),
          'can_edit' => $this->payments()->canEdit($user),
          'can_delete' => $this->payments()->canDelete($user)
        ),
        'show_payment_btn' => ($this->payments()->hasDefinedGateways() && $this->payments()->canMake($user) || ($user->isFinancialManager())) && $this->getStatus() == INVOICE_STATUS_ISSUED,
        'add_url' => $this->payments()->getAddUrl()
      );
   
      
      $project = $this->getProject();
      if ($project instanceof Project) {
      	$result['project'] = array(
      		'name' => $project->getName(),
      		'permalink' => $project->getViewUrl()
      	);
      } // if
      
      $result['client'] = array(
      	'id' => $this->getCompanyId(),
      	'name' => $this->getCompany()->getName(),
      	'address' => $this->getCompanyAddress(),
      	'permalink'	=> $this->getCompanyViewUrl()
      );
      
      $result['note'] = $this->getNote();
      $result['comment'] = $this->getComment();

      $result['subtotal'] = $this->getTotal(false);
      $result['tax'] = $this->getTax(false);
      $result['total'] = $this->getTaxedTotal(false);
      
      
      $items = $this->getItems();
      if (is_foreachable($items)) {
      	foreach ($items as $item) {
      		$result['items'][] = $item->describe($user, false, $for_interface);
      	} // foreach
      } // if
      
      $result['common_tax'] = $this->commonTaxRate();
      $result['tax_column_text'] = $this->getTaxColumnText();
            
      $result['permissions'] = array(
      	'can_view' => $this->canView($user),
      	'can_add'	=> $this->canAdd($user),
      	'can_edit' => $this->canEdit($user),
      	'can_delete' => $this->canDelete($user),
      	'can_issue' => $this->canIssue($user),
      	'can_cancel' => $this->canCancel($user),
      	'can_add_payment' => $this->canAddPayment($user),
      );
      
      return $result;
    } // describe
    
    /**
     * Validate before save
     *
     * @param ValidationErrors $errors
     */
    function validate(ValidationErrors &$errors) {
      if(!$this->validatePresenceOf('company_id')) {
        $errors->addError(lang('Client is required'), 'company_address');
      } // if
      
      if(!$this->validatePresenceOf('company_address')) {
        $errors->addError(lang('Client address is required'), 'company_address');
      } // if
      
      if($this->validatePresenceOf('number')) {
        if(!$this->validateUniquenessOf('number')) {
          $errors->addError(lang('Invoice No. needs to be unqiue'), 'number');
        } // if
      } // if
    } // validate
    
    /**
     * Delete existing invoice from database
     *
     * @return boolean
     */
    function delete() {
      try {
        DB::beginWork('Removing invoice @ ' . __CLASS__);
        
        parent::delete();
        
        InvoiceItems::deleteByInvoice($this);
        //InvoicePayments::deleteByInvoice($this);
        $this->payments()->delete();
        
        DB::commit('Invoice removed @ ' . __CLASS__);
      } catch(Exception $e) {
        DB::rollback('Failed to remove invoice @ ' . __CLASS__);
        throw $e;
      } // try
      
      return true;
    } // delete

  }