private static function save_job_tasks($job_id, $data) { $result = array('status' => false); $check_completed = false; $job_data = false; $job_task_creation_permissions = self::get_job_task_creation_permissions(); // check for new tasks or changed tasks. $tasks = self::get_tasks($job_id); if (isset($data['job_task']) && is_array($data['job_task'])) { foreach ($data['job_task'] as $task_id => $task_data) { if (isset($task_data['manual_percent']) && strlen($task_data['manual_percent']) == 0) { unset($task_data['manual_percent']); } $original_task_id = $task_id; $task_id = (int) $task_id; if (!is_array($task_data)) { continue; } if ($task_id > 0 && !isset($tasks[$task_id])) { $task_id = 0; // creating a new task on this job. } if (!isset($task_data['description']) || $task_data['description'] == '' || $task_data['description'] == _TASK_DELETE_KEY) { if ($task_id > 0 && $task_data['description'] == _TASK_DELETE_KEY) { // remove task. // but onyl remove it if it hasn't been invoiced. if (isset($tasks[$task_id]) && $tasks[$task_id]['invoiced']) { // it has been invoiced! dont remove it. set_error('Unable to remove an invoiced task'); $result['status'] = 'error'; break; // break out of loop saving tasks. } else { $sql = "DELETE FROM `" . _DB_PREFIX . "task` WHERE task_id = '{$task_id}' AND job_id = {$job_id} LIMIT 1"; query($sql); $sql = "DELETE FROM `" . _DB_PREFIX . "task_log` WHERE task_id = '{$task_id}'"; query($sql); $result['status'] = 'deleted'; $result['task_id'] = $task_id; } } continue; } // add / save this task. $task_data['job_id'] = $job_id; if (module_job::job_task_only_show_split_hours($job_id, $job_data, $task_id, $task_data)) { if (isset($task_data['hours']) && !isset($task_data['staff_hours'])) { $task_data['staff_hours'] = $task_data['hours']; $task_data['staff_amount'] = $task_data['amount']; } if (isset($task_data['hours'])) { unset($task_data['hours']); unset($task_data['amount']); } } if (isset($task_data['hours'])) { $task_data['hours'] = function_exists('decimal_time_in') ? decimal_time_in($task_data['hours']) : $task_data['hours']; } if (isset($task_data['staff_hours'])) { $task_data['staff_hours'] = function_exists('decimal_time_in') ? decimal_time_in($task_data['staff_hours']) : $task_data['staff_hours']; } if (isset($task_data['log_hours'])) { $task_data['log_hours'] = function_exists('decimal_time_in') ? decimal_time_in($task_data['log_hours']) : $task_data['log_hours']; } // remove the amount of it equals the hourly rate. if (isset($task_data['amount']) && $task_data['amount'] != 0 && isset($task_data['hours']) && $task_data['hours'] > 0) { if (isset($data['hourly_rate']) && $task_data['amount'] - $task_data['hours'] * $data['hourly_rate'] == 0) { unset($task_data['amount']); } } if (isset($task_data['staff_amount']) && $task_data['staff_amount'] != 0 && isset($task_data['staff_hours']) && $task_data['staff_hours'] > 0) { if (isset($data['staff_hourly_rate']) && $task_data['staff_amount'] - $task_data['staff_hours'] * $data['staff_hourly_rate'] == 0) { unset($task_data['staff_amount']); } } // check if we haven't unticked a non-hourly task if (isset($task_data['fully_completed_t']) && $task_data['fully_completed_t']) { if (!isset($task_data['fully_completed']) || !$task_data['fully_completed']) { // we have unchecked that tickbox $task_data['fully_completed'] = 0; } else { if (isset($tasks[$task_id]) && !$tasks[$task_id]['fully_completed']) { // we completed a preveiously incomplete task. // chekc if this task has a custom percentage filled in, we remove this custom percentage. if (isset($task_data['manual_percent']) && $task_data['manual_percent'] >= 0) { $task_data['manual_percent'] = -1; } // hack: if we haven't logged any hours for this, we log the number of hours. // if we have logged some hours already then we don't log anything extra. // this is so they can log 0.5hours for a 1 hour completed task etc.. if (isset($task_data['hours']) && $task_data['hours'] > 0 && (!isset($task_data['log_hours']) || !$task_data['log_hours'])) { $logged_hours = 0; foreach (get_multiple('task_log', array('job_id' => $job_id, 'task_id' => $task_id)) as $task_log) { $logged_hours += $task_log['hours']; } if ($logged_hours == 0) { $task_data['log_hours'] = $task_data['hours']; } } } } $check_completed = true; } // check if we haven't unticked a billable task if (isset($task_data['billable_t']) && $task_data['billable_t'] && !isset($task_data['billable'])) { $task_data['billable'] = 0; } // set default taxable status if (!$task_id && !isset($task_data['taxable_t'])) { // we're creating a new task. $task_data['taxable'] = module_config::c('task_taxable_default', 1); } if (isset($task_data['taxable_t']) && $task_data['taxable_t'] && !isset($task_data['taxable'])) { $task_data['taxable'] = 0; } if (isset($task_data['completed']) && $task_data['completed'] > 0) { // check the completed date of all our tasks. $check_completed = true; } if (!$task_id && isset($task_data['new_fully_completed']) && $task_data['new_fully_completed']) { $task_data['fully_completed'] = 1; // is this bad for set amount tasks? if (!isset($task_data['date_done']) || !$task_data['date_done']) { $task_data['date_done'] = print_date(time()); } if (isset($task_data['hours'])) { $task_data['log_hours'] = $task_data['hours']; } $check_completed = true; } // todo: move the task creation code into a public method so that the public user can add tasks to their jobs. if (!$task_id && module_security::is_logged_in() && !module_job::can_i('create', 'Job Tasks')) { continue; // dont allow new tasks. } // check if the user is allowed to create new tasks. // check the approval status of jobs switch ($job_task_creation_permissions) { case _JOB_TASK_CREATION_NOT_ALLOWED: if (!$task_id) { continue; // dont allow new tasks. } break; case _JOB_TASK_CREATION_REQUIRES_APPROVAL: $task_data['approval_required'] = 1; break; case _JOB_TASK_CREATION_WITHOUT_APPROVAL: // no action required . break; } if (isset($tasks[$task_id]) && $tasks[$task_id]['approval_required'] == 2) { // task has been rejected, saving it again for approval. $task_data['approval_required'] = 1; } $task_id = update_insert('task_id', $task_id, 'task', $task_data); // todo - fix cross task job boundary issue. meh. $result['task_id'] = $task_id; if ($task_id != $original_task_id) { $result['status'] = 'created'; } else { $result['status'] = 'edited'; } if ($task_id && isset($task_data['log_hours']) && (double) $task_data['log_hours'] > 0) { // we are increasing the task complete hours by the amount specified in log hours. // log a new task record, and incrase the "completed" column. //$original_task_data = $tasks[$task_id]; //$task_data['completed'] = $task_data['completed'] + $task_data['log_hours']; // only log hours if it's an hourly task. if (!isset($task_data['manual_task_type']) || $task_data['manual_task_type'] < 0) { if (!$job_data) { $job_data = self::get_job($job_id); } $task_data['manual_task_type'] = $job_data['default_task_type']; } if ($task_data['manual_task_type'] == _TASK_TYPE_HOURS_AMOUNT) { update_insert('task_log_id', 'new', 'task_log', array('task_id' => $task_id, 'job_id' => $job_id, 'hours' => (double) $task_data['log_hours'], 'log_time' => time())); $result['log_hours'] = $task_data['log_hours']; } } } } if ($check_completed) { self::update_job_completion_status($job_id); } module_cache::clear('job'); return $result; }
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; }
private static function save_quote_tasks($quote_id, $data) { $result = array('status' => false); $check_completed = false; $quote_data = false; // check for new tasks or changed tasks. $tasks = self::get_tasks($quote_id); if (isset($data['quote_task']) && is_array($data['quote_task'])) { foreach ($data['quote_task'] as $quote_task_id => $task_data) { if (isset($task_data['manual_percent']) && strlen($task_data['manual_percent']) == 0) { unset($task_data['manual_percent']); } $original_quote_task_id = $quote_task_id; $quote_task_id = (int) $quote_task_id; if (!is_array($task_data)) { continue; } if ($quote_task_id > 0 && !isset($tasks[$quote_task_id])) { $quote_task_id = 0; // creating a new task on this quote. } if (!isset($task_data['description']) || $task_data['description'] == '' || $task_data['description'] == _TASK_DELETE_KEY) { if ($quote_task_id > 0 && $task_data['description'] == _TASK_DELETE_KEY) { // remove task. // but onyl remove it if it hasn't been invoiced. $sql = "DELETE FROM `" . _DB_PREFIX . "quote_task` WHERE quote_task_id = '{$quote_task_id}' AND quote_id = {$quote_id} LIMIT 1"; query($sql); $result['status'] = 'deleted'; $result['quote_task_id'] = $quote_task_id; } continue; } // add / save this task. $task_data['quote_id'] = $quote_id; $task_data['hours'] = function_exists('decimal_time_in') ? decimal_time_in($task_data['hours']) : $task_data['hours']; // remove the amount of it equals the hourly rate. if (isset($task_data['amount']) && $task_data['amount'] != 0 && isset($task_data['hours']) && $task_data['hours'] > 0) { if (isset($data['hourly_rate']) && $task_data['amount'] - $task_data['hours'] * $data['hourly_rate'] == 0) { unset($task_data['amount']); } } // check if we haven't unticked a non-hourly task // check if we haven't unticked a billable task if (isset($task_data['billable_t']) && $task_data['billable_t'] && !isset($task_data['billable'])) { $task_data['billable'] = 0; } // set default taxable status if (!isset($task_data['taxable_t'])) { // we're creating a new task. $task_data['taxable'] = module_config::c('task_taxable_default', 1); } if (isset($task_data['taxable_t']) && $task_data['taxable_t'] && !isset($task_data['taxable'])) { $task_data['taxable'] = 0; } // todo: move the task creation code into a public method so that the public user can add tasks to their quotes. if (!$quote_task_id && module_security::is_logged_in() && !module_quote::can_i('create', 'Quote Tasks')) { continue; // dont allow new tasks. } // check if the user is allowed to create new tasks. $quote_task_id = update_insert('quote_task_id', $quote_task_id, 'quote_task', $task_data); // todo - fix cross task quote boundary issue. meh. $result['quote_task_id'] = $quote_task_id; if ($quote_task_id != $original_quote_task_id) { $result['status'] = 'created'; } else { $result['status'] = 'edited'; } } } if ($check_completed) { self::update_quote_completion_status($quote_id); } module_cache::clear('quote'); return $result; }