This repository has been archived on 2024-04-08. You can view files and clone it, but cannot push or open issues or pull requests.
Deon George 04ebda2aaa Theme work with focusbusiness and baseadmin
Improvements to NAVBAR, updates to StaticList methods, other minor items
Enable product category rendering and other minor improvements
Added ADSL-large category price plan
2013-10-10 13:56:13 +11:00

650 lines
15 KiB
PHP

<?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(
array('StaticList_YesNo::get',array(':value')),
),
);
// Items belonging to an invoice
private $invoice_items = array();
/** 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));
}
public function __construct($id = NULL) {
// Load our Model
parent::__construct($id);
// Autoload our Sub Items
if ($this->loaded())
$this->_load_sub_items();
return $this;
}
/**
* Load our invoice items
* We need these so that we can calculate totals, etc
*/
private function _load_sub_items() {
// Load our sub items
$this->invoice_items = $this->invoice_item->find_all()->as_array();
}
/**
* Add an item to an invoice
*/
public function add_item() {
if ($this->loaded() and ! $this->invoice_items)
throw new Kohana_Exception('Need to load invoice_items?');
$c = count($this->invoice_items);
$this->invoice_items[$c] = ORM::factory('Invoice_Item');
return $this->invoice_items[$c];
}
/**
* 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();
foreach ($this->invoice_items as $ito) {
$return = FALSE;
switch ($type) {
case 'CHARGE':
if ($ito->quantity > 0)
$return = TRUE;
break;
case 'CREDIT':
if ($ito->quantity < 0)
$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;
$t = $ito->product->title();
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) {
if (! $this->loaded())
return NULL;
if (! trim($this->reminders))
return FALSE;
if (! preg_match('/^a:/',$this->reminders))
throw new Kohana_Exception('Reminder is not an array? (:reminder)',array(':remind',$this->reminders));
$remind = unserialize($this->reminders);
if (isset($remind[$key]))
return (is_array($remind[$key])) ? end($remind[$key]) : $remind[$key];
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();
// Save the invoice
if ($this->changed())
parent::save($validation);
// 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));
}
// @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) {
if (! $this->loaded())
throw new Kohana_Exception('Cant call :method when a record not loaded.',array(':method',__METHOD__));
if (! trim($this->reminders)) {
$remind = array();
} else {
if (! preg_match('/^a:/',$this->reminders))
throw new Kohana_Exception('Reminder is not an array? (:reminder)',array(':remind',$this->reminders));
$remind = unserialize($this->reminders);
}
// If our value is null, we'll remove it.
if (is_null($value) AND isset($remind[$key]))
unset($remind[$key]);
elseif ($add) {
if (! is_array($a=$remind[$key]))
$remind[$key] = array($a);
$remind[$key][] = $value;
} else
$remind[$key] = $value;
$this->reminders = serialize($remind);
$this->save();
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;
// @todo - This should be required, but during checkout payment processing $pio->invoice->total() showed no invoice items?
if ($this->loaded() AND ! count($this->items()))
$this->_load_sub_items();
// 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
*/
public function list_autocomplete($term,$index='id') {
$result = array();
if (is_numeric($term)) {
$this->clear();
$value = 'account->name(TRUE)';
// Build our where clause
$this->where('id','like','%'.$term.'%');
// @todo This should limit the results so that users dont see other users services.
foreach ($this->find_all() as $o)
$result[$o->$index] = array(
'value'=>$o->$index,
'label'=>sprintf('INV %s: %s',$o->id,Table::resolve($o,$value)),
);
}
return $result;
}
private function _list_due() {
static $result = array();
if (! $result)
foreach ($this->_where_active()->_where_unprocessed()->find_all() as $io)
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();
foreach ($this->_list_due() as $io)
if ($io->due_date > time())
if (is_null($time))
array_push($result,$io);
elseif ($io->due_date <= $time)
array_push($result,$io);
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();
}
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;
}
}
?>