From f0c1f8800e7fc2de8507b5cbb5845d444814874e Mon Sep 17 00:00:00 2001 From: Deon George Date: Mon, 9 Jan 2012 12:35:24 +1100 Subject: [PATCH] Work on HTML invoice and internal logic --- application/classes/company.php | 4 + application/classes/currency.php | 6 +- application/classes/lnapp/systemmessage.php | 2 +- application/classes/ormosb.php | 2 +- .../classes/controller/user/invoice.php | 53 +++++ modules/invoice/classes/model/invoice.php | 136 +++++++++---- .../invoice/classes/model/invoice/item.php | 73 +++++-- modules/invoice/views/invoice/user/view.php | 191 ++++++++++++------ modules/payment/classes/model/payment.php | 2 +- .../classes/controller/admin/service.php | 31 +++ modules/service/classes/model/service.php | 5 + modules/service/views/service/admin/view.php | 41 ++++ 12 files changed, 431 insertions(+), 115 deletions(-) create mode 100644 modules/service/views/service/admin/view.php diff --git a/application/classes/company.php b/application/classes/company.php index 1188832a..8a7a5fae 100644 --- a/application/classes/company.php +++ b/application/classes/company.php @@ -50,6 +50,10 @@ class Company { return Config::instance()->so->site_details('fax'); } + public static function email() { + return Config::instance()->so->site_details('email'); + } + public static function contacts() { return 'Tel: '.static::phone(); } diff --git a/application/classes/currency.php b/application/classes/currency.php index 0850b702..a3a9153f 100644 --- a/application/classes/currency.php +++ b/application/classes/currency.php @@ -12,8 +12,12 @@ class Currency { public static function display($amount) { // @todo $cid and therefore precision should come from a global session value. + return Num::format(Currency::round($amount),2,TRUE); + } + + public static function round($amount) { // @todo This rounding needs to be system configurable. - return Num::format(round($amount,2),2,TRUE); + return Num::round($amount,2); } } ?> diff --git a/application/classes/lnapp/systemmessage.php b/application/classes/lnapp/systemmessage.php index 237bf01c..909f7744 100644 --- a/application/classes/lnapp/systemmessage.php +++ b/application/classes/lnapp/systemmessage.php @@ -69,7 +69,7 @@ class lnApp_SystemMessage extends HTMLRender { /** * Render an image for the System Message */ - private static function image($type,$raw=false,$big=false,$alt='') { + public static function image($type,$raw=false,$big=false,$alt='') { $mediapath = Route::get(static::$_media_path); switch ($type) { diff --git a/application/classes/ormosb.php b/application/classes/ormosb.php index 6c8149a2..e1a4097d 100644 --- a/application/classes/ormosb.php +++ b/application/classes/ormosb.php @@ -129,7 +129,7 @@ abstract class ORMOSB extends ORM { if ($this->_changed) foreach ($this->_changed as $c) if ($this->_table_columns[$c]['data_type'] == 'blob') { - $this->$c = $this->blob($this->$c,TRUE); + $this->_object[$c] = $this->blob($this->_object[$c],TRUE); // We need to reset our auto_convert flag if (isset($this->_table_columns[$c]['auto_convert'])) diff --git a/modules/invoice/classes/controller/user/invoice.php b/modules/invoice/classes/controller/user/invoice.php index 30c6c479..d7c71ef8 100644 --- a/modules/invoice/classes/controller/user/invoice.php +++ b/modules/invoice/classes/controller/user/invoice.php @@ -53,6 +53,7 @@ class Controller_User_Invoice extends Controller_TemplateDefault_User { if (! $io->loaded() OR (! Auth::instance()->authorised($io->account_id,$io->affiliate_id) AND ! in_array($this->ao->affiliate->id,$io->service_affiliates()))) { $this->template->content = 'Unauthorised or doesnt exist?'; + return FALSE; } @@ -60,6 +61,58 @@ class Controller_User_Invoice extends Controller_TemplateDefault_User { ->set('mediapath',Route::get('default/media')) ->set('io',$io); + if (! $io->status) { + // Add a gribber popup + // @todo Make a gribber popup a class on its own. + Style::add(array( + 'type'=>'file', + 'data'=>'css/jquery.gritter.css', + 'media'=>'screen', + )); + Script::add(array( + 'type'=>'file', + 'data'=>'js/jquery.gritter-1.5.js', + )); + Script::add(array( + 'type'=>'stdin', + 'data'=>sprintf( +'$(document).ready(function() { + $.extend($.gritter.options, { + fade_in_speed: "medium", + fade_out_speed: 2000, + time: "3000", + sticky: false, + }); + $.gritter.add({ + title: "%s", + text: "%s", + image: "%s", +});});', + 'Cancelled','Invoice CANCELLED',URL::site().SystemMessage::image('info',true) + ) + )); + + Style::add(array( + 'type'=>'stdin', + 'data'=>' +#watermark { + color: #800000; + font-size: 4em; + -webkit-transform: rotate(-45deg); + -moz-transform: rotate(-45deg); + position: absolute; + width: 100%; + height: 100%; + margin: 0; + z-index: 1; + left:350px; + top:-200px; +} + ')); + + $output .= '

Invoice CANCELLED.

'; + } + Block::add(array( 'title'=>sprintf('%s: %s - %s',_('Invoice'),$io->refnum(),$io->account->name()), 'body'=>$output, diff --git a/modules/invoice/classes/model/invoice.php b/modules/invoice/classes/model/invoice.php index eba515e0..4469667b 100644 --- a/modules/invoice/classes/model/invoice.php +++ b/modules/invoice/classes/model/invoice.php @@ -49,6 +49,33 @@ class Model_Invoice extends ORMOSB { // Items belonging to an invoice private $invoice_items = array(); + private $subitems_load = FALSE; + + public function __construct($id = NULL) { + // Load our model. + parent::__construct($id); + + return $this->load_sub_items(); + } + + private function load_sub_items() { + // Load our sub items + if (! $this->subitems_load AND $this->loaded()) { + $this->invoice_items = $this->invoice_item->find_all()->as_array(); + $this->subitems_load = TRUE; + } + + return $this; + } + + /** + * Return a list of invoice items for this payment. + */ + public function items() { + $this->load_sub_items(); + + return $this->invoice_items; + } /** * Display the Invoice Number @@ -69,20 +96,13 @@ class Model_Invoice extends ORMOSB { */ public function due($format=FALSE) { // If the invoice is active calculate the due amount - $result = $this->status ? round($this->total()-$this->payments_total(),2) : 0; + $result = $this->status ? $this->total()-$this->payments_total() : 0; - return $format ? Currency::display($result) : $result; - } + // @todo This should not be required. + if ((Currency::round($result) == .01) or Currency::round($result) == .02) + $result = 0; - /** - * Return a list of invoice items for this invoice. - */ - public function items() { - // 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; + return $format ? Currency::display($result) : Currency::round($result); } /** @@ -91,31 +111,28 @@ class Model_Invoice extends ORMOSB { public function subtotal($format=FALSE) { $result = 0; - // @todo This rounding should be a system config. foreach ($this->items() as $ito) - $result += round($ito->subtotal(),2); + $result += $ito->subtotal(); - return $format ? Currency::display($result) : $result; + return $format ? Currency::display($result) : Currency::round($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); + $result += $ito->discount_amt; - return $format ? Currency::display($result) : $result; + return $format ? Currency::display($result) : Currency::round($result); } public function tax($format=FALSE) { $result = 0; - // @todo This rounding should be a system config. foreach ($this->items() as $ito) - $result += round($ito->tax(),2); + $result += $ito->tax(); - return $format ? Currency::display($result) : $result; + return $format ? Currency::display($result) : Currency::round($result); } /** @@ -124,18 +141,17 @@ class Model_Invoice extends ORMOSB { public function total($format=FALSE) { $result = 0; - // @todo This rounding should be a system config. foreach ($this->items() as $ito) - $result += round($ito->total(),2); + $result += $ito->total(); // Reduce by any credits - $result -= round($this->credit_amt,2); + $result -= $this->credit_amt; - return $format ? Currency::display($result) : $result; + return $format ? Currency::display($result) : Currency::round($result); } public function payments() { - return ($this->loaded() AND ! $this->_changed) ? $this->payment_item->find_all() : array(); + return $this->payment_item->find_all(); } public function payments_total($format=FALSE) { @@ -144,7 +160,44 @@ class Model_Invoice extends ORMOSB { foreach ($this->payments() as $po) $result += $po->alloc_amt; - return $format ? Currency::display($result) : $result; + return $format ? Currency::display($result) : Currency::round($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(); } /** @@ -152,6 +205,7 @@ class Model_Invoice extends ORMOSB { * * 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) @@ -164,21 +218,33 @@ class Model_Invoice extends ORMOSB { return $result; } - /** - * Return all invoice items for a service optionally by recurring schedule - */ - public function items_service($sid,$rs=NULL) { +// @todo to retire + public function items_invoice() { $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); + 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 * @@ -207,6 +273,7 @@ class Model_Invoice extends ORMOSB { * * We summaries based on product. */ +// @todo public function items_summary() { $result = array(); @@ -303,6 +370,7 @@ class Model_Invoice extends ORMOSB { } public function min_due($date) { + // @todo This should be a DB confirm item return ($date < time()) ? time()+Kohana::config('config.invoice.min_due_days')*86400 : $date; } diff --git a/modules/invoice/classes/model/invoice/item.php b/modules/invoice/classes/model/invoice/item.php index 19b68f25..9b6ff5eb 100644 --- a/modules/invoice/classes/model/invoice/item.php +++ b/modules/invoice/classes/model/invoice/item.php @@ -63,41 +63,83 @@ class Model_Invoice_Item extends ORMOSB { if (! $result) $result += round($this->subtotal() *.1,2); - return $result; + return Currency::round($result); } // This total of this item before discounts and taxes public function subtotal() { - return $this->price_base*$this->quantity; + return Currency::round($this->price_base*$this->quantity); } // The total of all discounts public function discount() { - return $this->discount_amt; + return Currency::round($this->discount_amt); } public function total() { - // @todo This rounding should be a system config - return round($this->subtotal()+$this->tax()-$this->discount(),2); + return Currency::round($this->subtotal()+$this->tax()-$this->discount()); } + /** + * Name for an invoice item + */ public function name() { - return $this->product_name ? $this->product_name : ($this->item_type == 0 ? _('Service') : _('Other')); + switch ($this->item_type) { + case 0: return _('Service'); + + case 1: return _('Item'); + + case 2: + case 3: + case 4: + case 6: + case 126: + case 127: return _('Charge'); + + case 5: return $this->charge->description; + + default: return _('Other'); + } } + /** + * Detail behind an invoice item + */ public function detail() { - return ($this->item_type == 0 OR $this->quantity == 1) ? HTML::nbsp('') : sprintf('%s@%3.2f',$this->quantity,$this->price_base); + switch ($this->item_type) { + case 0: return ''; + + case 1: return _('Hardware'); + + case 2: return _('Service Relocation Fee'); + + case 3: return _('Service Change Fee'); + + case 4: return _('Service Connection Fee'); + + case 5: return sprintf('%s@%3.2f',$this->quantity,$this->price_base); + + case 6: return _('Service Excess Fee'); + + case 126: return _('Rounding'); + + case 127: return _('Late Payment Fee'); + + default: ''; + } } public function invoice_detail_items() { - // @todo To fix up this function - needs to be better formed. - if ($this->item_type == 5) - return $this->charge->details('invoice_detail_items'); - - if ($this->item_type != 0) - return; - - return $this->service->details('invoice_detail_items'); + switch ($this->item_type) { + case 0: + return $this->service->details('invoice_detail_items'); + case 4: + return array('Charge'=>_('Service Connection Fee')); + case 5: + return $this->charge->details('invoice_detail_items'); + default: + return array('Item'=>$this->item_type); + } } public function save(Validation $validation = NULL) { @@ -107,6 +149,7 @@ class Model_Invoice_Item extends ORMOSB { // Need to save the taxes and discounts associated with the invoice_item // @todo This needs to only check if the records have previously been saved, and update them. if ($this->saved()) { +//@todo When updating a record, we shouldnt create a new tax item. $iito = ORM::factory('invoice_item_tax'); // Save TAX details diff --git a/modules/invoice/views/invoice/user/view.php b/modules/invoice/views/invoice/user/view.php index cb828d9e..82f46c69 100644 --- a/modules/invoice/views/invoice/user/view.php +++ b/modules/invoice/views/invoice/user/view.php @@ -5,7 +5,14 @@ - +


+
+
+
+
+
+ +
@@ -42,102 +49,162 @@   - +
- items_service_periods() as $rs => $items) { ?> + items_index('period') as $rs => $items) { ?> - - - - + + + + + - - - - - discount()) { ?> - - - - + items_index('account')) { ?> + + + + + + + + + + + + + + + + + credit_amt) { ?> + + + + + + + + discount()) { ?> + + + + + + + + - tax_summary() as $tid => $amount) { - $m = ORM::factory('tax',$tid); - ?> + tax_summary() as $tid => $amount) { + $m = ORM::factory('tax',$tid); ?> - - + + - + + - - + + + + - - + + +
Charges Detail:
uri(array('file'=>'img/toggle-closed.png')),array('alt'=>'+')); ?>
Other Items  Other Items
 
- - items_services($items) as $sid => $ito) { ?> - - - - - - - - - items_service($sid) as $ito) { ?> - - - - - - - - - - + $service_id) { + $i = 0; + $lp = NULL; + $ito_tax = NULL; + foreach ($io->items_service($service_id) as $ito) { + $ito_tax = $ito; + // Our first line we show the Service Details + if ($ito->item_type == 0 AND $ito->product_id != $lp) { + $lp = $ito->product_id; ?> + + + + + + + + + + + + + + + + + + + + discount_amt) { ?> + + + + + + + - - discount_amt) { ?> - - - - - + + + + + + + + - - - - - - -
service_id,$ito->service->id()); ?>product->name(),$ito->service->name()); ?> (product_id; ?>)items_service_total($ito->service_id));?>
 trannum();?>name();?>detail();?>period();?>subtotal());?> 
service_id,$ito->service->id()); ?>product->name(),$ito->service->name()); ?> (product_id; ?>)items_service_total($ito->service_id)) : ' ');?>
 trannum();?>name();?>detail();?>period();?>subtotal());?> 
 (items_service_discount($ito->service_id));?>)
 (items_service_discount($ito->service_id));?>)
 items_service_tax($ito->service_id));?> 
 items_service_tax($ito->service_id));?> 
 
Sub Total of Items:subtotal(TRUE); ?> 
Discounts:(discount(TRUE); ?>)
uri(array('file'=>'img/toggle-closed.png')),array('alt'=>'+')); ?>Other Invoice Items 
  +
+ + items_index('account') as $id => $ito) { ?> + + + + + + + + + + + + + + + + +
 trannum();?>name();?>detail();?>period();?>subtotal());?> 
 items_service_tax($ito->service_id));?> 
+
+
 
Sub Total of Items:subtotal(TRUE); ?>
Credits Received:display('credit_amt'); ?>
Discounts:(discount(TRUE); ?>)
Taxes Included:
 description; ?> description; ?>
Total This Invoice:total(TRUE); ?> Total This Invoice:total(TRUE); ?>
Total Outstanding This Account:account->invoices_due_total(NULL,TRUE); ?> Total Outstanding This Account:account->invoices_due_total(NULL,TRUE); ?>
diff --git a/modules/payment/classes/model/payment.php b/modules/payment/classes/model/payment.php index d9c99a3f..219bc7d2 100644 --- a/modules/payment/classes/model/payment.php +++ b/modules/payment/classes/model/payment.php @@ -167,7 +167,7 @@ class Model_Payment extends ORMOSB { if ($pio->changed()) { $old_pio = ORM::factory('payment_item',$pio->id); - if ($it = $pio->invoice->due()+ORM::factory('payment_item',$pio->id)->alloc_amt-$pio->alloc_amt < 0) + if (($it = $pio->invoice->due()+ORM::factory('payment_item',$pio->id)->alloc_amt-$pio->alloc_amt) < 0) $msg .= ($msg ? ' ' : '').sprintf('Invoice %s over allocated by %3.2f.',$pio->invoice_id,$it); } diff --git a/modules/service/classes/controller/admin/service.php b/modules/service/classes/controller/admin/service.php index abe810d9..5b38faff 100644 --- a/modules/service/classes/controller/admin/service.php +++ b/modules/service/classes/controller/admin/service.php @@ -24,6 +24,7 @@ class Controller_Admin_Service extends Controller_TemplateDefault_Admin { 'listwebservices'=>TRUE, 'listinvoicesoon'=>TRUE, 'update'=>TRUE, + 'view'=>TRUE, ); public function action_autolist() { @@ -591,5 +592,35 @@ class Controller_Admin_Service extends Controller_TemplateDefault_Admin { 'data'=>'css/dhtml.calendar.css', )); } + + public function action_view() { + list($id,$output) = Table::page(__METHOD__); + + $so = ORM::factory('service',$id); + + if (! $so->loaded() OR ! Auth::instance()->authorised($so->account_id,$so->affiliate_id)) { + $this->template->content = 'Unauthorised or doesnt exist?'; + return FALSE; + } + + $loutput = ''; + + $loutput .= View::factory($this->viewpath()) + ->set('so',$so); + + Block::add(array( + 'title'=>sprintf('Transaction History for %s: %s',$so->id(),$so->name()), + 'body'=>$loutput, + )); + + $output .= View::factory('service/user/view') + ->set('so',$so); + + Block::add(array( + 'title'=>sprintf('%s: %s',$so->id(),$so->service_name()), + 'body'=>$output, + )); + + } } ?> diff --git a/modules/service/classes/model/service.php b/modules/service/classes/model/service.php index c382eaef..3514d3e7 100644 --- a/modules/service/classes/model/service.php +++ b/modules/service/classes/model/service.php @@ -20,6 +20,7 @@ class Model_Service extends ORMOSB { 'service_billing'=>array('far_key'=>'account_billing_id','foreign_key'=>'id'), ); protected $_has_many = array( + 'invoice_item'=>array('far_key'=>'id'), 'invoice'=>array('through'=>'invoice_item'), ); protected $_belongs_to = array( @@ -118,6 +119,10 @@ class Model_Service extends ORMOSB { return $plugin->admin_update(); } + public function transactions() { + return $this->invoice_item->order_by('date_start,date_stop')->find_all(); + } + /** LIST FUNCTIONS **/ private function _list_active() { diff --git a/modules/service/views/service/admin/view.php b/modules/service/views/service/admin/view.php new file mode 100644 index 00000000..2c540a05 --- /dev/null +++ b/modules/service/views/service/admin/view.php @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + transactions() as $iio) { ?> + product_id) AND $lp != $iio->product_id) { + $lp = $iio->product_id; ?> + + + + + + + + + + + + + + + + + +
Transactions for this service
 
IDInvoiceProductITRSStartStopDescChargeQuantity
product_id,$iio->product->name()); ?>
id; ?>invoice_id,$iio->invoice_id); ?>display('product_id'); ?>display('item_type'); ?>display('recurring_schedule'); ?>display('date_start'); ?>display('date_stop'); ?>display('product_name'); ?>display('price_base'); ?>display('quantity'); ?>