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('Site::Date',array(':value')), ), 'due_date'=>array( array('Site::Date',array(':value')), ), 'status'=>array( array('StaticList_YesNo::get',array(':value',TRUE)), ), 'void'=>array( array('StaticList_YesNo::get',array(':value',TRUE)), ), ); // Items belonging to an invoice protected $_sub_items_load = array( 'invoice_item'=>array('service_id','product_id','item_type','date_start','date_stop'), ); protected $_compress_column = array( 'reminders', ); private $_render = array(); /** REQUIRED ABSTRACT METHODS **/ /** 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)); } /** LOCAL METHODS **/ /** * Add items to the invoice while building it */ public function add_sub_item(Model_Invoice_Item $iio,$taxed=FALSE) { $iio->add_sub_item($this->account->country,$taxed); $this->subitem_add($iio); } /** * 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); } /** * Return an array of items to render, in appropriate order */ public function items_render() { $result = $track = array(); // If the invoice hasnt been saved, then we'll need to assign some fake IDs to the items $c = 0; if (! $this->_loaded) foreach ($this->_sub_items as $iio) $iio->id = '****:'.$c++; // First work through our recurring schedule items $result['s'] = array(); foreach ($this->recur_schedules() as $rs) foreach ($this->recur_schedule_services($rs) as $sid) foreach ($this->service_products($sid) as $pid) { // Get all the services with this recur schedule first, then get service charges with rs=NULL foreach ($this->_sub_items as $iio) { // If this is not the recur schedule or service we are working with, skip it if ($iio->service_id != $sid OR $iio->product_id != $pid OR $iio->recurring_schedule != $rs OR in_array($iio->id,$track)) continue; // Ensure we dont process this item again array_push($track,$iio->id); if (! $iio->void) array_push($result['s'],$iio); } // Get all the items for this service with a NULL rs foreach ($this->_sub_items as $iio) { // If this is not the recur schedule or service we are working with, skip it if ($iio->service_id != $sid OR $iio->product_id != $pid OR ! is_null($iio->recurring_schedule) OR in_array($iio->id,$track)) continue; // Ensure we dont process this item again array_push($track,$iio->id); if (! $iio->void) array_push($result['s'],$iio); } } // Get service items that dont have a recurring schedule foreach ($this->_sub_items as $iio) if (! $iio->void AND ! in_array($iio->id,$track) AND $iio->service_id) { // Ensure we dont process this item again array_push($track,$iio->id); array_push($result['s'],$iio); } // Next get the items we havent already got $result['other'] = array(); foreach ($this->_sub_items as $iio) if (! $iio->void AND ! in_array($iio->id,$track)) array_push($result['other'],$iio); // Debug #foreach ($result['s'] as $iio) # echo Debug::vars(array('s'=>$iio->object())); #foreach ($result['other'] as $iio) # echo Debug::vars(array('o'=>$iio->object())); return $result; } /** * Return a summary of all itemss on an invoice */ public function items_summary() { $result = array(); $lo = $this->account->language; $track['p'] = $track = array(); foreach ($this->items_render() as $key => $items) { switch ($key) { case 's': $last = ''; foreach ($items as $iio) { if ($iio->product) { $p = $iio->title($lo); } if (! isset($result[$p])) { $result[$p]['quantity'] = 0; $result[$p]['subtotal'] = 0; } $result[$p]['quantity']++; $result[$p]['subtotal'] += $iio->subtotal(); } break; case 'other': break; } } ksort($result); 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; } /** * For a particular recurring schedule, get al lthe services */ private function recur_schedule_services($rs) { $result = array(); if (! $rs) return $result; foreach ($this->_sub_items as $o) if ($o->service_id AND $o->recurring_schedule == $rs AND ! in_array($o->service_id,$result)) array_push($result,$o->service_id); return $result; } /** * Get the recurring schedules on an invoice * Exclude any NULL recurring schedules */ private function recur_schedules() { $result = array(); foreach ($this->_sub_items as $o) if (! is_null($o->recurring_schedule) AND ! in_array($o->recurring_schedule,$result)) array_push($result,$o->recurring_schedule); sort($result); return $result; } /** * Display the Invoice Reference Number */ public function refnum($short=FALSE) { return ($short ? '' : $this->account->refnum(FALSE).'-').sprintf('%06s',$this->id); } /** * Check the reminder value */ public function remind($key) { if (isset($this->reminders[$key])) { $x = $this->reminders[$key]; return is_array($x) ? end($x) : $x; } 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; } /** * For a particular service, get all the products */ private function service_products($sid) { $result = array(); foreach ($this->_sub_items as $o) if ($o->product_id AND $o->service_id == $sid AND ! in_array($o->product_id,$result)) array_push($result,$o->product_id); return $result; } public function service_products_total($sid,$pid,$format=FALSE) { $result = 0; foreach ($this->_sub_items as $o) if ($o->service_id == $sid AND $o->product_id == $pid) $result += $o->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(); } /** * 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=array()) { // 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(); } } ?>