2013-10-10 02:44:53 +00:00
< ? php defined ( 'SYSPATH' ) or die ( 'No direct access allowed.' );
/**
* This class provides invoice capabilities .
*
* @ package Invoice
* @ category Models
* @ author Deon George
* @ copyright ( c ) 2009 - 2013 Open Source Billing
* @ license http :// dev . osbill . net / license . html
*/
class Model_Invoice extends ORM_OSB implements Cartable {
protected $_belongs_to = array (
'account' => array ()
);
protected $_has_many = array (
'invoice_item' => array ( 'far_key' => 'id' ),
'invoice_item_tax' => array ( 'through' => 'invoice_item' ),
'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 (
2013-04-26 01:42:09 +00:00
array ( 'StaticList_YesNo::get' , array ( ':value' )),
2013-10-10 02:44:53 +00:00
),
);
// Items belonging to an invoice
2013-09-06 05:39:56 +00:00
protected $_sub_items_load = array (
'invoice_item' => 'service_id,item_type' ,
);
2013-10-10 02:44:53 +00:00
/** INTERFACE REQUIREMENTS **/
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 ));
}
/**
* Add an item to an invoice
*/
2013-06-13 13:35:19 +00:00
public function add_item ( Model_Invoice_Item $iio = NULL ) {
// @todo This is the old calling of this function, which should be removed
if ( is_null ( $iio )) {
2013-09-06 05:39:56 +00:00
$c = count ( $this -> _sub_items );
2013-10-10 02:44:53 +00:00
2013-09-06 05:39:56 +00:00
$this -> _sub_items [ $c ] = ORM :: factory ( 'Invoice_Item' );
2013-10-10 02:44:53 +00:00
2013-09-06 05:39:56 +00:00
return $this -> _sub_items [ $c ];
2013-06-13 13:35:19 +00:00
}
2013-09-06 05:39:56 +00:00
array_push ( $this -> _sub_items , $iio );
$this -> _sub_items_sorted = FALSE ;
2013-06-13 13:35:19 +00:00
}
/**
* Return the recurring schedules that are on an invoice
*
* @ param $period Return an Array of items for that period
*/
2013-09-06 05:39:56 +00:00
public function items_periods ( $period = FALSE ) {
2013-06-13 13:35:19 +00:00
$result = array ();
foreach ( $this -> items () as $ito ) {
// We are only interested in item_type=0
if ( $ito -> item_type != 0 )
continue ;
2013-09-06 05:39:56 +00:00
if ( $period === FALSE AND ! in_array ( $ito -> recurring_schedule , $result ))
2013-06-13 13:35:19 +00:00
array_push ( $result , $ito -> recurring_schedule );
2013-09-06 05:39:56 +00:00
elseif ( $ito -> recurring_schedule == $period ) {
// If we have this service already, we'll skip
if ( ! Object :: in_array ( 'service_id' , $ito -> service_id , $result ))
array_push ( $result , $ito );
}
}
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 -> items () 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' );
}
2013-06-13 13:35:19 +00:00
}
return $result ;
}
2013-09-06 05:39:56 +00:00
/**
* 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 -> items () as $iio )
if ( $iio -> item_type == 0 AND $iio -> service_id == $o -> service_id )
array_push ( $result , $iio );
return $result ;
}
2013-06-13 13:35:19 +00:00
/**
* 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 ();
// At the moment, we only return extra items pertaining to a service
if ( ! $o -> service_id )
return $result ;
foreach ( $this -> items () as $iio )
if ( $iio -> item_type != 0 AND $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 -> items () 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 -> items () as $iio )
if ( $iio -> service_id == $o -> service_id )
$result += $iio -> total ();
return $format ? Currency :: display ( $result ) : $result ;
2013-10-10 02:44:53 +00:00
}
/**
* 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 ? $this -> total () - $this -> payments_total () : 0 ;
return $format ? Currency :: display ( $result ) : Currency :: round ( $result );
}
/**
* Display the Invoice Number
*/
public function id () {
return sprintf ( '%06s' , $this -> id );
}
/**
* Return a list of invoice items for this invoice .
* @ param type [ CHARGE | CREDIT | ALL ]
* @ see invoice_items
*/
public function items ( $type = 'ALL' ) {
$result = array ();
2013-09-06 05:39:56 +00:00
foreach ( $this -> _sub_items as $ito ) {
2013-10-09 12:26:59 +00:00
// Ignore voided items
if ( $ito -> void )
continue ;
2013-10-10 02:44:53 +00:00
$return = FALSE ;
switch ( $type ) {
case 'CHARGE' :
2013-10-09 12:26:59 +00:00
if ( $ito -> product_id OR $ito -> quantity > 0 )
2013-10-10 02:44:53 +00:00
$return = TRUE ;
break ;
case 'CREDIT' :
2013-10-09 12:26:59 +00:00
if ( ! $ito -> product_id AND $ito -> quantity < 0 )
2013-10-10 02:44:53 +00:00
$return = TRUE ;
break ;
case 'ALL' :
default :
$return = TRUE ;
break ;
}
if ( $return )
array_push ( $result , $ito );
}
return $result ;
}
/**
* Provide a sorted list of items by an index
*/
public function items_index ( $index ) {
static $result = array ();
// We'll return a cached result for quicker processing
if ( ! $this -> _changed AND array_key_exists ( $index , $result ))
return $result [ $index ];
foreach ( $this -> items () as $ito ) {
switch ( $index ) {
case 'account' :
if ( ! $ito -> service_id )
$result [ $index ][ $ito -> id ] = $ito ;
break ;
case 'period' :
// We only show the services in this period
if ( ! is_null ( $ito -> recurring_schedule ) AND ( empty ( $result [ $index ][ $ito -> recurring_schedule ]) OR ! in_array ( $ito -> service_id , $result [ $index ][ $ito -> recurring_schedule ])))
$result [ $index ][ $ito -> recurring_schedule ][] = $ito -> service_id ;
break ;
case 'service' :
default :
if ( $ito -> service_id )
$result [ $index ][ $ito -> service_id ][] = $ito ;
break ;
}
}
return array_key_exists ( $index , $result ) ? $result [ $index ] : array ();
}
/**
* Get a list of invoice_items for a service_id on an invoice
*
* We use this to list details by service on an invoice .
*/
// @todo to retire
public function items_services ( array $items = array ()) {
$result = array ();
if ( ! $items )
$items = $this -> items ();
foreach ( $items as $ito )
if ( $ito -> service_id AND empty ( $result [ $ito -> service_id ]))
$result [ $ito -> service_id ] = $ito ;
return $result ;
}
// @todo to retire
public function items_invoice () {
$result = array ();
$items = $this -> items ();
foreach ( $items as $ito )
if ( ! $ito -> service_id AND empty ( $result [ $ito -> id ]))
$result [ $ito -> id ] = $ito ;
return $result ;
}
/**
* Return all invoice items for a specific service
*/
public function items_service ( $service_id ) {
$svs = $this -> items_index ( 'service' );
if ( array_key_exists ( $service_id , $svs )) {
Sort :: MAsort ( $svs [ $service_id ], 'item_type' );
return $svs [ $service_id ];
} else
return array ();
}
// @todo to retire
/**
* Return a list of periods and services
*
* This is so that we can list items summarised by billing period
*/
public function items_service_periods () {
$result = array ();
$c = array ();
foreach ( $this -> items () as $ito )
if ( $ito -> service_id ) {
// If we have already covered a service with no recurring_schedule
if ( ! $ito -> recurring_schedule AND in_array ( $ito -> service_id , $c ))
continue ;
array_push ( $c , $ito -> service_id );
$result [ $ito -> recurring_schedule ][] = $ito ;
}
return $result ;
}
/**
* Summarise the items on an invoice
*
* We summaries based on product .
*/
// @todo
public function items_summary () {
$result = array ();
foreach ( $this -> items () as $ito ) {
// We only summarise item_type=0
if ( ! $ito -> item_type == 0 )
continue ;
2013-04-26 01:42:09 +00:00
$t = $ito -> product -> title ();
2013-10-10 02:44:53 +00:00
if ( ! isset ( $result [ $t ])) {
$result [ $t ][ 'quantity' ] = 0 ;
$result [ $t ][ 'subtotal' ] = 0 ;
}
$result [ $t ][ 'quantity' ] += $ito -> quantity ;
$result [ $t ][ 'subtotal' ] += $ito -> subtotal ();
}
return $result ;
}
/**
* Calculate the total for items for a service
*/
public function items_service_total ( $sid ) {
$total = 0 ;
foreach ( $this -> items_service ( $sid ) as $ito )
$total += $ito -> total ();
return $total ;
}
/**
* Calculate the tax of items for a service
*/
public function items_service_tax ( $sid ) {
$total = 0 ;
foreach ( $this -> items_service ( $sid ) as $ito )
$total += $ito -> tax ();
return $total ;
}
/**
* Calculate the discounts of items for a service
*/
public function items_service_discount ( $sid ) {
$total = 0 ;
foreach ( $this -> items_service ( $sid ) as $ito )
$total += $ito -> discount ();
return $total ;
}
public function min_due ( $date ) {
return strtotime ( date ( 'Y-M-d' ,( $date < time ()) ? time () + ORM :: factory ( 'Invoice' ) -> config ( 'DUE_DAYS_MIN' ) * 86400 : $date ));
}
/**
* Display the Invoice Reference Number
*/
public function refnum () {
return sprintf ( '%s-%06s' , $this -> account -> accnum (), $this -> id );
}
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 ) : Currency :: round ( $result );
}
/**
* Check the reminder value
*/
public function remind ( $key ) {
2013-07-05 06:11:37 +00:00
if ( isset ( $this -> reminders [ $key ]))
return ( is_array ( $this -> reminders [ $key ])) ? end ( $this -> reminders [ $key ]) : $this -> reminders [ $key ];
2013-10-10 02:44:53 +00:00
else
return FALSE ;
}
public function save ( Validation $validation = NULL ) {
// Our items will be clobbered once we save the object, so we need to save it here.
$items = $this -> items ();
2013-09-06 05:39:56 +00:00
// If this is a new invoice, we'll need to do more work
$new = ! $this -> loaded ();
2013-10-10 02:44:53 +00:00
// Save the invoice
2013-09-06 05:39:56 +00:00
parent :: save ( $validation );
2013-10-10 02:44:53 +00:00
// Need to save the associated items and their taxes
if ( $this -> loaded ()) {
foreach ( $items as $iio ) {
$iio -> invoice_id = $this -> id ;
if ( ! $iio -> changed ())
continue ;
if ( ! $iio -> check ()) {
// @todo Mark invoice as cancelled and write a memo, then...
throw new Kohana_Exception ( 'Problem saving invoice_item for invoice :invoice - Failed check()' , array ( ':invoice' => $this -> id ));
}
$iio -> save ();
if ( ! $iio -> saved ()) {
// @todo Mark invoice as cancelled and write a memo, then...
throw new Kohana_Exception ( 'Problem saving invoice_item for invoice :invoice - Failed save()' , array ( ':invoice' => $this -> id ));
}
2013-09-06 05:39:56 +00:00
// 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 ( $iio -> charge_id ) {
$iio -> charge -> processed = 1 ;
// @todo Mark invoice as cancelled and write a memo, then...
if ( ! $iio -> charge -> save ())
throw new Kohana_Exception ( 'Problem saving charge :charge for invoice_item :invoice_item - Failed save()' , array ( ':invoice_item' => $iio -> id , ':charge' => $iio -> charge_id ));
}
}
2013-10-10 02:44:53 +00:00
// @todo Need to save discount information
}
} else
throw new Kohana_Exception ( 'Couldnt save invoice for some reason?' );
return TRUE ;
}
public function set_remind ( $key , $value , $add = FALSE ) {
2013-07-05 13:37:06 +00:00
$x = $this -> reminders ;
2013-10-10 02:44:53 +00:00
// If our value is null, we'll remove it.
2013-07-05 13:37:06 +00:00
if ( is_null ( $value ) AND isset ( $x [ $key ]))
unset ( $x [ $key ]);
2013-07-05 06:11:37 +00:00
2013-10-10 02:44:53 +00:00
elseif ( $add ) {
2013-07-05 13:37:06 +00:00
if ( ! is_array ( $a = $x [ $key ]))
$x [ $key ] = array ( $a );
2013-10-10 02:44:53 +00:00
2013-07-05 13:37:06 +00:00
$x [ $key ][] = $value ;
2013-10-10 02:44:53 +00:00
} else
2013-07-05 13:37:06 +00:00
$x [ $key ] = $value ;
2013-10-10 02:44:53 +00:00
2013-07-05 13:37:06 +00:00
$this -> reminders = $x ;
2013-10-10 02:44:53 +00:00
$this -> save ();
2013-07-05 06:11:37 +00:00
2013-10-10 02:44:53 +00:00
return $this -> saved ();
}
/**
* Return the subtotal of all items
*/
public function subtotal ( $format = FALSE ) {
$result = 0 ;
foreach ( $this -> items () as $ito )
$result += $ito -> subtotal ();
return $format ? Currency :: display ( $result ) : Currency :: round ( $result );
}
public function tax ( $format = FALSE ) {
$result = 0 ;
foreach ( $this -> items () as $ito )
$result += $ito -> tax ();
return $format ? Currency :: display ( $result ) : Currency :: round ( $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 -> items () as $ito ) {
foreach ( $ito -> invoice_item_tax -> find_all () as $item_tax ) {
if ( ! isset ( $summary [ $item_tax -> tax_id ]))
$summary [ $item_tax -> tax_id ] = $item_tax -> amount ;
else
$summary [ $item_tax -> tax_id ] += $item_tax -> 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 -> items () as $ito )
$result += $ito -> total ();
return $format ? Currency :: display ( $result ) : Currency :: round ( $result );
}
public function total_charges ( $format = FALSE ) {
$result = 0 ;
foreach ( $this -> items ( 'CHARGE' ) as $ito )
$result += $ito -> subtotal () + $ito -> tax ();
return $format ? Currency :: display ( $result ) : Currency :: round ( $result );
}
public function total_credits ( $format = FALSE ) {
$result = 0 ;
foreach ( $this -> items ( 'CREDIT' ) as $ito )
$result += ( $ito -> subtotal () + $ito -> tax ()) *- 1 ;
return $format ? Currency :: display ( $result ) : Currency :: round ( $result );
}
public function total_discounts ( $format = FALSE ) {
$result = 0 ;
foreach ( $this -> items () as $ito )
$result += $ito -> discount ();
return $format ? Currency :: display ( $result ) : Currency :: round ( $result );
}
/** LIST FUNCTIONS **/
/**
* Search for invoices matching a term
*/
2013-05-08 09:00:47 +00:00
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 ();
2013-10-10 02:44:53 +00:00
2013-05-08 09:00:47 +00:00
$ao = Auth :: instance () -> get_user ();
2013-10-10 02:44:53 +00:00
2013-05-08 09:00:47 +00:00
$this -> clear ();
$this -> where_active ();
2013-10-10 02:44:53 +00:00
2013-05-08 09:00:47 +00:00
// Build our where clause
$this -> where_open ()
-> where ( 'id' , 'like' , '%' . $term . '%' )
-> where_close ();
2013-10-10 02:44:53 +00:00
2013-05-08 09:00:47 +00:00
// 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 );
2013-10-10 02:44:53 +00:00
}
private function _list_due () {
static $result = array ();
if ( ! $result )
2013-10-09 05:43:41 +00:00
foreach ( $this -> _where_active () -> _where_unprocessed () -> where_authorised () -> find_all () as $io )
2013-10-10 02:44:53 +00:00
if ( $io -> due ())
array_push ( $result , $io );
return $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 ();
}
/**
* Identify all the invoices that are due
*/
public function list_overdue ( $time = NULL ) {
$result = array ();
if ( is_null ( $time ))
$time = time ();
foreach ( $this -> _list_due () 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 ) {
$result = array ();
foreach ( $this -> list_overdue ( $time ) 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 are due , excluding overdue .
*/
public function list_due ( $time = NULL ) {
$result = array ();
2013-10-09 05:43:41 +00:00
if ( is_null ( $time ))
$time = time ();
2013-10-10 02:44:53 +00:00
foreach ( $this -> _list_due () as $io )
2013-10-09 05:43:41 +00:00
if ( $io -> due_date > $time )
array_push ( $result , $io );
2013-10-10 02:44:53 +00:00
return $result ;
}
2013-05-28 11:35:54 +00:00
public function list_due_total ( $format = FALSE , $time = NULL ) {
$result = 0 ;
foreach ( $this -> list_due ( $time ) as $io )
$result += $io -> due ();
return $format ? Currency :: display ( $result ) : Currency :: round ( $result );
}
2013-10-10 02:44:53 +00:00
/**
* 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 ();
}
public function html () {
// @todo This should be in a config file.
$css = '<style type="text/css">' ;
$css .= 'table.box-left { border: 1px solid #AAAACC; margin-right: auto; }' ;
$css .= 'tr.head { font-weight: bold; }' ;
$css .= 'td.head { font-weight: bold; }' ;
$css .= 'td.right { text-align: right; }' ;
$css .= 'tr.odd { background-color: #FCFCFE; }' ;
$css .= 'tr.even { background-color: #F6F6F8; }' ;
$css .= '</style>' ;
$output = View :: factory ( 'invoice/user/email' )
-> set ( 'mediapath' , Route :: get ( 'default/media' ))
-> set ( 'io' , $this );
return $css . $output ;
}
}
?>