Server IP : 192.185.129.71 / Your IP : 18.191.236.193 Web Server : Apache System : Linux bh-ht-3.webhostbox.net 4.19.286-203.ELK.el7.x86_64 #1 SMP Wed Jun 14 04:33:55 CDT 2023 x86_64 User : svymadmin ( 4072) PHP Version : 7.4.33 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON Directory (0755) : /home4/svymadmin/public_html/vivekaexcel.edu.in/bkp/application/models/ |
[ Home ] | [ C0mmand ] | [ Upload File ] |
---|
<?php defined('BASEPATH') or exit('No direct script access allowed'); class Invoices_model extends App_Model { public const STATUS_UNPAID = 1; public const STATUS_PAID = 2; public const STATUS_PARTIALLY = 3; public const STATUS_OVERDUE = 4; public const STATUS_CANCELLED = 5; public const STATUS_DRAFT = 6; public const STATUS_DRAFT_NUMBER = 1000000000; private $statuses = [ self::STATUS_UNPAID, self::STATUS_PAID, self::STATUS_PARTIALLY, self::STATUS_OVERDUE, self::STATUS_CANCELLED, self::STATUS_DRAFT, ]; private $shipping_fields = [ 'shipping_street', 'shipping_city', 'shipping_city', 'shipping_state', 'shipping_zip', 'shipping_country', ]; public function __construct() { parent::__construct(); } public function get_statuses() { return $this->statuses; } public function get_sale_agents() { return $this->db->query('SELECT DISTINCT(sale_agent) as sale_agent, CONCAT(firstname, \' \', lastname) as full_name FROM ' . db_prefix() . 'invoices JOIN ' . db_prefix() . 'staff ON ' . db_prefix() . 'staff.staffid=' . db_prefix() . 'invoices.sale_agent WHERE sale_agent != 0')->result_array(); } public function get_unpaid_invoices() { if (!staff_can('view', 'invoices')) { $where = get_invoices_where_sql_for_staff(get_staff_user_id()); $this->db->where($where); } $this->db->select(db_prefix() . 'invoices.*,' . get_sql_select_client_company()); $this->db->join(db_prefix() . 'clients', db_prefix() . 'invoices.clientid=' . db_prefix() . 'clients.userid', 'left'); $this->db->where_not_in('status', [self::STATUS_CANCELLED, self::STATUS_PAID]); $this->db->where('total >', 0); $this->db->order_by('number,YEAR(date)', 'desc'); $invoices = $this->db->get(db_prefix() . 'invoices')->result(); if (!class_exists('payment_modes_model', false)) { $this->load->model('payment_modes_model'); } return array_map(function ($invoice) { $allowedModes = []; foreach (unserialize($invoice->allowed_payment_modes) as $modeId) { $allowedModes[] = $this->payment_modes_model->get($modeId); } $invoice->allowed_payment_modes = $allowedModes; $invoice->total_left_to_pay = get_invoice_total_left_to_pay($invoice->id, $invoice->total); return $invoice; }, $invoices); } /** * Get invoice by id * @param mixed $id * @return array|object */ public function get($id = '', $where = []) { $this->db->select('*, ' . db_prefix() . 'currencies.id as currencyid, ' . db_prefix() . 'invoices.id as id, ' . db_prefix() . 'currencies.name as currency_name'); $this->db->from(db_prefix() . 'invoices'); $this->db->join(db_prefix() . 'currencies', '' . db_prefix() . 'currencies.id = ' . db_prefix() . 'invoices.currency', 'left'); $this->db->where($where); if (is_numeric($id)) { $this->db->where(db_prefix() . 'invoices' . '.id', $id); $invoice = $this->db->get()->row(); if ($invoice) { $invoice->total_left_to_pay = get_invoice_total_left_to_pay($invoice->id, $invoice->total); $invoice->items = get_items_by_type('invoice', $id); $invoice->attachments = $this->get_attachments($id); if ($invoice->project_id != 0) { $this->load->model('projects_model'); $invoice->project_data = $this->projects_model->get($invoice->project_id); } $invoice->visible_attachments_to_customer_found = false; foreach ($invoice->attachments as $attachment) { if ($attachment['visible_to_customer'] == 1) { $invoice->visible_attachments_to_customer_found = true; break; } } $client = $this->clients_model->get($invoice->clientid); $invoice->client = $client; if (!$invoice->client) { $invoice->client = new stdClass(); $invoice->client->company = $invoice->deleted_customer_name; } $this->load->model('payments_model'); $invoice->payments = $this->payments_model->get_invoice_payments($id); $this->load->model('email_schedule_model'); $invoice->scheduled_email = $this->email_schedule_model->get($id, 'invoice'); } return hooks()->apply_filters('get_invoice', $invoice); } $this->db->order_by('number,YEAR(date)', 'desc'); return $this->db->get()->result_array(); } public function get_invoice_item($id) { $this->db->where('id', $id); return $this->db->get(db_prefix() . 'itemable')->row(); } public function mark_as_cancelled($id) { $isDraft = $this->is_draft($id); $this->db->where('id', $id); $this->db->update(db_prefix() . 'invoices', [ 'status' => self::STATUS_CANCELLED, 'sent' => 1, ]); if ($this->db->affected_rows() > 0) { if ($isDraft) { $this->change_invoice_number_when_status_draft($id); } $this->log_invoice_activity($id, 'invoice_activity_marked_as_cancelled'); hooks()->do_action('invoice_marked_as_cancelled', $id); return true; } return false; } public function unmark_as_cancelled($id) { $this->db->where('id', $id); $this->db->update(db_prefix() . 'invoices', [ 'status' => self::STATUS_UNPAID, ]); if ($this->db->affected_rows() > 0) { $this->log_invoice_activity($id, 'invoice_activity_unmarked_as_cancelled'); hooks()->do_action('invoice_unmarked_as_cancelled', $id); return true; } return false; } /** * Get this invoice generated recurring invoices * @since Version 1.0.1 * @param mixed $id main invoice id * @return array */ public function get_invoice_recurring_invoices($id) { $this->db->select('id'); $this->db->where('is_recurring_from', $id); $invoices = $this->db->get(db_prefix() . 'invoices')->result_array(); $recurring_invoices = []; foreach ($invoices as $invoice) { $recurring_invoices[] = $this->get($invoice['id']); } return $recurring_invoices; } /** * Get invoice total from all statuses * @since Version 1.0.2 * @param mixed $data $_POST data * @return array */ public function get_invoices_total($data) { $this->load->model('currencies_model'); if (isset($data['currency'])) { $currencyid = $data['currency']; } elseif (isset($data['customer_id']) && $data['customer_id'] != '') { $currencyid = $this->clients_model->get_customer_default_currency($data['customer_id']); if ($currencyid == 0) { $currencyid = $this->currencies_model->get_base_currency()->id; } } elseif (isset($data['project_id']) && $data['project_id'] != '') { $this->load->model('projects_model'); $currencyid = $this->projects_model->get_currency($data['project_id'])->id; } else { $currencyid = $this->currencies_model->get_base_currency()->id; } $result = []; $result['due'] = []; $result['paid'] = []; $result['overdue'] = []; $has_permission_view = has_permission('invoices', '', 'view'); $has_permission_view_own = has_permission('invoices', '', 'view_own'); $allow_staff_view_invoices_assigned = get_option('allow_staff_view_invoices_assigned'); $noPermissionsQuery = get_invoices_where_sql_for_staff(get_staff_user_id()); for ($i = 1; $i <= 3; $i++) { $select = 'id,total'; if ($i == 1) { $select .= ', (SELECT total - (SELECT COALESCE(SUM(amount),0) FROM ' . db_prefix() . 'invoicepaymentrecords WHERE invoiceid = ' . db_prefix() . 'invoices.id) - (SELECT COALESCE(SUM(amount),0) FROM ' . db_prefix() . 'credits WHERE ' . db_prefix() . 'credits.invoice_id=' . db_prefix() . 'invoices.id)) as outstanding'; } elseif ($i == 2) { $select .= ',(SELECT SUM(amount) FROM ' . db_prefix() . 'invoicepaymentrecords WHERE invoiceid=' . db_prefix() . 'invoices.id) as total_paid'; } $this->db->select($select); $this->db->from(db_prefix() . 'invoices'); $this->db->where('currency', $currencyid); // Exclude cancelled invoices $this->db->where('status !=', self::STATUS_CANCELLED); // Exclude draft $this->db->where('status !=', self::STATUS_DRAFT); if (isset($data['project_id']) && $data['project_id'] != '') { $this->db->where('project_id', $data['project_id']); } elseif (isset($data['customer_id']) && $data['customer_id'] != '') { $this->db->where('clientid', $data['customer_id']); } if ($i == 3) { $this->db->where('status', self::STATUS_OVERDUE); } elseif ($i == 1) { $this->db->where('status !=', self::STATUS_PAID); } if (isset($data['years']) && count($data['years']) > 0) { $this->db->where_in('YEAR(date)', $data['years']); } else { $this->db->where('YEAR(date)', date('Y')); } if (!$has_permission_view) { $whereUser = $noPermissionsQuery; $this->db->where('(' . $whereUser . ')'); } $invoices = $this->db->get()->result_array(); foreach ($invoices as $invoice) { if ($i == 1) { $result['due'][] = $invoice['outstanding']; } elseif ($i == 2) { $result['paid'][] = $invoice['total_paid']; } elseif ($i == 3) { $result['overdue'][] = $invoice['total']; } } } $currency = get_currency($currencyid); $result['due'] = array_sum($result['due']); $result['paid'] = array_sum($result['paid']); $result['overdue'] = array_sum($result['overdue']); $result['currency'] = $currency; $result['currencyid'] = $currencyid; return $result; } /** * Insert new invoice to database * @param array $data invoice data * @return mixed - false if not insert, invoice ID if succes */ public function add($data, $expense = false) { $data['prefix'] = get_option('invoice_prefix'); $data['number_format'] = get_option('invoice_number_format'); $data['datecreated'] = date('Y-m-d H:i:s'); $save_and_send = isset($data['save_and_send']); $data['addedfrom'] = !DEFINED('CRON') ? get_staff_user_id() : 0; $data['cancel_overdue_reminders'] = isset($data['cancel_overdue_reminders']) ? 1 : 0; $data['allowed_payment_modes'] = isset($data['allowed_payment_modes']) ? serialize($data['allowed_payment_modes']) : serialize([]); $billed_tasks = isset($data['billed_tasks']) ? array_map('unserialize', array_unique(array_map('serialize', $data['billed_tasks']))) : []; $billed_expenses = isset($data['billed_expenses']) ? array_map('unserialize', array_unique(array_map('serialize', $data['billed_expenses']))) : []; $invoices_to_merge = isset($data['invoices_to_merge']) ? $data['invoices_to_merge'] : []; $cancel_merged_invoices = isset($data['cancel_merged_invoices']); $tags = isset($data['tags']) ? $data['tags'] : ''; if (isset($data['save_as_draft'])) { $data['status'] = self::STATUS_DRAFT; unset($data['save_as_draft']); } elseif (isset($data['save_and_send_later'])) { $data['status'] = self::STATUS_DRAFT; unset($data['save_and_send_later']); } if (isset($data['recurring'])) { if ($data['recurring'] == 'custom') { $data['recurring_type'] = $data['repeat_type_custom']; $data['custom_recurring'] = 1; $data['recurring'] = $data['repeat_every_custom']; } } else { $data['custom_recurring'] = 0; $data['recurring'] = 0; } if (isset($data['custom_fields'])) { $custom_fields = $data['custom_fields']; unset($data['custom_fields']); } $data['hash'] = app_generate_hash(); $items = []; if (isset($data['newitems'])) { $items = $data['newitems']; unset($data['newitems']); } $data = $this->map_shipping_columns($data, $expense); if (isset($data['shipping_street'])) { $data['shipping_street'] = trim($data['shipping_street']); $data['shipping_street'] = nl2br($data['shipping_street']); } $data['billing_street'] = trim($data['billing_street']); $data['billing_street'] = nl2br($data['billing_street']); if (isset($data['status']) && $data['status'] == self::STATUS_DRAFT) { $data['number'] = self::STATUS_DRAFT_NUMBER; } $data['duedate'] = isset($data['duedate']) && empty($data['duedate']) ? null : $data['duedate']; $hook = hooks()->apply_filters('before_invoice_added', [ 'data' => $data, 'items' => $items, ]); $data = $hook['data']; $items = $hook['items']; $this->db->insert(db_prefix() . 'invoices', $data); $insert_id = $this->db->insert_id(); if ($insert_id) { if (isset($custom_fields)) { handle_custom_fields_post($insert_id, $custom_fields); } handle_tags_save($tags, $insert_id, 'invoice'); foreach ($invoices_to_merge as $m) { $merged = false; $or_merge = $this->get($m); if ($cancel_merged_invoices == false) { if ($this->delete($m, true)) { $merged = true; } } else { if ($this->mark_as_cancelled($m)) { $merged = true; $admin_note = $or_merge->adminnote; $note = 'Merged into invoice ' . format_invoice_number($insert_id); if ($admin_note != '') { $admin_note .= "\n\r" . $note; } else { $admin_note = $note; } $this->db->where('id', $m); $this->db->update(db_prefix() . 'invoices', [ 'adminnote' => $admin_note, ]); // Delete the old items related from the merged invoice foreach ($or_merge->items as $or_merge_item) { $this->db->where('item_id', $or_merge_item['id']); $this->db->delete(db_prefix() . 'related_items'); } } } if ($merged) { $this->db->where('invoiceid', $or_merge->id); $is_expense_invoice = $this->db->get(db_prefix() . 'expenses')->row(); if ($is_expense_invoice) { $this->db->where('id', $is_expense_invoice->id); $this->db->update(db_prefix() . 'expenses', [ 'invoiceid' => $insert_id, ]); } if (total_rows(db_prefix() . 'estimates', [ 'invoiceid' => $or_merge->id, ]) > 0) { $this->db->where('invoiceid', $or_merge->id); $estimate = $this->db->get(db_prefix() . 'estimates')->row(); $this->db->where('id', $estimate->id); $this->db->update(db_prefix() . 'estimates', [ 'invoiceid' => $insert_id, ]); } elseif (total_rows(db_prefix() . 'proposals', [ 'invoice_id' => $or_merge->id, ]) > 0) { $this->db->where('invoice_id', $or_merge->id); $proposal = $this->db->get(db_prefix() . 'proposals')->row(); $this->db->where('id', $proposal->id); $this->db->update(db_prefix() . 'proposals', [ 'invoice_id' => $insert_id, ]); } } } foreach ($billed_tasks as $key => $tasks) { foreach ($tasks as $t) { $this->db->select('status') ->where('id', $t); $_task = $this->db->get(db_prefix() . 'tasks')->row(); $taskUpdateData = [ 'billed' => 1, 'invoice_id' => $insert_id, ]; if ($_task->status != Tasks_model::STATUS_COMPLETE) { $taskUpdateData['status'] = Tasks_model::STATUS_COMPLETE; $taskUpdateData['datefinished'] = date('Y-m-d H:i:s'); } $this->db->where('id', $t); $this->db->update(db_prefix() . 'tasks', $taskUpdateData); } } foreach ($billed_expenses as $key => $val) { foreach ($val as $expense_id) { $this->db->where('id', $expense_id); $this->db->update(db_prefix() . 'expenses', [ 'invoiceid' => $insert_id, ]); } } update_invoice_status($insert_id); // Update next invoice number in settings if status is not draft if (!$this->is_draft($insert_id)) { $this->increment_next_number(); } foreach ($items as $key => $item) { if ($itemid = add_new_sales_item_post($item, $insert_id, 'invoice')) { if (isset($billed_tasks[$key])) { foreach ($billed_tasks[$key] as $_task_id) { $this->db->insert(db_prefix() . 'related_items', [ 'item_id' => $itemid, 'rel_id' => $_task_id, 'rel_type' => 'task', ]); } } elseif (isset($billed_expenses[$key])) { foreach ($billed_expenses[$key] as $_expense_id) { $this->db->insert(db_prefix() . 'related_items', [ 'item_id' => $itemid, 'rel_id' => $_expense_id, 'rel_type' => 'expense', ]); } } _maybe_insert_post_item_tax($itemid, $item, $insert_id, 'invoice'); } } update_sales_total_tax_column($insert_id, 'invoice', db_prefix() . 'invoices'); if (!DEFINED('CRON') && $expense == false) { $lang_key = 'invoice_activity_created'; } elseif (!DEFINED('CRON') && $expense == true) { $lang_key = 'invoice_activity_from_expense'; } elseif (DEFINED('CRON') && $expense == false) { $lang_key = 'invoice_activity_recurring_created'; } else { $lang_key = 'invoice_activity_recurring_from_expense_created'; } $this->log_invoice_activity($insert_id, $lang_key); if ($save_and_send === true) { $this->send_invoice_to_client($insert_id, '', true, '', true); } hooks()->do_action('after_invoice_added', $insert_id); return $insert_id; } return false; } public function get_expenses_to_bill($clientid) { $this->load->model('expenses_model'); $where = 'billable=1 AND clientid=' . $clientid . ' AND invoiceid IS NULL'; if (!has_permission('expenses', '', 'view')) { $where .= ' AND ' . db_prefix() . 'expenses.addedfrom=' . get_staff_user_id(); } return $this->expenses_model->get('', $where); } public function check_for_merge_invoice($client_id, $current_invoice = '') { if ($current_invoice != '') { $this->db->select('status'); $this->db->where('id', $current_invoice); $row = $this->db->get(db_prefix() . 'invoices')->row(); // Cant merge on paid invoice and partialy paid and cancelled if ($row->status == self::STATUS_PAID || $row->status == self::STATUS_PARTIALLY || $row->status == self::STATUS_CANCELLED) { return []; } } $statuses = [ self::STATUS_UNPAID, self::STATUS_OVERDUE, self::STATUS_DRAFT, ]; $noPermissionsQuery = get_invoices_where_sql_for_staff(get_staff_user_id()); $has_permission_view = has_permission('invoices', '', 'view'); $this->db->select('id'); $this->db->where('clientid', $client_id); $this->db->where('STATUS IN (' . implode(', ', $statuses) . ')'); if (!$has_permission_view) { $whereUser = $noPermissionsQuery; $this->db->where('(' . $whereUser . ')'); } if ($current_invoice != '') { $this->db->where('id !=', $current_invoice); } $invoices = $this->db->get(db_prefix() . 'invoices')->result_array(); $invoices = hooks()->apply_filters('invoices_ids_available_for_merging', $invoices); $_invoices = []; foreach ($invoices as $invoice) { $inv = $this->get($invoice['id']); if ($inv) { $_invoices[] = $inv; } } return $_invoices; } /** * Copy invoice * @param mixed $id invoice id to copy * @return mixed */ public function copy($id) { $_invoice = $this->get($id); $new_invoice_data = []; $new_invoice_data['clientid'] = $_invoice->clientid; $new_invoice_data['number'] = get_option('next_invoice_number'); $new_invoice_data['date'] = _d(date('Y-m-d')); if ($_invoice->duedate && get_option('invoice_due_after') != 0) { $new_invoice_data['duedate'] = _d(date('Y-m-d', strtotime('+' . get_option('invoice_due_after') . ' DAY', strtotime(date('Y-m-d'))))); } $new_invoice_data['save_as_draft'] = true; $new_invoice_data['recurring_type'] = $_invoice->recurring_type; $new_invoice_data['custom_recurring'] = $_invoice->custom_recurring; $new_invoice_data['show_quantity_as'] = $_invoice->show_quantity_as; $new_invoice_data['currency'] = $_invoice->currency; $new_invoice_data['subtotal'] = $_invoice->subtotal; $new_invoice_data['total'] = $_invoice->total; $new_invoice_data['adminnote'] = $_invoice->adminnote; $new_invoice_data['adjustment'] = $_invoice->adjustment; $new_invoice_data['discount_percent'] = $_invoice->discount_percent; $new_invoice_data['discount_total'] = $_invoice->discount_total; $new_invoice_data['recurring'] = $_invoice->recurring; $new_invoice_data['discount_type'] = $_invoice->discount_type; $new_invoice_data['terms'] = $_invoice->terms; $new_invoice_data['sale_agent'] = $_invoice->sale_agent; $new_invoice_data['project_id'] = $_invoice->project_id; $new_invoice_data['cycles'] = $_invoice->cycles; $new_invoice_data['total_cycles'] = 0; // Since version 1.0.6 $new_invoice_data['billing_street'] = clear_textarea_breaks($_invoice->billing_street); $new_invoice_data['billing_city'] = $_invoice->billing_city; $new_invoice_data['billing_state'] = $_invoice->billing_state; $new_invoice_data['billing_zip'] = $_invoice->billing_zip; $new_invoice_data['billing_country'] = $_invoice->billing_country; $new_invoice_data['shipping_street'] = clear_textarea_breaks($_invoice->shipping_street); $new_invoice_data['shipping_city'] = $_invoice->shipping_city; $new_invoice_data['shipping_state'] = $_invoice->shipping_state; $new_invoice_data['shipping_zip'] = $_invoice->shipping_zip; $new_invoice_data['shipping_country'] = $_invoice->shipping_country; if ($_invoice->include_shipping == 1) { $new_invoice_data['include_shipping'] = $_invoice->include_shipping; } $new_invoice_data['show_shipping_on_invoice'] = $_invoice->show_shipping_on_invoice; // Set to unpaid status automatically $new_invoice_data['status'] = self::STATUS_UNPAID; $new_invoice_data['clientnote'] = $_invoice->clientnote; $new_invoice_data['adminnote'] = $_invoice->adminnote; $new_invoice_data['allowed_payment_modes'] = unserialize($_invoice->allowed_payment_modes); $new_invoice_data['newitems'] = []; $key = 1; $custom_fields_items = get_custom_fields('items'); foreach ($_invoice->items as $item) { $new_invoice_data['newitems'][$key]['description'] = $item['description']; $new_invoice_data['newitems'][$key]['long_description'] = clear_textarea_breaks($item['long_description']); $new_invoice_data['newitems'][$key]['qty'] = $item['qty']; $new_invoice_data['newitems'][$key]['unit'] = $item['unit']; $new_invoice_data['newitems'][$key]['taxname'] = []; $taxes = get_invoice_item_taxes($item['id']); foreach ($taxes as $tax) { // tax name is in format TAX1|10.00 array_push($new_invoice_data['newitems'][$key]['taxname'], $tax['taxname']); } $new_invoice_data['newitems'][$key]['rate'] = $item['rate']; $new_invoice_data['newitems'][$key]['order'] = $item['item_order']; foreach ($custom_fields_items as $cf) { $new_invoice_data['newitems'][$key]['custom_fields']['items'][$cf['id']] = get_custom_field_value($item['id'], $cf['id'], 'items', false); if (!defined('COPY_CUSTOM_FIELDS_LIKE_HANDLE_POST')) { define('COPY_CUSTOM_FIELDS_LIKE_HANDLE_POST', true); } } $key++; } $id = $this->invoices_model->add($new_invoice_data); if ($id) { $this->db->where('id', $id); $this->db->update(db_prefix() . 'invoices', [ 'cancel_overdue_reminders' => $_invoice->cancel_overdue_reminders, ]); $custom_fields = get_custom_fields('invoice'); foreach ($custom_fields as $field) { $value = get_custom_field_value($_invoice->id, $field['id'], 'invoice', false); if ($value == '') { continue; } $this->db->insert(db_prefix() . 'customfieldsvalues', [ 'relid' => $id, 'fieldid' => $field['id'], 'fieldto' => 'invoice', 'value' => $value, ]); } $tags = get_tags_in($_invoice->id, 'invoice'); handle_tags_save($tags, $id, 'invoice'); log_activity('Copied Invoice ' . format_invoice_number($_invoice->id)); hooks()->do_action('invoice_copied', ['copy_from' => $_invoice->id, 'copy_id' => $id]); return $id; } return false; } /** * Update invoice data * @param array $data invoice data * @param mixed $id invoiceid * @return boolean */ public function update($data, $id) { $original_invoice = $this->get($id); $updated = false; // Perhaps draft? if (isset($data['nubmer'])) { $data['number'] = trim($data['number']); } $original_number_formatted = format_invoice_number($id); $original_number = $original_invoice->number; $save_and_send = isset($data['save_and_send']); $cancel_merged_invoices = isset($data['cancel_merged_invoices']); $invoices_to_merge = isset($data['invoices_to_merge']) ? $data['invoices_to_merge'] : []; $billed_tasks = isset($data['billed_tasks']) ? $data['billed_tasks'] : []; $billed_expenses = isset($data['billed_expenses']) ? array_map('unserialize', array_unique(array_map('serialize', $data['billed_expenses']))) : []; $data['cancel_overdue_reminders'] = isset($data['cancel_overdue_reminders']) ? 1 : 0; $data['cycles'] = !isset($data['cycles']) ? 0 : $data['cycles']; $data['allowed_payment_modes'] = isset($data['allowed_payment_modes']) ? serialize($data['allowed_payment_modes']) : serialize([]); if (isset($data['recurring'])) { if ($data['recurring'] == 'custom') { $data['recurring_type'] = $data['repeat_type_custom']; $data['custom_recurring'] = 1; $data['recurring'] = $data['repeat_every_custom']; } else { $data['recurring_type'] = null; $data['custom_recurring'] = 0; } } else { $data['custom_recurring'] = 0; $data['recurring'] = 0; $data['recurring_type'] = null; } // Recurring invoice set to NO, Cancelled if ($original_invoice->recurring != 0 && $data['recurring'] == 0) { $data['cycles'] = 0; $data['total_cycles'] = 0; $data['last_recurring_date'] = null; } $data = $this->map_shipping_columns($data); $data['billing_street'] = trim($data['billing_street']); if ($data['shipping_street'] ?? false) { $data['shipping_street'] = trim($data['shipping_street']); } $data['billing_street'] = nl2br($data['billing_street']); if ($data['shipping_street'] ?? false) { $data['shipping_street'] = nl2br($data['shipping_street']); } $data['duedate'] = isset($data['duedate']) && empty($data['duedate']) ? null : $data['duedate']; $items = $data['items'] ?? []; $newitems = $data['newitems'] ?? []; if (handle_custom_fields_post($id, $custom_fields = $data['custom_fields'] ?? [])) { $updated = true; } if (array_key_exists('tags', $data)) { if (handle_tags_save($tags = $data['tags'], $id, 'invoice')) { $updated = true; } } unset($data['items'], $data['newitems'], $data['custom_fields'], $data['tags']); $hook = apply_filters_deprecated('before_invoice_updated', [[ 'data' => array_merge($data, [ 'tags' => $tags ?? null, 'custom_fields' => $custom_fields, ]), 'items' => $items, 'newitems' => $newitems, 'removed_items' => isset($data['removed_items']) ? $data['removed_items'] : [], ], $id], '3.0.0', 'before_update_invoice'); extract($hook); unset($data['custom_fields'], $data['tags']); $hookData = [ 'id' => $id, 'data' => $data, 'items' => $items, 'new_items' => $newitems, 'removed_items' => $removed_items, 'custom_fields' => $custom_fields, 'billed_tasks' => $billed_tasks, 'invoices_to_merge' => $invoices_to_merge, 'cancel_merged_invoices' => $cancel_merged_invoices, 'tags' => $tags ?? null, 'billed_expenses' => $billed_expenses, 'billed_tasks' => $billed_tasks, ]; $hook = hooks()->apply_filters('before_update_invoice', $hookData, $id); $data = $hook['data']; $items = $hook['items']; $newitems = $hook['new_items']; $removed_items = $hook['removed_items']; $custom_fields = $hook['custom_fields']; $billed_tasks = $hook['billed_tasks']; $invoices_to_merge = $hook['invoices_to_merge']; $tags = $hook['tags']; $billed_expenses = $hook['billed_expenses']; $billed_tasks = $hook['billed_tasks']; foreach ($billed_tasks as $tasks) { foreach ($tasks as $taskId) { $this->db->select('status')->where('id', $taskId); $taskStatus = $this->db->get('tasks')->row()->status; $taskUpdateData = ['billed' => 1, 'invoice_id' => $id]; if ($taskStatus != Tasks_model::STATUS_COMPLETE) { $taskUpdateData['status'] = Tasks_model::STATUS_COMPLETE; $taskUpdateData['datefinished'] = date('Y-m-d H:i:s'); } $this->db->where('id', $taskId)->update('tasks', $taskUpdateData); } } foreach ($billed_expenses as $val) { foreach ($val as $expenseId) { $this->db->where('id', $expenseId)->update('expenses', [ 'invoiceid' => $id, ]); } } // Delete items checked to be removed from database if ($this->remove_items($removed_items, $id)) { $updated = true; } unset($data['removed_items']); $this->db->where('id', $id)->update('invoices', $data); if ($this->db->affected_rows() > 0) { $updated = true; if (isset($data['data']) && $original_number != $data['number']) { $this->log_invoice_activity( $original_invoice->id, 'invoice_activity_number_changed', false, serialize([ $original_number_formatted, format_invoice_number($original_invoice->id), ]) ); } } if ($this->save_items($items, $id)) { $updated = true; } if ($this->add_new_items($newitems, $billed_tasks, $billed_expenses, $id)) { $updated = true; } if ($this->merge_invoices($invoices_to_merge, $id, $cancel_merged_invoices)) { $updated = true; } if ($updated) { update_sales_total_tax_column($id, 'invoice', db_prefix() . 'invoices'); update_invoice_status($id); } if ($save_and_send === true) { $this->send_invoice_to_client($id, '', true, '', true); } do_action_deprecated('after_invoice_updated', [$id], '3.0.0', 'invoice_updated'); hooks()->do_action('invoice_updated', array_merge($hookData, ['updated' => &$updated])); return $updated; } protected function remove_items($items, $id) { $updated = false; foreach ($items as $itemId) { $original_item = $this->get_invoice_item($itemId); if (handle_removed_sales_item_post($itemId, 'invoice')) { $updated = true; $this->log_invoice_activity($id, 'invoice_estimate_activity_removed_item', false, serialize([ $original_item->description, ])); $this->db->where('item_id', $original_item->id); $related_items = $this->db->get('related_items')->result_array(); foreach ($related_items as $rel_item) { if ($rel_item['rel_type'] == 'task') { $this->db->where('id', $rel_item['rel_id'])->update('tasks', [ 'invoice_id' => null, 'billed' => 0, ]); } elseif ($rel_item['rel_type'] == 'expense') { $this->db->where('id', $rel_item['rel_id'])->update('expenses', [ 'invoiceid' => null, ]); } $this->db->where('item_id', $original_item->id)->delete('related_items'); } } } return $updated; } protected function merge_invoices($invoices, $id, $cancel) { foreach ($invoices as $mergeId) { $merged = false; $originalInvoice = $this->get($mergeId); if ($cancel == false) { if ($this->delete($mergeId, true)) { $merged = true; } } else { if ($this->mark_as_cancelled($mergeId)) { $merged = true; $admin_note = $originalInvoice->adminnote; $note = 'Merged into invoice ' . format_invoice_number($id); if ($admin_note != '') { $admin_note .= "\n\r" . $note; } else { $admin_note = $note; } $this->db->where('id', $mergeId)->update('invoices', [ 'adminnote' => $admin_note, ]); } } if ($merged) { $this->db->where('invoiceid', $originalInvoice->id); $is_expense_invoice = $this->db->get('expenses')->row(); if ($is_expense_invoice) { $this->db->where('id', $is_expense_invoice->id)->update('expenses', [ 'invoiceid' => $id, ]); } if (total_rows('estimates', [ 'invoiceid' => $originalInvoice->id, ]) > 0) { $this->db->where('invoiceid', $originalInvoice->id); $estimate = $this->db->get('estimates')->row(); $this->db->where('id', $estimate->id)->update('estimates', [ 'invoiceid' => $id, ]); } elseif (total_rows('proposals', [ 'invoice_id' => $originalInvoice->id, ]) > 0) { $this->db->where('invoice_id', $originalInvoice->id); $proposal = $this->db->get('proposals')->row(); $this->db->where('id', $proposal->id)->db->update('proposals', [ 'invoice_id' => $id, ]); } } } } protected function add_new_items($items, $billed_tasks, $billed_expenses, $id) { $updated = false; foreach ($items as $key => $item) { if ($new_item_added = add_new_sales_item_post($item, $id, 'invoice')) { if (isset($billed_tasks[$key])) { foreach ($billed_tasks[$key] as $_task_id) { $this->db->insert('related_items', [ 'item_id' => $new_item_added, 'rel_id' => $_task_id, 'rel_type' => 'task', ]); } } elseif (isset($billed_expenses[$key])) { foreach ($billed_expenses[$key] as $_expense_id) { $this->db->insert('related_items', [ 'item_id' => $new_item_added, 'rel_id' => $_expense_id, 'rel_type' => 'expense', ]); } } _maybe_insert_post_item_tax($new_item_added, $item, $id, 'invoice'); $this->log_invoice_activity($id, 'invoice_estimate_activity_added_item', false, serialize([ $item['description'], ])); $updated = true; } } return $updated; } protected function save_items($items, $id) { $updated = false; foreach ($items as $item) { $original_item = $this->get_invoice_item($item['itemid']); if (update_sales_item_post($item['itemid'], $item, 'item_order')) { $updated = true; } if (update_sales_item_post($item['itemid'], $item, 'unit')) { $updated = true; } if (update_sales_item_post($item['itemid'], $item, 'description')) { $this->log_invoice_activity($id, 'invoice_estimate_activity_updated_item_short_description', false, serialize([ $original_item->description, $item['description'], ])); $updated = true; } if (update_sales_item_post($item['itemid'], $item, 'long_description')) { $this->log_invoice_activity($id, 'invoice_estimate_activity_updated_item_long_description', false, serialize([ $original_item->long_description, $item['long_description'], ])); $updated = true; } if (update_sales_item_post($item['itemid'], $item, 'rate')) { $this->log_invoice_activity($id, 'invoice_estimate_activity_updated_item_rate', false, serialize([ $original_item->rate, $item['rate'], ])); $updated = true; } if (update_sales_item_post($item['itemid'], $item, 'qty')) { $this->log_invoice_activity($id, 'invoice_estimate_activity_updated_qty_item', false, serialize([ $item['description'], $original_item->qty, $item['qty'], ])); $updated = true; } if (isset($item['custom_fields'])) { if (handle_custom_fields_post($item['itemid'], $item['custom_fields'])) { $updated = true; } } if (!isset($item['taxname']) || (isset($item['taxname']) && count($item['taxname']) == 0)) { if (delete_taxes_from_item($item['itemid'], 'invoice')) { $updated = true; } } else { $item_taxes = get_invoice_item_taxes($item['itemid']); $_item_taxes_names = []; foreach ($item_taxes as $_item_tax) { array_push($_item_taxes_names, $_item_tax['taxname']); } $i = 0; foreach ($_item_taxes_names as $_item_tax) { if (!in_array($_item_tax, $item['taxname'])) { $this->db->where('id', $item_taxes[$i]['id'])->delete('item_tax'); if ($this->db->affected_rows() > 0) { $updated = true; } } $i++; } if (_maybe_insert_post_item_tax($item['itemid'], $item, $id, 'invoice')) { $updated = true; } } } return $updated; } public function get_attachments($invoiceid, $id = '') { // If is passed id get return only 1 attachment if (is_numeric($id)) { $this->db->where('id', $id); } else { $this->db->where('rel_id', $invoiceid); } $this->db->where('rel_type', 'invoice'); $result = $this->db->get(db_prefix() . 'files'); if (is_numeric($id)) { return $result->row(); } return $result->result_array(); } /** * Delete invoice attachment * @since Version 1.0.4 * @param mixed $id attachmentid * @return boolean */ public function delete_attachment($id) { $attachment = $this->get_attachments('', $id); $deleted = false; if ($attachment) { if (empty($attachment->external)) { unlink(get_upload_path_by_type('invoice') . $attachment->rel_id . '/' . $attachment->file_name); } $this->db->where('id', $attachment->id); $this->db->delete(db_prefix() . 'files'); if ($this->db->affected_rows() > 0) { $deleted = true; log_activity('Invoice Attachment Deleted [InvoiceID: ' . $attachment->rel_id . ']'); } if (is_dir(get_upload_path_by_type('invoice') . $attachment->rel_id)) { // Check if no attachments left, so we can delete the folder also $other_attachments = list_files(get_upload_path_by_type('invoice') . $attachment->rel_id); if (count($other_attachments) == 0) { // okey only index.html so we can delete the folder also delete_dir(get_upload_path_by_type('invoice') . $attachment->rel_id); } } } return $deleted; } /** * Delete invoice items and all connections * @param mixed $id invoiceid * @return boolean */ public function delete($id, $simpleDelete = false) { if (get_option('delete_only_on_last_invoice') == 1 && $simpleDelete == false && !is_last_invoice($id)) { return false; } $number = format_invoice_number($id); $isDraft = $this->is_draft($id); hooks()->do_action('before_invoice_deleted', $id); $invoice = $this->get($id); $this->db->where('id', $id); $this->db->delete(db_prefix() . 'invoices'); if ($this->db->affected_rows() > 0) { if (!is_null($invoice->short_link)) { app_archive_short_link($invoice->short_link); } if (get_option('invoice_number_decrement_on_delete') == 1 && get_option('next_invoice_number') > 1 && $simpleDelete == false && !$isDraft) { // Decrement next invoice number to $this->decrement_next_number(); } if ($simpleDelete == false) { $this->db->where('invoiceid', $id); $this->db->update(db_prefix() . 'expenses', [ 'invoiceid' => null, ]); $this->db->where('invoice_id', $id); $this->db->update(db_prefix() . 'proposals', [ 'invoice_id' => null, 'date_converted' => null, ]); $this->db->where('invoice_id', $id); $this->db->update(db_prefix() . 'tasks', [ 'invoice_id' => null, 'billed' => 0, ]); // if is converted from estimate set the estimate invoice to null if (total_rows(db_prefix() . 'estimates', [ 'invoiceid' => $id, ]) > 0) { $this->db->where('invoiceid', $id); $estimate = $this->db->get(db_prefix() . 'estimates')->row(); $this->db->where('id', $estimate->id); $this->db->update(db_prefix() . 'estimates', [ 'invoiceid' => null, 'invoiced_date' => null, ]); $this->load->model('estimates_model'); $this->estimates_model->log_estimate_activity($estimate->id, 'not_estimate_invoice_deleted'); } } $this->db->select('id'); $this->db->where('id IN (SELECT credit_id FROM ' . db_prefix() . 'credits WHERE invoice_id=' . $this->db->escape_str($id) . ')'); $linked_credit_notes = $this->db->get(db_prefix() . 'creditnotes')->result_array(); $this->db->where('invoice_id', $id); $this->db->delete(db_prefix() . 'credits'); $this->load->model('credit_notes_model'); foreach ($linked_credit_notes as $credit_note) { $this->credit_notes_model->update_credit_note_status($credit_note['id']); } $this->db->where('rel_id', $id); $this->db->where('rel_type', 'invoice'); $this->db->delete(db_prefix() . 'notes'); delete_tracked_emails($id, 'invoice'); $this->db->where('rel_type', 'invoice'); $this->db->where('rel_id', $id); $this->db->delete(db_prefix() . 'taggables'); $this->db->where('rel_type', 'invoice'); $this->db->where('rel_id', $id); $this->db->delete(db_prefix() . 'reminders'); $this->db->where('rel_type', 'invoice'); $this->db->where('rel_id', $id); $this->db->delete(db_prefix() . 'views_tracking'); $this->db->where('relid IN (SELECT id from ' . db_prefix() . 'itemable WHERE rel_type="invoice" AND rel_id="' . $this->db->escape_str($id) . '")'); $this->db->where('fieldto', 'items'); $this->db->delete(db_prefix() . 'customfieldsvalues'); $items = get_items_by_type('invoice', $id); $this->db->where('rel_id', $id); $this->db->where('rel_type', 'invoice'); $this->db->delete(db_prefix() . 'itemable'); foreach ($items as $item) { $this->db->where('item_id', $item['id']); $this->db->delete(db_prefix() . 'related_items'); } $this->db->where('rel_id', $id); $this->db->where('rel_type', 'invoice'); $this->db->delete('scheduled_emails'); $this->db->where('invoiceid', $id); $this->db->delete(db_prefix() . 'invoicepaymentrecords'); $this->db->where('rel_id', $id); $this->db->where('rel_type', 'invoice'); $this->db->delete(db_prefix() . 'sales_activity'); $this->db->where('is_recurring_from', $id); $this->db->update(db_prefix() . 'invoices', [ 'is_recurring_from' => null, ]); // Delete the custom field values $this->db->where('relid', $id); $this->db->where('fieldto', 'invoice'); $this->db->delete(db_prefix() . 'customfieldsvalues'); $this->db->where('rel_id', $id); $this->db->where('rel_type', 'invoice'); $this->db->delete(db_prefix() . 'item_tax'); // Get billed tasks for this invoice and set to unbilled $this->db->where('invoice_id', $id); $tasks = $this->db->get(db_prefix() . 'tasks')->result_array(); foreach ($tasks as $task) { $this->db->where('id', $task['id']); $this->db->update(db_prefix() . 'tasks', [ 'invoice_id' => null, 'billed' => 0, ]); } $attachments = $this->get_attachments($id); foreach ($attachments as $attachment) { $this->delete_attachment($attachment['id']); } // Get related tasks $this->db->where('rel_type', 'invoice'); $this->db->where('rel_id', $id); $tasks = $this->db->get(db_prefix() . 'tasks')->result_array(); foreach ($tasks as $task) { $this->tasks_model->delete_task($task['id']); } if ($simpleDelete == false) { log_activity('Invoice Deleted [' . $number . ']'); } hooks()->do_action('after_invoice_deleted', $id); return true; } return false; } /** * Set invoice to sent when email is successfuly sended to client * @param mixed $id invoiceid * @param mixed $manually is staff manually marking this invoice as sent * @return boolean */ public function set_invoice_sent($id, $manually = false, $emails_sent = [], $is_status_updated = false) { $this->db->where('id', $id); $this->db->update(db_prefix() . 'invoices', [ 'sent' => 1, 'datesend' => date('Y-m-d H:i:s'), ]); $marked = $this->db->affected_rows() > 0; if ($marked && $this->is_draft($id)) { $this->change_invoice_number_when_status_draft($id); } if (DEFINED('CRON')) { $additional_activity_data = serialize([ '<custom_data>' . implode(', ', $emails_sent) . '</custom_data>', ]); $description = 'invoice_activity_sent_to_client_cron'; } else { if ($manually == false) { $additional_activity_data = serialize([ '<custom_data>' . implode(', ', $emails_sent) . '</custom_data>', ]); $description = 'invoice_activity_sent_to_client'; } else { $additional_activity_data = serialize([]); $description = 'invoice_activity_marked_as_sent'; } } if ($is_status_updated == false) { update_invoice_status($id, true); } $this->db->where('rel_id', $id); $this->db->where('rel_type', 'invoice'); $this->db->delete('scheduled_emails'); $this->log_invoice_activity($id, $description, false, $additional_activity_data); return $marked; } /** * Send overdue notice to client for this invoice * @param mxied $id invoiceid * @return boolean */ public function send_invoice_overdue_notice($id) { $invoice = $this->get($id); $invoice_number = format_invoice_number($invoice->id); set_mailing_constant(); $pdf = invoice_pdf($invoice); $attach_pdf = hooks()->apply_filters('invoice_overdue_notice_attach_pdf', true); if ($attach_pdf === true) { $attach = $pdf->Output($invoice_number . '.pdf', 'S'); } $emails_sent = []; $email_sent = false; $sms_sent = false; $sms_reminder_log = []; // For all cases update this to prevent sending multiple reminders eq on fail $this->db->where('id', $id); $this->db->update(db_prefix() . 'invoices', [ 'last_overdue_reminder' => date('Y-m-d'), ]); $contacts = $this->get_contacts_for_invoice_emails($invoice->clientid); foreach ($contacts as $contact) { $template = mail_template('invoice_overdue_notice', $invoice, $contact); if ($attach_pdf === true) { $template->add_attachment([ 'attachment' => $attach, 'filename' => str_replace('/', '-', $invoice_number . '.pdf'), 'type' => 'application/pdf', ]); } $merge_fields = $template->get_merge_fields(); if ($template->send()) { array_push($emails_sent, $contact['email']); $email_sent = true; } if (can_send_sms_based_on_creation_date($invoice->datecreated) && $this->app_sms->trigger(SMS_TRIGGER_INVOICE_OVERDUE, $contact['phonenumber'], $merge_fields)) { $sms_sent = true; array_push($sms_reminder_log, $contact['firstname'] . ' (' . $contact['phonenumber'] . ')'); } } if ($email_sent || $sms_sent) { if ($email_sent) { $this->log_invoice_activity($id, 'user_sent_overdue_reminder', false, serialize([ '<custom_data>' . implode(', ', $emails_sent) . '</custom_data>', defined('CRON') ? ' ' : get_staff_full_name(), ])); } if ($sms_sent) { $this->log_invoice_activity($id, 'sms_reminder_sent_to', false, serialize([ implode(', ', $sms_reminder_log), ])); } hooks()->do_action('invoice_overdue_reminder_sent', [ 'invoice_id' => $id, 'sent_to' => $emails_sent, 'sms_send' => $sms_sent, ]); return true; } return false; } /** * Send due notice to client for the given invoice * * @param mxied $id invoiceid * * @return boolean */ public function send_invoice_due_notice($id) { $invoice = $this->get($id); $invoice_number = format_invoice_number($invoice->id); set_mailing_constant(); $pdf = invoice_pdf($invoice); if ($attach_pdf = hooks()->apply_filters('invoice_due_notice_attach_pdf', true) === true) { $attach = $pdf->Output($invoice_number . '.pdf', 'S'); } $emails_sent = []; $email_sent = false; $sms_sent = false; $sms_reminder_log = []; // For all cases update this to prevent sending multiple reminders eq on fail $this->db->where('id', $id); $this->db->update(db_prefix() . 'invoices', [ 'last_due_reminder' => date('Y-m-d'), ]); $contacts = $this->get_contacts_for_invoice_emails($invoice->clientid); foreach ($contacts as $contact) { $template = mail_template('invoice_due_notice', $invoice, $contact); if ($attach_pdf === true) { $template->add_attachment([ 'attachment' => $attach, 'filename' => str_replace('/', '-', $invoice_number . '.pdf'), 'type' => 'application/pdf', ]); } $merge_fields = $template->get_merge_fields(); if ($template->send()) { array_push($emails_sent, $contact['email']); $email_sent = true; } if (can_send_sms_based_on_creation_date($invoice->datecreated) && $this->app_sms->trigger(SMS_TRIGGER_INVOICE_DUE, $contact['phonenumber'], $merge_fields)) { $sms_sent = true; array_push($sms_reminder_log, $contact['firstname'] . ' (' . $contact['phonenumber'] . ')'); } } if ($email_sent || $sms_sent) { if ($email_sent) { $this->log_invoice_activity($id, 'activity_due_reminder_is_sent', false, serialize([ '<custom_data>' . implode(', ', $emails_sent) . '</custom_data>', defined('CRON') ? ' ' : get_staff_full_name(), ])); } if ($sms_sent) { $this->log_invoice_activity($id, 'sms_reminder_sent_to', false, serialize([ implode(', ', $sms_reminder_log), ])); } hooks()->do_action('invoice_due_reminder_sent', [ 'invoice_id' => $id, 'sent_to' => $emails_sent, 'sms_send' => $sms_sent, ]); return true; } return false; } /** * Send invoice to client * @param mixed $id invoiceid * @param string $template email template to sent * @param boolean $attachpdf attach invoice pdf or not * @return boolean */ public function send_invoice_to_client($id, $template_name = '', $attachpdf = true, $cc = '', $manually = false, $attachStatement = []) { $originalNumber = null; if ($isDraft = $this->is_draft($id)) { // Update invoice number from draft before sending $originalNumber = $this->change_invoice_number_when_status_draft($id); } $invoice = hooks()->apply_filters( 'invoice_object_before_send_to_client', $this->get($id) ); if ($template_name == '') { $template_name = $invoice->sent == 0 ? 'invoice_send_to_customer' : 'invoice_send_to_customer_already_sent'; $template_name = hooks()->apply_filters('after_invoice_sent_template_statement', $template_name); } $emails_sent = []; $send_to = []; // Manually is used when sending the invoice via add/edit area button Save & Send if (!DEFINED('CRON') && $manually === false) { $send_to = $this->input->post('sent_to'); } elseif (isset($GLOBALS['scheduled_email_contacts'])) { $send_to = $GLOBALS['scheduled_email_contacts']; } else { $contacts = $this->get_contacts_for_invoice_emails($invoice->clientid); foreach ($contacts as $contact) { array_push($send_to, $contact['id']); } } $attachStatementPdf = false; if (is_array($send_to) && count($send_to) > 0) { if (isset($attachStatement['attach']) && $attachStatement['attach'] == true) { $statement = $this->clients_model->get_statement($invoice->clientid, $attachStatement['from'], $attachStatement['to']); $statementPdf = statement_pdf($statement); $statementPdfFileName = slug_it(_l('customer_statement') . '-' . $statement['client']->company); $attachStatementPdf = $statementPdf->Output($statementPdfFileName . '.pdf', 'S'); } $status_updated = update_invoice_status($invoice->id, true, true); $invoice_number = format_invoice_number($invoice->id); if ($attachpdf) { set_mailing_constant(); $pdf = invoice_pdf($this->get($id)); $attach = $pdf->Output($invoice_number . '.pdf', 'S'); } $i = 0; foreach ($send_to as $contact_id) { if ($contact_id != '') { // Send cc only for the first contact if (!empty($cc) && $i > 0) { $cc = ''; } $contact = $this->clients_model->get_contact($contact_id); if (!$contact) { continue; } $template = mail_template($template_name, $invoice, $contact, $cc); if ($attachpdf) { $template->add_attachment([ 'attachment' => $attach, 'filename' => str_replace('/', '-', $invoice_number . '.pdf'), 'type' => 'application/pdf', ]); } if ($attachStatementPdf) { $template->add_attachment([ 'attachment' => $attachStatementPdf, 'filename' => $statementPdfFileName . '.pdf', 'type' => 'application/pdf', ]); } if ($template->send()) { $sent = true; array_push($emails_sent, $contact->email); } } $i++; } } elseif ($isDraft) { // Revert the number on failure $this->db->where('id', $id); $this->db->update('invoices', ['number' => $originalNumber]); $this->decrement_next_number(); return false; } if (count($emails_sent) > 0) { $this->set_invoice_sent($id, false, $emails_sent, true); hooks()->do_action('invoice_sent', $id); return true; } // In case the invoice not sent and the status was draft and // the invoice status is updated before send return back to draft status // and actually update the number to the orignal number if ($isDraft && $status_updated !== false) { $this->decrement_next_number(); $this->db->where('id', $invoice->id); $this->db->update('invoices', [ 'status' => self::STATUS_DRAFT, 'number' => $originalNumber, ]); } return false; } /** * @since 2.7.0 * * Check whether the given invoice is draft * * @param int $id * * @return boolean */ public function is_draft($id) { return total_rows('invoices', ['id' => $id, 'status' => self::STATUS_DRAFT]) === 1; } /** * All invoice activity * @param mixed $id invoiceid * @return array */ public function get_invoice_activity($id) { $this->db->where('rel_id', $id); $this->db->where('rel_type', 'invoice'); $this->db->order_by('date', 'asc'); return $this->db->get(db_prefix() . 'sales_activity')->result_array(); } /** * Log invoice activity to database * @param mixed $id invoiceid * @param string $description activity description */ public function log_invoice_activity($id, $description = '', $client = false, $additional_data = '') { if (DEFINED('CRON')) { $staffid = '[CRON]'; $full_name = '[CRON]'; } elseif (defined('STRIPE_SUBSCRIPTION_INVOICE')) { $staffid = null; $full_name = '[Stripe]'; } elseif ($client == true) { $staffid = null; $full_name = ''; } else { $staffid = get_staff_user_id(); $full_name = get_staff_full_name(get_staff_user_id()); } $this->db->insert(db_prefix() . 'sales_activity', [ 'description' => $description, 'date' => date('Y-m-d H:i:s'), 'rel_id' => $id, 'rel_type' => 'invoice', 'staffid' => $staffid, 'full_name' => $full_name, 'additional_data' => $additional_data, ]); } /** * Get the invoices years * * @return array */ public function get_invoices_years() { return $this->db->query('SELECT DISTINCT(YEAR(date)) as year FROM ' . db_prefix() . 'invoices ORDER BY year DESC')->result_array(); } /** * Map the shipping columns into the data * * @param array $data * @param boolean $expense * * @return array */ private function map_shipping_columns($data, $expense = false) { if (!isset($data['include_shipping'])) { foreach ($this->shipping_fields as $_s_field) { if (isset($data[$_s_field])) { $data[$_s_field] = null; } } $data['show_shipping_on_invoice'] = 1; $data['include_shipping'] = 0; } else { // We dont need to overwrite to 1 unless its coming from the main function add if (!DEFINED('CRON') && $expense == false) { $data['include_shipping'] = 1; // set by default for the next time to be checked if (isset($data['show_shipping_on_invoice']) && ($data['show_shipping_on_invoice'] == 1 || $data['show_shipping_on_invoice'] == 'on')) { $data['show_shipping_on_invoice'] = 1; } else { $data['show_shipping_on_invoice'] = 0; } } // else its just like they are passed } return $data; } /** * @since 2.7.0 * * Change the invoice number for status when it's draft * * @param int $id * * @return int */ public function change_invoice_number_when_status_draft($id) { $this->db->select('number')->where('id', $id); $invoice = $this->db->get('invoices')->row(); $newNumber = get_option('next_invoice_number'); $this->db->where('id', $id); $this->db->update('invoices', ['number' => $newNumber]); $this->increment_next_number(); return $invoice->number; } /** * @since 2.7.0 * * Increment the invoies next nubmer * * @return void */ public function increment_next_number() { // Update next invoice number in settings $this->db->where('name', 'next_invoice_number'); $this->db->set('value', 'value+1', false); $this->db->update(db_prefix() . 'options'); } /** * @since 2.7.0 * * Decrement the invoies next number * * @return void */ public function decrement_next_number() { $this->db->where('name', 'next_invoice_number'); $this->db->set('value', 'value-1', false); $this->db->update(db_prefix() . 'options'); } /** * Get the contacts that should receive invoice related emails * * @param int $client_id * * @return array */ protected function get_contacts_for_invoice_emails($client_id) { return $this->clients_model->get_contacts($client_id, [ 'active' => 1, 'invoice_emails' => 1, ]); } }