* @copyright 2009 Deon George * @link http://osb.leenooks.net * * @link http://www.agileco.com/ * @copyright 2004-2008 Agileco, LLC. * @license http://www.agileco.com/agilebill/license1-4.txt * @author Tony Landis * @package AgileBill * @subpackage Modules:Invoice */ /** * The main AgileBill Invoice Class * * @package AgileBill * @subpackage Modules:Invoice */ class invoice extends OSB_module { # Hold the invoice items associated with this invoice private $items = array(); # Array holding all our print information public $print = array(); # Enable summary invoice view that rolls multiple instances of the same sku w/identical base&setup price & attributes into one line item private $summarizeInvoice = true; /** * Delete an invoice * * @uses service */ public function delete($VAR) { $db = &DB(); # Get the array if (isset($VAR['delete_id'])) $ids = explode(',',preg_replace('/,$/','',$VAR['delete_id'])); elseif (isset($VAR['id'])) $ids = explode(',',preg_replace('/,$/','',$VAR['id'])); # Load the service module include_once(PATH_MODULES.'service/service.inc.php'); $so = new service; foreach ($ids as $id) { # Loop through all services for this invoice and delete: $rs = $db->Execute(sqlSelect('service',array('where'=>array('invoice_id'=>$id)))); if (! $rs) { global $C_debug; $C_debug->error(__FILE__,__METHOD__,$db->ErrorMsg()); return false; } if ($rs->RecordCount()) { while (! $rs->EOF) { $so->delete($rs->fields['id']); $rs->MoveNext(); } } # Delete the service record $this->associated_DELETE = array(); array_push($this->associated_DELETE,array('table'=>'invoice_commission','field'=>'invoice_id')); array_push($this->associated_DELETE,array('table'=>'invoice_item','field'=>'invoice_id')); array_push($this->associated_DELETE,array('table'=>'invoice_memo','field'=>'invoice_id')); // array_push($this->associated_DELETE,array('table'=>'service','field'=>'invoice_id')); array_push($this->associated_DELETE,array('table'=>'invoice_item_tax','field'=>'invoice_id')); array_push($this->associated_DELETE,array('table'=>'invoice_item_discount','field'=>'invoice_id')); $result = parent::delete($VAR); } } /** * View an Invoice * Shown both in the admin pages and after checkout * * @uses net_term */ public function view($VAR) { global $C_translate,$C_list; $db = &DB(); if ($smart = parent::view($VAR)) { # Get the product checkout plugin name if (! empty($smart['checkout_plugin_id'])) { $rs = $db->Execute(sqlSelect('checkout','name',array('where'=>array('id'=>$smart['checkout_plugin_id'])))); if ($rs && $rs->RecordCount()) $smart['checkout_plugin'] = $rs->fields['name']; } $smart['balance'] = ($smart['total_amt'] == 0) ? 0 : $smart['total_amt']-$smart['billed_amt']-$smart['credit_amt']; # Get the tax details if (! empty($smart['tax_amt'])) { $rs = $db->Execute(sqlSelect($db,array('invoice_item_tax','tax'),'A.amount,B.description',sprintf('A.tax_id=B.id AND A.invoice_id=%s',$smart['id']))); if ($rs && $rs->RecordCount()) { $taxes = array(); while (! $rs->EOF) { @$taxes[$rs->fields['description']] += $rs->fields['amount']; $rs->MoveNext(); } $smart['tax_arr'] = array(); foreach ($taxes as $txds => $txamt) array_push($smart['tax_arr'],array('description'=>$txds,'amount'=>$txamt)); } } # Get the discount details if (! empty($smart['discount_amt'])) { $rs = $db->Execute(sqlSelect('invoice_item_discount','amount,discount',array('where'=>array('invoice_id'=>$smart['id'])))); if ($rs && $rs->RecordCount()) { $discounts = array(); while (! $rs->EOF) { @$discounts[$rs->fields['discount']] += $rs->fields["amount"]; $rs->MoveNext(); } $dhtml = ''; foreach ($discounts as $dsds => $dsamt) $dhtml .= sprintf('%s -
',$dsds,$dsds,number_format($dsamt,2)); $smart['discount_popup'] = $dhtml; $dhtml = ''; foreach ($discounts as $dsds=>$dsamt) $dhtml .= sprintf('%s - %s
',$dsds,number_format($dsamt,2)); $smart['discount_popup_user'] = $dhtml; } } # Get the checkout plugin details if (! empty($smart['checkout_plugin_data'])) { $plugin_data = unserialize($smart['checkout_plugin_data']); if (is_array($plugin_data)) $smart['checkout_plugin_data'] = $plugin_data; else $smart['checkout_plugin_data'] = array($smart['checkout_plugin_data']); } # Get the term dates include_once(PATH_MODULES.'net_term/net_term.inc.php'); $net_term = new net_term; $smart['termdates'] = $net_term->getTermDates($smart['net_term_id'],$smart['date_orig'],$smart['due_date']); # Get the line items $rs = $db->Execute(sqlSelect('invoice_item','*',array('where'=>array('invoice_id'=>$smart['id'])))); if (! $rs) { global $C_debug; $C_debug->error(__FILE__,__METHOD__,$db->ErrorMsg()); return false; } $ii=0; while (! $rs->EOF) { $smart_items[$ii] = $rs->fields; # Get the product attribs if (! empty($rs->fields['product_attr'])) { @$attrib = explode("\r\n",$rs->fields['product_attr']); $js = ''; for ($attr_i=0; $attr_i%s : %s
',$attributei[0],$attributei[1]); } $smart_items[$ii]['attribute_popup'] = $js; } # Get the date range if set if (! empty($rs->fields['date_start']) && ! empty($rs->fields['date_stop'])) { $C_translate->value('invoice','start',date(UNIX_DATE_FORMAT,$rs->fields['date_start'])); $C_translate->value('invoice','stop',date(UNIX_DATE_FORMAT,$rs->fields['date_stop'])); $smart_items[$ii]['range'] = $C_translate->translate('recur_date_range','invoice',''); } # Set charge type for payment option list $any_new = true; if ($rs->fields['price_type']=='1' && ! empty($smart['recurr_arr']) && is_array(unserialize($smart['recurr_arr']))) $any_recurring = true; $rs->MoveNext(); $ii++; } # Create a summary (for duplicate skus w/identical price,and attributes, roll into a single value if ($this->summarizeInvoice) $smart_items = $this->sInvoiceItemsSummary(); # Get the checkout (payment) options if ($VAR['_page'] != 'invoice:view') { # Get the converted amount due: if ($smart['billed_currency_id'] != $smart['actual_billed_currency_id']) { global $C_list; $CURRENCY = $smart['actual_billed_currency_id']; if ($smart['billed_amt'] <= 0) $total = $C_list->format_currency_decimal($smart['total_amt'],$CURRENCY); else $total = $C_list->format_currency_decimal($smart['total_amt'],$CURRENCY)-$smart['actual_billed_amt']; } else { $CURRENCY = $smart['billed_currency_id']; $total = $smart['total_amt']-$smart['billed_amt']; } $q = sqlSelect('checkout','*',array('where'=>array('active'=>'1'))); if ($any_trial) $q .= ' AND allow_trial=1'; if ($any_recurring) $q .= ' AND allow_recurring=1'; if ($any_new) $q .= ' AND allow_new=1'; $rs = $db->Execute($q); if (! $rs) { global $C_debug; $C_debug->error(__FILE__,__METHOD__,$db->ErrorMsg()); return false; } if ($rs->RecordCount()) { while (! $rs->EOF) { $show = true; # Check that the cart total is not to high: if ($rs->fields['total_maximum'] != '' && $smart['total_amt'] >= $rs->fields['total_maximum']) $show = false; # Check that the cart total is not to low: if ($rs->fields['total_miniumum'] != '' && $smart['total_amt'] <= $rs->fields['total_miniumum']) $show = false; # Check that the group requirement is met: if ($show && ! empty($rs->fields['required_groups'])) { global $C_auth; $arr = unserialize($rs->fields['required_groups']); if (count($arr) > 0 && ! empty($arr[0])) $show = false; for ($i=0; $iauth_group_by_id($arr)) { $show = true; $i = count($arr); } } } # Check that the customer is not ordering a blocked SKU if ($show && ! empty($rs->fields['excluded_products'])) { $arr = unserialize($rs->fields['excluded_products']); if (count($arr) > 0) { for ($i=0; $ifields['default_when_amount'])) { $arr = unserialize($rs->fields['default_when_amount']); for ($idx=0; $idx= $arr[$idx]) $list_ord--; $idx = count($arr); } } # By Currency if (! empty($rs->fields['default_when_currency'])) { $arr = unserialize($rs->fields['default_when_currency']); for ($idx=0; $idxfields['default_when_group'])) { $arr = unserialize($rs->fields['default_when_group']); global $C_auth; for ($idx=0; $idxauth_group_by_id($arr[$idx])) $list_ord--; $idx = count($arr); } } # By Country if (! empty($rs->fields['default_when_country'])) { $arr = unserialize($rs->fields['default_when_country']); for ($idx=0; $idxfields['country_id'] == $arr[$idx]) $list_ord--; $idx = count($arr); } } # Add to the array $checkout_optionsx[] = array('sort'=>$list_ord,'fields'=>$rs->fields); } $rs->MoveNext(); } # Sort the checkout_options array by the [fields] element if (count($checkout_optionsx)>0) { foreach ($checkout_optionsx as $key => $row) $sort[$key] = $row['sort']; array_multisort($sort,SORT_ASC,$checkout_optionsx); } } } # Get the payment details if ($C_list->is_installed('payment')) { require_once(PATH_MODULES.'payment/payment.inc.php'); require_once(PATH_MODULES.'payment_item/payment_item.inc.php'); $pii = new payment_item(); $i=0; foreach ($pii->sql_GetRecords(array('where'=>array('invoice_id'=>$VAR['id']),'orderby'=>'date_last,invoice_id')) as $payment) { if ($payment['alloc_amt']) { $pi = new payment($payment['payment_id']); $smart['payment_data'][$i]['payment_id'] = $payment['payment_id']; $smart['payment_data'][$i]['date_payment'] = $pi->getRecordAttr('date_payment'); $smart['payment_data'][$i]['total'] = $pi->getRecordAttr('total_amt'); $smart['payment_data'][$i]['alloc'] = $payment['alloc_amt']; $i++; } } } # No results if (count($smart) == 0) { global $C_debug; $C_debug->error(__FILE__, __METHOD__,'The selected record does not exist any longer, or your account is not authorized to view it'); return; } # Define the DB vars as a Smarty accessible block global $smarty; # Define the results $smarty->assign('cart',$smart_items); $smarty->assign('record',$smart); $smarty->assign('checkoutoptions',$checkout_optionsx); } } public function user_search_show($VAR) { global $smarty; $smart = parent::user_search_show($VAR); # Add the balance to the array foreach ($smart as $index => $details) $smart[$index]['balance'] = $details['total_amt']-$details['billed_amt']-$details['credit_amt']; $smarty->assign('search_show',$smart); } /** * User view an invoice */ public function user_view($VAR) { global $C_auth; if (! SESS_LOGGED) return false; # Verify the account_id for this order is the SESS_ACCOUNT if ($C_auth->auth_method_by_name('invoice','view') == false) { $invoices = $this->sql_GetRecords(array('where'=>array('id'=>$VAR['id']))); if (! count($invoices) || $invoices[0]['account_id'] != SESS_ACCOUNT) return false; } $this->view($VAR); } /** * Get the balance of the account */ public function sPreviousBalance() { static $CACHE = array(); $id = $this->getRecordAttr('id'); if (! isset($CACHE[$id])) { $CACHE[$id] = 0; foreach ($this->sPreviousInvoices() as $item) $CACHE[$id] += round($item['total_amt']-$item['billed_amt']-$item['credit_amt'],2); } return $CACHE[$id]; } /** * Get all the previous invoices still unpaid */ private function sPreviousInvoices() { static $CACHE = array(); $id = $this->getRecordAttr('id'); if (! isset($CACHE[$id])) { $CACHE[$id] = $this->sql_GetRecords( array('where'=>sprintf('account_id=%s AND status=1 AND (billed_amt+IFNULL(credit_amt,0)getRecordAttr('account_id'),$this->getRecordAttr('due_date'),$id))); } return $CACHE[$id]; } /** * Get the items on an invoice * * @uses invoice_item */ private function sInvoiceItems() { static $CACHE = array(); $id = $this->getRecordAttr('id'); if (! isset($CACHE[$id])) { include_once(PATH_MODULES.'invoice_item/invoice_item.inc.php'); $ito = new invoice_item(); $CACHE[$id] = $ito->sInvoiceItems($this->getRecordAttr('id')); } return $CACHE[$id]; } /** * Create modified array for invoice summarization * * This function will summarise the invoice items based on the same * SKU, BASE_PRICE, SETUP_PRICE & PRODUCT_ATTR */ public function sInvoiceItemsSummary() { $sum = array(); foreach ($this->sInvoiceItems() as $index => $item) { $unique = true; # Unique line item if (isset($sum[$item['sku']])) { # Is unique price/attributes? foreach ($sum[$item['sku']] as $sid => $flds) { if ($flds['price_base'] == $item['price_base'] && $flds['price_setup'] == $item['price_setup']) { $sum[$item['sku']][$sid]['quantity'] += $item['quantity']; $unique = false; break; } } } # Unique line item if ($unique) { $a = count($sum[$item['sku']]); $sum[$item['sku']][$a] = $item; $sum[$item['sku']][$a]['product_name'] = $this->sLineItemDesc($index,true); } } if (count($sum)) { $items = array(); foreach ($sum as $sku => $item) foreach ($item as $sitem) array_push($items,$sitem); return $items; } } /** * Return a line description, suitable for printing on invoices * * @uses product_translate */ private function sLineItemDesc($id,$summary=false) { $li = $this->sInvoiceItems(); if (! isset($li[$id])) return 'Other Item'; require_once(PATH_MODULES.'product_translate/product_translate.inc.php'); $pdo = new product_translate($li[$id]['product_id']); if ($summary) { if (is_null($li[$id]['sku']) && is_null($li[$id]['product_id'])) return _('Other Item'); elseif (is_null($li[$id]['product_id'])) { switch ($li[$id]['sku']) { case 'DOMAIN-REGISTER': $name = _('Register Domain'); break; case 'DOMAIN-TRANSFER': $name = _('Transfer Domain'); break; case 'DOMAIN-PARK': $name = _('Park Domain'); break; case 'DOMAIN-RENEW': $name = _('Renew Domain'); break; default: $name = $sku; } return $name; } elseif ($li[$id]['product_id']) return $pdo->getRecordAttr('name'); } else { switch ($li[$id]['sku']) { case 'DOMAIN-REGISTER': $name = _('Register Domain'); break; case 'DOMAIN-TRANSFER': $name = _('Transfer Domain'); break; case 'DOMAIN-PARK': $name = _('Park Domain'); break; case 'DOMAIN-RENEW': $name = _('Renew Domain'); break; default: return $li[$id]['product_name'] ? $li[$id]['product_name'] : ($pdo->getRecordAttr('description_short') ? $pdo->getRecordAttr('description_short') : $pdo->getRecordAttr('name')); } return $li[$id]['product_name'] ? $li[$id]['product_name'] : sprintf('%s (%s.%s)',$name,$li[$id]['domain_name'],$li[$id]['domain_tld']); } return 'Other Item'; } public function sql_invoice_soon($fields=null,$adddays=0,$account=null,$orderby=null) { return $this->sql_InvoiceSoon($fields,$adddays,$account,$orderby); } /** * Return the SQL that determines which invoices need to be generated * * Invoices are generated when the greater of: * + system default (setup:max_inv_gen_period), (to be deprecated) * + system default (setup_invoice:invoice_advance_gen), * + the account setting (account:invoice_advance_gen) * + the net_terms setting (net_term:invoice_advance_gen) (linked from the account:net_term_id)) * * This function can be called to display the SQL that is need to generate upcoming invoices. * * @param string The SQL to use in the SELECT portion of the query, to return the fields * @param int Days in advance of the normal invoice generation date to obtain * @param int|array Limit the query to just a(n) (set of) Account ID(s) * @param string The SQL to use in the ORDER BY portion of the query. */ public function sql_InvoiceSoon($fields=null,$adddays=0,$account=null,$orderby=null) { global $C_list; # Get the max invoice days from the system configuration tables. $days = $this->sInvoiceDays(); # Pre-notification date for service $max_date = date('Y-m-d',time()+(($adddays+$days)*86400)); if ($account) { if (is_array($account)) $account_where = sprintf('AND account.id IN (%s)',implode(',',$account)); else $account_where = sprintf('AND account.id=%s',$account); } else $account_where = ''; if (is_null($fields)) $fields = 'DISTINCT service.id AS sid,account.id AS account_id,invoice.id AS iid,FROM_UNIXTIME(service.date_next_invoice,\'%Y-%m-%d\') AS invoice_date,service.sku AS sku,service.price AS price,account.first_name AS first_name,account.last_name AS last_name,account.currency_id AS billed_currency_id,service.date_orig AS date_orig'; if (is_null($orderby)) $orderby = 'ORDER BY account_id,invoice_date,sid'; else $orderby = sprintf('ORDER BY %s',$orderby); // @todo NET_TERM is not tested. if ($C_list->is_installed('net_term')) { $net_term = 'LEFT JOIN {p}net_term AS net_term ON (account.net_term_id=net_term.id AND net_term.site_id={s})'; $net_term_where = sprintf('OR ((net_term.invoice_advance_gen!="" OR net_term.invoice_advance_gen IS NOT NULL) AND service.date_next_invoice<=((86400*(net_term.invoice_advance_gen+%s+%s))+(UNIX_TIMESTAMP(CURDATE()))))',$adddays,$days); } else { $net_term = ''; $net_term_where = ''; } $sql = sprintf(' SELECT %s FROM {p}service AS service JOIN {p}account AS account ON (service.account_id=account.id AND account.site_id={s}) LEFT JOIN {p}invoice AS invoice ON (service.invoice_id=invoice.id AND invoice.site_id={s}) %s WHERE service.site_id={s} AND service.active=1 AND price > 0 AND (service.suspend_billing IS NULL OR service.suspend_billing=0) AND (service.date_next_invoice>0 AND service.date_next_invoice IS NOT NULL) AND ( ((account.invoice_advance_gen!="" OR account.invoice_advance_gen IS NOT NULL) AND service.date_next_invoice<=((86400*(account.invoice_advance_gen+%s+%s))+(UNIX_TIMESTAMP(CURDATE())))) %s OR service.date_next_invoice<=UNIX_TIMESTAMP("%s") ) %s %s', $fields,$net_term,$adddays,$days,$net_term_where,$max_date,$account_where,$orderby); $sql = str_replace('{p}',AGILE_DB_PREFIX,$sql); $sql = str_replace('{s}',DEFAULT_SITE,$sql); return $sql; } /** * Template method to list all invoices that will be generated soon */ public function tmInvoiceSoon($VAR) { global $smarty,$C_list; $db = &DB(); $order_by = isset($VAR['order_by']) ? $VAR['order_by'] : 'account_id,invoice_date,sid'; # Then from the setup_invoice table. $setup = $db->Execute(sqlSelect('setup_invoice','advance_notice')); # Run the database query $result = $db->Execute($this->sql_InvoiceSoon(null,$setup->fields['advance_notice'],null,$order_by)); # Error reporting if (! $result) { global $C_debug; $C_debug->error(__FILE__,__METHOD__,$db->ErrorMsg()); return false; } elseif (! $result->RecordCount()) { return false; } $invoice = array(); $i = 0; while (! $result->EOF) { $result->fields['_C'] = ++$i%2 ? 'row1' : 'row2'; $result->fields['id'] = $result->fields['sid']; array_push($invoice,$result->fields); $result->MoveNext(); } # Create the search record: if (count($invoice) > 0) { # create the search record #include_once(PATH_CORE.'search.inc.php'); #$search = new CORE_search; #$arr['module'] = $this->module; #$arr['sql'] = $this->sql_InvoiceSoon(); #$arr['limit'] = $limit; #$arr['order_by']= $order_by; #$arr['results'] = $results; #$search->add($arr); # Define the search id and other parameters for Smarty $smarty->assign('search_id',$search->id); $smarty->assign('page','1'); $smarty->assign('order_by',$order_by); } # Define the result count $smarty->assign('results',count($invoice)); $smarty->assign('search_show',$invoice); } public function performance($VAR) { return $this->tmPerformance($VAR); } /** * Site Performance (for the admin dashboard) */ public function tmPerformance($VAR) { global $smarty,$C_list,$C_translate; $db = &DB(); $period = array(); # Get the period type, default to month $period['period'] = empty($VAR['period']) ? 'y' : $VAR['period']; switch ($period['period']) { case 'w': $smarty->assign('period_compare',sprintf('%s %s %s',_('This Week'),_('vs'),('Last Week'))); $smarty->assign('period_forcast',_('This Week')); $period['this_start'] = mktime(0,0,0,date('m'),date('d')-date('w'),date('y')); $period['this_end'] = mktime(23,59,59,date('m'),date('d'),date('y')); $period['last_start'] = mktime(0,0,0,date('m'),date('d',$period['this_start'])-7,date('y')); $period['last_end'] = mktime(23,59,59,date('m'),date('d')-7,date('y')); break; case 'm': $smarty->assign('period_compare',sprintf('%s %s %s',_('This Month'),_('vs'),('Last Month'))); $smarty->assign('period_forcast',_('This Month')); $period['this_start'] = mktime(0,0,0,date('m'),1,date('y')); $period['this_end'] = mktime(23,59,59,date('m'),date('d'),date('y')); $period['last_start'] = mktime(0,0,0,date('m',$period['this_start'])-1,1,date('y')); $period['last_end'] = mktime(23,59,59,date('m')-1,date('d'),date('y')); break; case 'y': default: $smarty->assign('period_compare',sprintf('%s %s %s',_('This Year'),_('vs'),('Last Year'))); $smarty->assign('period_forcast',_('This Year')); $period['this_start'] = mktime(0,0,0,1,1,date('y',time())); $period['this_end'] = mktime(23,59,59,date('m'),date('d'),date('y')); $period['last_start'] = mktime(0,0,0,1,1,date('y',$period['this_start'])-1); $period['last_end'] = mktime(23,59,59,date('m'),date('d'),date('y')-1); break; } # Get sales for this period $rs = $db->Execute(sqlSelect('invoice','SUM(total_amt) as total_amt', array('where'=>sprintf('date_orig>=%s AND date_orig<=%s',$period['this_start'],$period['this_end'])))); if ($rs && $rs->RecordCount()) $this_amt = $rs->fields['total_amt']; else $this_amt = 0; # Get sales for last period $rs = $db->Execute(sqlSelect('invoice','SUM(total_amt) as total_amt', array('where'=>sprintf('date_orig>=%s AND date_orig<=%s',$period['last_start'],$period['last_end'])))); if ($rs && $rs->RecordCount()) $last_amt = $rs->fields['total_amt']; else $last_amt = 0; $smarty->assign('sales_current',$this_amt); $smarty->assign('sales_previous',$last_amt); $smarty->assign('sales_change',($last_amt > 0) ? $this_amt/$last_amt*100-100 : 0); # Get forcast for current period switch ($period['period']) { case 'w': $dow = date('w')+1; $forcast_daily = $this_amt/$dow; $forcast_l_daily = $last_amt/7; $forcast_change = $forcast_daily/$forcast_1_daily*100-100; $forcast_current = $forcast_daily*7; break; case 'm': $forcast_daily = $this_amt/date('d'); $forcast_1_daily = $last_amt/date('t',mktime(0,0,0,date('m')-1,1,date('y'))); $forcast_change = $forcast_daily/$forcast_1_daily*100-100; $forcast_current = $forcast_daily*date('t'); break; case 'y': default: $forcast_daily = $this_amt/date('z'); $forcast_1_daily = $last_amt/356; $forcast_change = $forcast_daily/$forcast_1_daily*100-100; $forcast_current = $forcast_daily*365; break; } $smarty->assign('forcast_current',$forcast_current); $smarty->assign('quota_current',$forcast_daily); $smarty->assign('forcast_change',($last_amt > 0) ? $forcast_daily/$forcast_1_daily*100 : 0); # Get AR credits for this period $rs = $db->Execute(sqlSelect('invoice','SUM(billed_amt) as total_amt', array('where'=>sprintf('date_orig>=%s AND date_orig<=%s AND billed_amt>0',$period['this_start'],$period['this_end'])))); if ($rs && $rs->RecordCount()) $this_billed_amt = $rs->fields['total_amt']; else $this_billed_amt = 0; # Get AR credits for last period $rs = $db->Execute(sqlSelect('invoice','SUM(billed_amt) as total_amt', array('where'=>sprintf('date_orig>=%s AND date_orig<=%s AND billed_amt>0',$period['last_start'],$period['last_end'])))); if ($rs && $rs->RecordCount()) $last_billed_amt = $rs->fields['total_amt']; else $last_billed_amt = 0; $smarty->assign('ar_credits_current',$this_billed_amt); $smarty->assign('ar_credits_previous',$last_billed_amt); $smarty->assign('ar_credit_change',($last_billed_amt > 0) ? $this_billed_amt/$last_billed_amt*100-100 : 0); # Get AR Balance $smarty->assign('ar_balance_current',$this_billed_amt-$this_amt); $smarty->assign('ar_balance_last',$last_billed_amt-$last_amt); # Get Users (current) $rs = $db->Execute(sqlSelect('account','COUNT(*) as count', array('where'=>sprintf('date_orig>=%s AND date_orig<=%s',$period['this_start'],$period['this_end'])))); if ($rs && $rs->RecordCount()) $users_current = $rs->fields['count']; else $users_current = 0; # Get Users (previous) $rs = $db->Execute(sqlSelect('account','COUNT(*) as count', array('where'=>sprintf('date_orig>=%s AND date_orig<=%s',$period['last_start'],$period['last_end'])))); if ($rs && $rs->RecordCount()) $users_previous = $rs->fields['count']; else $user_previous = 0; $smarty->assign('users_current',$users_current); $smarty->assign('users_previous',$users_previous); $smarty->assign('users_change',($users_previous > 0) ? $users_current/$users_current*100-100 : 0); # Get Affiliate stats if ($C_list->is_installed('affiliate')) { $smarty->assign('show_affiliates',true); # Get affiliate sales for this period $rs = $db->Execute(sqlSelect('invoice','SUM(total_amt) as total_amt', array('where'=>sprintf('date_orig>=%s AND date_orig<=%s AND affiliate_id NOT IN (0,"")',$period['this_start'],$period['this_end'])))); if ($rs && $rs->RecordCount()) $this_amt = $rs->fields['total_amt']; else $this_amt = 0; # Get affiliate sales for last period $rs = $db->Execute(sqlSelect('invoice','SUM(total_amt) as total_amt', array('where'=>sprintf('date_orig>=%s AND date_orig<=%s AND affiliate_id NOT IN (0,"")',$period['last_start'],$period['last_end'])))); if ($rs && $rs->RecordCount()) $last_amt = $rs->fields['total_amt']; else $last_amt = 0; $smarty->assign('affiliate_sales_current',$this_amt); $smarty->assign('affiliate_sales_previous',$last_amt); $smarty->assign('affiliate_sales_change',($last_amt > 0) ? $this_amt/$last_amt*100-100 : 0); } # Generate the Calendar Overview include_once(PATH_MODULES.'core/calendar.inc.php'); $calendar = new calendar; $C_list->currency(DEFAULT_CURRENCY); $currency_symbol = $C_list->format_currency[DEFAULT_CURRENCY]['symbol']; # Get the paid/due invoice statistics $records = $this->sql_GetRecords(array('where'=>sprintf('date_orig>=%s AND date_orig<=%s',$calendar->start,$calendar->end))); $paid = array(); $due = array(); if (count($records)) { foreach ($records as $rs) { $day = date('j',$rs['date_orig']); if ($rs['billed_amt'] > 0 && ($rs['billing_status'] == 1 || $rs['refund_status'] != 1)) @$paid[$day] += $rs['billed_amt']; if ($rs['billing_status'] != 1 && $rs['refund_status'] != 1) @$due[$day] += $rs['total_amt']-$rs['billed_amt']; } foreach ($paid as $day => $item) $calendar->add(sprintf('%s - %s%s',_('Paid'),$currency_symbol,number_format($item,2)),$day,'green','green'); foreach ($due as $day => $item) $calendar->add(sprintf('%s - %s%s',_('Due'),$currency_symbol,number_format($item,2)),$day,'red','red'); } # Get the upcoming due services $rs = $db->Execute(sqlSelect('service','date_next_invoice,price', array('where'=>sprintf('price>0 AND date_next_invoice>=%s AND date_next_invoice<=%s AND suspend_billing<>1', $calendar->start,$calendar->end)))); if ($rs && $rs->RecordCount()) { $due = array(); while (! $rs->EOF) { $day = date('j',$rs->fields['date_next_invoice']); @$due[$day] += $rs->fields['price']; $rs->MoveNext(); } foreach ($due as $day=>$item) $calendar->add(sprintf('%s - %s%s',_('Recurring'),$currency_symbol,number_format($item,2)),$day,'grey','grey'); } $smarty->assign('calendar',$calendar->generate()); return; } /** * Task based function to e-mail or store printable PDF of all unprinted invoices * * @todo This seems to be hard limited to do 100 invoices in a run - why? (make it configurable, or remove the limit?) */ public function task_DeliverInvoices() { # Get all unprinted invoices $db = &DB(); $rs = $db->SelectLimit(sqlSelect($db,array('invoice','account'), 'A.id,B.email,B.first_name,B.last_name,B.invoice_delivery,B.invoice_show_itemized', '(A.billing_status=0 OR A.billing_status IS NULL) AND (A.print_status=0 OR A.print_status=NULL) AND (A.status=1) AND A.account_id=B.id AND (B.invoice_delivery IS NOT NULL AND B.invoice_delivery>0)'),100); if ($rs && $rs->RecordCount()) { # Send the e-mail.... require_once(PATH_INCLUDES.'phpmailer/class.phpmailer.php'); $mail = new PHPMailer(); $mail->From = SITE_EMAIL; $mail->FromName = SITE_NAME; /* $mail->SMTPAuth = true; $mail->Host = "smtp.domain.com"; $mail->Username = "user"; $mail->Password = "pass"; $mail->Mailer = "smtp"; $mail->Debug = true; */ while (! $rs->EOF) { $this->sql_LoadRecord($rs->fields['id']); switch ($rs->fields['invoice_delivery']) { # Email Invoice case 1: if ($file = $this->pdf(null,null,array('dest'=>'S'))) { $mail->AddAddress($rs->fields['email'], sprintf('%s %s',$rs->fields['first_name'],$rs->fields['last_name'])); $mail->AddBcc('deon@leenooks.vpn'); $mail->AddBcc('chris@graytech.com.au'); $mail->Subject = sprintf('%s %s: %s',SITE_NAME,_('Invoice'),$this->getRecordAttr('id')); $mail->AltBody = sprintf("Please find the printable version of invoice number %s attached.\r\n\r\nThank you,\r\n%s",$this->getRecordAttr('id'),SITE_NAME); $mail->IsHTML(true); $mail->AddEmbeddedImage(sprintf('%s/%s',PATH_THEMES.DEFAULT_THEME,'invoice/invoice-logo.png'),'logoimg','','base64','image/png'); $mail->Body = sprintf('

Please find attached invoice %s from %s

',$this->getRecordAttr('id'),SITE_NAME); $mail->Body .= sprintf('

A PDF version is also attached with more detail and payment options. Alternatively, you can visit our website and pay online.

', URL,$this->getRecordAttr('id')); $mail->Body .= "
\n"; $mail->Body .= wordwrap($this->html(array('id'=>$this->getRecordAttr('id')))); $mail->AddStringAttachment($file,sprintf('%s.pdf',$this->getPrintInvoiceID()),'base64','application/pdf'); if ($mail->Send()) $db->Execute(sqlUpdate($db,'invoice',array('print_status'=>1),array('id'=>$this->getRecordAttr('id')))); else printf('Unable to email invoice # %s to %s
',$this->getRecordAttr('id'),$rs->fields['email']); $mail->ClearAddresses(); $mail->ClearAttachments(); } break; # Print Invoice case 2: $file = tempnam(PATH_FILES,sprintf('pdf_inv_%s.pdf',$this->getPrintInvoiceID())); $this->pdf(null,null,array('dest'=>'F','file'=>$file)); if (copy($file,sprintf('%sinvoice_%s.pdf',AGILE_PDF_INVOICE_PATH,$this->getPrintInvoiceID()))) $db->Execute(sqlUpdate($db,'invoice',array('print_status'=>1),array('id'=>$this->getRecordAttr('id')))); unlink($file); break; default: printf('Unknown invoice_delivery: %s for %s
',$rs->fields['invoice_delivery'],$this->getRecordAttr('id')); } $rs->MoveNext(); } } } /** * Email a list of overdue invoices. * * @uses PHPMailer * @uses staff */ public function task_OverdueListEmail() { # @todo Make this configurable somewhere. $receipient_dep = 'Accounts'; require_once(PATH_MODULES.'staff/staff.inc.php'); $so = new staff; if (! $staff = $so->sDepartmentMemberEmail($receipient_dep)) return false; $db = &DB(); $rs = $db->Execute(sqlSelect($db,array('invoice','account'),'A.id,A.account_id,ROUND(SUM(A.total_amt-A.billed_amt-IFNULL(A.credit_amt,0)),2) as total,B.first_name,B.last_name,A.due_date',sprintf('A.status=1 AND ROUND(A.total_amt-A.billed_amt-IFNULL(A.credit_amt,0),2)>0 AND A.account_id=B.id AND A.due_date<%s',time()),false,false,false,'account_id,id')); if ($rs && $rs->RecordCount()) { $body = ''; $account_total = 0; $account_id = ''; $i = 0; $count = 0; $grand_total = 0; while (! $rs->EOF) { if ($account_id != $rs->fields['account_id'] && $i) { $body .= sprintf("Total: %3.2f\n\n",$account_total); $account_total = 0; $i = 0; } if (! $i) $body .= sprintf("%s %s (%s)\n",$rs->fields['last_name'],$rs->fields['first_name'],$rs->fields['account_id']); $body .= sprintf(" Invoice: %s, Due Date: %s, Amount: %3.2f\n",$rs->fields['id'],date('Y-m-d',$rs->fields['due_date']),$rs->fields['total']); $account_total += $rs->fields['total']; $grand_total += $rs->fields['total']; $account_id = $rs->fields['account_id']; $count++; $i++; $rs->MoveNext(); } if ($account_total) $body .= sprintf("Total: %3.2f\n",$account_total); $body .= "\n"; if ($count || $ground_total) $body .= sprintf("%3.2f outstanding in %s invoices\n",$grand_total,$count); # Send the e-mail.... require_once(PATH_INCLUDES.'phpmailer/class.phpmailer.php'); $mail = new PHPMailer(); foreach ($staff as $email => $name) $mail->AddAddress($email,$name); $mail->From = SITE_EMAIL; $mail->FromName = SITE_NAME; $mail->Subject = _('List of Invoices Overdue'); $mail->Body = $body; $mail->Send(); } } /** * Return the invoice ID */ public function getPrintInvoiceID() { return sprintf('%02s-%04s-%06s',DEFAULT_SITE,$this->getRecordAttr('account_id'),$this->getRecordAttr('id')); } public function getPrintInvoiceNum() { return $this->getRecordAttr('id'); } /** * Add an item to the invoice * * @uses host_tld * @uses invoice_item * @uses product */ public function aaddItem($item) { include_once(PATH_MODULES.'invoice_item/invoice_item.inc.php'); $ito = new invoice_item; foreach ($item as $i => $v) if (isset($ito->field[$i])) $ito->setRecordAttr($i,$v); $ito->setRecordAttr('invoice_id',$this->getRecordAttr('id')); if (! is_null($this->getRecordAttr('account_id'))) $ito->setRecordAttr('account_id',$this->getRecordAttr('account_id')); if (! empty($item['product_id'])) { include_once(PATH_MODULES.'product/product.inc.php'); $po = new product($item['product_id']); $ito->setRecordAttr('product_name',$po->getTranslateField('name')); $ito->setRecordAttr('product_id',$po->getRecordAttr('id')); // $ito->setRecordAttr('sku',$po->getRecordAttr('sku')); $ito->setRecordAttr('quantity',isset($item['quantity']) ? $item['quantity'] : 1); $ito->setRecordAttr('price_type',$po->getRecordAttr('price_type')); $ito->setRecordAttr('recurring_schedule',isset($item['recurring_schedule']) ? $item['recurring_schedule'] : $po->getRecordAttr('price_recurr_default')); if (isset($item['price_base'])) $ito->setBaseRate($item['price_base']); else { $price = $po->price_prod( array( 'price_type'=>$po->getRecordAttr('price_type'), 'price_recurr_type'=>$po->getRecordAttr('price_recurr_type'), 'price_recurr_weekday'=>$po->getRecordAttr('price_recurr_weekday'), 'price_recurr_week'=>$po->getRecordAttr('price_recurr_week'), 'price_group'=>$po->getRecordAttr('price_group'), 'price_base'=>$po->getRecordAttr('price_base'), 'price_setup'=>$po->getRecordAttr('price_setup')), $ito->getRecordAttr('recurring_schedule'),$this->getRecordAttr('account_id'),false); $ito->setRecordAttr('price_setup',isset($item['price_setup']) ? $item['price_setup'] : $price['setup']); $ito->setBaseRate($price['base']); } $billdates = $po->recurrDates($ito->getRecordAttr('recurring_schedule'),$po->getRecordAttr('recur_weekday'),null, is_null($ito->getRecordAttr('date_start')) ? time() : $ito->getRecordAttr('date_start')); $ito->setRecordAttr('date_start',$billdates['date']); $ito->setRecordAttr('date_stop',$billdates['end']); $ito->setProRata($billdates['prorata']); } elseif (! empty($item['charge_id'])) { } elseif ($item['type'] == 'domain') { include_once(PATH_MODULES.'host_tld/host_tld.inc.php'); $hto = new host_tld(); #@todo - TEMP until we have another recurrDates() function that we can use. include_once(PATH_MODULES.'product/product.inc.php'); $po = new product(); $tld = $hto->sql_GetRecords(array('where'=>array('name'=>$item['domain_tld']))); if (! count($tld)) { printf('NO TLD FOR %s??',$item['domain_tld']);die();}; $tld = array_pop($tld); $pg = unserialize($tld['price_group']); # @todo - need to improve this code after reworking host_tld switch (@$item['host_type']) { case 'register' : $ito->setRecordAttr('product_name','Domain Name Register'); break; default: $ito->setRecordAttr('product_name','Domain Name Renewal'); } $ito->setRecordAttr('recurring_schedule',isset($item['recurring_schedule']) ? $item['recurring_schedule'] : 5);// get this from host_tld $billdates = $po->recurrDates($ito->getRecordAttr('recurring_schedule'),null,null, is_null($ito->getRecordAttr('date_start')) ? time() : $ito->getRecordAttr('date_start'),true); $ito->setRecordAttr('date_start',$billdates['date']); $ito->setRecordAttr('date_stop',$billdates['end']); $ito->setProRata($billdates['prorata']); $ito->setRecordAttr('price_type',1); $ito->setRecordAttr('price_setup',0); if (is_null($ito->getRecordAttr('price'))) { include_once(PATH_MODULES.'host_tld/host_tld.inc.php'); $tldObj = new host_tld; $tldprice = $tldObj->price_tld_arr($ito->getRecordAttr('domain_tld'),$item['host_type'], false,false,false,$ito->getRecordAttr('account_id')); $ito->setBaseRate($tldprice[$ito->getRecordAttr('domain_term')]); } } else { echo '
';print_r(array('i'=>$item,'ii'=>$ito));
			echo 'NEED TO FIGURE OUT THE PRICE IF WE ARE NOT A PRODUCT';die();
			$ito->setRecordAttr('price_setup',0);
			$ito->setRecordAttr('recurring_schedule',isset($item['recurr_schedule']) ? $item['recurr_schedule'] : 0);
		}

		# If we are a cart, we'll set a cart ID, so the item can be deleted.
		if (isset($item['cart_id']))
			$ito->setRecordAttr('cart_id',$item['cart_id']);

		array_push($this->items,$ito);

		# Return the item id.
		return count($this->items)-1;
	}

	public function sCountItems() {
		return count($this->items);
	}

	public function getItems() {
		$items = array();

		foreach ($this->items as $item)
			array_push($items,$item->getRecord());

		return $items;
	}

	public function getProductItems() {
		$items = array();

		foreach ($this->items as $item)
			if (! is_null($item->getRecordAttr('product_id')))
				array_push($items,$item->getRecordAttr('product_id'));

		return $items;
	}

	public function getProductItemTypes() {
		$items = array();

		foreach ($this->items as $item)
			if (! is_null($item->getRecordAttr('price_type')))
				array_push($items,$item->getRecordAttr('price_type'));

		return array_unique($items,SORT_NUMERIC);
	}

	/**
	 * This function will get all the discount codes used in the invoice
	 * it will also cause all the discounts to be re-calcated
	 */
	public function getDiscountDetails() {
		$discounts = array();

		foreach ($this->items as $item)
			foreach ($item->getDiscountArr($this->sSubTotal()) as $discount)
				@$discounts[$discount['discount']] += $discount['amount'];

		$d = array();
		foreach ($discounts as $k=>$v)
			array_push($d,array('name'=>$k,'total'=>$v));

		return $d;
	}

	public function sTotalDiscount($recalc=false) {
		$total = 0;

		if ($recalc)
			$this->getDiscountDetails();

		foreach ($this->items as $item)
			$total += $item->getRecordAttr('discount_amt');

		return $total;
	}

	# @todo change this to work the same way as getDiscountDetails()
	public function getTaxDetails() {
		$taxes = array();

		foreach ($this->items as $item)
			foreach ($item->getTaxArr() as $tax)
				@$taxes[$tax['name']] += $tax['rate'];

		return $taxes;
	}

	public function sTotalTax($recalc) {
		$total = 0;

		if ($recalc)
			$this->getTaxDetails();

		foreach ($this->items as $item)
			$total += $item->getRecordAttr('tax_amt');

		return $total;
	}

	/**
	 * This function will calculate the pre-tax/pre-discounted totals for this invoice
	 */
	public function sSubTotal($calc=false) {
		static $total;

		if ($total && ! $calc)
			return $total;
		else
			$total = 0;

		foreach ($this->items as $item)
			$total += $item->sGetSubTotalAmt();

		return $total;
	}

	public function sTotal($calc=false) {
		static $total;

		if ($total && ! $calc)
			return $total;
		else
			$total = 0;

		foreach ($this->items as $item)
			$total += $item->sGetTotalAmt($calc);

		return $total;
	}

	/**
	 * Calculate the recurring amount for this invoice
	 *
	 * The recurring amount is only applicable if all items have the same recur_schedule
	 */
	public function sRecurAmt($calc=false) {
		static $total;

		if (! is_null($total) && ! $calc)
			return $total;
		else
			$total = null;

		$sched = null;
		foreach ($this->items as $item) {
			if (is_null($sched))
				$sched = $item->getRecordAttr('recurring_schedule');

			if ($sched != $item->getRecordAttr('recurring_schedule'))
				return null;

			$total += $item->sGetRecurAmt();
		}

		return $total;
	}

	public function sql_SaveRecord($noconvert=false) {
		global $VAR;

		# (Re)Calculate our discounts and taxes
		$this->setRecordAttr('discount_amt',$this->sTotalDiscount(true));
		$this->setRecordAttr('tax_amt',$this->sTotalTax(true));
		$this->setRecordAttr('total_amt',$this->sTotal(true));

		# Save the invoice and items
		# @todo make this into a transaction, so if the item records fail, we dont have a partial save
		if ($id = parent::sql_SaveRecord($noconvert)) {
			foreach ($this->items as $item) {
				$item->setRecordAttr('invoice_id',$id);

				# Remove the cart id
				$item->delRecordAttr('cart_id');

				if (! $item->sql_SaveRecord($noconvert,false)) {
					echo '
';print_r($item);die();
				}
			}
		}

		return $id;
	}

	public function custom_tracking($VAR) { return $this->drCustomTracking($VAR); }
	/**
	 * Custom Tracking
	 */
	public function drCustomTracking($VAR) {
		# If we dont have a tracking file, or we are not logged in, no point continuing.
		if (! is_file(PATH_FILES.'tracking.txt') || ! SESS_LOGGED)
			return false;

		# Check if we are in the iframe, otherwise render the iframe.
		if (empty($VAR['_escape']) || empty($VAR['confirm'])) {
			printf('',
				md5(microtime()));
			return;
		}

		# Get the un-tracked invoice details
		$db = &DB();

		$result = $this->sql_GetRecords(array('where'=>sprintf('(custom_affiliate_status IS NULL OR custom_affiliate_status=0) AND billing_status=1 AND account_id=%s',SESS_ACCOUNT)));

		if (! count($result))
			return false;

		# Get the totals
		$invoice = '';
		$total_amount = 0;
		foreach ($result as $record) {
			if (! empty($invoice))
				$invoice .= '-';

			$invoice .= $record['id'];
			$total_amount += $record['total_amt'];
		}

		# Echo the custom tracking code to the screen:
		$tracking = file_get_contents(PATH_FILES.'tracking.txt');
		$tracking = str_replace('%%amount%%',$total_amount,$tracking);
		$tracking = str_replace('%%invoice%%',$invoice,$tracking);
		$tracking = str_replace('%%affiliate%%',SESS_AFFILIATE,$tracking);
		$tracking = str_replace('%%campaign%%',SESS_CAMPAIGN,$tracking);
		$tracking = str_replace('%%account%%',SESS_ACCOUNT,$tracking);

		echo $tracking;

		# Update the record so it is not tracked again
		$rs = $db->Execute(
			sqlUpdate('invoice',array('custom_affiliate_status'=>1),array('where'=>sprintf('account_id=%s AND billing_status=1',SESS_ACCOUNT))));

		if ($rs === false) {
			global $C_debug;

			$C_debug->error(__FILE__,__METHOD__,sprintf('%s (%s)',$db->ErrorMsg(),$sql));
		}

		return true;
	}

	public function autoApproveInvoice($id) { return $this->pAutoApprove($id); }
	/**
	 * Auto approve Invoice
	 */
	public function pAutoApprove($id) {
		$db = &DB();
		$do = false;

		# Get the invoice details
		$invoices = $this->sql_GetRecords(array('where'=>array('id'=>$id,'process_status'=>0)));
		if (! count($invoices))
			return false;

		$invoice = array_pop($invoices);

		# Get the checkout details
		$checkout = $db->Execute(sqlSelect('checkout','*',array('where'=>array('id'=>$invoice['checkout_plugin_id']))));
		if (! $checkout) {
			global $C_debug;
			$C_debug->error(__FILE__,__METHOD__,$db->ErrorMsg());

			return false;
		}

		# Get the account details
		$account = $db->Execute(sqlSelect('account','*',array('where'=>array('id'=>$invoice['account_id']))));
		if ($account) {
			global $C_debug;
			$C_debug->error(__FILE__,__METHOD__,$db->ErrorMsg());

			return false;
		}

		# Is this a recurring invoices, and is manual approvale req?
		if ($invoice['type'] == 1 && $checkout->fields['manual_approval_recur'] != 1)
			$do = true;

		# Manual approval required for all?
		if ($invoice['type'] != 1 && $checkout->fields['manual_approval_all'] != 1)
			$do = true;

		if (! $do) {
			# Manual approval required for invoice amount?
			if (! empty($checkout->fields['manual_approval_amount']) && $do == true)
				if ($checkout->fields['manual_approval_amount'] <= $invoice['total_amt'])
					$do = false;

			# Manual approval required for user's country?
			if (! empty($checkout->fields['manual_approval_country']) && $do == true) {
				$arr = unserialize($checkout->fields['manual_approval_country']);

				for ($i=0; $ifields['country_id'] == $arr[$i])
						$do = false;
			}

			# Manual approval req. for user's currency?
			if (! empty($checkout->fields['manual_approval_currency']) && $do == true) {
				$arr = unserialize($checkout->fields['manual_approval_currency']);

				for ($i=0; $ifields['manual_approval_group']) && $do == true) {
				# Get the group details
				$groups = $db->Execute(sqlSelect('account_group','group_id',array('where'=>array('account_id'=>$invoice['account_id'],'active'=>1))));

				if (! $groups) {
					global $C_debug;
					$C_debug->error(__FILE__,__METHOD__,$db->ErrorMsg());

					return false;
				}

				$arr = unserialize($checkout->fields['manual_approval_group']);
				while (! $groups->EOF) {
					for ($i=0; $ifields['group_id'];

						if ($idx == $arr[$i])
							$do = false;
					}

					$groups->MoveNext();
				}
			}
		}

		# Approve the invoice
		if ($do)
			$this->pApprove($id);

		else {
			# Admin manual approval notice
			include_once(PATH_MODULES.'email_template/email_template.inc.php');
			$mail = new email_template;

			$mail->send('invoice_manual_auth_admin',$invoice['account_id'],$invoice['id'],$invoice['checkout_plugin_id'],'');
		}
	}

	public function approveInvoice($id) { return $this->pApprove($id); }
	/**
	 * Approve an invoice, which will enable services to be provisioned.
	 *
	 * @param int $id Invoice ID to approve
	 * @uses service
	 */
	public function pApprove($id) {
		$db = &DB();

		# Get the invoice details
		$invoices = $this->sql_GetRecords(array('where'=>array('id'=>$id,'process_status'=>0)));
		if (! count($invoices))
			return false;

		$invoice = array_pop($invoices);

		# Update the invoice approval status:
		$rs = $db->Execute(sqlUpdate('invoice',array('date_last'=>time(),'process_status'=>1),array('where'=>array('id'=>$id))));
		if (! $rs) {
			global $C_debug;

			$C_debug->error(__FILE__,__METHOD__,$db->ErrorMsg());

			return false;
		}

		# Send approval notice to user:
		include_once(PATH_MODULES.'email_template/email_template.inc.php');
		$mail = new email_template;

		$mail->send('invoice_approved_user',$invoice['account_id'],$id,'','');

		# Include the service class
		include_once(PATH_MODULES.'service/service.inc.php');
		$so = new service;

		# Determine if services have already been created for this invoice
		switch ($invoice['type']) {
			# Recurring invoice, just update assoc services
			case 1:
				# Loop through invoice items & approve assoc services
				$rs = $db->Execute(sqlSelect('invoice_item','service_id',array('where'=>array('invoice_id'=>$id))));

				if (! $rs) {
					global $C_debug;

					$C_debug->error(__FILE__,__METHOD__,$db->ErrorMsg());
					return false;
				}

				# Update services status
				if ($rs->RecordCount())
					while (! $rs->EOF) {
						$so->approveService($rs->fields['service_id']);
						$rs->MoveNext();
					}

				break;

			default:
				$rs = $db->Execute(sqlSelect('service','id',array('where'=>array('invoice_id'=>$id))));

				if (! $rs) {
					global $C_debug;

					$C_debug->error(__FILE__,__METHOD__,$db->ErrorMsg());
					return false;
				}

				# If there are already existing services, just update their status
				if ($rs->RecordCount()) {
					while (! $rs->EOF) {
						$so->approveService($rs->fields['id']);
						$rs->MoveNext();
					}

				# No services exist, they can be provisioned
				} else {

					# Get the invoice items in this invoice
					$ii = $db->Execute(sqlSelect('invoice_item','*',array('where'=>sprintf('(parent_id IN (0,"") OR parent_id IS NULL)'))));
					if (! $ii) {
						global $C_debug;

						$C_debug->error(__FILE__,__METHOD__,$db->ErrorMsg());
						return false;
					}

					while (! $ii->EOF) {
						if (! $ii->fields['service_id']) {
							# Add the service
							$so->invoiceItemToService($ii->fields['id'],$invoice);

							# Check for any children items in this invoice
							$iii = $db->Execute(sqlSelect('invoice_item','*',array('where'=>array('parent_id'=>$ii->fields['id'],'invoice_id'=>$id))));

							if (! $iii) {
								global $C_debug;

								$C_debug->error(__FILE__,__METHOD__,$db->ErrorMsg());
								return false;
							}

							while (! $iii->EOF) {
								# Add the service
								$so->invoiceItemToService($iii->fields['id'],$invoice);
								$iii->MoveNext();
							}

						} else {
							# This is a domain renewal
							if ($ii->fields['item_type'] == 2 && $ii->fields['domain_type'] == 'renew')
								$so->renewDomain($ii,$invoice->fields['account_billing_id']);
							# This is an upgrade for an existing service
							else
								$so->modifyService($ii,$invoice->fields['account_billing_id']);
						}

						$ii->MoveNext();
					}
				}
		}

		# Create a memo
		$rs = $db->Execute(sqlInsert($db,'invoice_memo',array(
			'date_orig'=>time(),
			'invoice_id'=>$id,
			'account_id'=>(defined('SESS_ACCOUNT')) ? SESS_ACCOUNT : 0,
			'type'=>'approval',
			'memo'=>_('Invoice Approved')
		)));

		if (! $rs) {
			global $C_debug;

			$C_debug->error(__FILE__,__METHOD__,$db->ErrorMsg());
			return false;
		}

		return true;
	}

	public function voidInvoice($id) { return $this->pVoid($id); }
	/**
	 * Void an invoice, which will suspend services.
	 */
	public function pVoid($id) {
		$db = &DB();

		# Get the invoice details
		$invoices = $this->sql_GetRecords(array('where'=>array('id'=>$id,'process_status'=>1)));
		if (! count($invoices))
			return false;

		# Update the invoice approval status:
		$rs = $db->Execute(sqlUpdate('invoice',array('date_last'=>time(),'process_status'=>0),array('where'=>array('id'=>$id))));
		if (! $rs) {
			global $C_debug;

			$C_debug->error(__FILE__,__METHOD__,$db->ErrorMsg());
			return false;
		}

		# Determine if services have already been created for this invoice and deactivate
		$rs = $db->Execute(sqlSelect('service','id',array('where'=>array('invoice_id'=>$id))));
		if (! $rs) {
			global $C_debug;

			$C_debug->error(__FILE__,__METHOD__,$db->ErrorMsg());
			return false;
		}

		# Include the service class
		include_once(PATH_MODULES.'service/service.inc.php');
		$so = new service;

		if ($rs->RecordCount()) {
			# Update services to inactive status:
			while (! $rs->EOF) {
				$so->voidService($rs->fields['id']);
				$rs->MoveNext();
			}
		}

		# Loop through invoice items & delete assoc services
		$rs = $db->Execute(sqlSelect('invoice_item','service_id',array('where'=>array('invoice_id'=>$id))));
		if (! $rs) {
			global $C_debug;

			$C_debug->error(__FILE__,__METHOD__,$db->ErrorMsg());
			return false;
		}

		# Update services to inactive status
		if ($rs->RecordCount()) {
			while (! $rs->EOF) {
				$so->voidService($rs->fields['service_id']);
				$rs->MoveNext();
			}
		}

		# If voided, create a memo
		$rs = $db->Execute(sqlInsert($db,'invoice_memo',array(
			'date_orig'=>time(),
			'invoice_id'=>$id,
			'account_id'=>(defined('SESS_ACCOUNT')) ? SESS_ACCOUNT : 0,
			'type'=>'void',
			'memo'=>_('Invoice Voided')
		)));

		if (! $rs) {
			global $C_debug;

			$C_debug->error(__FILE__,__METHOD__,$db->ErrorMsg());
			return false;
		}

		return true;
	}

	/**
	 * Reconcile Invoice
	 */
	public function reconcile($VAR) {
		global $C_translate,$C_debug,$C_list;

		$db = &DB();

		# Reconcile is disabled when payment is installed
		# @todo Think of a better way (dynamic) to handle this
		if (! $C_list->is_installed('payment')) {
			$C_debug->alert('Reconcile is disabled, please use the payment option!');

			return false;
		}

		# Validate amt
		if ($VAR['amount'] <= 0) {
			$C_debug->alert(_('Payment amount to low!'));

			return false;
		}

		# Get the invoice details
		$invoices = $this->sql_GetRecords(array('where'=>array('id'=>$VAR['id'])));
		if (! count($invoices))
			return false;

		$invoice = array_pop($invoices);
		$billing_status = 1;

		if ($VAR['amount'] > $invoice['total_amt']-$invoice['billed_amt']) {
			$update = $invoice['total_amt'];

			$C_translate->value['invoice']['amt'] = number_format($VAR['amount']-$invoice['total_amt']-$invoice['billed_amt'],2);
			$alert = $C_translate->translate('rec_over','invoice','');

		} elseif ($VAR['amount'] == $invoice['total_amt']-$invoice['billed_amt']) {
			$update = $invoice['total_amt'];

		} else {
			$update = $VAR['amount'] + $invoice['billed_amt'];
			$billing_status = 0;
		}

		# Update the invoice record
		$rs = $db->Execute(
			sqlUpdate('invoice',array('date_last'=>time(),'billed_amt'=>$update,'billing_status'=>$billing_status),
				array('where'=>array('id'=>$VAR['id']))));

		# Create a memo
		$rs = $db->Execute(sqlInsert($db,'invoice_memo',array(
			'date_orig'=>time(),
			'invoice_id'=>$VAR['id'],
			'account_id'=>(defined('SESS_ACCOUNT')) ? SESS_ACCOUNT : 0,
			'type'=>'reconcile',
			'memo'=>sprintf('%s: %s (%s)',_('Payment Added to Invoice'),number_format($VAR['amount'],2),isset($VAR['memo']) ? $VAR['memo'] : '')
		)));

		# Receipt printing
		# @todo Move this to be consistent with invoice printing.
		include_once PATH_MODULES.'invoice/receipt_print.php';
		$receipt = new receipt_print;

		$receipt->add($invoice,number_format($VAR['amount'],2),number_format($update,2));

		# Auto update if billed complete
		if ($billing_status) {
			$this->autoApproveInvoice($VAR['id']);

			# User invoice creation confirmation
			include_once(PATH_MODULES.'email_template/email_template.inc.php');
			$email = new email_template;
			$email->send('invoice_paid_user',$invoice['account_id'],$VAR['id'],$invoice['billed_currency_id'],'');

			# Admin alert of payment processed
			$email = new email_template;
			$email->send('admin->invoice_paid_admin',$invoice['account_id'],$VAR['id'],$invoice['billed_currency_id'],'');
		}

		# Redirect
		if (! empty($VAR['redirect'])) {
			printf('';

			exit;
		}

		$C_debug->alert($C_translate->translate('ref_comp','invoice',''));

		return;
	}

	/**
	 * Refund Invoice
	 */
	public function refund($VAR) {
		global $C_translate,$C_debug;

		# Validate amt
		if ($VAR['amount'] <= 0) {
			$C_debug->alert(_('Refund amount to low!'));

			return false;
		}

		$update = $this->getRecordAttr('billed_amt')-$VAR['amount'];
		$billing_status = ($update>0) ? 1 : 0;

		# Update the invoice record
		$rs = $db->Execute(
			sqlUpdate('invoice',
				array('date_last'=>time(),'billed_amt'=>$update,'billing_status'=>$billing_status,'suspend_billing'=>1,'refund_status'=>1),
				array('where'=>array('id'=>$VAR['id']))));

		if (! $rs) {
			$C_debug->error(__FILE__,__METHOD__,$db->ErrorMsg());

			return false;
		}

		# Create a memo
		$rs = $db->Execute(sqlInsert($db,'invoice_memo',array(
			'date_orig'=>time(),
			'invoice_id'=>$VAR['id'],
			'account_id'=>(defined('SESS_ACCOUNT')) ? SESS_ACCOUNT : 0,
			'type'=>'refund',
			'memo'=>sprintf('%s: %s (%s)',_('Refunded Invoice'),number_format($VAR['amount'],2),isset($VAR['memo']) ? $VAR['memo'] : '')
		)));

		if (! $rs) {
			$C_debug->error(__FILE__,__METHOD__,$db->ErrorMsg());

			return false;
		}

		# Void
		$this->pVoid($VAR['id']);

		# Call into the checkout plugin and attempt realtime refund
		$C_debug->alert('Realtime refund processing not enabled.');
		if (! true) {
			$billing = $db->Execute(
				sqlSelect($db,
					array('account_billing','checkout'),
					'A.*,B.checkout_plugin',sprintf('A.id=%s AND A.checkout_plugin_id=B.id',$this->getRecordAttr('account_billing_id'))));

			if ($billing && $billing->RecordCount() && ! empty($billing->fields['checkout_plugin'])) {
				$plugin_file = sprintf('%scheckout/%s.php',PATH_PLUGINS,$billing->fields['checkout_plugin']);

				if (is_file($plugin_file)) {
					include_once($plugin_file);

					eval(sprintf('$PLG = new plg_chout_%s("%s");',$billing->fields['checkout_plugin'],$billing->fields['checkout_plugin_id']));
					if (is_callable(array($PLG,'refund')))
						$PLG->refund($this->getRecord(),$billing->fields,$VAR['amount']);
				}
			}
		}

		# Redirect
		if (! empty($VAR['redirect'])) {
			printf('",key($this->invoice));

		} else {
			echo _('No due invoices selected for payment.');
		}
	}

	/**
	 * Make a payment now
	 */
	public function checkoutnow($VAR) {
		global $C_translate,$smarty,$C_list,$VAR;

		# Validate user logged in:
		if (SESS_LOGGED != '1') {
			echo '';
			return false;
		}

		# If the ID is blank, this will get all unpaid invoices.
		if (! isset($VAR['invoice_id']))
			return false;

		# Some defaults
		$recur_amt = 0;

		$db = &DB();
		if(preg_match('/^MULTI-/',@$VAR['invoice_id'])) {
			# Get multi-invoice details
			$total = $this->multiple_invoice_total($VAR['invoice_id'],SESS_ACCOUNT);
			if (! $total)
				return false;

			$recur_arr = false;
			$account_id = SESS_ACCOUNT;
			$this->invoice_id = $VAR['invoice_id'];
			$CURRENCY = DEFAULT_CURRENCY;
			$multi = true;

		} else {
			# Validate the invoice selected, & get the totals:
			$result = $db->Execute($q=sqlSelect($db,'invoice','*',array('id'=>$VAR['invoice_id'])));
			if (! $result || $result->RecordCount() == 0)
				return false;

			# Determine the price & currency
			if ($result->fields['billed_currency_id'] != $result->fields['actual_billed_currency_id']) {
				global $C_list;

				$CURRENCY = $result->fields['actual_billed_currency_id'];
				if($result->fields['billed_amt'] <= 0)
					$total = $C_list->format_currency_decimal($result->fields['total_amt'],$CURRENCY);
				else
					$total = $C_list->format_currency_decimal($result->fields['total_amt'],$CURRENCY)-$result->fields['actual_billed_amt'];

			} else {
				$CURRENCY = $result->fields['billed_currency_id'];
				$total = $result->fields['total_amt']-$result->fields['billed_amt'];
			}

			if ($result->fields['recur_amt'] > 0)
				$recur_amt = $C_list->format_currency_decimal($result->fields['recur_amt'],$CURRENCY);

			@$recur_arr = unserialize($result->fields['recur_arr']);
			$account_id = $result->fields['account_id'];
			$this->invoice_id = $result->fields['id'];
			$this->invoice[$result->fields['id']] = $total;
			$multi = false;
		}
		$amount = round($total, 2);

		# Get the account details:
		$sql    = 'SELECT * FROM ' . AGILE_DB_PREFIX . 'account WHERE site_id = ' . $db->qstr(DEFAULT_SITE) . ' AND id = ' . $db->qstr($account_id);
		$account = $db->Execute($sql);
		if (!$account || !$account->RecordCount()) return false;

		# Validate checkout option selected is allowed for purchase:
		$q  = "SELECT * FROM ".AGILE_DB_PREFIX."checkout WHERE site_id = ".$db->qstr(DEFAULT_SITE)." AND id = ".$db->qstr(@$VAR['option'])." AND active = 1 AND ";
		if($recur_amt>0 && @$billed_amt == 0) $q .= "allow_recurring = 1 "; else $q .= "allow_new = 1 ";
		$chopt = $db->Execute($q);
		if (!$chopt || !$chopt->RecordCount()) return false;
		if($chopt && $chopt->RecordCount()) {
			$show = true;
			if ( @$chopt->fields["total_maximum"] != "" && $total > $chopt->fields["total_maximum"] )   $show = false;
			if ( @$chopt->fields["total_miniumum"] != "" && $total < $chopt->fields["total_miniumum"] ) $show = false;
		}
		if(!$show) {
			echo ' ';
			return false;
		}

		# Load the checkout plugin:
		$plugin_file = PATH_PLUGINS . 'checkout/'. $chopt->fields["checkout_plugin"] . '.php';
		include_once ( $plugin_file );
		eval ( '$PLG = new plg_chout_' .   $chopt->fields["checkout_plugin"] . '("'.@$VAR["option"].'",$multi);');

		if(!empty($VAR['account_billing_id']) && @$VAR['new_card']==2) {
			/* validate credit card on file details */
			$account_billing_id=$VAR['account_billing_id'];
			if(!$PLG->setBillingFromDB($account_id, $account_billing_id, $VAR['option'])) {
				global $C_debug;
				$C_debug->alert("Sorry, we cannot use that billing record for this purchase.");
				return false;
			}
		} else {
			/* use passed in vars */
			$PLG->setBillingFromParams($VAR);
		}

		# Set Invoice Vars:
		$this->total_amt					= $amount;
		$this->currency_iso 				= $C_list->currency_iso($CURRENCY);
		$this->currency_iso_admin			= $C_list->currency_iso($CURRENCY);
		$this->account_id					= $account_id;
		$this->actual_billed_currency_id	= $CURRENCY;
		$this->billed_currency_id			= $CURRENCY;
		$this->checkout_plugin_id           = @$VAR["option"];

		# Run the plugin bill_checkout() method:
		$this->checkout_plugin_data = $PLG->bill_checkout($amount, $this->invoice_id, $this->currency_iso, $account->fields, $recur_amt, $recur_arr,$this->invoice);

		# redirect
		if(!empty($this->checkout_plugin_data['redirect'])) echo $this->checkout_plugin_data['redirect'];

		# determine results
		if( $this->checkout_plugin_data === false ) {
			if(!empty($PLG->redirect)) echo $PLG->redirect;
			return false;
		} elseif ($PLG->type == "gateway" && empty($PLG->redirect)) {
			if(empty($this->admin_checkout)) {
				$VAR['_page'] = "invoice:thankyou";
			} else {
				$VAR['_page'] = "invoice:view";
			}
		} elseif ($PLG->type == "redirect") {

			echo "
Please wait while we redirect you to the secure payment site.... {$PLG->redirect}
"; } # Call the Plugin method for storing the checkout data, if new data entered: $this->account_billing_id = $PLG->store_billing($VAR, $PLG); # Load the email template module include_once(PATH_MODULES.'email_template/email_template.inc.php'); $mail = new email_template; # Update billing details for this invoice, if realtime billing succeeded: if($PLG->type == 'gateway' || $amount == 0) { $q = "UPDATE ".AGILE_DB_PREFIX."invoice SET account_billing_id = " .$db->qstr($this->account_billing_id). ", billing_status = " .$db->qstr(1). ", billed_amt = " .$db->qstr($total). ", actual_billed_amt = " .$db->qstr($amount). ", date_last = " .$db->qstr(time()). ", checkout_plugin_id = " .$db->qstr($this->checkout_plugin_id) .", checkout_plugin_data = " .$db->qstr(serialize($this->checkout_plugin_data)). " WHERE site_id = ".$db->qstr(DEFAULT_SITE)." AND id = ".$db->qstr($this->invoice_id); $rst = $db->Execute($q); if ($rst === false) { global $C_debug; $C_debug->error(__FILE__,__METHOD__,$db->ErrorMsg()); return false; } // loop through each invoice paid foreach($this->invoice as $this->invoice_id) { # Send billed e-mail notice to user $email = new email_template; $email->send('invoice_paid_user', $this->account_id, $this->invoice_id, $this->currency_iso, ''); # Admin alert of payment processed $email = new email_template; $email->send('admin->invoice_paid_admin', $this->account_id, $this->invoice_id, $this->currency_iso_admin, ''); # Submit the invoice for approval $arr['id'] = $this->invoice_id; $this->pApprove($this->invoice_id); } } else { # Just update the last_date and plugin data $q = "UPDATE ".AGILE_DB_PREFIX."invoice SET account_billing_id = " .$db->qstr($this->account_billing_id). ", date_last = " .$db->qstr(time()). ", checkout_plugin_id = " .$db->qstr($this->checkout_plugin_id) .", checkout_plugin_data = " .$db->qstr(serialize($this->checkout_plugin_data)). " WHERE site_id = ".$db->qstr(DEFAULT_SITE)." AND id = ".$db->qstr($this->invoice_id); $rst = $db->Execute($q); if ($rst === false) { global $C_debug; $C_debug->error(__FILE__,__METHOD__,$db->ErrorMsg()); return false; } # Admin e-mail alert of manual payment processing if ( $PLG->getName() == 'MANUAL' ) { $date_due = $C_list->date(time()); foreach($this->invoice as $this->invoice_id) { $email = new email_template; $email->send('admin->invoice_due_admin', $this->account_id, $this->invoice_id, '', $date_due); } global $C_debug; $C_debug->alert($C_translate->translate('manual_alert','checkout')); } } } /** GENERIC INVOICE METHODS **/ /** * Return a list of accounts with their current outsanding invoices balance * * @return array List of Accounts and their current balance */ public function sAccountsBal() { static $sAccountBal = array(); if (! count($sAccountBal)) { $db = &DB(); $rs = $db->Execute(sqlSelect($db,'invoice','account_id,ROUND(SUM(total_amt-billed_amt-IFNULL(credit_amt,0)),2) AS balance',false,'account_id','','','account_id')); if ($rs && $rs->RecordCount()) { while (! $rs->EOF) { $sAccountBal[$rs->fields['account_id']] = $rs->fields['balance']; $rs->MoveNext(); } } } return $sAccountBal; } /** * Get a list of invoices with a non-zero balance * * @param $account_id Get the invoices for this account, otherwise all invoices are returned. * @param $refresh If true, force re-reading database to get the list of invoices. * @return array List of invoices with balance */ public function sInvoicesBal($account_id=null,$refresh=false) { static $sInvoicesBal = array(); if ($refresh || ! count($sInvoicesBal)) { $sInvoicesBal = array(); $db = &DB(); $rs = $db->Execute(sqlSelect('invoice','date_orig,account_id,id,total_amt,billed_amt,IFNULL(credit_amt,0) as credit_amt,ROUND(total_amt-billed_amt-IFNULL(credit_amt,0),2) as balance', array('where'=>'(refund_status=0 OR refund_status IS NULL) AND status=1 AND total_amt-billed_amt-IFNULL(credit_amt,0)!=0','orderby'=>'account_id,date_orig,id'))); if ($rs && $rs->RecordCount()) { while (! $rs->EOF) { $invoice = array(); $invoice['invoice_id'] = $rs->fields['id']; $invoice['balance'] = $rs->fields['balance']; $invoice['total_amt'] = $rs->fields['total_amt']; $invoice['billed_amt'] = $rs->fields['billed_amt']; $invoice['credit_amt'] = $rs->fields['credit_amt']; $invoice['date_orig'] = $rs->fields['date_orig']; $sInvoicesBal[$rs->fields['account_id']][$rs->fields['id']] = $invoice; $rs->MoveNext(); } } } return (is_null($account_id) ? $sInvoicesBal : isset($sInvoicesBal[$account_id]) ? $sInvoicesBal[$account_id] : array()); } /** * Get a list of invoices for an account. * * @param $account_id * @param $invoices Optional array of invoices to retrieve, otherwise all invoices are return. * @return array Requested invoices */ public function sInvoicesAcc($account_id,$invoices=array()) { static $sInvoicesAccount = array(); $return = array(); if (count($invoices)) { foreach ($invoices as $invoice) if (isset($sInvoicesAccount[$account_id][$invoice_id])) $return[$invoice_id] = $sInvoicesAccount[$account_id][$invoice_id]; } else { if (isset($sInvoicesAccount[$account_id])) $return = $sInvoicesAccount[$account_id]; } # Do we need to get the invoices from the DB? if ((! count($invoices) && ! count($return)) || count($invoices) != count($return)) { $db = &DB(); if (count($invoices)) $where = sprintf('AND id IN (%s)',join(',',$invoices)); else $where = ''; $rs = $db->Execute(sqlSelect('invoice','id,date_orig,total_amt,billed_amt,IFNULL(credit_amt,0) as credit_amt,ROUND(total_amt-billed_amt-IFNULL(credit_amt,0),2) AS balance',array('where'=>sprintf('account_id=%s %s',$account_id,$where),'orderby'=>'id'))); if ($rs && $rs->RecordCount()) { while (! $rs->EOF) { $sInvoicesAccount[$account_id][$rs->fields['id']] = $rs->fields; $return[$rs->fields['id']] = $rs->fields; $rs->MoveNext(); } } } return $return; } public function invoice_days() { return $this->sInvoiceDays(); } /** * Determine the number of days in advance an invoice should be generated. * Invoices are generated when the greater of: * + system default (setup:max_inv_gen_period), (to be deprecated) * + system default (setup_invoice:invoice_advance_gen), * * @return int Days in Advance to Issue Invoices. */ public function sInvoiceDays() { $db = &DB(); # Get the max invoice days from the setup_invoice table $days = 0; # First from the setup table. $setup = $db->Execute(sqlSelect($db,'setup','max_inv_gen_period','')); if (isset($setup->fields['max_inv_gen_period'])) $days = $setup->fields['max_inv_gen_period']; # Then from the setup_invoice table. $setup = $db->Execute(sqlSelect($db,'setup_invoice','invoice_advance_gen,advance_notice','')); if (isset($setup->fields['invoice_advance_gen']) && $setup->fields['invoice_advance_gen'] > $days) $days = $setup->fields['invoice_advance_gen']; if (isset($setup->fields['advance_notice']) && $setup->fields['advance_notice'] > $days) $days = $setup->fields['advance_notice']; return $days; } } ?>