Open Source Billing

This commit is contained in:
Deon George
2013-10-10 13:44:53 +11:00
commit b02d70adf0
2344 changed files with 392978 additions and 0 deletions

View 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'),
)),
));
}
}
?>

View 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;
}
}
?>

View 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();
}
}
?>

View 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);
}
}
?>

View 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 {
}
?>

View 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();
}
}
?>

View 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);
}
}
?>

View 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++;
}
}
?>

View 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;
}
}
?>

View 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?');
}
}
?>

View 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);
}
}
?>