array() ); protected $_has_many = array( 'invoice_item'=>array('far_key'=>'id'), 'invoice_item_tax'=>array('through'=>'invoice_item'), 'invoice_memo'=>array('far_key'=>'id'), 'service'=>array('through'=>'invoice_item'), 'payment'=>array('through'=>'payment_item'), 'payment_item'=>array('far_key'=>'id'), ); protected $_sorting = array( 'id'=>'DESC', ); protected $_display_filters = array( 'date_orig'=>array( array('Config::date',array(':value')), ), 'due_date'=>array( array('Config::date',array(':value')), ), 'status'=>array( array('StaticList_YesNo::get',array(':value',TRUE)), ), ); // Items belonging to an invoice protected $_sub_items_load = array( 'invoice_item'=>'service_id,item_type,date_start,date_stop', ); private $_render = array(); // Our required Interface Methods public function cart_item() { return new Cart_Item(1,sprintf('Invoice: %s',$this->refnum()),$this->due()); } /** * Return if this invoice is already in the cart */ public function cart_exists() { return count(Cart::instance()->get($this->mid(),$this->id)); } // Our local methods /** * Return a list of valid checkout options for this invoice */ public function checkout() { $due = $this->due(); return ORM::factory('Checkout') ->where_active() ->where('amount_min','<=',$due) ->where_open() ->and_where('amount_max','>=',$due) ->or_where('amount_max','is',null) ->where_close()->find_all(); } /** * Display the amount due */ public function due($format=FALSE) { // If the invoice is active calculate the due amount $result = $this->status ? Currency::round($this->total()-$this->payments_total(),1) : 0; return $format ? Currency::display($result) : $result; } public function email() { return ORM::factory('Email_Log') ->where('module_id','=',$this->mid()) ->where('module_data','=',$this); } /** * Display the Invoice Number */ public function id() { return sprintf('%06s',$this->id); } /** * Return the recurring schedules that are on an invoice * * @param $period Return an Array of items for that period */ public function items_periods($period=FALSE) { if (is_null($period)) return isset($this->_render['OTHER']) ? $this->_render['OTHER'] : array(); elseif ($period) return isset($this->_render['SCHED'][$period]) ? $this->_render['SCHED'][$period] : array(); else return array_keys($this->_render['SCHED']); } /** * Return a summary of all itemss on an invoice */ public function items_summary() { $result = array(); foreach ($this->subitems() as $iio) { // We only summarise item_type=0 if (! $iio->item_type == 0) continue; if ($iio->module() instanceof Model_Product) { $p = $iio->module()->title(); if (! isset($result[$p])) { $result[$p]['quantity'] = 0; $result[$p]['subtotal'] = 0; } $result[$p]['quantity']++; $result[$p]['subtotal'] += $iio->subtotal(); } } return $result; } /** * Return the item titles that are on an invoice * * @param $title Return an Array of items for that title */ public function items_titles($title=NULL,$period=NULL) { $result = array(); foreach ($this->subitems() as $ito) { if ($ito->service_id) { if (is_null($title) AND ! in_array($ito->title(),$result) AND (is_null($period) OR ($period == $ito->recurring_schedule))) array_push($result,$ito->title()); elseif (($ito->title() == $title AND (is_null($period) OR ($period == $ito->recurring_schedule))) OR is_null($ito->recurring_schedule)) array_push($result,$ito); } else { throw HTTP_Exception::factory(501,'Not handled non-services'); } } return $result; } public function payments() { return $this->payment_item->find_all(); } public function payments_total($format=FALSE) { $result = 0; foreach ($this->payments() as $po) $result += $po->alloc_amt; return $format ? Currency::display($result) : $result; } /** * This function takes care of all the invoice items so that they are rendered only once * * We organise items by * + recurring_schedule for service items item_type=0 * + recurring_schedule for service items for item_type!=0 (that have an item_type=0 previously selected) * + Other Items */ public function pre_render() { $this->_render = array(); $this->_render['SCHED'] = array(); $processed = array(); foreach ($this->subitems() as $iio) { if ($iio->id AND in_array($iio->id,$processed)) continue; if ($iio->recurring_schedule) { $this->_render['SCHED'][$iio->recurring_schedule][] = $iio; } elseif (Object::in_array('service_id',$iio->service_id,$this->_render['SCHED'],TRUE)) { // Do nothing } else { $this->_render['OTHER'][] = $iio; } array_push($processed,$iio->id); } } /** * Display the Invoice Reference Number */ public function refnum() { return sprintf('%s-%06s',$this->account->accnum(),$this->id); } /** * Check the reminder value */ public function remind($key) { if (isset($this->reminders[$key])) return (is_array($this->reminders[$key])) ? end($this->reminders[$key]) : $this->reminders[$key]; else return FALSE; } /** * Returns the array of Email Template Objects */ public function reminders($key=NULL,$format=FALSE) { if (is_null($key) AND ! $this->reminders) return array(); $prefix = 'task_invoice_'; if (is_null($key)) { $result = array(); foreach ($this->reminders as $k=>$v) array_push($result,ORM::factory('Email_Template',array('name'=>$prefix.$k))); return $result; } else { $key = preg_replace("/^$prefix/",'',$key); return isset($this->reminders[$key]) ? ($format ? Config::date($this->reminders[$key]) : $this->reminders[$key]) : NULL; } } public function save(Validation $validation = NULL) { // Our items will be clobbered once we save the object, so we need to save it here. $subitems = $this->subitems(); // If this is a new invoice, we'll need to do more work $new = ! $this->loaded(); // Save the invoice parent::save($validation); // Need to save the associated items and their taxes if ($this->loaded()) { foreach ($subitems as $iio) { $iio->invoice_id = $this->id; if (! $iio->changed()) continue; $iio->save(); if (! $iio->saved()) { $this->void = 1; $this->save(); break; } // If this is a new invoice, we'll need to update some other items if ($new) { // Update next invoice date if ($iio->item_type == 0) { $iio->service->date_next_invoice = $iio->date_stop+86400; // @todo Mark invoice as cancelled and write a memo, then... if (! $iio->service->save()) throw new Kohana_Exception('Problem saving service :service for invoice_item :invoice_item - Failed save()',array(':invoice_item'=>$iio->id,':service'=>$iio->service_id)); } // Update charge iteme if (($x=$iio->module()) AND ($x instanceof Model_Charge)) { $x->processed = 1; // @todo Mark invoice as cancelled and write a memo, then... if (! $x->save()) throw new Kohana_Exception('Problem saving charge :charge for invoice_item :invoice_item - Failed save()',array(':invoice_item'=>$iio->id,':charge'=>$x->id)); } } // @todo Need to save discount information } } else throw new Kohana_Exception('Couldnt save invoice for some reason?'); return $this; } /** * Return the service items on an invoice relating to an invoice_item * IE: item_type == 0 */ public function service_items(Model_Invoice_Item $o) { $result = array(); // At the moment, we only return items pertaining to a service if (! $o->service_id) return $result; foreach ($this->subitems() as $iio) if ($iio->item_type == 0 AND $iio->service_id == $o->service_id) array_push($result,$iio); return $result; } /** * Return the extra items on an invoice relating to an invoice_item * IE: item_type != 0 */ public function service_items_extra(Model_Invoice_Item $o) { $result = array(); foreach ($this->subitems() as $iio) if ($iio->item_type != 0 AND (! $o->service_id OR ($iio->service_id == $o->service_id))) array_push($result,$iio); return $result; } /** * Return the total of all items relating to a service */ public function service_items_tax(Model_Invoice_Item $o,$format=FALSE) { $result = 0; // At the moment, we only return extra items pertaining to a service if (! $o->service_id) return $result; foreach ($this->subitems() as $iio) if ($iio->service_id == $o->service_id) $result += $iio->tax(); return $format ? Currency::display($result) : $result; } /** * Return the total of all items relating to a service */ public function service_items_total(Model_Invoice_Item $o,$format=FALSE) { $result = 0; // At the moment, we only return extra items pertaining to a service if (! $o->service_id) return $result; foreach ($this->subitems() as $iio) if ($iio->service_id == $o->service_id) $result += $iio->total(); return $format ? Currency::display($result) : $result; } public function set_remind($key,$value,$add=FALSE) { $x = $this->reminders; // If our value is null, we'll remove it. if (is_null($value) AND isset($x[$key])) unset($x[$key]); elseif ($add) { if (! is_array($a=$x[$key])) $x[$key] = array($a); $x[$key][] = $value; } else $x[$key] = $value; $this->reminders = $x; $this->save(); return $this->saved(); } /** * Add an item to an invoice */ public function subitem_add(Model_Invoice_Item $iio,Model_Country $co,$taxed=FALSE) { $iio->subitem_add($co,$taxed); array_push($this->_sub_items,$iio); $this->_sub_items_sorted = FALSE; } /** * Return a list of invoice items for this invoice. * @param type [CHARGE|CREDIT|ALL] * @see invoice_items */ public function subitems($type='ALL') { $result = array(); foreach ($this->_sub_items as $iio) { // Ignore voided items if ($iio->void) continue; $return = FALSE; switch ($type) { case 'CHARGE': if ($iio->quantity > 0) $return = TRUE; break; case 'CREDIT': if ($iio->quantity < 0) $return = TRUE; break; case 'ALL': default: $return = TRUE; break; } if ($return) array_push($result,$iio); } return $result; } /** * Return the subtotal of all items */ public function subtotal($format=FALSE) { $result = 0; foreach ($this->subitems() as $ito) $result += $ito->subtotal(); return $format ? Currency::display($result) : $result; } public function tax($format=FALSE) { $result = 0; foreach ($this->subitems() as $ito) $result += $ito->tax(); return $format ? Currency::display($result) : $result; } /** * Return a list of taxes used on this invoice * @todo Move some of this to invoice_item_tax. */ public function tax_summary() { $summary = array(); foreach ($this->subitems() as $iio) { foreach ($iio->tax->find_all() as $iito) { if (! isset($summary[$iito->tax_id])) $summary[$iito->tax_id] = $iito->amount; else $summary[$iito->tax_id] += $iito->amount; } } // @todo This should be removed eventually if (! $summary) $summary[1] = $this->tax(); return $summary; } /** * Return the total of all items */ public function total($format=FALSE) { $result = 0; // This will include charges and credits foreach ($this->subitems() as $ito) $result += $ito->total(); return $format ? Currency::display($result) : $result; } public function total_charges($format=FALSE) { $result = 0; foreach ($this->subitems('CHARGE') as $ito) $result += $ito->subtotal()+$ito->tax(); return $format ? Currency::display($result) : $result; } public function total_credits($format=FALSE) { $result = 0; foreach ($this->subitems('CREDIT') as $ito) $result += ($ito->subtotal()+$ito->tax())*-1; return $format ? Currency::display($result) : $result; } public function total_discounts($format=FALSE) { $result = 0; foreach ($this->subitems() as $ito) $result += $ito->discount(); return $format ? Currency::display($result) : $result; } private function _where_unprocessed() { return $this->where_open()->where('process_status','!=',1)->or_where('process_status','is',NULL)->where_close(); } public function where_unprocessed() { return $this->_where_unprocessed(); } // Our list function /** * Search for invoices matching a term */ public function list_autocomplete($term,$index,$value,array $label,array $limit=array(),array $options=NULL) { // We only show invoice numbers. if (! is_numeric($term)) return array(); $ao = Auth::instance()->get_user(); $this->clear(); $this->where_active(); // Build our where clause $this->where_open() ->where('id','like','%'.$term.'%') ->where_close(); // Restrict results to authorised accounts array_push($limit,array('account_id','IN',$ao->RTM->customers($ao->RTM))); return parent::list_autocomplete($term,$index,$value,$label,$limit,$options); } private function _list_due($authorised) { static $result = array(); if ($authorised) $this->where_authorised(); if (! $result) foreach ($this->_where_active()->_where_unprocessed()->find_all() as $io) if ($io->due()) array_push($result,$io); return $result; } /** * Return a list of invoices that are due, excluding overdue. */ public function list_due($time=NULL,$authorised=TRUE) { $result = array(); foreach ($this->_list_due($authorised) as $io) if (is_null($time) OR $io->due_date > $time) array_push($result,$io); return $result; } public function list_due_total($format=FALSE,$time=NULL,$authorised=TRUE) { $result = 0; foreach ($this->list_due($time,$authorised) as $io) $result += $io->due(); return $format ? Currency::display($result) : $result; } /** * Identify all the invoices that are due */ public function list_overdue($time=NULL,$authorised=TRUE) { $result = array(); if (is_null($time)) $time = time(); foreach ($this->_list_due($authorised) as $io) if ($io->due_date <= $time) array_push($result,$io); return $result; } /** * Return a list of invoices that are over their due date with/without auto billing */ public function list_overdue_billing($time=NULL,$billing=FALSE,$authorised=TRUE) { $result = array(); foreach ($this->list_overdue($time,$authorised) as $io) { $i = FALSE; foreach ($io->service->find_all() as $so) if (($billing AND $so->account_billing_id) OR (! $billing AND ! $so->account_billing_id)) { array_push($result,$io); break; } } return $result; } /** * Return a list of invoices that need to be sent. * @todo This should be optimised a little to return only invoices to send, instead of looking for them. */ public function list_tosend() { return ORM::factory('Invoice')->where_active()->where_open()->where('print_status','is',NULL)->or_where('print_status','!=',1)->where_close(); } } ?>