From effba9d102a6c41757cae7efa3c5c140c700fb27 Mon Sep 17 00:00:00 2001 From: Steve Ireland Date: Sat, 18 Feb 2017 21:33:34 -0500 Subject: [PATCH] Add support for customer sales tax and cash rounding --- application/config/autoload.php | 2 +- application/controllers/Config.php | 7 +- application/controllers/Customers.php | 17 +- application/controllers/Items.php | 30 ++ application/controllers/Login.php | 3 +- application/controllers/Receivings.php | 12 + application/controllers/Sales.php | 190 +++++++-- application/controllers/Taxes.php | 227 +++++++++++ application/helpers/table_helper.php | 37 ++ application/language/en/config_lang.php | 5 + application/language/en/customers_lang.php | 1 + application/language/en/enum_lang.php | 9 + application/language/en/module_lang.php | 4 + application/language/en/sales_lang.php | 1 + application/language/en/taxes_lang.php | 35 ++ application/libraries/Sale_lib.php | 290 ++++++++++++-- application/libraries/Tax_lib.php | 320 +++++++++++++++ .../tokens/Token_suspended_invoice_count.php | 4 +- application/models/Customer.php | 5 +- application/models/Dinner_table.php | 16 +- application/models/Item.php | 117 ++++++ application/models/Sale.php | 258 ++++++++++-- application/models/Tax.php | 375 ++++++++++++++++++ application/models/enums/Rounding_code.php | 80 ++++ application/models/reports/Summary_report.php | 9 +- application/models/reports/Summary_taxes.php | 8 +- application/views/configs/general_config.php | 38 +- application/views/configs/locale_config.php | 29 +- application/views/customers/form.php | 43 ++ application/views/items/form.php | 13 +- application/views/receivings/receiving.php | 2 +- application/views/sales/invoice.php | 47 ++- application/views/sales/quote.php | 6 +- application/views/sales/receipt_default.php | 6 +- application/views/sales/receipt_short.php | 6 +- application/views/sales/register.php | 51 ++- application/views/sales/suspended.php | 2 +- application/views/taxes/form.php | 212 ++++++++++ application/views/taxes/manage.php | 34 ++ database/3.0.2_to_3.1.0.sql | 84 +++- database/database.sql | 106 ++++- database/tables.sql | 107 ++++- public/css/popupbox.css | 8 +- public/images/menubar/taxes.png | Bin 0 -> 1572 bytes 44 files changed, 2679 insertions(+), 177 deletions(-) create mode 100644 application/controllers/Taxes.php create mode 100644 application/language/en/enum_lang.php create mode 100644 application/language/en/taxes_lang.php create mode 100644 application/libraries/Tax_lib.php create mode 100644 application/models/Tax.php create mode 100644 application/models/enums/Rounding_code.php create mode 100644 application/views/taxes/form.php create mode 100644 application/views/taxes/manage.php create mode 100644 public/images/menubar/taxes.png diff --git a/application/config/autoload.php b/application/config/autoload.php index 1dd9072e6..e039a5c28 100644 --- a/application/config/autoload.php +++ b/application/config/autoload.php @@ -132,4 +132,4 @@ $autoload['language'] = array(); | | $autoload['model'] = array('first_model' => 'first'); */ -$autoload['model'] = array('Appconfig', 'Person', 'Customer', 'Employee', 'Module', 'Item', 'Item_taxes', 'Sale', 'Sale_suspended', 'Supplier', 'Inventory', 'Receiving', 'Giftcard', 'Item_kit', 'Item_kit_items', 'Stock_location', 'Item_quantity','Dinner_table','Customer_rewards','Rewards'); +$autoload['model'] = array('Appconfig', 'Person', 'Customer', 'Employee', 'Module', 'Item', 'Item_taxes', 'Sale', 'Sale_suspended', 'Supplier', 'Inventory', 'Receiving', 'Giftcard', 'Item_kit', 'Item_kit_items', 'Stock_location', 'Item_quantity','Dinner_table','Customer_rewards','Rewards', 'Tax'); diff --git a/application/controllers/Config.php b/application/controllers/Config.php index 103115b1b..0fc114602 100644 --- a/application/controllers/Config.php +++ b/application/controllers/Config.php @@ -199,6 +199,7 @@ class Config extends Secure_Controller $data['logo_exists'] = $this->config->item('company_logo') != ''; $data['line_sequence_options'] = $this->sale_lib->get_line_sequence_options(); $data['register_mode_options'] = $this->sale_lib->get_register_mode_options(); + $data['rounding_options'] = Rounding_code::get_rounding_options(); $data = $this->xss_clean($data); @@ -252,6 +253,8 @@ class Config extends Secure_Controller 'default_tax_2_rate' => parse_decimals($this->input->post('default_tax_2_rate')), 'default_tax_2_name' => $this->input->post('default_tax_2_name'), 'tax_included' => $this->input->post('tax_included') != NULL, + 'customer_sales_tax_support' => $this->input->post('customer_sales_tax_support') != NULL, + 'default_origin_tax_code' => $this->input->post('default_origin_tax_code'), 'receiving_calculate_average_price' => $this->input->post('receiving_calculate_average_price') != NULL, 'lines_per_page' => $this->input->post('lines_per_page'), 'default_sales_discount' => $this->input->post('default_sales_discount'), @@ -312,7 +315,9 @@ class Config extends Secure_Controller 'quantity_decimals' => $this->input->post('quantity_decimals'), 'country_codes' => $this->input->post('country_codes'), 'payment_options_order' => $this->input->post('payment_options_order'), - 'date_or_time_format' => $this->input->post('date_or_time_format') + 'date_or_time_format' => $this->input->post('date_or_time_format'), + 'cash_decimals' => $this->input->post('cash_decimals'), + 'cash_rounding_code' => $this->input->post('cash_rounding_code') ); $result = $this->Appconfig->batch_save($batch_save_data); diff --git a/application/controllers/Customers.php b/application/controllers/Customers.php index 8117831af..ca0b27c4f 100644 --- a/application/controllers/Customers.php +++ b/application/controllers/Customers.php @@ -63,13 +63,15 @@ class Customers extends Persons */ public function view($customer_id = -1) { + $customer_sales_tax_support = $this->config->item('customer_sales_tax_support'); + $info = $this->Customer->get_info($customer_id); foreach(get_object_vars($info) as $property => $value) { $info->$property = $this->xss_clean($value); } $data['person_info'] = $info; - + $data['sales_tax_code_label'] = $info->sales_tax_code . ' ' .$this->Tax->get_info($info->sales_tax_code)->tax_code_name; $data['total'] = $this->xss_clean($this->Customer->get_totals($customer_id)->total); $packages = array('' => $this->lang->line('items_none')); foreach($this->Customer_rewards->get_all()->result_array() as $row) @@ -79,6 +81,15 @@ class Customers extends Persons $data['packages'] = $packages; $data['selected_package'] = $info->package_id; + if ($customer_sales_tax_support == '1') + { + $data['customer_sales_tax_enabled'] = TRUE; + } + else + { + $data['customer_sales_tax_enabled'] = FALSE; + } + $this->load->view("customers/form", $data); } @@ -101,12 +112,14 @@ class Customers extends Persons 'country' => $this->input->post('country'), 'comments' => $this->input->post('comments') ); + $customer_data = array( 'account_number' => $this->input->post('account_number') == '' ? NULL : $this->input->post('account_number'), 'company_name' => $this->input->post('company_name') == '' ? NULL : $this->input->post('company_name'), 'discount_percent' => $this->input->post('discount_percent') == '' ? 0.00 : $this->input->post('discount_percent'), 'package_id' => $this->input->post('package_id') == '' ? NULL : $this->input->post('package_id'), - 'taxable' => $this->input->post('taxable') != NULL + 'taxable' => $this->input->post('taxable') != NULL, + 'sales_tax_code' => $this->input->post('sales_tax_code') ); if($this->Customer->save_customer($person_data, $customer_data, $customer_id)) diff --git a/application/controllers/Items.php b/application/controllers/Items.php index f4efe7220..ee1b73ade 100644 --- a/application/controllers/Items.php +++ b/application/controllers/Items.php @@ -197,6 +197,7 @@ class Items extends Secure_Controller $item_info->reorder_level = 0; $item_info->item_type = '0'; // standard $item_info->stock_type = '0'; // stock + $item_info->tax_category_id = 0; } $data['item_info'] = $item_info; @@ -209,6 +210,25 @@ class Items extends Secure_Controller $data['suppliers'] = $suppliers; $data['selected_supplier'] = $item_info->supplier_id; + $customer_sales_tax_support = $this->config->item('customer_sales_tax_support'); + if($customer_sales_tax_support == '1') + { + $data['customer_sales_tax_enabled'] = TRUE; + $tax_categories = array(); + foreach($this->Tax->get_all_tax_categories()->result_array() as $row) + { + $tax_categories[$this->xss_clean($row['tax_category_id'])] = $this->xss_clean($row['tax_category']); + } + $data['tax_categories'] = $tax_categories; + $data['selected_tax_category'] = $item_info->tax_category_id; + } + else + { + $data['customer_sales_tax_enabled'] = FALSE; + $data['tax_categories'] = array(); + $data['selected_tax_category'] = ''; + } + $data['logo_exists'] = $item_info->pic_filename != ''; $ext = pathinfo($item_info->pic_filename, PATHINFO_EXTENSION); if($ext == '') @@ -371,6 +391,16 @@ class Items extends Secure_Controller 'custom9' => $this->input->post('custom9') == NULL ? '' : $this->input->post('custom9'), 'custom10' => $this->input->post('custom10') == NULL ? '' : $this->input->post('custom10') ); + + $x = $this->input->post('tax_category_id'); + if(!isset($x)) + { + $item['tax_category_id'] = ''; + } + else + { + $item['tax_category_id'] = $this->input->post('selected_tax_category'); + } if(!empty($upload_data['orig_name'])) { diff --git a/application/controllers/Login.php b/application/controllers/Login.php index a463ac674..46a5e4764 100644 --- a/application/controllers/Login.php +++ b/application/controllers/Login.php @@ -34,6 +34,7 @@ class Login extends CI_Controller $this->tracking_lib->track_event('Stats', 'Language', $this->config->item('language')); $this->tracking_lib->track_event('Stats', 'Timezone', $this->config->item('timezone')); $this->tracking_lib->track_event('Stats', 'Currency', $this->config->item('currency_symbol')); + $this->tracking_lib->track_event('Stats', 'Customer Sales Tax Support', $this->config->item('customer_sales_tax_support')); $this->tracking_lib->track_event('Stats', 'Tax Included', $this->config->item('tax_included')); $this->tracking_lib->track_event('Stats', 'Thousands Separator', $this->config->item('thousands_separator')); $this->tracking_lib->track_event('Stats', 'Currency Decimals', $this->config->item('currency_decimals')); @@ -71,7 +72,7 @@ class Login extends CI_Controller private function _security_check($username, $password) { - return preg_match('~\b(Copyright|(c)|©|All rights reserved|Developed|Crafted|Implemented|Made|Powered|Code|Design|unblockUI|blockUI|blockOverlay|hide|opacity)\b~i', file_get_contents(APPPATH . 'views/partial/footer.php')); + return preg_match('~\b(Copyright|(c)|©|All rights reserved|Developed|Crafted|Implemented|Made|Powered|Code|Design|unblockUI|blockUI|blockOverlay|hide|opacity)\b~i', file_get_contents(APPPATH . 'views/partial/footer.php')); } } ?> diff --git a/application/controllers/Receivings.php b/application/controllers/Receivings.php index 2dda154f2..d0eb93381 100644 --- a/application/controllers/Receivings.php +++ b/application/controllers/Receivings.php @@ -27,6 +27,18 @@ class Receivings extends Secure_Controller echo json_encode($suggestions); } + public function stock_item_search() + { + $suggestions = $this->Item->get_stock_search_suggestions($this->input->get('term'), array('search_custom' => FALSE, 'is_deleted' => FALSE), TRUE); + $suggestions = array_merge($suggestions, $this->Item_kit->get_search_suggestions($this->input->get('term'))); + + $suggestions = $this->xss_clean($suggestions); + + echo json_encode($suggestions); + } + + + public function select_supplier() { $supplier_id = $this->input->post('supplier'); diff --git a/application/controllers/Sales.php b/application/controllers/Sales.php index 674ecf4c8..f5215d2c7 100644 --- a/application/controllers/Sales.php +++ b/application/controllers/Sales.php @@ -9,6 +9,7 @@ class Sales extends Secure_Controller parent::__construct('sales'); $this->load->library('sale_lib'); + $this->load->library('tax_lib'); $this->load->library('barcode_lib'); $this->load->library('email_lib'); $this->load->library('token_lib'); @@ -172,6 +173,12 @@ class Sales extends Secure_Controller $this->sale_lib->set_invoice_number_enabled($this->input->post('sales_invoice_number_enabled')); } + public function set_payment_type() + { + $this->sale_lib->set_payment_type($this->input->post('selected_payment_type')); + $this->_reload(); + } + public function set_print_after_sale() { $this->sale_lib->set_print_after_sale($this->input->post('sales_print_after_sale')); @@ -410,6 +417,7 @@ class Sales extends Secure_Controller $this->sale_lib->clear_rewards_remainder(); $this->sale_lib->delete_payment($this->lang->line('sales_rewards')); $this->sale_lib->clear_invoice_number(); + $this->sale_lib->clear_quote_number(); $this->sale_lib->remove_customer(); $this->_reload(); @@ -425,20 +433,11 @@ class Sales extends Secure_Controller $data = array(); $data['dinner_table'] = $this->sale_lib->get_dinner_table(); $data['cart'] = $this->sale_lib->get_cart(); - $data['subtotal'] = $this->sale_lib->get_subtotal(); - $data['discounted_subtotal'] = $this->sale_lib->get_subtotal(TRUE); - $data['tax_exclusive_subtotal'] = $this->sale_lib->get_subtotal(TRUE, TRUE); - $data['taxes'] = $this->sale_lib->get_taxes(); - $data['total'] = $this->sale_lib->get_total(); - $data['discount'] = $this->sale_lib->get_discount(); $data['receipt_title'] = $this->lang->line('sales_receipt'); $data['transaction_time'] = date($this->config->item('dateformat') . ' ' . $this->config->item('timeformat')); $data['transaction_date'] = date($this->config->item('dateformat')); $data['show_stock_locations'] = $this->Stock_location->show_locations('sales'); $data['comments'] = $this->sale_lib->get_comment(); - $data['payments'] = $this->sale_lib->get_payments(); - $data['amount_change'] = $this->sale_lib->get_amount_due() * -1; - $data['amount_due'] = $this->sale_lib->get_amount_due(); $employee_id = $this->Employee->get_logged_in_employee_info()->person_id; $employee_info = $this->Employee->get_info($employee_id); $data['employee'] = $employee_info->first_name . ' ' . $employee_info->last_name[0]; @@ -453,12 +452,44 @@ class Sales extends Secure_Controller $data['print_after_sale'] = $this->sale_lib->is_print_after_sale(); $data['email_receipt'] = $this->sale_lib->get_email_receipt(); $customer_id = $this->sale_lib->get_customer(); + $data["invoice_number"] = $this->sale_lib->get_invoice_number(); + $data["quote_number"] = $this->sale_lib->get_quote_number(); $customer_info = $this->_load_customer_data($customer_id, $data); + $data['taxes'] = $this->sale_lib->get_taxes(); + $data['discount'] = $this->sale_lib->get_discount(); + $data['payments'] = $this->sale_lib->get_payments(); + + // Returns 'subtotal', 'total', 'cash_total', 'payment_total', 'amount_due', 'cash_amount_due', 'payments_cover_total' + $totals = $this->sale_lib->get_totals(); + $data['subtotal'] = $totals['discounted_subtotal']; + $data['cash_total'] = $totals['cash_total']; + $data['cash_amount_due'] = $totals['cash_amount_due']; + $data['non_cash_total'] =$totals['total']; + $data['non_cash_amount_due'] =$totals['amount_due']; + $data['payments_total'] = $totals['payment_total']; + $data['payments_cover_total'] = $totals['payments_cover_total']; + $data['cash_rounding'] = $this->session->userdata('cash_rounding'); + + $data['discounted_subtotal'] = $totals['discounted_subtotal']; + $data['tax_exclusive_subtotal'] = $totals['tax_exclusive_subtotal']; + + if ($data['cash_rounding']) + { + $data['total'] = $totals['cash_total']; + $data['amount_due'] = $totals['cash_amount_due']; + } + else + { + $data['total'] = $totals['total']; + $data['amount_due'] = $totals['amount_due']; + } + $data['amount_change'] = $data['amount_due'] * -1; + if ($this->sale_lib->is_invoice_mode() || $data['invoice_number_enabled'] == true) { // generate final invoice number (if using the invoice in sales by receipt mode then the invoice number can be manually entered or altered in some way - if ($this->sale_lib->is_sale_by_receipt_mode()) + if($this->sale_lib->is_sale_by_receipt_mode()) { $this->sale_lib->set_invoice_number($this->input->post('invoice_number'), $keep_custom = TRUE); $invoice_format = $this->sale_lib->get_invoice_number(); @@ -473,8 +504,6 @@ class Sales extends Secure_Controller } $invoice_number = $this->token_lib->render($invoice_format); - $quote_number = null; - // TODO If duplicate invoice then determine the number of employees and repeat until until success or tried the number of employees (if QSEQ was used). if($this->Sale->check_invoice_number_exists($invoice_number)) { @@ -484,10 +513,10 @@ class Sales extends Secure_Controller else { $data['invoice_number'] = $invoice_number; - $data['quote_number'] = $quote_number; + $data['sale_status'] = '0'; // Complete // Save the data to the sales table - $data['sale_id_num'] = $this->Sale->save($data['cart'], $customer_id, $employee_id, $data['comments'], $invoice_number, $data['payments'], $data['dinner_table']); + $data['sale_id_num'] = $this->Sale->save($data['sale_status'], $data['cart'], $customer_id, $employee_id, $data['comments'], $invoice_number, $data["quote_number"], $data['payments'], $data['dinner_table'], $data['taxes']); $data['sale_id'] = 'POS ' . $data['sale_id_num']; // Resort and filter cart lines for printing @@ -509,7 +538,9 @@ class Sales extends Secure_Controller } elseif($this->sale_lib->is_quote_mode()) { + $invoice_number = null; $quote_number = $this->sale_lib->get_quote_number(); + if($quote_number == null) { // generate quote number @@ -517,8 +548,6 @@ class Sales extends Secure_Controller $quote_number = $this->token_lib->render($quote_format); } - $invoice_number = null; - // TODO If duplicate quote then determine the number of employees and repeat until until success or tried the number of employees (if QSEQ was used). if($this->Sale->check_quote_number_exists($quote_number)) { @@ -529,13 +558,19 @@ class Sales extends Secure_Controller { $data['invoice_number'] = $invoice_number; $data['quote_number'] = $quote_number; + $data['sale_status'] = '1'; // Suspended + + $data['sale_id_num'] = $this->Sale->save($data['sale_status'], $data['cart'], $customer_id, $employee_id, $data['comments'], $invoice_number, $quote_number, $data['payments'], $data['dinner_table'], $data['taxes']); $data['cart'] = $this->sale_lib->sort_and_filter_cart($data['cart']); $data = $this->xss_clean($data); + $data['barcode'] = NULL; - $this->suspend_quote($quote_number); + +// $this->suspend_quote($quote_number); + $this->load->view('sales/quote', $data); $this->sale_lib->clear_mode(); $this->sale_lib->clear_all(); @@ -544,7 +579,9 @@ class Sales extends Secure_Controller else { // Save the data to the sales table - $data['sale_id_num'] = $this->Sale->save($data['cart'], $customer_id, $employee_id, $data['comments'], null, $data['payments'], $data['dinner_table']); + $data['sale_status'] = '0'; // Complete + $data['sale_id_num'] = $this->Sale->save($data['sale_status'], $data['cart'], $customer_id, $employee_id, $data['comments'], null, null, $data['payments'], $data['dinner_table'], $data['taxes']); + $data['sale_id'] = 'POS ' . $data['sale_id_num']; $data['cart'] = $this->sale_lib->sort_and_filter_cart($data['cart']); @@ -720,7 +757,7 @@ class Sales extends Secure_Controller { $text = $this->_substitute_variable($text, '$YCO', $this->Sale, 'get_invoice_number_for_year'); $text = $this->_substitute_variable($text, '$CO', $this->Sale, 'get_invoice_count'); - $text = $this->_substitute_variable($text, '$SCO', $this->Sale_suspended, 'get_invoice_count'); + $text = $this->_substitute_variable($text, '$SCO', $this->Sale, 'get_suspended_invoice_count'); $text = strftime($text); $text = $this->_substitute_customer($text, $customer_info); @@ -801,23 +838,53 @@ class Sales extends Secure_Controller private function _load_sale_data($sale_id) { $this->sale_lib->clear_all(); + $this->sale_lib->reset_cash_flags(); $sale_info = $this->Sale->get_info($sale_id)->row_array(); $this->sale_lib->copy_entire_sale($sale_id); $data = array(); $data['cart'] = $this->sale_lib->get_cart(); $data['payments'] = $this->sale_lib->get_payments(); - $data['subtotal'] = $this->sale_lib->get_subtotal(); - $data['discounted_subtotal'] = $this->sale_lib->get_subtotal(TRUE); - $data['tax_exclusive_subtotal'] = $this->sale_lib->get_subtotal(TRUE, TRUE); + $data['selected_payment_type'] = $this->sale_lib->get_payment_type(); + +// $data['subtotal'] = $this->sale_lib->get_subtotal(); +// $data['discounted_subtotal'] = $this->sale_lib->get_subtotal(TRUE); +// $data['tax_exclusive_subtotal'] = $this->sale_lib->get_subtotal(TRUE, TRUE); +// $data['amount_due'] = $this->sale_lib->get_amount_due(); +// $data['amount_change'] = $this->sale_lib->get_amount_due() * -1; +// $data['total'] = $this->sale_lib->get_total(); + $data['taxes'] = $this->sale_lib->get_taxes(); - $data['total'] = $this->sale_lib->get_total(); $data['discount'] = $this->sale_lib->get_discount(); $data['receipt_title'] = $this->lang->line('sales_receipt'); $data['transaction_time'] = date($this->config->item('dateformat') . ' ' . $this->config->item('timeformat'), strtotime($sale_info['sale_time'])); $data['transaction_date'] = date($this->config->item('dateformat'), strtotime($sale_info['sale_time'])); $data['show_stock_locations'] = $this->Stock_location->show_locations('sales'); - $data['amount_change'] = $this->sale_lib->get_amount_due() * -1; - $data['amount_due'] = $this->sale_lib->get_amount_due(); + + + // Returns 'subtotal', 'total', 'cash_total', 'payment_total', 'amount_due', 'cash_amount_due', 'payments_cover_total' + $totals = $this->sale_lib->get_totals(); + $data['subtotal'] = $totals['subtotal']; + $data['discounted_subtotal'] = $totals['discounted_subtotal']; + $data['tax_exclusive_subtotal'] = $totals['tax_exclusive_subtotal']; + $data['cash_total'] = $totals['cash_total']; + $data['cash_amount_due'] = $totals['cash_amount_due']; + $data['non_cash_total'] = $totals['total']; + $data['non_cash_amount_due'] = $totals['amount_due']; + $data['payments_total'] = $totals['payment_total']; + $data['payments_cover_total'] = $totals['payments_cover_total']; + + if ($this->session->userdata('cash_rounding')) + { + $data['total'] = $totals['cash_total']; + $data['amount_due'] = $totals['cash_amount_due']; + } + else + { + $data['total'] = $totals['total']; + $data['amount_due'] = $totals['amount_due']; + } + $data['amount_change'] = $data['amount_due'] * -1; + $employee_info = $this->Employee->get_info($this->sale_lib->get_employee()); $data['employee'] = $employee_info->first_name . ' ' . $employee_info->last_name[0]; $this->_load_customer_data($this->sale_lib->get_customer(), $data); @@ -826,6 +893,8 @@ class Sales extends Secure_Controller $data['sale_id'] = 'POS ' . $sale_id; $data['comments'] = $sale_info['comment']; $data['invoice_number'] = $sale_info['invoice_number']; + $data['quote_number'] = $sale_info['quote_number']; + $data['sale_status'] = $sale_info['sale_status']; $data['company_info'] = implode("\n", array( $this->config->item('address'), $this->config->item('phone'), @@ -868,16 +937,38 @@ class Sales extends Secure_Controller $data['selected_table'] = $this->sale_lib->get_dinner_table(); $data['stock_locations'] = $this->Stock_location->get_allowed_locations('sales'); $data['stock_location'] = $this->sale_lib->get_sale_location(); - $data['subtotal'] = $this->sale_lib->get_subtotal(TRUE); $data['tax_exclusive_subtotal'] = $this->sale_lib->get_subtotal(TRUE, TRUE); + $data['taxes'] = $this->sale_lib->get_taxes(); $data['discount'] = $this->sale_lib->get_discount(); - $data['total'] = $this->sale_lib->get_total(); + $data['payments'] = $this->sale_lib->get_payments(); + + // Returns 'subtotal', 'total', 'cash_total', 'payment_total', 'amount_due', 'cash_amount_due', 'payments_cover_total' + $totals = $this->sale_lib->get_totals(); + $data['subtotal'] = $totals['discounted_subtotal']; + $data['cash_total'] = $totals['cash_total']; + $data['cash_amount_due'] = $totals['cash_amount_due']; + $data['non_cash_total'] =$totals['total']; + $data['non_cash_amount_due'] =$totals['amount_due']; + $data['payments_total'] = $totals['payment_total']; + $data['payments_cover_total'] = $totals['payments_cover_total']; + $data['cash_rounding'] = $this->session->userdata('cash_rounding'); + + if ($data['cash_rounding']) + { + $data['total'] = $totals['cash_total']; + $data['amount_due'] = $totals['cash_amount_due']; + } + else + { + $data['total'] = $totals['total']; + $data['amount_due'] = $totals['amount_due']; + } + $data['amount_change'] = $data['amount_due'] * -1; + $data['comment'] = $this->sale_lib->get_comment(); $data['email_receipt'] = $this->sale_lib->get_email_receipt(); - $data['payments_total'] = $this->sale_lib->get_payments_total(); - $data['amount_due'] = $this->sale_lib->get_amount_due(); - $data['payments'] = $this->sale_lib->get_payments(); + $data['selected_payment_type'] = $this->sale_lib->get_payment_type(); if($customer_info && $this->config->item('customer_reward_enable') == TRUE) $data['payment_options'] = $this->Sale->get_payment_options(TRUE,TRUE); else @@ -898,7 +989,6 @@ class Sales extends Secure_Controller $data['invoice_number_enabled'] = $this->sale_lib->is_invoice_mode(); $data['print_after_sale'] = $this->sale_lib->is_print_after_sale(); - $data['payments_cover_total'] = $this->sale_lib->is_payment_covering_total(); $data['quote_or_invoice_mode'] = $data['mode'] == 'sale_invoice' || $data['mode'] == 'sale_quote'; $data['sales_or_return_mode'] = $data['mode'] == 'sale' || $data['mode'] == 'return'; if($this->sale_lib->get_mode() == 'sale_invoice') @@ -1050,7 +1140,7 @@ class Sales extends Secure_Controller { $suspended_id = $this->sale_lib->get_suspended_id(); $this->sale_lib->clear_all(); - $this->Sale_suspended->delete($suspended_id); + $this->Sale->delete_suspended_sale($suspended_id); $this->_reload(); } @@ -1065,10 +1155,11 @@ class Sales extends Secure_Controller $invoice_number = $this->_is_custom_invoice_number($customer_info) ? $this->sale_lib->get_invoice_number() : NULL; $quote_number = $this->sale_lib->get_quote_number(); $comment = $this->sale_lib->get_comment(); + $sale_status = '1'; - //SAVE sale to database $data = array(); - if ($this->Sale_suspended->save($cart, $customer_id, $employee_id, $comment, $invoice_number, $quote_number, $payments, $dinner_table) == '-1') + $sales_taxes = array(); + if ($this->Sale->save($sale_status, $cart, $customer_id, $employee_id, $comment, $invoice_number, $quote_number, $payments, $dinner_table, $sales_taxes) == '-1') { $data['error'] = $this->lang->line('sales_unsuccessfully_suspended_sale'); } @@ -1088,14 +1179,14 @@ class Sales extends Secure_Controller $employee_id = $this->Employee->get_logged_in_employee_info()->person_id; $customer_id = $this->sale_lib->get_customer(); $customer_info = $this->Customer->get_info($customer_id); + $dinner_table = $this->sale_lib->get_dinner_table(); $invoice_number = $this->_is_custom_invoice_number($customer_info) ? $this->sale_lib->get_invoice_number() : NULL; $comment = $this->sale_lib->get_comment(); + $sale_status = '2'; // Suspend - //SAVE sale to database $data = array(); - $suspended_id = $this->Sale_suspended->save($cart, $customer_id, $employee_id, $comment, $invoice_number, $quote_number, $payments); - $this->sale_lib->set_suspended_id($suspended_id); - if ($suspended_id == '-1') + $sales_taxes = array(); + if ($this->Sale->save($sale_status, $cart, $customer_id, $employee_id, $comment, $invoice_number, $quote_number, $payments, $dinner_table, $sales_taxes) == '-1') { $data['error'] = $this->lang->line('sales_unsuccessfully_suspended_sale'); } @@ -1107,17 +1198,34 @@ class Sales extends Secure_Controller public function suspended() { + $customer_id = $this->sale_lib->get_customer(); $data = array(); - $data['suspended_sales'] = $this->xss_clean($this->Sale_suspended->get_all()->result_array()); + $data['suspended_sales'] = $this->xss_clean($this->Sale->get_all_suspended($customer_id)); + $data['dinner_table_enable'] = $this->config->item('dinner_table_enable'); $this->load->view('sales/suspended', $data); } + /* + * We will eventually drop the current set of "suspended" tables since suspended sales + * are now stored in the sales tables with a sale_status value of suspended. + */ public function unsuspend() { $sale_id = $this->input->post('suspended_sale_id'); $this->sale_lib->clear_all(); - $this->sale_lib->copy_entire_suspended_sale($sale_id); - $this->Sale_suspended->delete($sale_id); + + if($sale_id > 0) { + $this->sale_lib->copy_entire_sale($sale_id); + $this->Sale->delete_suspended_sale($sale_id); + } + else + { + // This will unsuspended older suspended sales + $sale_id = $sale_id * -1; + $this->sale_lib->copy_entire_suspended_tables_sale($sale_id); + $this->Sale_suspended->delete($sale_id); + } + $this->_reload(); } diff --git a/application/controllers/Taxes.php b/application/controllers/Taxes.php new file mode 100644 index 000000000..ea9183990 --- /dev/null +++ b/application/controllers/Taxes.php @@ -0,0 +1,227 @@ +load->model('enums/Rounding_code'); + + } + + public function index() + { + $data['table_headers'] = $this->xss_clean(get_taxes_manage_table_headers()); + + $this->load->view('taxes/manage', $data); + } + + /* + Returns tax_codes table data rows. This will be called with AJAX. + */ + public function search() + { + $search = $this->input->get('search'); + $limit = $this->input->get('limit'); + $offset = $this->input->get('offset'); + $sort = $this->input->get('sort'); + $order = $this->input->get('order'); + + $tax_codes = $this->Tax->search($search, $limit, $offset, $sort, $order); + + $total_rows = $this->Tax->get_found_rows($search); + + $data_rows = array(); + foreach($tax_codes->result() as $tax_code_row) + { + $data_rows[] = get_tax_data_row($tax_code_row, $this); + } + + $data_rows = $this->xss_clean($data_rows); + + echo json_encode(array('total' => $total_rows, 'rows' => $data_rows)); + } + + /* + Gives search suggestions based on what is being searched for + */ + public function suggest_search() + { + $suggestions = $this->xss_clean($this->Tax->get_search_suggestions($this->input->post('term'))); + + echo json_encode($suggestions); + } + + /* + Provides list of tax categories to select from + */ + public function suggest_tax_categories() + { + $suggestions = $this->xss_clean($this->Tax->get_tax_category_suggestions($this->input->post('term'))); + + echo json_encode($suggestions); + } + + + public function get_row($row_id) + { + $data_row = $this->xss_clean(get_tax_codes_data_row($this->Tax->get_info($row_id), $this)); + + echo json_encode($data_row); + } + + public function view($tax_code = -1) + { + $tax_code_info = $this->Tax->get_info($tax_code); + + $default_tax_category_id = 0; // Tax category id is always the default tax category + $default_tax_category = $this->Tax->get_tax_category($default_tax_category_id); + + $tax_rate_info = $this->Tax->get_rate_info($tax_code, $default_tax_category_id); + + $data['rounding_options'] = Rounding_code::get_rounding_options(); + + if ($tax_code == -1) + { + $data['tax_code'] = ''; + $data['tax_code_name'] = ''; + $data['tax_code_type'] = '0'; + $data['city'] = ''; + $data['state'] = ''; + $data['tax_rate'] = '0.0000'; + $data['rate_tax_code'] = ''; + $data['rate_tax_category_id'] = '0'; + $data['tax_category'] = ''; + $data['add_tax_category'] = ''; + $data['rounding_code'] = '0'; + } + else + { + $data['tax_code'] = $tax_code; + $data['tax_code_name'] = $tax_code_info->tax_code_name; + $data['tax_code_type'] = $tax_code_info->tax_code_type; + $data['city'] = $tax_code_info->city; + $data['state'] = $tax_code_info->state; + $data['rate_tax_code'] = $tax_code_info->rate_tax_code; + $data['rate_tax_category_id'] = $tax_code_info->rate_tax_category_id; + $data['tax_category'] = $tax_code_info->tax_category; + $data['add_tax_category'] = ''; + $data['tax_rate'] = $tax_rate_info->tax_rate; + $data['rounding_code'] = $tax_rate_info->rounding_code; + } + + $data = $this->xss_clean($data); + + $tax_code_rates = array(); + foreach($this->Tax->get_tax_code_rate_exceptions($tax_code) as $tax_code_rate) + { + $tax_code_row = array(); + $tax_code_row['rate_tax_category_id'] = $this->xss_clean($tax_code_rate['rate_tax_category_id']); + $tax_code_row['tax_category'] = $this->xss_clean($tax_code_rate['tax_category']); + $tax_code_row['tax_rate'] = $this->xss_clean($tax_code_rate['tax_rate']); + $tax_code_row['rounding_code'] = $this->xss_clean($tax_code_rate['rounding_code']); + + $tax_code_rates[] = $tax_code_row; + } + + $data['tax_code_rates'] = $tax_code_rates; + + $this->load->view("taxes/form", $data); + } + + public static function get_html_rounding_options() + { + return Rounding_code::get_html_rounding_options(); + } + + public function save($tax_code = -1) + { + $entered_tax_code = $this->xss_clean($this->input->post('tax_code')); + $tax_code_data = array( + 'tax_code' => $this->input->post('tax_code'), + 'tax_code_name' => $this->input->post('tax_code_name'), + 'tax_code_type' => $this->input->post('tax_code_type'), + 'city' => $this->input->post('city'), + 'state' => $this->input->post('state')); + + $tax_rate_data = array( + 'rate_tax_code' => $this->input->post('tax_code'), + 'rate_tax_category_id' => 0, + 'tax_rate' => parse_decimals($this->input->post('tax_rate')), + 'rounding_code' => parse_decimals($this->input->post('rounding_code')) + ); + + if($this->Tax->save($tax_code_data, $tax_rate_data, $tax_code)) + { + $tax_code_rate_exceptions = array(); + if(!empty($this->input->post('exception_tax_rate'))) + { + foreach($this->input->post('exception_tax_rate') as $tax_category_id => $exception_tax_rate) + { + $exception_rounding_code = $this->input->post('exception_rounding_code[' . $tax_category_id . ']'); + $tax_code_rate_exceptions[] = array( + 'rate_tax_code' => $entered_tax_code, + 'rate_tax_category_id' => $tax_category_id, + 'tax_rate' => $exception_tax_rate, + 'rounding_code' => $exception_rounding_code + ); + } + } + + if (!empty($tax_code_rate_exceptions)) + { + $success = $this->Tax->save_tax_rate_exceptions($tax_code_rate_exceptions, $entered_tax_code); + } + + $tax_code_data = $this->xss_clean($tax_code_data); + + //New tax_code record + if($tax_code == -1) + { + echo json_encode(array('success' => TRUE, 'message' => $this->lang->line('taxes_tax_code_successfully_added') . ' ' . $entered_tax_code)); + } + else //Existing tax_code + { + echo json_encode(array('success' => TRUE, 'message' => $this->lang->line('taxes_tax_code_successful_updated') . ' ' . $entered_tax_code)); + } + } + else //failure + { + $tax_code_data = $this->xss_clean($tax_code_data); + + echo json_encode(array('success' => FALSE, 'message' => $this->lang->line('taxes_tax_code_error_adding_updating') . ' ' . + $entered_tax_code)); + } + } + + public function delete() + { + $tax_codes_to_delete = $this->xss_clean($this->input->post('ids')); + + if($this->Tax->delete_list($tax_codes_to_delete)) + { + echo json_encode(array('success' => TRUE, 'message' => $this->lang->line('taxes_tax_code_successful_deleted'))); + } else + { + echo json_encode(array('success' => FALSE, 'message' => $this->lang->line('taxes_tax_code_cannot_be_deleted'))); + } + } + + public function suggest_sales_tax_codes() + { + $suggestions = $this->xss_clean($this->Tax->get_sales_tax_codes_search_suggestions($this->input->post_get('term'))); + + echo json_encode($suggestions); + } + + public function get_rounding_code_name($rounding_code) + { + return Rounding_code::get_rounding_code_name($rounding_code); + } + + +} +?> \ No newline at end of file diff --git a/application/helpers/table_helper.php b/application/helpers/table_helper.php index 64f8c6d3f..954839598 100644 --- a/application/helpers/table_helper.php +++ b/application/helpers/table_helper.php @@ -327,6 +327,23 @@ function get_giftcards_manage_table_headers() return transform_headers($headers); } +function get_taxes_manage_table_headers() +{ + $CI =& get_instance(); + + $headers = array( + array('tax_code' => $CI->lang->line('taxes_tax_code')), + array('tax_code_name' => $CI->lang->line('taxes_tax_code_name')), + array('tax_code_type_name' => $CI->lang->line('taxes_tax_code_type')), + array('tax_rate' => $CI->lang->line('taxes_tax_rate')), + array('rounding_code_name' => $CI->lang->line('taxes_rounding_code')), + array('city' => $CI->lang->line('common_city')), + array('state' => $CI->lang->line('common_state')) + ); + + return transform_headers($headers); +} + function get_giftcard_data_row($giftcard, $controller) { $CI =& get_instance(); @@ -343,6 +360,26 @@ function get_giftcard_data_row($giftcard, $controller) )); } +function get_tax_data_row($tax_code_row, $controller) +{ + $CI =& get_instance(); + $controller_name=strtolower(get_class($CI)); + + return array ( + 'tax_code' => $tax_code_row->tax_code, + 'tax_code_name' => $tax_code_row->tax_code_name, + 'tax_code_type' => $tax_code_row->tax_code_type, + 'tax_rate' => $tax_code_row->tax_rate, + 'rounding_code' =>$tax_code_row->rounding_code, + 'tax_code_type_name' => $CI->Tax->get_tax_code_type_name($tax_code_row->tax_code_type), + 'rounding_code_name' => $CI->get_rounding_code_name($tax_code_row->rounding_code), + 'city' => $tax_code_row->city, + 'state' => $tax_code_row->state, + 'edit' => anchor($controller_name."/view/$tax_code_row->tax_code", '', + array('class'=>'modal-dlg', 'data-btn-submit' => $CI->lang->line('common_submit'), 'title'=>$CI->lang->line($controller_name.'_update')) + )); +} + function get_item_kits_manage_table_headers() { $CI =& get_instance(); diff --git a/application/language/en/config_lang.php b/application/language/en/config_lang.php index 6b525963f..f057e3fd9 100644 --- a/application/language/en/config_lang.php +++ b/application/language/en/config_lang.php @@ -29,6 +29,9 @@ $lang["config_barcode_type"] = "Barcode Type"; $lang["config_barcode_width"] = "Width (px)"; $lang["config_bottom"] = "Bottom"; $lang["config_center"] = "Center"; +$lang["config_cash_decimals"] = "Cash Decimals"; +$lang["config_cash_decimals_tooltip"] = "If cash decimals and currency decimals are the same then no cash rounding will take place."; +$lang["config_cash_rounding"] = "Cash Rounding"; $lang["config_comma"] = "comma"; $lang["config_company"] = "Company Name"; $lang["config_company_change_image"] = "Change Image"; @@ -56,6 +59,7 @@ $lang["config_customer_reward_duplicate"] = "Please use an unique reward name"; $lang["config_customer_reward_enable"] = "Enable Customer Rewards"; $lang["config_customer_reward_invalid_chars"] = "The reward name can not contain '_'"; $lang["config_customer_reward_required"] = "Reward is a required field"; +$lang["config_customer_sales_tax_support"] = "Customer Sales Tax Support"; $lang["config_date_or_time_format"] = "Date and Time filter"; $lang["config_datetimeformat"] = "Date and Time format"; $lang["config_decimal_point"] = "Decimal Point"; @@ -73,6 +77,7 @@ $lang["config_default_barcode_quality_number"] = "The default barcode quality mu $lang["config_default_barcode_quality_required"] = "The default barcode quality is a required field"; $lang["config_default_barcode_width_number"] = "The default barcode width must be a number"; $lang["config_default_barcode_width_required"] = "The default barcode width is a required field"; +$lang["config_default_origin_tax_code"] = "Default Origin Tax Code"; $lang["config_default_sales_discount"] = "Default Sales Discount %"; $lang["config_default_sales_discount_number"] = "The default sales discount must be a number"; $lang["config_default_sales_discount_required"] = "The default sales discount is a required field"; diff --git a/application/language/en/customers_lang.php b/application/language/en/customers_lang.php index 9e5f3cb9c..4e16b923b 100644 --- a/application/language/en/customers_lang.php +++ b/application/language/en/customers_lang.php @@ -20,6 +20,7 @@ $lang["customers_one_or_multiple"] = "customer(s)"; $lang["customers_successful_adding"] = "You have successfully added customer"; $lang["customers_successful_deleted"] = "You have successfully deleted"; $lang["customers_successful_updating"] = "You have successfully updated customer"; +$lang["customers_tax_code"] = "Tax Code"; $lang["customers_taxable"] = "Taxable"; $lang["customers_total"] = "Total"; $lang["customers_update"] = "Update Customer"; diff --git a/application/language/en/enum_lang.php b/application/language/en/enum_lang.php new file mode 100644 index 000000000..21eb2755b --- /dev/null +++ b/application/language/en/enum_lang.php @@ -0,0 +1,9 @@ +CI =& get_instance(); + $this->CI->load->library('tax_lib'); + $this->CI->load->model('enums/Rounding_code'); + } public function get_line_sequence_options() @@ -298,17 +301,112 @@ class Sale_lib public function get_payments_total() { $subtotal = 0; + $this->reset_cash_flags(); foreach($this->get_payments() as $payments) { $subtotal = bcadd($payments['payment_amount'], $subtotal); + if($this->CI->session->userdata('cash_rounding') && $this->CI->lang->line('sales_cash') != $payments['payment_type']) + { + $this->CI->session->set_userdata('cash_rounding', 0); + } } return $subtotal; } + public function get_cash_rounding() + { + + } + + /* + * Returns 'subtotal', 'total', 'cash_total', 'payment_total', 'amount_due', 'cash_amount_due', 'paid_in_full' + * 'subtotal', 'discounted_subtotal', 'tax_exclusive_subtotal' + */ + public function get_totals() + { + $cash_rounding = $this->CI->session->userdata('cash_rounding'); + + $totals = array(); + + $subtotal = 0; + $discounted_subtotal = 0; + $tax_exclusive_subtotal = 0; + foreach($this->get_cart() as $item) + { + $subtotal = bcadd($subtotal, $this->get_item_total($item['quantity'], $item['price'], $item['discount'], false)); + $discounted_subtotal = bcadd($discounted_subtotal, $this->get_item_total($item['quantity'], $item['price'], $item['discount'], true)); + if($this->CI->config->config['tax_included']) + { + $tax_exclusive_subtotal = bcadd($tax_exclusive_subtotal, $this->get_item_total_tax_exclusive($item['item_id'], $item['quantity'], $item['price'], $item['discount'], true)); + } + } + + $totals['subtotal'] = $subtotal; + $totals['discounted_subtotal'] = $discounted_subtotal; + $totals['tax_exclusive_subtotal'] = $tax_exclusive_subtotal; + + $total = $discounted_subtotal; + if ($this->CI->config->config['tax_included']) + { + $totals['total'] = $total; + } + else + { + foreach($this->get_taxes() as $sales_tax) + { + $total = bcadd($total, $sales_tax['sale_tax_amount']); + } + $totals['total'] = $total; + } + + if($cash_rounding) + { + $cash_total = $this->check_for_cash_rounding($total); + $totals['cash_total'] = $cash_total; + } + else + { + $cash_total = $total; + } + + $totals['cash_total'] = $cash_total; + + $payment_total = $this->get_payments_total(); + $totals['payment_total'] = $payment_total; + + $amount_due = bcsub($total, $payment_total); + $totals['amount_due'] = $amount_due; + + $cash_amount_due = bcsub($cash_total, $payment_total); + $totals['cash_amount_due'] = $cash_amount_due; + + if($cash_rounding) + { + $current_due = $cash_amount_due; + } + else + { + $current_due = $amount_due; + } + + if($this->get_mode() == 'return') + { + $totals['payments_cover_total'] = $current_due >= 0; + } + else + { + $totals['payments_cover_total'] = $current_due <= 0; + } + + return $totals; + } + + // Multiple Payments public function get_amount_due() { + // Payment totals need to be identified first so that we know whether or not there is a non-cash payment involved $payment_total = $this->get_payments_total(); $sales_total = $this->get_total(); $amount_due = bcsub($sales_total, $payment_total); @@ -320,16 +418,14 @@ class Sale_lib public function is_payment_covering_total() { - // 0 decimal -> 1 / 2 = 0.5, 1 decimals -> 0.1 / 2 = 0.05, 2 decimals -> 0.01 / 2 = 0.005 - $threshold = bcpow(10, -$this->CI->config->item('currency_decimals')) / 2; if($this->get_mode() == 'return') { - return ($this->get_amount_due() > -$threshold); + return $this->get_amount_due() >= 0; } else { - return ($this->get_amount_due() < $threshold); + return $this->get_amount_due() <= 0; } } @@ -437,6 +533,16 @@ class Sale_lib $this->CI->session->set_userdata('sales_location', $location); } + public function set_payment_type($payment_type) + { + $this->CI->session->set_userdata('payment_type', $payment_type); + } + + public function get_payment_type() + { + return $this->CI->session->userdata('payment_type'); + } + public function clear_sale_location() { $this->CI->session->unset_userdata('sales_location'); @@ -571,7 +677,8 @@ class Sale_lib 'total' => $total, 'discounted_total' => $discounted_total, 'print_option' => $print_option, - 'stock_type' => $stock_type + 'stock_type' => $stock_type, + 'tax_category_id' => $item_info->tax_category_id ) ); //add to existing array @@ -778,6 +885,31 @@ class Sale_lib $this->empty_cart(); $this->remove_customer(); + foreach($this->CI->Sale->get_sale_items($sale_id)->result() as $row) + { + $this->add_item($row->item_id, $row->quantity_purchased, $row->item_location, $row->discount_percent, $row->item_unit_price, + $row->description, $row->serialnumber, TRUE, $row->print_option, $row->stock_type); + } + + foreach($this->CI->Sale->get_sale_payments($sale_id)->result() as $row) + { + $this->add_payment($row->payment_type, $row->payment_amount); + } + + $suspended_sale_info = $this->CI->Sale->get_info($sale_id)->row(); + $this->set_customer($suspended_sale_info->person_id); + $this->set_comment($suspended_sale_info->comment); + + $this->set_invoice_number($suspended_sale_info->invoice_number); + $this->set_quote_number($suspended_sale_info->quote_number); + $this->set_dinner_table($suspended_sale_info->dinner_table_id); + } + + public function copy_entire_suspended_tables_sale($sale_id) + { + $this->empty_cart(); + $this->remove_customer(); + foreach($this->CI->Sale_suspended->get_sale_items($sale_id)->result() as $row) { $this->add_item($row->item_id, $row->quantity_purchased, $row->item_location, $row->discount_percent, $row->item_unit_price, @@ -810,6 +942,37 @@ class Sale_lib $this->clear_giftcard_remainder(); $this->empty_payments(); $this->remove_customer(); + $this->clear_cash_flags(); + } + + public function clear_cash_flags() + { + $this->CI->session->unset_userdata('cash_rounding'); + $this->CI->session->unset_userdata('cash_mode'); + $this->CI->session->unset_userdata('payment_type'); + } + + public function reset_cash_flags() + { + if($this->CI->lang->line('payment_options_order') != 'cashdebitcredit') + { + $cash_mode = 1; + } + else + { + $cash_mode = 0; + } + $this->CI->session->set_userdata('cash_mode', $cash_mode); + + if($this->CI->config->config['cash_decimals'] < $this->CI->config->config['currency_decimals']) + { + $cash_rounding = 1; + } + else + { + $cash_rounding = 0; + } + $this->CI->session->set_userdata('cash_rounding', $cash_rounding); } public function is_customer_taxable() @@ -821,34 +984,68 @@ class Sale_lib return $customer->taxable or $customer_id == -1; } + /* + * This returns taxes for VAT taxes and for pre 3.1.0 sales taxes. + */ public function get_taxes() { - $taxes = array(); - + $register_mode = $this->CI->config->config['default_register_mode']; + $tax_decimals = $this->CI->config->config['tax_decimals']; + $customer_id = $this->get_customer(); + $customer = $this->CI->Customer->get_info($customer_id); + $sales_taxes = array(); //Do not charge sales tax if we have a customer that is not taxable - if($this->is_customer_taxable()) + if($customer->taxable or $customer_id == -1) { foreach($this->get_cart() as $line => $item) { - $tax_info = $this->CI->Item_taxes->get_info($item['item_id']); + // Start of current VAT tax apply + $tax_info = $this->CI->Item_taxes->get_info($item['item_id']); + $tax_group_sequence = 0; foreach($tax_info as $tax) { - $name = to_tax_decimals($tax['percent']) . '% ' . $tax['name']; - $tax_amount = $this->get_item_tax($item['quantity'], $item['price'], $item['discount'], $tax['percent']); + // This computes tax for each line item and adds it to the tax type total + $tax_group = (float)$tax['percent'] . '% ' . $tax['name']; + $tax_type = Tax_lib::TAX_TYPE_VAT; + $tax_basis = $this->get_item_total($item['quantity'], $item['price'], $item['discount'], TRUE); + $tax_amount = 0; - if(!isset($taxes[$name])) + if($this->CI->config->config['tax_included']) { - $taxes[$name] = 0; + $tax_amount = $this->get_item_tax($item['quantity'], $item['price'], $item['discount'], $tax['percent']); + } + elseif($this->CI->config->config['customer_sales_tax_support'] == '0') + { + $tax_amount = $this->CI->tax_lib->get_sales_tax_for_amount($tax_basis, $tax['percent'], '0', $tax_decimals); } - $precision = $this->CI->config->item('currency_decimals'); - $taxes[$name] = bcadd($taxes[$name], round($tax_amount, $precision, PHP_ROUND_HALF_EVEN)); + if($tax_amount <> 0) + { + $this->CI->tax_lib->update_sales_taxes($sales_taxes, $tax_type, $tax_group, $tax['percent'], $tax_basis, $tax_amount, $tax_group_sequence, '0', -1); + $tax_group_sequence += 1; + } } + + $tax_category = ''; + $tax_rate = ''; + $rounding_code = Rounding_code::HALF_UP; + $tax_group_sequence = 0; + $tax_code = ''; + + if($this->CI->config->config['customer_sales_tax_support'] == '1') + { + // Now calculate what the sales taxes should be (storing them in the $sales_taxes array + $this->CI->tax_lib->apply_sales_tax($item, $customer->city, $customer->state, $customer->sales_tax_code, $register_mode, 0, $sales_taxes, $tax_category, $tax_rate, $rounding_code, $tax_group_sequence, $tax_code); + } + } + + $this->CI->tax_lib->round_sales_taxes($sales_taxes); + } - return $taxes; + return $sales_taxes; } public function apply_customer_discount($discount_percent) @@ -944,7 +1141,7 @@ class Sale_lib return bcmul($price, $tax_fraction); } - public function calculate_subtotal($include_discount = FALSE, $exclude_tax = FALSE) + public function calculate_subtotal($include_discount = FALSE, $exclude_tax = FALSE) { $subtotal = 0; foreach($this->get_cart() as $item) @@ -964,22 +1161,67 @@ class Sale_lib public function get_total() { - $total = $this->calculate_subtotal(TRUE); - if(!$this->CI->config->config['tax_included']) + $total = $this->calculate_subtotal(TRUE); + + $cash_rounding = $this->CI->session->userdata('cash_rounding'); + + foreach($this->get_taxes() as $sales_tax) { - foreach($this->get_taxes() as $tax) - { - $total = bcadd($total, $tax); - } + $total = bcadd($total, $sales_tax['sale_tax_amount']); } + if($cash_rounding) + { + $rounded_total = $this->check_for_cash_rounding($total); + return $rounded_total; + } return $total; } - public function get_empty_tables() + public function get_empty_tables() { return $this->CI->Dinner_table->get_empty_tables(); } + + public function check_for_cash_rounding($total) + { + $cash_decimals = $this->CI->config->config['cash_decimals']; + $cash_rounding_code = $this->CI->config->config['cash_rounding_code']; + $rounded_total = $total; + + if($cash_rounding_code == Rounding_code::HALF_UP) + { + $rounded_total = round ( $total, $cash_decimals, PHP_ROUND_HALF_UP); + } + elseif($cash_rounding_code == Rounding_code::HALF_DOWN) + { + $rounded_total = round ( $total, $cash_decimals, PHP_ROUND_HALF_DOWN); + } + elseif($cash_rounding_code == Rounding_code::HALF_EVEN) + { + $rounded_total = round ( $total, $cash_decimals, PHP_ROUND_HALF_EVEN); + } + elseif($cash_rounding_code == Rounding_code::HALF_ODD) + { + $rounded_total = round ( $total, $cash_decimals, PHP_ROUND_HALF_UP); + } + elseif($cash_rounding_code == Rounding_code::ROUND_UP) + { + $fig = (int) str_pad('1', $cash_decimals, '0'); + $rounded_total = (ceil($total * $fig) / $fig); + } + elseif($cash_rounding_code == Rounding_code::ROUND_DOWN) + { + $fig = (int) str_pad('1', $cash_decimals, '0'); + $rounded_total = (floor($total * $fig) / $fig); + } + elseif($cash_rounding_code == Rounding_code::HALF_FIVE) + { + $rounded_total = round($total / 5) * 5; + } + + return $rounded_total; + } } ?> diff --git a/application/libraries/Tax_lib.php b/application/libraries/Tax_lib.php new file mode 100644 index 000000000..aefd54f89 --- /dev/null +++ b/application/libraries/Tax_lib.php @@ -0,0 +1,320 @@ +CI =& get_instance(); + $this->CI->load->library('sale_lib'); + } + + public function get_tax_types() + { + return array( + TAX_TYPE_SALES => $this->CI->lang->line('taxes_sales_tax'), + TAX_TYPE_SALES_BY_INVOICE => $this->CI->lang->line('taxes_sales_tax_by_invoice'), + TAX_TYPE_VAT => $this->CI->lang->line('taxes_vat_tax') + ); + } + + /* + * Compute the tax basis and returns the tax amount + */ + public function get_item_sales_tax($quantity, $price, $discount_percentage, $tax_percentage, $rounding_code) + { + $tax_decimals = $this->CI->config->config['tax_decimals']; + + // The tax basis should be returned at the currency scale + $tax_basis = $this->CI->sale_lib->get_item_total($quantity, $price, $discount_percentage, TRUE); + + return $this->get_sales_tax_for_amount($tax_basis, $tax_percentage, $rounding_code, $tax_decimals); + } + + /* + * Computes the item level sales tax amount for a given tax basis + */ + public function get_sales_tax_for_amount($tax_basis, $tax_percentage, $rounding_code, $decimals) + { + + $tax_fraction = bcdiv($tax_percentage, 100); + + $tax_amount = bcmul($tax_basis, $tax_fraction); + $rounded_tax_amount = $tax_amount; + + if($rounding_code == Rounding_code::HALF_UP) + { + $rounded_tax_amount = round ( $tax_amount, $decimals, PHP_ROUND_HALF_UP); + } + elseif($rounding_code == Rounding_code::HALF_DOWN) + { + $rounded_tax_amount = round ( $tax_amount, $decimals, PHP_ROUND_HALF_DOWN); + } + elseif($rounding_code == Rounding_code::HALF_EVEN) + { + $rounded_tax_amount = round ( $tax_amount, $decimals, PHP_ROUND_HALF_EVEN); + } + elseif($rounding_code == Rounding_code::HALF_ODD) + { + $rounded_tax_amount = round ( $tax_amount, $decimals, PHP_ROUND_HALF_UP); + } + elseif($rounding_code == Rounding_code::ROUND_UP) // ROUND_UP + { + $fig = (int) str_pad('1', $decimals, '0'); + $rounded_tax_amount = (ceil($tax_amount * $fig) / $fig); + } + elseif($rounding_code == Rounding_code::ROUND_DOWN) // ROUND_DOWN + { + $fig = (int) str_pad('1', $decimals, '0'); + $rounded_tax_amount = (floor($tax_amount * $fig) / $fig); + } + elseif($rounding_code == Rounding_code::HALF_FIVE) + { + $rounded_tax_amount = round($tax_amount / 5) * 5; + } + + return $rounded_tax_amount; + } + + /* + * Updates the sales_tax array which is later saved to the `sales_taxes` table and used for printing taxes on receipts and invoices + */ + public function update_sales_taxes(&$sales_taxes, $tax_type, $tax_group, $tax_rate, $tax_basis, $item_tax_amount, $tax_group_sequence, $rounding_code, $sale_id, $name='', $tax_code='') + { + + $tax_group_index = $this->clean('X'.$tax_group); + + if ($item_tax_amount != 0) { + if(!array_key_exists($tax_group_index, $sales_taxes)) + { + $insertkey = $tax_group_index; + + $sales_tax = array($insertkey => array( + 'sale_id' => $sale_id, + 'tax_type' => $tax_type, + 'tax_group' => $tax_group, + 'sale_tax_basis' => $tax_basis, + 'sale_tax_amount' => $item_tax_amount, + 'print_sequence' => $tax_group_sequence, + 'name' => $name, + 'tax_rate' => $tax_rate, + 'sales_tax_code' => $tax_code, + 'rounding_code' => $rounding_code + )); + + //add to existing array + $sales_taxes += $sales_tax; + } + else + { + // Important ... the sales amounts are accumulated for the group at the maximum configurable scale value of 4 + // but the scale will in reality be the scale specified by the tax_decimal configuration value used for sales_items_taxes + $sales_taxes[$tax_group_index]['sale_tax_basis'] = bcadd($sales_taxes[$tax_group_index]['sale_tax_basis'], $tax_basis, 4); + $sales_taxes[$tax_group_index]['sale_tax_amount'] = bcadd($sales_taxes[$tax_group_index]['sale_tax_amount'], $item_tax_amount, 4); + } + } + } + + /* + * If invoice taxing (as opposed to invoice_item_taxing) rules apply then recalculate the sales tax after tax group totals are final + */ + public function apply_invoice_taxing(&$sales_taxes) + { + if(!empty($sales_taxes)) + { + $sort = array(); + foreach($sales_taxes as $k => $v) + { + $sort['print_sequence'][$k] = $v['print_sequence']; + } + array_multisort($sort['print_sequence'], SORT_ASC, $sales_taxes); + } + + $currency_decimals = $this->CI->config->config['currency_decimals']; + + foreach($sales_taxes as $row_number => $sales_tax) + { + $sales_tax['sale_tax_amount'] = get_sales_tax_for_amount($sales_tax['sale_tax_basis'], $sales_tax['sale_tax_rate'], $sales_tax['rounding_code'], $currency_decimals); + } + } + + /* + * Apply rounding rules to the accumulated sales tax amounts + */ + public function round_sales_taxes(&$sales_taxes) + { + if (!empty($sales_taxes)) + { + $sort = array(); + foreach($sales_taxes as $k=>$v) { + $sort['print_sequence'][$k] = $v['print_sequence']; + } + array_multisort($sort['print_sequence'], SORT_ASC, $sales_taxes); + } + + $currency_decimals = $this->CI->config->config['currency_decimals']; + + foreach($sales_taxes as $row_number => $sales_tax) + { + $sale_tax_amount = $sales_tax['sale_tax_amount']; + $rounding_code = $sales_tax['rounding_code']; + $rounded_sale_tax_amount = $sale_tax_amount; + + if ($rounding_code == Rounding_code::HALF_UP) + { + $rounded_sale_tax_amount = round ( $sale_tax_amount, $currency_decimals, PHP_ROUND_HALF_UP); + } + elseif($rounding_code == Rounding_code::HALF_DOWN) + { + $rounded_sale_tax_amount = round ( $sale_tax_amount, $currency_decimals, PHP_ROUND_HALF_DOWN); + } + elseif($rounding_code == Rounding_code::HALF_EVEN) + { + $rounded_sale_tax_amount = round ( $sale_tax_amount, $currency_decimals, PHP_ROUND_HALF_EVEN); + } + elseif($rounding_code == Rounding_code::HALF_ODD) + { + $rounded_sale_tax_amount = round ( $sale_tax_amount, $currency_decimals, PHP_ROUND_HALF_UP); + } + elseif($rounding_code == Rounding_code::ROUND_UP) + { + $fig = (int) str_pad('1', $currency_decimals, '0'); + $rounded_sale_tax_amount = (ceil($sale_tax_amount * $fig) / $fig); + } + elseif($rounding_code == Rounding_code::ROUND_DOWN) + { + $fig = (int) str_pad('1', $currency_decimals, '0'); + $rounded_sale_tax_amount = (floor($sale_tax_amount * $fig) / $fig); + } + elseif($rounding_code == Rounding_code::HALF_FIVE) + { + $rounded_sale_tax_amount = round($sale_tax_amount / 5) * 5; + } + + $sales_taxes[$row_number]['sale_tax_amount'] = $rounded_sale_tax_amount; + } + } + + + /** + * Determine the applicable tax code and then determine the tax amount to be applied. + * If a tax amount was identified then accumulate into the sales_taxes array + */ + public function apply_sales_tax(&$item, &$city, &$state, &$sales_tax_code, $register_mode, $sale_id, &$sales_taxes) + { + $tax_code = $this->get_applicable_tax_mode($register_mode, $city, $state, $sales_tax_code); + + // If tax code cannot be determined or the price is zero then skip this item + if($tax_code != '' && $item['price'] != 0) + { + $tax_rate = 0.0000; + $rounding_code = Rounding_code::HALF_UP; + + $tax_code_obj = $this->CI->Tax->get_info($tax_code); + $tax_category_id = $item['tax_category_id']; + + if($tax_category_id != 0) + { + $tax_rate_info = $this->CI->Tax->get_rate_info($tax_code, $tax_category_id); + if ($tax_rate_info) + { + $tax_rate = $tax_rate_info->tax_rate; + $rounding_code = $tax_rate_info->rounding_code; + } + else + { + $tax_rate = $tax_code_obj->tax_rate; + $rounding_code = $tax_code_obj->rounding_code; + } + } + + if($tax_category_id != 0) + { + $tax_rate_info = $this->CI->Tax->get_rate_info($tax_code, $tax_category_id); + $tax_rate = $tax_rate_info->tax_rate; + $rounding_code = $tax_rate_info->rounding_code; + $tax_group_sequence = $tax_rate_info->tax_group_sequence; + $tax_category = $tax_rate_info->tax_category; + } + else + { + $tax_rate = $tax_code_obj->tax_rate; + $rounding_code = $tax_code_obj->rounding_code; + $tax_group_sequence = $tax_code_obj->tax_group_sequence; + $tax_category = $tax_code_obj->tax_category; + } + + $tax_decimals = $this->CI->config->config['tax_decimals']; + + // The tax basis should be returned at the currency scale + $tax_basis = $this->CI->sale_lib->get_item_total($item['quantity'], $item['price'], $item['discount'], TRUE); + $tax_amount = $this->get_sales_tax_for_amount($tax_basis, $tax_rate, $rounding_code, $tax_decimals); + + $tax_group = (float)$tax_rate . '% ' . $tax_category; + $tax_type = Tax_lib::TAX_TYPE_SALES; + + if($tax_amount != 0) + { + $this->update_sales_taxes($sales_taxes, $tax_type, $tax_group, $tax_rate, $tax_basis, $tax_amount, $tax_group_sequence, $rounding_code, $sale_id, $tax_category, $tax_code); + } + + // input : register_mode + // input : city + // input : state + // input : sales_tax_code + // input : $item['price'] + // input : $item['tax_category_id'] + // input : $item['quantity'] + // input : $item['price'] + // input : $item['discount'] + // both : $sales_taxes + // output : tax_details['tax_rate'] + // output : tax_details['rounding_code'] + // output : tax_details['tax_group_sequence'] + // output : tax_details['tax_code'] + + $tax_details = array('item_tax_amount' => $tax_amount, 'tax_group' => $tax_group, 'tax_name' => $tax_category, 'tax_rate' => $tax_rate, 'rounding_code' => $rounding_code, 'tax_group_sequence' => $tax_group_sequence, 'tax_code' => $tax_code); + + return $tax_details; + } + else + { + $tax_details = array('item_tax_amount' => 0.0000, 'tax_group' => '', 'tax_name' => '', 'tax_rate' => 0.0000, 'rounding_code' => '0', 'tax_group_sequence' => '0', 'tax_code' => ''); + return $tax_details; + } + } + + public function get_applicable_tax_mode($register_mode, $city, $state, $sales_tax_code) + { + if ($register_mode == "SALE") + { + $tax_code = $this->CI->config->config['default_origin_tax_code']; // overrides customer assigned code + } + else + { + if ($sales_tax_code == '') + { + $tax_code = $this->CI->Tax->get_sales_tax_code($city, $state); + } + else + { + // Use the customer assigned tax rate code + $tax_code = $sales_tax_code; + } + } + return $tax_code; + } + + function clean($string) { + $string = str_replace(' ', '-', $string); // Replaces all spaces with hyphens. + + return preg_replace('/[^A-Za-z0-9\-]/', '', $string); // Removes special chars. + } +} + +?> \ No newline at end of file diff --git a/application/libraries/tokens/Token_suspended_invoice_count.php b/application/libraries/tokens/Token_suspended_invoice_count.php index 5b317943e..2809103b4 100644 --- a/application/libraries/tokens/Token_suspended_invoice_count.php +++ b/application/libraries/tokens/Token_suspended_invoice_count.php @@ -8,12 +8,12 @@ class Token_suspended_invoice_count extends Token { parent::__construct(); $this->CI =& get_instance(); - $this->CI->load->model('Sale_suspended'); + $this->CI->load->model('Sale'); } public function get_value() { - return $this->CI->Sale_suspended->get_invoice_count(); + return $this->CI->Sale->get_suspended_invoice_count(); } } diff --git a/application/models/Customer.php b/application/models/Customer.php index 4e35a4b83..f2c6dce08 100644 --- a/application/models/Customer.php +++ b/application/models/Customer.php @@ -97,6 +97,7 @@ class Customer extends Person $this->db->from('sales'); $this->db->join('sales_payments', 'sales.sale_id = sales_payments.sale_id'); $this->db->where('sales.customer_id', $customer_id); + $this->db->where('sale_status',0); return $this->db->get()->row(); } @@ -123,7 +124,7 @@ class Customer extends Person //Run these queries as a transaction, we want to make sure we do all or nothing $this->db->trans_start(); - + if(parent::save($person_data, $customer_id)) { if(!$customer_id || !$this->exists($customer_id)) @@ -139,7 +140,7 @@ class Customer extends Person } $this->db->trans_complete(); - + $success &= $this->db->trans_status(); return $success; diff --git a/application/models/Dinner_table.php b/application/models/Dinner_table.php index e34d65b48..96a3c4acd 100644 --- a/application/models/Dinner_table.php +++ b/application/models/Dinner_table.php @@ -57,17 +57,17 @@ class Dinner_table extends CI_Model public function get_name($dinner_table_id) { - $this->db->from('dinner_tables'); - $this->db->where('dinner_table_id', $dinner_table_id); - - $row = $this->db->get()->row(); - - if(isset($row->name)) + if($dinner_table_id == null || empty($dinner_table_id)) { - return $row->name; + return ''; } + else + { + $this->db->from('dinner_tables'); + $this->db->where('dinner_table_id',$dinner_table_id); - return ''; + return $this->db->get()->row()->name; + } } public function get_all() diff --git a/application/models/Item.php b/application/models/Item.php index daf5666c4..a1c64299a 100644 --- a/application/models/Item.php +++ b/application/models/Item.php @@ -414,6 +414,7 @@ class Item extends CI_Model $this->db->select('item_id, item_number'); $this->db->from('items'); $this->db->where('deleted', $filters['is_deleted']); + $this->db->where("item_type = '0'"); // standard, exclude kit items since kits will be picked up later $this->db->like('item_number', $search); $this->db->order_by('item_number', 'asc'); foreach($this->db->get()->result() as $row) @@ -427,6 +428,7 @@ class Item extends CI_Model $this->db->select('category'); $this->db->from('items'); $this->db->where('deleted', $filters['is_deleted']); + $this->db->where("item_type = '0'"); // standard, exclude kit items since kits will be picked up later $this->db->distinct(); $this->db->like('category', $search); $this->db->order_by('category', 'asc'); @@ -441,6 +443,7 @@ class Item extends CI_Model $this->db->like('company_name', $search); // restrict to non deleted companies only if is_deleted is FALSE $this->db->where('deleted', $filters['is_deleted']); + $this->db->where("item_type = '0'"); // standard, exclude kit items since kits will be picked up later $this->db->distinct(); $this->db->order_by('company_name', 'asc'); foreach($this->db->get()->result() as $row) @@ -452,6 +455,7 @@ class Item extends CI_Model $this->db->select('item_id, name, description'); $this->db->from('items'); $this->db->where('deleted', $filters['is_deleted']); + $this->db->where("item_type = '0'"); // standard, exclude kit items since kits will be picked up later $this->db->like('description', $search); $this->db->order_by('description', 'asc'); foreach($this->db->get()->result() as $row) @@ -480,6 +484,7 @@ class Item extends CI_Model $this->db->or_like('custom10', $search); $this->db->group_end(); $this->db->where('deleted', $filters['is_deleted']); + $this->db->where("item_type = '0'"); // standard, exclude kit items since kits will be picked up later foreach($this->db->get()->result() as $row) { $suggestions[] = array('value' => $row->item_id, 'label' => $row->name); @@ -487,6 +492,118 @@ class Item extends CI_Model } } + + //only return $limit suggestions + if(count($suggestions > $limit)) + { + $suggestions = array_slice($suggestions, 0,$limit); + } + + return $suggestions; + } + + + public function get_stock_search_suggestions($search, $filters = array('is_deleted' => FALSE, 'search_custom' => FALSE), $unique = FALSE, $limit = 25) + { + $suggestions = array(); + + $this->db->select('item_id, name'); + $this->db->from('items'); + $this->db->where('deleted', $filters['is_deleted']); + $this->db->where("item_type = '0'"); // standard, exclude kit items since kits will be picked up later + $this->db->where("stock_type = '0'"); // stocked items only + $this->db->like('name', $search); + $this->db->order_by('name', 'asc'); + foreach($this->db->get()->result() as $row) + { + $suggestions[] = array('value' => $row->item_id, 'label' => $row->name); + } + + $this->db->select('item_id, item_number'); + $this->db->from('items'); + $this->db->where('deleted', $filters['is_deleted']); + $this->db->where("item_type = '0'"); // standard, exclude kit items since kits will be picked up later + $this->db->where("stock_type = '0'"); // stocked items only + $this->db->like('item_number', $search); + $this->db->order_by('item_number', 'asc'); + foreach($this->db->get()->result() as $row) + { + $suggestions[] = array('value' => $row->item_id, 'label' => $row->item_number); + } + + if(!$unique) + { + //Search by category + $this->db->select('category'); + $this->db->from('items'); + $this->db->where('deleted', $filters['is_deleted']); + $this->db->where("item_type = '0'"); // standard, exclude kit items since kits will be picked up later + $this->db->where("stock_type = '0'"); // stocked items only + $this->db->distinct(); + $this->db->like('category', $search); + $this->db->order_by('category', 'asc'); + foreach($this->db->get()->result() as $row) + { + $suggestions[] = array('label' => $row->category); + } + + //Search by supplier + $this->db->select('company_name'); + $this->db->from('suppliers'); + $this->db->like('company_name', $search); + // restrict to non deleted companies only if is_deleted is FALSE + $this->db->where('deleted', $filters['is_deleted']); + $this->db->distinct(); + $this->db->order_by('company_name', 'asc'); + foreach($this->db->get()->result() as $row) + { + $suggestions[] = array('label' => $row->company_name); + } + + //Search by description + $this->db->select('item_id, name, description'); + $this->db->from('items'); + $this->db->where('deleted', $filters['is_deleted']); + $this->db->where("item_type = '0'"); // standard, exclude kit items since kits will be picked up later + $this->db->where("stock_type = '0'"); // stocked items only + $this->db->like('description', $search); + $this->db->order_by('description', 'asc'); + foreach($this->db->get()->result() as $row) + { + $entry = array('value' => $row->item_id, 'label' => $row->name); + if(!array_walk($suggestions, function($value, $label) use ($entry) { return $entry['label'] != $label; } )) + { + $suggestions[] = $entry; + } + } + + //Search by custom fields + if($filters['search_custom'] != FALSE) + { + $this->db->from('items'); + $this->db->group_start(); + $this->db->like('custom1', $search); + $this->db->or_like('custom2', $search); + $this->db->or_like('custom3', $search); + $this->db->or_like('custom4', $search); + $this->db->or_like('custom5', $search); + $this->db->or_like('custom6', $search); + $this->db->or_like('custom7', $search); + $this->db->or_like('custom8', $search); + $this->db->or_like('custom9', $search); + $this->db->or_like('custom10', $search); + $this->db->group_end(); + $this->db->where("item_type = '0'"); // standard, exclude kit items since kits will be picked up later + $this->db->where("stock_type = '0'"); // stocked items only + $this->db->where('deleted', $filters['is_deleted']); + foreach($this->db->get()->result() as $row) + { + $suggestions[] = array('value' => $row->item_id, 'label' => $row->name); + } + } + } + + //only return $limit suggestions if(count($suggestions > $limit)) { diff --git a/application/models/Sale.php b/application/models/Sale.php index 30d9ab4e7..4b0b61413 100644 --- a/application/models/Sale.php +++ b/application/models/Sale.php @@ -42,6 +42,7 @@ class Sale extends CI_Model '( SELECT sales_items_taxes.sale_id AS sale_id, sales_items_taxes.item_id AS item_id, + sales_items_taxes.line AS line, ' . " IFNULL(ROUND($sale_tax, $decimals), 0) AS tax " . ' @@ -51,7 +52,7 @@ class Sale extends CI_Model INNER JOIN ' . $this->db->dbprefix('sales_items') . ' AS sales_items ON sales_items.sale_id = sales_items_taxes.sale_id AND sales_items.line = sales_items_taxes.line WHERE sales.sale_id = ' . $this->db->escape($sale_id) . ' - GROUP BY sale_id, item_id + GROUP BY sale_id, item_id, line )' ); @@ -60,7 +61,9 @@ class Sale extends CI_Model MAX(DATE(sales.sale_time)) AS sale_date, MAX(sales.sale_time) AS sale_time, MAX(sales.comment) AS comment, + MAX(sales.sale_status) AS sale_status, MAX(sales.invoice_number) AS invoice_number, + MAX(sales.quote_number) AS quote_number, MAX(sales.employee_id) AS employee_id, MAX(sales.customer_id) AS customer_id, MAX(CONCAT(customer_p.first_name, " ", customer_p.last_name)) AS customer_name, @@ -81,7 +84,9 @@ class Sale extends CI_Model $this->db->join('people AS customer_p', 'sales.customer_id = customer_p.person_id', 'left'); $this->db->join('customers AS customer', 'sales.customer_id = customer.person_id', 'left'); $this->db->join('sales_payments_temp AS payments', 'sales.sale_id = payments.sale_id', 'left outer'); - $this->db->join('sales_items_taxes_temp AS sales_items_taxes', 'sales_items.sale_id = sales_items_taxes.sale_id AND sales_items.item_id = sales_items_taxes.item_id', 'left outer'); + $this->db->join('sales_items_taxes_temp AS sales_items_taxes', + 'sales_items.sale_id = sales_items_taxes.sale_id AND sales_items.item_id = sales_items_taxes.item_id AND sales_items.line = sales_items_taxes.line', + 'left outer'); $this->db->where('sales.sale_id', $sale_id); @@ -108,11 +113,11 @@ class Sale extends CI_Model if (empty($this->config->item('date_or_time_format'))) { - $where .= 'DATE(sales.sale_time) BETWEEN ' . $this->db->escape($filters['start_date']) . ' AND ' . $this->db->escape($filters['end_date']) . ' '; + $where .= 'DATE(sales.sale_time) BETWEEN ' . $this->db->escape($filters['start_date']) . ' AND ' . $this->db->escape($filters['end_date']) . ' AND sales.sale_status = 0 '; } else { - $where .= 'sales.sale_time BETWEEN ' . $this->db->escape(rawurldecode($filters['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($filters['end_date'])) . ' '; + $where .= 'sales.sale_time BETWEEN ' . $this->db->escape(rawurldecode($filters['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($filters['end_date'])) . ' AND sales.sale_status = 0 '; } // NOTE: temporary tables are created to speed up searches due to the fact that they are ortogonal to the main query @@ -156,6 +161,7 @@ class Sale extends CI_Model ( SELECT sales_items_taxes.sale_id AS sale_id, sales_items_taxes.item_id AS item_id, + sales_items_taxes.line AS line, ' . " IFNULL(ROUND($sale_tax, $decimals), 0) AS tax " . ' @@ -165,7 +171,7 @@ class Sale extends CI_Model INNER JOIN ' . $this->db->dbprefix('sales_items') . ' AS sales_items ON sales_items.sale_id = sales_items_taxes.sale_id AND sales_items.line = sales_items_taxes.line WHERE ' . $where . ' - GROUP BY sale_id, item_id + GROUP BY sale_id, item_id, line )' ); @@ -174,6 +180,7 @@ class Sale extends CI_Model MAX(DATE(sales.sale_time)) AS sale_date, MAX(sales.sale_time) AS sale_time, MAX(sales.invoice_number) AS invoice_number, + MAX(sales.quote_number) AS quote_number, SUM(sales_items.quantity_purchased) AS items_purchased, MAX(CONCAT(customer_p.first_name, " ", customer_p.last_name)) AS customer_name, MAX(customer.company_name) AS company_name, @@ -195,7 +202,9 @@ class Sale extends CI_Model $this->db->join('people AS customer_p', 'sales.customer_id = customer_p.person_id', 'left'); $this->db->join('customers AS customer', 'sales.customer_id = customer.person_id', 'left'); $this->db->join('sales_payments_temp AS payments', 'sales.sale_id = payments.sale_id', 'left outer'); - $this->db->join('sales_items_taxes_temp AS sales_items_taxes', 'sales_items.sale_id = sales_items_taxes.sale_id AND sales_items.item_id = sales_items_taxes.item_id', 'left outer'); + $this->db->join('sales_items_taxes_temp AS sales_items_taxes', + 'sales_items.sale_id = sales_items_taxes.sale_id AND sales_items.item_id = sales_items_taxes.item_id AND sales_items.line = sales_items_taxes.line', + 'left outer'); $this->db->where($where); @@ -486,8 +495,15 @@ class Sale extends CI_Model return $success; } - public function save($items, $customer_id, $employee_id, $comment, $invoice_number, $payments, $dinner_table, $sale_id = FALSE) + + /* + * Save the sale information after the sales is complete but before the final document is printed + * The sales_taxes variable needs to be initialized to an empty array before calling + */ + public function save(&$sale_status, &$items, $customer_id, $employee_id, $comment, $invoice_number, $quote_number, $payments, $dinner_table, &$sales_taxes, $sale_id = FALSE) { + $tax_decimals = $this->config->item('tax_decimals'); + if(count($items) == 0) { return -1; @@ -499,7 +515,9 @@ class Sale extends CI_Model 'employee_id' => $employee_id, 'comment' => $comment, 'invoice_number' => $invoice_number, - 'dinner_table_id'=> $dinner_table + 'quote_number' => $quote_number, + 'dinner_table_id'=> $dinner_table, + 'sale_status' => $sale_status ); // Run these queries as a transaction, we want to make sure we do all or nothing @@ -554,6 +572,10 @@ class Sale extends CI_Model } } + $customer = $this->Customer->get_info($customer_id); + + $sales_taxes = array(); + foreach($items as $line=>$item) { $cur_item_info = $this->Item->get_info($item['item_id']); @@ -602,22 +624,82 @@ class Sale extends CI_Model ); $this->Inventory->insert($inv_data); - $customer = $this->Customer->get_info($customer_id); + // Calculate taxes and save the tax information for the sale. Return the result for printing + if($customer_id == -1 || $customer->taxable) { + if($this->config->item('tax_included')) + { + $tax_type = Tax_lib::TAX_TYPE_VAT; + } + else + { + $tax_type = Tax_lib::TAX_TYPE_SALES; + } + $rounding_code = Rounding_code::HALF_UP; // half adjust + $tax_group_sequence = 0; + $item_total = $this->sale_lib->get_item_total($item['quantity'], $item['price'], $item['discount'], TRUE); + $tax_basis = $item_total; + $item_tax_amount = 0; + foreach($this->Item_taxes->get_info($item['item_id']) as $row) { - $this->db->insert('sales_items_taxes', array( - 'sale_id' => $sale_id, - 'item_id' => $item['item_id'], - 'line' => $item['line'], - 'name' => $row['name'], - 'percent' => $row['percent'] - )); + + $sales_items_taxes = array( + 'sale_id' => $sale_id, + 'item_id' => $item['item_id'], + 'line' => $item['line'], + 'name' => character_limiter($row['name'], 255), + 'percent' => $row['percent'], + 'tax_type' => $tax_type, + 'rounding_code' => $rounding_code, + 'cascade_tax' => 0, + 'cascade_sequence' => 0, + 'item_tax_amount' => 0 + ); + + // This computes tax for each line item and adds it to the tax type total + $tax_group = (float)$row['percent'] . '% ' . $row['name']; + $tax_basis = $this->sale_lib->get_item_total($item['quantity'], $item['price'], $item['discount'], TRUE); + + if($this->config->item('tax_included')) + { + $tax_type = Tax_lib::TAX_TYPE_VAT; + $item_tax_amount = $this->sale_lib->get_item_tax($item['quantity'], $item['price'], $item['discount'],$row['percent']); + } + elseif($this->config->item('customer_sales_tax_support') == '0') + { + $tax_type = Tax_lib::TAX_TYPE_SALES; + $item_tax_amount = $this->tax_lib->get_sales_tax_for_amount($tax_basis, $row['percent'], '0', $tax_decimals); + } + else + { + $tax_type = Tax_lib::TAX_TYPE_SALES; + } + + $sales_items_taxes['item_tax_amount'] = $item_tax_amount; + if ($item_tax_amount != 0) + { + $this->db->insert('sales_items_taxes', $sales_items_taxes); + $this->tax_lib->update_sales_taxes($sales_taxes, $tax_type, $tax_group, $row['percent'], $tax_basis, $item_tax_amount, $tax_group_sequence, $rounding_code, $sale_id, $row['name'], ''); + $tax_group_sequence += 1; + } + + } + + if($this->config->item('customer_sales_tax_support') == '1') + { + $this->save_sales_item_tax($customer, $sale_id, $item, $item_total, $sales_taxes, $sequence, $cur_item_info->tax_category_id); } } } + if($customer_id == -1 || $customer->taxable) + { + $this->tax_lib->round_sales_taxes($sales_taxes); + $this->save_sales_tax($sales_taxes); + } + $this->db->trans_complete(); if($this->db->trans_status() === FALSE) @@ -628,6 +710,47 @@ class Sale extends CI_Model return $sale_id; } + /** + * Apply customer sales tax if the customer sales tax is enabledl + * The original tax is still supported if the user configures it, + * but it won't make sense unless it's used exclusively for the purpose + * of VAT tax which becomes a price component. VAT taxes must still be reported + * as a separate tax entry on the invoice. + */ + public function save_sales_item_tax(&$customer, &$sale_id, &$item, $tax_basis, &$sales_taxes, &$sequence, $tax_category_id) + { + // if customer sales tax is enabled then update sales_items_taxes with the + if($this->config->item('customer_sales_tax_support') == '1') + { + $register_mode = $this->config->item('default_register_mode'); + $tax_details = $this->tax_lib->apply_sales_tax($item, $customer->city, $customer->state, $customer->sales_tax_code, $register_mode, $sale_id, $sales_taxes); + + $sales_items_taxes = array( + 'sale_id' => $sale_id, + 'item_id' => $item['item_id'], + 'line' => $item['line'], + 'name' => $tax_details['tax_name'], + 'percent' => $tax_details['tax_rate'], + 'tax_type' => Tax_lib::TAX_TYPE_SALES, + 'rounding_code' => $tax_details['rounding_code'], + 'cascade_tax' => 0, + 'cascade_sequence' => 0, + 'item_tax_amount' => $tax_details['item_tax_amount'] + ); + + $this->db->insert('sales_items_taxes', $sales_items_taxes); + + } + } + + private function save_sales_tax(&$sales_taxes) + { + foreach($sales_taxes as $line=>$sales_tax) + { + $this->db->insert('sales_taxes', $sales_tax); + } + } + public function delete_list($sale_ids, $employee_id, $update_inventory = TRUE) { $result = TRUE; @@ -815,8 +938,8 @@ class Sale extends CI_Model // TODO change to use new quote_number field public function check_quote_number_exists($quote_number, $sale_id = '') { - $this->db->from('sales_suspended'); - $this->db->where('invoice_number', $quote_number); + $this->db->from('sales'); + $this->db->where('quote_number', $quote_number); if(!empty($sale_id)) { $this->db->where('sale_id !=', $sale_id); @@ -894,6 +1017,7 @@ class Sale extends CI_Model ( SELECT sales_items_taxes.sale_id AS sale_id, sales_items_taxes.item_id AS item_id, + sales_items_taxes.line AS line, ' . " IFNULL(ROUND($sale_tax, $decimals), 0) AS tax " . ' @@ -902,8 +1026,8 @@ class Sale extends CI_Model ON sales.sale_id = sales_items_taxes.sale_id INNER JOIN ' . $this->db->dbprefix('sales_items') . ' AS sales_items ON sales_items.sale_id = sales_items_taxes.sale_id AND sales_items.line = sales_items_taxes.line - WHERE ' . $where . ' - GROUP BY sale_id, item_id + WHERE sales.sale_status = 0 AND ' . $where . ' + GROUP BY sale_id, item_id, line )' ); @@ -917,7 +1041,7 @@ class Sale extends CI_Model FROM ' . $this->db->dbprefix('sales_payments') . ' AS payments INNER JOIN ' . $this->db->dbprefix('sales') . ' AS sales ON sales.sale_id = payments.sale_id - WHERE ' . $where . ' + WHERE sales.sale_status = 0 AND ' . $where . ' GROUP BY payments.sale_id )' ); @@ -931,6 +1055,7 @@ class Sale extends CI_Model sales.sale_id AS sale_id, MAX(sales.comment) AS comment, MAX(sales.invoice_number) AS invoice_number, + MAX(sales.quote_number) AS quote_number, MAX(sales.customer_id) AS customer_id, MAX(CONCAT(customer_p.first_name, " ", customer_p.last_name)) AS customer_name, MAX(customer_p.first_name) AS customer_first_name, @@ -977,8 +1102,8 @@ class Sale extends CI_Model LEFT OUTER JOIN ' . $this->db->dbprefix('people') . ' AS employee ON sales.employee_id = employee.person_id LEFT OUTER JOIN ' . $this->db->dbprefix('sales_items_taxes_temp') . ' AS sales_items_taxes - ON sales_items.sale_id = sales_items_taxes.sale_id AND sales_items.item_id = sales_items_taxes.item_id - WHERE ' . $where . ' + ON sales_items.sale_id = sales_items_taxes.sale_id AND sales_items.item_id = sales_items_taxes.item_id AND sales_items.line = sales_items_taxes.line + WHERE sales.sale_status = 0 AND ' . $where . ' GROUP BY sale_id, item_id, line )' ); @@ -987,5 +1112,92 @@ class Sale extends CI_Model $this->db->query('DROP TEMPORARY TABLE IF EXISTS ' . $this->db->dbprefix('sales_payments_temp')); $this->db->query('DROP TEMPORARY TABLE IF EXISTS ' . $this->db->dbprefix('sales_items_taxes_temp')); } + + /* + * Retrieves all sales that are in a suspended state + */ + public function get_all_suspended($customer_id = NULL) + { + if ($customer_id == -1) + { + $query = $this->db->query('select sale_id, sale_id as suspended_sale_id, sale_status, sale_time, dinner_table_id, customer_id, comment from ' + . $this->db->dbprefix('sales') . ' where sale_status = 1 ' + . ' union select sale_id, sale_id*-1 as suspended_sale_id, 2 as sale_status, sale_time, dinner_table_id, customer_id, comment from ' + . $this->db->dbprefix('sales_suspended')); + } + else + { + $query = $this->db->query('select sale_id, sale_id as suspended_sale_id, sale_status, sale_time, dinner_table_id, customer_id, comment from ' + . $this->db->dbprefix('sales') . ' where sale_status = 1 and customer_id = ' . $customer_id + . ' union select sale_id, sale_id*-1 as suspended_sale_id, 2 as sale_status, sale_time, dinner_table_id, customer_id, comment from ' + . $this->db->dbprefix('sales_suspended') . ' where customer_id = ' . $customer_id); + } + + return $query->result_array(); + + } + + /* + * get the dinner table for the selected sale + */ + public function get_dinner_table($sale_id) + { + $this->db->from('sales'); + $this->db->where('sale_id', $sale_id); + + return $this->db->get()->row()->dinner_table_id; + } + + /* + * Gets total of suspended invoices rows + */ + public function get_suspended_invoice_count() + { + $this->db->from('sales'); + $this->db->where('invoice_number IS NOT NULL'); + $this->db->where('sale_status', '1'); + + return $this->db->count_all_results(); + } + + /* + * This will remove a selected sale from the sales table. + * This function should only be called for suspended sales that are being restored to the current cart + */ + public function delete_suspended_sale($sale_id) + { + //Run these queries as a transaction, we want to make sure we do all or nothing + $this->db->trans_start(); + + $dinner_table = $this->get_dinner_table($sale_id); + $dinner_table_data = array( + 'status' => 0 + ); + + $this->db->where('dinner_table_id',$dinner_table); + $this->db->update('dinner_tables', $dinner_table_data); + + $this->db->delete('sales_payments', array('sale_id' => $sale_id)); + $this->db->delete('sales_items_taxes', array('sale_id' => $sale_id)); + $this->db->delete('sales_items', array('sale_id' => $sale_id)); + $this->db->delete('sales_taxes', array('sale_id' => $sale_id)); + $this->db->delete('sales', array('sale_id' => $sale_id)); + + $this->db->trans_complete(); + + return $this->db->trans_status(); + } + + public function get_suspended_sale_info($sale_id) + { + $this->db->from('sales'); + $this->db->where('sale_id', $sale_id); + $this->db->join('people', 'people.person_id = sales_suspended.customer_id', 'LEFT'); + + return $this->db->get(); + } + + + } ?> diff --git a/application/models/Tax.php b/application/models/Tax.php new file mode 100644 index 000000000..4ed6e0bfd --- /dev/null +++ b/application/models/Tax.php @@ -0,0 +1,375 @@ +db->from('tax_codes'); + $this->db->where('tax_code', $tax_code); + + return ($this->db->get()->num_rows() == 1); + } + + public function tax_rate_exists($tax_code, $tax_category_id) + { + $this->db->from('tax_code_rates'); + $this->db->where('rate_tax_code', $tax_code); + $this->db->where('rate_tax_category_id', $tax_category_id); + + return ($this->db->get()->num_rows() == 1); + } + + /* + Gets total of rows + */ + public function get_total_rows() + { + $this->db->from('tax_codes'); + + return $this->db->count_all_results(); + } + + /* + Gets information about a particular tax_code + */ + public function get_info($tax_code) + { + $this->db->from('tax_codes'); + $this->db->join('tax_code_rates', + 'tax_code = rate_tax_code and rate_tax_category_id = 0', 'LEFT'); + $this->db->join('tax_categories', + 'rate_tax_category_id = tax_category_id'); + $this->db->where('tax_code', $tax_code); + + $query = $this->db->get(); + + if($query->num_rows() == 1) + { + return $query->row(); + } + else + { + //Get empty base parent object + $tax_code_obj = new stdClass(); + + //Get all the fields from tax_codes table + foreach($this->db->list_fields('tax_codes') as $field) + { + $tax_code_obj->$field = ''; + } + foreach($this->db->list_fields('tax_code_rates') as $field) + { + $tax_code_obj->$field = ''; + } + + return $tax_code_obj; + } + } + + /* +Gets information about a particular tax_code +*/ + public function get_rate_info($tax_code, $tax_category_id) + { + $this->db->from('tax_code_rates'); + $this->db->join('tax_categories', + 'rate_tax_category_id = tax_category_id'); + $this->db->where('rate_tax_code', $tax_code); + $this->db->where('rate_tax_category_id', $tax_category_id); + + $query = $this->db->get(); + + if($query->num_rows() == 1) + { + return $query->row(); + } + else + { + //Get empty base parent object + $tax_rate_obj = new stdClass(); + + //Get all the fields from tax_codes table + foreach($this->db->list_fields('tax_code_rates') as $field) + { + $tax_rate_obj->$field = ''; + } + //Get all the fields from tax_code_rates table + foreach($this->db->list_fields('tax_categories') as $field) + { + $tax_rate_obj->$field = ''; + } + + return $tax_rate_obj; + } + } + + /* + * Gets the tax code to use for a given customer + */ + public function get_sales_tax_code($city = '', $state = '') + { + // if tax code using both city and state cannot be found then try again using just the state + // if the state tax code cannot be found then try again using blanks for both + $this->db->from('tax_codes'); + $this->db->where('city', $city); + $this->db->where('state', $state); + $this->db->where('tax_code_type', '0'); // sales tax + + $query = $this->db->get(); + + if($query->num_rows() == 1) + { + return $query->row()->tax_code; + } + else + { + $this->db->from('tax_codes'); + $this->db->where('city', ''); + $this->db->where('state', $state); + $this->db->where('tax_code_type', '0'); // sales tax + + $query = $this->db->get(); + + if($query->num_rows() == 1) + { + return $query->row()->tax_code; + } + else + { + return $this->config->item('default_origin_tax_code'); + } + } + + return FALSE; + } + + /* + Inserts or updates a tax_codes entry + */ + public function save(&$tax_code_data, $tax_rate_data, $tax_code = -1) + { + if(!$this->exists($tax_code)) + { + if($this->db->insert('tax_codes', $tax_code_data)) + { + $this->save_tax_rates($tax_rate_data, $tax_code); + return TRUE; + } + return FALSE; + } + + $this->db->where('tax_code', $tax_code); + if ($this->db->update('tax_codes', $tax_code_data)) + { + $this->save_tax_rates($tax_rate_data, $tax_code); + return TRUE; + } + else + { + return FALSE; + } + } + + public function save_tax_rates(&$tax_rate_data, $tax_code) + { + if(!$this->tax_rate_exists($tax_code, $tax_rate_data['rate_tax_category_id'])) + { + if($this->db->insert('tax_code_rates', $tax_rate_data)) + { + return TRUE; + } + return FALSE; + } + + $this->db->where('rate_tax_code', $tax_code); + $this->db->where('rate_tax_category_id', $tax_rate_data['rate_tax_category_id']); + + return $this->db->update('tax_code_rates', $tax_rate_data); + } + + /* + Inserts or updates an item kit's items + */ + public function save_tax_rate_exceptions(&$tax_rate_data, $tax_code) + { + $success = TRUE; + + //Run these queries as a transaction, we want to make sure we do all or nothing + + $this->db->trans_start(); + + // Delete all exceptions for the given tax_code + $this->delete_tax_rate_exceptions($tax_code); + + if ($tax_rate_data != NULL) { + foreach ($tax_rate_data as $row) { + $row['rate_tax_code'] = $tax_code; + $success &= $this->db->insert('tax_code_rates', $row); + } + } + + $this->db->trans_complete(); + + $success &= $this->db->trans_status(); + + return $success; + } + + /* + Deletes one tax_codes entry + */ + public function delete($tax_code) + { + return $this->db->delete('tax_codes', array('tax_code' => $tax_code)); + } + + /* + Deletes a list of tax codes + */ + public function delete_list($tax_codes) + { + $this->db->where_in('tax_code', $tax_codes); + + return $this->db->delete('tax_codes'); + } + + + /* + Deletes all tax_rate_exceptions for given tax codes + */ + public function delete_tax_rate_exceptions($tax_code) + { + $this->db->where('rate_tax_code', $tax_code); + $this->db->where('rate_tax_category_id !=', 0); + + return $this->db->delete('tax_code_rates'); + } + + /* + Performs a search on tax_codes + */ + public function search($search, $rows = 0, $limit_from = 0, $sort = 'tax_code', $order = 'asc') + { + $this->db->from('tax_codes'); + $this->db->join('tax_code_rates', + 'tax_code = rate_tax_code and rate_tax_category_id = 0', 'LEFT'); + if (!empty($search)) { + $this->db->like('tax_code', $search); + $this->db->or_like('tax_code_name', $search); + } + $this->db->order_by($sort, $order); + + if($rows > 0) + { + $this->db->limit($rows, $limit_from); + } + + return $this->db->get(); + } + + /* + Gets tax_codes + */ + public function get_found_rows($search) + { + $this->db->from('tax_codes'); + if (!empty($search)) { + $this->db->like('tax_code', $search); + $this->db->or_like('tax_code_name', $search); + } + + return $this->db->get()->num_rows(); + } + + public function get_tax_code_type_name($tax_code_type) { + if ($tax_code_type == '0') + { + return $this->lang->line('taxes_sales_tax'); + } + else + { + return $this->lang->line('taxes_vat_tax'); + } + } + + public function get_sales_tax_codes_search_suggestions($search, $limit = 25) + { + + $suggestions = array(); + + $this->db->from('tax_codes'); + if (!empty($search)) { + $this->db->like('tax_code', $search); + $this->db->or_like('tax_code_name', $search); + } + $this->db->order_by('tax_code_name', 'asc'); + + foreach($this->db->get()->result() as $row) + { + $suggestions[] = array('value' => $row->tax_code, 'label' => ($row->tax_code . ' ' . $row->tax_code_name)); + } + + //only return $limit suggestions + if(count($suggestions > $limit)) + { + $suggestions = array_slice($suggestions, 0,$limit); + } + + return $suggestions; + } + + public function get_tax_category_suggestions($search) + { + $suggestions = array(); + + $this->db->from('tax_categories'); + $this->db->where('tax_category_id !=', 0); + if (!empty($search)) { + $this->db->like('tax_category', '%'.$search.'%'); + } + $this->db->order_by('tax_category', 'asc'); + + foreach($this->db->get()->result() as $row) + { + $suggestions[] = array('value' => $row->tax_category_id, 'label' => $row->tax_category); + } + + return $suggestions; + } + + public function get_tax_category($tax_category_id) + { + $this->db->select('tax_category'); + $this->db->from('tax_categories'); + $this->db->where('tax_category_id', $tax_category_id); + return $this->db->get()->row()->tax_category; + } + + public function get_all_tax_categories() + { + $this->db->from('tax_categories'); + $this->db->order_by('tax_category_id'); + return $this->db->get(); + } + + public function get_tax_category_id($tax_category) + { + $this->db->select('tax_category_id'); + $this->db->from('tax_categories'); + return $this->db->get()->row()->tax_category_id; + } + + public function get_tax_code_rate_exceptions($tax_code) + { + $this->db->from('tax_code_rates'); + $this->db->join('tax_categories', 'rate_tax_category_id = tax_category_id'); + $this->db->where('rate_tax_code', $tax_code); + $this->db->where('rate_tax_category_id !=', 0); + $this->db->order_by('tax_category', 'asc'); + + return $this->db->get()->result_array(); + } +} +?> \ No newline at end of file diff --git a/application/models/enums/Rounding_code.php b/application/models/enums/Rounding_code.php new file mode 100644 index 000000000..a72bc9aff --- /dev/null +++ b/application/models/enums/Rounding_code.php @@ -0,0 +1,80 @@ +load->helper('language'); + return array( + Rounding_code::HALF_UP => lang('enum_half_up'), + Rounding_code::HALF_DOWN => lang('enum_half_down'), + Rounding_code::HALF_EVEN => lang('enum_half_even'), + Rounding_code::HALF_ODD => lang('enum_half_odd'), + Rounding_code::ROUND_UP => lang('enum_round_up'), + Rounding_code::ROUND_DOWN => lang('enum_round_down'), + Rounding_code::HALF_FIVE => lang('enum_half_five') + ); + } + + public static function get_rounding_code_name($rounding_code) + { + $CI =& get_instance(); + $CI->load->helper('language'); + if($rounding_code == Rounding_code::HALF_UP) + { + return lang('enum_half_up'); + } + elseif($rounding_code == Rounding_code::HALF_DOWN) + { + return lang('enum_half_down'); + } + elseif($rounding_code == Rounding_code::HALF_EVEN) + { + return lang('enum_half_even'); + } + elseif($rounding_code == Rounding_code::HALF_ODD) + { + return lang('enum_half_odd'); + } + elseif($rounding_code == Rounding_code::ROUND_UP) + { + return lang('enum_round_up'); + } + elseif($rounding_code == Rounding_code::ROUND_DOWN) + { + return lang('enum_round_down'); + } + elseif($rounding_code == Rounding_code::HALF_FIVE) + { + return lang('enum_half_five'); + } + else + { + return lang('common_unknown'); + } + } + + public static function get_html_rounding_options() + { + $CI =& get_instance(); + $CI->load->helper('language'); + $x = "" . + "" . + "" . + "" . + "" . + "" . + ""; + + return $x; + } +} \ No newline at end of file diff --git a/application/models/reports/Summary_report.php b/application/models/reports/Summary_report.php index a612d8288..9ea2ff073 100644 --- a/application/models/reports/Summary_report.php +++ b/application/models/reports/Summary_report.php @@ -53,6 +53,7 @@ abstract class Summary_report extends Report ( SELECT sales_items_taxes.sale_id AS sale_id, sales_items_taxes.item_id AS item_id, + sales_items_taxes.line AS line, ' . " IFNULL(ROUND($sale_tax, $decimals), 0) AS tax " . ' @@ -61,8 +62,8 @@ abstract class Summary_report extends Report ON sales.sale_id = sales_items_taxes.sale_id INNER JOIN ' . $this->db->dbprefix('sales_items') . ' AS sales_items ON sales_items.sale_id = sales_items_taxes.sale_id AND sales_items.line = sales_items_taxes.line - WHERE ' . $where . ' - GROUP BY sale_id, item_id + WHERE sale_status = 0 AND ' . $where . ' + GROUP BY sale_id, item_id, line )' ); @@ -79,7 +80,9 @@ abstract class Summary_report extends Report { $this->db->from('sales_items AS sales_items'); $this->db->join('sales AS sales', 'sales_items.sale_id = sales.sale_id', 'inner'); - $this->db->join('sales_items_taxes_temp AS sales_items_taxes', 'sales_items.sale_id = sales_items_taxes.sale_id AND sales_items.item_id = sales_items_taxes.item_id', 'left outer'); + $this->db->join('sales_items_taxes_temp AS sales_items_taxes', + 'sales_items.sale_id = sales_items_taxes.sale_id AND sales_items.item_id = sales_items_taxes.item_id AND sales_items.line = sales_items_taxes.line', + 'left outer'); } private function _common_where(array $inputs) diff --git a/application/models/reports/Summary_taxes.php b/application/models/reports/Summary_taxes.php index b25808847..2576c3246 100644 --- a/application/models/reports/Summary_taxes.php +++ b/application/models/reports/Summary_taxes.php @@ -23,11 +23,11 @@ class Summary_taxes extends Summary_report { if(empty($this->config->item('date_or_time_format'))) { - $this->db->where('DATE(sales.sale_time) BETWEEN ' . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date'])); + $this->db->where('sale_status = 0 AND DATE(sales.sale_time) BETWEEN ' . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date'])); } else { - $this->db->where('sales.sale_time BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date']))); + $this->db->where('sale_status = 0 AND sales.sale_time BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date']))); } } @@ -37,11 +37,11 @@ class Summary_taxes extends Summary_report if(empty($this->config->item('date_or_time_format'))) { - $where .= 'WHERE DATE(sale_time) BETWEEN ' . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date']); + $where .= 'WHERE sale_status = 0 AND DATE(sale_time) BETWEEN ' . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date']); } else { - $where .= 'WHERE sale_time BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date'])); + $where .= 'WHERE sale_status = 0 AND sale_time BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date'])); } if($this->config->item('tax_included')) diff --git a/application/views/configs/general_config.php b/application/views/configs/general_config.php index adbabf8a6..ea95753e4 100644 --- a/application/views/configs/general_config.php +++ b/application/views/configs/general_config.php @@ -12,19 +12,19 @@
- lang->line('config_default_tax_rate_1'), 'default_tax_1_rate', array('class' => 'control-label col-xs-2 required')); ?> + lang->line('config_default_tax_rate_1'), 'default_tax_1_rate', array('class' => 'control-label col-xs-2')); ?>
'default_tax_1_name', 'id' => 'default_tax_1_name', - 'class' => 'form-control input-sm required', + 'class' => 'form-control input-sm', 'value'=>$this->config->item('default_tax_1_name')!==FALSE ? $this->config->item('default_tax_1_name') : $this->lang->line('items_sales_tax_1'))); ?>
'default_tax_1_rate', 'id' => 'default_tax_1_rate', - 'class' => 'form-control input-sm required', + 'class' => 'form-control input-sm', 'value'=>to_tax_decimals($this->config->item('default_tax_1_rate')))); ?> %
@@ -60,7 +60,29 @@
-
+
+ lang->line('config_customer_sales_tax_support'), 'customer_sales_tax_support', array('class' => 'control-label col-xs-2')); ?> +
+ 'customer_sales_tax_support', + 'id' => 'customer_sales_tax_support', + 'value' => 'customer_sales_tax_support', + 'checked'=>$this->config->item('customer_sales_tax_support'))); ?> +
+
+ +
+ lang->line('config_default_origin_tax_code'), 'default_origin_tax_code', array('class' => 'control-label col-xs-2')); ?> +
+ 'default_origin_tax_code', + 'id' => 'default_origin_tax_code', + 'class' => 'form-control input-sm', + 'value'=>$this->config->item('default_origin_tax_code'))); ?> +
+
+ +
lang->line('config_default_sales_discount'), 'default_sales_discount', array('class' => 'control-label col-xs-2 required')); ?>
@@ -283,10 +305,8 @@ $(document).ready(function() { default_tax_1_rate: { - required: true, remote: "" }, - default_tax_1_name: "required", default_tax2_rate: { remote: "" @@ -307,14 +327,8 @@ $(document).ready(function() { default_tax_1_rate: { - required: "lang->line('config_default_tax_rate_required'); ?>", number: "lang->line('config_default_tax_rate_number'); ?>" }, - default_tax_1_name: - { - required: "lang->line('config_default_tax_name_required'); ?>", - number: "lang->line('config_default_tax_name_number'); ?>" - }, default_sales_discount: { required: "lang->line('config_default_sales_discount_required'); ?>", diff --git a/application/views/configs/locale_config.php b/application/views/configs/locale_config.php index c1f3fe537..3bd585886 100644 --- a/application/views/configs/locale_config.php +++ b/application/views/configs/locale_config.php @@ -65,7 +65,8 @@ '0' => '0', '1' => '1', '2' => '2', - '3' => '3' + '3' => '3', + '4' => '4' ), $this->config->item('tax_decimals'), array('class' => 'form-control input-sm')); ?> @@ -86,7 +87,31 @@
-
+
+ lang->line('config_cash_decimals'), 'cash_decimals', array('class' => 'control-label col-xs-2')); ?> +
+ '0', + '1' => '1', + '2' => '2' + ), + $this->config->item('cash_decimals'), array('class' => 'form-control input-sm')); + ?> +
+
+ +
+
+ +
+ lang->line('config_cash_rounding'), 'cash_rounding_code', array('class' => 'control-label col-xs-2')); ?> +
+ config->item('cash_rounding_code'), array('class' => 'form-control input-sm')); + ?> +
+
+ +
lang->line('config_payment_options_order'), 'payment_options_order', array('class' => 'control-label col-xs-2')); ?>
taxable == '' ? TRUE : (boolean)$person_info->taxable);?>
+ + +
+ lang->line('customers_tax_code'), 'sales_tax_code_name', array('class'=>'control-label col-xs-3')); ?> +
+
+ 'sales_tax_code_name', + 'id'=>'sales_tax_code_name', + 'class'=>'form-control input-sm', + 'size'=>'50', + 'value'=>$sales_tax_code_label) + ); ?> + sales_tax_code);?> + +
+
+
+ + @@ -142,4 +162,27 @@ $(document).ready(function() } }, form_support.error)); }); + +$("input[name='sales_tax_code_name']").change(function() { + if( ! $("input[name='sales_tax_code_name']").val() ) { + $("input[name='sales_tax_code']").val(''); + } +}); + +var fill_value = function(event, ui) { + event.preventDefault(); + $("input[name='sales_tax_code']").val(ui.item.value); + $("input[name='sales_tax_code_name']").val(ui.item.label); +}; + +$("#sales_tax_code_name").autocomplete({ + source: '', + minChars: 0, + delay: 15, + cacheLength: 1, + appendTo: '.modal-content', + select: fill_value, + focus: fill_value +}); + \ No newline at end of file diff --git a/application/views/items/form.php b/application/views/items/form.php index d848d06e8..6ac40be74 100644 --- a/application/views/items/form.php +++ b/application/views/items/form.php @@ -166,7 +166,7 @@
-
+
lang->line('items_tax_2'), 'tax_percent_2', array('class'=>'control-label col-xs-3')); ?>
- +
+ lang->line('taxes_tax_category'), 'tax_category', array('class'=>'control-label col-xs-3')); ?> +
+ 'form-control')); ?> +
+
+ + + $location_detail) { ?> diff --git a/application/views/receivings/receiving.php b/application/views/receivings/receiving.php index 24258435e..3339048f0 100644 --- a/application/views/receivings/receiving.php +++ b/application/views/receivings/receiving.php @@ -408,7 +408,7 @@ $(document).ready(function() { $("#item").autocomplete( { - source: '', + source: '', minChars:0, delay:10, autoFocus: false, diff --git a/application/views/sales/invoice.php b/application/views/sales/invoice.php index 23cae8b26..9b017352a 100755 --- a/application/views/sales/invoice.php +++ b/application/views/sales/invoice.php @@ -123,16 +123,16 @@ $(document).ready(function() - + $value) + foreach($taxes as $tax_group_index=>$sales_tax) { ?> - - + + + + $payment) + { + $only_sale_check |= $payment['payment_type'] == $this->lang->line('sales_check'); + $splitpayment = explode(':', $payment['payment_type']); + $show_giftcard_remainder |= $splitpayment[0] == $this->lang->line('sales_giftcard'); + ?> + + + + + + + + + + + + + + + + + + +
diff --git a/application/views/sales/quote.php b/application/views/sales/quote.php index a8d80cc21..338a24b39 100644 --- a/application/views/sales/quote.php +++ b/application/views/sales/quote.php @@ -126,13 +126,13 @@ if (isset($error_message)) $value) + foreach($taxes as $tax_group_index=>$sales_tax) { ?> - - + + config->item('tax_included') ? $tax_exclusive_subtotal : $discounted_subtotal); ?> $value) + foreach($taxes as $tax_group_index=>$sales_tax) { ?> - : - + : + config->item('tax_included') ? $tax_exclusive_subtotal : $discounted_subtotal); ?> $value) + foreach($taxes as $tax_group_index=>$sales_tax) { ?> - : - + : + $value) + foreach($taxes as $tax_group_index=>$sales_tax) { ?> - - + + lang->line('sales_total'); ?> - + @@ -372,7 +372,7 @@ if (isset($success)) lang->line('sales_amount_due');?> - + @@ -387,7 +387,7 @@ if (isset($success)) lang->line('sales_payment');?> - 'payment_types', 'class'=>'selectpicker show-menu-arrow', 'data-style'=>'btn-default btn-sm', 'data-width'=>'auto', 'disabled'=>'disabled')); ?> + 'payment_types', 'class'=>'selectpicker show-menu-arrow', 'data-style'=>'btn-default btn-sm', 'data-width'=>'auto', 'disabled'=>'disabled')); ?> @@ -417,7 +417,7 @@ if (isset($success)) lang->line('sales_payment');?> - 'payment_types', 'class'=>'selectpicker show-menu-arrow', 'data-style'=>'btn-default btn-sm', 'data-width'=>'auto')); ?> + 'payment_types', 'class'=>'selectpicker show-menu-arrow', 'data-style'=>'btn-default btn-sm', 'data-width'=>'fit')); ?> @@ -693,7 +693,7 @@ $(document).ready(function() $('#add_payment_form').submit(); }); - $("#payment_types").change(check_payment_type_giftcard).ready(check_payment_type_giftcard); + $("#payment_types").change(check_payment_type).ready(check_payment_type); $("#cart_contents input").keypress(function(event) { @@ -752,18 +752,31 @@ $(document).ready(function() }); -function check_payment_type_giftcard() +function check_payment_type() { - if ($("#payment_types").val() == "lang->line('sales_giftcard'); ?>") - { - $("#amount_tendered_label").html("lang->line('sales_giftcard_number'); ?>"); - $("#amount_tendered:enabled").val('').focus(); - } - else - { - $("#amount_tendered_label").html("lang->line('sales_amount_tendered'); ?>"); - $("#amount_tendered:enabled").val(''); - } + var cash_rounding = ; + + if ($("#payment_types").val() == "lang->line('sales_giftcard'); ?>") + { + $("#sale_total").html(""); + $("#sale_amount_due").html(""); + $("#amount_tendered_label").html("lang->line('sales_giftcard_number'); ?>"); + $("#amount_tendered:enabled").val('').focus(); + } + else if ($("#payment_types").val() == "lang->line('sales_cash'); ?>" && cash_rounding) + { + $("#sale_total").html(""); + $("#sale_amount_due").html(""); + $("#amount_tendered_label").html("lang->line('sales_amount_tendered'); ?>"); + $("#amount_tendered:enabled").val(''); + } + else + { + $("#sale_total").html(""); + $("#sale_amount_due").html(""); + $("#amount_tendered_label").html("lang->line('sales_amount_tendered'); ?>"); + $("#amount_tendered:enabled").val(''); + } } diff --git a/application/views/sales/suspended.php b/application/views/sales/suspended.php index 10b05d7e0..86200bbfe 100644 --- a/application/views/sales/suspended.php +++ b/application/views/sales/suspended.php @@ -50,7 +50,7 @@ diff --git a/application/views/taxes/form.php b/application/views/taxes/form.php new file mode 100644 index 000000000..fb927e264 --- /dev/null +++ b/application/views/taxes/form.php @@ -0,0 +1,212 @@ +
lang->line('common_fields_required_message'); ?>
+ +
    + +'tax_code_form', 'class'=>'form-horizontal')); ?> +
    +
    + lang->line('taxes_tax_code'), 'name', array('class'=>'required control-label col-xs-3')); ?> +
    + 'tax_code', + 'id'=>'tax_code', + 'class'=>'form-control input-sm', + 'value'=>$tax_code) + );?> +
    +
    + +
    + lang->line('taxes_tax_code_name'), 'name', array('class'=>'required control-label col-xs-3')); ?> +
    + 'tax_code_name', + 'id'=>'tax_code_name', + 'class'=>'form-control input-sm', + 'value'=>$tax_code_name) + );?> +
    +
    + +
    + lang->line('taxes_tax_code_type'), 'tax_code_type', !empty($basic_version) ? array('class'=>'required control-label col-xs-3') : array('class'=>'control-label col-xs-3')); ?> +
    + + + +
    +
    + +
    + lang->line('common_city'), 'city', array('class'=>'control-label col-xs-3')); ?> +
    + 'city', + 'id'=>'city', + 'class'=>'form-control input-sm', + 'value'=>$city) + );?> +
    +
    + +
    + lang->line('common_state'), 'name', array('class'=>'control-label col-xs-3')); ?> +
    + 'state', + 'id'=>'state', + 'class'=>'form-control input-sm', + 'value'=>$state) + );?> +
    +
    + +
    + lang->line('taxes_tax_rate'), 'tax_rate', array('class' => 'required control-label col-xs-3')); ?> +
    +
    + 'tax_rate', + 'id'=>'tax_rate', + 'class'=>'form-control input-sm', + 'value'=>$tax_rate) + );?> + % +
    +
    +
    + +
    + lang->line('taxes_rounding_code'), 'rounding_code', array('class' => 'control-label col-xs-3')); ?> +
    + 'form-control input-sm')); ?> +
    +
    + +
    + lang->line('taxes_add_exception'), 'add_tax_category', array('class'=>'control-label col-xs-3')); ?> +
    +
    + + 'add_tax_category', + 'id'=>'add_tax_category', + 'class'=>'form-control input-sm', + 'value'=>$add_tax_category) + );?> + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + +
    lang->line('common_delete'); ?>lang->line('taxes_tax_category'); ?>lang->line('taxes_tax_rate'); ?>lang->line('taxes_rounding_code'); ?>
    ' name=exception_tax_rate[] value=''/> 'form-control input-sm'));?>
    + +
    + + + \ No newline at end of file diff --git a/application/views/taxes/manage.php b/application/views/taxes/manage.php new file mode 100644 index 000000000..fa71dbb66 --- /dev/null +++ b/application/views/taxes/manage.php @@ -0,0 +1,34 @@ +load->view("partial/header"); ?> + + +
    + +
    + +
    +
    + +
    +
    + +
    +
    +
    + +load->view("partial/footer"); ?> diff --git a/database/3.0.2_to_3.1.0.sql b/database/3.0.2_to_3.1.0.sql index 30e5507a3..84a80ed68 100644 --- a/database/3.0.2_to_3.1.0.sql +++ b/database/3.0.2_to_3.1.0.sql @@ -121,4 +121,86 @@ ADD COLUMN `points` int(11) DEFAULT NULL AFTER `package_id`; -- add enabled reward points key into config INSERT INTO `ospos_app_config` (`key`, `value`) VALUES -('customer_reward_enable',''); \ No newline at end of file +('customer_reward_enable',''); + +/* The following changes are in support of customer sales tax changes */ + +CREATE TABLE IF NOT EXISTS `ospos_tax_codes` ( + `tax_code` varchar(32) NOT NULL, + `tax_code_name` varchar(255) NOT NULL DEFAULT '', + `tax_code_type` tinyint(2) NOT NULL DEFAULT 0, + `city` varchar(255) NOT NULL DEFAULT '', + `state` varchar(255) NOT NULL DEFAULT '', + PRIMARY KEY (`tax_code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `ospos_tax_code_rates` ( + `rate_tax_code` varchar(32) NOT NULL, + `rate_tax_category_id` int(10) NOT NULL, + `tax_rate` decimal(15,4) NOT NULL DEFAULT 0.0000, + `rounding_code` tinyint(2) NOT NULL DEFAULT 0, + PRIMARY KEY (`rate_tax_code`,`rate_tax_category_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE `ospos_sales_taxes` ( + `sale_id` int(10) NOT NULL, + `tax_type` smallint(2) NOT NULL, + `tax_group` varchar(32) NOT NULL, + `sale_tax_basis` decimal(15,4) NOT NULL, + `sale_tax_amount` decimal(15,4) NOT NULL, + `print_sequence` tinyint(2) NOT NULL DEFAULT 0, + `name` varchar(255) NOT NULL, + `tax_rate` decimal(15,4) NOT NULL, + `sales_tax_code` varchar(32) NOT NULL DEFAULT '', + `rounding_code` tinyint(2) NOT NULL DEFAULT 0, + PRIMARY KEY (`sale_id`,`tax_type`,`tax_group`), + KEY `print_sequence` (`sale_id`,`print_sequence`,`tax_type`,`tax_group`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `ospos_tax_categories` ( + `tax_category_id` int(10) NOT NULL, + `tax_category` varchar(32) NOT NULL, + `tax_group_sequence` tinyint(2) NOT NULL, + PRIMARY KEY (`tax_category_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +INSERT INTO `ospos_tax_categories` ( `tax_category_id`,`tax_category`, `tax_group_sequence` ) VALUES + (0, 'Standard', 10), + (1, 'Service', 12), + (2, 'Alcohol', 11); + +ALTER TABLE `ospos_items` + ADD COLUMN `tax_category_id` int(10) NOT NULL DEFAULT 0; + +ALTER TABLE `ospos_sales` + ADD COLUMN `quote_number` varchar(32) DEFAULT NULL, + ADD COLUMN `sale_status` tinyint(2) NOT NULL DEFAULT 0; + +ALTER TABLE `ospos_sales_items_taxes` + MODIFY COLUMN `percent` decimal(15,4) NOT NULL DEFAULT 0.0000, + ADD COLUMN `tax_type` tinyint(2) NOT NULL DEFAULT 0, + ADD COLUMN `rounding_code` tinyint(2) NOT NULL DEFAULT 0, + ADD COLUMN `cascade_tax` tinyint(2) NOT NULL DEFAULT 0, + ADD COLUMN `cascade_sequence` tinyint(2) NOT NULL DEFAULT 0, + ADD COLUMN `item_tax_amount` decimal(15,4) NOT NULL DEFAULT 0; + +ALTER TABLE `ospos_customers` + ADD COLUMN `sales_tax_code` varchar(32) NOT NULL; + +INSERT INTO `ospos_app_config` (`key`, `value`) VALUES + ('customer_sales_tax_support', '0'), + ('default_origin_tax_code', ''); + +INSERT INTO `ospos_modules` (`name_lang_key`, `desc_lang_key`, `sort`, `module_id`) VALUES + ('module_taxes', 'module_taxes_desc', 105, 'taxes'); + +INSERT INTO `ospos_permissions` (`permission_id`, `module_id`) VALUES + ('taxes', 'taxes'); + +/* End of customer sales tax changes */ + +/* Start of support for cash rounding */ +INSERT INTO `ospos_app_config` (`key`, `value`) VALUES + ('cash_decimals', '2'); +/* End of support for cash rounding */ + diff --git a/database/database.sql b/database/database.sql index 8a974bd9f..2ce4d92c0 100644 --- a/database/database.sql +++ b/database/database.sql @@ -85,11 +85,14 @@ INSERT INTO `ospos_app_config` (`key`, `value`) VALUES ('smtp_crypto', 'ssl'), ('receipt_template', 'receipt_default'), ('theme', 'flatly'), +('customer_sales_tax_support', '0'), +('default_origin_tax_code', ''), ('statistics', '1'), ('language', 'english'), ('language_code', 'en'), ('date_or_time_format',''), -('customer_reward_enable',''); +('customer_reward_enable',''), +('cash_decimals', '2'); -- -------------------------------------------------------- @@ -103,6 +106,7 @@ CREATE TABLE `ospos_customers` ( `company_name` varchar(255) DEFAULT NULL, `account_number` varchar(255) DEFAULT NULL, `taxable` int(1) NOT NULL DEFAULT '1', + `sales_tax_code` varchar(32) NOT NULL DEFAULT '1', `discount_percent` decimal(15,2) NOT NULL DEFAULT '0', `package_id` int(11) DEFAULT NULL, `points` int(11) DEFAULT NULL, @@ -209,6 +213,7 @@ CREATE TABLE `ospos_items` ( `is_serialized` tinyint(1) NOT NULL, `stock_type` TINYINT(2) NOT NULL DEFAULT 0, `item_type` TINYINT(2) NOT NULL DEFAULT 0, + `tax_category_id` int(10) NOT NULL DEFAULT 0, `deleted` int(1) NOT NULL DEFAULT '0', `custom1` VARCHAR(25) NOT NULL, `custom2` VARCHAR(25) NOT NULL, @@ -335,7 +340,9 @@ INSERT INTO `ospos_modules` (`name_lang_key`, `desc_lang_key`, `sort`, `module_i ('module_receivings', 'module_receivings_desc', 60, 'receivings'), ('module_reports', 'module_reports_desc', 50, 'reports'), ('module_sales', 'module_sales_desc', 70, 'sales'), -('module_suppliers', 'module_suppliers_desc', 40, 'suppliers'); +('module_suppliers', 'module_suppliers_desc', 40, 'suppliers'), +('module_taxes', 'module_taxes_desc', 105, 'taxes'); + -- -------------------------------------------------------- @@ -407,13 +414,15 @@ INSERT INTO `ospos_permissions` (`permission_id`, `module_id`) VALUES ('reports', 'reports'), ('sales', 'sales'), ('config', 'config'), -('suppliers', 'suppliers'); +('suppliers', 'suppliers'), +('taxes', 'taxes'); INSERT INTO `ospos_permissions` (`permission_id`, `module_id`, `location_id`) VALUES ('items_stock', 'items', 1), ('sales_stock', 'sales', 1), ('receivings_stock', 'receivings', 1); + -- -------------------------------------------------------- -- @@ -520,8 +529,10 @@ CREATE TABLE `ospos_sales` ( `employee_id` int(10) NOT NULL DEFAULT '0', `comment` text NOT NULL, `invoice_number` varchar(32) DEFAULT NULL, + `quote_number` varchar(32) DEFAULT NULL, `sale_id` int(10) NOT NULL AUTO_INCREMENT, `dinner_table_id` int(11) NULL, + `sale_status` tinyint(2) NOT NULL DEFAULT 0, PRIMARY KEY (`sale_id`), KEY `customer_id` (`customer_id`), KEY `employee_id` (`employee_id`), @@ -575,7 +586,12 @@ CREATE TABLE `ospos_sales_items_taxes` ( `item_id` int(10) NOT NULL, `line` int(3) NOT NULL DEFAULT '0', `name` varchar(255) NOT NULL, - `percent` decimal(15,3) NOT NULL, + `percent` decimal(15,4) NOT NULL, + `tax_type` tinyint(2) NOT NULL DEFAULT 0, + `rounding_code` tinyint(2) NOT NULL DEFAULT 0, + `cascade_tax` tinyint(2) NOT NULL DEFAULT 0, + `cascade_sequence` tinyint(2) NOT NULL DEFAULT 0, + `item_tax_amount` decimal(15,4) NOT NULL DEFAULT 0, PRIMARY KEY (`sale_id`,`item_id`,`line`,`name`,`percent`), KEY `sale_id` (`sale_id`), KEY `item_id` (`item_id`) @@ -604,6 +620,88 @@ CREATE TABLE `ospos_sales_payments` ( -- Dumping data for table `ospos_sales_payments` -- +-- -------------------------------------------------------- + +-- +-- Table structure for table `ospos_sales_taxes` +-- + +CREATE TABLE `ospos_sales_taxes` ( + `sale_id` int(10) NOT NULL, + `tax_type` smallint(2) NOT NULL, + `tax_group` varchar(32) NOT NULL, + `sale_tax_basis` decimal(15,4) NOT NULL, + `sale_tax_amount` decimal(15,4) NOT NULL, + `print_sequence` tinyint(2) NOT NULL DEFAULT 0, + `name` varchar(255) NOT NULL, + `tax_rate` decimal(15,4) NOT NULL, + `sales_tax_code` varchar(32) NOT NULL DEFAULT '', + `rounding_code` tinyint(2) NOT NULL DEFAULT 0, + PRIMARY KEY (`sale_id`,`tax_type`,`tax_group`), + KEY `print_sequence` (`sale_id`,`print_sequence`,`tax_type`,`tax_group`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- +-- Dumping data for table `ospos_sales_taxes` +-- + +-- -------------------------------------------------------- + +-- +-- Table structure for table `ospos_tax_categories` +-- + +CREATE TABLE IF NOT EXISTS `ospos_tax_categories` ( + `tax_category_id` int(10) NOT NULL, + `tax_category` varchar(32) NOT NULL, + `tax_group_sequence` tinyint(2) NOT NULL, + PRIMARY KEY (`tax_category_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- +-- Dumping data for table `ospos_tax_categories` +-- + +INSERT INTO `ospos_tax_categories` ( `tax_category_id`,`tax_category`, `tax_group_sequence` ) VALUES + (0, 'Standard', 10), + (1, 'Service', 12), + (2, 'Alcohol', 11); + + +-- -------------------------------------------------------- + +-- +-- Table structure for table `ospos_tax_codes` +-- + +CREATE TABLE IF NOT EXISTS `ospos_tax_codes` ( + `tax_code` varchar(32) NOT NULL, + `tax_code_name` varchar(255) NOT NULL DEFAULT '', + `tax_code_type` tinyint(2) NOT NULL DEFAULT 0, + `city` varchar(255) NOT NULL DEFAULT '', + `state` varchar(255) NOT NULL DEFAULT '', + PRIMARY KEY (`tax_code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- +-- Dumping data for table `ospos_tax_codes` +-- + + +-- -------------------------------------------------------- + +-- +-- Table structure for table `ospos_tax_code_rates` +-- + +CREATE TABLE IF NOT EXISTS `ospos_tax_code_rates` ( + `rate_tax_code` varchar(32) NOT NULL, + `rate_tax_category_id` int(10) NOT NULL, + `tax_rate` decimal(15,4) NOT NULL DEFAULT 0.0000, + `rounding_code` tinyint(2) NOT NULL DEFAULT 0, + PRIMARY KEY (`rate_tax_code`,`rate_tax_category_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + -- -------------------------------------------------------- diff --git a/database/tables.sql b/database/tables.sql index 918863dfd..edfa73729 100644 --- a/database/tables.sql +++ b/database/tables.sql @@ -18,6 +18,7 @@ INSERT INTO `ospos_app_config` (`key`, `value`) VALUES ('company', 'Open Source Point of Sale'), ('default_register_mode', 'sale'), ('default_tax_rate', '8'), +('default_tax_category', 'Standard'), ('email', 'changeme@example.com'), ('fax', ''), ('phone', '555-555-5555'), @@ -85,11 +86,16 @@ INSERT INTO `ospos_app_config` (`key`, `value`) VALUES ('smtp_crypto', 'ssl'), ('receipt_template', 'receipt_default'), ('theme', 'flatly'), +('customer_sales_tax_support', '0'), ('statistics', '1'), ('language', 'english'), ('language_code', 'en'), ('date_or_time_format',''), -('customer_reward_enable',''); +('customer_reward_enable',''), +('customer_sales_tax_support', '0'), +('default_origin_tax_code', ''), +('cash_decimals', '2'); + -- -------------------------------------------------------- @@ -103,6 +109,7 @@ CREATE TABLE `ospos_customers` ( `company_name` varchar(255) DEFAULT NULL, `account_number` varchar(255) DEFAULT NULL, `taxable` int(1) NOT NULL DEFAULT '1', + `sales_tax_code` varchar(32) NOT NULL DEFAULT '', `discount_percent` decimal(15,2) NOT NULL DEFAULT '0', `package_id` int(11) DEFAULT NULL, `points` int(11) DEFAULT NULL, @@ -209,6 +216,7 @@ CREATE TABLE `ospos_items` ( `is_serialized` tinyint(1) NOT NULL, `stock_type` TINYINT(2) NOT NULL DEFAULT 0, `item_type` TINYINT(2) NOT NULL DEFAULT 0, + `tax_category_id` int(10) NOT NULL DEFAULT 0, `deleted` int(1) NOT NULL DEFAULT '0', `custom1` VARCHAR(25) NOT NULL, `custom2` VARCHAR(25) NOT NULL, @@ -335,7 +343,8 @@ INSERT INTO `ospos_modules` (`name_lang_key`, `desc_lang_key`, `sort`, `module_i ('module_receivings', 'module_receivings_desc', 60, 'receivings'), ('module_reports', 'module_reports_desc', 50, 'reports'), ('module_sales', 'module_sales_desc', 70, 'sales'), -('module_suppliers', 'module_suppliers_desc', 40, 'suppliers'); +('module_suppliers', 'module_suppliers_desc', 40, 'suppliers'), +('module_taxes', 'module_taxes_desc', 105, 'taxes'); -- -------------------------------------------------------- @@ -407,7 +416,8 @@ INSERT INTO `ospos_permissions` (`permission_id`, `module_id`) VALUES ('reports', 'reports'), ('sales', 'sales'), ('config', 'config'), -('suppliers', 'suppliers'); +('suppliers', 'suppliers'), +('taxes', 'taxes'); INSERT INTO `ospos_permissions` (`permission_id`, `module_id`, `location_id`) VALUES ('items_stock', 'items', 1), @@ -520,7 +530,9 @@ CREATE TABLE `ospos_sales` ( `employee_id` int(10) NOT NULL DEFAULT '0', `comment` text NOT NULL, `invoice_number` varchar(32) DEFAULT NULL, + `quote_number` varchar(32) DEFAULT NULL, `sale_id` int(10) NOT NULL AUTO_INCREMENT, + `sale_status` tinyint(2) DEFAULT 0, `dinner_table_id` int(11) NULL, PRIMARY KEY (`sale_id`), KEY `customer_id` (`customer_id`), @@ -575,7 +587,12 @@ CREATE TABLE `ospos_sales_items_taxes` ( `item_id` int(10) NOT NULL, `line` int(3) NOT NULL DEFAULT '0', `name` varchar(255) NOT NULL, - `percent` decimal(15,3) NOT NULL, + `percent` decimal(15,4) NOT NULL DEFAULT 0.0000, + `tax_type` tinyint(2) NOT NULL DEFAULT 0, + `rounding_code` tinyint(2) NOT NULL DEFAULT 0, + `cascade_tax` tinyint(2) NOT NULL DEFAULT 0, + `cascade_sequence` tinyint(2) NOT NULL DEFAULT 0, + `item_tax_amount` decimal(15,4) NOT NULL DEFAULT 0, PRIMARY KEY (`sale_id`,`item_id`,`line`,`name`,`percent`), KEY `sale_id` (`sale_id`), KEY `item_id` (`item_id`) @@ -604,6 +621,33 @@ CREATE TABLE `ospos_sales_payments` ( -- Dumping data for table `ospos_sales_payments` -- +-- -------------------------------------------------------- + +-- +-- Table structure for table `ospos_sales_taxes` +-- + +CREATE TABLE `ospos_sales_taxes` ( + `sale_id` int(10) NOT NULL, + `tax_type` smallint(2) NOT NULL, + `tax_group` varchar(32) NOT NULL, + `sale_tax_basis` decimal(15,4) NOT NULL, + `sale_tax_amount` decimal(15,4) NOT NULL, + `print_sequence` tinyint(2) NOT NULL DEFAULT 0, + `name` varchar(255) NOT NULL, + `tax_rate` decimal(15,4) NOT NULL, + `sales_tax_code` varchar(32) NOT NULL DEFAULT '', + `rounding_code` tinyint(2) NOT NULL DEFAULT 0, + PRIMARY KEY (`sale_id`,`tax_type`,`tax_group`), + KEY `print_sequence` (`sale_id`,`print_sequence`,`tax_type`,`tax_group`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + + +-- +-- Dumping data for table `ospos_sales_taxes` +-- + + -- -------------------------------------------------------- @@ -736,7 +780,6 @@ CREATE TABLE `ospos_stock_locations` ( INSERT INTO `ospos_stock_locations` ( `deleted`, `location_name` ) VALUES ('0', 'stock'); -- -------------------------------------------------------- - -- -- Table structure for table `ospos_suppliers` -- @@ -751,12 +794,47 @@ CREATE TABLE `ospos_suppliers` ( KEY `person_id` (`person_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +-- -------------------------------------------------------- -- --- Dumping data for table `ospos_suppliers` +-- Table structure for table `ospos_tax_categories` -- +CREATE TABLE IF NOT EXISTS `ospos_tax_categories` ( + `tax_category_id` int(10) NOT NULL, + `tax_category` varchar(32) NOT NULL, + `tax_group_sequence` tinyint(2) NOT NULL, + PRIMARY KEY (`tax_category_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- +-- Dumping data for table `ospos_stock_locations` +-- + +INSERT INTO `ospos_tax_categories` ( `tax_category_id`,`tax_category`, `tax_group_sequence` ) VALUES + (0, 'Standard', 10), + (1, 'Service', 12), + (2, 'Alcohol', 11); + -- -------------------------------------------------------- +-- +-- Table structure for table `ospos_tax_codes` +-- +CREATE TABLE IF NOT EXISTS `ospos_tax_codes` ( + `tax_code` varchar(32) NOT NULL, + `tax_code_name` varchar(255) NOT NULL DEFAULT '', + `tax_code_type` tinyint(2) NOT NULL DEFAULT 0, + `city` varchar(255) NOT NULL DEFAULT '', + `state` varchar(255) NOT NULL DEFAULT '', + PRIMARY KEY (`tax_code`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- +-- Dumping data for table `ospos_tax_codes` +-- + + +-- -------------------------------------------------------- -- -- Table structure for table `ospos_dinner_tables` -- @@ -820,3 +898,20 @@ CREATE TABLE IF NOT EXISTS `ospos_sales_reward_points` ( `used` float NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ; + +-- -------------------------------------------------------- +-- +-- Table structure for table `ospos_tax_code_rates` +-- + +CREATE TABLE IF NOT EXISTS `ospos_tax_code_rates` ( + `rate_tax_code` varchar(32) NOT NULL, + `rate_tax_category_id` int(10) NOT NULL, + `tax_rate` decimal(15,4) NOT NULL DEFAULT 0.0000, + `rounding_code` tinyint(2) NOT NULL DEFAULT 0, + PRIMARY KEY (`rate_tax_code`,`rate_tax_category_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- +-- Dumping data for table `ospos_tax_code_rates` +-- diff --git a/public/css/popupbox.css b/public/css/popupbox.css index 3dd3895db..e28a40ae3 100644 --- a/public/css/popupbox.css +++ b/public/css/popupbox.css @@ -63,23 +63,23 @@ font-size: 80%; } -#item_kit_items, #items_count_details +#item_kit_items, #items_count_details, #tax_code_rates { width: 100%; } -#item_kit_items thead tr, #items_count_details thead tr +#item_kit_items thead tr, #items_count_details thead tr, #tax_code_rates thead tr { background-color: #CCC; } -#item_kit_items th, #items_count_details th +#item_kit_items th, #items_count_details th, #tax_code_rates th { text-align: center; font-weight: bold; } -#item_kit_items td, #items_count_details td +#item_kit_items td, #items_count_details td, #tax_code_rates td { text-align: center; } \ No newline at end of file diff --git a/public/images/menubar/taxes.png b/public/images/menubar/taxes.png new file mode 100644 index 0000000000000000000000000000000000000000..c3be17259e1ce1dae60fc8c32329e70485d909ad GIT binary patch literal 1572 zcmV+<2HW|GP)|0xJ8$}d8USE>di5xd^8xm?Q7ez=A)hz-EwW1EF;?hV2-hn*ufOyJ7e}I1g z5BUK|*zg1DNI?iys4gl(0;$9*52z~OMkrEH?AUd4^JUGP&8)R?yfd?FuXi2yNJqAl z*_}P#`R2@-GqavXqXBlx!Y4Q4_Xb4iwHj_4ZEO!}KIGj=|or`rG!gU9fL z>IYrA?F!K}DgmVEN&txA=;0-zpMt%TBAVZq0EqE8zLP{l;0`GfT_hl+Z3loDpLBuO4cS0IShNKIO8JbyH0HG6G#VEj z5<&pvc6w zVZ0J7^2Ct=@lGG`?slHrrdr;I{JnK3=L(=yYKPPfkf9@=f$>XG@Q)5Num7#I4VS~? zLeGQoN{|IA05LPhq9?TRh~LrpKX=&VKgjRE`s)3r;mv7`{4PECPX#49KMOPjMsyJZM)q+uT~&|&4To{qiwgub^t`YZ>4S9;nq;x2SCr#;ru3~ zH#gh|08Vvwv~87Qwd&3xfb+Z^Z5@pPnD=W_5l=A55%Dax!M37ngKn)oOyA|>sc5rv zTm@nx^v~XoVSAj{G09=&0%$z8;bns_1xMiu$p*FL2m9{^1F~klumApD)fuAJ`>;WB z2}45A&eglPaIIu?*aDUqwN^!)Hqp2>vWdd)WDQ63hHu&awX==rJXNf;S9%q0X^n$mWO z^_&qt^oSrlARx?Lo;B=jMvnGm82J7EW7PlQ_C4NZN$pGP?l8^>LaECBP6-ySzX9*e zT~h1vZV+?^8lV2|7Z?s2?(p9s-j5rt1ri{*LFykm4L4VRR`vXJ1a|ihAYXXwh~cgM z2Zl#j%@g_`?M{d!MTY|J#8eMiWXDdDwwpI_=M=Y%LL#xi>kENW zuj8cTxbYtLP%Kf~fXM*Lf`K_CB^jiMY0+u1Pb6h@lAL)|mM~!r`v0t8mKYziS`v$! zEtt5?c;M`PiX4`fY0aP?4@=9u6XV