diff --git a/app/Config/Routes.php b/app/Config/Routes.php index b1cc0a484..5db3d85b8 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -1,4 +1,4 @@ -get('login', 'Login::index'); $routes->post('login', 'Login::index'); $routes->post('migrate', 'Login::migrate'); +$routes->get('sales', 'Sales::getIndex'); +$routes->get('sales/customerDisplay', 'Sales::getCustomerDisplay'); +$routes->get('sales/itemSearch', 'Sales::getItemSearch'); +$routes->post('sales/selectCustomer', 'Sales::postSelectCustomer'); +$routes->post('sales/changeMode', 'Sales::postChangeMode'); +$routes->post('sales/setComment', 'Sales::postSetComment'); +$routes->post('sales/setInvoiceNumber', 'Sales::postSetInvoiceNumber'); +$routes->post('sales/setPaymentType', 'Sales::postSetPaymentType'); +$routes->post('sales/setPrintAfterSale', 'Sales::postSetPrintAfterSale'); +$routes->post('sales/setPriceWorkOrders', 'Sales::postSetPriceWorkOrders'); +$routes->post('sales/setEmailReceipt', 'Sales::postSetEmailReceipt'); +$routes->post('sales/addPayment', 'Sales::postAddPayment'); +$routes->post('sales/add', 'Sales::postAdd'); +$routes->post('sales/editItem/(:segment)', 'Sales::postEditItem/$1'); +$routes->post('sales/deleteItem/(:segment)', 'Sales::getDeleteItem/$1'); +$routes->post('sales/deletePayment/(:segment)', 'Sales::getDeletePayment/$1'); +$routes->post('sales/removeCustomer', 'Sales::getRemoveCustomer'); +$routes->post('sales/complete', 'Sales::postComplete'); +$routes->post('sales/cancel', 'Sales::postCancel'); +$routes->post('sales/suspend', 'Sales::postSuspend'); +$routes->post('sales/unsuspend', 'Sales::postUnsuspend'); +$routes->post('sales/checkInvoiceNumber', 'Sales::postCheckInvoiceNumber'); +$routes->post('sales/changeItemNumber', 'Sales::postChangeItemNumber'); +$routes->post('sales/changeItemName', 'Sales::postChangeItemName'); +$routes->post('sales/changeItemDescription', 'Sales::postChangeItemDescription'); +$routes->get('sales/suspended', 'Sales::getSuspended'); +$routes->get('sales/discardSuspendedSale', 'Sales::getDiscardSuspendedSale'); +$routes->get('sales/sales_keyboard_help', 'Sales::getSalesKeyboardHelp'); +$routes->get('sales/receipt/(:num)', 'Sales::getReceipt/$1'); +$routes->get('sales/invoice/(:num)', 'Sales::getInvoice/$1'); +$routes->get('sales/edit/(:num)', 'Sales::getEdit/$1'); +$routes->post('sales/delete/(:num)', 'Sales::postDelete/$1'); +$routes->post('sales/save/(:num)', 'Sales::postSave/$1'); + $routes->add('no_access/index/(:segment)', 'No_access::index/$1'); $routes->add('no_access/index/(:segment)/(:segment)', 'No_access::index/$1/$2'); @@ -39,4 +73,4 @@ $routes->add('reports/specific_(:any)/(:any)/(:any)/(:any)', 'Reports::Specific_ $routes->add('reports/specific_customers', 'Reports::specific_customer_input'); $routes->add('reports/specific_employees', 'Reports::specific_employee_input'); $routes->add('reports/specific_discounts', 'Reports::specific_discount_input'); -$routes->add('reports/specific_suppliers', 'Reports::specific_supplier_input'); +$routes->add('reports/specific_suppliers', 'Reports::specific_supplier_input'); diff --git a/app/Controllers/Config.php b/app/Controllers/Config.php index df95fc82a..e1d38adc2 100644 --- a/app/Controllers/Config.php +++ b/app/Controllers/Config.php @@ -1,224 +1,224 @@ -barcode_lib = new Barcode_lib(); - $this->sale_lib = new Sale_lib(); - $this->receiving_lib = new receiving_lib(); - $this->tax_lib = new Tax_lib(); - $this->appconfig = model(Appconfig::class); - $this->attribute = model(Attribute::class); - $this->customer_rewards = model(Customer_rewards::class); - $this->dinner_table = model(Dinner_table::class); - $this->module = model(Module::class); - $this->stock_location = model(Stock_location::class); - $this->tax = model(Tax::class); - $this->config = config(OSPOS::class)->settings; - $this->db = Database::connect(); - - helper('security'); - if (check_encryption()) { - $this->encrypter = Services::encrypter(); - } else { - log_message('alert', 'Error preparing encryption key'); - } - } - - /** - * This function loads all the licenses starting with the first one being OSPOS one - */ - private function _licenses(): array // TODO: remove hungarian notation. Super long function. Perhaps we need to refactor out functions? - { - $i = 0; - $composer = false; - $npmProd = false; - $npmDev = false; - $license = []; - - $license[$i]['title'] = 'Open Source Point of Sale ' . config('App')->application_version; - - if (file_exists('license/LICENSE')) { - $license[$i]['text'] = file_get_contents('license/LICENSE', false, null, 0, 3000); - } else { - $license[$i]['text'] = 'LICENSE file must be in OSPOS license directory. You are not allowed to use OSPOS application until the distribution copy of LICENSE file is present.'; - } - - $dir = new DirectoryIterator('license'); // Read all the files in the dir license - - foreach ($dir as $fileinfo) { // TODO: $fileinfo doesn't match our variable naming convention - // License files must be in couples: .version (name & version) & .license (license text) - if ($fileinfo->isFile()) { - if ($fileinfo->getExtension() == 'version') { - ++$i; - - $basename = 'license/' . $fileinfo->getBasename('.version'); - - $license[$i]['title'] = file_get_contents($basename . '.version', false, null, 0, 100); - - $license_text_file = $basename . '.license'; - - if (file_exists($license_text_file)) { - $license[$i]['text'] = file_get_contents($license_text_file, false, null, 0, 2000); - } else { - $license[$i]['text'] = $license_text_file . ' file is missing'; - } - } elseif ($fileinfo->getBasename() == 'composer.LICENSES') { - // Set a flag to indicate that the composer.LICENSES file is available and needs to be attached at the end - $composer = true; - } elseif ($fileinfo->getBasename() == 'npm-prod.LICENSES') { - // Set a flag to indicate that the npm-prod.LICENSES file is available and needs to be attached at the end - $npmProd = true; - } elseif ($fileinfo->getBasename() == 'npm-dev.LICENSES') { - // Set a flag to indicate that the npm-dev.LICENSES file is available and needs to be attached at the end - $npmDev = true; - } - } - } - - // Attach the licenses from the LICENSES file generated by Composer - if ($composer) { - ++$i; - $license[$i]['title'] = 'Composer Libraries'; - $license[$i]['text'] = ''; - - $file = file_get_contents('license/composer.LICENSES'); - $array = json_decode($file, true); - - if (isset($array['dependencies'])) { - foreach ($array['dependencies'] as $dependency => $details) { - $license[$i]['text'] .= "library: $dependency\n"; - - foreach ($details as $key => $value) { - if (is_array($value)) { - $license[$i]['text'] .= "$key: " . implode(' ', $value) . "\n"; - } else { - $license[$i]['text'] .= "$key: $value\n"; - } - } - - $license[$i]['text'] .= "\n"; - } - $license[$i]['text'] = rtrim($license[$i]['text'], "\n"); - } - } - - // Attach the licenses from the LICENSES file generated by license-report - if ($npmProd) { - ++$i; - $license[$i]['title'] = 'NPM Production Libraries'; - $license[$i]['text'] = ''; - - $file = file_get_contents('license/npm-prod.LICENSES'); - $array = json_decode($file, true); - - foreach ($array as $dependency) { - $license[$i]['text'] .= "library: {$dependency['name']}\n"; - $license[$i]['text'] .= "authors: {$dependency['author']}\n"; - $license[$i]['text'] .= "website: {$dependency['homepage']}\n"; - $license[$i]['text'] .= "version: {$dependency['installedVersion']}\n"; - $license[$i]['text'] .= "license: {$dependency['licenseType']}\n"; - - $license[$i]['text'] .= "\n"; - } - $license[$i]['text'] = rtrim($license[$i]['text'], "\n"); - } - - if ($npmDev) { - ++$i; - $license[$i]['title'] = 'NPM Development Libraries'; - $license[$i]['text'] = ''; - - $file = file_get_contents('license/npm-dev.LICENSES'); - $array = json_decode($file, true); - - foreach ($array as $dependency) { - $license[$i]['text'] .= "library: {$dependency['name']}\n"; - $license[$i]['text'] .= "authors: {$dependency['author']}\n"; - $license[$i]['text'] .= "website: {$dependency['homepage']}\n"; - $license[$i]['text'] .= "version: {$dependency['installedVersion']}\n"; - $license[$i]['text'] .= "license: {$dependency['licenseType']}\n"; - - $license[$i]['text'] .= "\n"; - } - $license[$i]['text'] = rtrim($license[$i]['text'], "\n"); - } - - return $license; - } - - /** - * This function loads all the available themes in the dist/bootswatch directory - * @return array - */ - private function _themes(): array // TODO: Hungarian notation - { - $themes = []; - - // Read all themes in the dist folder - $dir = new DirectoryIterator('resources/bootswatch'); - - foreach ($dir as $dirinfo) { // TODO: $dirinfo doesn't follow naming convention - if ($dirinfo->isDir() && !$dirinfo->isDot() && $dirinfo->getFileName() != 'fonts') { - $file = $dirinfo->getFileName(); - $themes[$file] = ucfirst($file); - } - } - - asort($themes); - - return $themes; - } - - /** - * @return string - */ +barcode_lib = new Barcode_lib(); + $this->sale_lib = new Sale_lib(); + $this->receiving_lib = new receiving_lib(); + $this->tax_lib = new Tax_lib(); + $this->appconfig = model(Appconfig::class); + $this->attribute = model(Attribute::class); + $this->customer_rewards = model(Customer_rewards::class); + $this->dinner_table = model(Dinner_table::class); + $this->module = model(Module::class); + $this->stock_location = model(Stock_location::class); + $this->tax = model(Tax::class); + $this->config = config(OSPOS::class)->settings; + $this->db = Database::connect(); + + helper('security'); + if (check_encryption()) { + $this->encrypter = Services::encrypter(); + } else { + log_message('alert', 'Error preparing encryption key'); + } + } + + /** + * This function loads all the licenses starting with the first one being OSPOS one + */ + private function _licenses(): array // TODO: remove hungarian notation. Super long function. Perhaps we need to refactor out functions? + { + $i = 0; + $composer = false; + $npmProd = false; + $npmDev = false; + $license = []; + + $license[$i]['title'] = 'Open Source Point Of Sale ' . config('App')->application_version; + + if (file_exists('license/LICENSE')) { + $license[$i]['text'] = file_get_contents('license/LICENSE', false, null, 0, 3000); + } else { + $license[$i]['text'] = 'LICENSE file must be in OSPOS license directory. You are not allowed to use OSPOS application until the distribution copy of LICENSE file is present.'; + } + + $dir = new DirectoryIterator('license'); // Read all the files in the dir license + + foreach ($dir as $fileinfo) { // TODO: $fileinfo doesn't match our variable naming convention + // License files must be in couples: .version (name & version) & .license (license text) + if ($fileinfo->isFile()) { + if ($fileinfo->getExtension() == 'version') { + ++$i; + + $basename = 'license/' . $fileinfo->getBasename('.version'); + + $license[$i]['title'] = file_get_contents($basename . '.version', false, null, 0, 100); + + $license_text_file = $basename . '.license'; + + if (file_exists($license_text_file)) { + $license[$i]['text'] = file_get_contents($license_text_file, false, null, 0, 2000); + } else { + $license[$i]['text'] = $license_text_file . ' file is missing'; + } + } elseif ($fileinfo->getBasename() == 'composer.LICENSES') { + // Set a flag to indicate that the composer.LICENSES file is available and needs to be attached at the end + $composer = true; + } elseif ($fileinfo->getBasename() == 'npm-prod.LICENSES') { + // Set a flag to indicate that the npm-prod.LICENSES file is available and needs to be attached at the end + $npmProd = true; + } elseif ($fileinfo->getBasename() == 'npm-dev.LICENSES') { + // Set a flag to indicate that the npm-dev.LICENSES file is available and needs to be attached at the end + $npmDev = true; + } + } + } + + // Attach the licenses from the LICENSES file generated by Composer + if ($composer) { + ++$i; + $license[$i]['title'] = 'Composer Libraries'; + $license[$i]['text'] = ''; + + $file = file_get_contents('license/composer.LICENSES'); + $array = json_decode($file, true); + + if (isset($array['dependencies'])) { + foreach ($array['dependencies'] as $dependency => $details) { + $license[$i]['text'] .= "library: $dependency\n"; + + foreach ($details as $key => $value) { + if (is_array($value)) { + $license[$i]['text'] .= "$key: " . implode(' ', $value) . "\n"; + } else { + $license[$i]['text'] .= "$key: $value\n"; + } + } + + $license[$i]['text'] .= "\n"; + } + $license[$i]['text'] = rtrim($license[$i]['text'], "\n"); + } + } + + // Attach the licenses from the LICENSES file generated by license-report + if ($npmProd) { + ++$i; + $license[$i]['title'] = 'NPM Production Libraries'; + $license[$i]['text'] = ''; + + $file = file_get_contents('license/npm-prod.LICENSES'); + $array = json_decode($file, true); + + foreach ($array as $dependency) { + $license[$i]['text'] .= "library: {$dependency['name']}\n"; + $license[$i]['text'] .= "authors: {$dependency['author']}\n"; + $license[$i]['text'] .= "website: {$dependency['homepage']}\n"; + $license[$i]['text'] .= "version: {$dependency['installedVersion']}\n"; + $license[$i]['text'] .= "license: {$dependency['licenseType']}\n"; + + $license[$i]['text'] .= "\n"; + } + $license[$i]['text'] = rtrim($license[$i]['text'], "\n"); + } + + if ($npmDev) { + ++$i; + $license[$i]['title'] = 'NPM Development Libraries'; + $license[$i]['text'] = ''; + + $file = file_get_contents('license/npm-dev.LICENSES'); + $array = json_decode($file, true); + + foreach ($array as $dependency) { + $license[$i]['text'] .= "library: {$dependency['name']}\n"; + $license[$i]['text'] .= "authors: {$dependency['author']}\n"; + $license[$i]['text'] .= "website: {$dependency['homepage']}\n"; + $license[$i]['text'] .= "version: {$dependency['installedVersion']}\n"; + $license[$i]['text'] .= "license: {$dependency['licenseType']}\n"; + + $license[$i]['text'] .= "\n"; + } + $license[$i]['text'] = rtrim($license[$i]['text'], "\n"); + } + + return $license; + } + + /** + * This function loads all the available themes in the dist/bootswatch directory + * @return array + */ + private function _themes(): array // TODO: Hungarian notation + { + $themes = []; + + // Read all themes in the dist folder + $dir = new DirectoryIterator('resources/bootswatch'); + + foreach ($dir as $dirinfo) { // TODO: $dirinfo doesn't follow naming convention + if ($dirinfo->isDir() && !$dirinfo->isDot() && $dirinfo->getFileName() != 'fonts') { + $file = $dirinfo->getFileName(); + $themes[$file] = ucfirst($file); + } + } + + asort($themes); + + return $themes; + } + + /** + * @return string + */ public function getIndex(): string { $data['config'] = $this->config; @@ -241,712 +241,745 @@ class Config extends Secure_Controller $data['show_office_group'] = $this->module->get_show_office_group(); $data['currency_code'] = $this->config['currency_code'] ?? ''; $data['dbVersion'] = mysqli_get_server_info($this->db->getConnection()); + $data['scale_export_formats'] = [ + 'txt' => 'TXT', + 'csv' => 'CSV', + ]; + $data['scale_export_charsets'] = [ + 'windows-1256' => 'Windows-1256', + 'utf-8' => 'UTF-8', + 'windows-1252' => 'Windows-1252', + ]; + $data['scale_export_delimiters'] = [ + ';' => ';', + ',' => ',', + "\t" => 'Tab', + ]; + $data['scale_export_fields_options'] = [ + 'legacy_code' => lang('Items.item_number'), + 'item_number' => lang('Items.item_number'), + 'repeat_item_number' => lang('Items.item_number'), + 'name' => lang('Items.name'), + 'unit_price' => lang('Items.unit_price'), + 'legacy_tail' => lang('Items.item_number'), + ]; // Load all the license statements, they are already XSS cleaned in the private function $data['licenses'] = $this->_licenses(); - - // Load all the themes, already XSS cleaned in the private function - $data['themes'] = $this->_themes(); - - // General related fields - $image_allowed_types = ['jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'png', 'tif', 'tiff']; - $data['image_allowed_types'] = array_combine($image_allowed_types, $image_allowed_types); - $data['selected_image_allowed_types'] = explode(',', $this->config['image_allowed_types']); - - // Integrations Related fields - $data['mailchimp'] = []; - - if (check_encryption()) { // TODO: Hungarian notation - if (!isset($this->encrypter)) { - helper('security'); - $this->encrypter = Services::encrypter(); - } - - $data['mailchimp']['api_key'] = (isset($this->config['mailchimp_api_key']) && !empty($this->config['mailchimp_api_key'])) - ? $this->encrypter->decrypt($this->config['mailchimp_api_key']) - : ''; - - $data['mailchimp']['list_id'] = (isset($this->config['mailchimp_list_id']) && !empty($this->config['mailchimp_list_id'])) - ? $this->encrypter->decrypt($this->config['mailchimp_list_id']) - : ''; - - // Remove any backup of .env created by check_encryption() - remove_backup(); - } else { - $data['mailchimp']['api_key'] = ''; - $data['mailchimp']['list_id'] = ''; - } - - $data['mailchimp']['lists'] = $this->_mailchimp(); - - return view('configs/manage', $data); - } - - /** - * Saves company information. Used in app/Views/configs/info_config.php - * - * @throws ReflectionException - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function postSaveInfo(): ResponseInterface - { - $upload_data = $this->upload_logo(); - $upload_success = empty($upload_data['error']); - - $batch_save_data = [ - 'company' => $this->request->getPost('company'), - 'address' => $this->request->getPost('address'), - 'phone' => $this->request->getPost('phone'), - 'email' => strtolower($this->request->getPost('email', FILTER_SANITIZE_EMAIL)), - 'fax' => $this->request->getPost('fax'), - 'website' => $this->request->getPost('website', FILTER_SANITIZE_URL), - 'return_policy' => $this->request->getPost('return_policy') - ]; - - if (!empty($upload_data['orig_name']) && $upload_data['raw_name']) { - $batch_save_data['company_logo'] = $upload_data['raw_name'] . '.' . $upload_data['file_ext']; - } - - $result = $this->appconfig->batch_save($batch_save_data); - $success = $upload_success && $result; - $message = lang('Config.saved_' . ($success ? '' : 'un') . 'successfully'); - $message = $upload_success ? $message : strip_tags($upload_data['error']); - - return $this->response->setJSON(['success' => $success, 'message' => $message]); - } - - - /** - * @return array - */ - private function upload_logo(): array - { - $file = $this->request->getFile('company_logo'); - if (!$file) { - return []; - } - - helper(['form']); - $validation_rule = [ - 'company_logo' => [ - 'label' => 'Company logo', - 'rules' => [ - 'uploaded[company_logo]', - 'is_image[company_logo]', - 'max_size[company_logo,1024]', - 'mime_in[company_logo,image/png,image/jpg,image/jpeg,image/gif]', - 'ext_in[company_logo,png,jpg,gif]', - 'max_dims[company_logo,800,680]', - ] - ] - ]; - - if (!$this->validate($validation_rule)) { - return (['error' => $this->validator->getError('company_logo')]); - } - - - $filename = $file->getClientName(); - $info = pathinfo($filename); - - $file_info = [ - 'orig_name' => $filename, - 'raw_name' => $info['filename'], - 'file_ext' => $file->guessExtension() - ]; - - $file->move(FCPATH . 'uploads/', $file_info['raw_name'] . '.' . $file_info['file_ext'], true); - - return ($file_info); - } - - /** - * Saves general configuration. Used in app/Views/configs/general_config.php - * - * @throws ReflectionException - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function postSaveGeneral(): ResponseInterface - { - $batchSaveData = [ - 'theme' => $this->request->getPost('theme'), - 'login_form' => $this->request->getPost('login_form'), - 'default_sales_discount_type' => $this->request->getPost('default_sales_discount_type') != null, - 'default_sales_discount' => parse_decimals($this->request->getPost('default_sales_discount')), - 'default_receivings_discount_type' => $this->request->getPost('default_receivings_discount_type') != null, - 'default_receivings_discount' => parse_decimals($this->request->getPost('default_receivings_discount')), - 'enforce_privacy' => $this->request->getPost('enforce_privacy') != null, - 'receiving_calculate_average_price' => $this->request->getPost('receiving_calculate_average_price') != null, - 'lines_per_page' => $this->request->getPost('lines_per_page', FILTER_SANITIZE_NUMBER_INT), - 'notify_horizontal_position' => $this->request->getPost('notify_horizontal_position'), - 'notify_vertical_position' => $this->request->getPost('notify_vertical_position'), - 'image_max_width' => $this->request->getPost('image_max_width', FILTER_SANITIZE_NUMBER_INT), - 'image_max_height' => $this->request->getPost('image_max_height', FILTER_SANITIZE_NUMBER_INT), - 'image_max_size' => $this->request->getPost('image_max_size', FILTER_SANITIZE_NUMBER_INT), - 'image_allowed_types' => implode(',', $this->request->getPost('image_allowed_types')), - 'gcaptcha_enable' => $this->request->getPost('gcaptcha_enable') != null, - 'gcaptcha_secret_key' => $this->request->getPost('gcaptcha_secret_key'), - 'gcaptcha_site_key' => $this->request->getPost('gcaptcha_site_key'), - 'suggestions_first_column' => $this->validateSuggestionsColumn($this->request->getPost('suggestions_first_column'), 'first'), - 'suggestions_second_column' => $this->validateSuggestionsColumn($this->request->getPost('suggestions_second_column'), 'other'), - 'suggestions_third_column' => $this->validateSuggestionsColumn($this->request->getPost('suggestions_third_column'), 'other'), - 'giftcard_number' => $this->request->getPost('giftcard_number'), - 'derive_sale_quantity' => $this->request->getPost('derive_sale_quantity') != null, - 'multi_pack_enabled' => $this->request->getPost('multi_pack_enabled') != null, - 'include_hsn' => $this->request->getPost('include_hsn') != null, - 'category_dropdown' => $this->request->getPost('category_dropdown') != null - ]; - - $this->module->set_show_office_group($this->request->getPost('show_office_group') != null); - - $this->db->transStart(); - - $attributeSuccess = true; - if ($batchSaveData['category_dropdown']) { - $definitionData['definition_name'] = 'ospos_category'; - $definitionData['definition_flags'] = 0; - $definitionData['definition_type'] = 'DROPDOWN'; - $definitionData['definition_id'] = CATEGORY_DEFINITION_ID; - $definitionData['deleted'] = 0; - - $attributeSuccess = $this->attribute->saveDefinition($definitionData, CATEGORY_DEFINITION_ID); - } elseif ($batchSaveData['category_dropdown'] == NO_DEFINITION_ID) { - $attributeSuccess = $this->attribute->deleteDefinition(CATEGORY_DEFINITION_ID); - } - - $success = $attributeSuccess && $this->appconfig->batch_save($batchSaveData); - - $this->db->transComplete(); - - $success = $success && $this->db->transStatus(); - - return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]); - } - - /** - * Checks a number against the currently selected locale. Used in app/Views/configs/locale_config.php - * - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function postCheckNumberLocale(): ResponseInterface - { - $numberLocale = $this->request->getPost('number_locale'); - $saveNumberLocale = $this->request->getPost('save_number_locale'); - $postedCurrencySymbol = $this->request->getPost('currency_symbol'); - $postedCurrencyCode = $this->request->getPost('currency_code'); - - $fmt = new NumberFormatter($numberLocale, NumberFormatter::CURRENCY); - - // Use posted values if provided, otherwise fall back to locale defaults - $currencySymbol = $postedCurrencySymbol !== '' ? $postedCurrencySymbol : $fmt->getSymbol(NumberFormatter::CURRENCY_SYMBOL); - $currencyCode = $postedCurrencyCode !== '' ? $postedCurrencyCode : $fmt->getTextAttribute(NumberFormatter::CURRENCY_CODE); - - // Update saved locale if it changed - if ($numberLocale !== $saveNumberLocale) { - $saveNumberLocale = $numberLocale; - } - - if ($this->request->getPost('thousands_separator') == 'false') { - $fmt->setTextAttribute(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, ''); - } - - $fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL, $currencySymbol); - $numberLocaleExample = $fmt->format(1234567890.12300); - - return $this->response->setJSON([ - 'success' => $numberLocaleExample != false, - 'save_number_locale' => $saveNumberLocale, - 'number_locale_example' => $numberLocaleExample, - 'currency_symbol' => $currencySymbol, - 'currency_code' => $currencyCode, - ]); - } - - /** - * Saves locale configuration. Used in app/Views/configs/locale_config.php - * - * @throws ReflectionException - * @return ResponseInterface - * @noinspection PhpUnused - */ + + // Load all the themes, already XSS cleaned in the private function + $data['themes'] = $this->_themes(); + + // General related fields + $image_allowed_types = ['jpg', 'jpeg', 'gif', 'svg', 'webp', 'bmp', 'png', 'tif', 'tiff']; + $data['image_allowed_types'] = array_combine($image_allowed_types, $image_allowed_types); + $data['selected_image_allowed_types'] = explode(',', $this->config['image_allowed_types']); + + // Integrations Related fields + $data['mailchimp'] = []; + + if (check_encryption()) { // TODO: Hungarian notation + if (!isset($this->encrypter)) { + helper('security'); + $this->encrypter = Services::encrypter(); + } + + $data['mailchimp']['api_key'] = (isset($this->config['mailchimp_api_key']) && !empty($this->config['mailchimp_api_key'])) + ? $this->encrypter->decrypt($this->config['mailchimp_api_key']) + : ''; + + $data['mailchimp']['list_id'] = (isset($this->config['mailchimp_list_id']) && !empty($this->config['mailchimp_list_id'])) + ? $this->encrypter->decrypt($this->config['mailchimp_list_id']) + : ''; + + // Remove any backup of .env created by check_encryption() + remove_backup(); + } else { + $data['mailchimp']['api_key'] = ''; + $data['mailchimp']['list_id'] = ''; + } + + $data['mailchimp']['lists'] = $this->_mailchimp(); + + return view('configs/manage', $data); + } + + /** + * Saves company information. Used in app/Views/configs/info_config.php + * + * @throws ReflectionException + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function postSaveInfo(): ResponseInterface + { + $upload_data = $this->upload_logo(); + $upload_success = empty($upload_data['error']); + + $batch_save_data = [ + 'company' => $this->request->getPost('company'), + 'address' => $this->request->getPost('address'), + 'phone' => $this->request->getPost('phone'), + 'email' => strtolower($this->request->getPost('email', FILTER_SANITIZE_EMAIL)), + 'fax' => $this->request->getPost('fax'), + 'website' => $this->request->getPost('website', FILTER_SANITIZE_URL), + 'return_policy' => $this->request->getPost('return_policy') + ]; + + if (!empty($upload_data['orig_name']) && $upload_data['raw_name']) { + $batch_save_data['company_logo'] = $upload_data['raw_name'] . '.' . $upload_data['file_ext']; + } + + $result = $this->appconfig->batch_save($batch_save_data); + $success = $upload_success && $result; + $message = lang('Config.saved_' . ($success ? '' : 'un') . 'successfully'); + $message = $upload_success ? $message : strip_tags($upload_data['error']); + + return $this->response->setJSON(['success' => $success, 'message' => $message]); + } + + + /** + * @return array + */ + private function upload_logo(): array + { + $file = $this->request->getFile('company_logo'); + if (!$file) { + return []; + } + + helper(['form']); + $validation_rule = [ + 'company_logo' => [ + 'label' => 'Company logo', + 'rules' => [ + 'uploaded[company_logo]', + 'is_image[company_logo]', + 'max_size[company_logo,1024]', + 'mime_in[company_logo,image/png,image/jpg,image/jpeg,image/gif]', + 'ext_in[company_logo,png,jpg,gif]', + 'max_dims[company_logo,800,680]', + ] + ] + ]; + + if (!$this->validate($validation_rule)) { + return (['error' => $this->validator->getError('company_logo')]); + } + + + $filename = $file->getClientName(); + $info = pathinfo($filename); + + $file_info = [ + 'orig_name' => $filename, + 'raw_name' => $info['filename'], + 'file_ext' => $file->guessExtension() + ]; + + $file->move(FCPATH . 'uploads/', $file_info['raw_name'] . '.' . $file_info['file_ext'], true); + + return ($file_info); + } + + /** + * Saves general configuration. Used in app/Views/configs/general_config.php + * + * @throws ReflectionException + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function postSaveGeneral(): ResponseInterface + { + $batchSaveData = [ + 'theme' => $this->request->getPost('theme'), + 'login_form' => $this->request->getPost('login_form'), + 'default_sales_discount_type' => $this->request->getPost('default_sales_discount_type') != null, + 'default_sales_discount' => parse_decimals($this->request->getPost('default_sales_discount')), + 'default_receivings_discount_type' => $this->request->getPost('default_receivings_discount_type') != null, + 'default_receivings_discount' => parse_decimals($this->request->getPost('default_receivings_discount')), + 'enforce_privacy' => $this->request->getPost('enforce_privacy') != null, + 'receiving_calculate_average_price' => $this->request->getPost('receiving_calculate_average_price') != null, + 'lines_per_page' => $this->request->getPost('lines_per_page', FILTER_SANITIZE_NUMBER_INT), + 'notify_horizontal_position' => $this->request->getPost('notify_horizontal_position'), + 'notify_vertical_position' => $this->request->getPost('notify_vertical_position'), + 'image_max_width' => $this->request->getPost('image_max_width', FILTER_SANITIZE_NUMBER_INT), + 'image_max_height' => $this->request->getPost('image_max_height', FILTER_SANITIZE_NUMBER_INT), + 'image_max_size' => $this->request->getPost('image_max_size', FILTER_SANITIZE_NUMBER_INT), + 'image_allowed_types' => implode(',', $this->request->getPost('image_allowed_types')), + 'gcaptcha_enable' => $this->request->getPost('gcaptcha_enable') != null, + 'gcaptcha_secret_key' => $this->request->getPost('gcaptcha_secret_key'), + 'gcaptcha_site_key' => $this->request->getPost('gcaptcha_site_key'), + 'suggestions_first_column' => $this->validateSuggestionsColumn($this->request->getPost('suggestions_first_column'), 'first'), + 'suggestions_second_column' => $this->validateSuggestionsColumn($this->request->getPost('suggestions_second_column'), 'other'), + 'suggestions_third_column' => $this->validateSuggestionsColumn($this->request->getPost('suggestions_third_column'), 'other'), + 'giftcard_number' => $this->request->getPost('giftcard_number'), + 'derive_sale_quantity' => $this->request->getPost('derive_sale_quantity') != null, + 'customer_display_enabled' => $this->request->getPost('customer_display_enabled') != null, + 'multi_pack_enabled' => $this->request->getPost('multi_pack_enabled') != null, + 'include_hsn' => $this->request->getPost('include_hsn') != null, + 'category_dropdown' => $this->request->getPost('category_dropdown') != null + ]; + + $this->module->set_show_office_group($this->request->getPost('show_office_group') != null); + + $this->db->transStart(); + + $attributeSuccess = true; + if ($batchSaveData['category_dropdown']) { + $definitionData['definition_name'] = 'ospos_category'; + $definitionData['definition_flags'] = 0; + $definitionData['definition_type'] = 'DROPDOWN'; + $definitionData['definition_id'] = CATEGORY_DEFINITION_ID; + $definitionData['deleted'] = 0; + + $attributeSuccess = $this->attribute->saveDefinition($definitionData, CATEGORY_DEFINITION_ID); + } elseif ($batchSaveData['category_dropdown'] == NO_DEFINITION_ID) { + $attributeSuccess = $this->attribute->deleteDefinition(CATEGORY_DEFINITION_ID); + } + + $success = $attributeSuccess && $this->appconfig->batch_save($batchSaveData); + + $this->db->transComplete(); + + $success = $success && $this->db->transStatus(); + + return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]); + } + + /** + * Checks a number against the currently selected locale. Used in app/Views/configs/locale_config.php + * + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function postCheckNumberLocale(): ResponseInterface + { + $numberLocale = $this->request->getPost('number_locale'); + $saveNumberLocale = $this->request->getPost('save_number_locale'); + $postedCurrencySymbol = $this->request->getPost('currency_symbol'); + $postedCurrencyCode = $this->request->getPost('currency_code'); + + $fmt = new NumberFormatter($numberLocale, NumberFormatter::CURRENCY); + + // Use posted values if provided, otherwise fall back to locale defaults + $currencySymbol = $postedCurrencySymbol !== '' ? $postedCurrencySymbol : $fmt->getSymbol(NumberFormatter::CURRENCY_SYMBOL); + $currencyCode = $postedCurrencyCode !== '' ? $postedCurrencyCode : $fmt->getTextAttribute(NumberFormatter::CURRENCY_CODE); + + // Update saved locale if it changed + if ($numberLocale !== $saveNumberLocale) { + $saveNumberLocale = $numberLocale; + } + + if ($this->request->getPost('thousands_separator') == 'false') { + $fmt->setTextAttribute(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, ''); + } + + $fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL, $currencySymbol); + $numberLocaleExample = $fmt->format(1234567890.12300); + + return $this->response->setJSON([ + 'success' => $numberLocaleExample != false, + 'save_number_locale' => $saveNumberLocale, + 'number_locale_example' => $numberLocaleExample, + 'currency_symbol' => $currencySymbol, + 'currency_code' => $currencyCode, + ]); + } + + /** + * Saves locale configuration. Used in app/Views/configs/locale_config.php + * + * @throws ReflectionException + * @return ResponseInterface + * @noinspection PhpUnused + */ public function postSaveLocale(): ResponseInterface { $exploded = explode(":", $this->request->getPost('language')); $currency_symbol = $this->request->getPost('currency_symbol'); + $secondaryCurrencyCode = strtoupper(trim((string) $this->request->getPost('secondary_currency_code'))); + + if (!preg_match('/^[A-Z]{3}$/', $secondaryCurrencyCode)) { + $secondaryCurrencyCode = ''; + } + $batch_save_data = [ 'currency_symbol' => htmlspecialchars($currency_symbol ?? ''), 'currency_code' => $this->request->getPost('currency_code'), + 'secondary_currency_enabled' => $this->request->getPost('secondary_currency_enabled') != null, + 'secondary_currency_symbol' => htmlspecialchars($this->request->getPost('secondary_currency_symbol') ?? ''), + 'secondary_currency_code' => $secondaryCurrencyCode, + 'secondary_currency_rate' => $this->request->getPost('secondary_currency_rate', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION), + 'secondary_currency_decimals' => $this->request->getPost('secondary_currency_decimals', FILTER_SANITIZE_NUMBER_INT), 'language_code' => $exploded[0], 'language' => $exploded[1], 'timezone' => $this->request->getPost('timezone'), - 'dateformat' => $this->request->getPost('dateformat'), - 'timeformat' => $this->request->getPost('timeformat'), - 'thousands_separator' => $this->request->getPost('thousands_separator') != null, - 'number_locale' => $this->request->getPost('number_locale'), - 'currency_decimals' => $this->request->getPost('currency_decimals', FILTER_SANITIZE_NUMBER_INT), - 'tax_decimals' => $this->request->getPost('tax_decimals', FILTER_SANITIZE_NUMBER_INT), - 'quantity_decimals' => $this->request->getPost('quantity_decimals', FILTER_SANITIZE_NUMBER_INT), - 'country_codes' => htmlspecialchars($this->request->getPost('country_codes')), - 'payment_options_order' => $this->request->getPost('payment_options_order'), - 'date_or_time_format' => $this->request->getPost('date_or_time_format') != null, - 'cash_decimals' => $this->request->getPost('cash_decimals', FILTER_SANITIZE_NUMBER_INT), - 'cash_rounding_code' => $this->request->getPost('cash_rounding_code'), - 'financial_year' => $this->request->getPost('financial_year', FILTER_SANITIZE_NUMBER_INT) - ]; - - $success = $this->appconfig->batch_save($batch_save_data); - - return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]); - } - - /** - * Saves email configuration. Used in app/Views/configs/email_config.php - * - * @throws ReflectionException - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function postSaveEmail(): ResponseInterface - { - $password = ''; - - if (check_encryption() && !empty($this->request->getPost('smtp_pass'))) { - $password = $this->encrypter->encrypt($this->request->getPost('smtp_pass')); - } - - $protocol = $this->request->getPost('protocol'); - $mailpath = $this->request->getPost('mailpath'); - - // Validate mailpath: required for sendmail, optional for others but must be safe if provided - $isMailpathRequired = ($protocol === 'sendmail'); - $isMailpathProvided = !empty($mailpath); - $isMailpathValid = $isMailpathProvided && preg_match('/^[a-zA-Z0-9_\-\/.]+$/', $mailpath); - - if (($isMailpathRequired && !$isMailpathProvided) || ($isMailpathProvided && !$isMailpathValid)) { - return $this->response->setJSON([ - 'success' => false, - 'message' => lang('Config.mailpath_invalid') - ]); - } - - $batch_save_data = [ - 'protocol' => $protocol, - 'mailpath' => $mailpath, - 'smtp_host' => $this->request->getPost('smtp_host'), - 'smtp_user' => $this->request->getPost('smtp_user'), - 'smtp_pass' => $password, - 'smtp_port' => $this->request->getPost('smtp_port', FILTER_SANITIZE_NUMBER_INT), - 'smtp_timeout' => $this->request->getPost('smtp_timeout', FILTER_SANITIZE_NUMBER_INT), - 'smtp_crypto' => $this->request->getPost('smtp_crypto') - ]; - - $success = $this->appconfig->batch_save($batch_save_data); - - return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]); - } - - /** - * Saves SMS message configuration. Used in app/Views/configs/message_config.php. - * - * @throws ReflectionException - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function postSaveMessage(): ResponseInterface - { - $password = ''; - - if (check_encryption() && !empty($this->request->getPost('msg_pwd'))) { - $password = $this->encrypter->encrypt($this->request->getPost('msg_pwd')); - } - - $batch_save_data = [ - 'msg_msg' => $this->request->getPost('msg_msg'), - 'msg_uid' => $this->request->getPost('msg_uid'), - 'msg_pwd' => $password, - 'msg_src' => $this->request->getPost('msg_src') - ]; - - $success = $this->appconfig->batch_save($batch_save_data); - - return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]); - } - - /** - * This function fetches all the available lists from Mailchimp for the given API key - */ - private function _mailchimp(string $api_key = ''): array // TODO: Hungarian notation - { - $mailchimp_lib = new Mailchimp_lib(['api_key' => $api_key]); - - $result = []; - - $lists = $mailchimp_lib->getLists(); - if ($lists !== false) { - if (is_array($lists) && !empty($lists['lists']) && is_array($lists['lists'])) { - foreach ($lists['lists'] as $list) { - $result[$list['id']] = $list['name'] . ' [' . $list['stats']['member_count'] . ']'; - } - } - } - - return $result; - } - - /** - * Gets Mailchimp lists when a valid API key is inserted. Used in app/Views/configs/integrations_config.php - * - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function postCheckMailchimpApiKey(): ResponseInterface - { - $lists = $this->_mailchimp($this->request->getPost('mailchimp_api_key')); - $success = count($lists) > 0; - - return $this->response->setJSON([ - 'success' => $success, - 'message' => lang('Config.mailchimp_key_' . ($success ? '' : 'un') . 'successfully'), - 'mailchimp_lists' => $lists - ]); - } - - /** - * Saves Mailchimp configuration. Used in app/Views/configs/integrations_config.php - * - * @throws ReflectionException - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function postSaveMailchimp(): ResponseInterface - { - $api_key = ''; - $list_id = ''; - - if (check_encryption()) { - $api_key_unencrypted = $this->request->getPost('mailchimp_api_key'); - if (!empty($api_key_unencrypted)) { - $api_key = $this->encrypter->encrypt($api_key_unencrypted); - } - - $list_id_unencrypted = $this->request->getPost('mailchimp_list_id'); - if (!empty($list_id_unencrypted)) { - $list_id = $this->encrypter->encrypt($list_id_unencrypted); - } - } - - $batch_save_data = ['mailchimp_api_key' => $api_key, 'mailchimp_list_id' => $list_id]; - - $success = $this->appconfig->batch_save($batch_save_data); - - return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]); - } - - /** - * Gets all stock locations. Used in app/Views/configs/stock_config.php - * - * @return string - * @noinspection PhpUnused - */ - public function getStockLocations(): string - { - $stock_locations = $this->stock_location->get_all()->getResultArray(); - - return view('partial/stock_locations', ['stock_locations' => $stock_locations]); - } - - /** - * @return string - */ - public function getDinnerTables(): string - { - $dinner_tables = $this->dinner_table->get_all()->getResultArray(); - - return view('partial/dinner_tables', ['dinner_tables' => $dinner_tables]); - } - - - /** - * Gets all tax categories. - * - * @return string - */ - public function ajax_tax_categories(): string // TODO: Is this function called anywhere in the code? - { - $tax_categories = $this->tax->get_all_tax_categories()->getResultArray(); - - return view('partial/tax_categories', ['tax_categories' => $tax_categories]); - } - - /** - * Gets all customer rewards. Used in app/Views/configs/reward_config.php - * - * @return string - * @noinspection PhpUnused - */ - public function getCustomerRewards(): string - { - $customer_rewards = $this->customer_rewards->get_all()->getResultArray(); - - return view('partial/customer_rewards', ['customer_rewards' => $customer_rewards]); - } - - /** - * @return void - */ - private function _clear_session_state(): void // TODO: Hungarian notation - { - $this->sale_lib->clear_sale_location(); - $this->sale_lib->clear_table(); - $this->sale_lib->clear_all(); - $this->receiving_lib = new Receiving_lib(); - $this->receiving_lib->clear_stock_source(); - $this->receiving_lib->clear_stock_destination(); - $this->receiving_lib->clear_all(); - } - - /** - * Saves stock locations. Used in app/Views/configs/stock_config.php - * - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function postSaveLocations(): ResponseInterface - { - $this->db->transStart(); - - $not_to_delete = []; - foreach ($this->request->getPost() as $key => $value) { - if (str_contains($key, 'stock_location')) { - // Save or update - foreach ($value as $location_id => $location_name) { - $location_data = ['location_name' => $location_name]; - if ($this->stock_location->save_value($location_data, $location_id)) { - $location_id = $this->stock_location->get_location_id($location_name); - $not_to_delete[] = $location_id; - $this->_clear_session_state(); - } - } - } - } - - // All locations not available in post will be deleted now - $deleted_locations = $this->stock_location->get_all()->getResultArray(); - - foreach ($deleted_locations as $location => $location_data) { - if (!in_array($location_data['location_id'], $not_to_delete)) { - $this->stock_location->delete($location_data['location_id']); - } - } - - $this->db->transComplete(); - - $success = $this->db->transStatus(); - - return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]); - } - - /** - * Saves all dinner tables. Used in app/Views/configs/table_config.php - * - * @throws ReflectionException - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function postSaveTables(): ResponseInterface - { - $this->db->transStart(); - - $dinner_table_enable = $this->request->getPost('dinner_table_enable') != null; - - $this->appconfig->save(['dinner_table_enable' => $dinner_table_enable]); - - if ($dinner_table_enable) { - $not_to_delete = []; - foreach ($this->request->getPost() as $key => $value) { // TODO: Not sure if this is the best way to filter the array - if (strstr($key, 'dinner_table') && $key != 'dinner_table_enable') { - $dinner_table_id = preg_replace("/.*?_(\d+)$/", "$1", $key); - $not_to_delete[] = $dinner_table_id; - - // Save or update - $table_data = ['name' => $value]; - if ($this->dinner_table->save_value($table_data, $dinner_table_id)) { - $this->_clear_session_state(); // TODO: Remove hungarian notation. - } - } - } - - // All tables not available in post will be deleted now - $deleted_tables = $this->dinner_table->get_all()->getResultArray(); - - foreach ($deleted_tables as $dinner_tables => $table) { - if (!in_array($table['dinner_table_id'], $not_to_delete)) { - $this->dinner_table->delete($table['dinner_table_id']); - } - } - } - - $this->db->transComplete(); - - $success = $this->db->transStatus(); - - return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]); - } - - /** - * Saves tax configuration. Used in app/Views/configs/tax_config.php - * - * @throws ReflectionException - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function postSaveTax(): ResponseInterface - { - $default_tax_1_rate = $this->request->getPost('default_tax_1_rate'); - $default_tax_2_rate = $this->request->getPost('default_tax_2_rate'); - - $batch_save_data = [ - 'default_tax_1_rate' => parse_tax(filter_var($default_tax_1_rate, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION)), - 'default_tax_1_name' => $this->request->getPost('default_tax_1_name'), - 'default_tax_2_rate' => parse_tax(filter_var($default_tax_2_rate, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION)), - 'default_tax_2_name' => $this->request->getPost('default_tax_2_name'), - 'tax_included' => $this->request->getPost('tax_included') != null, - 'use_destination_based_tax' => $this->request->getPost('use_destination_based_tax') != null, - 'default_tax_code' => $this->request->getPost('default_tax_code'), - 'default_tax_category' => $this->request->getPost('default_tax_category'), - 'default_tax_jurisdiction' => $this->request->getPost('default_tax_jurisdiction'), - 'tax_id' => $this->request->getPost('tax_id', FILTER_SANITIZE_NUMBER_INT) - ]; - - $success = $this->appconfig->batch_save($batch_save_data); - - $message = lang('Config.saved_' . ($success ? '' : 'un') . 'successfully'); - - return $this->response->setJSON(['success' => $success, 'message' => $message]); - } - - /** - * Saves customer rewards configuration. Used in app/Views/configs/reward_config.php - * - * @throws ReflectionException - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function postSaveRewards(): ResponseInterface - { - $this->db->transStart(); - - $customer_reward_enable = $this->request->getPost('customer_reward_enable') != null; - - $this->appconfig->save(['customer_reward_enable' => $customer_reward_enable]); - - if ($customer_reward_enable) { - $not_to_delete = []; - $array_save = []; - foreach ($this->request->getPost() as $key => $value) { - if (strstr($key, 'customer_reward') && $key != 'customer_reward_enable') { - $customer_reward_id = preg_replace("/.*?_(\d+)$/", "$1", $key); - $not_to_delete[] = $customer_reward_id; - $array_save[$customer_reward_id]['package_name'] = $value; - } elseif (str_contains($key, 'reward_points')) { - $customer_reward_id = preg_replace("/.*?_(\d+)$/", "$1", $key); - $array_save[$customer_reward_id]['points_percent'] = $value; - } - } - - if (!empty($array_save)) { - foreach ($array_save as $key => $value) { - // Save or update - $package_data = ['package_name' => $value['package_name'], 'points_percent' => $value['points_percent']]; - $this->customer_rewards->save_value($package_data, $key); // TODO: reflection exception - } - } - - // All packages not available in post will be deleted now - $deleted_packages = $this->customer_rewards->get_all()->getResultArray(); - - foreach ($deleted_packages as $customer_rewards => $reward_category) { - if (!in_array($reward_category['package_id'], $not_to_delete)) { - $this->customer_rewards->delete($reward_category['package_id']); - } - } - } - - $this->db->transComplete(); - - $success = $this->db->transStatus(); - - return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]); - } - - /** - * Saves barcode configuration. Used in app/Views/configs/barcode_config.php - * - * @throws ReflectionException - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function postSaveBarcode(): ResponseInterface - { - $batch_save_data = [ - 'barcode_type' => $this->request->getPost('barcode_type'), - 'barcode_width' => $this->request->getPost('barcode_width', FILTER_SANITIZE_NUMBER_INT), - 'barcode_height' => $this->request->getPost('barcode_height', FILTER_SANITIZE_NUMBER_INT), - 'barcode_font' => $this->request->getPost('barcode_font'), - 'barcode_font_size' => $this->request->getPost('barcode_font_size', FILTER_SANITIZE_NUMBER_INT), - 'barcode_first_row' => $this->request->getPost('barcode_first_row'), - 'barcode_second_row' => $this->request->getPost('barcode_second_row'), - 'barcode_third_row' => $this->request->getPost('barcode_third_row'), - 'barcode_num_in_row' => $this->request->getPost('barcode_num_in_row', FILTER_SANITIZE_NUMBER_INT), - 'barcode_page_width' => $this->request->getPost('barcode_page_width', FILTER_SANITIZE_NUMBER_INT), - 'barcode_page_cellspacing' => $this->request->getPost('barcode_page_cellspacing', FILTER_SANITIZE_NUMBER_INT), - 'barcode_generate_if_empty' => $this->request->getPost('barcode_generate_if_empty') != null, - 'allow_duplicate_barcodes' => $this->request->getPost('allow_duplicate_barcodes') != null, - 'barcode_content' => $this->request->getPost('barcode_content'), - 'barcode_formats' => json_encode($this->request->getPost('barcode_formats')) - ]; - - $success = $this->appconfig->batch_save($batch_save_data); - - return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]); - } - - /** - * Saves receipt configuration. Used in app/Views/configs/receipt_config.php. - * - * @throws ReflectionException - * @return ResponseInterface - * @noinspection PhpUnused - */ + 'dateformat' => $this->request->getPost('dateformat'), + 'timeformat' => $this->request->getPost('timeformat'), + 'thousands_separator' => $this->request->getPost('thousands_separator') != null, + 'number_locale' => $this->request->getPost('number_locale'), + 'currency_decimals' => $this->request->getPost('currency_decimals', FILTER_SANITIZE_NUMBER_INT), + 'tax_decimals' => $this->request->getPost('tax_decimals', FILTER_SANITIZE_NUMBER_INT), + 'quantity_decimals' => $this->request->getPost('quantity_decimals', FILTER_SANITIZE_NUMBER_INT), + 'country_codes' => htmlspecialchars($this->request->getPost('country_codes')), + 'payment_options_order' => $this->request->getPost('payment_options_order'), + 'date_or_time_format' => $this->request->getPost('date_or_time_format') != null, + 'cash_decimals' => $this->request->getPost('cash_decimals', FILTER_SANITIZE_NUMBER_INT), + 'cash_rounding_code' => $this->request->getPost('cash_rounding_code'), + 'financial_year' => $this->request->getPost('financial_year', FILTER_SANITIZE_NUMBER_INT) + ]; + + $success = $this->appconfig->batch_save($batch_save_data); + + return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]); + } + + /** + * Saves email configuration. Used in app/Views/configs/email_config.php + * + * @throws ReflectionException + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function postSaveEmail(): ResponseInterface + { + $password = ''; + + if (check_encryption() && !empty($this->request->getPost('smtp_pass'))) { + $password = $this->encrypter->encrypt($this->request->getPost('smtp_pass')); + } + + $protocol = $this->request->getPost('protocol'); + $mailpath = $this->request->getPost('mailpath'); + + // Validate mailpath: required for sendmail, optional for others but must be safe if provided + $isMailpathRequired = ($protocol === 'sendmail'); + $isMailpathProvided = !empty($mailpath); + $isMailpathValid = $isMailpathProvided && preg_match('/^[a-zA-Z0-9_\-\/.]+$/', $mailpath); + + if (($isMailpathRequired && !$isMailpathProvided) || ($isMailpathProvided && !$isMailpathValid)) { + return $this->response->setJSON([ + 'success' => false, + 'message' => lang('Config.mailpath_invalid') + ]); + } + + $batch_save_data = [ + 'protocol' => $protocol, + 'mailpath' => $mailpath, + 'smtp_host' => $this->request->getPost('smtp_host'), + 'smtp_user' => $this->request->getPost('smtp_user'), + 'smtp_pass' => $password, + 'smtp_port' => $this->request->getPost('smtp_port', FILTER_SANITIZE_NUMBER_INT), + 'smtp_timeout' => $this->request->getPost('smtp_timeout', FILTER_SANITIZE_NUMBER_INT), + 'smtp_crypto' => $this->request->getPost('smtp_crypto') + ]; + + $success = $this->appconfig->batch_save($batch_save_data); + + return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]); + } + + /** + * Saves SMS message configuration. Used in app/Views/configs/message_config.php. + * + * @throws ReflectionException + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function postSaveMessage(): ResponseInterface + { + $password = ''; + + if (check_encryption() && !empty($this->request->getPost('msg_pwd'))) { + $password = $this->encrypter->encrypt($this->request->getPost('msg_pwd')); + } + + $batch_save_data = [ + 'msg_msg' => $this->request->getPost('msg_msg'), + 'msg_uid' => $this->request->getPost('msg_uid'), + 'msg_pwd' => $password, + 'msg_src' => $this->request->getPost('msg_src') + ]; + + $success = $this->appconfig->batch_save($batch_save_data); + + return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]); + } + + /** + * This function fetches all the available lists from Mailchimp for the given API key + */ + private function _mailchimp(string $api_key = ''): array // TODO: Hungarian notation + { + $mailchimp_lib = new Mailchimp_lib(['api_key' => $api_key]); + + $result = []; + + $lists = $mailchimp_lib->getLists(); + if ($lists !== false) { + if (is_array($lists) && !empty($lists['lists']) && is_array($lists['lists'])) { + foreach ($lists['lists'] as $list) { + $result[$list['id']] = $list['name'] . ' [' . $list['stats']['member_count'] . ']'; + } + } + } + + return $result; + } + + /** + * Gets Mailchimp lists when a valid API key is inserted. Used in app/Views/configs/integrations_config.php + * + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function postCheckMailchimpApiKey(): ResponseInterface + { + $lists = $this->_mailchimp($this->request->getPost('mailchimp_api_key')); + $success = count($lists) > 0; + + return $this->response->setJSON([ + 'success' => $success, + 'message' => lang('Config.mailchimp_key_' . ($success ? '' : 'un') . 'successfully'), + 'mailchimp_lists' => $lists + ]); + } + + /** + * Saves Mailchimp configuration. Used in app/Views/configs/integrations_config.php + * + * @throws ReflectionException + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function postSaveMailchimp(): ResponseInterface + { + $api_key = ''; + $list_id = ''; + + if (check_encryption()) { + $api_key_unencrypted = $this->request->getPost('mailchimp_api_key'); + if (!empty($api_key_unencrypted)) { + $api_key = $this->encrypter->encrypt($api_key_unencrypted); + } + + $list_id_unencrypted = $this->request->getPost('mailchimp_list_id'); + if (!empty($list_id_unencrypted)) { + $list_id = $this->encrypter->encrypt($list_id_unencrypted); + } + } + + $batch_save_data = ['mailchimp_api_key' => $api_key, 'mailchimp_list_id' => $list_id]; + + $success = $this->appconfig->batch_save($batch_save_data); + + return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]); + } + + /** + * Gets all stock locations. Used in app/Views/configs/stock_config.php + * + * @return string + * @noinspection PhpUnused + */ + public function getStockLocations(): string + { + $stock_locations = $this->stock_location->get_all()->getResultArray(); + + return view('partial/stock_locations', ['stock_locations' => $stock_locations]); + } + + /** + * @return string + */ + public function getDinnerTables(): string + { + $dinner_tables = $this->dinner_table->get_all()->getResultArray(); + + return view('partial/dinner_tables', ['dinner_tables' => $dinner_tables]); + } + + + /** + * Gets all tax categories. + * + * @return string + */ + public function ajax_tax_categories(): string // TODO: Is this function called anywhere in the code? + { + $tax_categories = $this->tax->get_all_tax_categories()->getResultArray(); + + return view('partial/tax_categories', ['tax_categories' => $tax_categories]); + } + + /** + * Gets all customer rewards. Used in app/Views/configs/reward_config.php + * + * @return string + * @noinspection PhpUnused + */ + public function getCustomerRewards(): string + { + $customer_rewards = $this->customer_rewards->get_all()->getResultArray(); + + return view('partial/customer_rewards', ['customer_rewards' => $customer_rewards]); + } + + /** + * @return void + */ + private function _clear_session_state(): void // TODO: Hungarian notation + { + $this->sale_lib->clear_sale_location(); + $this->sale_lib->clear_table(); + $this->sale_lib->clear_all(); + $this->receiving_lib = new Receiving_lib(); + $this->receiving_lib->clear_stock_source(); + $this->receiving_lib->clear_stock_destination(); + $this->receiving_lib->clear_all(); + } + + /** + * Saves stock locations. Used in app/Views/configs/stock_config.php + * + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function postSaveLocations(): ResponseInterface + { + $this->db->transStart(); + + $not_to_delete = []; + foreach ($this->request->getPost() as $key => $value) { + if (str_contains($key, 'stock_location')) { + // Save or update + foreach ($value as $location_id => $location_name) { + $location_data = ['location_name' => $location_name]; + if ($this->stock_location->save_value($location_data, $location_id)) { + $location_id = $this->stock_location->get_location_id($location_name); + $not_to_delete[] = $location_id; + $this->_clear_session_state(); + } + } + } + } + + // All locations not available in post will be deleted now + $deleted_locations = $this->stock_location->get_all()->getResultArray(); + + foreach ($deleted_locations as $location => $location_data) { + if (!in_array($location_data['location_id'], $not_to_delete)) { + $this->stock_location->delete($location_data['location_id']); + } + } + + $this->db->transComplete(); + + $success = $this->db->transStatus(); + + return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]); + } + + /** + * Saves all dinner tables. Used in app/Views/configs/table_config.php + * + * @throws ReflectionException + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function postSaveTables(): ResponseInterface + { + $this->db->transStart(); + + $dinner_table_enable = $this->request->getPost('dinner_table_enable') != null; + + $this->appconfig->save(['dinner_table_enable' => $dinner_table_enable]); + + if ($dinner_table_enable) { + $not_to_delete = []; + foreach ($this->request->getPost() as $key => $value) { // TODO: Not sure if this is the best way to filter the array + if (strstr($key, 'dinner_table') && $key != 'dinner_table_enable') { + $dinner_table_id = preg_replace("/.*?_(\d+)$/", "$1", $key); + $not_to_delete[] = $dinner_table_id; + + // Save or update + $table_data = ['name' => $value]; + if ($this->dinner_table->save_value($table_data, $dinner_table_id)) { + $this->_clear_session_state(); // TODO: Remove hungarian notation. + } + } + } + + // All tables not available in post will be deleted now + $deleted_tables = $this->dinner_table->get_all()->getResultArray(); + + foreach ($deleted_tables as $dinner_tables => $table) { + if (!in_array($table['dinner_table_id'], $not_to_delete)) { + $this->dinner_table->delete($table['dinner_table_id']); + } + } + } + + $this->db->transComplete(); + + $success = $this->db->transStatus(); + + return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]); + } + + /** + * Saves tax configuration. Used in app/Views/configs/tax_config.php + * + * @throws ReflectionException + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function postSaveTax(): ResponseInterface + { + $default_tax_1_rate = $this->request->getPost('default_tax_1_rate'); + $default_tax_2_rate = $this->request->getPost('default_tax_2_rate'); + + $batch_save_data = [ + 'default_tax_1_rate' => parse_tax(filter_var($default_tax_1_rate, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION)), + 'default_tax_1_name' => $this->request->getPost('default_tax_1_name'), + 'default_tax_2_rate' => parse_tax(filter_var($default_tax_2_rate, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION)), + 'default_tax_2_name' => $this->request->getPost('default_tax_2_name'), + 'tax_included' => $this->request->getPost('tax_included') != null, + 'use_destination_based_tax' => $this->request->getPost('use_destination_based_tax') != null, + 'default_tax_code' => $this->request->getPost('default_tax_code'), + 'default_tax_category' => $this->request->getPost('default_tax_category'), + 'default_tax_jurisdiction' => $this->request->getPost('default_tax_jurisdiction'), + 'tax_id' => $this->request->getPost('tax_id', FILTER_SANITIZE_NUMBER_INT) + ]; + + $success = $this->appconfig->batch_save($batch_save_data); + + $message = lang('Config.saved_' . ($success ? '' : 'un') . 'successfully'); + + return $this->response->setJSON(['success' => $success, 'message' => $message]); + } + + /** + * Saves customer rewards configuration. Used in app/Views/configs/reward_config.php + * + * @throws ReflectionException + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function postSaveRewards(): ResponseInterface + { + $this->db->transStart(); + + $customer_reward_enable = $this->request->getPost('customer_reward_enable') != null; + + $this->appconfig->save(['customer_reward_enable' => $customer_reward_enable]); + + if ($customer_reward_enable) { + $not_to_delete = []; + $array_save = []; + foreach ($this->request->getPost() as $key => $value) { + if (strstr($key, 'customer_reward') && $key != 'customer_reward_enable') { + $customer_reward_id = preg_replace("/.*?_(\d+)$/", "$1", $key); + $not_to_delete[] = $customer_reward_id; + $array_save[$customer_reward_id]['package_name'] = $value; + } elseif (str_contains($key, 'reward_points')) { + $customer_reward_id = preg_replace("/.*?_(\d+)$/", "$1", $key); + $array_save[$customer_reward_id]['points_percent'] = $value; + } + } + + if (!empty($array_save)) { + foreach ($array_save as $key => $value) { + // Save or update + $package_data = ['package_name' => $value['package_name'], 'points_percent' => $value['points_percent']]; + $this->customer_rewards->save_value($package_data, $key); // TODO: reflection exception + } + } + + // All packages not available in post will be deleted now + $deleted_packages = $this->customer_rewards->get_all()->getResultArray(); + + foreach ($deleted_packages as $customer_rewards => $reward_category) { + if (!in_array($reward_category['package_id'], $not_to_delete)) { + $this->customer_rewards->delete($reward_category['package_id']); + } + } + } + + $this->db->transComplete(); + + $success = $this->db->transStatus(); + + return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]); + } + + /** + * Saves barcode configuration. Used in app/Views/configs/barcode_config.php + * + * @throws ReflectionException + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function postSaveBarcode(): ResponseInterface + { + $batch_save_data = [ + 'barcode_type' => $this->request->getPost('barcode_type'), + 'barcode_width' => $this->request->getPost('barcode_width', FILTER_SANITIZE_NUMBER_INT), + 'barcode_height' => $this->request->getPost('barcode_height', FILTER_SANITIZE_NUMBER_INT), + 'barcode_font' => $this->request->getPost('barcode_font'), + 'barcode_font_size' => $this->request->getPost('barcode_font_size', FILTER_SANITIZE_NUMBER_INT), + 'barcode_first_row' => $this->request->getPost('barcode_first_row'), + 'barcode_second_row' => $this->request->getPost('barcode_second_row'), + 'barcode_third_row' => $this->request->getPost('barcode_third_row'), + 'barcode_num_in_row' => $this->request->getPost('barcode_num_in_row', FILTER_SANITIZE_NUMBER_INT), + 'barcode_page_width' => $this->request->getPost('barcode_page_width', FILTER_SANITIZE_NUMBER_INT), + 'barcode_page_cellspacing' => $this->request->getPost('barcode_page_cellspacing', FILTER_SANITIZE_NUMBER_INT), + 'barcode_generate_if_empty' => $this->request->getPost('barcode_generate_if_empty') != null, + 'allow_duplicate_barcodes' => $this->request->getPost('allow_duplicate_barcodes') != null, + 'barcode_content' => $this->request->getPost('barcode_content'), + 'barcode_formats' => json_encode($this->request->getPost('barcode_formats')) + ]; + + $success = $this->appconfig->batch_save($batch_save_data); + + return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]); + } + + /** + * Saves receipt configuration. Used in app/Views/configs/receipt_config.php. + * + * @throws ReflectionException + * @return ResponseInterface + * @noinspection PhpUnused + */ public function postSaveReceipt(): ResponseInterface { $batch_save_data = [ - 'receipt_template' => Sale_lib::isValidReceiptTemplate($this->request->getPost('receipt_template')) - ? $this->request->getPost('receipt_template') - : 'receipt_default', + 'receipt_template' => $this->request->getPost('receipt_template'), 'receipt_font_size' => $this->request->getPost('receipt_font_size', FILTER_SANITIZE_NUMBER_INT), - 'print_delay_autoreturn' => $this->request->getPost('print_delay_autoreturn', FILTER_SANITIZE_NUMBER_INT), - 'email_receipt_check_behaviour' => $this->request->getPost('email_receipt_check_behaviour'), - 'print_receipt_check_behaviour' => $this->request->getPost('print_receipt_check_behaviour'), - 'receipt_show_company_name' => $this->request->getPost('receipt_show_company_name') != null, - 'receipt_show_taxes' => $this->request->getPost('receipt_show_taxes') != null, + 'print_delay_autoreturn' => $this->request->getPost('print_delay_autoreturn', FILTER_SANITIZE_NUMBER_INT), + 'email_receipt_check_behaviour' => $this->request->getPost('email_receipt_check_behaviour'), + 'print_receipt_check_behaviour' => $this->request->getPost('print_receipt_check_behaviour'), + 'receipt_show_company_name' => $this->request->getPost('receipt_show_company_name') != null, + 'receipt_show_taxes' => $this->request->getPost('receipt_show_taxes') != null, 'receipt_show_tax_ind' => $this->request->getPost('receipt_show_tax_ind') != null, 'receipt_show_total_discount' => $this->request->getPost('receipt_show_total_discount') != null, 'receipt_show_description' => $this->request->getPost('receipt_show_description') != null, + 'receipt_show_secondary_currency' => $this->request->getPost('receipt_show_secondary_currency') != null, 'receipt_show_serialnumber' => $this->request->getPost('receipt_show_serialnumber') != null, - 'print_silently' => $this->request->getPost('print_silently') != null, - 'print_header' => $this->request->getPost('print_header') != null, - 'print_footer' => $this->request->getPost('print_footer') != null, - 'print_top_margin' => $this->request->getPost('print_top_margin', FILTER_SANITIZE_NUMBER_INT), - 'print_left_margin' => $this->request->getPost('print_left_margin', FILTER_SANITIZE_NUMBER_INT), - 'print_bottom_margin' => $this->request->getPost('print_bottom_margin', FILTER_SANITIZE_NUMBER_INT), - 'print_right_margin' => $this->request->getPost('print_right_margin', FILTER_SANITIZE_NUMBER_INT) - ]; - - $success = $this->appconfig->batch_save($batch_save_data); + 'print_silently' => $this->request->getPost('print_silently') != null, + 'print_header' => $this->request->getPost('print_header') != null, + 'print_footer' => $this->request->getPost('print_footer') != null, + 'print_top_margin' => $this->request->getPost('print_top_margin', FILTER_SANITIZE_NUMBER_INT), + 'print_left_margin' => $this->request->getPost('print_left_margin', FILTER_SANITIZE_NUMBER_INT), + 'print_bottom_margin' => $this->request->getPost('print_bottom_margin', FILTER_SANITIZE_NUMBER_INT), + 'print_right_margin' => $this->request->getPost('print_right_margin', FILTER_SANITIZE_NUMBER_INT) + ]; + + $success = $this->appconfig->batch_save($batch_save_data); return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]); } @@ -964,7 +997,7 @@ class Config extends Secure_Controller $batchSaveData = []; foreach ($currentShortcuts as $name => $shortcut) { - $postedValue = trim((string)$this->request->getPost('key_' . $name)); + $postedValue = trim((string) $this->request->getPost('key_' . $name)); if (!in_array($postedValue, $allowedShortcuts, true)) { $postedValue = $shortcut['value']; @@ -994,77 +1027,80 @@ class Config extends Secure_Controller * * @throws ReflectionException * @return ResponseInterface - * @noinspection PhpUnused - */ - public function postSaveInvoice(): ResponseInterface - { - $batch_save_data = [ - 'invoice_enable' => $this->request->getPost('invoice_enable') != null, - 'sales_invoice_format' => $this->request->getPost('sales_invoice_format'), - 'sales_quote_format' => $this->request->getPost('sales_quote_format'), - 'recv_invoice_format' => $this->request->getPost('recv_invoice_format'), - 'invoice_default_comments' => $this->request->getPost('invoice_default_comments'), - 'invoice_email_message' => $this->request->getPost('invoice_email_message'), - 'line_sequence' => $this->request->getPost('line_sequence'), - 'last_used_invoice_number' => $this->request->getPost('last_used_invoice_number', FILTER_SANITIZE_NUMBER_INT), - 'last_used_quote_number' => $this->request->getPost('last_used_quote_number', FILTER_SANITIZE_NUMBER_INT), - 'quote_default_comments' => $this->request->getPost('quote_default_comments'), - 'work_order_enable' => $this->request->getPost('work_order_enable') != null, - 'work_order_format' => $this->request->getPost('work_order_format'), - 'last_used_work_order_number' => $this->request->getPost('last_used_work_order_number', FILTER_SANITIZE_NUMBER_INT), - 'invoice_type' => Sale_lib::isValidInvoiceType($this->request->getPost('invoice_type')) - ? $this->request->getPost('invoice_type') - : 'invoice' - ]; - - $success = $this->appconfig->batch_save($batch_save_data); - - // Update the register mode with the latest change so that if the user - // switches immediately back to the register the mode reflects the change - if ($success) { - if ($this->config['invoice_enable']) { - $this->sale_lib->set_mode($this->config['default_register_mode']); - } else { - $this->sale_lib->set_mode('sale'); - } - } - - return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]); - } - - /** - * Removes the company logo from the database. Used in app/Views/configs/info_config.php. - * - * @return ResponseInterface - * @throws ReflectionException - * @noinspection PhpUnused - */ - public function postRemoveLogo(): ResponseInterface - { - $success = $this->appconfig->save(['company_logo' => '']); - - return $this->response->setJSON(['success' => $success]); - } - - /** - * Validates suggestions column configuration to prevent SQL injection. - * - * @param mixed $column The column value from POST - * @param string $fieldType Either 'first' or 'other' to determine default fallback - * @return string Validated column name - */ - private function validateSuggestionsColumn(mixed $column, string $fieldType): string - { - if (!is_string($column)) { - return $fieldType === 'first' ? 'name' : ''; - } - - $allowed = $fieldType === 'first' - ? Item::ALLOWED_SUGGESTIONS_COLUMNS - : Item::ALLOWED_SUGGESTIONS_COLUMNS_WITH_EMPTY; - - $fallback = $fieldType === 'first' ? 'name' : ''; - - return in_array($column, $allowed, true) ? $column : $fallback; - } -} + * @noinspection PhpUnused + */ + public function postSaveInvoice(): ResponseInterface + { + $batch_save_data = [ + 'invoice_enable' => $this->request->getPost('invoice_enable') != null, + 'sales_invoice_format' => $this->request->getPost('sales_invoice_format'), + 'sales_quote_format' => $this->request->getPost('sales_quote_format'), + 'recv_invoice_format' => $this->request->getPost('recv_invoice_format'), + 'invoice_default_comments' => $this->request->getPost('invoice_default_comments'), + 'invoice_email_message' => $this->request->getPost('invoice_email_message'), + 'line_sequence' => $this->request->getPost('line_sequence'), + 'last_used_invoice_number' => $this->request->getPost('last_used_invoice_number', FILTER_SANITIZE_NUMBER_INT), + 'last_used_quote_number' => $this->request->getPost('last_used_quote_number', FILTER_SANITIZE_NUMBER_INT), + 'quote_default_comments' => $this->request->getPost('quote_default_comments'), + 'work_order_enable' => $this->request->getPost('work_order_enable') != null, + 'work_order_format' => $this->request->getPost('work_order_format'), + 'last_used_work_order_number' => $this->request->getPost('last_used_work_order_number', FILTER_SANITIZE_NUMBER_INT), + 'invoice_type' => Sale_lib::isValidInvoiceType($this->request->getPost('invoice_type')) + ? $this->request->getPost('invoice_type') + : 'invoice' + ]; + + $success = $this->appconfig->batch_save($batch_save_data); + + // Update the register mode with the latest change so that if the user + // switches immediately back to the register the mode reflects the change + if ($success) { + if ($this->config['invoice_enable']) { + $this->sale_lib->set_mode($this->config['default_register_mode']); + } else { + $this->sale_lib->set_mode('sale'); + } + } + + return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]); + } + + /** + * Removes the company logo from the database. Used in app/Views/configs/info_config.php. + * + * @return ResponseInterface + * @throws ReflectionException + * @noinspection PhpUnused + */ + public function postRemoveLogo(): ResponseInterface + { + $success = $this->appconfig->save(['company_logo' => '']); + + return $this->response->setJSON(['success' => $success]); + } + + /** + * Validates suggestions column configuration to prevent SQL injection. + * + * @param mixed $column The column value from POST + * @param string $fieldType Either 'first' or 'other' to determine default fallback + * @return string Validated column name + */ + private function validateSuggestionsColumn(mixed $column, string $fieldType): string + { + if (!is_string($column)) { + return $fieldType === 'first' ? 'name' : ''; + } + + $allowed = $fieldType === 'first' + ? Item::ALLOWED_SUGGESTIONS_COLUMNS + : Item::ALLOWED_SUGGESTIONS_COLUMNS_WITH_EMPTY; + + $fallback = $fieldType === 'first' ? 'name' : ''; + + return in_array($column, $allowed, true) ? $column : $fallback; + } +} + + + diff --git a/app/Controllers/Sales.php b/app/Controllers/Sales.php index 1d60d44bb..73bc36e02 100644 --- a/app/Controllers/Sales.php +++ b/app/Controllers/Sales.php @@ -1,71 +1,101 @@ -session = session(); - $this->barcode_lib = new Barcode_lib(); - $this->email_lib = new Email_lib(); - $this->sale_lib = new Sale_lib(); - $this->tax_lib = new Tax_lib(); - $this->token_lib = new Token_lib(); - $this->config = config(OSPOS::class)->settings; - - $this->customer = model(Customer::class); - $this->sale = model(Sale::class); - $this->item = model(Item::class); - $this->item_kit = model(Item_kit::class); - $this->stock_location = model(Stock_location::class); +session = session(); + $this->barcode_lib = new Barcode_lib(); + $this->email_lib = new Email_lib(); + $this->sale_lib = new Sale_lib(); + $this->tax_lib = new Tax_lib(); + $this->token_lib = new Token_lib(); + $this->config = config(OSPOS::class)->settings; + + $this->customer = model(Customer::class); + $this->sale = model(Sale::class); + $this->item = model(Item::class); + $this->item_kit = model(Item_kit::class); + $this->stock_location = model(Stock_location::class); $this->customer_rewards = model(Customer_rewards::class); $this->dinner_table = model(Dinner_table::class); $this->employee = model(Employee::class); } + /** + * Adds the shared secondary currency context to a view data array. + * + * @param array $data + * @return void + */ + private function _append_secondary_currency(array &$data): void + { + $secondaryCurrency = secondary_currency_context($this->config); + $data['secondaryCurrency'] = $secondaryCurrency; + + if (!$secondaryCurrency['show']) { + return; + } + + $displayFields = [ + 'total' => 'secondaryTotalDisplay', + 'amount_due' => 'secondaryAmountDueDisplay', + 'cash_amount_due' => 'secondaryCashAmountDueDisplay', + 'non_cash_total' => 'secondaryNonCashTotalDisplay', + 'non_cash_amount_due' => 'secondaryNonCashAmountDueDisplay' + ]; + + foreach ($displayFields as $sourceField => $targetField) { + if (array_key_exists($sourceField, $data)) { + $data[$targetField] = to_secondary_currency((float) $data[$sourceField], $secondaryCurrency); + } + } + } + public function getIndex(): ResponseInterface|string { $this->session->set('allow_temp_items', 1); @@ -73,1047 +103,1165 @@ class Sales extends Secure_Controller } /** - * Load the sale edit modal. Used in app/Views/sales/register.php. + * Load the customer display popup. * * @return ResponseInterface|string * @noinspection PhpUnused */ - public function getManage(): ResponseInterface|string + public function getCustomerDisplay(): ResponseInterface|string { - $personId = $this->session->get('person_id'); - - if (!$this->employee->has_grant('reports_sales', $personId)) { - return redirect()->to('no_access/sales/reports_sales'); - } else { - $data['table_headers'] = get_sales_manage_table_headers(); - - $data['filters'] = [ - 'only_cash' => lang('Sales.cash_filter'), - 'only_due' => lang('Sales.due_filter'), - 'only_check' => lang('Sales.check_filter'), - 'only_creditcard' => lang('Sales.credit_filter'), - 'only_debit' => lang('Sales.debit'), - 'only_bank_transfer'=> lang('Sales.bank_transfer'), - 'only_wallet' => lang('Sales.wallet'), - 'only_invoices' => lang('Sales.invoice_filter'), - 'selected_customer' => lang('Sales.selected_customer') - ]; - - if ($this->sale_lib->get_customer() != -1) { - $selectedFilters = ['selected_customer']; - $data['customer_selected'] = true; - } else { - $data['customer_selected'] = false; - $selectedFilters = []; - } - - // Restore filters from URL query string - $filters = restoreTableFilters($this->request); - if (!empty($filters['selected_filters'])) { - $selectedFilters = array_merge($selectedFilters, $filters['selected_filters']); - } - if (isset($filters['start_date'])) { - $data['start_date'] = $filters['start_date']; - } - if (isset($filters['end_date'])) { - $data['end_date'] = $filters['end_date']; - } - $data['selected_filters'] = $selectedFilters; - - return view('sales/manage', $data); + if (($this->config['customer_display_enabled'] ?? false) != 1) { + return $this->response->setStatusCode(404)->setBody(''); } - } - /** - * @param int $row_id - * @return ResponseInterface - */ - public function getRow(int $row_id): ResponseInterface - { - $sale_info = $this->sale->get_info($row_id)->getRow(); - $data_row = get_sale_data_row($sale_info); + if ($this->session->get('sale_id') == '') { + $this->session->set('sale_id', NEW_ENTRY); + } - return $this->response->setJSON($data_row); - } + $secondaryCurrency = secondary_currency_context($this->config); + $secondaryCurrencyEnabled = (($this->config['secondary_currency_enabled'] ?? false) == 1); + $cashRounding = $this->sale_lib->reset_cash_rounding(); + $showCustomerDisplay = $secondaryCurrencyEnabled && !empty($secondaryCurrency['rate']) && (float) $secondaryCurrency['rate'] > 0; + $companyLines = preg_split("/\r\n|\r|\n/", (string) ($this->config['company'] ?? '')) ?: []; + $companyName = array_shift($companyLines) ?? ''; + $companyDetails = trim(implode("\n", $companyLines)); + $secondaryCurrencySymbol = trim((string) ($this->config['secondary_currency_symbol'] ?? '')); + $secondaryCurrencyCode = trim((string) ($this->config['secondary_currency_code'] ?? '')); + $originalCurrencySymbol = trim((string) ($this->config['currency_symbol'] ?? '')); + $customerDisplayCurrencyLabel = $secondaryCurrencyCode !== '' ? $secondaryCurrencyCode : ($secondaryCurrencySymbol !== '' ? $secondaryCurrencySymbol : 'LBP'); + $originalCurrencyLabel = $originalCurrencySymbol !== '' ? $originalCurrencySymbol : '$'; + $cartHasCustomerDisplay = $showCustomerDisplay; + $cartColspan = $cartHasCustomerDisplay ? 6 : 5; + $cartItemWidth = $cartHasCustomerDisplay ? 32 : 44; + $cartPriceWidth = $cartHasCustomerDisplay ? 18 : 0; + $cartOriginalWidth = $cartHasCustomerDisplay ? 18 : 26; + $cartQuantityWidth = $cartHasCustomerDisplay ? 12 : 10; + $cartDiscountWidth = $cartHasCustomerDisplay ? 10 : 9; + $cartTotalWidth = $cartHasCustomerDisplay ? 10 : 11; - /** - * @return void - */ - public function getSearch(): ResponseInterface - { - $search = $this->request->getGet('search', FILTER_SANITIZE_FULL_SPECIAL_CHARS); - $limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT); - $offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT); - $sort = $this->sanitizeSortColumn(sales_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'sale_id'); - $order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS); - - $filters = [ - 'sale_type' => 'all', - 'location_id' => 'all', - 'start_date' => $this->request->getGet('start_date', FILTER_SANITIZE_FULL_SPECIAL_CHARS), - 'end_date' => $this->request->getGet('end_date', FILTER_SANITIZE_FULL_SPECIAL_CHARS), - 'only_cash' => false, - 'only_due' => false, - 'only_check' => false, - 'selected_customer' => false, - 'only_creditcard' => false, - 'only_debit' => false, - 'only_bank_transfer'=> false, - 'only_wallet' => false, - 'only_invoices' => $this->config['invoice_enable'] && $this->request->getGet('only_invoices', FILTER_SANITIZE_NUMBER_INT), - 'is_valid_receipt' => $this->sale->is_valid_receipt($search) + $data = [ + 'cash_rounding' => $cashRounding, + 'cart' => $this->sale_lib->get_cart() + ]; + $customer_info = $this->_load_customer_data($this->sale_lib->get_customer(), $data, true); + $data += [ + 'customer_name' => $data['customer'] ?? lang('Sales.walk_in_customer'), + 'customer_reward_points' => (int) ($data['customer_rewards']['points'] ?? 0), + 'customer_reward_package' => $data['customer_rewards']['package_name'] ?? '', + 'giftcard_remainder' => $this->sale_lib->get_giftcard_remainder(), + 'rewards_remainder' => $this->sale_lib->get_rewards_remainder(), + 'customerName' => $data['customer'] ?? lang('Sales.walk_in_customer'), + 'customerRewardPoints' => (int) ($data['customer_rewards']['points'] ?? 0), + 'giftcardRemainder' => $this->sale_lib->get_giftcard_remainder() ]; - // Check if any filter is set in the multiselect dropdown - $request_filters = array_fill_keys($this->request->getGet('filters', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?? [], true); - $filters = array_merge($filters, $request_filters); - - $sales = $this->sale->search($search, $filters, $limit, $offset, $sort, $order); - $total_rows = $this->sale->get_found_rows($search, $filters); - $payments = $this->sale->get_payments_summary($search, $filters); - $payment_summary = get_sales_manage_payments_summary($payments); - - $data_rows = []; - foreach ($sales->getResult() as $sale) { - $data_rows[] = get_sale_data_row($sale); - } - - if ($total_rows > 0) { - $data_rows[] = get_sale_data_last_row($sales); - } - - return $this->response->setJSON(['total' => $total_rows, 'rows' => $data_rows, 'payment_summary' => $payment_summary]); - } - - /** - * Gets search suggestions for an item or item kit. Used in app/Views/sales/register.php. - * - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function getItemSearch(): ResponseInterface - { - $suggestions = []; - $receipt = $search = $this->request->getGet('term') != '' - ? $this->request->getGet('term') - : null; - - if ($this->sale_lib->get_mode() == 'return' && $this->sale->is_valid_receipt($receipt)) { - // If a valid receipt or invoice was found the search term will be replaced with a receipt number (POS #) - $suggestions[] = $receipt; - } - $suggestions = array_merge($suggestions, $this->item->get_search_suggestions($search, ['search_custom' => false, 'is_deleted' => false], true)); - $suggestions = array_merge($suggestions, $this->item_kit->get_search_suggestions($search)); - - return $this->response->setJSON($suggestions); - } - - /** - * @return ResponseInterface - */ - public function suggest_search(): ResponseInterface - { - $search = $this->request->getPost('term') != '' - ? $this->request->getPost('term') - : null; - - $suggestions = $this->sale->get_search_suggestions($search); - - return $this->response->setJSON($suggestions); - } - - /** - * Set a given customer. Used in app/Views/sales/register.php. - * - * @return ResponseInterface|string - * @noinspection PhpUnused - */ - public function postSelectCustomer(): ResponseInterface|string - { - $customer_id = (int)$this->request->getPost('customer', FILTER_SANITIZE_NUMBER_INT); - if ($this->customer->exists($customer_id)) { - $this->sale_lib->set_customer($customer_id); - $discount = $this->customer->get_info($customer_id)->discount; - $discount_type = $this->customer->get_info($customer_id)->discount_type; - - // Apply customer default discount to items that have 0 discount - if ($discount != '') { - $this->sale_lib->apply_customer_discount($discount, $discount_type); - } - } - - return $this->_reload(); - } - - /** - * Changes the sale mode in the register to carry out different types of sales - * - * @return ResponseInterface|string - * @noinspection PhpUnused - */ - public function postChangeMode(): ResponseInterface|string - { - $mode = $this->request->getPost('mode', FILTER_SANITIZE_FULL_SPECIAL_CHARS); - $this->sale_lib->set_mode($mode); - - if ($mode == 'sale') { - $this->sale_lib->set_sale_type(SALE_TYPE_POS); - } elseif ($mode == 'sale_quote') { - $this->sale_lib->set_sale_type(SALE_TYPE_QUOTE); - } elseif ($mode == 'sale_work_order') { - $this->sale_lib->set_sale_type(SALE_TYPE_WORK_ORDER); - } elseif ($mode == 'sale_invoice') { - $this->sale_lib->set_sale_type(SALE_TYPE_INVOICE); - } else { - $this->sale_lib->set_sale_type(SALE_TYPE_RETURN); - } - - if ($this->config['dinner_table_enable']) { - $occupied_dinner_table = $this->request->getPost('dinner_table', FILTER_SANITIZE_NUMBER_INT); - $released_dinner_table = $this->sale_lib->get_dinner_table(); - $occupied = $this->dinner_table->is_occupied($released_dinner_table); - - if ($occupied && ($occupied_dinner_table != $released_dinner_table)) { - $this->dinner_table->swap_tables($released_dinner_table, $occupied_dinner_table); - } - - $this->sale_lib->set_dinner_table($occupied_dinner_table); - } - - $stock_location = $this->request->getPost('stock_location', FILTER_SANITIZE_NUMBER_INT); - - if (!$stock_location || $stock_location == $this->sale_lib->get_sale_location()) { - // TODO: The code below was removed in 2017 by @steveireland. We either need to reinstate some of it or remove this entire if block but we can't leave an empty if block - // $dinner_table = $this->request->getPost('dinner_table'); - // $this->sale_lib->set_dinner_table($dinner_table); - } elseif ($this->stock_location->is_allowed_location($stock_location, 'sales')) { - $this->sale_lib->set_sale_location($stock_location); - } - - $this->sale_lib->empty_payments(); - - return $this->_reload(); - } - - /** - * @param int $sale_type - * @return ResponseInterface|string - */ - public function change_register_mode(int $sale_type): ResponseInterface|string - { - $mode = match ($sale_type) { - SALE_TYPE_QUOTE => 'sale_quote', - SALE_TYPE_WORK_ORDER => 'sale_work_order', - SALE_TYPE_INVOICE => 'sale_invoice', - SALE_TYPE_RETURN => 'return', - default => 'sale' // SALE_TYPE_POS - }; - - $this->sale_lib->set_mode($mode); - return $this->_reload(); - } - - - /** - * Sets the sales comment. Used in app/Views/sales/register.php - * - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function postSetComment(): ResponseInterface - { - $this->sale_lib->set_comment($this->request->getPost('comment', FILTER_SANITIZE_FULL_SPECIAL_CHARS)); - return $this->response->setJSON(['success' => true]); - } - - /** - * Sets the invoice number. Used in app/Views/sales/register.php - * - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function postSetInvoiceNumber(): ResponseInterface|string - { - $this->sale_lib->set_invoice_number($this->request->getPost('sales_invoice_number', FILTER_SANITIZE_NUMBER_INT)); - return $this->response->setJSON(['success' => true]); - } - - /** - * @return ResponseInterface - */ - public function postSetPaymentType(): ResponseInterface|string // TODO: This function does not appear to be called anywhere in the code. - { - $this->sale_lib->set_payment_type($this->request->getPost('selected_payment_type', FILTER_SANITIZE_FULL_SPECIAL_CHARS)); - return $this->_reload(); // TODO: Hungarian notation. - } - - /** - * Sets PrintAfterSale flag. Used in app/Views/sales/register.php - * - * @return ResponseInterface|string - * @noinspection PhpUnused - */ - public function postSetPrintAfterSale(): ResponseInterface - { - $this->sale_lib->set_print_after_sale($this->request->getPost('sales_print_after_sale') != 'false'); - return $this->response->setJSON(['success' => true]); - } - - /** - * Sets the flag to include prices in the work order. Used in app/Views/sales/register.php - * - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function postSetPriceWorkOrders(): ResponseInterface - { - $price_work_orders = parse_decimals($this->request->getPost('price_work_orders')); - $this->sale_lib->set_price_work_orders($price_work_orders); - return $this->response->setJSON(['success' => true]); - } - - /** - * Sets the flag to email receipt to the customer. Used in app/Views/sales/register.php - * - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function postSetEmailReceipt(): ResponseInterface - { - $this->sale_lib->set_email_receipt($this->request->getPost('email_receipt', FILTER_SANITIZE_FULL_SPECIAL_CHARS)); - return $this->response->setJSON(['success' => true]); - } - - /** - * Add a payment to the sale. Used in app/Views/sales/register.php - * - * @return ResponseInterface|string - * @noinspection PhpUnused - */ - public function postAddPayment(): ResponseInterface|string - { - $data = []; - $giftcard = model(Giftcard::class); - $payment_type = $this->request->getPost('payment_type', FILTER_SANITIZE_FULL_SPECIAL_CHARS); - - if ($payment_type !== lang('Sales.giftcard')) { - $rules = ['amount_tendered' => 'trim|required|decimal_locale',]; - $messages = ['amount_tendered' => lang('Sales.must_enter_numeric')]; - } else { - $rules = ['amount_tendered' => 'trim|required',]; - $messages = ['amount_tendered' => lang('Sales.must_enter_numeric_giftcard')]; - } - - if (!$this->validate($rules, $messages)) { - $data['error'] = $payment_type === lang('Sales.giftcard') - ? lang('Sales.must_enter_numeric_giftcard') - : lang('Sales.must_enter_numeric'); - } else { - if ($payment_type === lang('Sales.giftcard')) { - // In the case of giftcard payment the register input amount_tendered becomes the giftcard number - $amount_tendered = parse_decimals($this->request->getPost('amount_tendered')); - $giftcard_num = $amount_tendered; - - $payments = $this->sale_lib->get_payments(); - $payment_type = $payment_type . ':' . $giftcard_num; - $current_payments_with_giftcard = isset($payments[$payment_type]) ? $payments[$payment_type]['payment_amount'] : 0; - $cur_giftcard_value = $giftcard->get_giftcard_value($giftcard_num); - $cur_giftcard_customer = $giftcard->get_giftcard_customer($giftcard_num); - $customer_id = $this->sale_lib->get_customer(); - - if (isset($cur_giftcard_customer) && $cur_giftcard_customer != $customer_id && $cur_giftcard_customer != null) { - $data['error'] = lang('Giftcards.cannot_use', [$giftcard_num]); - } elseif (($cur_giftcard_value - $current_payments_with_giftcard) <= 0 && $this->sale_lib->get_mode() === 'sale') { - $data['error'] = lang('Giftcards.remaining_balance', [$giftcard_num, $cur_giftcard_value]); - } else { - $new_giftcard_value = $giftcard->get_giftcard_value($giftcard_num) - $this->sale_lib->get_amount_due(); - $new_giftcard_value = max($new_giftcard_value, 0); - $this->sale_lib->set_giftcard_remainder($new_giftcard_value); - $new_giftcard_value = to_currency($new_giftcard_value); - $data['warning'] = lang('Giftcards.remaining_balance', [$giftcard_num, $new_giftcard_value]); - $amount_tendered = min($this->sale_lib->get_amount_due(), $giftcard->get_giftcard_value($giftcard_num)); - - $this->sale_lib->add_payment($payment_type, $amount_tendered); - } - } elseif ($payment_type === lang('Sales.rewards')) { - $customer_id = $this->sale_lib->get_customer(); - $package_id = $this->customer->get_info($customer_id)->package_id; - if (!empty($package_id)) { - $points = $this->customer->get_info($customer_id)->points; - $points = ($points == null ? 0 : $points); - - $payments = $this->sale_lib->get_payments(); - $current_payments_with_rewards = isset($payments[$payment_type]) ? $payments[$payment_type]['payment_amount'] : 0; - $cur_rewards_value = $points; - - if (($cur_rewards_value - $current_payments_with_rewards) <= 0) { - $data['error'] = lang('Sales.rewards_remaining_balance') . to_currency($cur_rewards_value); - } else { - $new_reward_value = $points - $this->sale_lib->get_amount_due(); - $new_reward_value = max($new_reward_value, 0); - $this->sale_lib->set_rewards_remainder($new_reward_value); - $new_reward_value = str_replace('$', '\$', to_currency($new_reward_value)); - $data['warning'] = lang('Sales.rewards_remaining_balance') . $new_reward_value; - $amount_tendered = min($this->sale_lib->get_amount_due(), $points); - - $this->sale_lib->add_payment($payment_type, $amount_tendered); - } - } - } elseif ($payment_type === lang('Sales.cash')) { - $amount_due = $this->sale_lib->get_total(); - $sales_total = $this->sale_lib->get_total(false); - $amount_tendered = parse_decimals($this->request->getPost('amount_tendered')); - $this->sale_lib->add_payment($payment_type, $amount_tendered); - $cash_adjustment_amount = $amount_due - $sales_total; - if ($cash_adjustment_amount <> 0) { - $this->session->set('cash_mode', CASH_MODE_TRUE); - $this->sale_lib->add_payment(lang('Sales.cash_adjustment'), $cash_adjustment_amount, CASH_ADJUSTMENT_TRUE); - } - } else { - $amount_tendered = parse_decimals($this->request->getPost('amount_tendered')); - $this->sale_lib->add_payment($payment_type, $amount_tendered); - } - } - - return $this->_reload($data); - } - - /** - * Multiple Payments. Used in app/Views/sales/register.php - * - * @param string $payment_id - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function getDeletePayment(string $payment_id): ResponseInterface|string - { - helper('url'); - - $this->sale_lib->delete_payment(base64url_decode($payment_id)); - - return $this->_reload(); - } - - /** - * Add an item to the sale. Used in app/Views/sales/register.php - * - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function postAdd(): ResponseInterface|string - { - $data = []; - - $discount = $this->config['default_sales_discount']; - $discount_type = $this->config['default_sales_discount_type']; - - // Check if any discount is assigned to the selected customer - $customer_id = $this->sale_lib->get_customer(); - if ($customer_id != NEW_ENTRY) { - // Load the customer discount if any - $customer_discount = $this->customer->get_info($customer_id)->discount; - $customer_discount_type = $this->customer->get_info($customer_id)->discount_type; - if ($customer_discount != '') { - $discount = $customer_discount; - $discount_type = $customer_discount_type; - } - } - - $item_id_or_number_or_item_kit_or_receipt = $this->request->getPost('item', FILTER_SANITIZE_FULL_SPECIAL_CHARS); - $this->token_lib->parse_barcode($quantity, $price, $item_id_or_number_or_item_kit_or_receipt); - $mode = $this->sale_lib->get_mode(); - $quantity = ($mode == 'return') ? -$quantity : $quantity; - $item_location = $this->sale_lib->get_sale_location(); - - if ($mode == 'return' && $this->sale->is_valid_receipt($item_id_or_number_or_item_kit_or_receipt)) { - $this->sale_lib->return_entire_sale($item_id_or_number_or_item_kit_or_receipt); - } elseif ($this->item_kit->is_valid_item_kit($item_id_or_number_or_item_kit_or_receipt)) { - // Add kit item to order if one is assigned - $pieces = explode(' ', $item_id_or_number_or_item_kit_or_receipt); - - $item_kit_id = (count($pieces) > 1) ? $pieces[1] : $item_id_or_number_or_item_kit_or_receipt; - $item_kit_info = $this->item_kit->get_info($item_kit_id); - $kit_item_id = $item_kit_info->kit_item_id; - $kit_price_option = $item_kit_info->price_option; - $kit_print_option = $item_kit_info->print_option; // 0-all, 1-priced, 2-kit-only - - if ($discount_type == $item_kit_info->kit_discount_type) { - if ($item_kit_info->kit_discount > $discount) { - $discount = $item_kit_info->kit_discount; - } - } else { - $discount = $item_kit_info->kit_discount; - $discount_type = $item_kit_info->kit_discount_type; - } - - $print_option = PRINT_ALL; // Always include in list of items on invoice // TODO: This variable is never used in the code - - if (!empty($kit_item_id)) { - if (!$this->sale_lib->add_item($kit_item_id, $item_location, $quantity, $discount, $discount_type, PRICE_MODE_KIT, $kit_price_option, $kit_print_option, $price)) { - $data['error'] = lang('Sales.unable_to_add_item'); - } else { - $data['warning'] = $this->sale_lib->out_of_stock($item_kit_id, $item_location); - } - } - - // Add item kit items to order - $stock_warning = null; - if (!$this->sale_lib->add_item_kit($item_id_or_number_or_item_kit_or_receipt, $item_location, $discount, $discount_type, $kit_price_option, $kit_print_option, $stock_warning)) { - $data['error'] = lang('Sales.unable_to_add_item'); - } elseif ($stock_warning != null) { - $data['warning'] = $stock_warning; - } - } else { - if ($item_id_or_number_or_item_kit_or_receipt == '' || !$this->sale_lib->add_item($item_id_or_number_or_item_kit_or_receipt, $item_location, $quantity, $discount, $discount_type, PRICE_MODE_STANDARD, null, null, $price)) { - $data['error'] = lang('Sales.unable_to_add_item'); - } else { - $data['warning'] = $this->sale_lib->out_of_stock($item_id_or_number_or_item_kit_or_receipt, $item_location); - } - } - - return $this->_reload($data); - } - - /** - * Edit an item in the sale. Used in app/Views/sales/register.php - * - * @param string $line - * @return ResponseInterface|string - * @noinspection PhpUnused - */ - public function postEditItem(string $line): ResponseInterface|string - { - $data = []; - - $rules = [ - 'price' => 'trim|required|decimal_locale|nonNegativeDecimal', - 'quantity' => 'trim|required|decimal_locale', - 'discount' => 'trim|permit_empty|decimal_locale|nonNegativeDecimal', + $tax_details = $this->tax_lib->get_taxes($data['cart']); + $data += [ + 'tax_exclusive_subtotal' => $this->sale_lib->get_subtotal(true, true), + 'taxes' => $tax_details[0], + 'discount' => $this->sale_lib->get_discount(), + 'payments' => $this->sale_lib->get_payments() ]; - $messages = [ - 'price' => [ - 'nonNegativeDecimal' => lang('Sales.negative_price_invalid'), - ], - 'discount' => [ - 'nonNegativeDecimal' => lang('Sales.negative_discount_invalid'), - ], - ]; - - if ($this->validate($rules, $messages)) { - $description = $this->request->getPost('description', FILTER_SANITIZE_FULL_SPECIAL_CHARS); - $serialnumber = $this->request->getPost('serialnumber', FILTER_SANITIZE_FULL_SPECIAL_CHARS); - $price = parse_decimals($this->request->getPost('price')); - $quantity = parse_decimals($this->request->getPost('quantity')); - $discount_type = $this->request->getPost('discount_type', FILTER_SANITIZE_FULL_SPECIAL_CHARS); - $discount = $discount_type - ? parse_quantity($this->request->getPost('discount')) - : parse_decimals($this->request->getPost('discount')); - $discount = $discount ?: 0; - - // Return mode legitimately uses negative quantities for refunds - if ($this->sale_lib->get_mode() != 'return' && $quantity < 0) { - $data['error'] = lang('Sales.negative_quantity_invalid'); - return $this->_reload($data); - } - - // Business logic: discount bounds depend on discount_type and item values - if ($discount_type == PERCENT && $discount > 100) { - $data['error'] = lang('Sales.discount_percent_exceeds_100'); - return $this->_reload($data); - } - - if ($discount_type == FIXED && bccomp((string)$discount, bcmul((string)abs($quantity), (string)$price, 2), 2) > 0) { - $data['error'] = lang('Sales.discount_exceeds_item_total'); - return $this->_reload($data); - } - - $item_location = $this->request->getPost('location', FILTER_SANITIZE_NUMBER_INT); - $discounted_total = $this->request->getPost('discounted_total') != '' - ? parse_decimals($this->request->getPost('discounted_total') ?? '') - : null; - - $this->sale_lib->edit_item($line, $description, $serialnumber, $quantity, $discount, $discount_type, $price, $discounted_total); - - $this->sale_lib->empty_payments(); - - $data['warning'] = $this->sale_lib->out_of_stock($this->sale_lib->get_item_id($line), $item_location); - } else { - $errors = $this->validator->getErrors(); - $data['error'] = $errors ? reset($errors) : lang('Sales.error_editing_item'); - } - - return $this->_reload($data); - } - - /** - * Deletes an item specified in the parameter from the shopping cart. Used in app/Views/sales/register.php - * - * @param int $item_id - * @return ResponseInterface - * @throws ReflectionException - * @noinspection PhpUnused - */ - public function getDeleteItem(int $item_id): ResponseInterface|string - { - $this->sale_lib->delete_item($item_id); - - $this->sale_lib->empty_payments(); - - return $this->_reload(); - } - - /** - * Remove the current customer from the sale. Used in app/Views/sales/register.php - * - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function getRemoveCustomer(): ResponseInterface|string - { - $this->sale_lib->clear_giftcard_remainder(); - $this->sale_lib->clear_rewards_remainder(); - $this->sale_lib->delete_payment(lang('Sales.rewards')); - $this->sale_lib->clear_invoice_number(); - $this->sale_lib->clear_quote_number(); - $this->sale_lib->remove_customer(); - - return $this->_reload(); - } - - /** - * Complete and finalize a sale. Used in app/Views/sales/register.php - * - * @return string - * @throws ReflectionException - * @noinspection PhpUnused - */ - public function postComplete(): string // TODO: this function is huge. Probably should be refactored. - { - $sale_id = $this->sale_lib->get_sale_id(); - $data = []; - $data['dinner_table'] = $this->sale_lib->get_dinner_table(); - - $data['cart'] = $this->sale_lib->get_cart(); - - $data['include_hsn'] = (bool)$this->config['include_hsn']; - $__time = time(); - $data['transaction_time'] = to_datetime($__time); - $data['transaction_date'] = to_date($__time); - $data['show_stock_locations'] = $this->stock_location->show_locations('sales'); - $data['comments'] = $this->sale_lib->get_comment(); - $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 . ' ' . mb_substr($employee_info->last_name, 0, 1); - - $data['company_info'] = implode("\n", [$this->config['address'], $this->config['phone']]); - - if ($this->config['account_number']) { - $data['company_info'] .= "\n" . lang('Sales.account_number') . ": " . $this->config['account_number']; - } - - if ($this->config['tax_id'] != '') { - $data['company_info'] .= "\n" . lang('Sales.tax_id') . ": " . $this->config['tax_id']; - } - - $data['invoice_number_enabled'] = $this->sale_lib->is_invoice_mode(); - $data['cur_giftcard_value'] = $this->sale_lib->get_giftcard_remainder(); - $data['cur_rewards_value'] = $this->sale_lib->get_rewards_remainder(); - $data['print_after_sale'] = $this->session->get('sales_print_after_sale'); - $data['price_work_orders'] = $this->sale_lib->is_price_work_orders(); - $data['email_receipt'] = $this->sale_lib->is_email_receipt(); - $customer_id = $this->sale_lib->get_customer(); - $invoice_number = $this->sale_lib->get_invoice_number(); - $data["invoice_number"] = $invoice_number; - $work_order_number = $this->sale_lib->get_work_order_number(); - $data["work_order_number"] = $work_order_number; - $quote_number = $this->sale_lib->get_quote_number(); - $data["quote_number"] = $quote_number; - $customer_info = $this->_load_customer_data($customer_id, $data); - - if ($customer_info != null) { - $data["customer_comments"] = $customer_info->comments; - $data['tax_id'] = $customer_info->tax_id; - } - $tax_details = $this->tax_lib->get_taxes($data['cart']); // TODO: Duplicated code - $data['taxes'] = $tax_details[0]; - $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($tax_details[0]); - $data['subtotal'] = $totals['subtotal']; - $data['total'] = $totals['total']; - $data['payments_total'] = $totals['payment_total']; - $data['payments_cover_total'] = $totals['payments_cover_total']; - $data['cash_rounding'] = $this->session->get('cash_rounding'); - $data['cash_mode'] = $this->session->get('cash_mode'); // TODO: Duplicated code - $data['prediscount_subtotal'] = $totals['prediscount_subtotal']; - $data['cash_total'] = $totals['cash_total']; - $data['non_cash_total'] = $totals['total']; - $data['cash_amount_due'] = $totals['cash_amount_due']; - $data['non_cash_amount_due'] = $totals['amount_due']; + $data += [ + 'item_count' => $totals['item_count'], + 'total_units' => $totals['total_units'], + 'subtotal' => $totals['subtotal'], + 'total' => $totals['total'], + 'payments_total' => $totals['payment_total'], + 'payments_cover_total' => $totals['payments_cover_total'], + 'prediscount_subtotal' => $totals['prediscount_subtotal'], + 'cash_total' => $totals['cash_total'], + 'non_cash_total' => $totals['total'], + 'cash_amount_due' => $totals['cash_amount_due'], + 'non_cash_amount_due' => $totals['amount_due'], + 'cash_mode' => $this->session->get('cash_mode'), + 'selected_payment_type' => $this->sale_lib->get_payment_type(), + 'comment' => $this->sale_lib->get_comment(), + 'email_receipt' => $this->sale_lib->is_email_receipt(), + 'config' => $this->config, + 'mode' => $this->sale_lib->get_mode(), + 'rate' => (float) ($secondaryCurrency['rate'] ?? $this->config['secondary_currency_rate'] ?? 0), + 'secondaryCurrency' => $secondaryCurrency, + 'secondaryCurrencyEnabled' => $secondaryCurrencyEnabled, + 'showCustomerDisplay' => $showCustomerDisplay, + 'companyName' => $companyName, + 'companyDetails' => $companyDetails, + 'secondaryCurrencySymbol' => $secondaryCurrencySymbol, + 'secondaryCurrencyCode' => $secondaryCurrencyCode, + 'originalCurrencySymbol' => $originalCurrencySymbol, + 'customerDisplayCurrencyLabel' => $customerDisplayCurrencyLabel, + 'originalCurrencyLabel' => $originalCurrencyLabel, + 'cartHasCustomerDisplay' => $cartHasCustomerDisplay, + 'cartColspan' => $cartColspan, + 'cartItemWidth' => $cartItemWidth, + 'cartPriceWidth' => $cartPriceWidth, + 'cartOriginalWidth' => $cartOriginalWidth, + 'cartQuantityWidth' => $cartQuantityWidth, + 'cartDiscountWidth' => $cartDiscountWidth, + 'cartTotalWidth' => $cartTotalWidth, + 'items_module_allowed' => $this->employee->has_grant('items', $this->employee->get_logged_in_employee_info()->person_id), + 'change_price' => $this->employee->has_grant('sales_change_price', $this->employee->get_logged_in_employee_info()->person_id) + ]; - // Prevent negative total sales (fraud/theft vector) - returns can have negative totals for legitimate refunds - if ($this->sale_lib->get_mode() != 'return' && bccomp($totals['total'], '0') < 0) { - $data['error'] = lang('Sales.negative_total_invalid'); - return $this->_reload($data); - } - - if ($data['cash_mode']) { // TODO: Convert this to ternary notation - $data['amount_due'] = $totals['cash_amount_due']; - } else { - $data['amount_due'] = $totals['amount_due']; + $invoice_number = $this->sale_lib->get_invoice_number(); + if ($invoice_number == null || $invoice_number == '') { + $invoice_number = $this->token_lib->render($this->config['sales_invoice_format'], [], false); } + $data += [ + 'invoice_number' => $invoice_number, + 'print_after_sale' => $this->sale_lib->is_print_after_sale(), + 'price_work_orders' => $this->sale_lib->is_price_work_orders(), + 'pos_mode' => $data['mode'] == 'sale' || $data['mode'] == 'return', + 'quote_number' => $this->sale_lib->get_quote_number(), + 'work_order_number' => $this->sale_lib->get_work_order_number(), + 'amount_due' => $data['cash_mode'] && ($data['selected_payment_type'] === lang('Sales.cash') || $data['payments_total'] > 0) ? $totals['cash_amount_due'] : $totals['amount_due'] + ]; $data['amount_change'] = $data['amount_due'] * -1; + $data['payment_change_due'] = ((float) $data['amount_due'] < 0) + ? abs((float) $data['amount_due']) + : max(((float) $data['payments_total']) - ((float) $data['amount_due']), 0); + $data['paymentChangeDue'] = $data['payment_change_due']; - if ($data['amount_change'] > 0) { - // Save cash refund to the cash payment transaction if found, if not then add as new Cash transaction - - if (array_key_exists(lang('Sales.cash'), $data['payments'])) { - $data['payments'][lang('Sales.cash')]['cash_refund'] = $data['amount_change']; - } else { - $payment = [ - lang('Sales.cash') => [ - 'payment_type' => lang('Sales.cash'), - 'payment_amount' => 0, - 'cash_refund' => $data['amount_change'] - ] - ]; - - $data['payments'] += $payment; - } - } - - $data['print_price_info'] = true; - - if ($this->sale_lib->is_invoice_mode()) { - $invoice_format = $this->config['sales_invoice_format']; - - // 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 (!empty($invoice_format) && $invoice_number == null) { - // The user can retain the default encoded format or can manually override it. It still passes through the rendering step. - $invoice_number = $this->token_lib->render($invoice_format); - } - - - if ($sale_id == NEW_ENTRY && $this->sale->check_invoice_number_exists($invoice_number)) { - $data['error'] = lang('Sales.invoice_number_duplicate', [$invoice_number]); - return $this->_reload($data); - } else { - $data['invoice_number'] = $invoice_number; - $data['sale_status'] = COMPLETED; - $sale_type = SALE_TYPE_INVOICE; - - $invoice_type = $this->config['invoice_type']; - if (!Sale_lib::isValidInvoiceType($invoice_type)) { - $invoice_type = 'invoice'; - } - $invoice_view = $invoice_type; - - // Save the data to the sales table - $data['sale_id_num'] = $this->sale->save_value($sale_id, $data['sale_status'], $data['cart'], $customer_id, $employee_id, $data['comments'], $invoice_number, $work_order_number, $quote_number, $sale_type, $data['payments'], $data['dinner_table'], $tax_details); + return view('sales/customer_display', $data); + } + + /** + * Load the sale edit modal. Used in app/Views/sales/register.php. + * + * @return ResponseInterface|string + * @noinspection PhpUnused + */ + public function getManage(): ResponseInterface|string + { + $personId = $this->session->get('person_id'); + + if (!$this->employee->has_grant('reports_sales', $personId)) { + return redirect()->to('no_access/sales/reports_sales'); + } else { + $data['table_headers'] = get_sales_manage_table_headers(); + + $data['filters'] = [ + 'only_cash' => lang('Sales.cash_filter'), + 'only_due' => lang('Sales.due_filter'), + 'only_check' => lang('Sales.check_filter'), + 'only_creditcard' => lang('Sales.credit_filter'), + 'only_debit' => lang('Sales.debit'), + 'only_invoices' => lang('Sales.invoice_filter'), + 'selected_customer' => lang('Sales.selected_customer') + ]; + + if ($this->sale_lib->get_customer() != -1) { + $selectedFilters = ['selected_customer']; + $data['customer_selected'] = true; + } else { + $data['customer_selected'] = false; + $selectedFilters = []; + } + + // Restore filters from URL query string + $filters = restoreTableFilters($this->request); + if (!empty($filters['selected_filters'])) { + $selectedFilters = array_merge($selectedFilters, $filters['selected_filters']); + } + if (isset($filters['start_date'])) { + $data['start_date'] = $filters['start_date']; + } + if (isset($filters['end_date'])) { + $data['end_date'] = $filters['end_date']; + } + $data['selected_filters'] = $selectedFilters; + + return view('sales/manage', $data); + } + } + + /** + * @param int $row_id + * @return ResponseInterface + */ + public function getRow(int $row_id): ResponseInterface + { + $sale_info = $this->sale->get_info($row_id)->getRow(); + $data_row = get_sale_data_row($sale_info); + + return $this->response->setJSON($data_row); + } + + /** + * @return void + */ + public function getSearch(): ResponseInterface + { + $search = $this->request->getGet('search', FILTER_SANITIZE_FULL_SPECIAL_CHARS); + $limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT); + $offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT); + $sort = $this->sanitizeSortColumn(sales_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'sale_id'); + $order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS); + + $filters = [ + 'sale_type' => 'all', + 'location_id' => 'all', + 'start_date' => $this->request->getGet('start_date', FILTER_SANITIZE_FULL_SPECIAL_CHARS), + 'end_date' => $this->request->getGet('end_date', FILTER_SANITIZE_FULL_SPECIAL_CHARS), + 'only_cash' => false, + 'only_due' => false, + 'only_check' => false, + 'selected_customer' => false, + 'only_creditcard' => false, + 'only_debit' => false, + 'only_invoices' => $this->config['invoice_enable'] && $this->request->getGet('only_invoices', FILTER_SANITIZE_NUMBER_INT), + 'is_valid_receipt' => $this->sale->is_valid_receipt($search) + ]; + + // Check if any filter is set in the multiselect dropdown + $request_filters = array_fill_keys($this->request->getGet('filters', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?? [], true); + $filters = array_merge($filters, $request_filters); + + $sales = $this->sale->search($search, $filters, $limit, $offset, $sort, $order); + $total_rows = $this->sale->get_found_rows($search, $filters); + $payments = $this->sale->get_payments_summary($search, $filters); + $payment_summary = get_sales_manage_payments_summary($payments); + + $data_rows = []; + foreach ($sales->getResult() as $sale) { + $data_rows[] = get_sale_data_row($sale); + } + + if ($total_rows > 0) { + $data_rows[] = get_sale_data_last_row($sales); + } + + return $this->response->setJSON(['total' => $total_rows, 'rows' => $data_rows, 'payment_summary' => $payment_summary]); + } + + /** + * Gets search suggestions for an item or item kit. Used in app/Views/sales/register.php. + * + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function getItemSearch(): ResponseInterface + { + $suggestions = []; + $receipt = $search = $this->request->getGet('term') != '' + ? $this->request->getGet('term') + : null; + + if ($this->sale_lib->get_mode() == 'return' && $this->sale->is_valid_receipt($receipt)) { + // If a valid receipt or invoice was found the search term will be replaced with a receipt number (POS #) + $suggestions[] = $receipt; + } + $suggestions = array_merge($suggestions, $this->item->get_search_suggestions($search, ['search_custom' => false, 'is_deleted' => false], true)); + $suggestions = array_merge($suggestions, $this->item_kit->get_search_suggestions($search)); + + return $this->response->setJSON($suggestions); + } + + /** + * @return ResponseInterface + */ + public function suggest_search(): ResponseInterface + { + $search = $this->request->getPost('term') != '' + ? $this->request->getPost('term') + : null; + + $suggestions = $this->sale->get_search_suggestions($search); + + return $this->response->setJSON($suggestions); + } + + /** + * Set a given customer. Used in app/Views/sales/register.php. + * + * @return ResponseInterface|string + * @noinspection PhpUnused + */ + public function postSelectCustomer(): ResponseInterface|string + { + $customer_id = (int)$this->request->getPost('customer', FILTER_SANITIZE_NUMBER_INT); + if ($this->customer->exists($customer_id)) { + $this->sale_lib->set_customer($customer_id); + $discount = $this->customer->get_info($customer_id)->discount; + $discount_type = $this->customer->get_info($customer_id)->discount_type; + + // Apply customer default discount to items that have 0 discount + if ($discount != '') { + $this->sale_lib->apply_customer_discount($discount, $discount_type); + } + } + + return $this->_reload(); + } + + /** + * Changes the sale mode in the register to carry out different types of sales + * + * @return ResponseInterface|string + * @noinspection PhpUnused + */ + public function postChangeMode(): ResponseInterface|string + { + $mode = $this->request->getPost('mode', FILTER_SANITIZE_FULL_SPECIAL_CHARS); + $this->sale_lib->set_mode($mode); + + if ($mode == 'sale') { + $this->sale_lib->set_sale_type(SALE_TYPE_POS); + } elseif ($mode == 'sale_quote') { + $this->sale_lib->set_sale_type(SALE_TYPE_QUOTE); + } elseif ($mode == 'sale_work_order') { + $this->sale_lib->set_sale_type(SALE_TYPE_WORK_ORDER); + } elseif ($mode == 'sale_invoice') { + $this->sale_lib->set_sale_type(SALE_TYPE_INVOICE); + } else { + $this->sale_lib->set_sale_type(SALE_TYPE_RETURN); + } + + if ($this->config['dinner_table_enable']) { + $occupied_dinner_table = $this->request->getPost('dinner_table', FILTER_SANITIZE_NUMBER_INT); + $released_dinner_table = $this->sale_lib->get_dinner_table(); + $occupied = $this->dinner_table->is_occupied($released_dinner_table); + + if ($occupied && ($occupied_dinner_table != $released_dinner_table)) { + $this->dinner_table->swap_tables($released_dinner_table, $occupied_dinner_table); + } + + $this->sale_lib->set_dinner_table($occupied_dinner_table); + } + + $stock_location = $this->request->getPost('stock_location', FILTER_SANITIZE_NUMBER_INT); + + if (!$stock_location || $stock_location == $this->sale_lib->get_sale_location()) { + // TODO: The code below was removed in 2017 by @steveireland. We either need to reinstate some of it or remove this entire if block but we can't leave an empty if block + // $dinner_table = $this->request->getPost('dinner_table'); + // $this->sale_lib->set_dinner_table($dinner_table); + } elseif ($this->stock_location->is_allowed_location($stock_location, 'sales')) { + $this->sale_lib->set_sale_location($stock_location); + } + + $this->sale_lib->empty_payments(); + + return $this->_reload(); + } + + /** + * @param int $sale_type + * @return ResponseInterface|string + */ + public function change_register_mode(int $sale_type): ResponseInterface|string + { + $mode = match ($sale_type) { + SALE_TYPE_QUOTE => 'sale_quote', + SALE_TYPE_WORK_ORDER => 'sale_work_order', + SALE_TYPE_INVOICE => 'sale_invoice', + SALE_TYPE_RETURN => 'return', + default => 'sale' // SALE_TYPE_POS + }; + + $this->sale_lib->set_mode($mode); + return $this->_reload(); + } + + + /** + * Sets the sales comment. Used in app/Views/sales/register.php + * + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function postSetComment(): ResponseInterface + { + $this->sale_lib->set_comment($this->request->getPost('comment', FILTER_SANITIZE_FULL_SPECIAL_CHARS)); + return $this->response->setJSON(['success' => true]); + } + + /** + * Sets the invoice number. Used in app/Views/sales/register.php + * + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function postSetInvoiceNumber(): ResponseInterface|string + { + $this->sale_lib->set_invoice_number($this->request->getPost('sales_invoice_number', FILTER_SANITIZE_NUMBER_INT)); + return $this->response->setJSON(['success' => true]); + } + + /** + * @return ResponseInterface + */ + public function postSetPaymentType(): ResponseInterface|string // TODO: This function does not appear to be called anywhere in the code. + { + $this->sale_lib->set_payment_type($this->request->getPost('selected_payment_type', FILTER_SANITIZE_FULL_SPECIAL_CHARS)); + return $this->_reload(); // TODO: Hungarian notation. + } + + /** + * Sets PrintAfterSale flag. Used in app/Views/sales/register.php + * + * @return ResponseInterface|string + * @noinspection PhpUnused + */ + public function postSetPrintAfterSale(): ResponseInterface + { + $this->sale_lib->set_print_after_sale($this->request->getPost('sales_print_after_sale') != 'false'); + return $this->response->setJSON(['success' => true]); + } + + /** + * Sets the flag to include prices in the work order. Used in app/Views/sales/register.php + * + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function postSetPriceWorkOrders(): ResponseInterface + { + $price_work_orders = parse_decimals($this->request->getPost('price_work_orders')); + $this->sale_lib->set_price_work_orders($price_work_orders); + return $this->response->setJSON(['success' => true]); + } + + /** + * Sets the flag to email receipt to the customer. Used in app/Views/sales/register.php + * + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function postSetEmailReceipt(): ResponseInterface + { + $this->sale_lib->set_email_receipt($this->request->getPost('email_receipt', FILTER_SANITIZE_FULL_SPECIAL_CHARS)); + return $this->response->setJSON(['success' => true]); + } + + /** + * Add a payment to the sale. Used in app/Views/sales/register.php + * + * @return ResponseInterface|string + * @noinspection PhpUnused + */ + public function postAddPayment(): ResponseInterface|string + { + $data = []; + $giftcard = model(Giftcard::class); + $payment_type = $this->request->getPost('payment_type', FILTER_SANITIZE_FULL_SPECIAL_CHARS); + + if ($payment_type !== lang('Sales.giftcard')) { + $rules = ['amount_tendered' => 'trim|required|decimal_locale',]; + $messages = ['amount_tendered' => lang('Sales.must_enter_numeric')]; + } else { + $rules = ['amount_tendered' => 'trim|required',]; + $messages = ['amount_tendered' => lang('Sales.must_enter_numeric_giftcard')]; + } + + if (!$this->validate($rules, $messages)) { + $data['error'] = $payment_type === lang('Sales.giftcard') + ? lang('Sales.must_enter_numeric_giftcard') + : lang('Sales.must_enter_numeric'); + } else { + if ($payment_type === lang('Sales.giftcard')) { + // In the case of giftcard payment the register input amount_tendered becomes the giftcard number + $amount_tendered = parse_decimals($this->request->getPost('amount_tendered')); + $giftcard_num = $amount_tendered; + + $payments = $this->sale_lib->get_payments(); + $payment_type = $payment_type . ':' . $giftcard_num; + $current_payments_with_giftcard = isset($payments[$payment_type]) ? $payments[$payment_type]['payment_amount'] : 0; + $cur_giftcard_value = $giftcard->get_giftcard_value($giftcard_num); + $cur_giftcard_customer = $giftcard->get_giftcard_customer($giftcard_num); + $customer_id = $this->sale_lib->get_customer(); + + if (isset($cur_giftcard_customer) && $cur_giftcard_customer != $customer_id && $cur_giftcard_customer != null) { + $data['error'] = lang('Giftcards.cannot_use', [$giftcard_num]); + } elseif (($cur_giftcard_value - $current_payments_with_giftcard) <= 0 && $this->sale_lib->get_mode() === 'sale') { + $data['error'] = lang('Giftcards.remaining_balance', [$giftcard_num, $cur_giftcard_value]); + } else { + $new_giftcard_value = $giftcard->get_giftcard_value($giftcard_num) - $this->sale_lib->get_amount_due(); + $new_giftcard_value = max($new_giftcard_value, 0); + $this->sale_lib->set_giftcard_remainder($new_giftcard_value); + $new_giftcard_value = to_currency($new_giftcard_value); + $data['warning'] = lang('Giftcards.remaining_balance', [$giftcard_num, $new_giftcard_value]); + $amount_tendered = min($this->sale_lib->get_amount_due(), $giftcard->get_giftcard_value($giftcard_num)); + + $this->sale_lib->add_payment($payment_type, $amount_tendered); + } + } elseif ($payment_type === lang('Sales.rewards')) { + $customer_id = $this->sale_lib->get_customer(); + $package_id = $this->customer->get_info($customer_id)->package_id; + if (!empty($package_id)) { + $points = $this->customer->get_info($customer_id)->points; + $points = ($points == null ? 0 : $points); + + $payments = $this->sale_lib->get_payments(); + $current_payments_with_rewards = isset($payments[$payment_type]) ? $payments[$payment_type]['payment_amount'] : 0; + $cur_rewards_value = $points; + + if (($cur_rewards_value - $current_payments_with_rewards) <= 0) { + $data['error'] = lang('Sales.rewards_remaining_balance') . to_currency($cur_rewards_value); + } else { + $new_reward_value = $points - $this->sale_lib->get_amount_due(); + $new_reward_value = max($new_reward_value, 0); + $this->sale_lib->set_rewards_remainder($new_reward_value); + $new_reward_value = str_replace('$', '\$', to_currency($new_reward_value)); + $data['warning'] = lang('Sales.rewards_remaining_balance') . $new_reward_value; + $amount_tendered = min($this->sale_lib->get_amount_due(), $points); + + $this->sale_lib->add_payment($payment_type, $amount_tendered); + } + } + } elseif ($payment_type === lang('Sales.cash')) { + $amount_due = $this->sale_lib->get_total(); + $sales_total = $this->sale_lib->get_total(false); + $amount_tendered = parse_decimals($this->request->getPost('amount_tendered')); + $this->sale_lib->add_payment($payment_type, $amount_tendered); + $cash_adjustment_amount = $amount_due - $sales_total; + if ($cash_adjustment_amount <> 0) { + $this->session->set('cash_mode', CASH_MODE_TRUE); + $this->sale_lib->add_payment(lang('Sales.cash_adjustment'), $cash_adjustment_amount, CASH_ADJUSTMENT_TRUE); + } + } else { + $amount_tendered = parse_decimals($this->request->getPost('amount_tendered')); + $this->sale_lib->add_payment($payment_type, $amount_tendered); + } + } + + return $this->_reload($data); + } + + /** + * Multiple Payments. Used in app/Views/sales/register.php + * + * @param string $payment_id + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function getDeletePayment(string $payment_id): ResponseInterface|string + { + helper('url'); + + $this->sale_lib->delete_payment(base64url_decode($payment_id)); + + return $this->_reload(); + } + + /** + * Add an item to the sale. Used in app/Views/sales/register.php + * + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function postAdd(): ResponseInterface|string + { + $data = []; + + $discount = $this->config['default_sales_discount']; + $discount_type = $this->config['default_sales_discount_type']; + + // Check if any discount is assigned to the selected customer + $customer_id = $this->sale_lib->get_customer(); + if ($customer_id != NEW_ENTRY) { + // Load the customer discount if any + $customer_discount = $this->customer->get_info($customer_id)->discount; + $customer_discount_type = $this->customer->get_info($customer_id)->discount_type; + if ($customer_discount != '') { + $discount = $customer_discount; + $discount_type = $customer_discount_type; + } + } + + $item_id_or_number_or_item_kit_or_receipt = $this->request->getPost('item', FILTER_SANITIZE_FULL_SPECIAL_CHARS); + $this->token_lib->parse_barcode($quantity, $price, $item_id_or_number_or_item_kit_or_receipt); + $mode = $this->sale_lib->get_mode(); + $quantity = ($mode == 'return') ? -$quantity : $quantity; + $item_location = $this->sale_lib->get_sale_location(); + + if ($mode == 'return' && $this->sale->is_valid_receipt($item_id_or_number_or_item_kit_or_receipt)) { + $this->sale_lib->return_entire_sale($item_id_or_number_or_item_kit_or_receipt); + } elseif ($this->item_kit->is_valid_item_kit($item_id_or_number_or_item_kit_or_receipt)) { + // Add kit item to order if one is assigned + $pieces = explode(' ', $item_id_or_number_or_item_kit_or_receipt); + + $item_kit_id = (count($pieces) > 1) ? $pieces[1] : $item_id_or_number_or_item_kit_or_receipt; + $item_kit_info = $this->item_kit->get_info($item_kit_id); + $kit_item_id = $item_kit_info->kit_item_id; + $kit_price_option = $item_kit_info->price_option; + $kit_print_option = $item_kit_info->print_option; // 0-all, 1-priced, 2-kit-only + + if ($discount_type == $item_kit_info->kit_discount_type) { + if ($item_kit_info->kit_discount > $discount) { + $discount = $item_kit_info->kit_discount; + } + } else { + $discount = $item_kit_info->kit_discount; + $discount_type = $item_kit_info->kit_discount_type; + } + + $print_option = PRINT_ALL; // Always include in list of items on invoice // TODO: This variable is never used in the code + + if (!empty($kit_item_id)) { + if (!$this->sale_lib->add_item($kit_item_id, $item_location, $quantity, $discount, $discount_type, PRICE_MODE_KIT, $kit_price_option, $kit_print_option, $price)) { + $data['error'] = lang('Sales.unable_to_add_item'); + } else { + $data['warning'] = $this->sale_lib->out_of_stock($item_kit_id, $item_location); + } + } + + // Add item kit items to order + $stock_warning = null; + if (!$this->sale_lib->add_item_kit($item_id_or_number_or_item_kit_or_receipt, $item_location, $discount, $discount_type, $kit_price_option, $kit_print_option, $stock_warning)) { + $data['error'] = lang('Sales.unable_to_add_item'); + } elseif ($stock_warning != null) { + $data['warning'] = $stock_warning; + } + } else { + if ($item_id_or_number_or_item_kit_or_receipt == '' || !$this->sale_lib->add_item($item_id_or_number_or_item_kit_or_receipt, $item_location, $quantity, $discount, $discount_type, PRICE_MODE_STANDARD, null, null, $price)) { + $data['error'] = lang('Sales.unable_to_add_item'); + } else { + $data['warning'] = $this->sale_lib->out_of_stock($item_id_or_number_or_item_kit_or_receipt, $item_location); + } + } + + return $this->_reload($data); + } + + /** + * Edit an item in the sale. Used in app/Views/sales/register.php + * + * @param string $line + * @return ResponseInterface|string + * @noinspection PhpUnused + */ + public function postEditItem(string $line): ResponseInterface|string + { + $data = []; + + $rules = [ + 'price' => 'trim|required|decimal_locale|nonNegativeDecimal', + 'quantity' => 'trim|required|decimal_locale', + 'discount' => 'trim|permit_empty|decimal_locale|nonNegativeDecimal', + ]; + + $messages = [ + 'price' => [ + 'nonNegativeDecimal' => lang('Sales.negative_price_invalid'), + ], + 'discount' => [ + 'nonNegativeDecimal' => lang('Sales.negative_discount_invalid'), + ], + ]; + + if ($this->validate($rules, $messages)) { + $description = $this->request->getPost('description', FILTER_SANITIZE_FULL_SPECIAL_CHARS); + $serialnumber = $this->request->getPost('serialnumber', FILTER_SANITIZE_FULL_SPECIAL_CHARS); + $price = parse_decimals($this->request->getPost('price')); + $quantity = parse_decimals($this->request->getPost('quantity')); + $discount_type = $this->request->getPost('discount_type', FILTER_SANITIZE_FULL_SPECIAL_CHARS); + $discount = $discount_type + ? parse_quantity($this->request->getPost('discount')) + : parse_decimals($this->request->getPost('discount')); + $discount = $discount ?: 0; + + // Return mode legitimately uses negative quantities for refunds + if ($this->sale_lib->get_mode() != 'return' && $quantity < 0) { + $data['error'] = lang('Sales.negative_quantity_invalid'); + return $this->_reload($data); + } + + // Business logic: discount bounds depend on discount_type and item values + if ($discount_type == PERCENT && $discount > 100) { + $data['error'] = lang('Sales.discount_percent_exceeds_100'); + return $this->_reload($data); + } + + if ($discount_type == FIXED && bccomp((string)$discount, bcmul((string)abs($quantity), (string)$price, 2), 2) > 0) { + $data['error'] = lang('Sales.discount_exceeds_item_total'); + return $this->_reload($data); + } + + $item_location = $this->request->getPost('location', FILTER_SANITIZE_NUMBER_INT); + $discounted_total = $this->request->getPost('discounted_total') != '' + ? parse_decimals($this->request->getPost('discounted_total') ?? '') + : null; + + $this->sale_lib->edit_item($line, $description, $serialnumber, $quantity, $discount, $discount_type, $price, $discounted_total); + + $this->sale_lib->empty_payments(); + + $data['warning'] = $this->sale_lib->out_of_stock($this->sale_lib->get_item_id($line), $item_location); + } else { + $errors = $this->validator->getErrors(); + $data['error'] = $errors ? reset($errors) : lang('Sales.error_editing_item'); + } + + return $this->_reload($data); + } + + /** + * Deletes an item specified in the parameter from the shopping cart. Used in app/Views/sales/register.php + * + * @param int $item_id + * @return ResponseInterface + * @throws ReflectionException + * @noinspection PhpUnused + */ + public function getDeleteItem(int $item_id): ResponseInterface|string + { + $this->sale_lib->delete_item($item_id); + + $this->sale_lib->empty_payments(); + + return $this->_reload(); + } + + /** + * Remove the current customer from the sale. Used in app/Views/sales/register.php + * + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function getRemoveCustomer(): ResponseInterface|string + { + $this->sale_lib->clear_giftcard_remainder(); + $this->sale_lib->clear_rewards_remainder(); + $this->sale_lib->delete_payment(lang('Sales.rewards')); + $this->sale_lib->clear_invoice_number(); + $this->sale_lib->clear_quote_number(); + $this->sale_lib->remove_customer(); + + return $this->_reload(); + } + + /** + * Complete and finalize a sale. Used in app/Views/sales/register.php + * + * @return string + * @throws ReflectionException + * @noinspection PhpUnused + */ + public function postComplete(): string // TODO: this function is huge. Probably should be refactored. + { + $sale_id = $this->sale_lib->get_sale_id(); + $data = []; + $data['dinner_table'] = $this->sale_lib->get_dinner_table(); + + $data['cart'] = $this->sale_lib->get_cart(); + + $data['include_hsn'] = (bool)$this->config['include_hsn']; + $__time = time(); + $data['transaction_time'] = to_datetime($__time); + $data['transaction_date'] = to_date($__time); + $data['show_stock_locations'] = $this->stock_location->show_locations('sales'); + $data['comments'] = $this->sale_lib->get_comment(); + $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 . ' ' . mb_substr($employee_info->last_name, 0, 1); + + $data['company_info'] = implode("\n", [$this->config['address'], $this->config['phone']]); + + if ($this->config['account_number']) { + $data['company_info'] .= "\n" . lang('Sales.account_number') . ": " . $this->config['account_number']; + } + + if ($this->config['tax_id'] != '') { + $data['company_info'] .= "\n" . lang('Sales.tax_id') . ": " . $this->config['tax_id']; + } + + $data['invoice_number_enabled'] = $this->sale_lib->is_invoice_mode(); + $data['cur_giftcard_value'] = $this->sale_lib->get_giftcard_remainder(); + $data['cur_rewards_value'] = $this->sale_lib->get_rewards_remainder(); + $data['print_after_sale'] = $this->session->get('sales_print_after_sale'); + $data['price_work_orders'] = $this->sale_lib->is_price_work_orders(); + $data['email_receipt'] = $this->sale_lib->is_email_receipt(); + $customer_id = $this->sale_lib->get_customer(); + $invoice_number = $this->sale_lib->get_invoice_number(); + $data["invoice_number"] = $invoice_number; + $work_order_number = $this->sale_lib->get_work_order_number(); + $data["work_order_number"] = $work_order_number; + $quote_number = $this->sale_lib->get_quote_number(); + $data["quote_number"] = $quote_number; + $customer_info = $this->_load_customer_data($customer_id, $data); + + if ($customer_info != null) { + $data["customer_comments"] = $customer_info->comments; + $data['tax_id'] = $customer_info->tax_id; + } + $tax_details = $this->tax_lib->get_taxes($data['cart']); // TODO: Duplicated code + $data['taxes'] = $tax_details[0]; + $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($tax_details[0]); + $data['subtotal'] = $totals['subtotal']; + $data['total'] = $totals['total']; + $data['payments_total'] = $totals['payment_total']; + $data['payments_cover_total'] = $totals['payments_cover_total']; + $data['cash_rounding'] = $this->session->get('cash_rounding'); + $data['cash_mode'] = $this->session->get('cash_mode'); // TODO: Duplicated code + $data['prediscount_subtotal'] = $totals['prediscount_subtotal']; + $data['cash_total'] = $totals['cash_total']; + $data['non_cash_total'] = $totals['total']; + $data['cash_amount_due'] = $totals['cash_amount_due']; + $data['non_cash_amount_due'] = $totals['amount_due']; + + // Prevent negative total sales (fraud/theft vector) - returns can have negative totals for legitimate refunds + if ($this->sale_lib->get_mode() != 'return' && bccomp($totals['total'], '0') < 0) { + $data['error'] = lang('Sales.negative_total_invalid'); + return $this->_reload($data); + } + + if ($data['cash_mode']) { // TODO: Convert this to ternary notation + $data['amount_due'] = $totals['cash_amount_due']; + } else { + $data['amount_due'] = $totals['amount_due']; + } + + $data['amount_change'] = $data['amount_due'] * -1; + + if ($data['amount_change'] > 0) { + // Save cash refund to the cash payment transaction if found, if not then add as new Cash transaction + + if (array_key_exists(lang('Sales.cash'), $data['payments'])) { + $data['payments'][lang('Sales.cash')]['cash_refund'] = $data['amount_change']; + } else { + $payment = [ + lang('Sales.cash') => [ + 'payment_type' => lang('Sales.cash'), + 'payment_amount' => 0, + 'cash_refund' => $data['amount_change'] + ] + ]; + + $data['payments'] += $payment; + } + } + + $data['print_price_info'] = true; + + if ($this->sale_lib->is_invoice_mode()) { + $invoice_format = $this->config['sales_invoice_format']; + + // 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 (!empty($invoice_format) && $invoice_number == null) { + // The user can retain the default encoded format or can manually override it. It still passes through the rendering step. + $invoice_number = $this->token_lib->render($invoice_format); + } + + + if ($sale_id == NEW_ENTRY && $this->sale->check_invoice_number_exists($invoice_number)) { + $data['error'] = lang('Sales.invoice_number_duplicate', [$invoice_number]); + return $this->_reload($data); + } else { + $data['invoice_number'] = $invoice_number; + $data['sale_status'] = COMPLETED; + $sale_type = SALE_TYPE_INVOICE; + + $invoice_type = $this->config['invoice_type']; + if (!Sale_lib::isValidInvoiceType($invoice_type)) { + $invoice_type = 'invoice'; + } + $invoice_view = $invoice_type; + + // Save the data to the sales table + $data['sale_id_num'] = $this->sale->save_value($sale_id, $data['sale_status'], $data['cart'], $customer_id, $employee_id, $data['comments'], $invoice_number, $work_order_number, $quote_number, $sale_type, $data['payments'], $data['dinner_table'], $tax_details); $data['sale_id'] = 'POS ' . $data['sale_id_num']; // Resort and filter cart lines for printing $data['cart'] = $this->sale_lib->sort_and_filter_cart($data['cart']); + $this->_append_secondary_currency($data); if ($data['sale_id_num'] == NEW_ENTRY) { $data['error_message'] = lang('Sales.transaction_failed'); return $this->_reload($data); - } else { - $data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']); - $this->sale_lib->clear_all(); - return view('sales/' . $invoice_view, $data); - } - } - } elseif ($this->sale_lib->is_work_order_mode()) { - - if (!($data['price_work_orders'] == 1)) { - $data['print_price_info'] = false; - } - - $data['sales_work_order'] = lang('Sales.work_order'); - $data['work_order_number_label'] = lang('Sales.work_order_number'); - - if ($work_order_number == null) { - // Generate work order number - $work_order_format = $this->config['work_order_format']; - $work_order_number = $this->token_lib->render($work_order_format); - } - - if ($sale_id == NEW_ENTRY && $this->sale->check_work_order_number_exists($work_order_number)) { - $data['error'] = lang('Sales.work_order_number_duplicate'); - return $this->_reload($data); - } else { - $data['work_order_number'] = $work_order_number; - $data['sale_status'] = SUSPENDED; - $sale_type = SALE_TYPE_WORK_ORDER; - + } else { + $data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']); + $this->sale_lib->clear_all(); + return view('sales/' . $invoice_view, $data); + } + } + } elseif ($this->sale_lib->is_work_order_mode()) { + + if (!($data['price_work_orders'] == 1)) { + $data['print_price_info'] = false; + } + + $data['sales_work_order'] = lang('Sales.work_order'); + $data['work_order_number_label'] = lang('Sales.work_order_number'); + + if ($work_order_number == null) { + // Generate work order number + $work_order_format = $this->config['work_order_format']; + $work_order_number = $this->token_lib->render($work_order_format); + } + + if ($sale_id == NEW_ENTRY && $this->sale->check_work_order_number_exists($work_order_number)) { + $data['error'] = lang('Sales.work_order_number_duplicate'); + return $this->_reload($data); + } else { + $data['work_order_number'] = $work_order_number; + $data['sale_status'] = SUSPENDED; + $sale_type = SALE_TYPE_WORK_ORDER; + $data['sale_id_num'] = $this->sale->save_value($sale_id, $data['sale_status'], $data['cart'], $customer_id, $employee_id, $data['comments'], $invoice_number, $work_order_number, $quote_number, $sale_type, $data['payments'], $data['dinner_table'], $tax_details); $this->sale_lib->set_suspended_id($data['sale_id_num']); $data['cart'] = $this->sale_lib->sort_and_filter_cart($data['cart']); $data['barcode'] = null; + $this->_append_secondary_currency($data); $this->sale_lib->clear_all(); return view('sales/work_order', $data); - } - } elseif ($this->sale_lib->is_quote_mode()) { - $data['sales_quote'] = lang('Sales.quote'); - $data['quote_number_label'] = lang('Sales.quote_number'); - - if ($quote_number == null) { - // Generate quote number - $quote_format = $this->config['sales_quote_format']; - $quote_number = $this->token_lib->render($quote_format); - } - - if ($sale_id == NEW_ENTRY && $this->sale->check_quote_number_exists($quote_number)) { - $data['error'] = lang('Sales.quote_number_duplicate'); - return $this->_reload($data); - } else { - $data['quote_number'] = $quote_number; - $data['sale_status'] = SUSPENDED; - $sale_type = SALE_TYPE_QUOTE; - + } + } elseif ($this->sale_lib->is_quote_mode()) { + $data['sales_quote'] = lang('Sales.quote'); + $data['quote_number_label'] = lang('Sales.quote_number'); + + if ($quote_number == null) { + // Generate quote number + $quote_format = $this->config['sales_quote_format']; + $quote_number = $this->token_lib->render($quote_format); + } + + if ($sale_id == NEW_ENTRY && $this->sale->check_quote_number_exists($quote_number)) { + $data['error'] = lang('Sales.quote_number_duplicate'); + return $this->_reload($data); + } else { + $data['quote_number'] = $quote_number; + $data['sale_status'] = SUSPENDED; + $sale_type = SALE_TYPE_QUOTE; + $data['sale_id_num'] = $this->sale->save_value($sale_id, $data['sale_status'], $data['cart'], $customer_id, $employee_id, $data['comments'], $invoice_number, $work_order_number, $quote_number, $sale_type, $data['payments'], $data['dinner_table'], $tax_details); $this->sale_lib->set_suspended_id($data['sale_id_num']); $data['cart'] = $this->sale_lib->sort_and_filter_cart($data['cart']); $data['barcode'] = null; + $this->_append_secondary_currency($data); $this->sale_lib->clear_all(); return view('sales/quote', $data); - } - } else { - // Save the data to the sales table - $data['sale_status'] = COMPLETED; - if ($this->sale_lib->is_return_mode()) { - $sale_type = SALE_TYPE_RETURN; - } else { - $sale_type = SALE_TYPE_POS; - } - - $data['sale_id_num'] = $this->sale->save_value($sale_id, $data['sale_status'], $data['cart'], $customer_id, $employee_id, $data['comments'], $invoice_number, $work_order_number, $quote_number, $sale_type, $data['payments'], $data['dinner_table'], $tax_details); - + } + } else { + // Save the data to the sales table + $data['sale_status'] = COMPLETED; + if ($this->sale_lib->is_return_mode()) { + $sale_type = SALE_TYPE_RETURN; + } else { + $sale_type = SALE_TYPE_POS; + } + + $data['sale_id_num'] = $this->sale->save_value($sale_id, $data['sale_status'], $data['cart'], $customer_id, $employee_id, $data['comments'], $invoice_number, $work_order_number, $quote_number, $sale_type, $data['payments'], $data['dinner_table'], $tax_details); + $data['sale_id'] = 'POS ' . $data['sale_id_num']; $data['cart'] = $this->sale_lib->sort_and_filter_cart($data['cart']); + $this->_append_secondary_currency($data); if ($data['sale_id_num'] == NEW_ENTRY) { $data['error_message'] = lang('Sales.transaction_failed'); return $this->_reload($data); - } else { - $data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']); - - // Validate receipt template to prevent path traversal - $receipt_template = $this->config['receipt_template'] ?? ''; - if (!Sale_lib::isValidReceiptTemplate($receipt_template)) { - $receipt_template = 'receipt_default'; - } - $data['receipt_template_view'] = $receipt_template; - - $this->sale_lib->clear_all(); - return view('sales/receipt', $data); - } - } - } - - /** - * Email PDF invoice to customer. Used in app/Views/sales/form.php, invoice.php, quote.php, tax_invoice.php and work_order.php - * - * @param int $sale_id - * @param string $type - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function getSendPdf(int $sale_id, string $type = 'invoice'): ResponseInterface - { - $sale_data = $this->_load_sale_data($sale_id); - - $result = false; - $message = lang('Sales.invoice_no_email'); - - if (!empty($sale_data['customer_email'])) { - $to = $sale_data['customer_email']; - $number = array_key_exists($type . "_number", $sale_data) ? $sale_data[$type . "_number"] : ""; - $subject = lang('Sales.' . $type) . ' ' . $number; - - $text = $this->config['invoice_email_message']; - $tokens = [ - new Token_invoice_sequence($number), - new Token_invoice_count('POS ' . $sale_data['sale_id']), - new Token_customer((array)$sale_data) - ]; - $text = $this->token_lib->render($text, $tokens); - $sale_data['mimetype'] = $this->email_lib->getLogoMimeType(); - - // Build img_tag for email views that need it (receipt_email.php) - $sale_data['img_tag'] = $this->email_lib->buildLogoImgTag(); - - // Generate email attachment: invoice in PDF format - $view = Services::renderer(); - $html = $view->setData($sale_data)->render("sales/$type" . '_email', $sale_data); - - // Load PDF helper - helper(['dompdf', 'file']); - $filename = sys_get_temp_dir() . '/' . lang('Sales.' . $type) . '-' . str_replace('/', '-', $number) . '.pdf'; - if (file_put_contents($filename, create_pdf($html)) !== false) { - $result = $this->email_lib->sendEmail($to, $subject, $text, $filename); - } - - $message = lang($result ? "Sales." . $type . "_sent" : "Sales." . $type . "_unsent") . ' ' . $to; - } - - $this->sale_lib->clear_all(); - - return $this->response->setJSON(['success' => $result, 'message' => $message, 'id' => $sale_id]); - } - - /** - * Emails sales receipt to customer. Used in app/Views/sales/receipt.php - * - * @param int $sale_id - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function getSendReceipt(int $sale_id): ResponseInterface - { - $sale_data = $this->_load_sale_data($sale_id); - - $result = false; - $message = lang('Sales.receipt_no_email'); - - if (!empty($sale_data['customer_email'])) { - $sale_data['barcode'] = $this->barcode_lib->generate_receipt_barcode($sale_data['sale_id']); - $sale_data['img_tag'] = $this->email_lib->buildLogoImgTag(); - - $to = $sale_data['customer_email']; - $subject = lang('Sales.receipt'); - - $view = Services::renderer(); - $text = $view->setData($sale_data)->render('sales/receipt_email'); - - $result = $this->email_lib->sendEmail($to, $subject, $text); - - $message = lang($result ? 'Sales.receipt_sent' : 'Sales.receipt_unsent') . ' ' . $to; - } - - $this->sale_lib->clear_all(); - - return $this->response->setJSON(['success' => $result, 'message' => $message, 'id' => $sale_id]); - } - - /** - * @param int $customer_id - * @param array $data - * @param bool $stats - * @return array|stdClass|string|null - */ - private function _load_customer_data(int $customer_id, array &$data, bool $stats = false): array|string|stdClass|null // TODO: Hungarian notation - { - $customer_info = ''; - - if ($customer_id != NEW_ENTRY) { - $customer_info = $this->customer->get_info($customer_id); - $data['customer_id'] = $customer_id; - - if (!empty($customer_info->company_name)) { - $data['customer'] = $customer_info->company_name; - } else { - $data['customer'] = $customer_info->first_name . ' ' . $customer_info->last_name; - } - - $data['first_name'] = $customer_info->first_name; - $data['last_name'] = $customer_info->last_name; - $data['customer_email'] = $customer_info->email; - $data['customer_address'] = $customer_info->address_1; - - if (!empty($customer_info->zip) || !empty($customer_info->city)) { - $data['customer_location'] = $customer_info->zip . ' ' . $customer_info->city . "\n" . $customer_info->state; - } else { - $data['customer_location'] = ''; - } - - $data['customer_account_number'] = $customer_info->account_number; - $data['customer_discount'] = $customer_info->discount; - $data['customer_discount_type'] = $customer_info->discount_type; - $package_id = $this->customer->get_info($customer_id)->package_id; - - if ($package_id != null) { - $package_name = $this->customer_rewards->get_name($package_id); - $points = $this->customer->get_info($customer_id)->points; - $data['customer_rewards']['package_id'] = $package_id; - $data['customer_rewards']['points'] = empty($points) ? 0 : $points; - $data['customer_rewards']['package_name'] = $package_name; - } - - if ($stats) { - $cust_stats = $this->customer->get_stats($customer_id); - $data['customer_total'] = empty($cust_stats) ? 0 : $cust_stats->total; - } - - $data['customer_info'] = implode("\n", [ - $data['customer'], - $data['customer_address'], - $data['customer_location'] - ]); - - if ($data['customer_account_number']) { - $data['customer_info'] .= "\n" . lang('Sales.account_number') . ": " . $data['customer_account_number']; - } - - if ($customer_info->tax_id != '') { - $data['customer_info'] .= "\n" . lang('Sales.tax_id') . ": " . $customer_info->tax_id; - } - $data['tax_id'] = $customer_info->tax_id; - } - - return $customer_info; - } - - /** - * @param $sale_id - * @return array - */ - private function _load_sale_data($sale_id): array // TODO: Hungarian notation - { - $this->sale_lib->clear_all(); - $cash_rounding = $this->sale_lib->reset_cash_rounding(); - $data['cash_rounding'] = $cash_rounding; - - $sale_info = $this->sale->get_info($sale_id)->getRowArray(); - $this->sale_lib->copy_entire_sale($sale_id); - $data = []; - $data['cart'] = $this->sale_lib->get_cart(); - $data['payments'] = $this->sale_lib->get_payments(); - $data['selected_payment_type'] = $this->sale_lib->get_payment_type(); - - $tax_details = $this->tax_lib->get_taxes($data['cart'], $sale_id); - $data['taxes'] = $this->sale->get_sales_taxes($sale_id); - $data['discount'] = $this->sale_lib->get_discount(); - $data['transaction_time'] = to_datetime(strtotime($sale_info['sale_time'])); - $data['transaction_date'] = to_date(strtotime($sale_info['sale_time'])); - $data['show_stock_locations'] = $this->stock_location->show_locations('sales'); - - $data['include_hsn'] = (bool)$this->config['include_hsn']; - - // Returns 'subtotal', 'total', 'cash_total', 'payment_total', 'amount_due', 'cash_amount_due', 'payments_cover_total' - $totals = $this->sale_lib->get_totals($tax_details[0]); - $this->session->set('cash_adjustment_amount', $totals['cash_adjustment_amount']); - $data['subtotal'] = $totals['subtotal']; - $data['payments_total'] = $totals['payment_total']; - $data['payments_cover_total'] = $totals['payments_cover_total']; - $data['cash_mode'] = $this->session->get('cash_mode'); // TODO: Duplicated code. - $data['prediscount_subtotal'] = $totals['prediscount_subtotal']; - $data['cash_total'] = $totals['cash_total']; - $data['non_cash_total'] = $totals['total']; - $data['cash_amount_due'] = $totals['cash_amount_due']; - $data['non_cash_amount_due'] = $totals['amount_due']; - + } else { + $data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']); + $this->sale_lib->clear_all(); + return view('sales/receipt', $data); + } + } + } + + /** + * Email PDF invoice to customer. Used in app/Views/sales/form.php, invoice.php, quote.php, tax_invoice.php and work_order.php + * + * @param int $sale_id + * @param string $type + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function getSendPdf(int $sale_id, string $type = 'invoice'): ResponseInterface + { + $sale_data = $this->_load_sale_data($sale_id); + + $result = false; + $message = lang('Sales.invoice_no_email'); + + if (!empty($sale_data['customer_email'])) { + $to = $sale_data['customer_email']; + $number = array_key_exists($type . "_number", $sale_data) ? $sale_data[$type . "_number"] : ""; + $subject = lang('Sales.' . $type) . ' ' . $number; + + $text = $this->config['invoice_email_message']; + $tokens = [ + new Token_invoice_sequence($number), + new Token_invoice_count('POS ' . $sale_data['sale_id']), + new Token_customer((array)$sale_data) + ]; + $text = $this->token_lib->render($text, $tokens); + $sale_data['mimetype'] = $this->email_lib->getLogoMimeType(); + + // Build img_tag for email views that need it (receipt_email.php) + $sale_data['img_tag'] = $this->email_lib->buildLogoImgTag(); + + // Generate email attachment: invoice in PDF format + $view = Services::renderer(); + $html = $view->setData($sale_data)->render("sales/$type" . '_email', $sale_data); + + // Load PDF helper + helper(['dompdf', 'file']); + $filename = sys_get_temp_dir() . '/' . lang('Sales.' . $type) . '-' . str_replace('/', '-', $number) . '.pdf'; + if (file_put_contents($filename, create_pdf($html)) !== false) { + $result = $this->email_lib->sendEmail($to, $subject, $text, $filename); + } + + $message = lang($result ? "Sales." . $type . "_sent" : "Sales." . $type . "_unsent") . ' ' . $to; + } + + $this->sale_lib->clear_all(); + + return $this->response->setJSON(['success' => $result, 'message' => $message, 'id' => $sale_id]); + } + + /** + * Emails sales receipt to customer. Used in app/Views/sales/receipt.php + * + * @param int $sale_id + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function getSendReceipt(int $sale_id): ResponseInterface + { + $sale_data = $this->_load_sale_data($sale_id); + + $result = false; + $message = lang('Sales.receipt_no_email'); + + if (!empty($sale_data['customer_email'])) { + $sale_data['barcode'] = $this->barcode_lib->generate_receipt_barcode($sale_data['sale_id']); + $sale_data['img_tag'] = $this->email_lib->buildLogoImgTag(); + + $to = $sale_data['customer_email']; + $subject = lang('Sales.receipt'); + + $view = Services::renderer(); + $text = $view->setData($sale_data)->render('sales/receipt_email'); + + $result = $this->email_lib->sendEmail($to, $subject, $text); + + $message = lang($result ? 'Sales.receipt_sent' : 'Sales.receipt_unsent') . ' ' . $to; + } + + $this->sale_lib->clear_all(); + + return $this->response->setJSON(['success' => $result, 'message' => $message, 'id' => $sale_id]); + } + + /** + * @param int $customer_id + * @param array $data + * @param bool $stats + * @return array|stdClass|string|null + */ + private function _load_customer_data(int $customer_id, array &$data, bool $stats = false): array|string|stdClass|null // TODO: Hungarian notation + { + $customer_info = ''; + + if ($customer_id != NEW_ENTRY) { + $customer_info = $this->customer->get_info($customer_id); + $data['customer_id'] = $customer_id; + + if (!empty($customer_info->company_name)) { + $data['customer'] = $customer_info->company_name; + } else { + $data['customer'] = $customer_info->first_name . ' ' . $customer_info->last_name; + } + + $data['first_name'] = $customer_info->first_name; + $data['last_name'] = $customer_info->last_name; + $data['customer_email'] = $customer_info->email; + $data['customer_address'] = $customer_info->address_1; + + if (!empty($customer_info->zip) || !empty($customer_info->city)) { + $data['customer_location'] = $customer_info->zip . ' ' . $customer_info->city . "\n" . $customer_info->state; + } else { + $data['customer_location'] = ''; + } + + $data['customer_account_number'] = $customer_info->account_number; + $data['customer_discount'] = $customer_info->discount; + $data['customer_discount_type'] = $customer_info->discount_type; + $package_id = $this->customer->get_info($customer_id)->package_id; + + if ($package_id != null) { + $package_name = $this->customer_rewards->get_name($package_id); + $points = $this->customer->get_info($customer_id)->points; + $data['customer_rewards']['package_id'] = $package_id; + $data['customer_rewards']['points'] = empty($points) ? 0 : $points; + $data['customer_rewards']['package_name'] = $package_name; + } + + if ($stats) { + $cust_stats = $this->customer->get_stats($customer_id); + $data['customer_total'] = empty($cust_stats) ? 0 : $cust_stats->total; + } + + $data['customer_info'] = implode("\n", [ + $data['customer'], + $data['customer_address'], + $data['customer_location'] + ]); + + if ($data['customer_account_number']) { + $data['customer_info'] .= "\n" . lang('Sales.account_number') . ": " . $data['customer_account_number']; + } + + if ($customer_info->tax_id != '') { + $data['customer_info'] .= "\n" . lang('Sales.tax_id') . ": " . $customer_info->tax_id; + } + $data['tax_id'] = $customer_info->tax_id; + } + + return $customer_info; + } + + /** + * @param $sale_id + * @return array + */ + private function _load_sale_data($sale_id): array // TODO: Hungarian notation + { + $this->sale_lib->clear_all(); + $cash_rounding = $this->sale_lib->reset_cash_rounding(); + $data['cash_rounding'] = $cash_rounding; + + $sale_info = $this->sale->get_info($sale_id)->getRowArray(); + $this->sale_lib->copy_entire_sale($sale_id); + $data = []; + $data['cart'] = $this->sale_lib->get_cart(); + $data['payments'] = $this->sale_lib->get_payments(); + $data['selected_payment_type'] = $this->sale_lib->get_payment_type(); + + $tax_details = $this->tax_lib->get_taxes($data['cart'], $sale_id); + $data['taxes'] = $this->sale->get_sales_taxes($sale_id); + $data['discount'] = $this->sale_lib->get_discount(); + $data['transaction_time'] = to_datetime(strtotime($sale_info['sale_time'])); + $data['transaction_date'] = to_date(strtotime($sale_info['sale_time'])); + $data['show_stock_locations'] = $this->stock_location->show_locations('sales'); + + $data['include_hsn'] = (bool)$this->config['include_hsn']; + + // Returns 'subtotal', 'total', 'cash_total', 'payment_total', 'amount_due', 'cash_amount_due', 'payments_cover_total' + $totals = $this->sale_lib->get_totals($tax_details[0]); + $this->session->set('cash_adjustment_amount', $totals['cash_adjustment_amount']); + $data['subtotal'] = $totals['subtotal']; + $data['payments_total'] = $totals['payment_total']; + $data['payments_cover_total'] = $totals['payments_cover_total']; + $data['cash_mode'] = $this->session->get('cash_mode'); // TODO: Duplicated code. + $data['prediscount_subtotal'] = $totals['prediscount_subtotal']; + $data['cash_total'] = $totals['cash_total']; + $data['non_cash_total'] = $totals['total']; + $data['cash_amount_due'] = $totals['cash_amount_due']; + $data['non_cash_amount_due'] = $totals['amount_due']; + if ($data['cash_mode'] && ($data['selected_payment_type'] === lang('Sales.cash') || $data['payments_total'] > 0)) { $data['total'] = $totals['cash_total']; $data['amount_due'] = $totals['cash_amount_due']; @@ -1127,113 +1275,107 @@ class Sales extends Secure_Controller $employee_info = $this->employee->get_info($this->sale_lib->get_employee()); $data['employee'] = $employee_info->first_name . ' ' . mb_substr($employee_info->last_name, 0, 1); $this->_load_customer_data($this->sale_lib->get_customer(), $data); - - $data['sale_id_num'] = $sale_id; - $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", [$this->config['address'], $this->config['phone']]); // TODO: Duplicated code. - - if ($this->config['account_number']) { - $data['company_info'] .= "\n" . lang('Sales.account_number') . ": " . $this->config['account_number']; - } - if ($this->config['tax_id'] != '') { - $data['company_info'] .= "\n" . lang('Sales.tax_id') . ": " . $this->config['tax_id']; - } - - $data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']); - $data['print_after_sale'] = false; - $data['price_work_orders'] = false; - - if ($this->sale_lib->get_mode() == 'sale_invoice') { // TODO: Duplicated code. - $data['mode_label'] = lang('Sales.invoice'); - $data['customer_required'] = lang('Sales.customer_required'); - } elseif ($this->sale_lib->get_mode() == 'sale_quote') { - $data['mode_label'] = lang('Sales.quote'); - $data['customer_required'] = lang('Sales.customer_required'); - } elseif ($this->sale_lib->get_mode() == 'sale_work_order') { - $data['mode_label'] = lang('Sales.work_order'); - $data['customer_required'] = lang('Sales.customer_required'); - } elseif ($this->sale_lib->get_mode() == 'return') { - $data['mode_label'] = lang('Sales.return'); - $data['customer_required'] = lang('Sales.customer_optional'); - } else { - $data['mode_label'] = lang('Sales.receipt'); - $data['customer_required'] = lang('Sales.customer_optional'); - } - + + $data['sale_id_num'] = $sale_id; + $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", [$this->config['address'], $this->config['phone']]); // TODO: Duplicated code. + + if ($this->config['account_number']) { + $data['company_info'] .= "\n" . lang('Sales.account_number') . ": " . $this->config['account_number']; + } + if ($this->config['tax_id'] != '') { + $data['company_info'] .= "\n" . lang('Sales.tax_id') . ": " . $this->config['tax_id']; + } + + $data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']); + $data['print_after_sale'] = false; + $data['price_work_orders'] = false; + + if ($this->sale_lib->get_mode() == 'sale_invoice') { // TODO: Duplicated code. + $data['mode_label'] = lang('Sales.invoice'); + $data['customer_required'] = lang('Sales.customer_required'); + } elseif ($this->sale_lib->get_mode() == 'sale_quote') { + $data['mode_label'] = lang('Sales.quote'); + $data['customer_required'] = lang('Sales.customer_required'); + } elseif ($this->sale_lib->get_mode() == 'sale_work_order') { + $data['mode_label'] = lang('Sales.work_order'); + $data['customer_required'] = lang('Sales.customer_required'); + } elseif ($this->sale_lib->get_mode() == 'return') { + $data['mode_label'] = lang('Sales.return'); + $data['customer_required'] = lang('Sales.customer_optional'); + } else { + $data['mode_label'] = lang('Sales.receipt'); + $data['customer_required'] = lang('Sales.customer_optional'); + } + $invoice_type = $this->config['invoice_type']; if (!Sale_lib::isValidInvoiceType($invoice_type)) { $invoice_type = 'invoice'; } $data['invoice_view'] = $invoice_type; - - // Validate receipt template to prevent path traversal - $receipt_template = $this->config['receipt_template'] ?? ''; - if (!Sale_lib::isValidReceiptTemplate($receipt_template)) { - $receipt_template = 'receipt_default'; - } - $data['receipt_template_view'] = $receipt_template; + $this->_append_secondary_currency($data); return $data; } - - /** - * @param array $data - * @return void - */ - private function _reload(array $data = []): ResponseInterface|string // TODO: Hungarian notation - { - $sale_id = $this->session->get('sale_id'); // TODO: This variable is never used - - if ($sale_id == '') { - $sale_id = NEW_ENTRY; - $this->session->set('sale_id', NEW_ENTRY); - } - $cash_rounding = $this->sale_lib->reset_cash_rounding(); - - // cash_rounding indicates only that the site is configured for cash rounding - $data['cash_rounding'] = $cash_rounding; - - $data['cart'] = $this->sale_lib->get_cart(); - $customer_info = $this->_load_customer_data($this->sale_lib->get_customer(), $data, true); - - $data['modes'] = $this->sale_lib->get_register_mode_options(); - $data['mode'] = $this->sale_lib->get_mode(); - $data['selected_table'] = $this->sale_lib->get_dinner_table(); - $data['empty_tables'] = $this->sale_lib->get_empty_tables($data['selected_table']); - $data['stock_locations'] = $this->stock_location->get_allowed_locations('sales'); - $data['stock_location'] = $this->sale_lib->get_sale_location(); - $data['tax_exclusive_subtotal'] = $this->sale_lib->get_subtotal(true, true); - $tax_details = $this->tax_lib->get_taxes($data['cart']); // TODO: Duplicated code. - $data['taxes'] = $tax_details[0]; - $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($tax_details[0]); - - $data['item_count'] = $totals['item_count']; - $data['total_units'] = $totals['total_units']; - $data['subtotal'] = $totals['subtotal']; - $data['total'] = $totals['total']; - $data['payments_total'] = $totals['payment_total']; - $data['payments_cover_total'] = $totals['payments_cover_total']; - - // cash_mode indicates whether this sale is going to be processed using cash_rounding - $cash_mode = $this->session->get('cash_mode'); - $data['cash_mode'] = $cash_mode; - $data['prediscount_subtotal'] = $totals['prediscount_subtotal']; // TODO: Duplicated code. - $data['cash_total'] = $totals['cash_total']; - $data['non_cash_total'] = $totals['total']; - $data['cash_amount_due'] = $totals['cash_amount_due']; - $data['non_cash_amount_due'] = $totals['amount_due']; - - $data['selected_payment_type'] = $this->sale_lib->get_payment_type(); - + + /** + * @param array $data + * @return void + */ + private function _reload(array $data = []): ResponseInterface|string // TODO: Hungarian notation + { + $sale_id = $this->session->get('sale_id'); // TODO: This variable is never used + + if ($sale_id == '') { + $sale_id = NEW_ENTRY; + $this->session->set('sale_id', NEW_ENTRY); + } + $cash_rounding = $this->sale_lib->reset_cash_rounding(); + + // cash_rounding indicates only that the site is configured for cash rounding + $data['cash_rounding'] = $cash_rounding; + + $data['cart'] = $this->sale_lib->get_cart(); + $customer_info = $this->_load_customer_data($this->sale_lib->get_customer(), $data, true); + + $data['modes'] = $this->sale_lib->get_register_mode_options(); + $data['mode'] = $this->sale_lib->get_mode(); + $data['selected_table'] = $this->sale_lib->get_dinner_table(); + $data['empty_tables'] = $this->sale_lib->get_empty_tables($data['selected_table']); + $data['stock_locations'] = $this->stock_location->get_allowed_locations('sales'); + $data['stock_location'] = $this->sale_lib->get_sale_location(); + $data['tax_exclusive_subtotal'] = $this->sale_lib->get_subtotal(true, true); + $tax_details = $this->tax_lib->get_taxes($data['cart']); // TODO: Duplicated code. + $data['taxes'] = $tax_details[0]; + $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($tax_details[0]); + + $data['item_count'] = $totals['item_count']; + $data['total_units'] = $totals['total_units']; + $data['subtotal'] = $totals['subtotal']; + $data['total'] = $totals['total']; + $data['payments_total'] = $totals['payment_total']; + $data['payments_cover_total'] = $totals['payments_cover_total']; + + // cash_mode indicates whether this sale is going to be processed using cash_rounding + $cash_mode = $this->session->get('cash_mode'); + $data['cash_mode'] = $cash_mode; + $data['prediscount_subtotal'] = $totals['prediscount_subtotal']; // TODO: Duplicated code. + $data['cash_total'] = $totals['cash_total']; + $data['non_cash_total'] = $totals['total']; + $data['cash_amount_due'] = $totals['cash_amount_due']; + $data['non_cash_amount_due'] = $totals['amount_due']; + + $data['selected_payment_type'] = $this->sale_lib->get_payment_type(); + if ($data['cash_mode'] && ($data['selected_payment_type'] == lang('Sales.cash') || $data['payments_total'] > 0)) { $data['total'] = $totals['cash_total']; $data['amount_due'] = $totals['cash_amount_due']; @@ -1243,545 +1385,545 @@ class Sales extends Secure_Controller } $data['amount_change'] = $data['amount_due'] * -1; + $this->_append_secondary_currency($data); $data['comment'] = $this->sale_lib->get_comment(); $data['email_receipt'] = $this->sale_lib->is_email_receipt(); - + if ($customer_info && $this->config['customer_reward_enable']) { $data['payment_options'] = $this->sale->get_payment_options(true, true); } else { $data['payment_options'] = $this->sale->get_payment_options(); } - - $data['items_module_allowed'] = $this->employee->has_grant('items', $this->employee->get_logged_in_employee_info()->person_id); - $data['change_price'] = $this->employee->has_grant('sales_change_price', $this->employee->get_logged_in_employee_info()->person_id); - - $temp_invoice_number = $this->sale_lib->get_invoice_number(); - $invoice_format = $this->config['sales_invoice_format']; - - if ($temp_invoice_number == null || $temp_invoice_number == '') { - $temp_invoice_number = $this->token_lib->render($invoice_format, [], false); - } - - $data['invoice_number'] = $temp_invoice_number; - - $data['print_after_sale'] = $this->sale_lib->is_print_after_sale(); - $data['price_work_orders'] = $this->sale_lib->is_price_work_orders(); - - $data['pos_mode'] = $data['mode'] == 'sale' || $data['mode'] == 'return'; - - $data['quote_number'] = $this->sale_lib->get_quote_number(); - $data['work_order_number'] = $this->sale_lib->get_work_order_number(); - $data['keyboardShortcuts'] = $this->sale_lib->getKeyShortcuts(); - - // TODO: the if/else set below should be converted to a switch - if ($this->sale_lib->get_mode() == 'sale_invoice') { // TODO: Duplicated code. - $data['mode_label'] = lang('Sales.invoice'); - $data['customer_required'] = lang('Sales.customer_required'); - } elseif ($this->sale_lib->get_mode() == 'sale_quote') { - $data['mode_label'] = lang('Sales.quote'); - $data['customer_required'] = lang('Sales.customer_required'); - } elseif ($this->sale_lib->get_mode() == 'sale_work_order') { - $data['mode_label'] = lang('Sales.work_order'); - $data['customer_required'] = lang('Sales.customer_required'); - } elseif ($this->sale_lib->get_mode() == 'return') { - $data['mode_label'] = lang('Sales.return'); - $data['customer_required'] = lang('Sales.customer_optional'); - } else { - $data['mode_label'] = lang('Sales.receipt'); - $data['customer_required'] = lang('Sales.customer_optional'); - } - - return view("sales/register", $data); - } - - /** - * Load the sales receipt for a sale. Used in app/Views/sales/form.php - * - * @param int $sale_id - * @return string - * @noinspection PhpUnused - */ - public function getReceipt(int $sale_id): string - { - $data = $this->_load_sale_data($sale_id); - $this->sale_lib->clear_all(); - - return view('sales/receipt', $data); - } - - /** - * Loads the sales invoice for a sale. Used in app/Views/sales/form.php - * - * @param int $sale_id - * @return string - * @noinspection PhpUnused - */ - public function getInvoice(int $sale_id): string - { - $data = $this->_load_sale_data($sale_id); - $this->sale_lib->clear_all(); - - return view('sales/' . $data['invoice_view'], $data); - } - - /** - * Edits an existing sale or work order. Used in app/Views/sales/form.php - * - * @param int $sale_id - * @return string - * @throws ReflectionException - */ - public function getEdit(int $sale_id): string - { - $data = []; - - $sale_info = $this->sale->get_info($sale_id)->getRowArray(); - $data['selected_customer_id'] = $sale_info['customer_id']; - $data['selected_customer_name'] = $sale_info['customer_name']; - $employee_info = $this->employee->get_info($sale_info['employee_id']); - $data['selected_employee_id'] = $sale_info['employee_id']; - $data['selected_employee_name'] = $employee_info->first_name . ' ' . $employee_info->last_name; - $data['sale_info'] = $sale_info; - $balance_due = round($sale_info['amount_due'] - $sale_info['amount_tendered'] + $sale_info['cash_refund'], totals_decimals(), PHP_ROUND_HALF_UP); - - if (!$this->sale_lib->reset_cash_rounding() && $balance_due < 0) { - $balance_due = 0; - } - - $data['payments'] = []; - - foreach ($this->sale->get_sale_payments($sale_id)->getResult() as $payment) { - foreach (get_object_vars($payment) as $property => $value) { - $payment->$property = $value; - } - $data['payments'][] = $payment; - } - - $data['payment_type_new'] = PAYMENT_TYPE_UNASSIGNED; - $data['payment_amount_new'] = $balance_due; - - $data['balance_due'] = $balance_due != 0; - - // Don't allow gift card to be a payment option in a sale transaction edit because it's a complex change - $payment_options = $this->sale->get_payment_options(false); - - if ($this->sale_lib->reset_cash_rounding()) { - $payment_options[lang('Sales.cash_adjustment')] = lang('Sales.cash_adjustment'); - } - - $data['payment_options'] = $payment_options; - - // Set up a slightly modified list of payment types for new payment entry - $payment_options["--"] = lang('Common.none_selected_text'); - - $data['new_payment_options'] = $payment_options; - - return view('sales/form', $data); - } - - /** - * @param int $sale_id - * @return ResponseInterface - * @throws ReflectionException - */ - public function postDelete(int $sale_id = NEW_ENTRY, bool $update_inventory = true): ResponseInterface - { - $employee_id = $this->employee->get_logged_in_employee_info()->person_id; - $has_grant = $this->employee->has_grant('sales_delete', $employee_id); - - if (!$has_grant) { - return $this->response->setJSON(['success' => false, 'message' => lang('Sales.not_authorized')]); - } else { - $sale_ids = $sale_id == NEW_ENTRY ? $this->request->getPost('ids', FILTER_SANITIZE_NUMBER_INT) : [$sale_id]; - - if ($this->sale->delete_list($sale_ids, $employee_id, $update_inventory)) { - return $this->response->setJSON([ - 'success' => true, - 'message' => lang('Sales.successfully_deleted') . ' ' . count($sale_ids) . ' ' . lang('Sales.one_or_multiple'), - 'ids' => $sale_ids - ]); - } else { - return $this->response->setJSON(['success' => false, 'message' => lang('Sales.unsuccessfully_deleted')]); - } - } - } - - /** - * @param int $sale_id - * @param bool $update_inventory - * @return ResponseInterface - */ - public function restore(int $sale_id = NEW_ENTRY, bool $update_inventory = true): ResponseInterface - { - $employee_id = $this->employee->get_logged_in_employee_info()->person_id; - $has_grant = $this->employee->has_grant('sales_delete', $employee_id); - - if (!$has_grant) { - return $this->response->setJSON(['success' => false, 'message' => lang('Sales.not_authorized')]); - } else { - $sale_ids = $sale_id == NEW_ENTRY ? $this->request->getPost('ids', FILTER_SANITIZE_NUMBER_INT) : [$sale_id]; - - if ($this->sale->restore_list($sale_ids, $employee_id, $update_inventory)) { - return $this->response->setJSON([ - 'success' => true, - 'message' => lang('Sales.successfully_restored') . ' ' . count($sale_ids) . ' ' . lang('Sales.one_or_multiple'), - 'ids' => $sale_ids - ]); - } else { - return $this->response->setJSON(['success' => false, 'message' => lang('Sales.unsuccessfully_restored')]); - } - } - } - - /** - * This saves the sale from the update sale view (sales/form). - * It only updates the sales table and payments. - * @param int $sale_id - * @return ResponseInterface - * @throws ReflectionException - */ - public function postSave(int $sale_id = NEW_ENTRY): ResponseInterface - { - $newdate = $this->request->getPost('date', FILTER_SANITIZE_FULL_SPECIAL_CHARS); - $employee_id = $this->employee->get_logged_in_employee_info()->person_id; - $inventory = model(Inventory::class); - $date_formatter = date_create_from_format($this->config['dateformat'] . ' ' . $this->config['timeformat'], $newdate); - $sale_time = $date_formatter->format('Y-m-d H:i:s'); - - $sale_data = [ - 'sale_time' => $sale_time, - 'customer_id' => $this->request->getPost('customer_id') != '' ? $this->request->getPost('customer_id', FILTER_SANITIZE_NUMBER_INT) : null, - 'employee_id' => $this->request->getPost('employee_id') != '' ? $this->request->getPost('employee_id', FILTER_SANITIZE_NUMBER_INT) : null, - 'comment' => $this->request->getPost('comment', FILTER_SANITIZE_FULL_SPECIAL_CHARS), - 'invoice_number' => $this->request->getPost('invoice_number') != '' ? $this->request->getPost('invoice_number', FILTER_SANITIZE_FULL_SPECIAL_CHARS) : null - ]; - - // In order to maintain tradition the only element that can change on prior payments is the payment type - $amount_tendered = 0; - $number_of_payments = $this->request->getPost('number_of_payments', FILTER_SANITIZE_NUMBER_INT); - for ($i = 0; $i < $number_of_payments; ++$i) { - $payment_id = $this->request->getPost("payment_id_$i", FILTER_SANITIZE_NUMBER_INT); - $payment_type = $this->request->getPost("payment_type_$i", FILTER_SANITIZE_FULL_SPECIAL_CHARS); - $payment_amount = parse_decimals($this->request->getPost("payment_amount_$i")); - $refund_type = $this->request->getPost("refund_type_$i", FILTER_SANITIZE_FULL_SPECIAL_CHARS); - $cash_refund = parse_decimals($this->request->getPost("refund_amount_$i")); - - $cash_adjustment = $payment_type == lang('Sales.cash_adjustment') ? CASH_ADJUSTMENT_TRUE : CASH_ADJUSTMENT_FALSE; - - if (!$cash_adjustment) { - $amount_tendered += $payment_amount - $cash_refund; - } - - // Non-cash positive refund amounts - if (empty(strstr($refund_type, lang('Sales.cash'))) && $cash_refund > 0) { // TODO: This if and the one below can be combined. - // Change it to be a new negative payment (a "non-cash refund") - $payment_type = $refund_type; - $payment_amount = $payment_amount - $cash_refund; - $cash_refund = 0.00; - } - - $sale_data['payments'][] = [ - 'payment_id' => $payment_id, - 'payment_type' => $payment_type, - 'payment_amount' => $payment_amount, - 'cash_refund' => $cash_refund, - 'cash_adjustment' => $cash_adjustment, - 'employee_id' => $employee_id - ]; - } - - $payment_id = NEW_ENTRY; - $payment_amount_new = $this->request->getPost('payment_amount_new'); - $payment_type = $this->request->getPost('payment_type_new', FILTER_SANITIZE_FULL_SPECIAL_CHARS); - - if ($payment_type != PAYMENT_TYPE_UNASSIGNED && !empty($payment_amount_new)) { - $payment_amount = parse_decimals($payment_amount_new); - $cash_refund = 0; - if ($payment_type == lang('Sales.cash_adjustment')) { - $cash_adjustment = CASH_ADJUSTMENT_TRUE; - } else { - $cash_adjustment = CASH_ADJUSTMENT_FALSE; - $amount_tendered += $payment_amount; - $sale_info = $this->sale->get_info($sale_id)->getRowArray(); - - if ($amount_tendered > $sale_info['amount_due']) { - $cash_refund = $amount_tendered - $sale_info['amount_due']; - } - } - - $sale_data['payments'][] = [ - 'payment_id' => $payment_id, - 'payment_type' => $payment_type, - 'payment_amount' => $payment_amount, - 'cash_refund' => $cash_refund, - 'cash_adjustment' => $cash_adjustment, - 'employee_id' => $employee_id - ]; - } - - $inventory->update('POS ' . $sale_id, ['trans_date' => $sale_time]); // TODO: Reflection Exception - if ($this->sale->update($sale_id, $sale_data)) { - return $this->response->setJSON(['success' => true, 'message' => lang('Sales.successfully_updated'), 'id' => $sale_id]); - } else { - return $this->response->setJSON(['success' => false, 'message' => lang('Sales.unsuccessfully_updated'), 'id' => $sale_id]); - } - } - - /** - * This is used to cancel a suspended pos sale, quote. - * Completed sales (POS Sales or Invoiced Sales) can not be removed from the system - * Work orders can be canceled but are not physically removed from the sales history. - * Used in app/Views/sales/register.php - * - * @return ResponseInterface - * @throws ReflectionException - * @noinspection PhpUnused - */ - public function postCancel(): ResponseInterface|string - { - $sale_id = $this->sale_lib->get_sale_id(); - if ($sale_id != NEW_ENTRY && $sale_id != '') { - $sale_type = $this->sale_lib->get_sale_type(); - - if ($this->config['dinner_table_enable']) { - $dinner_table = $this->sale_lib->get_dinner_table(); - $this->dinner_table->release($dinner_table); - } - - if ($sale_type == SALE_TYPE_WORK_ORDER) { - $this->sale->update_sale_status($sale_id, CANCELED); - } else { - $this->sale->delete($sale_id); - $this->session->set('sale_id', NEW_ENTRY); - } - } else { - $this->sale_lib->remove_temp_items(); - } - - $this->sale_lib->clear_all(); - return $this->_reload(); - } - - /** - * Discards the suspended sale. Used in app/Views/sales/quote.php - * - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function getDiscardSuspendedSale(): ResponseInterface|string - { - $suspended_id = $this->sale_lib->get_suspended_id(); - $this->sale_lib->clear_all(); - $this->sale->delete_suspended_sale($suspended_id); - return $this->_reload(); - } - - /** - * Suspend the current sale. - * If the current sale is already suspended then update the existing suspended sale otherwise create - * it as a new suspended sale. Used in app/Views/sales/register.php - * - * @return ResponseInterface|string - * @noinspection PhpUnused - */ - public function postSuspend(): ResponseInterface|string - { - $sale_id = $this->sale_lib->get_sale_id(); - $dinner_table = $this->sale_lib->get_dinner_table(); - $cart = $this->sale_lib->get_cart(); - $payments = $this->sale_lib->get_payments(); - $employee_id = $this->employee->get_logged_in_employee_info()->person_id; - $customer_id = $this->sale_lib->get_customer(); - $invoice_number = $this->sale_lib->get_invoice_number(); - $work_order_number = $this->sale_lib->get_work_order_number(); - $quote_number = $this->sale_lib->get_quote_number(); - $sale_type = $this->sale_lib->get_sale_type(); - - if ($sale_type == '') { - $sale_type = SALE_TYPE_POS; - } - - $comment = $this->sale_lib->get_comment(); - $sale_status = SUSPENDED; - - $data = []; - $sales_taxes = [[], []]; - - if ($this->sale->save_value($sale_id, $sale_status, $cart, $customer_id, $employee_id, $comment, $invoice_number, $work_order_number, $quote_number, $sale_type, $payments, $dinner_table, $sales_taxes) == '-1') { - $data['error'] = lang('Sales.unsuccessfully_suspended_sale'); - } else { - $data['success'] = lang('Sales.successfully_suspended_sale'); - } - - $this->sale_lib->clear_all(); - - return $this->_reload($data); - } - - /** - * List suspended sales - * @return string - */ - public function getSuspended(): string - { - $data = []; - $customer_id = $this->sale_lib->get_customer(); - $data['suspended_sales'] = $this->sale->get_all_suspended($customer_id); - return view('sales/suspended', $data); - } - - /** - * Unsuspended sales are now left in the tables and are only removed - * when they are intentionally cancelled. Used in app/Views/sales/suspended.php. - * - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function postUnsuspend(): ResponseInterface|string - { - $sale_id = $this->request->getPost('suspended_sale_id', FILTER_SANITIZE_NUMBER_INT); - $this->sale_lib->clear_all(); - - if ($sale_id > 0) { - $this->sale_lib->copy_entire_sale($sale_id); - } - - // Set current register mode to reflect that of unsuspended order type - $this->change_register_mode($this->sale_lib->get_sale_type()); - - return $this->_reload(); - } - - /** - * Show Keyboard shortcut modal. Used in app/Views/sales/register.php - * - * @return string - * @noinspection PhpUnused - */ - public function getSalesKeyboardHelp(): string - { - return view('sales/help', [ - 'keyboardShortcuts' => $this->sale_lib->getKeyShortcuts() - ]); - } - - /** - * Check the validity of an invoice number. Used in app/Views/sales/form.php. - * - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function postCheckInvoiceNumber(): ResponseInterface - { - $sale_id = $this->request->getPost('sale_id', FILTER_SANITIZE_NUMBER_INT); - $invoice_number = $this->request->getPost('invoice_number', FILTER_SANITIZE_FULL_SPECIAL_CHARS); - $exists = !empty($invoice_number) && $this->sale->check_invoice_number_exists($invoice_number, $sale_id); - return $this->response->setJSON(!$exists ? 'true' : 'false'); - } - - /** - * @param array $cart - * @return array - */ - public function get_filtered(array $cart): array - { - $filtered_cart = []; - foreach ($cart as $id => $item) { - if ($item['print_option'] == PRINT_ALL) // Always include - { - $filtered_cart[$id] = $item; - } elseif ($item['print_option'] == PRINT_PRICED && $item['price'] != 0) // Include only if the price is not zero - { - $filtered_cart[$id] = $item; - } - // print_option 2 is never included - } - - return $filtered_cart; - } - - /** - * Update the item number in the register. Used in app/Views/sales/register.php - * - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function postChangeItemNumber(): ResponseInterface - { - $item_id = $this->request->getPost('item_id', FILTER_SANITIZE_NUMBER_INT); - $item_number = $this->request->getPost('item_number', FILTER_SANITIZE_FULL_SPECIAL_CHARS); - $this->item->update_item_number($item_id, $item_number); - $cart = $this->sale_lib->get_cart(); - $x = $this->search_cart_for_item_id($item_id, $cart); - if ($x !== null) { - $cart[$x]['item_number'] = $item_number; - } - $this->sale_lib->set_cart($cart); - return $this->response->setJSON(['success' => true]); - } - - /** - * Change a given item name. Used in app/Views/sales/register.php. - * - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function postChangeItemName(): ResponseInterface - { - $item_id = $this->request->getPost('item_id', FILTER_SANITIZE_NUMBER_INT); - $name = $this->request->getPost('item_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS); - - $this->item->update_item_name($item_id, $name); - - $cart = $this->sale_lib->get_cart(); - $x = $this->search_cart_for_item_id($item_id, $cart); - - if ($x !== null) { - $cart[$x]['name'] = $name; - } - - $this->sale_lib->set_cart($cart); - return $this->response->setJSON(['success' => true]); - } - - /** - * Update the given item description. Used in app/Views/sales/register.php - * - * @return ResponseInterface - * @noinspection PhpUnused - */ - public function postChangeItemDescription(): ResponseInterface - { - $item_id = $this->request->getPost('item_id', FILTER_SANITIZE_NUMBER_INT); - $description = $this->request->getPost('item_description', FILTER_SANITIZE_FULL_SPECIAL_CHARS); - - $this->item->update_item_description($item_id, $description); - - $cart = $this->sale_lib->get_cart(); - $x = $this->search_cart_for_item_id($item_id, $cart); - - if ($x !== null) { - $cart[$x]['description'] = $description; - } - - $this->sale_lib->set_cart($cart); - return $this->response->setJSON(['success' => true]); - } - - /** - * @param int $search_item_id - * @param array $shopping_cart - * @return int|string|null - */ - public function search_cart_for_item_id(int $search_item_id, array $shopping_cart): int|string|null - { - foreach ($shopping_cart as $key => $val) { - if ($val['item_id'] === $search_item_id) { - return $key; - } - } - - return null; - } -} + + $data['items_module_allowed'] = $this->employee->has_grant('items', $this->employee->get_logged_in_employee_info()->person_id); + $data['change_price'] = $this->employee->has_grant('sales_change_price', $this->employee->get_logged_in_employee_info()->person_id); + + $temp_invoice_number = $this->sale_lib->get_invoice_number(); + $invoice_format = $this->config['sales_invoice_format']; + + if ($temp_invoice_number == null || $temp_invoice_number == '') { + $temp_invoice_number = $this->token_lib->render($invoice_format, [], false); + } + + $data['invoice_number'] = $temp_invoice_number; + + $data['print_after_sale'] = $this->sale_lib->is_print_after_sale(); + $data['price_work_orders'] = $this->sale_lib->is_price_work_orders(); + + $data['pos_mode'] = $data['mode'] == 'sale' || $data['mode'] == 'return'; + + $data['quote_number'] = $this->sale_lib->get_quote_number(); + $data['work_order_number'] = $this->sale_lib->get_work_order_number(); + + // TODO: the if/else set below should be converted to a switch + if ($this->sale_lib->get_mode() == 'sale_invoice') { // TODO: Duplicated code. + $data['mode_label'] = lang('Sales.invoice'); + $data['customer_required'] = lang('Sales.customer_required'); + } elseif ($this->sale_lib->get_mode() == 'sale_quote') { + $data['mode_label'] = lang('Sales.quote'); + $data['customer_required'] = lang('Sales.customer_required'); + } elseif ($this->sale_lib->get_mode() == 'sale_work_order') { + $data['mode_label'] = lang('Sales.work_order'); + $data['customer_required'] = lang('Sales.customer_required'); + } elseif ($this->sale_lib->get_mode() == 'return') { + $data['mode_label'] = lang('Sales.return'); + $data['customer_required'] = lang('Sales.customer_optional'); + } else { + $data['mode_label'] = lang('Sales.receipt'); + $data['customer_required'] = lang('Sales.customer_optional'); + } + + return view("sales/register", $data); + } + + /** + * Load the sales receipt for a sale. Used in app/Views/sales/form.php + * + * @param int $sale_id + * @return string + * @noinspection PhpUnused + */ + public function getReceipt(int $sale_id): string + { + $data = $this->_load_sale_data($sale_id); + $this->sale_lib->clear_all(); + + return view('sales/receipt', $data); + } + + /** + * Loads the sales invoice for a sale. Used in app/Views/sales/form.php + * + * @param int $sale_id + * @return string + * @noinspection PhpUnused + */ + public function getInvoice(int $sale_id): string + { + $data = $this->_load_sale_data($sale_id); + $this->sale_lib->clear_all(); + + return view('sales/' . $data['invoice_view'], $data); + } + + /** + * Edits an existing sale or work order. Used in app/Views/sales/form.php + * + * @param int $sale_id + * @return string + * @throws ReflectionException + */ + public function getEdit(int $sale_id): string + { + $data = []; + + $sale_info = $this->sale->get_info($sale_id)->getRowArray(); + $data['selected_customer_id'] = $sale_info['customer_id']; + $data['selected_customer_name'] = $sale_info['customer_name']; + $employee_info = $this->employee->get_info($sale_info['employee_id']); + $data['selected_employee_id'] = $sale_info['employee_id']; + $data['selected_employee_name'] = $employee_info->first_name . ' ' . $employee_info->last_name; + $data['sale_info'] = $sale_info; + $balance_due = round($sale_info['amount_due'] - $sale_info['amount_tendered'] + $sale_info['cash_refund'], totals_decimals(), PHP_ROUND_HALF_UP); + + if (!$this->sale_lib->reset_cash_rounding() && $balance_due < 0) { + $balance_due = 0; + } + + $data['payments'] = []; + + foreach ($this->sale->get_sale_payments($sale_id)->getResult() as $payment) { + foreach (get_object_vars($payment) as $property => $value) { + $payment->$property = $value; + } + $data['payments'][] = $payment; + } + + $data['payment_type_new'] = PAYMENT_TYPE_UNASSIGNED; + $data['payment_amount_new'] = $balance_due; + + $data['balance_due'] = $balance_due != 0; + + // Don't allow gift card to be a payment option in a sale transaction edit because it's a complex change + $payment_options = $this->sale->get_payment_options(false); + + if ($this->sale_lib->reset_cash_rounding()) { + $payment_options[lang('Sales.cash_adjustment')] = lang('Sales.cash_adjustment'); + } + + $data['payment_options'] = $payment_options; + + // Set up a slightly modified list of payment types for new payment entry + $payment_options["--"] = lang('Common.none_selected_text'); + + $data['new_payment_options'] = $payment_options; + + return view('sales/form', $data); + } + + /** + * @param int $sale_id + * @return ResponseInterface + * @throws ReflectionException + */ + public function postDelete(int $sale_id = NEW_ENTRY, bool $update_inventory = true): ResponseInterface + { + $employee_id = $this->employee->get_logged_in_employee_info()->person_id; + $has_grant = $this->employee->has_grant('sales_delete', $employee_id); + + if (!$has_grant) { + return $this->response->setJSON(['success' => false, 'message' => lang('Sales.not_authorized')]); + } else { + $sale_ids = $sale_id == NEW_ENTRY ? $this->request->getPost('ids', FILTER_SANITIZE_NUMBER_INT) : [$sale_id]; + + if ($this->sale->delete_list($sale_ids, $employee_id, $update_inventory)) { + return $this->response->setJSON([ + 'success' => true, + 'message' => lang('Sales.successfully_deleted') . ' ' . count($sale_ids) . ' ' . lang('Sales.one_or_multiple'), + 'ids' => $sale_ids + ]); + } else { + return $this->response->setJSON(['success' => false, 'message' => lang('Sales.unsuccessfully_deleted')]); + } + } + } + + /** + * @param int $sale_id + * @param bool $update_inventory + * @return ResponseInterface + */ + public function restore(int $sale_id = NEW_ENTRY, bool $update_inventory = true): ResponseInterface + { + $employee_id = $this->employee->get_logged_in_employee_info()->person_id; + $has_grant = $this->employee->has_grant('sales_delete', $employee_id); + + if (!$has_grant) { + return $this->response->setJSON(['success' => false, 'message' => lang('Sales.not_authorized')]); + } else { + $sale_ids = $sale_id == NEW_ENTRY ? $this->request->getPost('ids', FILTER_SANITIZE_NUMBER_INT) : [$sale_id]; + + if ($this->sale->restore_list($sale_ids, $employee_id, $update_inventory)) { + return $this->response->setJSON([ + 'success' => true, + 'message' => lang('Sales.successfully_restored') . ' ' . count($sale_ids) . ' ' . lang('Sales.one_or_multiple'), + 'ids' => $sale_ids + ]); + } else { + return $this->response->setJSON(['success' => false, 'message' => lang('Sales.unsuccessfully_restored')]); + } + } + } + + /** + * This saves the sale from the update sale view (sales/form). + * It only updates the sales table and payments. + * @param int $sale_id + * @return ResponseInterface + * @throws ReflectionException + */ + public function postSave(int $sale_id = NEW_ENTRY): ResponseInterface + { + $newdate = $this->request->getPost('date', FILTER_SANITIZE_FULL_SPECIAL_CHARS); + $employee_id = $this->employee->get_logged_in_employee_info()->person_id; + $inventory = model(Inventory::class); + $date_formatter = date_create_from_format($this->config['dateformat'] . ' ' . $this->config['timeformat'], $newdate); + $sale_time = $date_formatter->format('Y-m-d H:i:s'); + + $sale_data = [ + 'sale_time' => $sale_time, + 'customer_id' => $this->request->getPost('customer_id') != '' ? $this->request->getPost('customer_id', FILTER_SANITIZE_NUMBER_INT) : null, + 'employee_id' => $this->request->getPost('employee_id') != '' ? $this->request->getPost('employee_id', FILTER_SANITIZE_NUMBER_INT) : null, + 'comment' => $this->request->getPost('comment', FILTER_SANITIZE_FULL_SPECIAL_CHARS), + 'invoice_number' => $this->request->getPost('invoice_number') != '' ? $this->request->getPost('invoice_number', FILTER_SANITIZE_FULL_SPECIAL_CHARS) : null + ]; + + // In order to maintain tradition the only element that can change on prior payments is the payment type + $amount_tendered = 0; + $number_of_payments = $this->request->getPost('number_of_payments', FILTER_SANITIZE_NUMBER_INT); + for ($i = 0; $i < $number_of_payments; ++$i) { + $payment_id = $this->request->getPost("payment_id_$i", FILTER_SANITIZE_NUMBER_INT); + $payment_type = $this->request->getPost("payment_type_$i", FILTER_SANITIZE_FULL_SPECIAL_CHARS); + $payment_amount = parse_decimals($this->request->getPost("payment_amount_$i")); + $refund_type = $this->request->getPost("refund_type_$i", FILTER_SANITIZE_FULL_SPECIAL_CHARS); + $cash_refund = parse_decimals($this->request->getPost("refund_amount_$i")); + + $cash_adjustment = $payment_type == lang('Sales.cash_adjustment') ? CASH_ADJUSTMENT_TRUE : CASH_ADJUSTMENT_FALSE; + + if (!$cash_adjustment) { + $amount_tendered += $payment_amount - $cash_refund; + } + + // Non-cash positive refund amounts + if (empty(strstr($refund_type, lang('Sales.cash'))) && $cash_refund > 0) { // TODO: This if and the one below can be combined. + // Change it to be a new negative payment (a "non-cash refund") + $payment_type = $refund_type; + $payment_amount = $payment_amount - $cash_refund; + $cash_refund = 0.00; + } + + $sale_data['payments'][] = [ + 'payment_id' => $payment_id, + 'payment_type' => $payment_type, + 'payment_amount' => $payment_amount, + 'cash_refund' => $cash_refund, + 'cash_adjustment' => $cash_adjustment, + 'employee_id' => $employee_id + ]; + } + + $payment_id = NEW_ENTRY; + $payment_amount_new = $this->request->getPost('payment_amount_new'); + $payment_type = $this->request->getPost('payment_type_new', FILTER_SANITIZE_FULL_SPECIAL_CHARS); + + if ($payment_type != PAYMENT_TYPE_UNASSIGNED && !empty($payment_amount_new)) { + $payment_amount = parse_decimals($payment_amount_new); + $cash_refund = 0; + if ($payment_type == lang('Sales.cash_adjustment')) { + $cash_adjustment = CASH_ADJUSTMENT_TRUE; + } else { + $cash_adjustment = CASH_ADJUSTMENT_FALSE; + $amount_tendered += $payment_amount; + $sale_info = $this->sale->get_info($sale_id)->getRowArray(); + + if ($amount_tendered > $sale_info['amount_due']) { + $cash_refund = $amount_tendered - $sale_info['amount_due']; + } + } + + $sale_data['payments'][] = [ + 'payment_id' => $payment_id, + 'payment_type' => $payment_type, + 'payment_amount' => $payment_amount, + 'cash_refund' => $cash_refund, + 'cash_adjustment' => $cash_adjustment, + 'employee_id' => $employee_id + ]; + } + + $inventory->update('POS ' . $sale_id, ['trans_date' => $sale_time]); // TODO: Reflection Exception + if ($this->sale->update($sale_id, $sale_data)) { + return $this->response->setJSON(['success' => true, 'message' => lang('Sales.successfully_updated'), 'id' => $sale_id]); + } else { + return $this->response->setJSON(['success' => false, 'message' => lang('Sales.unsuccessfully_updated'), 'id' => $sale_id]); + } + } + + /** + * This is used to cancel a suspended pos sale, quote. + * Completed sales (POS Sales or Invoiced Sales) can not be removed from the system + * Work orders can be canceled but are not physically removed from the sales history. + * Used in app/Views/sales/register.php + * + * @return ResponseInterface + * @throws ReflectionException + * @noinspection PhpUnused + */ + public function postCancel(): ResponseInterface|string + { + $sale_id = $this->sale_lib->get_sale_id(); + if ($sale_id != NEW_ENTRY && $sale_id != '') { + $sale_type = $this->sale_lib->get_sale_type(); + + if ($this->config['dinner_table_enable']) { + $dinner_table = $this->sale_lib->get_dinner_table(); + $this->dinner_table->release($dinner_table); + } + + if ($sale_type == SALE_TYPE_WORK_ORDER) { + $this->sale->update_sale_status($sale_id, CANCELED); + } else { + $this->sale->delete($sale_id); + $this->session->set('sale_id', NEW_ENTRY); + } + } else { + $this->sale_lib->remove_temp_items(); + } + + $this->sale_lib->clear_all(); + return $this->_reload(); + } + + /** + * Discards the suspended sale. Used in app/Views/sales/quote.php + * + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function getDiscardSuspendedSale(): ResponseInterface|string + { + $suspended_id = $this->sale_lib->get_suspended_id(); + $this->sale_lib->clear_all(); + $this->sale->delete_suspended_sale($suspended_id); + return $this->_reload(); + } + + /** + * Suspend the current sale. + * If the current sale is already suspended then update the existing suspended sale otherwise create + * it as a new suspended sale. Used in app/Views/sales/register.php + * + * @return ResponseInterface|string + * @noinspection PhpUnused + */ + public function postSuspend(): ResponseInterface|string + { + $sale_id = $this->sale_lib->get_sale_id(); + $dinner_table = $this->sale_lib->get_dinner_table(); + $cart = $this->sale_lib->get_cart(); + $payments = $this->sale_lib->get_payments(); + $employee_id = $this->employee->get_logged_in_employee_info()->person_id; + $customer_id = $this->sale_lib->get_customer(); + $invoice_number = $this->sale_lib->get_invoice_number(); + $work_order_number = $this->sale_lib->get_work_order_number(); + $quote_number = $this->sale_lib->get_quote_number(); + $sale_type = $this->sale_lib->get_sale_type(); + + if ($sale_type == '') { + $sale_type = SALE_TYPE_POS; + } + + $comment = $this->sale_lib->get_comment(); + $sale_status = SUSPENDED; + + $data = []; + $sales_taxes = [[], []]; + + if ($this->sale->save_value($sale_id, $sale_status, $cart, $customer_id, $employee_id, $comment, $invoice_number, $work_order_number, $quote_number, $sale_type, $payments, $dinner_table, $sales_taxes) == '-1') { + $data['error'] = lang('Sales.unsuccessfully_suspended_sale'); + } else { + $data['success'] = lang('Sales.successfully_suspended_sale'); + } + + $this->sale_lib->clear_all(); + + return $this->_reload($data); + } + + /** + * List suspended sales + * @return string + */ + public function getSuspended(): string + { + $data = []; + $customer_id = $this->sale_lib->get_customer(); + $data['suspended_sales'] = $this->sale->get_all_suspended($customer_id); + return view('sales/suspended', $data); + } + + /** + * Unsuspended sales are now left in the tables and are only removed + * when they are intentionally cancelled. Used in app/Views/sales/suspended.php. + * + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function postUnsuspend(): ResponseInterface|string + { + $sale_id = $this->request->getPost('suspended_sale_id', FILTER_SANITIZE_NUMBER_INT); + $this->sale_lib->clear_all(); + + if ($sale_id > 0) { + $this->sale_lib->copy_entire_sale($sale_id); + } + + // Set current register mode to reflect that of unsuspended order type + $this->change_register_mode($this->sale_lib->get_sale_type()); + + return $this->_reload(); + } + + /** + * Show Keyboard shortcut modal. Used in app/Views/sales/register.php + * + * @return string + * @noinspection PhpUnused + */ + public function getSalesKeyboardHelp(): string + { + return view('sales/help'); + } + + /** + * Check the validity of an invoice number. Used in app/Views/sales/form.php. + * + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function postCheckInvoiceNumber(): ResponseInterface + { + $sale_id = $this->request->getPost('sale_id', FILTER_SANITIZE_NUMBER_INT); + $invoice_number = $this->request->getPost('invoice_number', FILTER_SANITIZE_FULL_SPECIAL_CHARS); + $exists = !empty($invoice_number) && $this->sale->check_invoice_number_exists($invoice_number, $sale_id); + return $this->response->setJSON(!$exists ? 'true' : 'false'); + } + + /** + * @param array $cart + * @return array + */ + public function get_filtered(array $cart): array + { + $filtered_cart = []; + foreach ($cart as $id => $item) { + if ($item['print_option'] == PRINT_ALL) // Always include + { + $filtered_cart[$id] = $item; + } elseif ($item['print_option'] == PRINT_PRICED && $item['price'] != 0) // Include only if the price is not zero + { + $filtered_cart[$id] = $item; + } + // print_option 2 is never included + } + + return $filtered_cart; + } + + /** + * Update the item number in the register. Used in app/Views/sales/register.php + * + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function postChangeItemNumber(): ResponseInterface + { + $item_id = $this->request->getPost('item_id', FILTER_SANITIZE_NUMBER_INT); + $item_number = $this->request->getPost('item_number', FILTER_SANITIZE_FULL_SPECIAL_CHARS); + $this->item->update_item_number($item_id, $item_number); + $cart = $this->sale_lib->get_cart(); + $x = $this->search_cart_for_item_id($item_id, $cart); + if ($x !== null) { + $cart[$x]['item_number'] = $item_number; + } + $this->sale_lib->set_cart($cart); + return $this->response->setJSON(['success' => true]); + } + + /** + * Change a given item name. Used in app/Views/sales/register.php. + * + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function postChangeItemName(): ResponseInterface + { + $item_id = $this->request->getPost('item_id', FILTER_SANITIZE_NUMBER_INT); + $name = $this->request->getPost('item_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS); + + $this->item->update_item_name($item_id, $name); + + $cart = $this->sale_lib->get_cart(); + $x = $this->search_cart_for_item_id($item_id, $cart); + + if ($x !== null) { + $cart[$x]['name'] = $name; + } + + $this->sale_lib->set_cart($cart); + return $this->response->setJSON(['success' => true]); + } + + /** + * Update the given item description. Used in app/Views/sales/register.php + * + * @return ResponseInterface + * @noinspection PhpUnused + */ + public function postChangeItemDescription(): ResponseInterface + { + $item_id = $this->request->getPost('item_id', FILTER_SANITIZE_NUMBER_INT); + $description = $this->request->getPost('item_description', FILTER_SANITIZE_FULL_SPECIAL_CHARS); + + $this->item->update_item_description($item_id, $description); + + $cart = $this->sale_lib->get_cart(); + $x = $this->search_cart_for_item_id($item_id, $cart); + + if ($x !== null) { + $cart[$x]['description'] = $description; + } + + $this->sale_lib->set_cart($cart); + return $this->response->setJSON(['success' => true]); + } + + /** + * @param int $search_item_id + * @param array $shopping_cart + * @return int|string|null + */ + public function search_cart_for_item_id(int $search_item_id, array $shopping_cart): int|string|null + { + foreach ($shopping_cart as $key => $val) { + if ($val['item_id'] === $search_item_id) { + return $key; + } + } + + return null; + } +} + + diff --git a/app/Helpers/locale_helper.php b/app/Helpers/locale_helper.php index 3ae8f71f1..547d8d5fe 100644 --- a/app/Helpers/locale_helper.php +++ b/app/Helpers/locale_helper.php @@ -272,9 +272,6 @@ function get_payment_options(): array $payments[lang('Sales.upi')] = lang('Sales.upi'); } - $payments[lang('Sales.bank_transfer')] = lang('Sales.bank_transfer'); - $payments[lang('Sales.wallet')] = lang('Sales.wallet'); - return $payments; } @@ -368,6 +365,74 @@ function to_currency_no_money(?string $number): string return to_decimals($number, 'currency_decimals'); } +/** + * Build the secondary currency rendering context from app config values. + * + * @param array $config + * @return array{show:bool,rate:float,symbol:string,code:string,decimals:int} + */ +function secondary_currency_context(array $config): array +{ + $rate = (float) ($config['secondary_currency_rate'] ?? 0); + $symbol = trim((string) ($config['secondary_currency_symbol'] ?? '')); + $code = trim((string) ($config['secondary_currency_code'] ?? '')); + $decimals = (int) ($config['secondary_currency_decimals'] ?? ($config['currency_decimals'] ?? DEFAULT_PRECISION)); + + return [ + 'show' => (($config['secondary_currency_enabled'] ?? false) == 1) && $rate > 0, + 'rate' => $rate, + 'symbol' => $symbol, + 'code' => $code, + 'decimals' => $decimals, + ]; +} + +/** + * Render a value in the secondary currency. + * + * @param float|int|string|null $number + * @param array{show:bool,rate:float,symbol:string,code:string,decimals:int} $secondaryCurrency + * @return string + */ +function to_secondary_currency(float|int|string|null $number, array $secondaryCurrency): string +{ + if (!isset($number) || !$secondaryCurrency['show']) { + return ''; + } + + $config = config(OSPOS::class)->settings; + $amount = (float) $number * (float) $secondaryCurrency['rate']; + $fmt = new NumberFormatter($config['number_locale'], NumberFormatter::CURRENCY); + $fmt->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $secondaryCurrency['decimals']); + $fmt->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $secondaryCurrency['decimals']); + + if (empty($config['thousands_separator'])) { + $fmt->setTextAttribute(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, ''); + } + + $fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL, $secondaryCurrency['symbol'] !== '' ? $secondaryCurrency['symbol'] : ($secondaryCurrency['code'] !== '' ? $secondaryCurrency['code'] : '')); + + return $fmt->format($amount); +} + +/** + * Render the secondary and primary currency amounts together. + * + * @param float|int|string|null $number + * @param array{show:bool,rate:float,symbol:string,code:string,decimals:int} $secondaryCurrency + * @return string + */ +function to_secondary_currency_dual(float|int|string|null $number, array $secondaryCurrency): string +{ + $secondary = to_secondary_currency($number, $secondaryCurrency); + + if ($secondary === '') { + return to_currency((string) $number); + } + + return $secondary . '
' . to_currency((string) $number); +} + /** * @param string|null $number * @return string diff --git a/app/Language/en/Config.php b/app/Language/en/Config.php index 1d32112b9..bd7432292 100644 --- a/app/Language/en/Config.php +++ b/app/Language/en/Config.php @@ -1,336 +1,344 @@ - "Company Address", - "address_required" => "Company address is a required field.", - "all_set" => "All file permissions are set correctly!", - "allow_duplicate_barcodes" => "Allow Duplicate Barcodes", - "apostrophe" => "apostrophe", - "backup_button" => "Backup", - "backup_database" => "Backup Database", - "barcode" => "Barcode", - "barcode_company" => "Company Name", - "barcode_configuration" => "Barcode Configuration", - "barcode_content" => "Barcode Content", - "barcode_first_row" => "Row 1", - "barcode_font" => "Font", - "barcode_formats" => "Input Formats", - "barcode_generate_if_empty" => "Generate if empty.", - "barcode_height" => "Height (px)", - "barcode_id" => "Item Id/Name", - "barcode_info" => "Barcode Configuration Information", - "barcode_layout" => "Barcode Layout", - "barcode_name" => "Name", - "barcode_number" => "Barcode", - "barcode_number_in_row" => "Number in row", - "barcode_page_cellspacing" => "Display page cellspacing.", - "barcode_page_width" => "Display page width", - "barcode_price" => "Price", - "barcode_second_row" => "Row 2", - "barcode_third_row" => "Row 3", - "barcode_tooltip" => "Warning: This feature can cause duplicate items to be imported or created. Do not use if you do not want duplicate barcodes.", - "barcode_type" => "Barcode Type", - "barcode_width" => "Width (px)", - "bottom" => "Bottom", - "cash_button" => "", - "cash_button_1" => "", - "cash_button_2" => "", - "cash_button_3" => "", - "cash_button_4" => "", - "cash_button_5" => "", - "cash_button_6" => "", - "cash_decimals" => "Cash Decimals", - "cash_decimals_tooltip" => "If Cash Decimals and Currency Decimals are the same then no cash triggered rounding will take place, unless Cash Rounding is set to Half Five.", - "cash_rounding" => "Cash Rounding", - "category_dropdown" => "Show Category as a dropdown", - "center" => "Center", - "change_apperance_tooltip" => "", - "comma" => "comma", - "company" => "Company Name", - "company_avatar" => "", - "company_change_image" => "Change Image", - "company_logo" => "Company Logo", - "company_remove_image" => "Remove Image", - "company_required" => "Company name is a required field", - "company_select_image" => "Select Image", - "company_website_url" => "Company website is not a valid URL (http://...).", - "country_codes" => "Country Codes", - "country_codes_tooltip" => "Comma separated list of country codes for nominatim address lookup.", - "currency_code" => "Currency Code", - "currency_decimals" => "Currency Decimals", - "currency_symbol" => "Currency Symbol", - "current_employee_only" => "", - "customer_reward" => "Reward", - "customer_reward_duplicate" => "Reward must be unique.", - "customer_reward_enable" => "Enable Customer Rewards", - "customer_reward_invalid_chars" => "Reward can not contain '_'", - "customer_reward_required" => "Reward is a required field", - "customer_sales_tax_support" => "", - "date_or_time_format" => "Date and Time Filter", - "datetimeformat" => "Date and Time Format", - "decimal_point" => "Decimal Point", - "default_barcode_font_size_number" => "Default Barcode Font Size must be a number.", - "default_barcode_font_size_required" => "Default Barcode Font Size is a required field.", - "default_barcode_height_number" => "Default Barcode Height must be a number.", - "default_barcode_height_required" => "Default Barcode Height is a required field.", - "default_barcode_num_in_row_number" => "Default Barcode Number in Row must be a number.", - "default_barcode_num_in_row_required" => "Default Barcode Number in Row is a required field.", - "default_barcode_page_cellspacing_number" => "Default Barcode Page Cellspacing must be a number.", - "default_barcode_page_cellspacing_required" => "Default Barcode Page Cellspacing is a required field.", - "default_barcode_page_width_number" => "Default Barcode Page Width must be a number.", - "default_barcode_page_width_required" => "Default Barcode Page Width is a required field.", - "default_barcode_width_number" => "Default Barcode Width must be a number.", - "default_barcode_width_required" => "Default Barcode Width is a required field.", - "default_item_columns" => "Default Visible Item Columns", - "default_origin_tax_code" => "Default Origin Tax Code", - "default_receivings_discount" => "Default Receivings Discount", - "default_receivings_discount_number" => "Default Receivings Discount must be a number.", - "default_receivings_discount_required" => "Default Receivings Discount is a required field.", - "default_sales_discount" => "Default Sales Discount", - "default_sales_discount_number" => "Default Sales Discount must be a number.", - "default_sales_discount_required" => "Default Sales Discount is a required field.", - "default_tax_category" => "Default Tax Category", - "default_tax_code" => "Default Tax Code", - "default_tax_jurisdiction" => "Default Tax Jurisdiction", - "default_tax_name_number" => "Default Tax Name must be a string.", - "default_tax_name_required" => "Default Tax Name is a required field.", - "default_tax_rate" => "Default Tax Rate %", - "default_tax_rate_1" => "Tax 1 Rate", - "default_tax_rate_2" => "Tax 2 Rate", - "default_tax_rate_3" => "", - "default_tax_rate_number" => "Default Tax Rate must be a number.", - "default_tax_rate_required" => "Default Tax Rate is a required field.", - "derive_sale_quantity" => "Allow Derived Sale Quantity", - "derive_sale_quantity_tooltip" => "If checked then a new item type will be provided for items ordered by extended amount", - "dinner_table" => "Table", - "dinner_table_duplicate" => "Table must be unique.", - "dinner_table_enable" => "Enable Dinner Tables", - "dinner_table_invalid_chars" => "Table Name can not contain '_'.", - "dinner_table_required" => "Table is a required field.", - "dot" => "dot", - "email" => "Email", - "email_configuration" => "Email Configuration", - "email_mailpath" => "Path to Sendmail", - "email_protocol" => "Protocol", - "email_receipt_check_behaviour" => "Email Receipt checkbox", - "email_receipt_check_behaviour_always" => "Always checked", - "email_receipt_check_behaviour_last" => "Remember last selection", - "email_receipt_check_behaviour_never" => "Always unchecked", - "email_smtp_crypto" => "SMTP Encryption", - "email_smtp_host" => "SMTP Server", - "email_smtp_pass" => "SMTP Password", - "email_smtp_port" => "SMTP Port", - "email_smtp_timeout" => "SMTP Timeout (s)", - "email_smtp_user" => "SMTP Username", - "enable_avatar" => "", - "enable_avatar_tooltip" => "", - "enable_dropdown_tooltip" => "", - "enable_new_look" => "", - "enable_right_bar" => "", - "enable_right_bar_tooltip" => "", - "enforce_privacy" => "Enforce privacy", - "enforce_privacy_tooltip" => "Protect Customers privacy enforcing data scrambling in case of their data being deleted", - "fax" => "Fax", - "file_perm" => "There are problems with file permissions. Please fix and reload this page.", - "financial_year" => "Fiscal Year Start", - "financial_year_apr" => "1st of April", - "financial_year_aug" => "1st of August", - "financial_year_dec" => "1st of December", - "financial_year_feb" => "1st of February", - "financial_year_jan" => "1st of January", - "financial_year_jul" => "1st of July", - "financial_year_jun" => "1st of June", - "financial_year_mar" => "1st of March", - "financial_year_may" => "1st of May", - "financial_year_nov" => "1st of November", - "financial_year_oct" => "1st of October", - "financial_year_sep" => "1st of September", - "floating_labels" => "Floating Labels", - "gcaptcha_enable" => "Login Page reCAPTCHA", - "gcaptcha_secret_key" => "reCAPTCHA Secret Key", - "gcaptcha_secret_key_required" => "reCAPTCHA Secret Key is a required field", - "gcaptcha_site_key" => "reCAPTCHA Site Key", - "gcaptcha_site_key_required" => "reCAPTCHA Site Key is a required field", - "gcaptcha_tooltip" => "Protect the Login page with Google reCAPTCHA, click the icon for an API key pair.", - "general" => "General", - "general_configuration" => "General Configuration", - "giftcard_number" => "Gift Card Number", - "giftcard_random" => "Generate Random", - "giftcard_series" => "Generate in Series", - "image_allowed_file_types" => "Allowed file types", - "image_max_height_tooltip" => "Maximum allowed height of image uploads in pixels (px).", - "image_max_size_tooltip" => "Maximum allowed file size of image uploads in kilobytes (kb).", - "image_max_width_tooltip" => "Maximum allowed width of image uploads in pixels (px).", - "image_restrictions" => "Image Upload Restrictions", - "include_hsn" => "Include Support for HSN Codes", - "info" => "Information", - "info_configuration" => "Store Information", - "input_groups" => "Input Groups", - "integrations" => "Integrations", - "integrations_configuration" => "Third Party Integrations", - "invoice" => "Invoice", - "invoice_configuration" => "Invoice Print Settings", - "invoice_default_comments" => "Default Invoice Comments", - "invoice_email_message" => "Invoice Email Template", - "invoice_enable" => "Enable Invoicing", - "invoice_printer" => "Invoice Printer", - "invoice_type" => "Invoice Type", - "is_readable" => "is readable, but the permissions are incorrectly set. Please set it to 640 or 660 and refresh.", - "is_writable" => "is writable, but the permissions are incorrectly set. Please set it to 750 and refresh.", - "item_markup" => "", - "jsprintsetup_required" => "Warning: This functionality will only work if you have the FireFox jsPrintSetup addon installed. Save anyway?", - "language" => "Language", - "last_used_invoice_number" => "Last used Invoice Number", - "last_used_quote_number" => "Last used Quote Number", - "last_used_work_order_number" => "Last used W/O Number", - "left" => "Left", - "license" => "License", - "license_configuration" => "License Statement", - "line_sequence" => "Line Sequence", - "lines_per_page" => "Lines per Page", - "lines_per_page_number" => "Lines per Page must be a number.", - "lines_per_page_required" => "Lines per Page is a required field.", - "locale" => "Localization", - "locale_configuration" => "Localization Configuration", - "locale_info" => "Localization Configuration Information", - "location" => "Stock", - "location_configuration" => "Stock Locations", - "location_info" => "Location Configuration Information", - "login_form" => "Login Form Style", - "logout" => "Do you want to make a backup before logging out? Click [OK] to backup or [Cancel] to logout.", - "mailchimp" => "MailChimp", - "mailchimp_api_key" => "MailChimp API Key", - "mailchimp_configuration" => "MailChimp Configuration", - "mailchimp_key_successfully" => "API Key is valid.", - "mailchimp_key_unsuccessfully" => "API Key is invalid.", - "mailchimp_lists" => "MailChimp List(s)", - "mailchimp_tooltip" => "Click the icon for an API Key.", - "message" => "Message", - "message_configuration" => "Message Configuration", - "msg_msg" => "Saved Text Message", - "msg_msg_placeholder" => "If you wish to use a SMS template save your message here, otherwise leave the box blank.", - "msg_pwd" => "SMS-API Password", - "msg_pwd_required" => "SMS-API Password is a required field", - "msg_src" => "SMS-API Sender ID", - "msg_src_required" => "SMS-API Sender ID is a required field", - "msg_uid" => "SMS-API Username", - "msg_uid_required" => "SMS-API Username is a required field", - "multi_pack_enabled" => "Multiple Packages per Item", - "no_risk" => "No security/vulnerability risks.", - "none" => "none", - "notify_alignment" => "Notification Popup Position", - "number_format" => "Number Format", - "number_locale" => "Localization", - "number_locale_invalid" => "The entered locale is invalid. Check the link in the tooltip to find a valid locale.", - "number_locale_required" => "Number Locale is a required field.", - "number_locale_tooltip" => "Find a suitable locale through this link.", - "os_timezone" => "OSPOS Timezone:", - "ospos_info" => "OSPOS Installation Info", - "payment_options_order" => "Payment Options Order", - "perm_risk" => "Incorrect permissions leaves this software at risk.", - "phone" => "Company Phone", - "phone_required" => "Company Phone is a required field.", - "print_bottom_margin" => "Margin Bottom", - "print_bottom_margin_number" => "Margin Bottom must be a number.", - "print_bottom_margin_required" => "Margin Bottom is a required field.", - "print_delay_autoreturn" => "Autoreturn to Sale delay", - "print_delay_autoreturn_number" => "Autoreturn to Sale delay is a required field.", - "print_delay_autoreturn_required" => "Autoreturn to Sale delay must be a number.", - "print_footer" => "Print Browser Footer", - "print_header" => "Print Browser Header", - "print_left_margin" => "Margin Left", - "print_left_margin_number" => "Margin Left must be a number.", - "print_left_margin_required" => "Margin Left is a required field.", - "print_receipt_check_behaviour" => "Print Receipt checkbox", - "print_receipt_check_behaviour_always" => "Always checked", - "print_receipt_check_behaviour_last" => "Remember last selection", - "print_receipt_check_behaviour_never" => "Always unchecked", - "print_right_margin" => "Margin Right", - "print_right_margin_number" => "Margin Right must be a number.", - "print_right_margin_required" => "Margin Right is a required field.", - "print_silently" => "Show Print Dialog", - "print_top_margin" => "Margin Top", - "print_top_margin_number" => "Margin Top must be a number.", - "print_top_margin_required" => "Margin Top is a required field.", - "quantity_decimals" => "Quantity Decimals", - "quick_cash_enable" => "", - "quote_default_comments" => "Default Quote Comments", - "receipt" => "Receipt", - "receipt_category" => "", - "receipt_configuration" => "Receipt Print Settings", - "receipt_default" => "Default", - "receipt_font_size" => "Font Size", - "receipt_font_size_number" => "Font Size must be a number.", - "receipt_font_size_required" => "Font Size is a required field.", - "receipt_info" => "Receipt Configuration Information", - "receipt_printer" => "Ticket Printer", - "receipt_short" => "Short", + "Company Address", + "address_required" => "Company address is a required field.", + "all_set" => "All file permissions are set correctly!", + "allow_duplicate_barcodes" => "Allow Duplicate Barcodes", + "apostrophe" => "apostrophe", + "backup_button" => "Backup", + "backup_database" => "Backup Database", + "barcode" => "Barcode", + "barcode_company" => "Company Name", + "barcode_configuration" => "Barcode Configuration", + "barcode_content" => "Barcode Content", + "barcode_first_row" => "Row 1", + "barcode_font" => "Font", + "barcode_formats" => "Input Formats", + "barcode_generate_if_empty" => "Generate if empty.", + "barcode_height" => "Height (px)", + "barcode_id" => "Item Id/Name", + "barcode_info" => "Barcode Configuration Information", + "barcode_layout" => "Barcode Layout", + "barcode_name" => "Name", + "barcode_number" => "Barcode", + "barcode_number_in_row" => "Number in row", + "barcode_page_cellspacing" => "Display page cellspacing.", + "barcode_page_width" => "Display page width", + "barcode_price" => "Price", + "barcode_second_row" => "Row 2", + "barcode_third_row" => "Row 3", + "barcode_tooltip" => "Warning: This feature can cause duplicate items to be imported or created. Do not use if you do not want duplicate barcodes.", + "barcode_type" => "Barcode Type", + "barcode_width" => "Width (px)", + "bottom" => "Bottom", + "cash_button" => "", + "cash_button_1" => "", + "cash_button_2" => "", + "cash_button_3" => "", + "cash_button_4" => "", + "cash_button_5" => "", + "cash_button_6" => "", + "cash_decimals" => "Cash Decimals", + "cash_decimals_tooltip" => "If Cash Decimals and Currency Decimals are the same then no cash triggered rounding will take place, unless Cash Rounding is set to Half Five.", + "cash_rounding" => "Cash Rounding", + "category_dropdown" => "Show Category as a dropdown", + "center" => "Center", + "change_apperance_tooltip" => "", + "comma" => "comma", + "company" => "Company Name", + "company_avatar" => "", + "company_change_image" => "Change Image", + "company_logo" => "Company Logo", + "company_remove_image" => "Remove Image", + "company_required" => "Company name is a required field", + "company_select_image" => "Select Image", + "company_website_url" => "Company website is not a valid URL (http://...).", + "country_codes" => "Country Codes", + "country_codes_tooltip" => "Comma separated list of country codes for nominatim address lookup.", + "currency_code" => "Currency Code", + "currency_decimals" => "Currency Decimals", + "currency_symbol" => "Currency Symbol", + "current_employee_only" => "", + "customer_reward" => "Reward", + "customer_reward_duplicate" => "Reward must be unique.", + "customer_reward_enable" => "Enable Customer Rewards", + "customer_reward_invalid_chars" => "Reward can not contain '_'", + "customer_reward_required" => "Reward is a required field", + "customer_sales_tax_support" => "", + "date_or_time_format" => "Date and Time Filter", + "datetimeformat" => "Date and Time Format", + "decimal_point" => "Decimal Point", + "default_barcode_font_size_number" => "Default Barcode Font Size must be a number.", + "default_barcode_font_size_required" => "Default Barcode Font Size is a required field.", + "default_barcode_height_number" => "Default Barcode Height must be a number.", + "default_barcode_height_required" => "Default Barcode Height is a required field.", + "default_barcode_num_in_row_number" => "Default Barcode Number in Row must be a number.", + "default_barcode_num_in_row_required" => "Default Barcode Number in Row is a required field.", + "default_barcode_page_cellspacing_number" => "Default Barcode Page Cellspacing must be a number.", + "default_barcode_page_cellspacing_required" => "Default Barcode Page Cellspacing is a required field.", + "default_barcode_page_width_number" => "Default Barcode Page Width must be a number.", + "default_barcode_page_width_required" => "Default Barcode Page Width is a required field.", + "default_barcode_width_number" => "Default Barcode Width must be a number.", + "default_barcode_width_required" => "Default Barcode Width is a required field.", + "default_item_columns" => "Default Visible Item Columns", + "default_origin_tax_code" => "Default Origin Tax Code", + "default_receivings_discount" => "Default Receivings Discount", + "default_receivings_discount_number" => "Default Receivings Discount must be a number.", + "default_receivings_discount_required" => "Default Receivings Discount is a required field.", + "default_sales_discount" => "Default Sales Discount", + "default_sales_discount_number" => "Default Sales Discount must be a number.", + "default_sales_discount_required" => "Default Sales Discount is a required field.", + "default_tax_category" => "Default Tax Category", + "default_tax_code" => "Default Tax Code", + "default_tax_jurisdiction" => "Default Tax Jurisdiction", + "default_tax_name_number" => "Default Tax Name must be a string.", + "default_tax_name_required" => "Default Tax Name is a required field.", + "default_tax_rate" => "Default Tax Rate %", + "default_tax_rate_1" => "Tax 1 Rate", + "default_tax_rate_2" => "Tax 2 Rate", + "default_tax_rate_3" => "", + "default_tax_rate_number" => "Default Tax Rate must be a number.", + "default_tax_rate_required" => "Default Tax Rate is a required field.", + "derive_sale_quantity" => "Allow Derived Sale Quantity", + "derive_sale_quantity_tooltip" => "If checked then a new item type will be provided for items ordered by extended amount", + "dinner_table" => "Table", + "dinner_table_duplicate" => "Table must be unique.", + "dinner_table_enable" => "Enable Dinner Tables", + "dinner_table_invalid_chars" => "Table Name can not contain '_'.", + "dinner_table_required" => "Table is a required field.", + "dot" => "dot", + "email" => "Email", + "email_configuration" => "Email Configuration", + "email_mailpath" => "Path to Sendmail", + "email_protocol" => "Protocol", + "email_receipt_check_behaviour" => "Email Receipt checkbox", + "email_receipt_check_behaviour_always" => "Always checked", + "email_receipt_check_behaviour_last" => "Remember last selection", + "email_receipt_check_behaviour_never" => "Always unchecked", + "email_smtp_crypto" => "SMTP Encryption", + "email_smtp_host" => "SMTP Server", + "email_smtp_pass" => "SMTP Password", + "email_smtp_port" => "SMTP Port", + "email_smtp_timeout" => "SMTP Timeout (s)", + "email_smtp_user" => "SMTP Username", + "enable_avatar" => "", + "enable_avatar_tooltip" => "", + "enable_dropdown_tooltip" => "", + "enable_new_look" => "", + "enable_right_bar" => "", + "enable_right_bar_tooltip" => "", + "enforce_privacy" => "Enforce privacy", + "enforce_privacy_tooltip" => "Protect Customers privacy enforcing data scrambling in case of their data being deleted", + "fax" => "Fax", + "file_perm" => "There are problems with file permissions. Please fix and reload this page.", + "financial_year" => "Fiscal Year Start", + "financial_year_apr" => "1st of April", + "financial_year_aug" => "1st of August", + "financial_year_dec" => "1st of December", + "financial_year_feb" => "1st of February", + "financial_year_jan" => "1st of January", + "financial_year_jul" => "1st of July", + "financial_year_jun" => "1st of June", + "financial_year_mar" => "1st of March", + "financial_year_may" => "1st of May", + "financial_year_nov" => "1st of November", + "financial_year_oct" => "1st of October", + "financial_year_sep" => "1st of September", + "floating_labels" => "Floating Labels", + "gcaptcha_enable" => "Login Page reCAPTCHA", + "gcaptcha_secret_key" => "reCAPTCHA Secret Key", + "gcaptcha_secret_key_required" => "reCAPTCHA Secret Key is a required field", + "gcaptcha_site_key" => "reCAPTCHA Site Key", + "gcaptcha_site_key_required" => "reCAPTCHA Site Key is a required field", + "gcaptcha_tooltip" => "Protect the Login page with Google reCAPTCHA, click the icon for an API key pair.", + "general" => "General", + "general_configuration" => "General Configuration", + "giftcard_number" => "Gift Card Number", + "giftcard_random" => "Generate Random", + "giftcard_series" => "Generate in Series", + "image_allowed_file_types" => "Allowed file types", + "image_max_height_tooltip" => "Maximum allowed height of image uploads in pixels (px).", + "image_max_size_tooltip" => "Maximum allowed file size of image uploads in kilobytes (kb).", + "image_max_width_tooltip" => "Maximum allowed width of image uploads in pixels (px).", + "image_restrictions" => "Image Upload Restrictions", + "include_hsn" => "Include Support for HSN Codes", + "info" => "Information", + "info_configuration" => "Store Information", + "input_groups" => "Input Groups", + "integrations" => "Integrations", + "integrations_configuration" => "Third Party Integrations", + "invoice" => "Invoice", + "invoice_configuration" => "Invoice Print Settings", + "invoice_default_comments" => "Default Invoice Comments", + "invoice_email_message" => "Invoice Email Template", + "invoice_enable" => "Enable Invoicing", + "invoice_printer" => "Invoice Printer", + "invoice_type" => "Invoice Type", + "is_readable" => "is readable, but the permissions are incorrectly set. Please set it to 640 or 660 and refresh.", + "is_writable" => "is writable, but the permissions are incorrectly set. Please set it to 750 and refresh.", + "item_markup" => "", + "jsprintsetup_required" => "Warning: This functionality will only work if you have the FireFox jsPrintSetup addon installed. Save anyway?", + "language" => "Language", + "last_used_invoice_number" => "Last used Invoice Number", + "last_used_quote_number" => "Last used Quote Number", + "last_used_work_order_number" => "Last used W/O Number", + "left" => "Left", + "license" => "License", + "license_configuration" => "License Statement", + "line_sequence" => "Line Sequence", + "lines_per_page" => "Lines per Page", + "lines_per_page_number" => "Lines per Page must be a number.", + "lines_per_page_required" => "Lines per Page is a required field.", + "locale" => "Localization", + "locale_configuration" => "Localization Configuration", + "locale_info" => "Localization Configuration Information", + "location" => "Stock", + "location_configuration" => "Stock Locations", + "location_info" => "Location Configuration Information", + "login_form" => "Login Form Style", + "logout" => "Do you want to make a backup before logging out? Click [OK] to backup or [Cancel] to logout.", + "mailchimp" => "MailChimp", + "mailchimp_api_key" => "MailChimp API Key", + "mailchimp_configuration" => "MailChimp Configuration", + "mailchimp_key_successfully" => "API Key is valid.", + "mailchimp_key_unsuccessfully" => "API Key is invalid.", + "mailchimp_lists" => "MailChimp List(s)", + "mailchimp_tooltip" => "Click the icon for an API Key.", + "message" => "Message", + "message_configuration" => "Message Configuration", + "msg_msg" => "Saved Text Message", + "msg_msg_placeholder" => "If you wish to use a SMS template save your message here, otherwise leave the box blank.", + "msg_pwd" => "SMS-API Password", + "msg_pwd_required" => "SMS-API Password is a required field", + "msg_src" => "SMS-API Sender ID", + "msg_src_required" => "SMS-API Sender ID is a required field", + "msg_uid" => "SMS-API Username", + "msg_uid_required" => "SMS-API Username is a required field", + "multi_pack_enabled" => "Multiple Packages per Item", + "no_risk" => "No security/vulnerability risks.", + "none" => "none", + "notify_alignment" => "Notification Popup Position", + "number_format" => "Number Format", + "number_locale" => "Localization", + "number_locale_invalid" => "The entered locale is invalid. Check the link in the tooltip to find a valid locale.", + "number_locale_required" => "Number Locale is a required field.", + "number_locale_tooltip" => "Find a suitable locale through this link.", + "os_timezone" => "OSPOS Timezone:", + "ospos_info" => "OSPOS Installation Info", + "payment_options_order" => "Payment Options Order", + "perm_risk" => "Incorrect permissions leaves this software at risk.", + "phone" => "Company Phone", + "phone_required" => "Company Phone is a required field.", + "print_bottom_margin" => "Margin Bottom", + "print_bottom_margin_number" => "Margin Bottom must be a number.", + "print_bottom_margin_required" => "Margin Bottom is a required field.", + "print_delay_autoreturn" => "Autoreturn to Sale delay", + "print_delay_autoreturn_number" => "Autoreturn to Sale delay is a required field.", + "print_delay_autoreturn_required" => "Autoreturn to Sale delay must be a number.", + "print_footer" => "Print Browser Footer", + "print_header" => "Print Browser Header", + "print_left_margin" => "Margin Left", + "print_left_margin_number" => "Margin Left must be a number.", + "print_left_margin_required" => "Margin Left is a required field.", + "print_receipt_check_behaviour" => "Print Receipt checkbox", + "print_receipt_check_behaviour_always" => "Always checked", + "print_receipt_check_behaviour_last" => "Remember last selection", + "print_receipt_check_behaviour_never" => "Always unchecked", + "print_right_margin" => "Margin Right", + "print_right_margin_number" => "Margin Right must be a number.", + "print_right_margin_required" => "Margin Right is a required field.", + "print_silently" => "Show Print Dialog", + "print_top_margin" => "Margin Top", + "print_top_margin_number" => "Margin Top must be a number.", + "print_top_margin_required" => "Margin Top is a required field.", + "quantity_decimals" => "Quantity Decimals", + "quick_cash_enable" => "", + "quote_default_comments" => "Default Quote Comments", + "receipt" => "Receipt", + "receipt_category" => "", + "receipt_configuration" => "Receipt Print Settings", + "receipt_default" => "Default", + "receipt_font_size" => "Font Size", + "receipt_font_size_number" => "Font Size must be a number.", + "receipt_font_size_required" => "Font Size is a required field.", + "receipt_info" => "Receipt Configuration Information", + "receipt_printer" => "Ticket Printer", + "receipt_short" => "Short", "receipt_show_company_name" => "Show Company Name", "receipt_show_description" => "Show Description", "receipt_show_serialnumber" => "Show Serial Number", + "receipt_show_secondary_currency" => "Show Secondary Currency", "receipt_show_tax_ind" => "Show Tax Indicator", "receipt_show_taxes" => "Show Taxes", "receipt_show_total_discount" => "Show Total Discount", "receipt_template" => "Receipt Template", + "secondary_currency" => "Secondary Currency", + "secondary_currency_decimals" => "Secondary Currency Decimals", + "secondary_currency_code" => "Secondary Currency Code", + "secondary_currency_enable" => "Enable Secondary Currency", + "secondary_currency_enable_tooltip" => "Show secondary currency fields and print/display values across the app.", + "secondary_currency_rate" => "Secondary Currency Rate", + "secondary_currency_settings" => "Secondary Currency Settings", + "secondary_currency_symbol" => "Secondary Currency Symbol", "receiving_calculate_average_price" => "Calc avg. Price (Receiving)", - "recv_invoice_format" => "Receivings Invoice Format", - "register_mode_default" => "Default Register Mode", - "report_an_issue" => "Report an issue", - "return_policy_required" => "Return policy is a required field.", - "reward" => "Reward", - "reward_configuration" => "Reward Configuration", - "right" => "Right", - "sales_invoice_format" => "Sales Invoice Format", - "sales_quote_format" => "Sales Quote Format", - "mailpath_invalid" => "Invalid sendmail path. Only letters, numbers, dashes, underscores, slashes and dots are allowed.", - "saved_successfully" => "Configuration save successful.", - "saved_unsuccessfully" => "Configuration save failed.", - "security_issue" => "Security Vulnerability Warning", + "recv_invoice_format" => "Receivings Invoice Format", + "register_mode_default" => "Default Register Mode", + "report_an_issue" => "Report an issue", + "return_policy_required" => "Return policy is a required field.", + "reward" => "Reward", + "reward_configuration" => "Reward Configuration", + "right" => "Right", + "sales_invoice_format" => "Sales Invoice Format", + "sales_quote_format" => "Sales Quote Format", + "mailpath_invalid" => "Invalid sendmail path. Only letters, numbers, dashes, underscores, slashes and dots are allowed.", + "saved_successfully" => "Configuration save successful.", + "saved_unsuccessfully" => "Configuration save failed.", + "security_issue" => "Security Vulnerability Warning", "server_notice" => "Please use the below info for issue reporting.", "service_charge" => "", + "customer_display" => "Customer Display", "show_due_enable" => "", - "show_office_group" => "Show office icon", - "statistics" => "Send Statistics", - "statistics_tooltip" => "Send statistics for development and feature improvement purposes.", - "stock_location" => "Stock location", - "stock_location_duplicate" => "Stock Location must be unique.", - "stock_location_invalid_chars" => "Stock Location can not contain '_'.", - "stock_location_required" => "Stock location is a required field.", - "suggestions_fifth_column" => "", - "suggestions_first_column" => "Column 1", - "suggestions_fourth_column" => "", - "suggestions_layout" => "Search Suggestions Layout", - "suggestions_second_column" => "Column 2", - "suggestions_third_column" => "Column 3", - "shortcuts" => "Shortcuts", - "shortcuts_configuration" => "Sales Keyboard Shortcut Configuration", - "shortcuts_duplicate_bindings" => "Shortcut bindings must be unique.", - "shortcuts_save_error" => "Unable to save shortcut settings.", - "system_conf" => "Setup & Conf", - "system_info" => "System Info", - "table" => "Table", - "table_configuration" => "Table Configuration", - "takings_printer" => "Receipt Printer", - "tax" => "Tax", - "tax_category" => "Tax Category", - "tax_category_duplicate" => "The entered tax category already exists.", - "tax_category_invalid_chars" => "The entered tax category is invalid.", - "tax_category_required" => "The tax category is required.", - "tax_category_used" => "Tax category cannot be deleted because it is being used.", - "tax_configuration" => "Tax Configuration", - "tax_decimals" => "Tax Decimals", - "tax_id" => "Tax Id", - "tax_included" => "Tax Included", - "theme" => "Theme", - "theme_preview" => "Preview Theme:", - "thousands_separator" => "Thousands Separator", - "timezone" => "Timezone", - "timezone_error" => "OSPOS Timezone is Different from your Local Timezone.", - "top" => "Top", - "use_destination_based_tax" => "Use Destination Based Tax", - "user_timezone" => "Local Timezone:", - "website" => "Website", - "wholesale_markup" => "", - "work_order_enable" => "Work Order Support", - "work_order_format" => "Work Order Format", -]; + "show_office_group" => "Show office icon", + "statistics" => "Send Statistics", + "statistics_tooltip" => "Send statistics for development and feature improvement purposes.", + "stock_location" => "Stock location", + "stock_location_duplicate" => "Stock Location must be unique.", + "stock_location_invalid_chars" => "Stock Location can not contain '_'.", + "stock_location_required" => "Stock location is a required field.", + "suggestions_fifth_column" => "", + "suggestions_first_column" => "Column 1", + "suggestions_fourth_column" => "", + "suggestions_layout" => "Search Suggestions Layout", + "suggestions_second_column" => "Column 2", + "suggestions_third_column" => "Column 3", + "system_conf" => "Setup & Conf", + "system_info" => "System Info", + "table" => "Table", + "table_configuration" => "Table Configuration", + "takings_printer" => "Receipt Printer", + "tax" => "Tax", + "tax_category" => "Tax Category", + "tax_category_duplicate" => "The entered tax category already exists.", + "tax_category_invalid_chars" => "The entered tax category is invalid.", + "tax_category_required" => "The tax category is required.", + "tax_category_used" => "Tax category cannot be deleted because it is being used.", + "tax_configuration" => "Tax Configuration", + "tax_decimals" => "Tax Decimals", + "tax_id" => "Tax Id", + "tax_included" => "Tax Included", + "theme" => "Theme", + "theme_preview" => "Preview Theme:", + "thousands_separator" => "Thousands Separator", + "timezone" => "Timezone", + "timezone_error" => "OSPOS Timezone is Different from your Local Timezone.", + "top" => "Top", + "use_destination_based_tax" => "Use Destination Based Tax", + "user_timezone" => "Local Timezone:", + "website" => "Website", + "wholesale_markup" => "", + "work_order_enable" => "Work Order Support", + "work_order_format" => "Work Order Format", +]; + + diff --git a/app/Language/en/Sales.php b/app/Language/en/Sales.php index a1524e7fa..bbec3fb90 100644 --- a/app/Language/en/Sales.php +++ b/app/Language/en/Sales.php @@ -7,9 +7,9 @@ return [ "account_number" => "Account #", "add_payment" => "Add Payment", "amount_due" => "Amount Due", + "amount_due_lbp" => "Amount Due LBP", "amount_tendered" => "Amount Tendered", "authorized_signature" => "Authorized Signature", - "bank_transfer" => "Bank Transfer", "cancel_sale" => "Cancel", "cash" => "Cash", "cash_1" => "", @@ -20,6 +20,8 @@ return [ "cash_deposit" => "Cash Deposit", "cash_filter" => "Cash", "change_due" => "Change Due", + "change" => "Change", + "currency_rate" => "Currency Rate", "change_price" => "Change Selling Price", "check" => "Check", "check_balance" => "Check remainder", @@ -41,6 +43,7 @@ return [ "customer_address" => "Address", "customer_discount" => "Discount", "customer_email" => "Email", + "customer_name" => "Customer Name", "customer_location" => "Location", "customer_mailchimp_status" => "MailChimp Status", "customer_optional" => "(Required for Due Payments)", @@ -74,12 +77,6 @@ return [ "employee" => "Employee", "entry" => "Entry", "error_editing_item" => "Error editing item", - "negative_price_invalid" => "Price cannot be negative.", - "negative_quantity_invalid" => "Quantity cannot be negative.", - "negative_discount_invalid" => "Discount cannot be negative.", - "discount_percent_exceeds_100" => "Percentage discount cannot exceed 100%.", - "discount_exceeds_item_total" => "Discount cannot exceed the item total.", - "negative_total_invalid" => "Sale total cannot be negative. Check item discounts and quantities.", "find_or_scan_item" => "Find or Scan Item", "find_or_scan_item_or_receipt" => "Find or Scan Item or Receipt", "giftcard" => "Gift Card", @@ -110,6 +107,7 @@ return [ "item_name" => "Item Name", "item_number" => "Item #", "item_out_of_stock" => "Item is out of stock.", + "items" => "Items", "key_browser" => "Helpful Shortcuts", "key_cancel" => "Cancels Current Quote/Invoice/Sale", "key_customer_search" => "Customer Search", @@ -151,7 +149,9 @@ return [ "payment_type" => "Type", "payments" => "", "payments_total" => "Payments Total", + "loyalty_reward_points" => "Loyalty Reward Points", "price" => "Price", + "price_with_currency" => "Price (%s)", "print_after_sale" => "Print after Sale", "quantity" => "Quantity", "quantity_less_than_reorder_level" => "Warning: Desired Quantity is below Reorder Level for that Item.", @@ -167,10 +167,13 @@ return [ "receipt_number" => "Sale #", "receipt_sent" => "Receipt sent to", "receipt_unsent" => "Receipt failed to be sent to", + "rate" => "Rate", "refund" => "Refund Type", "register" => "Sales Register", "remove_customer" => "Remove Customer", "remove_discount" => "", + "customer_display" => "Customer Display", + "summary" => "Summary", "return" => "Return", "rewards" => "Reward Points", "rewards_balance" => "Reward Points Balance", @@ -182,6 +185,7 @@ return [ "sales_total" => "", "select_customer" => "Select Customer", "selected_customer" => "Selected Customer", + "walk_in_customer" => "Walk-in Customer", "send_invoice" => "Send Invoice", "send_quote" => "Send Quote", "send_receipt" => "Send Receipt", @@ -212,6 +216,7 @@ return [ "tax_percent" => "Tax %", "taxed_ind" => "T", "total" => "Total", + "total_lbp" => "Total LBP", "total_tax_exclusive" => "Tax excluded", "transaction_failed" => "Sales Transaction failed.", "unable_to_add_item" => "Item add to Sale failed", @@ -224,7 +229,6 @@ return [ "update" => "Update", "upi" => "UPI", "visa" => "", - "wallet" => "Wallet", "wholesale" => "", "work_order" => "Work Order", "work_order_number" => "Work Order Number", @@ -232,3 +236,5 @@ return [ "work_order_sent" => "Work Order sent to", "work_order_unsent" => "Work Order failed to be sent to", ]; + + diff --git a/app/Views/configs/general_config.php b/app/Views/configs/general_config.php index b9c9f71da..815e070e2 100644 --- a/app/Views/configs/general_config.php +++ b/app/Views/configs/general_config.php @@ -1,407 +1,422 @@ - - - 'general_config_form', 'enctype' => 'multipart/form-data', 'class' => 'form-horizontal']) ?> -
-
- -
- - + + + 'general_config_form', 'enctype' => 'multipart/form-data', 'class' => 'form-horizontal']) ?> +
+
+ +
+
    + +
    + 'control-label col-xs-2']) ?> +
    +
    +
    + +
    + +
    +
    +
    + +
    + 'control-label col-xs-2']) ?> +
    + lang('Config.floating_labels'), + 'input_groups' => lang('Config.input_groups') + ], + $config['login_form'], + 'class="form-control input-sm"' + ) ?> +
    +
    + +
    + 'control-label col-xs-2 required']) ?> +
    +
    + 'default_sales_discount', + 'id' => 'default_sales_discount', + 'class' => 'form-control input-sm required', + 'type' => 'number', + 'min' => 0, + 'max' => 100, + 'value' => $config['default_sales_discount'] + ]) ?> + + 'default_sales_discount_type', + 'name' => 'default_sales_discount_type', + 'value' => 1, + 'data-toggle' => 'toggle', + 'data-size' => 'normal', + 'data-onstyle' => 'success', + 'data-on' => '' . $config['currency_symbol'] . '', + 'data-off' => '%', + 'checked' => $config['default_sales_discount_type'] == 1 + ]) ?> + +
    +
    +
    + +
    + 'control-label col-xs-2 required']) ?> +
    +
    + 'default_receivings_discount', + 'id' => 'default_receivings_discount', + 'class' => 'form-control input-sm required', + 'type' => 'number', + 'min' => 0, + 'max' => 100, + 'value' => $config['default_receivings_discount'] + ]) ?> + + 'default_receivings_discount_type', + 'name' => 'default_receivings_discount_type', + 'value' => 1, + 'data-toggle' => 'toggle', + 'data-size' => 'normal', + 'data-onstyle' => 'success', + 'data-on' => '' . $config['currency_symbol'] . '', + 'data-off' => '%', + 'checked' => $config['default_receivings_discount_type'] == 1 + ]) ?> + +
    +
    +
    + +
    + 'control-label col-xs-2']) ?> +
    + 'enforce_privacy', + 'id' => 'enforce_privacy', + 'value' => 'enforce_privacy', + 'checked' => $config['enforce_privacy'] == 1 + ]) ?> +   + +
    +
    +
    - 'control-label col-xs-2']) ?> -
    -
    -
    - -
    - -
    -
    -
    - -
    - 'control-label col-xs-2']) ?> -
    + 'control-label col-xs-2']) ?> +
    lang('Config.floating_labels'), - 'input_groups' => lang('Config.input_groups') + 'average' => lang('Config.receiving_cost_price_method_average'), + 'new' => lang('Config.receiving_cost_price_method_new'), ], - $config['login_form'], - 'class="form-control input-sm"' + (($config['receiving_cost_price_method'] ?? (($config['receiving_calculate_average_price'] ?? 1) ? 'average' : 'new'))), + ['id' => 'receiving_cost_price_method', 'class' => 'form-control'] ) ?>
    - -
    - 'control-label col-xs-2 required']) ?> -
    -
    - 'default_sales_discount', - 'id' => 'default_sales_discount', - 'class' => 'form-control input-sm required', - 'type' => 'number', - 'min' => 0, - 'max' => 100, - 'value' => $config['default_sales_discount'] - ]) ?> - - 'default_sales_discount_type', - 'name' => 'default_sales_discount_type', - 'value' => 1, - 'data-toggle' => 'toggle', - 'data-size' => 'normal', - 'data-onstyle' => 'success', - 'data-on' => '' . $config['currency_symbol'] . '', - 'data-off' => '%', - 'checked' => $config['default_sales_discount_type'] == 1 - ]) ?> - -
    -
    -
    - -
    - 'control-label col-xs-2 required']) ?> -
    -
    - 'default_receivings_discount', - 'id' => 'default_receivings_discount', - 'class' => 'form-control input-sm required', - 'type' => 'number', - 'min' => 0, - 'max' => 100, - 'value' => $config['default_receivings_discount'] - ]) ?> - - 'default_receivings_discount_type', - 'name' => 'default_receivings_discount_type', - 'value' => 1, - 'data-toggle' => 'toggle', - 'data-size' => 'normal', - 'data-onstyle' => 'success', - 'data-on' => '' . $config['currency_symbol'] . '', - 'data-off' => '%', - 'checked' => $config['default_receivings_discount_type'] == 1 - ]) ?> - -
    -
    -
    - -
    - 'control-label col-xs-2']) ?> -
    - 'enforce_privacy', - 'id' => 'enforce_privacy', - 'value' => 'enforce_privacy', - 'checked' => $config['enforce_privacy'] == 1 - ]) ?> -   - -
    -
    - -
    - 'control-label col-xs-2']) ?> -
    - 'receiving_calculate_average_price', - 'id' => 'receiving_calculate_average_price', - 'value' => 'receiving_calculate_average_price', - 'checked' => $config['receiving_calculate_average_price'] == 1 - ]) ?> -
    -
    - -
    - 'control-label col-xs-2 required']) ?> -
    - 'lines_per_page', - 'id' => 'lines_per_page', - 'class' => 'form-control input-sm required', - 'type' => 'number', - 'min' => 10, - 'max' => 1000, - 'value' => $config['lines_per_page'] - ]) ?> -
    -
    - -
    - 'control-label col-xs-2']) ?> -
    -
    -
    - lang('Config.top'), - 'bottom' => lang('Config.bottom') - ], - $config['notify_vertical_position'], - 'class="form-control input-sm"' - ) ?> -
    -
    - lang('Config.left'), - 'center' => lang('Config.center'), - 'right' => lang('Config.right') - ], - $config['notify_horizontal_position'], - 'class="form-control input-sm"' - ) ?> -
    -
    -
    -
    - -
    - 'control-label col-xs-2']) ?> -
    -
    -
    -
    - - - - 'image_max_width', - 'id' => 'image_max_width', - 'class' => 'form-control input-sm required', - 'type' => 'number', - 'min' => 128, - 'max' => 3840, - 'value' => $config['image_max_width'], - 'data-toggle' => 'tooltip', - 'data-placement' => 'top', - 'title' => lang('Config.image_max_width_tooltip') - ]) ?> -
    -
    -
    -
    - - - - 'image_max_height', - 'id' => 'image_max_height', - 'class' => 'form-control input-sm required', - 'type' => 'number', - 'min' => 128, - 'max' => 3840, - 'value' => $config['image_max_height'], - 'data-toggle' => 'tooltip', - 'data-placement' => 'top', - 'title' => lang('Config.image_max_height_tooltip') - ]) ?> -
    -
    -
    -
    - - - - 'image_max_size', - 'id' => 'image_max_size', - 'class' => 'form-control input-sm required', - 'type' => 'number', - 'min' => 128, - 'max' => 2048, - 'value' => $config['image_max_size'], - 'data-toggle' => 'tooltip', - 'data-placement' => 'top', - 'title' => lang('Config.image_max_size_tooltip') - ]) ?> -
    -
    -
    -
    - - 'image_allowed_types[]', - 'options' => $image_allowed_types, - 'selected' => $selected_image_allowed_types, - 'id' => 'image_allowed_types', - 'class' => 'selectpicker show-menu-arrow', - 'data-none-selected-text' => lang('Common.none_selected_text'), - 'data-selected-text-format' => 'count > 1', - 'data-style' => 'btn-default btn-sm', - 'data-width' => '100%' - ]) ?> -
    -
    -
    -
    -
    - -
    - 'control-label col-xs-2']) ?> -
    - 'gcaptcha_enable', - 'id' => 'gcaptcha_enable', - 'value' => 'gcaptcha_enable', - 'checked' => $config['gcaptcha_enable'] == 1 - ]) ?> - -
    -
    - -
    - 'required control-label col-xs-2', 'id' => 'config_gcaptcha_site_key']) ?> -
    - 'gcaptcha_site_key', - 'id' => 'gcaptcha_site_key', - 'class' => 'form-control input-sm required', - 'value' => $config['gcaptcha_site_key'] - ]) ?> -
    -
    - -
    - 'required control-label col-xs-2', 'id' => 'config_gcaptcha_secret_key']) ?> -
    - 'gcaptcha_secret_key', - 'id' => 'gcaptcha_secret_key', - 'class' => 'form-control input-sm required', - 'value' => $config['gcaptcha_secret_key'] - ]) ?> -
    -
    - -
    - 'control-label col-xs-2']) ?> -
    -
    -
    -
    - - lang('Items.name'), - 'item_number' => lang('Items.number_information'), - 'unit_price' => lang('Items.unit_price'), - 'cost_price' => lang('Items.cost_price') - ], - $config['suggestions_first_column'], - 'class="form-control input-sm"' - ) ?> -
    -
    -
    -
    - - lang('Config.none'), - 'name' => lang('Items.name'), - 'item_number' => lang('Items.number_information'), - 'unit_price' => lang('Items.unit_price'), - 'cost_price' => lang('Items.cost_price') - ], - $config['suggestions_second_column'], - 'class="form-control input-sm"' - ) ?> -
    -
    -
    -
    - - lang('Config.none'), - 'name' => lang('Items.name'), - 'item_number' => lang('Items.number_information'), - 'unit_price' => lang('Items.unit_price'), - 'cost_price' => lang('Items.cost_price') - ], - $config['suggestions_third_column'], - 'class="form-control input-sm"' - ) ?> -
    -
    -
    -
    -
    - -
    - 'control-label col-xs-2']) ?> -
    - - -
    -
    - + +
    + 'control-label col-xs-2 required']) ?> +
    + 'lines_per_page', + 'id' => 'lines_per_page', + 'class' => 'form-control input-sm required', + 'type' => 'number', + 'min' => 10, + 'max' => 1000, + 'value' => $config['lines_per_page'] + ]) ?> +
    +
    + +
    + 'control-label col-xs-2']) ?> +
    +
    +
    + lang('Config.top'), + 'bottom' => lang('Config.bottom') + ], + $config['notify_vertical_position'], + 'class="form-control input-sm"' + ) ?> +
    +
    + lang('Config.left'), + 'center' => lang('Config.center'), + 'right' => lang('Config.right') + ], + $config['notify_horizontal_position'], + 'class="form-control input-sm"' + ) ?> +
    +
    +
    +
    + +
    + 'control-label col-xs-2']) ?> +
    +
    +
    +
    + + + + 'image_max_width', + 'id' => 'image_max_width', + 'class' => 'form-control input-sm required', + 'type' => 'number', + 'min' => 128, + 'max' => 3840, + 'value' => $config['image_max_width'], + 'data-toggle' => 'tooltip', + 'data-placement' => 'top', + 'title' => lang('Config.image_max_width_tooltip') + ]) ?> +
    +
    +
    +
    + + + + 'image_max_height', + 'id' => 'image_max_height', + 'class' => 'form-control input-sm required', + 'type' => 'number', + 'min' => 128, + 'max' => 3840, + 'value' => $config['image_max_height'], + 'data-toggle' => 'tooltip', + 'data-placement' => 'top', + 'title' => lang('Config.image_max_height_tooltip') + ]) ?> +
    +
    +
    +
    + + + + 'image_max_size', + 'id' => 'image_max_size', + 'class' => 'form-control input-sm required', + 'type' => 'number', + 'min' => 128, + 'max' => 2048, + 'value' => $config['image_max_size'], + 'data-toggle' => 'tooltip', + 'data-placement' => 'top', + 'title' => lang('Config.image_max_size_tooltip') + ]) ?> +
    +
    +
    +
    + + 'image_allowed_types[]', + 'options' => $image_allowed_types, + 'selected' => $selected_image_allowed_types, + 'id' => 'image_allowed_types', + 'class' => 'selectpicker show-menu-arrow', + 'data-none-selected-text' => lang('Common.none_selected_text'), + 'data-selected-text-format' => 'count > 1', + 'data-style' => 'btn-default btn-sm', + 'data-width' => '100%' + ]) ?> +
    +
    +
    +
    +
    + +
    + 'control-label col-xs-2']) ?> +
    + 'gcaptcha_enable', + 'id' => 'gcaptcha_enable', + 'value' => 'gcaptcha_enable', + 'checked' => $config['gcaptcha_enable'] == 1 + ]) ?> + +
    +
    + +
    + 'required control-label col-xs-2', 'id' => 'config_gcaptcha_site_key']) ?> +
    + 'gcaptcha_site_key', + 'id' => 'gcaptcha_site_key', + 'class' => 'form-control input-sm required', + 'value' => $config['gcaptcha_site_key'] + ]) ?> +
    +
    + +
    + 'required control-label col-xs-2', 'id' => 'config_gcaptcha_secret_key']) ?> +
    + 'gcaptcha_secret_key', + 'id' => 'gcaptcha_secret_key', + 'class' => 'form-control input-sm required', + 'value' => $config['gcaptcha_secret_key'] + ]) ?> +
    +
    + +
    + 'control-label col-xs-2']) ?> +
    +
    +
    +
    + + lang('Items.name'), + 'item_number' => lang('Items.number_information'), + 'unit_price' => lang('Items.unit_price'), + 'cost_price' => lang('Items.cost_price') + ], + $config['suggestions_first_column'], + 'class="form-control input-sm"' + ) ?> +
    +
    +
    +
    + + lang('Config.none'), + 'name' => lang('Items.name'), + 'item_number' => lang('Items.number_information'), + 'unit_price' => lang('Items.unit_price'), + 'cost_price' => lang('Items.cost_price') + ], + $config['suggestions_second_column'], + 'class="form-control input-sm"' + ) ?> +
    +
    +
    +
    + + lang('Config.none'), + 'name' => lang('Items.name'), + 'item_number' => lang('Items.number_information'), + 'unit_price' => lang('Items.unit_price'), + 'cost_price' => lang('Items.cost_price') + ], + $config['suggestions_third_column'], + 'class="form-control input-sm"' + ) ?> +
    +
    +
    +
    +
    + +
    + 'control-label col-xs-2']) ?> +
    + + +
    +
    +
    'control-label col-xs-2']) ?>
    'derive_sale_quantity', - 'id' => 'derive_sale_quantity', - 'value' => 'derive_sale_quantity', - 'checked' => $config['derive_sale_quantity'] == 1 + 'id' => 'derive_sale_quantity', + 'value' => 'derive_sale_quantity', + 'checked' => $config['derive_sale_quantity'] == 1 + ]) ?> +   + +
    +
    + +
    + 'control-label col-xs-2']) ?> +
    + 'customer_display_enabled', + 'id' => 'customer_display_enabled', + 'value' => 'customer_display_enabled', + 'checked' => ($config['customer_display_enabled'] ?? 1) == 1 ]) ?> -   -
    @@ -410,134 +425,138 @@
    'show_office_group', - 'id' => 'show_office_group', - 'value' => 'show_office_group', - 'checked' => $show_office_group > 0 + 'id' => 'show_office_group', + 'value' => 'show_office_group', + 'checked' => $show_office_group > 0 + ]) ?> +
    +
    + +
    + 'control-label col-xs-2']) ?> +
    + 'multi_pack_enabled', + 'id' => 'multi_pack_enabled', + 'value' => 'multi_pack_enabled', + 'checked' => $config['multi_pack_enabled'] == 1 + ]) ?> +
    +
    + +
    + 'control-label col-xs-2']) ?> +
    + 'include_hsn', + 'id' => 'include_hsn', + 'value' => 'include_hsn', + 'checked' => $config['include_hsn'] == 1 ]) ?>
    -
    - 'control-label col-xs-2']) ?> -
    - 'multi_pack_enabled', - 'id' => 'multi_pack_enabled', - 'value' => 'multi_pack_enabled', - 'checked' => $config['multi_pack_enabled'] == 1 - ]) ?> -
    -
    - -
    - 'control-label col-xs-2']) ?> -
    - 'include_hsn', - 'id' => 'include_hsn', - 'value' => 'include_hsn', - 'checked' => $config['include_hsn'] == 1 - ]) ?> -
    -
    'control-label col-xs-2']) ?> -
    - 'category_dropdown', - 'id' => 'category_dropdown', - 'value' => 'category_dropdown', - 'checked' => $config['category_dropdown'] == 1 - ]) ?> -
    -
    - - 'submit_general', - 'id' => 'submit_general', - 'value' => lang('Common.submit'), - 'class' => 'btn btn-primary btn-sm pull-right' - ]) ?> - -
    -
    - - - +
    + 'category_dropdown', + 'id' => 'category_dropdown', + 'value' => 'category_dropdown', + 'checked' => $config['category_dropdown'] == 1 + ]) ?> +
    +
    + + 'submit_general', + 'id' => 'submit_general', + 'value' => lang('Common.submit'), + 'class' => 'btn btn-primary btn-sm pull-right' + ]) ?> + + + + + + + + + diff --git a/app/Views/partial/customer_display_header.php b/app/Views/partial/customer_display_header.php new file mode 100644 index 000000000..d517a96c0 --- /dev/null +++ b/app/Views/partial/customer_display_header.php @@ -0,0 +1,249 @@ + + + + + + + <?= lang('Sales.customer_display') ?> + + + + + + +
    Open Source Point of Sale
    +
    +
    + + company_logo + +
    +
    Phone:
    + +
    + +
    + +
    + diff --git a/app/Views/sales/customer_display.php b/app/Views/sales/customer_display.php new file mode 100644 index 000000000..c3469ffec --- /dev/null +++ b/app/Views/sales/customer_display.php @@ -0,0 +1,224 @@ + + + + +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + $item) { ?> + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    + +
    + + + + + + + + + +
    + + + +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    +
    +
    + + +
    + + + + + + + diff --git a/app/Views/sales/register.php b/app/Views/sales/register.php index 685b78fe5..37b51612f 100644 --- a/app/Views/sales/register.php +++ b/app/Views/sales/register.php @@ -1,92 +1,116 @@ - - - - -' . esc($error) . ''; -} - -if (!empty($warning)) { - echo '
    ' . esc($warning) . '
    '; -} - -if (isset($success)) { - echo '
    ' . esc($success) . '
    '; -} - + + + + +' . esc($error) . ''; +} + +if (!empty($warning)) { + echo '
    ' . esc($warning) . '
    '; +} + +if (isset($success)) { + echo '
    ' . esc($success) . '
    '; +} + helper('url'); ?> -
    + + + + + + + + + + + +
    :
    :
    + - - 'mode_form', 'class' => 'form-horizontal panel panel-default']) ?> -
    -
    +
    + - - + + + + +