Open Source Billing
This commit is contained in:
72
modules/invoice/classes/Controller/Admin/Invoice.php
Normal file
72
modules/invoice/classes/Controller/Admin/Invoice.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php defined('SYSPATH') or die('No direct access allowed.');
|
||||
|
||||
/**
|
||||
* This class provides invoice capabilities.
|
||||
*
|
||||
* @package Invoice
|
||||
* @category Controllers/Admin
|
||||
* @author Deon George
|
||||
* @copyright (c) 2009-2013 Open Source Billing
|
||||
* @license http://dev.osbill.net/license.html
|
||||
*
|
||||
* Column Definitions:
|
||||
* + price_type: 0=One Time, 1=Recurring, 2=Trial, 3=Extra Item
|
||||
* + item_type: 0=MAIN Service Item,2=?,3=?,4=Connection/Setup,5=Excess Service Item,6=Change Service,126=Payment Fee,127=Late Fee
|
||||
*/
|
||||
class Controller_Admin_Invoice extends Controller_TemplateDefault_Admin {
|
||||
protected $secure_actions = array(
|
||||
'list'=>TRUE,
|
||||
'setup'=>TRUE,
|
||||
);
|
||||
|
||||
public function action_setup() {
|
||||
$this->setup(array(
|
||||
'EMAIL_INV_MAX'=>_('Email this many invoices in a run (0=no limit)'),
|
||||
'GEN_DAYS'=>_('Generate Invoices this many days in advance of the due date'),
|
||||
'GEN_INV_MAX'=>_('Generate this many invoices in a run (0=no limit)'),
|
||||
'GEN_SOON_DAYS'=>_('Days before GEN_DAYS to list invoices that will be generated'),
|
||||
'DUE_DAYS_MIN'=>_('When invoices are generated, the minimum days in advance the due date should be set to'),
|
||||
'REMIND_DUE'=>_('Days before an invoice due to sent out a reminder'),
|
||||
'REMIND_OVERDUE_1'=>_('Days after an invoice is due to send first reminder'),
|
||||
'REMIND_OVERDUE_2'=>_('Days after an invoice is due to send second reminder'),
|
||||
'REMIND_OVERDUE_3'=>_('Days after an invoice is due to send third and final reminder'),
|
||||
'TAX_ID'=>_('TAX ID shown on invoices'),
|
||||
'TAX_ID_NAME'=>_('TAX ID name shown on invoices'),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a list of invoices
|
||||
*/
|
||||
public function action_list() {
|
||||
$id = $this->request->param('id');
|
||||
|
||||
$invs = ORM::factory('Invoice');
|
||||
|
||||
if ($id)
|
||||
$invs->where('account_id','=',$id);
|
||||
|
||||
Block::add(array(
|
||||
'title'=>_('System Customer Invoices'),
|
||||
'body'=>Table::display(
|
||||
$invs->find_all(),
|
||||
25,
|
||||
array(
|
||||
'id'=>array('label'=>'ID','url'=>URL::link('user','invoice/view/')),
|
||||
'date_orig'=>array('label'=>'Date'),
|
||||
'total(TRUE)'=>array('label'=>'Total','class'=>'right'),
|
||||
'total_credits(TRUE)'=>array('label'=>'Credits','class'=>'right'),
|
||||
'payments_total(TRUE)'=>array('label'=>'Payments','class'=>'right'),
|
||||
'due(TRUE)'=>array('label'=>'Still Due','class'=>'right'),
|
||||
'account->accnum()'=>array('label'=>'Cust ID'),
|
||||
'account->name()'=>array('label'=>'Customer'),
|
||||
),
|
||||
array(
|
||||
'page'=>TRUE,
|
||||
'type'=>'select',
|
||||
'form'=>URL::link('user','invoice/view'),
|
||||
)),
|
||||
));
|
||||
}
|
||||
}
|
||||
?>
|
138
modules/invoice/classes/Controller/Invoice.php
Normal file
138
modules/invoice/classes/Controller/Invoice.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php defined('SYSPATH') or die('No direct access allowed.');
|
||||
|
||||
/**
|
||||
* This class provides invoice capabilities.
|
||||
*
|
||||
* @package Invoice
|
||||
* @category Controllers
|
||||
* @author Deon George
|
||||
* @copyright (c) 2009-2013 Open Source Billing
|
||||
* @license http://dev.osbill.net/license.html
|
||||
*/
|
||||
class Controller_Invoice extends Controller_TemplateDefault {
|
||||
protected $secure_actions = array(
|
||||
'download'=>TRUE,
|
||||
'list'=>TRUE,
|
||||
'view'=>TRUE,
|
||||
);
|
||||
|
||||
/**
|
||||
* Show a list of invoices
|
||||
*/
|
||||
public function action_list() {
|
||||
Block::add(array(
|
||||
'title'=>sprintf('%s: %s - %s',_('Invoices For'),$this->ao->accnum(),$this->ao->name(TRUE)),
|
||||
'body'=>Table::display(
|
||||
$this->ao->invoice->find_all(),
|
||||
25,
|
||||
array(
|
||||
'id'=>array('label'=>'ID','url'=>URL::link('user','invoice/view/')),
|
||||
'date_orig'=>array('label'=>'Date Issued'),
|
||||
'due_date'=>array('label'=>'Date Due'),
|
||||
'total(TRUE)'=>array('label'=>'Total','class'=>'right'),
|
||||
'total_credits(TRUE)'=>array('label'=>'Credits','class'=>'right'),
|
||||
'payments_total(TRUE)'=>array('label'=>'Payments','class'=>'right'),
|
||||
'due(TRUE)'=>array('label'=>'Still Due','class'=>'right'),
|
||||
),
|
||||
array(
|
||||
'page'=>TRUE,
|
||||
'type'=>'select',
|
||||
'form'=>URL::link('user','invoice/view'),
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* View an Invoice
|
||||
*/
|
||||
public function action_view() {
|
||||
list($id,$output) = Table::page(__METHOD__);
|
||||
|
||||
$io = ORM::factory('Invoice',$id);
|
||||
|
||||
if (! $io->loaded() OR ! Auth::instance()->authorised($io->account)) {
|
||||
$this->template->content = 'Unauthorised or doesnt exist?';
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$output .= View::factory($this->viewpath())
|
||||
->set('mediapath',Route::get('default/media'))
|
||||
->set('io',$io);
|
||||
|
||||
if ($io->due() AND ! $io->cart_exists()) {
|
||||
$output .= View::factory($this->viewpath().'/pay')
|
||||
->set('mid',$io->mid())
|
||||
->set('o',$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:250px;
|
||||
top:-20px;
|
||||
}
|
||||
'));
|
||||
|
||||
$output .= '<div id="watermark"><p>Invoice CANCELLED.</p></div>';
|
||||
}
|
||||
|
||||
Block::add(array(
|
||||
'title'=>sprintf('%s: %s - %s',_('Invoice'),$io->refnum(),$io->account->name()),
|
||||
'body'=>$output,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Download an invoice
|
||||
*/
|
||||
public function action_download() {
|
||||
$io = ORM::factory('Invoice',$this->request->param('id'));
|
||||
|
||||
$this->response->body(Invoice::instance($io)->pdf()->Output(sprintf('%s.pdf',$io->refnum()),'D'));
|
||||
$this->response->headers(array('Content-Type' => 'application/pdf'));
|
||||
$this->auto_render = FALSE;
|
||||
}
|
||||
}
|
||||
?>
|
33
modules/invoice/classes/Controller/Reseller/Invoice.php
Normal file
33
modules/invoice/classes/Controller/Reseller/Invoice.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php defined('SYSPATH') or die('No direct access allowed.');
|
||||
|
||||
/**
|
||||
* This class provides Reseller Invoice viewing functions
|
||||
*
|
||||
* @package Invoice
|
||||
* @category Controllers/Reseller
|
||||
* @author Deon George
|
||||
* @copyright (c) 2009-2013 Open Source Billing
|
||||
* @license http://dev.osbill.net/license.html
|
||||
*/
|
||||
class Controller_Reseller_Invoice extends Controller_Invoice {
|
||||
public function action_list() {
|
||||
list($id,$output) = Table::page(__METHOD__);
|
||||
|
||||
$ao = ORM::factory('Account',$id);
|
||||
|
||||
if (! $ao->loaded() OR ! Auth::instance()->authorised($ao)) {
|
||||
$this->template->content = 'Unauthorised or doesnt exist?';
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
Block::add(array(
|
||||
'body'=>$output,
|
||||
));
|
||||
|
||||
$this->ao = $ao;
|
||||
|
||||
// @todo Our pagination is broken if we select multiple accounts, and those accounts have multiple invoices.
|
||||
return parent::action_list();
|
||||
}
|
||||
}
|
||||
?>
|
340
modules/invoice/classes/Controller/Task/Invoice.php
Normal file
340
modules/invoice/classes/Controller/Task/Invoice.php
Normal file
@@ -0,0 +1,340 @@
|
||||
<?php defined('SYSPATH') or die('No direct access allowed.');
|
||||
|
||||
/**
|
||||
* This class provides OSB invoice task capabilities.
|
||||
*
|
||||
* @package Invoice
|
||||
* @category Controllers
|
||||
* @author Deon George
|
||||
* @copyright (c) 2009-2013 Open Source Billing
|
||||
* @license http://dev.osbill.net/license.html
|
||||
*/
|
||||
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() {
|
||||
$mode = $this->request->param('id');
|
||||
|
||||
$i = ORM::factory('Invoice');
|
||||
$tm = 'list_'.$mode;
|
||||
|
||||
if (! method_exists($i,$tm))
|
||||
throw new Kohana_Exception('Unknown Task List command :command',array(':command'=>$mode));
|
||||
|
||||
$total = $numinv = 0;
|
||||
$duelist = View::factory('invoice/task/'.$tm.'_head');
|
||||
foreach ($i->$tm() as $t) {
|
||||
$duelist .= View::factory('invoice/task/'.$tm.'_body')
|
||||
->set('io',$t);
|
||||
|
||||
$numinv++;
|
||||
$total += $t->due();
|
||||
}
|
||||
$duelist .= View::factory('invoice/task/'.$tm.'_foot');
|
||||
|
||||
// Send our email
|
||||
$et = Email_Template::instance('task_invoice_list_overdue');
|
||||
|
||||
// @todo Update this to be dynamic
|
||||
$et->to = array('account'=>array(1,68));
|
||||
$et->variables = array(
|
||||
'TABLE'=>$duelist,
|
||||
'NUM_INV'=>$numinv,
|
||||
'TOTAL'=>$total,
|
||||
);
|
||||
$et->send();
|
||||
|
||||
$output = sprintf('List (%s) sent to: %s',$mode,implode(',',array_keys($et->to)));
|
||||
$this->response->body($output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Email a customers a reminder of their upcoming invoices that are due.
|
||||
*/
|
||||
public function action_remind_due() {
|
||||
$action = array();
|
||||
$key = 'remind_due';
|
||||
$days = ORM::factory('Invoice')->config('REMIND_DUE');
|
||||
|
||||
foreach (ORM::factory('Invoice')->list_due(time()+86400*$days) as $io) {
|
||||
// @todo Use another option to supress reminders
|
||||
// If we have already sent a reminder, we'll skip to the next one.
|
||||
if (($io->remind($key) AND (is_null($x=$this->request->param('id')) OR $x != 'again')) OR ($io->account->invoice_delivery != 1))
|
||||
continue;
|
||||
|
||||
// Send our email
|
||||
$et = Email_Template::instance('task_invoice_'.$key);
|
||||
|
||||
$et->to = array('account'=>array($io->account_id));
|
||||
$et->variables = array(
|
||||
'DUE'=>$io->due(TRUE),
|
||||
'DUE_DATE'=>$io->display('due_date'),
|
||||
'FIRST_NAME'=>$io->account->first_name,
|
||||
'INV_NUM'=>$io->refnum(),
|
||||
'INV_URL'=>URL::site(URL::link('user','invoice/view/'.$io->id),'http'),
|
||||
'SITE_NAME'=>Company::instance()->name(),
|
||||
);
|
||||
|
||||
// @todo Record email log id if possible.
|
||||
if ($et->send()) {
|
||||
$io->set_remind($key,time());
|
||||
array_push($action,(string)$io);
|
||||
}
|
||||
}
|
||||
|
||||
$this->response->body(_('Due Reminders Sent: ').join('|',$action));
|
||||
}
|
||||
|
||||
/**
|
||||
* Email a customers when their invoices are now overdue.
|
||||
*/
|
||||
public function action_remind_overdue() {
|
||||
$action = array();
|
||||
$notice = $this->request->param('id');
|
||||
$x = NULL;
|
||||
|
||||
if (preg_match('/:/',$notice))
|
||||
list($notice,$x) = explode(':',$notice);
|
||||
|
||||
switch ($notice) {
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
$days = ORM::factory('Invoice')->config('REMIND_OVERDUE_'.$notice);
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->response->body(_('Unknown Remind Period: ').$notice);
|
||||
return;
|
||||
}
|
||||
|
||||
$key = 'remind_overdue_'.$notice;
|
||||
|
||||
foreach (ORM::factory('Invoice')->list_overdue_billing(time()-86400*$days,FALSE) as $io) {
|
||||
// @todo Use another option to supress reminders
|
||||
// If we have already sent a reminder, we'll skip to the next one.
|
||||
if (($io->remind($key) AND (is_null($x=$this->request->param('id')) OR $x != 'again')) OR ($io->account->invoice_delivery != 1))
|
||||
continue;
|
||||
|
||||
// Send our email
|
||||
$et = Email_Template::instance('task_invoice_'.$key);
|
||||
|
||||
$et->to = array('account'=>array($io->account_id));
|
||||
$et->variables = array(
|
||||
'DUE'=>$io->due(TRUE),
|
||||
'DUE_DATE'=>$io->display('due_date'),
|
||||
'EMAIL'=>Company::instance()->email(),
|
||||
'FIRST_NAME'=>$io->account->first_name,
|
||||
'INV_NUM'=>$io->refnum(),
|
||||
'INV_URL'=>URL::site(URL::link('user','invoice/view/'.$io->id),'http'),
|
||||
'LATE_FEE'=>'5.50', // @todo This should come from a config file.
|
||||
'PAYMENTS_TABLE'=>$io->account->payment->list_recent_table(),
|
||||
'SITE_NAME'=>Company::instance()->name(),
|
||||
);
|
||||
|
||||
// @todo Record email log id if possible.
|
||||
if ($et->send()) {
|
||||
$io->set_remind($key,time());
|
||||
array_push($action,(string)$io);
|
||||
}
|
||||
}
|
||||
|
||||
$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) - multiple services colon separated
|
||||
*/
|
||||
public function action_services() {
|
||||
// Used to only process X invoices in a row.
|
||||
$max = ($x=Kohana::$config->load('debug')->invoice) ? $x : ORM::factory('Invoice')->config('GEN_INV_MAX');
|
||||
// Our service next billing dates that need to be updated if this is successful.
|
||||
$snd = array();
|
||||
// Our charges that need to be updated if this is successful.
|
||||
$chgs = array();
|
||||
// If we are invoicing a specific service
|
||||
$sid = is_null($this->request->param('id')) ? NULL : explode(':',$this->request->param('id'));
|
||||
// Sort our service by account_id, then we can generate 1 invoice.
|
||||
$svs = ORM::factory('Service')->list_invoicesoon()->as_array();
|
||||
Sort::MAsort($svs,'account_id,date_next_invoice');
|
||||
|
||||
$aid = $due = $io = NULL;
|
||||
$max_count = 0;
|
||||
foreach ($svs as $so) {
|
||||
// If we are generating an invoice for a service, skip to that service.
|
||||
if (! is_null($sid) AND ! in_array($so->id,$sid))
|
||||
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_object($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));
|
||||
}
|
||||
|
||||
// If we have issued the max number of invoices this round, finish.
|
||||
if ($max AND (++$max_count > $max))
|
||||
break;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
$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 = $pdata['prorata'];
|
||||
$iio->item_type = 0; // Service Billing
|
||||
$iio->discount_amt = NULL; // @todo
|
||||
$iio->price_type = $so->product->price_type;
|
||||
$iio->price_base = $so->price();
|
||||
$iio->recurring_schedule = $so->recur_schedule;
|
||||
$iio->date_start = $pdata['start_time'];
|
||||
$iio->date_stop = $pdata['end_time'];
|
||||
|
||||
// Our service next billing date, if this invoice generation is successful.
|
||||
$snd[$so->id] = $pdata['end_time']+86400;
|
||||
|
||||
// Check if there are any charges
|
||||
$c = ORM::factory('Charge')
|
||||
->where('service_id','=',$so->id)
|
||||
->where('status','=',0)
|
||||
->where('sweep_type','=',6); // @todo This needs to be dynamic, not "6"
|
||||
|
||||
foreach ($c->find_all() as $co) {
|
||||
$iio = $io->add_item();
|
||||
$iio->service_id = $co->service_id;
|
||||
$iio->product_id = $co->product_id;
|
||||
$iio->charge_id = $co->id;
|
||||
$iio->quantity = $co->quantity;
|
||||
$iio->item_type = 5; // @todo This probably should not be hard coded as "5".
|
||||
$iio->discount_amt = NULL; // @todo
|
||||
$iio->price_base = $co->amount;
|
||||
$iio->date_start = $co->date_orig;
|
||||
$iio->date_stop = $co->date_orig; // @todo
|
||||
|
||||
// @todo Temp
|
||||
// We'll mark any charges as temporarily processed, although they should be set to status=1 later.
|
||||
$co->status=2;
|
||||
$co->save();
|
||||
array_push($chgs,$co->id);
|
||||
}
|
||||
}
|
||||
|
||||
// Save our invoice.
|
||||
if ($io AND ! $io->saved() AND ! $io->save()) {
|
||||
print_r($io->items());
|
||||
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.
|
||||
// @todo Catch any update errors
|
||||
foreach ($snd as $sid=>$date) {
|
||||
$so = ORM::factory('Service',$sid);
|
||||
$so->date_next_invoice = $date;
|
||||
$so->save();
|
||||
}
|
||||
|
||||
// Update any processed charges as such
|
||||
// @todo Catch any update errors
|
||||
foreach ($chgs as $cid) {
|
||||
$co = ORM::factory('Charge',$cid);
|
||||
$co->status=1;
|
||||
$co->save();
|
||||
}
|
||||
|
||||
$this->response->body(_('Services Invoiced: ').join('|',array_keys($snd)));
|
||||
}
|
||||
|
||||
public function action_send() {
|
||||
// Used to only process X invoices in a row.
|
||||
$max = ORM::factory('Invoice')->config('EMAIL_INV_MAX');
|
||||
|
||||
$action = array();
|
||||
$iid = $this->request->param('id');
|
||||
$x = NULL;
|
||||
|
||||
if (preg_match('/:/',$iid))
|
||||
list($iid,$x) = explode(':',$iid);
|
||||
|
||||
// Get our list of invoices to send
|
||||
$i = $iid ? ORM::factory('Invoice')->where('id','=',$iid) : ORM::factory('Invoice')->list_tosend();
|
||||
|
||||
$key = 'send';
|
||||
|
||||
$max_count = 0;
|
||||
foreach ($i->find_all() as $io) {
|
||||
// If we have already sent a reminder or we dont email invoices we'll skip to the next one.
|
||||
if (($io->remind($key) AND (is_null($x) OR $x != 'again')) OR ($io->account->invoice_delivery != 1))
|
||||
continue;
|
||||
|
||||
// If we have issued the max number of invoices this round, finish.
|
||||
if (++$max_count > $max)
|
||||
break;
|
||||
|
||||
// Send our email
|
||||
$et = Email_Template::instance('task_invoice_'.$key);
|
||||
$token = ORM::factory('Module_Method_Token')
|
||||
->method(array('invoice','user_download'))
|
||||
->account($io->account)
|
||||
->expire(time()+86400*21)
|
||||
->uses(3)
|
||||
->generate();
|
||||
|
||||
$et->to = array('account'=>array($io->account_id));
|
||||
$et->variables = array(
|
||||
'DUE'=>$io->due(TRUE),
|
||||
'DUE_DATE'=>$io->display('due_date'),
|
||||
'EMAIL'=>Company::instance()->email(),
|
||||
'FIRST_NAME'=>$io->account->first_name,
|
||||
'HTML_INVOICE'=>$io->html(),
|
||||
'INV_NUM'=>$io->refnum(),
|
||||
'INV_URL'=>URL::site(URL::link('user','invoice/view/'.$io->id),'http'),
|
||||
'INV_URL_DOWNLOAD'=>URL::site(URL::link('user',sprintf('invoice/download/%s?token=%s',$io->id,$token)),'http'),
|
||||
'SITE_NAME'=>Company::instance()->name(),
|
||||
);
|
||||
|
||||
// @todo Record email log id if possible.
|
||||
if ($et->send()) {
|
||||
$io->print_status = 1;
|
||||
$io->set_remind($key,time(),($x=='again' ? TRUE : FALSE));
|
||||
array_push($action,(string)$io);
|
||||
}
|
||||
}
|
||||
|
||||
$this->response->body(_('Invoices Sent: ').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())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->response->body($output);
|
||||
}
|
||||
}
|
||||
?>
|
14
modules/invoice/classes/Controller/User/Invoice.php
Normal file
14
modules/invoice/classes/Controller/User/Invoice.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php defined('SYSPATH') or die('No direct access allowed.');
|
||||
|
||||
/**
|
||||
* This class provides User Invoice functions
|
||||
*
|
||||
* @package Invoice
|
||||
* @category Controllers/User
|
||||
* @author Deon George
|
||||
* @copyright (c) 2009-2013 Open Source Billing
|
||||
* @license http://dev.osbill.net/license.html
|
||||
*/
|
||||
class Controller_User_Invoice extends Controller_Invoice {
|
||||
}
|
||||
?>
|
144
modules/invoice/classes/Invoice.php
Normal file
144
modules/invoice/classes/Invoice.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php defined('SYSPATH') or die('No direct access allowed.');
|
||||
|
||||
/**
|
||||
* This class provides invoice information
|
||||
*
|
||||
* @package Invoice
|
||||
* @category Helpers
|
||||
* @author Deon George
|
||||
* @copyright (c) 2009-2013 Open Source Billing
|
||||
* @license http://dev.osbill.net/license.html
|
||||
*/
|
||||
class Invoice {
|
||||
// This invoice Object
|
||||
private $io;
|
||||
|
||||
public function __construct($io) {
|
||||
$this->io = $io;
|
||||
}
|
||||
|
||||
public static function instance($io) {
|
||||
return new Invoice($io);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of invoices for an service
|
||||
*
|
||||
* @param $id int Service ID
|
||||
* @param $paid boolean Optionally only list the ones that are not paid.
|
||||
* @return array
|
||||
*/
|
||||
// @todo Function Not Used
|
||||
public static function servicelist($id,$paid=TRUE) {
|
||||
// @todo need to add the db prefix
|
||||
$invoices = DB::Query(Database::SELECT,'
|
||||
SELECT i.id AS iid,i.due_date AS due FROM ab_invoice i,ab_invoice_item ii WHERE ii.invoice_id=i.id AND service_id=:id GROUP BY i.id
|
||||
')
|
||||
->param(':id',$id)
|
||||
->execute();
|
||||
|
||||
$service_invoices = array();
|
||||
foreach ($invoices as $item) {
|
||||
if ($bal = Invoice::balance($item['iid']) OR $paid) {
|
||||
$service_invoices[$item['iid']]['id'] = $item['iid'];
|
||||
$service_invoices[$item['iid']]['total'] = $bal;
|
||||
$service_invoices[$item['iid']]['due'] = $item['due'];
|
||||
}
|
||||
}
|
||||
|
||||
return $service_invoices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the total of amount outstanding for a service
|
||||
*
|
||||
* @param $id int Service ID
|
||||
* @param $paid boolean Optionally only list the ones that are not paid.
|
||||
* @return real Total amount outstanding
|
||||
* @see Invoice::listservice()
|
||||
*/
|
||||
// @todo Function Not Used
|
||||
public static function servicetotal($id,$paid=TRUE) {
|
||||
$total = 0;
|
||||
|
||||
foreach (Invoice::servicelist($id,$paid) as $item)
|
||||
$total += $item['total'];
|
||||
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the earliest due date of an outstanding invoice
|
||||
*
|
||||
* @param $id int Service ID
|
||||
* @return datetime
|
||||
*/
|
||||
// @todo Function Not Used
|
||||
public static function servicedue($id) {
|
||||
$due = 0;
|
||||
|
||||
foreach (Invoice::servicelist($id,FALSE) as $item)
|
||||
if ($due < $item['due'])
|
||||
$due = $item['due'];
|
||||
|
||||
return $due;
|
||||
}
|
||||
|
||||
// @todo Function Not Used
|
||||
public static function balance($id) {
|
||||
return ORM::factory('Invoice',$id)->due();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a PDF invoice
|
||||
*/
|
||||
public function pdf() {
|
||||
$invoice_class = Kohana::classname('Invoice_TCPDF_'.Kohana::$config->load('invoice')->driver);
|
||||
|
||||
$pdf = new $invoice_class($this->io);
|
||||
|
||||
if ($pdf->getTemplate()) {
|
||||
$pagecount = $pdf->setSourceFile($pdf->getTemplate());
|
||||
$tplidx = $pdf->ImportPage(1);
|
||||
}
|
||||
|
||||
$pdf->addPage();
|
||||
|
||||
# If we are using FPDI
|
||||
if (isset($tplidx))
|
||||
$pdf->useTemplate($tplidx);
|
||||
|
||||
$this->draw_summary_invoice($pdf);
|
||||
|
||||
# If we get here, all is OK.
|
||||
return $pdf;
|
||||
}
|
||||
|
||||
private function draw_summary_invoice($pdf) {
|
||||
// Draw Invoice Basics
|
||||
$pdf->drawCompanyLogo();
|
||||
$pdf->drawCompanyAddress();
|
||||
$pdf->drawInvoiceHeader();
|
||||
// @todo Get news from DB
|
||||
$pdf->drawNews('');
|
||||
$pdf->drawRemittenceStub();
|
||||
$pdf->drawPaymentMethods();
|
||||
|
||||
if ($this->io->billing_status !=1 && $this->io->due_date <= time())
|
||||
$pdf->drawInvoiceDueNotice();
|
||||
elseif($this->io->billing_status == 1)
|
||||
$pdf->drawInvoicePaidNotice();
|
||||
|
||||
if ($this->io->account->invoices_due_total())
|
||||
$pdf->drawSummaryInvoicesDue();
|
||||
|
||||
$pdf->drawSummaryLineItems();
|
||||
|
||||
// Next Page
|
||||
$pdf->drawDetailLineItems();
|
||||
|
||||
// Draw any Custom functions:
|
||||
$pdf->drawCustom();
|
||||
}
|
||||
}
|
||||
?>
|
127
modules/invoice/classes/Invoice/Tcpdf.php
Normal file
127
modules/invoice/classes/Invoice/Tcpdf.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php defined('SYSPATH') or die('No direct access allowed.');
|
||||
|
||||
/**
|
||||
* This class provides invoice PDF rendering capability
|
||||
*
|
||||
* @package Invoice
|
||||
* @category Helpers
|
||||
* @author Deon George
|
||||
* @copyright (c) 2009-2013 Open Source Billing
|
||||
* @license http://dev.osbill.net/license.html
|
||||
*/
|
||||
|
||||
define('FPDF_FONTPATH','includes/tcpdf/fonts/');
|
||||
require_once('includes/tcpdf/tcpdf.php');
|
||||
|
||||
abstract class Invoice_TCPDF extends TCPDF {
|
||||
// Our invoice object
|
||||
protected $io;
|
||||
// Our company object
|
||||
protected $co;
|
||||
|
||||
protected $billToCompany = true;
|
||||
protected $itemsSummaryMax = 16;
|
||||
protected $itemsPreviousMax = 5;
|
||||
protected $news = '';
|
||||
protected $pageType = 'blank';
|
||||
protected $show_itemized = true;
|
||||
protected $show_service_range = false;
|
||||
private $invoiceCurrency = '$';
|
||||
private $invoiceDecimals = 2;
|
||||
|
||||
# Store previous invoices due
|
||||
private $itemsPrevious = array();
|
||||
# Stores the invoice items
|
||||
protected $invoice;
|
||||
protected $itemsFull;
|
||||
protected $account;
|
||||
# Iteration of drawing the items on the invoice
|
||||
protected $iteration;
|
||||
# Store the date range, that the invoice covers
|
||||
protected $dateRange;
|
||||
|
||||
public function __construct($io) {
|
||||
parent::__construct();
|
||||
|
||||
$this->io = $io;
|
||||
$this->co = Company::instance();
|
||||
|
||||
// Set up the invoice
|
||||
$this->SetCreator('Open Source Billing');
|
||||
$this->SetAuthor($this->co->name());
|
||||
$this->SetTitle(sprintf('%s Invoice',$this->co->name()));
|
||||
$this->SetSubject(sprintf('Invoice #%06s',$this->io->id()));
|
||||
$this->SetKeywords($this->io->id());
|
||||
$this->SetAutoPageBreak(TRUE,25);
|
||||
$this->SetHeaderMargin(1);
|
||||
$this->SetFooterMargin(10);
|
||||
$this->SetDisplayMode('fullwidth');
|
||||
#$this->setHeaderFont(array('helvetica','',8));
|
||||
$this->setFooterFont(array('helvetica','',8));
|
||||
}
|
||||
|
||||
abstract public function drawCompanyLogo();
|
||||
abstract public function drawCompanyAddress();
|
||||
abstract public function drawInvoiceHeader();
|
||||
abstract public function drawSummaryLineItems();
|
||||
abstract public function drawPaymentMethods();
|
||||
abstract public function drawRemittenceStub();
|
||||
|
||||
public function drawCustom() {}
|
||||
public function drawInvoiceDueNotice() {}
|
||||
public function drawInvoicePaidNotice() {}
|
||||
public function setLateFeeNotice() {}
|
||||
|
||||
/**
|
||||
* Get a PDF invoice template
|
||||
*/
|
||||
public function getTemplate() {}
|
||||
|
||||
/*
|
||||
public function setItemsFull($items) {
|
||||
$this->itemsFull = $items;
|
||||
}
|
||||
|
||||
public function setItemsPrevious($items) {
|
||||
$this->itemsPrevious = $items;
|
||||
}
|
||||
|
||||
public function setDateRange($periodStart,$periodEnd) {
|
||||
$this->dateRange = sprintf('%s - %s',date(UNIX_DATE_FORMAT,$periodStart),date(UNIX_DATE_FORMAT,$periodEnd));
|
||||
}
|
||||
|
||||
public function setCurrency($currency) {
|
||||
$this->invoiceCurrency = $currency;
|
||||
}
|
||||
|
||||
public function setDecimals($decimals) {
|
||||
$this->invoiceDecimals = $decimals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an amount into a currency display
|
||||
*/
|
||||
/*
|
||||
protected function _currency($num) {
|
||||
global $C_list;
|
||||
|
||||
if ($this->invoiceDecimals>3)
|
||||
return $this->invoiceCurrency.number_format($num,$this->invoiceDecimals);
|
||||
else
|
||||
return $C_list->format_currency_num($num,$this->invoice['actual_billed_currency_id']);
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Add a watermark to the PDF
|
||||
*/
|
||||
public function addWaterMark($text) {
|
||||
$this->SetFont('helvetica','B',50);
|
||||
$this->SetTextColor(203,203,203);
|
||||
$this->Rotate(0);
|
||||
$this->Text(10,50,$text);
|
||||
$this->Rotate(0);
|
||||
$this->SetTextColor(0,0,0);
|
||||
}
|
||||
}
|
||||
?>
|
518
modules/invoice/classes/Invoice/Tcpdf/Default.php
Normal file
518
modules/invoice/classes/Invoice/Tcpdf/Default.php
Normal file
@@ -0,0 +1,518 @@
|
||||
<?php defined('SYSPATH') or die('No direct access allowed.');
|
||||
|
||||
/**
|
||||
* This class provides invoice rending use TCPDF
|
||||
*
|
||||
* @package Invoice
|
||||
* @category Helpers
|
||||
* @author Deon George
|
||||
* @copyright (c) 2009-2013 Open Source Billing
|
||||
* @license http://dev.osbill.net/license.html
|
||||
*/
|
||||
class Invoice_TCPDF_Default extends Invoice_Tcpdf {
|
||||
// Current line being printed
|
||||
public $sum_y = 0;
|
||||
|
||||
private $max_lines_page = 51;
|
||||
protected $show_service_range = TRUE;
|
||||
|
||||
/**
|
||||
* Draw the logo
|
||||
*/
|
||||
public function drawCompanyLogo() {
|
||||
$x = 9; $y = 7;
|
||||
$size = 25;
|
||||
$logo = $this->co->logo_file();
|
||||
|
||||
if (is_file($logo))
|
||||
$this->Image($logo,$x,$y,$size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the Company Address
|
||||
*/
|
||||
public function drawCompanyAddress() {
|
||||
// Add the company address next to the logo
|
||||
$x = 40; $y = 7;
|
||||
|
||||
$this->SetFont('helvetica','B',10);
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,$this->co->name()); $y += 4;
|
||||
|
||||
$this->SetFont('helvetica','',10);
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,$this->co->taxid()); $y += 6;
|
||||
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,$this->co->street(', ')); $y += 4;
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,sprintf('%s, %s %s',$this->co->city(),$this->co->state(),$this->co->pcode())); $y += 4;
|
||||
|
||||
$y += 2;
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,'Phone:'); $this->SetXY($x+16,$y); $this->Cell(0,0,$this->co->phone()); $y += 4;
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,'Fax:'); $this->SetXY($x+16,$y); $this->Cell(0,0,$this->co->fax()); $y += 4;
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,'Web:'); $this->SetXY($x+16,$y); $this->addHtmlLink(URL::base(TRUE,TRUE),URL::base(TRUE,TRUE)); $y += 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the remmittence stub
|
||||
*/
|
||||
public function drawRemittenceStub() {
|
||||
// Draw the remittance line
|
||||
$this->Line(9,195,200,195);
|
||||
|
||||
$x = 18; $y = 200;
|
||||
|
||||
$this->SetFont('helvetica','B',13);
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,_('Payment Remittence')); $y +=5;
|
||||
|
||||
$this->SetFont('helvetica','',8);
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,_('Please return this portion with your cheque or money order')); $y +=3;
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,_('made payable to').' '.$this->co->name());
|
||||
|
||||
// Due Date
|
||||
$x = 110; $y = 200;
|
||||
$this->SetFont('helvetica','',10);
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,_('Issue Date'));
|
||||
$this->SetFont('helvetica','B',11);
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,$this->io->display('date_orig'),0,0,'R');
|
||||
|
||||
// Account ID
|
||||
$y = 205;
|
||||
$this->SetFont('helvetica','',10);
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,_('Account Number'));
|
||||
$this->SetFont('helvetica','B',11);
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,$this->io->account->accnum(),0,0,'R');
|
||||
|
||||
// Invoice number
|
||||
$y = 210;
|
||||
$this->SetFont('helvetica','',10);
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,_('Invoice Number'));
|
||||
$this->SetFont('helvetica','B',11);
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,$this->io->id(),0,0,'R');
|
||||
|
||||
// Company Address
|
||||
$y = 216;
|
||||
$this->SetFont('helvetica','',10);
|
||||
$this->SetXY(18,$y); $this->Cell(0,0,$this->co->name()); $y += 4;
|
||||
$this->SetXY(18,$y); $this->Cell(0,0,$this->co->street(', ')); $y += 4;
|
||||
$this->SetXY(18,$y); $this->Cell(0,0,sprintf('%s, %s %s',$this->co->city(),$this->co->state(),$this->co->pcode())); $y += 4;
|
||||
|
||||
// Previous Due
|
||||
$y = 215;
|
||||
$this->SetFont('helvetica','',9);
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,_('Previous Due'));
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,$this->io->account->invoices_due_total($this->io->date_orig,TRUE),0,0,'R');
|
||||
|
||||
$y = 219;
|
||||
$this->SetFont('helvetica','',9);
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,_('Amount Due').' '.$this->io->display('due_date'));
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,$this->io->due(TRUE),0,0,'R');
|
||||
|
||||
// Total Due
|
||||
$y = 224;
|
||||
$this->SetFont('helvetica','B',10);
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,_('Total Payable'));
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,Currency::display($this->io->due() ? $this->io->total()+$this->io->account->invoices_due_total($this->io->date_orig,TRUE) : 0),0,0,'R');
|
||||
|
||||
// Draw the Customers Address
|
||||
$x = 25; $y = 248;
|
||||
|
||||
$this->SetFont('helvetica','B',12);
|
||||
|
||||
if ($this->billToCompany && ! empty($this->io->account->company))
|
||||
$name = $this->io->account->company;
|
||||
else
|
||||
$name = $this->io->account->name();
|
||||
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,html_entity_decode($name,ENT_NOQUOTES)); $y += 5;
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,sprintf('%s %s ',$this->io->account->address1,$this->io->account->address2)); $y += 5;
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,sprintf('%s, %s %s',$this->io->account->city,$this->io->account->state,$this->io->account->zip)); $y += 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the invoice header
|
||||
*/
|
||||
public function drawInvoiceHeader() {
|
||||
$x = 125; $y = 10;
|
||||
|
||||
// Draw a box.
|
||||
$this->SetFillColor(245);
|
||||
$this->SetXY($x-1,$y-1); $this->Cell(0,35+5+($this->io->total_credits() ? 5 : 0),'',1,0,'',1);
|
||||
|
||||
// Draw a box around the invoice due date and amount due.
|
||||
$this->SetFont('helvetica','B',11);
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,'TAX INVOICE');
|
||||
$this->SetFont('helvetica','B',11);
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,$this->io->id(),0,0,'R');
|
||||
|
||||
// Invoice number at top of page.
|
||||
$y += 7;
|
||||
$this->SetFont('helvetica','',10);
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,_('Issue Date')); $y += 5;
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,_('Amount Due'));
|
||||
|
||||
$y -= 5;
|
||||
$this->SetFont('helvetica','B',11);
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,$this->io->display('date_orig'),0,0,'R'); $y += 5;
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,$this->io->display('due_date'),0,0,'R');
|
||||
|
||||
$y += 5;
|
||||
$this->SetFont('helvetica','',10);
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,_('Previous Due'));
|
||||
$this->SetFont('helvetica','B',11);
|
||||
$this->SetXY($x+55,$y); $this->Cell(0,0,$this->io->account->invoices_due_total($this->io->date_orig,TRUE),0,0,'R');
|
||||
|
||||
$y += 5;
|
||||
$this->SetFont('helvetica','',10);
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,_('Current Charges'));
|
||||
$this->SetFont('helvetica','B',11);
|
||||
$this->SetXY($x+55,$y); $this->Cell(0,0,$this->io->total(TRUE),0,0,'R');
|
||||
|
||||
$y += 5;
|
||||
$this->SetFont('helvetica','',10);
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,'Payments Received');
|
||||
$this->SetFont('helvetica','B',11);
|
||||
$this->SetXY($x+55,$y); $this->Cell(0,0,$this->io->payments_total('TRUE'),0,0,'R');
|
||||
|
||||
if ($this->io->total_credits()) {
|
||||
$y += 5;
|
||||
$this->SetFont('helvetica','',10);
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,'Credits Received');
|
||||
$this->SetFont('helvetica','B',11);
|
||||
$this->SetXY($x+55,$y); $this->Cell(0,0,$this->io->total_credits(TRUE),0,0,'R');
|
||||
}
|
||||
|
||||
$y += 5;
|
||||
$this->SetFont('helvetica','',10);
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,'Total Payable');
|
||||
$this->SetFont('helvetica','B',11);
|
||||
$this->SetXY($x+55,$y); $this->Cell(0,0,Currency::display($this->io->due() ? $this->io->total()+$this->io->account->invoices_due_total($this->io->date_orig) : 0),0,0,'R');
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw any news messages
|
||||
* @todo Limit the size of the news to 6 lines
|
||||
*/
|
||||
public function drawNews($news) {
|
||||
if (! $news)
|
||||
return;
|
||||
|
||||
$x = 9; $y = 170;
|
||||
|
||||
# Draw a box.
|
||||
$this->SetFillColor(243);
|
||||
$this->SetXY($x-1,$y-1); $this->Cell(0,20,'',1,0,'',1);
|
||||
|
||||
$this->SetFont('helvetica','',8);
|
||||
$this->SetXY($x,$y); $this->MultiCell(0,3,str_replace('\n',"\n",$news),0,'L',0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw our available payment methods
|
||||
* @todo make this list dynamic
|
||||
*/
|
||||
public function drawPaymentMethods() {
|
||||
$x = 120; $y = 242;
|
||||
|
||||
# Draw a box.
|
||||
$this->SetFillColor(235);
|
||||
$this->SetXY($x-1,$y-2); $this->Cell(0,32,'',1,0,'',1);
|
||||
|
||||
$this->SetFont('helvetica','B',8);
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,'This invoice can be paid by:'); $y += 4;
|
||||
|
||||
# Direct Credit
|
||||
$logo = Kohana::find_file('media','img/invoice-payment-dd','png');
|
||||
$this->Image($logo,$x+1,$y,8);
|
||||
$this->SetFont('helvetica','B',8);
|
||||
$this->SetXY($x+10,$y); $this->Cell(0,0,'Direct Credit to our Bank Account'); $y += 3;
|
||||
$this->SetFont('helvetica','',8);
|
||||
$this->SetXY($x+10,$y); $this->Cell(0,0,'BSB:'); $y += 3;
|
||||
$this->SetXY($x+10,$y); $this->Cell(0,0,'ACCOUNT:'); $y += 3;
|
||||
$this->SetXY($x+10,$y); $this->Cell(0,0,'REF:'); $y += 3;
|
||||
|
||||
$y -= 9;
|
||||
$this->SetFont('helvetica','B',8);
|
||||
$this->SetXY($x+30,$y); $this->Cell(0,0,Company::bsb()); $y += 3;
|
||||
$this->SetXY($x+30,$y); $this->Cell(0,0,Company::account()); $y += 3;
|
||||
$this->SetXY($x+30,$y); $this->Cell(0,0,$this->io->refnum()); $y += 3;
|
||||
|
||||
/*
|
||||
# Direct Debit
|
||||
$y += 3;
|
||||
$logo = sprintf('%s/%s',PATH_THEMES.DEFAULT_THEME,'invoice/invoice-payment-dd.png');
|
||||
$this->Image($logo,$x+1,$y,8);
|
||||
$this->SetFont('helvetica','B',8);
|
||||
$this->SetXY($x+10,$y); $this->Cell(0,0,'Direct Debit'); $y += 3;
|
||||
$this->SetFont('helvetica','',8);
|
||||
$this->SetXY($x+10,$y); $this->Cell(0,0,'Please visit '); $this->SetXY($x+30,$y); $this->addHtmlLink($inv->print['site']['URL'].'?_page=invoice:user_view&id='.$inv->getPrintInvoiceNum(),$inv->print['site']['URL']); $y += 3;
|
||||
*/
|
||||
|
||||
# Paypal
|
||||
$y += 3;
|
||||
$logo = Kohana::find_file('media','img/invoice-payment-pp','png');
|
||||
$this->Image($logo,$x+1,$y,8);
|
||||
$this->SetFont('helvetica','B',8);
|
||||
$this->SetXY($x+10,$y); $this->Cell(0,0,'Pay Pal/Credit Card'); $y += 3;
|
||||
$this->SetFont('helvetica','',8);
|
||||
$this->SetXY($x+10,$y); $this->Cell(0,0,'Please visit '); $this->SetXY($x+30,$y); $this->addHtmlLink(URL::base(TRUE,TRUE),URL::base(TRUE,TRUE)); $y += 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw previous invoices due
|
||||
*/
|
||||
public function drawSummaryInvoicesDue() {
|
||||
$x = 125; $y = $this->sum_y ? $this->sum_y : 50;
|
||||
|
||||
$items = $this->io->account->invoices_due($this->io->date_orig);
|
||||
|
||||
# Calculate the box size
|
||||
$box = count($items) < $this->itemsPreviousMax ? count($items) : $this->itemsPreviousMax;
|
||||
|
||||
# Draw a box.
|
||||
$this->SetFillColor(245);
|
||||
$this->SetXY($x-1,$y-1); $this->Cell(0,5*(1+$box)+1,'',1,0,'',1);
|
||||
|
||||
$this->SetFont('helvetica','B',11);
|
||||
$this->SetXY($x,$y); $this->Cell(0,0,_('Previous Invoices Due')); $y += 5;
|
||||
|
||||
$this->SetFont('helvetica','',11);
|
||||
$i = 0;
|
||||
$sum_total = 0;
|
||||
foreach ($items as $line) {
|
||||
if (++$i < $this->itemsPreviousMax) {
|
||||
$this->SetXY($x,$y);
|
||||
$this->Cell(0,0,sprintf('%s %s',$line->display('date_orig'),$line->id()));
|
||||
$this->Cell(0,0,$line->due(TRUE),0,0,'R'); $y += 5;
|
||||
|
||||
} else {
|
||||
$sum_total += $line->due();
|
||||
}
|
||||
}
|
||||
|
||||
if ($sum_total) {
|
||||
$this->SetXY($x,$y);
|
||||
$this->SetFont('helvetica','I',11);
|
||||
$this->Cell(0,0,'Other invoices');
|
||||
$this->SetFont('helvetica','',11);
|
||||
$this->Cell(0,0,Currency::display($sum_total),0,0,'R'); $y += 5;
|
||||
}
|
||||
|
||||
$this->sum_y = $y+5;
|
||||
}
|
||||
|
||||
/**
|
||||
* This will draw the Summary Box, with the summary of the items
|
||||
* on the invoice.
|
||||
*/
|
||||
public function drawSummaryLineItems() {
|
||||
if (! $this->show_itemized)
|
||||
return;
|
||||
|
||||
$items = $this->io->items_summary();
|
||||
|
||||
// Calculate the box size
|
||||
$box = count($items) < $this->itemsSummaryMax ? count($items) : $this->itemsSummaryMax;
|
||||
|
||||
// Our starting position
|
||||
$x = 10; $y = $this->sum_y ? $this->sum_y : 55;
|
||||
|
||||
// Draw a box.
|
||||
$this->SetFillColor(245);
|
||||
$this->SetXY($x-1,$y-1);
|
||||
$this->Cell(0,5*(
|
||||
1+1+1+3+($this->io->total_discounts() ? 1 : 0)+1+($this->io->total_credits() ? 1 : 0)+$box
|
||||
)+1+4,'',1,0,'',1);
|
||||
|
||||
$this->SetFont('helvetica','B',11);
|
||||
$this->SetXY($x,$y);
|
||||
$this->Cell(0,0,_('Current Charges Summary')); $y += 5;
|
||||
|
||||
$this->SetY($y);
|
||||
$this->SetFont('helvetica','',9);
|
||||
|
||||
$i = $subtotal = 0;
|
||||
foreach ($items as $name => $line) {
|
||||
if ($i < $this->itemsSummaryMax) {
|
||||
$this->SetX($x);
|
||||
$this->Cell(0,0,sprintf('%3.2f',$line['quantity']));
|
||||
$this->SetX($x+8);
|
||||
$this->Cell(0,0,$name);
|
||||
$this->SetX($x+135);
|
||||
$this->Cell(0,0,Currency::display($line['subtotal']),0,0,'R');
|
||||
|
||||
$y += 5;
|
||||
$this->SetY($y);
|
||||
}
|
||||
|
||||
$i++;
|
||||
if ($i == $this->itemsSummaryMax) {
|
||||
$this->SetFont('helvetica','B',11);
|
||||
$this->SetX($x);
|
||||
$this->Cell(0,0,_('The above is just a summary. To view a detailed list of charges, please visit our website.'));
|
||||
}
|
||||
|
||||
$subtotal += $line['subtotal'];
|
||||
}
|
||||
|
||||
// Calculate our rounding error
|
||||
// @todo This shouldnt be required.
|
||||
#$subtotal = Currency::round($subtotal-$this->io->total_discounts());
|
||||
|
||||
if (Currency::round($this->io->subtotal()) != $subtotal) {
|
||||
$this->SetFont('helvetica','',9);
|
||||
$this->SetX($x);
|
||||
$this->Cell(0,0,'Other');
|
||||
$this->SetX($x+135);
|
||||
$this->Cell(0,0,Currency::display($this->io->subtotal()-$subtotal),0,0,'R');
|
||||
|
||||
$y += 5;
|
||||
$this->SetY($y);
|
||||
}
|
||||
|
||||
// Draw Discounts.
|
||||
if ($this->io->total_discounts()) {
|
||||
$y += 5;
|
||||
$this->SetY($y);
|
||||
|
||||
$this->SetFont('helvetica','B',9);
|
||||
$this->SetX($x+8);
|
||||
$this->Cell(0,0,_('Discount'));
|
||||
$this->SetX($x+135);
|
||||
$this->Cell(0,0,Currency::display(-$this->io->total_discounts()),0,0,'R');
|
||||
}
|
||||
|
||||
// Subtotal and tax.
|
||||
$y += 5;
|
||||
$this->SetY($y);
|
||||
|
||||
$this->SetFont('helvetica','B',9);
|
||||
$this->SetX($x+8);
|
||||
$this->Cell(0,0,'Sub Total');
|
||||
$this->SetX($x+135);
|
||||
$this->Cell(0,0,Currency::display($this->io->subtotal()),0,0,'R');
|
||||
|
||||
$y += 5;
|
||||
$this->SetY($y);
|
||||
|
||||
$this->SetX($x+8);
|
||||
$this->Cell(0,0,'Taxes');
|
||||
$this->SetX($x+135);
|
||||
$this->Cell(0,0,Currency::display($this->io->tax()),0,0,'R');
|
||||
|
||||
$y += 5;
|
||||
$this->SetY($y);
|
||||
|
||||
$this->SetX($x+8);
|
||||
$this->Cell(0,0,'Total Charges This Invoice');
|
||||
$this->SetX($x+135);
|
||||
$this->Cell(0,0,Currency::display($this->io->total()),0,0,'R');
|
||||
|
||||
// Show payments already received for this invoice
|
||||
$y += 5;
|
||||
$this->SetY($y);
|
||||
|
||||
$this->SetX($x+8);
|
||||
$this->Cell(0,0,'Payments Received');
|
||||
$this->SetX($x+135);
|
||||
$this->Cell(0,0,$this->io->payments_total(TRUE),0,0,'R');
|
||||
|
||||
if ($this->io->total_credits()) {
|
||||
$y += 5;
|
||||
$this->SetY($y);
|
||||
|
||||
$this->SetFont('helvetica','B',9);
|
||||
$this->SetX($x+8);
|
||||
$this->Cell(0,0,_('Less Credits'));
|
||||
$this->SetX($x+135);
|
||||
$this->Cell(0,0,Currency::display(-$this->io->total_credits()),0,0,'R');
|
||||
}
|
||||
|
||||
$y += 5;
|
||||
$this->SetY($y);
|
||||
$this->SetX($x+8);
|
||||
$this->Cell(0,0,'Balance Due');
|
||||
$this->SetX($x+135);
|
||||
$this->Cell(0,0,$this->io->due(TRUE),0,0,'R');
|
||||
}
|
||||
|
||||
/**
|
||||
* This will draw the Summary Box, with the summary of the items
|
||||
* on the invoice.
|
||||
*/
|
||||
public function drawDetailLineItems() {
|
||||
$this->i = 0;
|
||||
foreach ($this->io->items() as $io)
|
||||
$this->drawLineItem($io);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws Invoice Detail Item
|
||||
*
|
||||
* @todo need to make sure that this pages well, when there are many items (with many sub details).
|
||||
*/
|
||||
private function drawLineItem($ito) {
|
||||
$x = 10;
|
||||
if ($this->i == 0 || $this->i%$this->max_lines_page == 0) {
|
||||
$this->y = 5;
|
||||
$this->AddPage();
|
||||
|
||||
$this->SetFont('helvetica','B',12);
|
||||
$this->SetXY($x,$this->y); $this->Cell(0,0,_('Itemised Charges'));
|
||||
$this->Cell(0,0,_('Page #').$this->PageNo(),0,0,'R');
|
||||
$this->SetXY($x,$this->y); $this->Cell(0,0,_('Invoice #').$this->io->id(),0,0,'C');
|
||||
|
||||
// Draw table headers
|
||||
$this->y += 10;
|
||||
$this->SetFont('helvetica','B',8);
|
||||
$this->SetXY($x,$this->y);
|
||||
$this->Cell(0,0,_('Description'));
|
||||
$this->SetX($x+135);
|
||||
$this->Cell(0,0,_('Quantity'));
|
||||
$this->SetX($x+160);
|
||||
$this->Cell(10,0,_('Unit Cost'),0,0,'R');
|
||||
$this->SetX($x+135);
|
||||
$this->Cell(0,0,_('Amount'),0,0,'R');
|
||||
$this->Line($x,$this->y+4,200,$this->y+4);
|
||||
|
||||
$this->y += 5;
|
||||
$this->SetY($this->y);
|
||||
}
|
||||
|
||||
$this->SetFont('helvetica','',8);
|
||||
$this->SetX($x);
|
||||
$this->Cell(0,0,sprintf('%s - %s',$ito->product->name(),$ito->service->name()));
|
||||
|
||||
if ($ito->price_base) {
|
||||
$this->SetX($x+160);
|
||||
$this->Cell(10,0,Currency::display($ito->price_base),0,0,'R');
|
||||
}
|
||||
|
||||
if ($ito->quantity) {
|
||||
$this->SetX($x+130);
|
||||
$this->Cell(10,0,$ito->quantity,0,0,'R');
|
||||
}
|
||||
|
||||
$this->SetX($x+130);
|
||||
$this->Cell(0,0,Currency::display($ito->total()),0,0,'R');
|
||||
|
||||
if ($this->show_service_range && $ito->period()) {
|
||||
$this->SetFont('helvetica','I',7);
|
||||
$this->y += 3;
|
||||
$this->SetXY($x+10,$this->y); $this->Cell(0,0,'Service Period');
|
||||
$this->SetFont('helvetica','',7);
|
||||
$this->SetXY($x+40,$this->y); $this->Cell(0,0,$ito->period());
|
||||
}
|
||||
|
||||
if ($ito->invoice_detail_items())
|
||||
foreach ($ito->invoice_detail_items() as $k=>$v) {
|
||||
$this->SetFont('helvetica','I',7);
|
||||
$this->y += 3;
|
||||
$this->SetXY($x+10,$this->y); $this->Cell(0,0,$k);
|
||||
$this->SetFont('helvetica','',7);
|
||||
$this->SetXY($x+40,$this->y); $this->Cell(0,0,$v);
|
||||
}
|
||||
|
||||
$this->y += 5;
|
||||
$this->SetY($this->y);
|
||||
$this->i++;
|
||||
}
|
||||
}
|
||||
?>
|
649
modules/invoice/classes/Model/Invoice.php
Normal file
649
modules/invoice/classes/Model/Invoice.php
Normal file
@@ -0,0 +1,649 @@
|
||||
<?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::display',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->name();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
?>
|
204
modules/invoice/classes/Model/Invoice/Item.php
Normal file
204
modules/invoice/classes/Model/Invoice/Item.php
Normal file
@@ -0,0 +1,204 @@
|
||||
<?php defined('SYSPATH') or die('No direct access allowed.');
|
||||
|
||||
/**
|
||||
* This class provides invoice item 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_Item extends ORM_OSB {
|
||||
// Relationships
|
||||
protected $_belongs_to = array(
|
||||
'product'=>array(),
|
||||
'invoice'=>array(),
|
||||
'service'=>array()
|
||||
);
|
||||
protected $_has_one = array(
|
||||
'charge'=>array('far_key'=>'charge_id','foreign_key'=>'id')
|
||||
);
|
||||
protected $_has_many = array(
|
||||
'invoice_item_tax'=>array('far_key'=>'id')
|
||||
);
|
||||
|
||||
protected $_display_filters = array(
|
||||
'date_orig'=>array(
|
||||
array('Config::date',array(':value')),
|
||||
),
|
||||
'date_start'=>array(
|
||||
array('Config::date',array(':value')),
|
||||
),
|
||||
'date_stop'=>array(
|
||||
array('Config::date',array(':value')),
|
||||
),
|
||||
);
|
||||
|
||||
// Items belonging to an invoice
|
||||
private $subitems = array();
|
||||
private $subitems_loaded = 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_loaded AND $this->loaded()) {
|
||||
$this->subitems['tax'] = $this->invoice_item_tax->find_all()->as_array();
|
||||
$this->subitems_loaded = TRUE;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Display a transaction number
|
||||
public function trannum() {
|
||||
return sprintf('%03s-%06s',$this->item_type,$this->id);
|
||||
}
|
||||
|
||||
// Display the period that a transaction applies
|
||||
public function period() {
|
||||
if ($this->date_start == $this->date_stop)
|
||||
return Config::date($this->date_start);
|
||||
|
||||
else
|
||||
return sprintf('%s -> %s',Config::date($this->date_start),Config::date($this->date_stop));
|
||||
}
|
||||
|
||||
// Sum up the tax that applies to this invoice item
|
||||
public function tax() {
|
||||
$result = 0;
|
||||
|
||||
foreach ($this->invoice_item_tax->find_all() as $iit)
|
||||
$result += $iit->amount;
|
||||
|
||||
// @todo This shouldnt be required.
|
||||
if (! $result)
|
||||
$result += round($this->price_base*$this->quantity*.1,2);
|
||||
|
||||
return Currency::round($result);
|
||||
}
|
||||
|
||||
// This total of this item before discounts and taxes
|
||||
public function subtotal() {
|
||||
return Currency::round($this->price_base*$this->quantity);
|
||||
}
|
||||
|
||||
// The total of all discounts
|
||||
public function discount() {
|
||||
return Currency::round($this->discount_amt);
|
||||
}
|
||||
|
||||
public function total() {
|
||||
return Currency::round($this->subtotal()+$this->tax()-$this->discount());
|
||||
}
|
||||
|
||||
/**
|
||||
* Name for an invoice item
|
||||
*/
|
||||
public function name() {
|
||||
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() {
|
||||
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 125: return _('Payment Fee');
|
||||
|
||||
case 126: return _('Rounding');
|
||||
|
||||
case 127: return _('Late Payment Fee');
|
||||
|
||||
default: '';
|
||||
}
|
||||
}
|
||||
|
||||
public function 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) {
|
||||
if (! $this->changed())
|
||||
return;
|
||||
|
||||
// Save the invoice item
|
||||
parent::save($validation);
|
||||
|
||||
// Need to save the discounts associated with the invoice_item
|
||||
if ($this->saved()) {
|
||||
$iito = ORM::factory('Invoice_Item_Tax');
|
||||
|
||||
if ($this->subitems_loaded) {
|
||||
foreach (array('tax') as $i)
|
||||
foreach ($this->subitems[$i] as $io)
|
||||
if ($io->changed())
|
||||
$io->save();
|
||||
|
||||
// Add TAX details
|
||||
} else
|
||||
// @todo tax parameters should come from user session
|
||||
foreach (Tax::detail(61,NULL,$this->subtotal()) as $tax) {
|
||||
$iito->clear();
|
||||
$iito->invoice_item_id = $this->id;
|
||||
$iito->tax_id = $tax['id'];
|
||||
// @todo Rounding here should come from a global config
|
||||
$iito->amount = round($tax['amount'],2);
|
||||
|
||||
if (! $iito->check())
|
||||
throw new Kohana_Exception('Couldnt save tax for some reason - failed check()?');
|
||||
|
||||
$iito->save();
|
||||
|
||||
if (! $iito->saved())
|
||||
throw new Kohana_Exception('Couldnt save tax for some reason - failed save()?');
|
||||
}
|
||||
} else
|
||||
throw new Kohana_Exception('Couldnt save invoice_item for some reason?');
|
||||
}
|
||||
}
|
||||
?>
|
32
modules/invoice/classes/Task/Invoice/Complete.php
Normal file
32
modules/invoice/classes/Task/Invoice/Complete.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php defined('SYSPATH') or die('No direct access allowed.');
|
||||
|
||||
/**
|
||||
* Mark all invoices that have been paid as complete, so that they are not processed again.
|
||||
*
|
||||
* @package Invoice
|
||||
* @category Tasks
|
||||
* @author Deon George
|
||||
* @copyright (c) 2009-2013 Open Source Billing
|
||||
* @license http://dev.osbill.net/license.html
|
||||
*/
|
||||
class Task_Invoice_Complete extends Task {
|
||||
protected function _execute(array $params) {
|
||||
$c = 0;
|
||||
|
||||
$o = ORM::factory('Invoice')
|
||||
->where_unprocessed();
|
||||
|
||||
foreach ($o->find_all() as $io) {
|
||||
if ($io->due() == 0)
|
||||
$io->process_status = 1;
|
||||
|
||||
$io->save();
|
||||
|
||||
if ($io->saved())
|
||||
$c++;
|
||||
}
|
||||
|
||||
printf('%s invoices updated',$c);
|
||||
}
|
||||
}
|
||||
?>
|
Reference in New Issue
Block a user