public static function save_invoice($invoice_id, $data) { if (!(int) $invoice_id && isset($data['job_id']) && $data['job_id']) { $linkedjob = module_job::get_job($data['job_id']); $data['currency_id'] = $linkedjob['currency_id']; $data['customer_id'] = $linkedjob['customer_id']; } if ($invoice_id) { // used when working out the hourly rate fix below $original_invoice_data = self::get_invoice($invoice_id); } else { $original_invoice_data = 0; } $invoice_id = update_insert("invoice_id", $invoice_id, "invoice", $data); if ($invoice_id) { module_cache::clear('invoice'); // save the invoice tax rates (copied to finance.php) if (isset($data['tax_ids']) && isset($data['tax_names']) && $data['tax_percents']) { $existing_taxes = get_multiple('invoice_tax', array('invoice_id' => $invoice_id), 'invoice_tax_id', 'exact', 'order'); $order = 1; foreach ($data['tax_ids'] as $key => $val) { if (isset($data['tax_percents'][$key]) && $data['tax_percents'][$key] == 0) { // we are not saving this particular tax item because it has a 0% tax rate } else { if ((int) $val > 0 && isset($existing_taxes[$val])) { // this means we are trying to update an existing record on the invoice_tax table, we confirm this id matches this invoice. $invoice_tax_id = $val; unset($existing_taxes[$invoice_tax_id]); // so we know which ones to remove from the end. } else { $invoice_tax_id = false; // create new record } $invoice_tax_data = array('invoice_id' => $invoice_id, 'percent' => isset($data['tax_percents'][$key]) ? $data['tax_percents'][$key] : 0, 'amount' => 0, 'name' => isset($data['tax_names'][$key]) ? $data['tax_names'][$key] : 'TAX', 'order' => $order++, 'increment' => isset($data['tax_increment_checkbox']) && $data['tax_increment_checkbox'] ? 1 : 0); $invoice_tax_id = update_insert('invoice_tax_id', $invoice_tax_id, 'invoice_tax', $invoice_tax_data); } } foreach ($existing_taxes as $existing_tax) { delete_from_db('invoice_tax', array('invoice_id', 'invoice_tax_id'), array($invoice_id, $existing_tax['invoice_tax_id'])); } } $invoice_data = self::get_invoice($invoice_id); if (!$invoice_data) { set_error('No permissions to access invoice.'); return $invoice_id; } // check for new invoice_items or changed invoice_items. $invoice_items = self::get_invoice_items($invoice_id, $invoice_data); if (isset($data['invoice_invoice_item']) && is_array($data['invoice_invoice_item'])) { foreach ($data['invoice_invoice_item'] as $invoice_item_id => $invoice_item_data) { $invoice_item_id = (int) $invoice_item_id; if (!is_array($invoice_item_data)) { continue; } if ($invoice_item_id > 0 && !isset($invoice_items[$invoice_item_id])) { continue; } // wrong invoice_item save - will never happen. if (!isset($invoice_item_data['description']) || $invoice_item_data['description'] == '') { if ($invoice_item_id > 0) { // remove invoice_item. $sql = "DELETE FROM `" . _DB_PREFIX . "invoice_item` WHERE invoice_item_id = '{$invoice_item_id}' AND invoice_id = {$invoice_id} LIMIT 1"; query($sql); } continue; } // add / save this invoice_item. $invoice_item_data['invoice_id'] = $invoice_id; // what type of task is this? $invoice_task_type = isset($invoice_item_data['manual_task_type']) && $invoice_item_data['manual_task_type'] >= 0 ? $invoice_item_data['manual_task_type'] : $invoice_data['default_task_type']; $invoice_item_data['hours_mins'] = 0; if (isset($invoice_item_data['hours']) && $invoice_task_type == _TASK_TYPE_HOURS_AMOUNT) { } if (isset($invoice_item_data['hours']) && $invoice_task_type == _TASK_TYPE_HOURS_AMOUNT && function_exists('decimal_time_in')) { $invoice_item_data['hours'] = decimal_time_in($invoice_item_data['hours']); if (strpos($invoice_item_data['hours'], ':') !== false) { $invoice_item_data['hours_mins'] = str_replace(":", ".", $invoice_item_data['hours']); } } else { if (isset($invoice_item_data['hours']) && strlen($invoice_item_data['hours'])) { $invoice_item_data['hours'] = number_in($invoice_item_data['hours']); } else { $invoice_item_data['hours'] = 0; } } // number formatting //print_r($invoice_item_data); if (isset($invoice_item_data['hourly_rate']) && strlen($invoice_item_data['hourly_rate'])) { $invoice_item_data['hourly_rate'] = number_in($invoice_item_data['hourly_rate'], module_config::c('task_amount_decimal_places', -1)); } //print_r($invoice_item_data);exit; // somenew hacks here to support out new method of creating an item. // the 'amount' column is never edited any more // this column is now always automatically calculated based on // 'hours' and 'hourly_rate' if (!isset($invoice_item_data['amount'])) { if ($invoice_task_type == _TASK_TYPE_AMOUNT_ONLY) { // ignore the quantity field all together. $invoice_item_data['amount'] = $invoice_item_data['hourly_rate']; $invoice_item_data['hourly_rate'] = 0; } else { if (isset($invoice_item_data['hourly_rate']) && strlen($invoice_item_data['hourly_rate']) > 0) { // if we have inputted an hourly rate (ie: not left empty) if (isset($invoice_item_data['hours']) && strlen($invoice_item_data['hours']) == 0) { // no hours entered (eg: empty) so we treat whatever was in 'hourly_rate' as the amount $invoice_item_data['amount'] = $invoice_item_data['hourly_rate']; } else { if (isset($invoice_item_data['hours']) && strlen($invoice_item_data['hours']) > 0) { // hours inputted, along with hourly rate. work out the new amount. $invoice_item_data['amount'] = round($invoice_item_data['hours'] * $invoice_item_data['hourly_rate'], module_config::c('currency_decimal_places', 2)); } } } } } if ($invoice_task_type == _TASK_TYPE_HOURS_AMOUNT) { if ($invoice_item_data['hourly_rate'] == $invoice_data['hourly_rate'] || isset($original_invoice_data['hourly_rate']) && $invoice_item_data['hourly_rate'] == $original_invoice_data['hourly_rate']) { $invoice_item_data['hourly_rate'] = -1; } } // remove the amount of it equals the hourly rate. /*if(isset($invoice_item_data['amount']) && isset($invoice_item_data['hours']) && $invoice_item_data['amount'] > 0 && $invoice_item_data['hours'] > 0){ if($invoice_item_data['amount'] - ($invoice_item_data['hours'] * $data['hourly_rate']) == 0){ unset($invoice_item_data['amount']); } }*/ // check if we haven't unticked a non-hourly invoice_item /*if(isset($invoice_item_data['completed_t']) && $invoice_item_data['completed_t'] && !isset($invoice_item_data['completed'])){ $invoice_item_data['completed'] = 0; }*/ if (!isset($invoice_item_data['taxable_t'])) { $invoice_item_data['taxable'] = module_config::c('task_taxable_default', 1); } else { if (isset($invoice_item_data['taxable_t']) && $invoice_item_data['taxable_t'] && !isset($invoice_item_data['taxable'])) { $invoice_item_data['taxable'] = 0; } } if (!strlen($invoice_item_data['hours'])) { $invoice_item_data['hours'] = 0; } $invoice_item_data['hourly_rate'] = number_out($invoice_item_data['hourly_rate'], false, module_config::c('task_amount_decimal_places', -1)); $invoice_item_data['hours'] = number_out($invoice_item_data['hours']); $invoice_item_data['amount'] = number_out($invoice_item_data['amount']); update_insert('invoice_item_id', $invoice_item_id, 'invoice_item', $invoice_item_data); } } $last_payment_time = 0; if (isset($data['invoice_invoice_payment']) && is_array($data['invoice_invoice_payment'])) { foreach ($data['invoice_invoice_payment'] as $invoice_payment_id => $invoice_payment_data) { $invoice_payment_id = (int) $invoice_payment_id; if (!is_array($invoice_payment_data)) { continue; } if (isset($invoice_payment_data['amount'])) { $invoice_payment_data['amount'] = number_in($invoice_payment_data['amount']); // toggle between 'normal' and 'refund' payment types if (isset($invoice_payment_data['payment_type'])) { if ($invoice_payment_data['amount'] < 0 && $invoice_payment_data['payment_type'] == _INVOICE_PAYMENT_TYPE_NORMAL) { // this is a refund. $invoice_payment_data['payment_type'] = _INVOICE_PAYMENT_TYPE_REFUND; } else { if ($invoice_payment_data['payment_type'] == _INVOICE_PAYMENT_TYPE_REFUND) { $invoice_payment_data['payment_type'] = _INVOICE_PAYMENT_TYPE_NORMAL; } } } } // check this invoice payment actually matches this invoice. $invoice_payment_data_existing = false; if ($invoice_payment_id > 0) { $invoice_payment_data_existing = get_single('invoice_payment', array('invoice_payment_id', 'invoice_id'), array($invoice_payment_id, $invoice_id)); if (!$invoice_payment_data_existing || $invoice_payment_data_existing['invoice_payment_id'] != $invoice_payment_id || $invoice_payment_data_existing['invoice_id'] != $invoice_id) { $invoice_payment_id = 0; $invoice_payment_data_existing = false; } } if (!isset($invoice_payment_data['amount']) || $invoice_payment_data['amount'] == '' || $invoice_payment_data['amount'] == 0) { // || $invoice_payment_data['amount'] <= 0 if ($invoice_payment_id > 0) { // if this is a customer credit payment, return that back to the customer account. if ($invoice_payment_data_existing && $invoice_data['customer_id']) { switch ($invoice_payment_data_existing['payment_type']) { case _INVOICE_PAYMENT_TYPE_CREDIT: module_customer::add_credit($invoice_data['customer_id'], $invoice_payment_data_existing['amount'], 'Refunded credit from invoice payment'); break; } } // remove invoice_payment. $sql = "DELETE FROM `" . _DB_PREFIX . "invoice_payment` WHERE invoice_payment_id = '{$invoice_payment_id}' AND invoice_id = {$invoice_id} LIMIT 1"; query($sql); // delete any existing transactions from the system as well. hook_handle_callback('invoice_payment_deleted', $invoice_payment_id, $invoice_id); } continue; } if (!$invoice_payment_id && (!isset($_REQUEST['add_payment']) || $_REQUEST['add_payment'] != 'go')) { continue; // not saving a new one. } // add / save this invoice_payment. $invoice_payment_data['invoice_id'] = $invoice_id; // $invoice_payment_data['currency_id'] = $invoice_data['currency_id']; $last_payment_time = max($last_payment_time, strtotime(input_date($invoice_payment_data['date_paid']))); if (isset($invoice_payment_data['custom_notes'])) { $details = @unserialize($invoice_payment_data['data']); if (!is_array($details)) { $details = array(); } $details['custom_notes'] = $invoice_payment_data['custom_notes']; $invoice_payment_data['data'] = serialize($details); } $invoice_payment_data['amount'] = number_out($invoice_payment_data['amount']); update_insert('invoice_payment_id', $invoice_payment_id, 'invoice_payment', $invoice_payment_data); } } if (!$last_payment_time) { $last_payment_time = strtotime(date('Y-m-d')); } // check if the invoice has been paid module_cache::clear('invoice'); //module_cache::clear_cache(); // this helps fix the bug where part payments are not caulcated a correct paid date. $invoice_data = self::get_invoice($invoice_id); if (!$invoice_data) { set_error('No permissions to access invoice.'); return $invoice_id; } if ((!$invoice_data['date_paid'] || $invoice_data['date_paid'] == '0000-00-00') && $invoice_data['total_amount_due'] <= 0 && ($invoice_data['total_amount_paid'] > 0 || $invoice_data['discount_amount'] > 0) && (!$invoice_data['date_cancel'] || $invoice_data['date_cancel'] == '0000-00-00')) { // find the date of the last payment history. // if the sent date is null also update that. $date_sent = $invoice_data['date_sent']; if (!$date_sent || $date_sent == '0000-00-00') { $date_sent = date('Y-m-d', $last_payment_time); } update_insert("invoice_id", $invoice_id, "invoice", array('date_paid' => date('Y-m-d', $last_payment_time), 'date_sent' => $date_sent, 'status' => _l('Paid'))); // hook for our ticketing plugin to mark a priority support ticket as paid. // or anything else down the track. module_cache::clear('invoice'); handle_hook('invoice_paid', $invoice_id); if (module_config::c('invoice_automatic_receipt', 1)) { // send receipt to customer. self::email_invoice_to_customer($invoice_id); } } if ($invoice_data['total_amount_due'] > 0) { // update the status to unpaid. update_insert("invoice_id", $invoice_id, "invoice", array('date_paid' => '', 'status' => $invoice_data['status'] == _l('Paid') ? module_config::s('invoice_status_default', 'New') : $invoice_data['status'])); } if (class_exists('module_extra', false) && module_extra::is_plugin_enabled()) { module_extra::save_extras('invoice', 'invoice_id', $invoice_id); } if ($invoice_data['customer_id']) { //module_cache::clear_cache(); module_cache::clear('invoice'); module_customer::update_customer_status($invoice_data['customer_id']); } hook_handle_callback('invoice_saved', $invoice_id, $invoice_data); } module_cache::clear('invoice'); module_cache::clear('job'); return $invoice_id; }