Work on invoices, products and other minor things

This commit is contained in:
Deon George 2011-10-11 19:52:31 +11:00
parent 50a096e22a
commit 718c42be65
12 changed files with 393 additions and 257 deletions

View File

@ -33,7 +33,7 @@ class Controller_Admin_Welcome extends Controller_TemplateDefault {
'account->name()'=>array('label'=>'Account'), 'account->name()'=>array('label'=>'Account'),
'account->display("status")'=>array('label'=>'Active'), 'account->display("status")'=>array('label'=>'Active'),
'id'=>array('label'=>'ID','url'=>'user/invoice/view/'), 'id'=>array('label'=>'ID','url'=>'user/invoice/view/'),
'total_amt'=>array('label'=>'Total','class'=>'right'), 'total(TRUE)'=>array('label'=>'Total','class'=>'right'),
'due(TRUE)'=>array('label'=>'Amount Due','class'=>'right'), 'due(TRUE)'=>array('label'=>'Amount Due','class'=>'right'),
), ),
array('page'=>TRUE)), array('page'=>TRUE)),
@ -51,7 +51,7 @@ class Controller_Admin_Welcome extends Controller_TemplateDefault {
'account->name()'=>array('label'=>'Account'), 'account->name()'=>array('label'=>'Account'),
'account->display("status")'=>array('label'=>'Active'), 'account->display("status")'=>array('label'=>'Active'),
'id'=>array('label'=>'ID','url'=>'user/invoice/view/'), 'id'=>array('label'=>'ID','url'=>'user/invoice/view/'),
'total_amt'=>array('label'=>'Total','class'=>'right'), 'total(TRUE)'=>array('label'=>'Total','class'=>'right'),
'due(TRUE)'=>array('label'=>'Amount Due','class'=>'right'), 'due(TRUE)'=>array('label'=>'Amount Due','class'=>'right'),
), ),
array('page'=>TRUE)), array('page'=>TRUE)),
@ -69,7 +69,7 @@ class Controller_Admin_Welcome extends Controller_TemplateDefault {
'account->name()'=>array('label'), 'account->name()'=>array('label'),
'account->display("status")'=>array('label'=>'Active'), 'account->display("status")'=>array('label'=>'Active'),
'id'=>array('label'=>'ID','url'=>'user/invoice/view/'), 'id'=>array('label'=>'ID','url'=>'user/invoice/view/'),
'total_amt'=>array('label'=>'Total','class'=>'right'), 'total(TRUE)'=>array('label'=>'Total','class'=>'right'),
'due(TRUE)'=>array('label'=>'Amount Due','class'=>'right'), 'due(TRUE)'=>array('label'=>'Amount Due','class'=>'right'),
), ),
array('show_other'=>'due()')), array('show_other'=>'due()')),
@ -78,12 +78,10 @@ class Controller_Admin_Welcome extends Controller_TemplateDefault {
)); ));
// Show un-applied payments // Show un-applied payments
$o = ORM::factory('payment');
Block_Sub::add(array( Block_Sub::add(array(
'title'=>'Unapplied Payments', 'title'=>'Unapplied Payments',
'body'=>Table::display( 'body'=>Table::display(
$o->list_unapplied(), ORM::factory('payment')->list_unapplied(),
25, 25,
array( array(
'date_payment'=>array('label'=>'Pay Date'), 'date_payment'=>array('label'=>'Pay Date'),

View File

@ -12,7 +12,8 @@
class Currency { class Currency {
public static function display($amount) { public static function display($amount) {
// @todo $cid and therefore precision should come from a global session value. // @todo $cid and therefore precision should come from a global session value.
return Num::format($amount,2,TRUE); // @todo This rounding needs to be system configurable.
return Num::format(round($amount,2),2,TRUE);
} }
} }
?> ?>

View File

@ -110,6 +110,10 @@ class Model_Account extends Model_Auth_UserDefault {
foreach ($this->invoices_due($date) as $io) foreach ($this->invoices_due($date) as $io)
$result += $io->due(); $result += $io->due();
// @todo This shouldnt really be required
if ($result < 0)
$result = 0;
return $format ? Currency::display($result) : $result; return $format ? Currency::display($result) : $result;
} }

View File

@ -105,8 +105,10 @@ class Period {
$return = array( $return = array(
'start'=>$period_start, 'start'=>$period_start,
'start_time'=>$period_start,
'date'=>$start, 'date'=>$start,
'end'=>$period_end, 'end'=>$period_end,
'end_time'=>$period_end,
'weekday'=>$weekday, 'weekday'=>$weekday,
'prorata'=>round($remain_time/$total_time,$precision), 'prorata'=>round($remain_time/$total_time,$precision),
'total_time'=>sprintf('%3.1f',$total_time/86400), 'total_time'=>sprintf('%3.1f',$total_time/86400),

View File

@ -371,7 +371,7 @@ class Model_Service_Plugin_ADSL extends Model_Service_Plugin {
switch ($type) { switch ($type) {
case 'invoice_detail_items': case 'invoice_detail_items':
return array( return array(
_('Service Address')=>$this->display('service_address'), _('Service Address')=>$this->service_address ? $this->display('service_address') : '>NotSet<',
_('Contract Until')=>$this->contract_date_end(), _('Contract Until')=>$this->contract_date_end(),
); );
break; break;

View File

@ -11,6 +11,12 @@
* @license http://dev.osbill.net/license.html * @license http://dev.osbill.net/license.html
*/ */
class Controller_Task_Invoice extends Controller_Task { class Controller_Task_Invoice extends Controller_Task {
/**
* Email a list of invoice balances
*
* This function is typically used to list the overdue invoices to the admins
* @param string mode The callback method to use as the data list eg: overdue
*/
public function action_list() { public function action_list() {
$mode = $this->request->param('id'); $mode = $this->request->param('id');
@ -47,7 +53,11 @@ class Controller_Task_Invoice extends Controller_Task {
$this->response->body($output); $this->response->body($output);
} }
/**
* Email a customers a reminder of their upcoming invoices that are due.
*/
public function action_remind_due() { public function action_remind_due() {
$action = array();
// @todo This should go in a config somewhere // @todo This should go in a config somewhere
$days = 5; $days = 5;
$io = ORM::factory('invoice'); $io = ORM::factory('invoice');
@ -72,14 +82,20 @@ class Controller_Task_Invoice extends Controller_Task {
); );
// @todo Record email log id if possible. // @todo Record email log id if possible.
if ($et->send()) if ($et->send()) {
$io->set_remind($key,time()); $io->set_remind($key,time());
array_push($action,(string)$io);
}
} }
$this->response->body(_('Due Reminders Sent.')); $this->response->body(_('Due Reminders Sent: ').join('|',$action));
} }
/**
* Email a customers when their invoices are now overdue.
*/
public function action_remind_overdue() { public function action_remind_overdue() {
$action = array();
$io = ORM::factory('invoice'); $io = ORM::factory('invoice');
$notice = $this->request->param('id'); $notice = $this->request->param('id');
$x = NULL; $x = NULL;
@ -133,11 +149,114 @@ class Controller_Task_Invoice extends Controller_Task {
); );
// @todo Record email log id if possible. // @todo Record email log id if possible.
if ($et->send()) if ($et->send()) {
$io->set_remind($key,time()); $io->set_remind($key,time());
array_push($action,(string)$io);
}
} }
$this->response->body(_('Overdue Reminders Sent: ').$notice); $this->response->body(_('Overdue Reminders Sent: ').join('|',$action));
}
/**
* Generate our services invoices, based on the service next invoice date
*
* @param int ID Service ID to generate invoice for (optional)
*/
public function action_serviceinvoices() {
$action = array();
$snd = array(); // Our service next billing dates that need to be updated if this is successful.
$sid = $this->request->param('id');
// Sort our service by account_id, then we can generate 1 invoice.
$svs = ORM::factory('service')->list_invoicesoon();
Sort::MAsort($svs,'account_id,date_next_invoice');
$aid = $due = $io = NULL;
foreach ($svs as $so) {
if (! is_null($sid) AND $sid != $so->id)
continue;
// Close off invoice, and start a new one.
if (is_null($io) OR (is_null($aid) AND $aid != $so->account_id) OR (is_null($due) AND $due != $io->min_due($so->date_next_invoice))) {
// Close this invoice.
if (! is_null($io)) {
// Save our invoice.
if (! $io->save())
throw new Kohana_Exception('Failed to save invoice :invoice for service :service',array(':invoice'=>$io->id,':service'=>$so->id));
}
// Start a new invoice.
$io = ORM::factory('invoice');
$io->due_date = $due = $io->min_due($so->date_next_invoice);
$io->account_id = $aid = $so->account_id;
$io->status = TRUE;
}
$ppa = $so->product->get_price_array();
// @todo Need to check our recurr_weekday configuration for items that need to be pro-rated and items that are billed on absolute dates.
$pdata = Period::details($so->recur_schedule,$so->product->price_recurr_weekday,$so->date_next_invoice,TRUE);
$iio = $io->add_item();
$iio->service_id = $so->id;
$iio->product_id = $so->product_id;
$iio->quantity = 1;
$iio->item_type = 0;
$iio->discount_amt = null; // @todo
$iio->price_type = $so->price_type; // @todo Do we need this?
// @todo Might be a better way to do this
$iio->price_base = isset($ppa[$so->recur_schedule]['price_base']) ? $ppa[$so->recur_schedule]['price_base'] : 0;
$iio->recurring_schedule = $so->recur_schedule;
$iio->date_start = $pdata['start_time']; // @todo
$iio->date_stop = $pdata['end_time']; // @todo
// Our service next billing date, if this invoice generation is successful.
$snd[$so->id] = $pdata['end_time']+86400;
array_push($action,(string)$so->id);
}
// Save our invoice.
if (! $io->save())
throw new Kohana_Exception('Failed to save invoice :invoice for service :service',array(':invoice'=>$io->id,':service'=>$so->id));
// Update our service next billing dates.
foreach ($snd as $sid=>$date) {
$so = ORM::factory('service',$sid);
$so->date_next_invoice = $date;
$so->save();
}
$this->response->body(_('Services Invoiced: ').join('|',$action));
}
/** END **/
public function action_audit_invoice_items() {
$output = '';
foreach (ORM::factory('invoice_item')->find_all() as $iio) {
if ($iio->product_name AND $iio->product_id) {
if (md5(strtoupper($iio->product_name)) == md5(strtoupper($iio->product->name()))) {
$iio->product_name = null;
$iio->save();
} else {
print_r(array("DIFF",'id'=>$iio->id,'pn'=>serialize($iio->product_name),'ppn'=>serialize($iio->product->name()),'pid'=>$iio->product_id,'test'=>strcasecmp($iio->product_name,$iio->product->name())));
}
}
#if ($iio->product->prod_plugin_file == 'HOST') {
# if ($iio->service->name() == strtoupper($iio->domain_name))
# $iio->domain_name=null;
#}
#if ($iio->product->prod_plugin_file == 'ADSL') {
# if ($iio->service->name() == strtoupper($iio->domain_name))
# $iio->domain_name=null;
# #print_r(array('pid'=>$iio->domain_name,'iio-service-name'=>$iio->service->name(),'iii-domain_name'=>$iio->domain_name));
#}
}
$this->response->body($output);
} }
} }
?> ?>

View File

@ -11,8 +11,6 @@
* @license http://dev.osbill.net/license.html * @license http://dev.osbill.net/license.html
*/ */
class Model_Invoice extends ORMOSB { class Model_Invoice extends ORMOSB {
private $invoice_items = array();
protected $_belongs_to = array( protected $_belongs_to = array(
'account'=>array() 'account'=>array()
); );
@ -28,20 +26,6 @@ class Model_Invoice extends ORMOSB {
'id'=>'DESC', 'id'=>'DESC',
); );
/**
* @var array Filters to render values properly
*/
protected $_filters = array(
// @todo This rounding should be a global configuration
'total_amt'=>array('round'=>array('2')),
);
protected $_callbacks = array(
'id'=>array('get_next_id'),
'total_amt'=>array('calc_total'),
'tax_amt'=>array('calc_tax'),
);
protected $_display_filters = array( protected $_display_filters = array(
'date_orig'=>array( 'date_orig'=>array(
array('Config::date',array(':value')), array('Config::date',array(':value')),
@ -63,6 +47,9 @@ class Model_Invoice extends ORMOSB {
), ),
); );
// Items belonging to an invoice
private $invoice_items = array();
/** /**
* Display the Invoice Number * Display the Invoice Number
*/ */
@ -82,18 +69,20 @@ class Model_Invoice extends ORMOSB {
*/ */
public function due($format=FALSE) { public function due($format=FALSE) {
// If the invoice is active calculate the due amount // If the invoice is active calculate the due amount
$result = $this->status ? round($this->total_amt-$this->credit_amt-$this->billed_amt,Kohana::config('config.currency_format')) : 0; $result = $this->status ? round($this->total()-$this->payments_total(),2) : 0;
return $format ? Currency::display($result) : $result; return $format ? Currency::display($result) : $result;
} }
/** /**
* Return a list of invoice items for this invoice. * Return a list of invoice items for this invoice.
*
* We only return the items, if the invoice hasnt been changed.
*/ */
public function items() { public function items() {
return ($this->loaded() AND ! $this->_changed) ? $this->invoice_item->order_by('service_id,item_type,module_id')->find_all() : NULL; // If we havent been changed, we'll load the records from the DB.
if ($this->loaded() AND ! $this->_changed)
return $this->invoice_item->order_by('service_id,item_type,module_id')->find_all()->as_array();
else
return $this->invoice_items;
} }
/** /**
@ -102,8 +91,19 @@ class Model_Invoice extends ORMOSB {
public function subtotal($format=FALSE) { public function subtotal($format=FALSE) {
$result = 0; $result = 0;
// @todo This rounding should be a system config.
foreach ($this->items() as $ito) foreach ($this->items() as $ito)
$result += $ito->subtotal(); $result += round($ito->subtotal(),2);
return $format ? Currency::display($result) : $result;
}
public function discount($format=FALSE) {
$result = 0;
// @todo This rounding should be a system config.
foreach ($this->items() as $ito)
$result += round($ito->discount_amt,2);
return $format ? Currency::display($result) : $result; return $format ? Currency::display($result) : $result;
} }
@ -111,12 +111,9 @@ class Model_Invoice extends ORMOSB {
public function tax($format=FALSE) { public function tax($format=FALSE) {
$result = 0; $result = 0;
// @todo This rounding should be a system config.
foreach ($this->items() as $ito) foreach ($this->items() as $ito)
$result += $ito->tax(); $result += round($ito->tax(),2);
// @todo This should eventually be removed.
if (! $result AND $this->tax_amt)
$result = $this->tax_amt;
return $format ? Currency::display($result) : $result; return $format ? Currency::display($result) : $result;
} }
@ -127,11 +124,12 @@ class Model_Invoice extends ORMOSB {
public function total($format=FALSE) { public function total($format=FALSE) {
$result = 0; $result = 0;
// @todo This rounding should be a system config.
foreach ($this->items() as $ito) foreach ($this->items() as $ito)
$result += $ito->total(); $result += round($ito->total(),2);
// Reduce by any credits // Reduce by any credits
$result -= $this->credit_amt; $result -= round($this->credit_amt,2);
return $format ? Currency::display($result) : $result; return $format ? Currency::display($result) : $result;
} }
@ -150,31 +148,75 @@ class Model_Invoice extends ORMOSB {
} }
/** /**
* Return a list of our main invoice items (item_id=0) * Get a list of services on an invoice
*
* We use this to list details by service on an invoice.
*/ */
public function items_main() { public function items_services(array $items=array()) {
return ($this->loaded() AND ! $this->_changed) ? $this->invoice_item->where('item_type','=',0)->find_all() : NULL; $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->service;
return $result;
} }
/** /**
* Return a list of our sub invoice items (item_id!=0) * Return all invoice items for a service optionally by recurring schedule
*
* @param sid int Service ID
*/ */
public function items_sub($sid) { public function items_service($sid,$rs=NULL) {
return ($this->loaded() AND ! $this->_changed) ? $this->invoice_item->where('service_id','=',$sid)->and_where('item_type','<>',0)->find_all() : NULL; $result = array();
$items = $this->items();
Sort::MAsort($items,'item_type');
foreach ($items as $ito)
if ($ito->service_id == $sid AND (is_null($rs) OR $ito->recurring_schedule == $rs))
array_push($result,$ito);
return $result;
}
/**
* 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 * Summarise the items on an invoice
*
* We summaries based on product.
*/ */
public function items_summary() { public function items_summary() {
$result = array(); $result = array();
foreach ($this->items_main() as $ito) { foreach ($this->items() as $ito) {
$unique = TRUE; // We conly summaries item_type=0
if (! $ito->item_type == 0)
continue;
$t = $ito->product_id;
$t = $ito->product->name();
if (! isset($result[$t])) { if (! isset($result[$t])) {
$result[$t]['quantity'] = 0; $result[$t]['quantity'] = 0;
$result[$t]['subtotal'] = 0; $result[$t]['subtotal'] = 0;
@ -187,22 +229,13 @@ class Model_Invoice extends ORMOSB {
return $result; return $result;
} }
/**
* Find all the invoice items relating to a service
*
* @param int Service ID
*/
private function list_items_service($sid) {
return $this->invoice_item->where('service_id','=',$sid)->find_all();
}
/** /**
* Calculate the total for items for a service * Calculate the total for items for a service
*/ */
public function items_service_total($sid) { public function items_service_total($sid) {
$total = 0; $total = 0;
foreach ($this->list_items_service($sid) as $ito) foreach ($this->items_service($sid) as $ito)
$total += $ito->total(); $total += $ito->total();
return $total; return $total;
@ -210,41 +243,26 @@ class Model_Invoice extends ORMOSB {
/** /**
* Calculate the tax of items for a service * Calculate the tax of items for a service
* @todo This can be optimised
*/ */
public function items_service_tax($sid) { public function items_service_tax($sid) {
$total = 0; $total = 0;
foreach ($this->list_items_service($sid) as $ito) foreach ($this->items_service($sid) as $ito)
$total += $ito->tax_amt; $total += $ito->tax();
return $total; return $total;
} }
// @todo Add discounts
/** /**
* Return a list of items based on a sort criteria * Calculate the discounts of items for a service
*/ */
public function sorted_service_items($index) { public function items_service_discount($sid) {
$summary = array(); $total = 0;
foreach ($this->items() as $ito) { foreach ($this->items_service($sid) as $ito)
$key = $ito->service->$index; $total += $ito->discount();
if (! isset($summary[$key]['items'])) { return $total;
$summary[$key]['items'] = array();
$summary[$key]['total'] = 0;
}
// Only record items with item_type=0
if ($ito->item_type == 0)
array_push($summary[$key]['items'],$ito);
$summary[$key]['total'] += $ito->total();
}
return $summary;
} }
/** /**
@ -264,8 +282,8 @@ class Model_Invoice extends ORMOSB {
} }
// @todo This should be removed eventually // @todo This should be removed eventually
if (! $summary AND $this->tax_amt) if (! $summary)
$summary[1] = $this->tax_amt; $summary[1] = $this->tax();
return $summary; return $summary;
} }
@ -274,6 +292,9 @@ class Model_Invoice extends ORMOSB {
* Add an item to an invoice * Add an item to an invoice
*/ */
public function add_item() { 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); $c = count($this->invoice_items);
$this->invoice_items[$c] = ORM::factory('invoice_item'); $this->invoice_items[$c] = ORM::factory('invoice_item');
@ -281,26 +302,31 @@ class Model_Invoice extends ORMOSB {
return $this->invoice_items[$c]; return $this->invoice_items[$c];
} }
public function min_due($date) {
// @todo This should be configurable;
return ($date < time()) ? time() : $date;
}
public function save(Validation $validation = NULL) { 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 // Save the invoice
parent::save($validation); parent::save($validation);
// Need to save the associated items and their taxes // Need to save the associated items and their taxes
if ($this->saved()) { if ($this->saved()) {
$tax_amt = 0; foreach ($items as $iio) {
$discount_amt = 0; $iio->invoice_id = $this->id;
foreach ($this->items() as $invoice_item) { if (! $iio->check()) {
$invoice_item->invoice_id = $this->id;
if (! $invoice_item->check()) {
// @todo Mark invoice as cancelled and write a memo, then... // @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'=>$invoice->id)); throw new Kohana_Exception('Problem saving invoice_item for invoice :invoice - Failed check()',array(':invoice'=>$invoice->id));
} }
$invoice_item->save(); $iio->save();
if (! $invoice_item->saved()) { if (! $iio->saved()) {
// @todo Mark invoice as cancelled and write a memo, then... // @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'=>$invoice->id)); throw new Kohana_Exception('Problem saving invoice_item for invoice :invoice - Failed save()',array(':invoice'=>$invoice->id));
} }
@ -313,28 +339,6 @@ class Model_Invoice extends ORMOSB {
throw new Kohana_Exception('Couldnt save invoice for some reason?'); throw new Kohana_Exception('Couldnt save invoice for some reason?');
} }
public function calc_total(Validate $array, $field) {
$array[$field] = 0;
// @todo Rounding here should come from a global config
foreach ($this->invoice_items as $ito)
$array[$field] += round($ito->price_base+$ito->price_setup,2);
$this->_changed[$field] = $field;
}
public function calc_tax(Validate $array, $field) {
$array[$field] = 0;
// @todo Rounding here should come from a global config
// @todo tax should be evaluated per item
// @todo tax parameters should come from user session
foreach ($this->invoice_items as $ito)
$array[$field] += round(Tax::total(61,NULL,$ito->price_base+$ito->price_setup),2);
$this->_changed[$field] = $field;
}
/** /**
* Check the reminder value * Check the reminder value
*/ */
@ -386,23 +390,30 @@ class Model_Invoice extends ORMOSB {
/** LIST FUNCTIONS **/ /** LIST FUNCTIONS **/
private function _list_due() { private function _list_due() {
// @todo This rounding should be a system configuration static $result = array();
return $this->where('round(total_amt-ifnull(credit_amt,0),2)','>','=billed_amt')
->and_where('status','=',1) if (! $result)
->order_by('due_date,account_id,id'); foreach (ORM::factory('invoice')->where('status','=',1)->find_all() as $io)
if ($io->due())
array_push($result,$io);
return $result;
} }
/** /**
* Identify all the invoices that are due * Identify all the invoices that are due
*/ */
public function list_overdue($time=NULL) { public function list_overdue($time=NULL) {
$result = array();
if (is_null($time)) if (is_null($time))
$time = time(); $time = time();
// @todo This rounding should be a system configuration foreach ($this->_list_due() as $io)
return $this->_list_due() if ($io->due_date <= $time)
->and_where('due_date','<=',$time) array_push($result,$io);
->find_all();
return $result;
} }
/** /**
@ -428,15 +439,16 @@ class Model_Invoice extends ORMOSB {
* Return a list of invoices that are due, excluding overdue. * Return a list of invoices that are due, excluding overdue.
*/ */
public function list_due($time=NULL) { public function list_due($time=NULL) {
$result = array();
foreach ($this->_list_due() as $io)
if ($io->due_date > time())
if (is_null($time)) if (is_null($time))
return $this->_list_due() array_push($result,$io);
->and_where('due_date','>',time()) elseif ($this->due_date <= $time)
->find_all(); array_push($result,$io);
else
return $this->_list_due() return $result;
->and_where('due_date','<=',$time)
->and_where('due_date','>',time())
->find_all();
} }
} }
?> ?>

View File

@ -32,7 +32,6 @@ class Model_Invoice_Item extends ORMOSB {
), ),
); );
// Display a transaction number // Display a transaction number
public function trannum() { public function trannum() {
return sprintf('%03s-%06s',$this->item_type,$this->id); return sprintf('%03s-%06s',$this->item_type,$this->id);
@ -41,7 +40,7 @@ class Model_Invoice_Item extends ORMOSB {
// Display the period that a transaction applies // Display the period that a transaction applies
public function period() { public function period() {
if ($this->date_start == $this->date_stop) if ($this->date_start == $this->date_stop)
return sprintf('%s: %s',_('Date'),Config::date($this->date_start)); return Config::date($this->date_start);
else else
return sprintf('%s -> %s',Config::date($this->date_start),Config::date($this->date_stop)); return sprintf('%s -> %s',Config::date($this->date_start),Config::date($this->date_stop));
@ -49,17 +48,21 @@ class Model_Invoice_Item extends ORMOSB {
// Sum up the tax that applies to this invoice item // Sum up the tax that applies to this invoice item
public function tax() { public function tax() {
$amount = 0; $result = 0;
foreach ($this->invoice_item_tax->find_all() as $iit) foreach ($this->invoice_item_tax->find_all() as $iit)
$amount += $iit->amount; $result += $iit->amount;
return $amount; // @todo This shouldnt be required.
if (! $result)
$result += round($this->subtotal() *.1,2);
return $result;
} }
// This total of this item before discounts and taxes // This total of this item before discounts and taxes
public function subtotal() { public function subtotal() {
return ($this->price_base)*$this->quantity; return $this->price_base*$this->quantity;
} }
// The total of all discounts // The total of all discounts
@ -72,6 +75,15 @@ class Model_Invoice_Item extends ORMOSB {
return round($this->subtotal()+$this->tax()-$this->discount(),2); return round($this->subtotal()+$this->tax()-$this->discount(),2);
} }
public function name() {
return $this->product_name ? $this->product_name : ($this->item_type == 0 ? _('Service') : _('Other'));
}
public function detail() {
return ($this->item_type == 0 OR $this->quantity == 1) ? HTML::nbsp('') : sprintf('%s@%3.2f',$this->quantity,$this->price_base);
}
// @todo This might not be required.
public function invoice_detail_items() { public function invoice_detail_items() {
if ($this->item_type != 0) if ($this->item_type != 0)
return; return;
@ -81,36 +93,29 @@ class Model_Invoice_Item extends ORMOSB {
public function save(Validation $validation = NULL) { public function save(Validation $validation = NULL) {
// Save the invoice item // Save the invoice item
parent::save(); parent::save($validation);
// Need to save the taxes and discounts associated with the invoice_item // Need to save the taxes and discounts associated with the invoice_item
if ($this->saved()) { if ($this->saved()) {
$invoice_item_tax = ORM::factory('invoice_item_tax'); $iito = ORM::factory('invoice_item_tax');
$tax_total = 0;
// Save TAX details // Save TAX details
// @todo tax parameters should come from user session // @todo tax parameters should come from user session
foreach (Tax::detail(61,NULL,$this->subtotal()) as $tax) { foreach (Tax::detail(61,NULL,$this->subtotal()) as $tax) {
$invoice_item_tax->clear(); $iito->clear();
$invoice_item_tax->invoice_item_id = $this->id; $iito->invoice_item_id = $this->id;
$iito->tax_id = $tax['id'];
// @todo Rounding here should come from a global config // @todo Rounding here should come from a global config
$tax_total += ($invoice_item_tax->amount = round($tax['amount'],2)); $iito->amount = round($tax['amount'],2);
$invoice_item_tax->tax_id = $tax['id'];
if (! $invoice_item_tax->check()) if (! $iito->check())
throw new Kohana_Exception('Couldnt save tax for some reason - failed check()?'); throw new Kohana_Exception('Couldnt save tax for some reason - failed check()?');
$invoice_item_tax->save(); $iito->save();
if (! $invoice_item_tax->saved()) if (! $iito->saved())
throw new Kohana_Exception('Couldnt save tax for some reason - failed save()?'); throw new Kohana_Exception('Couldnt save tax for some reason - failed save()?');
} }
// Save DISCOUNT details
// @todo calculate discounts
parent::save();
} else } else
throw new Kohana_Exception('Couldnt save invoice_item for some reason?'); throw new Kohana_Exception('Couldnt save invoice_item for some reason?');
} }

View File

@ -26,11 +26,11 @@
</tr> </tr>
<tr> <tr>
<td>Current Charges Due</td> <td>Current Charges Due</td>
<td class="bold-right"><?php echo $io->display('total_amt'); ?></td> <td class="bold-right"><?php echo $io->total(TRUE); ?></td>
</tr> </tr>
<tr> <tr>
<td>Payments Received to Date</td> <td>Payments Received to Date</td>
<td class="bold-right"><?php echo $io->display('billed_amt'); ?></td> <td class="bold-right"><?php echo $io->payments_total(TRUE); ?></td>
</tr> </tr>
<tr> <tr>
<td>Total Charges Due</td> <td>Total Charges Due</td>
@ -46,94 +46,61 @@
<tr> <tr>
<td class="head" colspan="4">Charges Detail:</td> <td class="head" colspan="4">Charges Detail:</td>
</tr> </tr>
<?php foreach ($io->sorted_service_items('recur_schedule') as $cat => $catitems) { ?> <?php foreach ($io->items_service_periods() as $rs => $items) { ?>
<?php if ($cat) { ?>
<tr> <tr>
<td><div id="toggle_<?php echo $cat; ?>"><?php echo HTML::image($mediapath->uri(array('file'=>'img/toggle-closed.png')),array('alt'=>'+')); ?></div><script type="text/javascript">$("#toggle_<?php echo $cat; ?>").click(function() {$('#detail_toggle_<?php echo $cat; ?>').toggle();});</script></td> <td><div id="toggle_<?php echo $rs; ?>"><?php echo HTML::image($mediapath->uri(array('file'=>'img/toggle-closed.png')),array('alt'=>'+')); ?></div><script type="text/javascript">$("#toggle_<?php echo $rs; ?>").click(function() {$('#detail_toggle_<?php echo $rs; ?>').toggle();});</script></td>
<td><?php echo StaticList_RecurSchedule::display($cat); ?></td> <?php if ($rs) { ?>
<td><?php printf('%s Services',count($catitems['items'])); ?></td> <td><?php echo StaticList_RecurSchedule::display($rs); ?></td>
<td class="bold-right"><?php echo Currency::display($catitems['total']); ?></td> <td colspan="1"><?php printf('%s Service(s)',count($items)); ?></td>
</tr>
<?php } else { ?> <?php } else { ?>
<tr> <td colspan="2">Other Items</td>
<td colspan="3">Other Items</td>
<td class="bold-right"><?php echo Currency::display($catitems['total']); ?></td>
</tr>
<?php } ?> <?php } ?>
<td>&nbsp;</td>
</tr>
<tr> <tr>
<td>&nbsp;</td> <td>&nbsp;</td>
<td colspan="2"> <td colspan="2">
<div id="detail_toggle_<?php echo $cat; ?>"> <div id="detail_toggle_<?php echo $rs; ?>">
<table class="box-full" border="0"> <table class="box-full" border="0">
<?php if ($catitems['items']) { ?> <?php if ($items) { ?>
<?php foreach ($catitems['items'] as $item) { ?> <?php foreach ($io->items_services($items) as $sid) { ?>
<?php $so = ORM::factory('service',$sid); ?>
<!-- Product Information --> <!-- Product Information -->
<tr class="head"> <tr class="head">
<td><?php echo HTML::anchor('/user/service/view/'.$item->service->id,$item->service->id()); ?></td> <td><?php echo HTML::anchor('/user/service/view/'.$so->id,$so->id()); ?></td>
<td colspan="3"><?php echo $item->product_name ? $item->product_name : $item->product->product_translate->find()->name; ?> (<?php echo $item->product_id; ?>)</td> <td colspan="5"><?php echo $so->service_name(); ?> (<?php echo $so->product_id; ?>)</td>
<td class="right"><?php echo Currency::display($io->items_service_total($item->service_id));?></td> <td class="right"><?php echo Currency::display($io->items_service_total($so->id));?></td>
</tr> </tr>
<!-- End Product Information --> <!-- End Product Information -->
<?php foreach ($io->items_service($sid) as $ito) { ?>
<!-- Product Sub Information --> <!-- Product Sub Information -->
<tr> <tr>
<td>&nbsp;</td> <td>&nbsp;</td>
<td><?php echo $item->trannum();?></td> <td><?php echo $ito->trannum();?></td>
<td><?php echo $item->period();?></td> <td><?php echo $ito->name();?></td>
<td class="right"><?php echo Currency::display($item->subtotal());?></td> <td><?php echo $ito->detail();?></td>
<td><?php echo $ito->period();?></td>
<td class="right"><?php echo Currency::display($ito->subtotal());?>&nbsp;</td>
</tr> </tr>
<!-- End Product Sub Information --> <!-- End Product Sub Information -->
<?php } ?>
<!-- Product Sub Items --> <?php if ($ito->discount_amt) { ?>
<?php
foreach ($io->items_sub($item->service_id) as $subitem) {
if (! is_null($subitem->module_id)) {
$m = StaticList_Module::record('module','name','id',$subitem->module_id);
// @todo Need to remove the explicit test for 'charge' and be more dynamic
$mi = ORM::factory($m,$m == 'charge' ? $subitem->charge_id : $subitem->id);
$display = $mi->details('invoice');
} else {
$display = 'Other';
}
?>
<tr> <tr>
<td>&nbsp;</td> <td colspan="4">&nbsp;</td>
<td><?php echo $subitem->trannum(); ?></td> <td><?php echo _('Discounts'); ?></td>
<td><?php echo $display; ?></td> <td class="right">(<?php echo Currency::display($io->items_service_discount($so->id));?>)</td>
<td class="right"><?php echo Currency::display($subitem->subtotal());?></td>
</tr> </tr>
<?php } ?> <?php } ?>
<!-- Product End Sub Items -->
<!-- Product Sub Items Tax --> <!-- Product Sub Items Tax -->
<tr> <tr>
<td colspan="2">&nbsp;</td> <td colspan="4">&nbsp;</td>
<td><?php echo _('Taxes'); ?></td> <td><?php echo _('Taxes'); ?></td>
<td class="right"><?php echo $io->tax(TRUE);?></td> <td class="right"><?php echo Currency::display($io->items_service_tax($so->id));?>&nbsp;</td>
</tr> </tr>
<!-- Product End Sub Items Tax --> <!-- Product End Sub Items Tax -->
<?php } ?> <?php } ?>
<?php } else { ?>
<!-- Product Sub Items -->
<?php
foreach ($io->items_sub(NULL) as $subitem) {
if (! is_null($subitem->module_id)) {
$m = StaticList_Module::record('module','name','id',$subitem->module_id);
// @todo Need to remove the explicit test for 'charge' and be more dynamic
$mi = ORM::factory($m,$m == 'charge' ? $subitem->charge_id : $subitem->id);
$display = $mi->details('invoice');
} else {
$display = 'Other';
}
?>
<tr>
<td>&nbsp;</td>
<td><?php echo $subitem->trannum(); ?></td>
<td><?php echo $display; ?></td>
<td class="right"><?php echo Currency::display($subitem->subtotal());?></td>
</tr>
<?php } ?>
<!-- Product End Sub Items -->
<?php } ?> <?php } ?>
</table> </table>
</div> </div>
@ -141,9 +108,15 @@
</tr> </tr>
<?php } ?> <?php } ?>
<tr> <tr>
<td class="head" colspan="2">Sub Total:</td> <td class="head" colspan="3">Sub Total of Items:</td>
<td class="bold-right"><?php echo $io->subtotal(TRUE); ?></td> <td class="bold-right"><?php echo $io->subtotal(TRUE); ?>&nbsp;</td>
</tr> </tr>
<?php if ($io->discount()) { ?>
<tr>
<td class="head" colspan="3">Discounts:</td>
<td class="bold-right">(<?php echo $io->discount(TRUE); ?>)</td>
</tr>
<?php } ?>
<tr> <tr>
<td class="head" colspan="4">Taxes Included:</td> <td class="head" colspan="4">Taxes Included:</td>
</tr> </tr>
@ -153,14 +126,14 @@
?> ?>
<tr> <tr>
<td>&nbsp;</td> <td>&nbsp;</td>
<td><?php echo $m->description; ?></td> <td colspan="2"><?php echo $m->description; ?></td>
<td class="bold-right"><?php echo Currency::display($amount); ?></td> <td class="bold-right"><?php echo Currency::display($amount); ?>&nbsp;</td>
</tr> </tr>
<?php }?> <?php }?>
<!-- @todo Add discounts --> <!-- @todo Add discounts -->
<tr> <tr>
<td class="head" colspan="3">Total:</td> <td class="head" colspan="3">Total:</td>
<td class="bold-right"><?php echo $io->total(TRUE); ?></td> <td class="bold-right"><?php echo $io->total(TRUE); ?>&nbsp;</td>
</tr> </tr>
</table> </table>
</td> </td>

View File

@ -0,0 +1,46 @@
<?php defined('SYSPATH') or die('No direct access allowed.');
/**
* This class provides Admin Product functions
*
* @package OSB
* @subpackage Product
* @category Controllers/Admin
* @author Deon George
* @copyright (c) 2010 Open Source Billing
* @license http://dev.osbill.net/license.html
*/
class Controller_Admin_Product extends Controller_TemplateDefault_Admin {
protected $secure_actions = array(
'list'=>TRUE,
);
/**
* Show a list of services
*/
public function action_list() {
Block::add(array(
'title'=>_('Customer Products'),
'body'=>Table::display(
ORM::factory('product')->order_by('prod_plugin_file')->find_all(),
25,
array(
'id'=>array('label'=>'ID','url'=>'user/product/view/'),
'name()'=>array('label'=>'Details'),
'active'=>array('label'=>'Active'),
'prod_plugin'=>array('label'=>'Plugin'),
'prod_plugin_file'=>array('label'=>'Plugin Name'),
'prod_plugin_data'=>array('label'=>'Plugin Data'),
'price_type'=>array('label'=>'Price Type'),
'price_base'=>array('label'=>'Price Base'),
'taxable'=>array('label'=>'Taxable'),
),
array(
'page'=>TRUE,
'type'=>'select',
'form'=>'user/product/view',
)),
));
}
}
?>

View File

@ -42,33 +42,6 @@ class Model_Service extends ORMOSB {
), ),
); );
/**
* The service_name should be implemented in child objects.
* It renders the name of the service, typically used on invoice
*/
protected function _service_name() {
throw new Kohana_Exception(':method not defined in child class :class',array(':method'=>__METHOD__,':class'=>get_class($this)));
}
/**
* The service_view should be implemented in child objects.
* It renders the details of the ordered service
*/
protected function _service_view() {
throw new Kohana_Exception(':method not defined in child class :class',array(':method'=>__METHOD__,':class'=>get_class($this)));
}
/**
* The _details should be implemented in child objects.
*/
protected function _details($type) {
throw new Kohana_Exception(':method not defined in child class :class',array(':method'=>__METHOD__,':class'=>get_class($this)));
}
protected function _admin_update() {
throw new Kohana_Exception(':method not defined in child class :class',array(':method'=>__METHOD__,':class'=>get_class($this)));
}
/** /**
* Return the object of the product plugin * Return the object of the product plugin
*/ */
@ -143,6 +116,8 @@ class Model_Service extends ORMOSB {
return $this->price * .1; return $this->price * .1;
} }
/** LIST FUNCTIONS **/
public function list_active() { public function list_active() {
return $this->where('active','=','1')->find_all(); return $this->where('active','=','1')->find_all();
} }
@ -171,7 +146,7 @@ class Model_Service extends ORMOSB {
$result = array(); $result = array();
foreach ($this->list_active() as $so) foreach ($this->list_active() as $so)
// @todo This should be configurable // @todo This should be configurable (days)
if (! $so->suspend_billing AND $so->date_next_invoice < time()+35*86400) if (! $so->suspend_billing AND $so->date_next_invoice < time()+35*86400)
array_push($result,$so); array_push($result,$so);

View File

@ -22,16 +22,17 @@ class Tax {
public static function detail($cid,$zone,$value=0) { public static function detail($cid,$zone,$value=0) {
$tax = ORM::factory('tax') $tax = ORM::factory('tax')
->where('country_id','=',$cid) ->where('country_id','=',$cid)
->and_where('zone','=',$zone); ->and_where('zone','=',$zone)
->find_all();
$taxes = array(); $taxes = array();
foreach ($tax->find_all() as $item) { foreach ($tax as $to) {
$total = array(); $total = array();
$total['id'] = $item->id; $total['id'] = $to->id;
$total['description'] = $item->description; $total['description'] = $to->description;
$total['amount'] = $item->rate*$value; $total['amount'] = $to->rate*$value;
$total['rate'] = $item->rate; $total['rate'] = $to->rate;
array_push($taxes,$total); array_push($taxes,$total);
} }