/home/edulekha/crm.edulekha.com/application/helpers/sales_helper.php
<?php

defined('BASEPATH') or exit('No direct script access allowed');

/**
 * Check if company using invoice with different currencies
 *
 * @param string $table table to check
 *
 * @return bool
 */
function is_using_multiple_currencies($table = null)
{
    if (! $table) {
        $table = db_prefix() . 'invoices';
    }

    $CI = &get_instance();
    $CI->load->model('currencies_model');
    $currencies            = $CI->currencies_model->get();
    $total_currencies_used = 0;
    $other_then_base       = false;
    $base_found            = false;

    foreach ($currencies as $currency) {
        $CI->db->where('currency', $currency['id']);
        $total = $CI->db->count_all_results($table);
        if ($total > 0) {
            $total_currencies_used++;
            if ($currency['isdefault'] == 0) {
                $other_then_base = true;
            } else {
                $base_found = true;
            }
        }
    }

    if ($total_currencies_used > 1 && $base_found == true && $other_then_base == true) {
        return true;
    }
    if ($total_currencies_used == 1 && $base_found == false && $other_then_base == true) {
        return true;
    }

    return ! ($total_currencies_used == 0 || $total_currencies_used == 1);
}
/**
 * Custom format number function for the app
 *
 * @param mixed $total
 * @param bool  $foce_check_zero_decimals whether to force check
 *
 * @return mixed
 */
function app_format_number($total, $foce_check_zero_decimals = false)
{
    if (! is_numeric($total)) {
        return $total;
    }

    $decimal_separator  = get_option('decimal_separator');
    $thousand_separator = get_option('thousand_separator');

    $d = get_decimal_places();
    if (get_option('remove_decimals_on_zero') == 1 || $foce_check_zero_decimals == true) {
        if (! is_decimal($total)) {
            $d = 0;
        }
    }

    $formatted = number_format($total, $d, $decimal_separator, $thousand_separator);

    return hooks()->apply_filters('number_after_format', $formatted, [
        'total'              => $total,
        'decimal_separator'  => $decimal_separator,
        'thousand_separator' => $thousand_separator,
        'decimal_places'     => $d,
    ]);
}

/**
 * Format money/amount based on currency settings
 *
 * @since  2.3.2
 *
 * @param mixed $amount        amount to format
 * @param mixed $currency      currency db object or currency name (ISO code)
 * @param bool  $excludeSymbol whether to exclude to symbol from the format
 *
 * @return string
 */
function app_format_money($amount, $currency, $excludeSymbol = false)
{
    /**
     *  Check ewhether the amount is numeric and valid
     */
    if (! is_numeric($amount) && $amount != 0) {
        return $amount;
    }

    if (is_null($amount)) {
        $amount = 0;
    }

    /**
     * Check if currency is passed as Object from database or just currency name e.q. USD
     */
    if (is_string($currency)) {
        $dbCurrency = get_currency($currency);

        // Check of currency found in case does not exists in database
        if ($dbCurrency) {
            $currency = $dbCurrency;
        } else {
            $currency = [
                'symbol'             => $currency,
                'name'               => $currency,
                'placement'          => 'before',
                'decimal_separator'  => get_option('decimal_separator'),
                'thousand_separator' => get_option('thousand_separator'),
            ];
            $currency = (object) $currency;
        }
    }

    /**
     * Determine the symbol
     *
     * @var string
     */
    $symbol = ! $excludeSymbol ? $currency->symbol : '';

    /**
     * Check decimal places
     *
     * @var mixed
     */
    $d = get_option('remove_decimals_on_zero') == 1 && ! is_decimal($amount) ? 0 : get_decimal_places();

    /**
     * Format the amount
     *
     * @var string
     */
    $amountFormatted = number_format($amount, $d, $currency->decimal_separator, $currency->thousand_separator);

    /**
     * Maybe add the currency symbol
     *
     * @var string
     */
    $formattedWithCurrency = $currency->placement === 'after' ? $amountFormatted . '' . $symbol : $symbol . '' . $amountFormatted;

    return hooks()->apply_filters('app_format_money', $formattedWithCurrency, [
        'amount'         => $amount,
        'currency'       => $currency,
        'exclude_symbol' => $excludeSymbol,
        'decimal_places' => $d,
    ]);
}

/**
 * Check if passed number is decimal
 *
 * @param mixed $val
 *
 * @return bool
 */
function is_decimal($val)
{
    return is_numeric($val) && floor($val) != $val;
}
/**
 * Function that will loop through taxes and will check if there is 1 tax or multiple
 *
 * @param array $taxes
 *
 * @return bool
 */
function multiple_taxes_found_for_item($taxes)
{
    $names = [];

    foreach ($taxes as $t) {
        array_push($names, $t['taxname']);
    }
    $names = array_map('unserialize', array_unique(array_map('serialize', $names)));

    return ! (count($names) == 1);
}

/**
 * If there is more then 200 items in the script the search when creating eq invoice, estimate, proposal
 * will be ajax based
 *
 * @return int
 */
function ajax_on_total_items()
{
    return hooks()->apply_filters('ajax_on_total_items', 200);
}

/**
 * Helper function to get tax by passedid
 *
 * @param int $id taxid
 *
 * @return object
 */
function get_tax_by_id($id)
{
    $CI = &get_instance();
    $CI->db->where('id', $id);

    return $CI->db->get(db_prefix() . 'taxes')->row();
}
/**
 * Helper function to get tax by passed name
 *
 * @param string $name tax name
 *
 * @return object
 */
function get_tax_by_name($name)
{
    $CI = &get_instance();
    $CI->db->where('name', $name);

    return $CI->db->get(db_prefix() . 'taxes')->row();
}
/**
 * This function replace <br /> only nothing exists in the line and first line other then <br />
 *  Replace first <br /> lines to prevent new spaces
 *
 * @param string $text The text to perform the action
 *
 * @return string
 */
function _maybe_remove_first_and_last_br_tag($text)
{
    $text = preg_replace('/^<br ?\/?>/is', '', $text);

    // Replace last <br /> lines to prevent new spaces while there is new line
    while (preg_match('/<br ?\/?>$/', $text)) {
        $text = preg_replace('/<br ?\/?>$/is', '', $text);
    }

    return $text;
}

/**
 * Helper function to replace info format merge fields
 * Info format = Address formats for customers, proposals, company information
 *
 * @param string $mergeCode merge field to check
 * @param mixed  $val       value to replace
 * @param string $txt       from format
 *
 * @return string
 */
function _info_format_replace($mergeCode, $val, $txt)
{
    $tmpVal = strip_tags($val ?: '');

    if ($tmpVal != '') {
        $result = preg_replace('/({' . $mergeCode . '})/i', $val, $txt);
    } else {
        $re     = '/\s{0,}{' . $mergeCode . '}(<br ?\/?>(\n))?/i';
        $result = preg_replace($re, '', $txt);
    }

    return $result;
}

/**
 * Helper function to replace info format custom field merge fields
 * Info format = Address formats for customers, proposals, company information
 *
 * @param mixed  $id    custom field id
 * @param string $label custom field label
 * @param mixed  $value custom field value
 * @param string $txt   from format
 *
 * @return string
 */
function _info_format_custom_field($id, $label, $value, $txt)
{
    if ($value != '') {
        $result = preg_replace('/({cf_' . $id . '})/i', e($label) . ': ' . e($value), $txt);
    } else {
        $re     = '/\s{0,}{cf_' . $id . '}(<br ?\/?>(\n))?/i';
        $result = preg_replace($re, '', $txt);
    }

    return hooks()->apply_filters('info_format_custom_field', $result, [
        'id'    => $id,
        'label' => $label,
        'txt'   => $txt,
    ]);
}

/**
 * Perform necessary checking for custom fields info format
 *
 * @param array  $custom_fields custom fields
 * @param string $txt           info format text
 *
 * @return string
 */
function _info_format_custom_fields_check($custom_fields, $txt)
{
    if (count($custom_fields) == 0 || preg_match_all('/({cf_[0-9]{1,}})/i', $txt, $matches, PREG_SET_ORDER, 0) > 0) {
        $txt = preg_replace('/\s{0,}{cf_[0-9]{1,}}(<br ?\/?>(\n))?/i', '', $txt);
    }

    return $txt;
}

if (! function_exists('format_customer_info')) {
    /**
     * Format customer address info
     *
     * @param object $data        customer object from database
     * @param string $for         where this format will be used? Eq statement invoice etc
     * @param string $type        billing/shipping
     * @param bool   $companyLink company link to be added on customer company/name, this is used in admin area only
     *
     * @return string
     */
    function format_customer_info($data, $for, $type, $companyLink = false)
    {
        $format   = get_option('customer_info_format');
        $clientId = '';

        if ($for == 'statement') {
            $clientId = $data->userid;
        } elseif ($type == 'billing') {
            $clientId = $data->clientid;
        }

        $filterData = [
            'data'         => $data,
            'for'          => $for,
            'type'         => $type,
            'client_id'    => $clientId,
            'company_link' => $companyLink,
        ];

        $companyName = '';
        if ($for == 'statement') {
            $companyName = e(get_company_name($clientId));
        } elseif ($type == 'billing') {
            $companyName = e($data->client->company);
        }

        $acceptsPrimaryContactDisplay = ['invoice', 'estimate', 'payment', 'credit_note'];

        if (in_array($for, $acceptsPrimaryContactDisplay)
            && isset($data->client->show_primary_contact)
            && $data->client->show_primary_contact == 1
            && $primaryContactId = get_primary_contact_user_id($clientId)) {
            $companyName = e(get_contact_full_name($primaryContactId)) . '<br />' . $companyName;
        }

        $companyName = hooks()->apply_filters('customer_info_format_company_name', $companyName, $filterData);

        $street  = in_array($type, ['billing', 'shipping']) ? $data->{$type . '_street'} : '';
        $city    = in_array($type, ['billing', 'shipping']) ? $data->{$type . '_city'} : '';
        $state   = in_array($type, ['billing', 'shipping']) ? $data->{$type . '_state'} : '';
        $zipCode = in_array($type, ['billing', 'shipping']) ? $data->{$type . '_zip'} : '';

        $countryCode = '';
        $countryName = '';

        if ($country = in_array($type, ['billing', 'shipping']) ? get_country($data->{$type . '_country'}) : '') {
            $countryCode = $country->iso2;
            $countryName = $country->short_name;
        }

        $phone = '';
        if ($for == 'statement' && isset($data->phonenumber)) {
            $phone = $data->phonenumber;
        } elseif (in_array($type, ['billing', 'shipping']) && isset($data->client->phonenumber)) {
            $phone = $data->client->phonenumber;
        }

        $vat = '';
        if ($for == 'statement' && isset($data->vat)) {
            $vat = $data->vat;
        } elseif (in_array($type, ['billing', 'shipping']) && isset($data->client->vat)) {
            $vat = $data->client->vat;
        }

        if ($companyLink && (! isset($data->deleted_customer_name)
            || (isset($data->deleted_customer_name)
                && empty($data->deleted_customer_name)))) {
            $companyName = '<a href="' . e(admin_url('clients/client/' . $clientId)) . '" target="_blank"><b>' . $companyName . '</b></a>';
        } elseif ($companyName != '') {
            $companyName = '<b>' . $companyName . '</b>';
        }

        $format = _info_format_replace('company_name', $companyName, $format);
        $format = _info_format_replace('customer_id', $clientId, $format);
        $format = _info_format_replace('street', process_text_content_for_display($street), $format);
        $format = _info_format_replace('city', e($city), $format);
        $format = _info_format_replace('state', e($state), $format);
        $format = _info_format_replace('zip_code', e($zipCode), $format);
        $format = _info_format_replace('country_code', e($countryCode), $format);
        $format = _info_format_replace('country_name', e($countryName), $format);
        $format = _info_format_replace('phone', e($phone), $format);
        $format = _info_format_replace('vat_number', e($vat), $format);
        $format = _info_format_replace('vat_number_with_label', $vat == '' ? '' : _l('client_vat_number') . ': ' . e($vat), $format);

        $customFieldsCustomer = [];

        // On shipping address no custom fields are shown
        if ($type != 'shipping') {
            $whereCF = [];

            if (is_custom_fields_for_customers_portal()) {
                $whereCF['show_on_client_portal'] = 1;
            }

            $customFieldsCustomer = get_custom_fields('customers', $whereCF);
        }

        foreach ($customFieldsCustomer as $field) {
            $value  = get_custom_field_value($clientId, $field['id'], 'customers');
            $format = _info_format_custom_field($field['id'], $field['name'], $value, $format);
        }

        // If no custom fields found replace all custom fields merge fields to empty
        $format = _info_format_custom_fields_check($customFieldsCustomer, $format);
        $format = _maybe_remove_first_and_last_br_tag($format);

        // Remove multiple white spaces
        $format = preg_replace('/\s+/', ' ', $format);
        // Remove multiple coma
        $format = preg_replace('/,{2,}/m', '', $format);

        $format = trim($format);

        return hooks()->apply_filters('customer_info_text', $format, $filterData);
    }
}

if (! function_exists('format_proposal_info')) {
    /**
     * Format proposal info format
     *
     * @param object $proposal proposal from database
     * @param string $for      where this info will be used? Admin area, HTML preview?
     *
     * @return string
     */
    function format_proposal_info($proposal, $for = '')
    {
        $format = get_option('proposal_info_format');

        $countryCode = '';
        $countryName = '';

        if ($country = get_country($proposal->country)) {
            $countryCode = $country->iso2;
            $countryName = $country->short_name;
        }

        $proposalTo = '<b>' . e($proposal->proposal_to) . '</b>';
        $phone      = $proposal->phone;
        $email      = $proposal->email;

        if ($for == 'admin') {
            $hrefAttrs = '';
            if ($proposal->rel_type == 'lead') {
                $hrefAttrs = ' href="#" onclick="init_lead(' . e($proposal->rel_id) . '); return false;" data-toggle="tooltip" data-title="' . e(_l('lead')) . '"';
            } else {
                $hrefAttrs = ' href="' . e(admin_url('clients/client/' . $proposal->rel_id)) . '" data-toggle="tooltip" data-title="' . e(_l('client')) . '"';
            }
            $proposalTo = '<a' . $hrefAttrs . '>' . $proposalTo . '</a>';
        }

        if ($for == 'html' || $for == 'admin') {
            $phone = '<a href="tel:' . e($proposal->phone) . '">' . e($proposal->phone) . '</a>';
            $email = '<a href="mailto:' . e($proposal->email) . '">' . e($proposal->email) . '</a>';
        }

        $format = _info_format_replace('proposal_to', $proposalTo, $format);
        $format = _info_format_replace('address', process_text_content_for_display($proposal->address), $format);
        $format = _info_format_replace('city', e($proposal->city), $format);
        $format = _info_format_replace('state', e($proposal->state), $format);

        $format = _info_format_replace('country_code', e($countryCode), $format);
        $format = _info_format_replace('country_name', e($countryName), $format);

        $format = _info_format_replace('zip_code', e($proposal->zip), $format);
        $format = _info_format_replace('phone', $phone, $format);
        $format = _info_format_replace('email', $email, $format);

        $whereCF = [];
        if (is_custom_fields_for_customers_portal()) {
            $whereCF['show_on_client_portal'] = 1;
        }
        $customFieldsProposals = get_custom_fields('proposal', $whereCF);

        foreach ($customFieldsProposals as $field) {
            $value  = get_custom_field_value($proposal->id, $field['id'], 'proposal');
            $format = _info_format_custom_field($field['id'], $field['name'], $value, $format);
        }

        // If no custom fields found replace all custom fields merge fields to empty
        $format = _info_format_custom_fields_check($customFieldsProposals, $format);
        $format = _maybe_remove_first_and_last_br_tag($format);

        // Remove multiple white spaces
        $format = preg_replace('/\s+/', ' ', $format);
        $format = trim($format);

        return hooks()->apply_filters('proposal_info_text', $format, ['proposal' => $proposal, 'for' => $for]);
    }
}

if (! function_exists('format_organization_info')) {
    /**
     * Format company info/address format
     *
     * @return string
     */
    function format_organization_info()
    {
        $format = get_option('company_info_format');
        $vat    = get_option('company_vat');

        $format = _info_format_replace('company_name', '<b style="color:black" class="company-name-formatted">' . e(get_option('invoice_company_name')) . '</b>', $format);
        $format = _info_format_replace('address', e(get_option('invoice_company_address')), $format);
        $format = _info_format_replace('city', e(get_option('invoice_company_city')), $format);
        $format = _info_format_replace('state', e(get_option('company_state')), $format);

        $format = _info_format_replace('zip_code', e(get_option('invoice_company_postal_code')), $format);
        $format = _info_format_replace('country_code', e(get_option('invoice_company_country_code')), $format);
        $format = _info_format_replace('phone', e(get_option('invoice_company_phonenumber')), $format);
        $format = _info_format_replace('vat_number', e($vat), $format);
        $format = _info_format_replace('vat_number_with_label', $vat == '' ? '' : _l('company_vat_number') . ': ' . e($vat), $format);

        $custom_company_fields = get_company_custom_fields();

        foreach ($custom_company_fields as $field) {
            $format = _info_format_custom_field($field['id'], $field['label'], $field['value'], $format);
        }

        $format = _info_format_custom_fields_check($custom_company_fields, $format);
        $format = _maybe_remove_first_and_last_br_tag($format);

        // Remove multiple white spaces
        $format = preg_replace('/\s+/', ' ', $format);
        $format = trim($format);

        return hooks()->apply_filters('organization_info_text', $format);
    }
}

/**
 * Return decimal places
 * The srcipt do not support more then 2 decimal places but developers can use action hook to change the decimal places
 *
 * @return [type] [description]
 */
function get_decimal_places()
{
    return hooks()->apply_filters('app_decimal_places', 2);
}

/**
 * Get all items by type eq. invoice, proposal, estimates, credit note
 *
 * @param string $type rel_type value
 * @param mixed  $id
 *
 * @return array
 */
function get_items_by_type($type, $id)
{
    $CI = &get_instance();
    $CI->db->select();
    $CI->db->from(db_prefix() . 'itemable');
    $CI->db->where('rel_id', $id);
    $CI->db->where('rel_type', $type);
    $CI->db->order_by('item_order', 'asc');

    $result = $CI->db->get()->result_array();

    // Get function name for taxes based on type
    $func_taxes = 'get_' . $type . '_item_taxes';

    return array_map(function ($item) use ($func_taxes) {
        // Get taxes for this item if function exists
        $item_taxes = [];
        if (function_exists($func_taxes)) {
            $item_taxes = call_user_func($func_taxes, $item['id']);
        }

        return array_merge($item, [
            'is_optional' => isset($item['is_optional']) ? (bool) $item['is_optional'] : false,
            'is_selected' => isset($item['is_selected']) ? (bool) $item['is_selected'] : true,
            'taxes'       => $item_taxes,
        ]);
    }, $result);
}
/**
 * Function that update total tax in sales table eq. invoice, proposal, estimates, credit note
 *
 * @param mixed $id
 * @param mixed $type
 * @param mixed $table
 *
 * @return void
 */
function update_sales_total_tax_column($id, $type, $table)
{
    $CI = &get_instance();
    $CI->db->select('discount_percent, discount_type, discount_total, subtotal');
    $CI->db->from($table);
    $CI->db->where('id', $id);

    $data = $CI->db->get()->row();

    $items = get_items_by_type($type, $id);

    $total_tax         = 0;
    $taxes             = [];
    $_calculated_taxes = [];

    $func_taxes = 'get_' . $type . '_item_taxes';

    foreach ($items as $item) {
        // Check if item is optional and not selected - skip calculation if so
        $isOptional = isset($item['is_optional']) && $item['is_optional'] == 1;
        $isSelected = isset($item['is_selected']) && $item['is_selected'] == 1;

        if ($isOptional && ! $isSelected) {
            continue; // skip this item from calculations
        }

        $item_taxes = call_user_func($func_taxes, $item['id']);
        if (count($item_taxes) > 0) {
            foreach ($item_taxes as $tax) {
                $calc_tax     = 0;
                $tax_not_calc = false;
                if (! in_array($tax['taxname'], $_calculated_taxes)) {
                    array_push($_calculated_taxes, $tax['taxname']);
                    $tax_not_calc = true;
                }

                if ($tax_not_calc == true) {
                    $taxes[$tax['taxname']]          = [];
                    $taxes[$tax['taxname']]['total'] = [];
                    array_push($taxes[$tax['taxname']]['total'], (($item['qty'] * $item['rate']) / 100 * $tax['taxrate']));
                    $taxes[$tax['taxname']]['tax_name'] = $tax['taxname'];
                    $taxes[$tax['taxname']]['taxrate']  = $tax['taxrate'];
                } else {
                    array_push($taxes[$tax['taxname']]['total'], (($item['qty'] * $item['rate']) / 100 * $tax['taxrate']));
                }
            }
        }
    }

    foreach ($taxes as $tax) {
        $total = array_sum($tax['total']);
        if ($data->discount_percent != 0 && $data->discount_type == 'before_tax') {
            $total_tax_calculated = ($total * $data->discount_percent) / 100;
            $total                = ($total - $total_tax_calculated);
        } elseif ($data->discount_total != 0 && $data->discount_type == 'before_tax') {
            $t     = ($data->discount_total / $data->subtotal) * 100;
            $total = ($total - $total * $t / 100);
        }
        $total_tax += $total;
    }

    $CI->db->where('id', $id);
    $CI->db->update($table, [
        'total_tax' => $total_tax,
    ]);
}

/**
 * Function used for sales eq. invoice, estimate, proposal, credit note
 *
 * @param mixed  $item_id   item id
 * @param array  $post_item $item from $_POST
 * @param mixed  $rel_id    rel_id
 * @param string $rel_type  where this item tax is related
 */
function _maybe_insert_post_item_tax($item_id, $post_item, $rel_id, $rel_type)
{
    $affectedRows = 0;
    if (isset($post_item['taxname']) && is_array($post_item['taxname'])) {
        $CI = &get_instance();

        foreach ($post_item['taxname'] as $taxname) {
            if ($taxname != '') {
                $tax_array = explode('|', $taxname);
                if (isset($tax_array[0], $tax_array[1])) {
                    $tax_name = trim($tax_array[0]);
                    $tax_rate = trim($tax_array[1]);
                    if (total_rows(db_prefix() . 'item_tax', [
                        'itemid'   => $item_id,
                        'taxrate'  => $tax_rate,
                        'taxname'  => $tax_name,
                        'rel_id'   => $rel_id,
                        'rel_type' => $rel_type,
                    ]) == 0) {
                        $CI->db->insert(db_prefix() . 'item_tax', [
                            'itemid'   => $item_id,
                            'taxrate'  => $tax_rate,
                            'taxname'  => $tax_name,
                            'rel_id'   => $rel_id,
                            'rel_type' => $rel_type,
                        ]);
                        $affectedRows++;
                    }
                }
            }
        }
    }

    return $affectedRows > 0 ? true : false;
}

/**
 * Add new item do database, used for proposals,estimates,credit notes,invoices
 * This is repetitive action, that's why this function exists
 *
 * @param array  $item     item from $_POST
 * @param mixed  $rel_id   relation id eq. invoice id
 * @param string $rel_type relation type eq invoice
 */
function add_new_sales_item_post($item, $rel_id, $rel_type)
{
    $custom_fields = false;

    if (isset($item['custom_fields'])) {
        $custom_fields = $item['custom_fields'];
    }

    $CI         = &get_instance();
    $isOptional = $item['is_optional'] ?? false;

    $CI->db->insert(db_prefix() . 'itemable', [
        'description'      => $item['description'],
        'long_description' => nl2br($item['long_description']),
        'qty'              => $item['qty'],
        'rate'             => number_format($item['rate'], get_decimal_places(), '.', ''),
        'rel_id'           => $rel_id,
        'rel_type'         => $rel_type,
        'item_order'       => $item['order'],
        'unit'             => $item['unit'],
        'is_optional'      => $isOptional ? 1 : 0,
        'is_selected'      => $isOptional ? ($item['is_selected'] ?? 0) : 1,
    ]);

    $id = $CI->db->insert_id();

    if ($custom_fields !== false) {
        handle_custom_fields_post($id, $custom_fields);
    }

    return $id;
}

/**
 * Update sales item from $_POST, eq invoice item, estimate item
 *
 * @param mixed  $item_id item id to update
 * @param array  $data    item $_POST data
 * @param string $field   field is require to be passed for long_description,rate,item_order to do some additional checkings
 *
 * @return bool
 */
function update_sales_item_post($item_id, $data, $field = '')
{
    $update = [];

    if ($field !== '') {
        if ($field == 'long_description') {
            $update[$field] = nl2br($data[$field]);
        } elseif ($field == 'rate') {
            $update[$field] = number_format($data[$field], get_decimal_places(), '.', '');
        } elseif ($field == 'item_order') {
            $update[$field] = $data['order'];
        } else {
            $update[$field] = $data[$field];
        }

        if ($field === 'is_optional') {
            $isOptional            = $data['is_optional'] ?? false;
            $update['is_optional'] = $isOptional ? 1 : 0;
            $update['is_selected'] = $isOptional ? ($data['is_selected'] ?? 0) : 1;
        }
    } else {
        $isOptional = $data['is_optional'] ?? false;

        $update = [
            'item_order'       => $data['order'],
            'description'      => $data['description'],
            'long_description' => nl2br($data['long_description']),
            'rate'             => number_format($data['rate'], get_decimal_places(), '.', ''),
            'qty'              => $data['qty'],
            'unit'             => $data['unit'],
            'is_optional'      => $isOptional ? 1 : 0,
            'is_selected'      => $isOptional ? ($data['is_selected'] ?? 0) : 1,
        ];
    }

    $CI = &get_instance();
    $CI->db->where('id', $item_id);
    $CI->db->update(db_prefix() . 'itemable', $update);

    return $CI->db->affected_rows() > 0 ? true : false;
}

/**
 * When item is removed eq from invoice will be stored in removed_items in $_POST
 * With foreach loop this function will remove the item from database and it's taxes
 *
 * @param mixed  $id       item id to remove
 * @param string $rel_type item relation eq. invoice, estimate
 *
 * @return boolena
 */
function handle_removed_sales_item_post($id, $rel_type)
{
    $CI = &get_instance();

    $CI->db->where('id', $id);
    $CI->db->delete(db_prefix() . 'itemable');
    if ($CI->db->affected_rows() > 0) {
        delete_taxes_from_item($id, $rel_type);

        $CI->db->where('relid', $id);
        $CI->db->where('fieldto', 'items');
        $CI->db->delete(db_prefix() . 'customfieldsvalues');

        return true;
    }

    return false;
}

/**
 * Remove taxes from item
 *
 * @param mixed  $item_id  item id
 * @param string $rel_type relation type eq. invoice, estimate etc.
 *
 * @return bool
 */
function delete_taxes_from_item($item_id, $rel_type)
{
    $CI = &get_instance();
    $CI->db->where('itemid', $item_id)
        ->where('rel_type', $rel_type)
        ->delete(db_prefix() . 'item_tax');

    return $CI->db->affected_rows() > 0 ? true : false;
}

function is_sale_discount_applied($data)
{
    return $data->discount_total > 0;
}

function is_sale_discount($data, $is)
{
    if ($data->discount_percent == 0 && $data->discount_total == 0) {
        return false;
    }

    $discount_type = 'fixed';
    if ($data->discount_percent != 0) {
        $discount_type = 'percent';
    }

    return $discount_type == $is;
}

/**
 * Get items table for preview
 *
 * @param object $transaction   e.q. invoice, estimate from database result row
 * @param string $type          type, e.q. invoice, estimate, proposal
 * @param string $for           where the items will be shown, html or pdf
 * @param bool   $admin_preview is the preview for admin area
 *
 * @return object
 */
function get_items_table_data($transaction, $type, $for = 'html', $admin_preview = false)
{
    include_once APPPATH . 'libraries/App_items_table.php';
    $class = new App_items_table($transaction, $type, $for, $admin_preview);

    $class = hooks()->apply_filters('items_table_class', $class, $transaction, $type, $for, $admin_preview);

    if (! $class instanceof App_items_table_template) {
        show_error(get_class($class) . ' must be instance of "App_items_template"');
    }

    return $class;
}

function sales_number_format($number, $format, $applied_prefix, $date)
{
    $originalNumber = $number;
    $prefixPadding  = get_option('number_padding_prefixes');

    if ($format == 1) {
        // Number based
        $number = $applied_prefix . str_pad($number, $prefixPadding, '0', STR_PAD_LEFT);
    } elseif ($format == 2) {
        // Year based
        $number = $applied_prefix . date('Y', strtotime($date)) . '/' . str_pad($number, $prefixPadding, '0', STR_PAD_LEFT);
    } elseif ($format == 3) {
        // Number-yy based
        $number = $applied_prefix . str_pad($number, $prefixPadding, '0', STR_PAD_LEFT) . '-' . date('y', strtotime($date));
    } elseif ($format == 4) {
        // Number-mm-yyyy based
        $number = $applied_prefix . str_pad($number, $prefixPadding, '0', STR_PAD_LEFT) . '/' . date('m', strtotime($date)) . '/' . date('Y', strtotime($date));
    }

    return hooks()->apply_filters('sales_number_format', $number, [
        'format'         => $format,
        'date'           => $date,
        'number'         => $originalNumber,
        'prefix_padding' => $prefixPadding,
    ]);
}

/**
 * Helper function to get currency by ID or by Name
 *
 * @since  2.3.2
 *
 * @param mixed $id_or_name
 *
 * @return object
 */
function get_currency($id_or_name)
{
    $CI = &get_instance();
    if (! class_exists('currencies_model', false)) {
        $CI->load->model('currencies_model');
    }

    if (is_numeric($id_or_name)) {
        return $CI->currencies_model->get($id_or_name);
    }

    return $CI->currencies_model->get_by_name($id_or_name);
}

/**
 * Get base currency
 *
 * @since  2.3.2
 *
 * @return object
 */
function get_base_currency()
{
    $CI = &get_instance();

    if (! class_exists('currencies_model', false)) {
        $CI->load->model('currencies_model');
    }

    return $CI->currencies_model->get_base_currency();
}

/**
 * Calculate sales totals (subtotal, taxes, discounts, total)
 * PHP equivalent of the JavaScript calculate_total() function
 *
 * @param array $items   Array of items with keys: qty, rate, taxname (array of tax names|rates)
 * @param array $options Array with keys: discount_percent, discount_total, discount_type, adjustment
 *
 * @return array Array with calculated values: subtotal, taxes, total_tax, discount_calculated, total, discount_html
 */
function calculate_sales_total($items = [], $options = [])
{
    // Default options
    $defaults = [
        'discount_percent' => 0,
        'discount_total'   => 0,
        'discount_type'    => 'before_tax',
        'adjustment'       => 0,
    ];

    $defaults['discount_total_type'] = $options['discount_total_type'] ?? ($options['discount_percent'] > 0 ? 'percent' : 'fixed');

    $options = array_merge($defaults, $options);

    $taxes                     = [];
    $subtotal                  = 0;
    $total                     = 0;
    $total_discount_calculated = 0;

    // Calculate subtotal and taxes for each item
    foreach ($items as $item) {
        $quantity = ! empty($item['qty']) ? $item['qty'] : 1;
        $rate     = ! empty($item['rate']) ? $item['rate'] : 0;

        // Check if item is optional and not chosen - skip calculation if so
        $isOptional = isset($item['is_optional']) && $item['is_optional'] == 1;
        $isChosen   = isset($item['is_selected']) && $item['is_selected'] == 1;

        if ($isOptional && ! $isChosen) {
            continue; // skip this item from calculations
        }

        // Calculate item amount
        $amount = $rate * $quantity;
        $amount = round($amount, get_decimal_places());

        $subtotal += $amount;

        // Process item taxes
        if (! empty($item['taxes']) && is_array($item['taxes'])) {
            foreach ($item['taxes'] as $tax) {
                if (! empty($tax['taxname']) && ! empty($tax['taxrate'])) {
                    $taxname  = $tax['taxname'];
                    $tax_rate = (float) $tax['taxrate'];

                    // Extract tax name from the taxname field (format: "TAX1|18.00")
                    $tax_parts = explode('|', $taxname);
                    $tax_name  = count($tax_parts) >= 1 ? trim($tax_parts[0]) : $taxname;

                    if ($tax_rate != 0) {
                        $calculated_tax = ($amount / 100) * $tax_rate;

                        if (! isset($taxes[$taxname])) {
                            $taxes[$taxname] = [
                                'name'  => $tax_name,
                                'rate'  => $tax_rate,
                                'total' => $calculated_tax,
                            ];
                        } else {
                            $taxes[$taxname]['total'] += $calculated_tax;
                        }
                    }
                }
            }
        } elseif (! empty($item['taxname']) && is_array($item['taxname'])) {
            // Fallback for the original format
            foreach ($item['taxname'] as $taxname) {
                if (! empty($taxname)) {
                    $tax_parts = explode('|', $taxname);
                    if (count($tax_parts) == 2) {
                        $tax_name = trim($tax_parts[0]);
                        $tax_rate = (float) trim($tax_parts[1]);

                        if ($tax_rate != 0) {
                            $calculated_tax = ($amount / 100) * $tax_rate;

                            if (! isset($taxes[$taxname])) {
                                $taxes[$taxname] = [
                                    'name'  => $tax_name,
                                    'rate'  => $tax_rate,
                                    'total' => $calculated_tax,
                                ];
                            } else {
                                $taxes[$taxname]['total'] += $calculated_tax;
                            }
                        }
                    }
                }
            }
        }
    }

    // Calculate discount before tax
    if ($options['discount_percent'] > 0
        && $options['discount_type'] == 'before_tax'
        && $options['discount_total_type'] == 'percent') {
        $total_discount_calculated = ($subtotal * $options['discount_percent']) / 100;
    } elseif ($options['discount_total'] > 0
              && $options['discount_type'] == 'before_tax'
              && $options['discount_total_type'] == 'fixed') {
        $total_discount_calculated = $options['discount_total'];
    }

    // Apply discount to taxes if discount is before tax
    $total_tax = 0;

    foreach ($taxes as $taxname => $tax_data) {
        $tax_total = $tax_data['total'];

        if ($options['discount_percent'] > 0
            && $options['discount_type'] == 'before_tax'
            && $options['discount_total_type'] == 'percent') {
            $tax_discount = ($tax_total * $options['discount_percent']) / 100;
            $tax_total    = $tax_total - $tax_discount;
        } elseif ($options['discount_total'] > 0
                  && $options['discount_type'] == 'before_tax'
                  && $options['discount_total_type'] == 'fixed') {
            $discount_percentage = ($options['discount_total'] / $subtotal) * 100;
            $tax_total           = $tax_total - ($tax_total * $discount_percentage / 100);
        }

        $taxes[$taxname]['total'] = $tax_total;
        $total_tax += $tax_total;
    }

    $total = $total_tax + $subtotal;

    // Calculate discount after tax
    if ($options['discount_percent'] > 0
        && $options['discount_type'] == 'after_tax'
        && $options['discount_total_type'] == 'percent') {
        $total_discount_calculated = ($total * $options['discount_percent']) / 100;
    } elseif ($options['discount_total'] > 0
              && $options['discount_type'] == 'after_tax'
              && $options['discount_total_type'] == 'fixed') {
        $total_discount_calculated = $options['discount_total'];
    }

    $total = $total - $total_discount_calculated;

    // Add adjustment
    if (! empty($options['adjustment']) && is_numeric($options['adjustment'])) {
        $total += $options['adjustment'];
    }

    // Format discount for display
    $discount_html = '-' . app_format_money($total_discount_calculated, get_base_currency());

    return [
        'subtotal'            => round($subtotal, get_decimal_places()),
        'taxes'               => $taxes,
        'total_tax'           => round($total_tax, get_decimal_places()),
        'discount_calculated' => round($total_discount_calculated, get_decimal_places()),
        'total'               => round($total, get_decimal_places()),
        'discount_html'       => $discount_html,
        'adjustment'          => $options['adjustment'],
    ];
}