From dbf31142672c20c3e816d1b9138694bab7b7e3fb Mon Sep 17 00:00:00 2001 From: FrancescoUK Date: Mon, 3 Jun 2019 21:13:21 +0100 Subject: [PATCH 1/3] Attributes csv import integration --- application/config/autoload.php | 2 +- application/controllers/Items.php | 474 +++++++++++------- application/helpers/importfile_helper.php | 80 +++ application/language/en-GB/items_lang.php | 3 +- application/language/en-US/items_lang.php | 5 +- .../20190415111200_stock_location.php | 26 + application/models/Attribute.php | 233 +++++---- application/models/Stock_location.php | 32 +- import_items.csv | 4 +- 9 files changed, 555 insertions(+), 304 deletions(-) create mode 100644 application/helpers/importfile_helper.php create mode 100644 application/migrations/20190415111200_stock_location.php diff --git a/application/config/autoload.php b/application/config/autoload.php index c1883aade..32ac62491 100644 --- a/application/config/autoload.php +++ b/application/config/autoload.php @@ -89,7 +89,7 @@ $autoload['drivers'] = array(); | | $autoload['helper'] = array('url', 'file'); */ -$autoload['helper'] = array('form', 'url', 'tabular', 'text', 'locale', 'html', 'download', 'directory', 'migration'); +$autoload['helper'] = array('form', 'url', 'tabular', 'text', 'locale', 'html', 'download', 'directory', 'migration', 'importfile'); /* | ------------------------------------------------------------------- diff --git a/application/controllers/Items.php b/application/controllers/Items.php index 518cd2391..ee68d077f 100644 --- a/application/controllers/Items.php +++ b/application/controllers/Items.php @@ -10,13 +10,13 @@ class Items extends Secure_Controller $this->load->library('item_lib'); } - + public function index() { $this->session->set_userdata('allow_temp_items', 0); $data['table_headers'] = $this->xss_clean(get_items_manage_table_headers()); - + $data['stock_location'] = $this->xss_clean($this->item_lib->get_item_location()); $data['stock_locations'] = $this->xss_clean($this->Stock_location->get_allowed_locations()); @@ -33,8 +33,8 @@ class Items extends Secure_Controller } /* - Returns Items table data rows. This will be called with AJAX. - */ + Returns Items table data rows. This will be called with AJAX. + */ public function search() { $search = $this->input->get('search'); @@ -48,17 +48,17 @@ class Items extends Secure_Controller $definition_names = $this->Attribute->get_definitions_by_flags(Attribute::SHOW_IN_ITEMS); $filters = array('start_date' => $this->input->get('start_date'), - 'end_date' => $this->input->get('end_date'), - 'stock_location_id' => $this->item_lib->get_item_location(), - 'empty_upc' => FALSE, - 'low_inventory' => FALSE, - 'is_serialized' => FALSE, - 'no_description' => FALSE, - 'search_custom' => FALSE, - 'is_deleted' => FALSE, - 'temporary' => FALSE, - 'definition_ids' => array_keys($definition_names)); - + 'end_date' => $this->input->get('end_date'), + 'stock_location_id' => $this->item_lib->get_item_location(), + 'empty_upc' => FALSE, + 'low_inventory' => FALSE, + 'is_serialized' => FALSE, + 'no_description' => FALSE, + 'search_custom' => FALSE, + 'is_deleted' => FALSE, + 'temporary' => FALSE, + 'definition_ids' => array_keys($definition_names)); + // check if any filter is set in the multiselect dropdown $filledup = array_fill_keys($this->input->get('filters'), TRUE); $filters = array_merge($filters, $filledup); @@ -79,7 +79,7 @@ class Items extends Secure_Controller echo json_encode(array('total' => $total_rows, 'rows' => $data_rows)); } - + public function pic_thumb($pic_filename) { $this->load->helper('file'); @@ -113,8 +113,8 @@ class Items extends Secure_Controller } /* - Gives search suggestions based on what is being searched for - */ + Gives search suggestions based on what is being searched for + */ public function suggest_search() { $suggestions = $this->xss_clean($this->Item->get_search_suggestions($this->input->post_get('term'), @@ -149,8 +149,8 @@ class Items extends Secure_Controller } /* - Gives search suggestions based on what is being searched for - */ + Gives search suggestions based on what is being searched for + */ public function suggest_category() { $suggestions = $this->xss_clean($this->Item->get_category_suggestions($this->input->get('term'))); @@ -160,7 +160,7 @@ class Items extends Secure_Controller /* Gives search suggestions based on what is being searched for - */ + */ public function suggest_location() { $suggestions = $this->xss_clean($this->Item->get_location_suggestions($this->input->get('term'))); @@ -348,14 +348,14 @@ class Items extends Secure_Controller { $location = $this->xss_clean($location); $quantity = $this->xss_clean($this->Item_quantity->get_item_quantity($item_id, $location['location_id'])->quantity); - + $data['stock_locations'][$location['location_id']] = $location['location_name']; $data['item_quantities'][$location['location_id']] = $quantity; } $this->load->view('items/form_inventory', $data); } - + public function count_details($item_id = -1) { $item_info = $this->Item->get_info($item_id); @@ -371,7 +371,7 @@ class Items extends Secure_Controller { $location = $this->xss_clean($location); $quantity = $this->xss_clean($this->Item_quantity->get_item_quantity($item_id, $location['location_id'])->quantity); - + $data['stock_locations'][$location['location_id']] = $location['location_name']; $data['item_quantities'][$location['location_id']] = $quantity; } @@ -393,14 +393,14 @@ class Items extends Secure_Controller foreach($result as &$item) { $item = $this->xss_clean($item); - + // update the barcode field if empty / NULL with the newly generated barcode if(empty($item['item_number']) && $this->config->item('barcode_generate_if_empty')) { // get the newly generated barcode $barcode_instance = Barcode_lib::barcode_instance($item, $config); $item['item_number'] = $barcode_instance->getData(); - + $save_item = array('item_number' => $item['item_number']); // update the item in the database in order to save the barcode field @@ -428,7 +428,7 @@ class Items extends Secure_Controller $values['attribute_id'] = $attribute_id; $values['attribute_value'] = $attribute_value; $values['selected_value'] = ''; - + if ($definition_value['definition_type'] == DROPDOWN) { $values['values'] = $this->Attribute->get_definition_values($definition_id); @@ -444,7 +444,7 @@ class Items extends Secure_Controller unset($data['definition_names'][$definition_id]); } - $this->load->view('attributes/item', $data); + $this->load->view('attributes/item', $data); } public function bulk_edit() @@ -458,12 +458,12 @@ class Items extends Secure_Controller } $data['suppliers'] = $suppliers; $data['allow_alt_description_choices'] = array( - '' => $this->lang->line('items_do_nothing'), + '' => $this->lang->line('items_do_nothing'), 1 => $this->lang->line('items_change_all_to_allow_alt_desc'), 0 => $this->lang->line('items_change_all_to_not_allow_allow_desc')); $data['serialization_choices'] = array( - '' => $this->lang->line('items_do_nothing'), + '' => $this->lang->line('items_do_nothing'), 1 => $this->lang->line('items_change_all_to_serialized'), 0 => $this->lang->line('items_change_all_to_unserialized')); @@ -535,7 +535,7 @@ class Items extends Secure_Controller { $item_data['tax_category_id'] = $this->input->post('tax_category_id') == '' ? NULL : $this->input->post('tax_category_id'); } - + if(!empty($upload_data['orig_name'])) { // XSS file image sanity check @@ -544,7 +544,7 @@ class Items extends Secure_Controller $item_data['pic_filename'] = $upload_data['raw_name']; } } - + $employee_id = $this->Employee->get_logged_in_employee_info()->person_id; if($this->Item->save($item_data, $item_id)) @@ -587,8 +587,8 @@ class Items extends Secure_Controller $updated_quantity = 0; } $location_detail = array('item_id' => $item_id, - 'location_id' => $location['location_id'], - 'quantity' => $updated_quantity); + 'location_id' => $location['location_id'], + 'quantity' => $updated_quantity); $item_quantity = $this->Item_quantity->get_item_quantity($item_id, $location['location_id']); @@ -639,11 +639,11 @@ class Items extends Secure_Controller else // failure { $message = $this->xss_clean($this->lang->line('items_error_adding_updating') . ' ' . $item_data['name']); - + echo json_encode(array('success' => FALSE, 'message' => $message, 'id' => -1)); } } - + public function check_item_number() { $exists = $this->Item->item_number_exists($this->input->post('item_number'), $this->input->post('item_id')); @@ -651,8 +651,8 @@ class Items extends Secure_Controller } /* - If adding a new item check to see if an item kit with the same name as the item already exists. - */ + If adding a new item check to see if an item kit with the same name as the item already exists. + */ public function check_kit_exists() { if($this->input->post('item_number') === -1) @@ -669,7 +669,7 @@ class Items extends Secure_Controller private function _handle_image_upload() { /* Let files be uploaded with their original name */ - + // load upload library $config = array('upload_path' => './uploads/item_pics/', 'allowed_types' => 'gif|jpg|png', @@ -679,7 +679,7 @@ class Items extends Secure_Controller ); $this->load->library('upload', $config); $this->upload->do_upload('item_image'); - + return strlen($this->upload->display_errors()) == 0 || !strcmp($this->upload->display_errors(), '

'.$this->lang->line('upload_no_file_selected').'

'); } @@ -692,7 +692,7 @@ class Items extends Secure_Controller } public function save_inventory($item_id = -1) - { + { $employee_id = $this->Employee->get_logged_in_employee_info()->person_id; $cur_item_info = $this->Item->get_info($item_id); $location_id = $this->input->post('stock_location'); @@ -704,9 +704,9 @@ class Items extends Secure_Controller 'trans_comment' => $this->input->post('trans_comment'), 'trans_inventory' => parse_decimals($this->input->post('newquantity')) ); - + $this->Inventory->insert($inv_data); - + //Update stock quantity $item_quantity = $this->Item_quantity->get_item_quantity($item_id, $location_id); $item_quantity_data = array( @@ -718,13 +718,13 @@ class Items extends Secure_Controller if($this->Item_quantity->save($item_quantity_data, $item_id, $location_id)) { $message = $this->xss_clean($this->lang->line('items_successful_updating') . ' ' . $cur_item_info->name); - + echo json_encode(array('success' => TRUE, 'message' => $message, 'id' => $item_id)); } else//failure { $message = $this->xss_clean($this->lang->line('items_error_adding_updating') . ' ' . $cur_item_info->name); - + echo json_encode(array('success' => FALSE, 'message' => $message, 'id' => -1)); } } @@ -735,10 +735,10 @@ class Items extends Secure_Controller $item_data = array(); foreach($_POST as $key => $value) - { + { //This field is nullable, so treat it differently if($key == 'supplier_id' && $value != '') - { + { $item_data["$key"] = $value; } elseif($value != '' && !(in_array($key, array('item_ids', 'tax_names', 'tax_percents')))) @@ -756,15 +756,15 @@ class Items extends Secure_Controller $tax_updated = FALSE; $count = count($tax_percents); for($k = 0; $k < $count; ++$k) - { + { if(!empty($tax_names[$k]) && is_numeric($tax_percents[$k])) { $tax_updated = TRUE; - + $items_taxes_data[] = array('name' => $tax_names[$k], 'percent' => $tax_percents[$k]); } } - + if($tax_updated) { $this->Item_taxes->save_multiple($items_taxes_data, $items_to_update); @@ -794,183 +794,108 @@ class Items extends Secure_Controller } /* - Items import from excel spreadsheet - */ + Items import from excel spreadsheet + */ public function excel() { $name = 'import_items.csv'; - $data = file_get_contents('../' . $name); - force_download($name, $data); + $allowed_locations = $this->Stock_location->get_allowed_locations(); + $allowed_attributes = $this->Attribute->get_definition_names(FALSE); + $data = generate_import_items_csv($allowed_locations,$allowed_attributes); + force_download($name, $data, TRUE); } - + public function excel_import() { $this->load->view('items/form_excel_import', NULL); } + /** + * Imports items from CSV formatted file. + */ public function do_excel_import() { - $non_repeating_element_count = 18; - if($_FILES['file_path']['error'] != UPLOAD_ERR_OK) { echo json_encode(array('success' => FALSE, 'message' => $this->lang->line('items_excel_import_failed'))); } else { - if(($handle = fopen($_FILES['file_path']['tmp_name'], 'r')) !== FALSE) + if(file_exists($_FILES['file_path']['tmp_name'])) { - // Skip the first row as it's the table description - fgetcsv($handle); - $i = 1; - - $failCodes = array(); + $line_array = get_csv_file($_FILES['file_path']['tmp_name']); + $failCodes = array(); + $keys = $line_array[0]; - while(($data = fgetcsv($handle)) !== FALSE) + $this->db->trans_begin(); + for($i = 1; $i < count($line_array); $i++) { - // XSS file data sanity check - $data = $this->xss_clean($data); - - if(sizeof($data) >= $non_repeating_element_count) + $invalidated = FALSE; + $line = array_combine($keys,$this->xss_clean($line_array[$i])); //Build a XSS-cleaned associative array with the row to use to assign values + + if(!empty($line)) { $item_data = array( - 'name' => $data[1], - 'description' => $data[11], - 'category' => $data[2], - 'cost_price' => $data[4], - 'unit_price' => $data[5], - 'reorder_level' => $data[10], - 'supplier_id' => $this->Supplier->exists($data[3]) ? $data[3] : NULL, - 'allow_alt_description' => $data[12] != '' ? '1' : '0', - 'is_serialized' => $data[13] != '' ? '1' : '0', - 'hsn_code' => $data[15] + 'name' => $line['Item Name'], + 'description' => $line['Description'], + 'category' => $line['Category'], + 'cost_price' => $line['Cost Price'], + 'unit_price' => $line['Unit Price'], + 'reorder_level' => $line['Reorder Level'], + 'supplier_id' => $this->Supplier->exists($line['Supplier ID']) ? $line['Supplier ID'] : NULL, + 'allow_alt_description' => $line['Allow Alt Description'] != '' ? '1' : '0', + 'is_serialized' => $line['Item has Serial Number'] != '' ? '1' : '0', + 'hsn_code' => $line['HSN'], + 'pic_filename' => $line['item_image'] ); - /* we could do something like this, however, the effectiveness of - this is rather limited, since for now, you have to upload files manually - into that directory, so you really can do whatever you want, this probably - needs further discussion */ + $item_number = $line['Barcode']; - $pic_file = $data[14]; - /*if(strcmp('.htaccess', $pic_file)==0) - { - $pic_file=''; - }*/ - $item_data['pic_filename'] = $pic_file; - - $item_number = $data[0]; - $invalidated = FALSE; if($item_number != '') { $item_data['item_number'] = $item_number; $invalidated = $this->Item->item_number_exists($item_number); } + + //Sanity check of data + if(!$invalidated) + { + $invalidated = $this->data_error_check($line, $item_data); + } } - else + else { $invalidated = TRUE; } + //Save to database if(!$invalidated && $this->Item->save($item_data)) { - $items_taxes_data = NULL; - //tax 1 - if(is_numeric($data[7]) && $data[6] != '') - { - $items_taxes_data[] = array('name' => $data[6], 'percent' => $data[7] ); - } - - //tax 2 - if(is_numeric($data[9]) && $data[8] != '') - { - $items_taxes_data[] = array('name' => $data[8], 'percent' => $data[9] ); - } - - // save tax values - if(count($items_taxes_data) > 0) - { - $this->Item_taxes->save($items_taxes_data, $item_data['item_id']); - } - - // quantities & inventory Info - $employee_id = $this->Employee->get_logged_in_employee_info()->person_id; - $emp_info = $this->Employee->get_info($employee_id); - $comment ='Qty CSV Imported'; - - $cols = count($data); - - // array to store information if location got a quantity - $allowed_locations = $this->Stock_location->get_allowed_locations(); - for($col = 16; $col < $cols; $col = $col + 2) - { - $location_id = $data[$col]; - if(array_key_exists($location_id, $allowed_locations)) - { - $item_quantity_data = array( - 'item_id' => $item_data['item_id'], - 'location_id' => $location_id, - 'quantity' => $data[$col + 1], - ); - $this->Item_quantity->save($item_quantity_data, $item_data['item_id'], $location_id); - - $excel_data = array( - 'trans_items' => $item_data['item_id'], - 'trans_user' => $employee_id, - 'trans_comment' => $comment, - 'trans_location' => $data[$col], - 'trans_inventory' => $data[$col + 1] - ); - - $this->Inventory->insert($excel_data); - unset($allowed_locations[$location_id]); - } - } - - /* - * now iterate through the array and check for which location_id no entry into item_quantities was made yet - * those get an entry with quantity as 0. - * unfortunately a bit duplicate code from above... - */ - foreach($allowed_locations as $location_id => $location_name) - { - $item_quantity_data = array( - 'item_id' => $item_data['item_id'], - 'location_id' => $location_id, - 'quantity' => 0, - ); - $this->Item_quantity->save($item_quantity_data, $item_data['item_id'], $data[$col]); - - $excel_data = array( - 'trans_items' => $item_data['item_id'], - 'trans_user' => $employee_id, - 'trans_comment' => $comment, - 'trans_location' => $location_id, - 'trans_inventory' => 0 - ); - - $this->Inventory->insert($excel_data); - } + $this->save_tax_data($line, $item_data); + $this->save_inventory_quantities($line, $item_data); + $this->save_attribute_data($line, $item_data); } else //insert or update item failure { - $failCodes[] = $i; + $failed_row = $i+1; + $failCodes[] = $failed_row; + log_message("ERROR","CSV Item import failed on line ". $failed_row .". This item was not imported."); } - - ++$i; } if(count($failCodes) > 0) { - $message = $this->lang->line('items_excel_import_partially_failed') . ' (' . count($failCodes) . '): ' . implode(', ', $failCodes); - + $message = $this->lang->line('items_excel_import_partially_failed', count($failCodes), implode(', ', $failCodes)); + $this->db->trans_rollback(); echo json_encode(array('success' => FALSE, 'message' => $message)); } else { + $this->db->trans_commit(); echo json_encode(array('success' => TRUE, 'message' => $this->lang->line('items_excel_import_success'))); } } - else + else { echo json_encode(array('success' => FALSE, 'message' => $this->lang->line('items_excel_import_nodata_wrongformat'))); } @@ -978,9 +903,202 @@ class Items extends Secure_Controller } /** - * Guess whether file extension is not in the table field, - * if it isn't, then it's an old-format (formerly pic_id) field, - * so we guess the right filename and update the table + * Checks the entire line of data for errors + * + * @param array $line + * @param array $item_data + * + * @return bool Returns FALSE if all data checks out and TRUE when there is an error in the data + */ + private function data_error_check($line, $item_data) + { + //Check for empty required fields + $check_for_empty = array( + $item_data['name'], + $item_data['category'], + $item_data['cost_price'], + $item_data['unit_price'] + ); + + if(in_array('',$check_for_empty,true)) + { + log_message("ERROR","Empty required value"); + return TRUE; //Return fail on empty required fields + } + + //Build array of fields to check for numerics + $check_for_numeric_values = array( + $item_data['cost_price'], + $item_data['unit_price'], + $item_data['reorder_level'], + $item_data['supplier_id'], + $line['Tax 1 Percent'], + $line['Tax 2 Percent'] + ); + + //Add in Stock Location values to check for numeric + $allowed_locations = $this->Stock_location->get_allowed_locations(); + + foreach($allowed_locations as $location_id => $location_name) + { + $check_for_numeric_values[] = $line['location_'. $location_name]; + } + + //Check for non-numeric values which require numeric + foreach($check_for_numeric_values as $value) + { + if(!is_numeric($value) && $value != '') + { + log_message("ERROR","non-numeric: '$value' when numeric is required"); + return TRUE; + } + } + + //Check Attribute Data + $definition_names = $this->Attribute->get_definition_names(); + + foreach($definition_names as $definition_name) + { + if(!empty($line['attribute_' . $definition_name])) + { + $attribute_data = $this->Attribute->get_definition_by_name($definition_name)[0]; + $attribute_type = $attribute_data['definition_type']; + $attribute_value = $line['attribute_' . $definition_name]; + + if($attribute_type == 'DROPDOWN') + { + $dropdown_values = $this->Attribute->get_definition_values($attribute_data['definition_id']); + $dropdown_values[] = ''; + + if(in_array($attribute_value, $dropdown_values) === FALSE) + { + log_message("ERROR","Value: '$attribute_value' is not an acceptable DROPDOWN value"); + return TRUE; + } + } + else if($attribute_type == 'DECIMAL') + { + if(!is_numeric($attribute_value) && $attribute_value != '') + { + log_message("ERROR","Decimal required: '$attribute_value' is not an acceptable DECIMAL value"); + return TRUE; + } + } + else if($attribute_type == 'DATETIME') + { + if(strtotime($attribute_value) === FALSE) + { + log_message("ERROR","Datetime required: '$attribute_value' is not an acceptable DATETIME value"); + return TRUE; + } + } + } + } + + return FALSE; + } + + /** + * @param line + * @param failCodes + * @param attribute_data + */ + private function save_attribute_data($line, $item_data) + { + $definition_names = $this->Attribute->get_definition_names(); + + foreach($definition_names as $definition_name) + { + if(!empty($line['attribute_' . $definition_name])) + { + //Create attribute value + $attribute_data = $this->Attribute->get_definition_by_name($definition_name)[0]; + $status = $this->Attribute->save_value($line['attribute_' . $definition_name], $attribute_data['definition_id'], $item_data['item_id'], FALSE, $attribute_data['definition_type']); + + if($status === FALSE) + { + return FALSE; + } + } + } + } + + /** + * Saves inventory quantities for the row in the appropriate stock locations. + * + * @param array line + * @param item_data + */ + private function save_inventory_quantities($line, $item_data) + { + //Quantities & Inventory Section + $employee_id = $this->Employee->get_logged_in_employee_info()->person_id; + $emp_info = $this->Employee->get_info($employee_id); + $comment = $this->lang->line('items_inventory_CSV_import_quantity'); + $allowed_locations = $this->Stock_location->get_allowed_locations(); + + foreach($allowed_locations as $location_id => $location_name) + { + $item_quantity_data = array( + 'item_id' => $item_data['item_id'], + 'location_id' => $location_id + ); + + $excel_data = array( + 'trans_items' => $item_data['item_id'], + 'trans_user' => $employee_id, + 'trans_comment' => $comment, + 'trans_location' => $location_id, + ); + + if(!empty($line['location_' . $location_name])) + { + $item_quantity_data['quantity'] = $line['location_' . $location_name]; + $this->Item_quantity->save($item_quantity_data, $item_data['item_id'], $location_id); + + $excel_data['trans_inventory'] = $line['location_' . $location_name]; + $this->Inventory->insert($excel_data); + } + else + { + $item_quantity_data['quantity'] = 0; + $this->Item_quantity->save($item_quantity_data, $item_data['item_id'], $line[$col]); + + $excel_data['trans_inventory'] = 0; + $this->Inventory->insert($excel_data); + } + } + } + + /** + * Saves the tax data found in the line of the CSV items import file + * + * @param array line + */ + private function save_tax_data($line, $item_data) + { + $items_taxes_data = array(); + + if(is_numeric($line['Tax 1 Percent']) && $line['Tax 1 Name'] != '') + { + $items_taxes_data[] = array('name' => $line['Tax 1 Name'], 'percent' => $line['Tax 1 Percent'] ); + } + + if(is_numeric($line['Tax 2 Percent']) && $line['Tax 2 Name'] != '') + { + $items_taxes_data[] = array('name' => $line['Tax 2 Name'], 'percent' => $line['Tax 2 Percent'] ); + } + + if(count($items_taxes_data) > 0) + { + $this->Item_taxes->save($items_taxes_data, $item_data['item_id']); + } + } + + + /** + * Guess whether file extension is not in the table field, if it isn't, then it's an old-format (formerly pic_id) field, so we guess the right filename and update the table + * * @param $item the item to update */ private function _update_pic_filename($item) diff --git a/application/helpers/importfile_helper.php b/application/helpers/importfile_helper.php new file mode 100644 index 000000000..43a57d8af --- /dev/null +++ b/application/helpers/importfile_helper.php @@ -0,0 +1,80 @@ + $location_name) + { + $location_headers .= ',"location_' . $location_name . '"'; + } + + return $location_headers; +} + +/** + * Generates a list of attribute names as a string + * + * @return string Comma-separated list of attribute names + */ +function generate_attribute_headers($attribute_names) +{ + $attribute_headers = ""; + unset($attribute_names[-1]); + + foreach($attribute_names as $attribute_name) + { + $attribute_headers .= ',"attribute_' . $attribute_name . '"'; + } + + return $attribute_headers; +} + +/** + * Read the contents of a given CSV formatted file into a two-dimensional array + * + * @param string $file_name Name of the file to read. + * @return boolean|array[][] two-dimensional array with the file contents or FALSE on failure. + */ +function get_csv_file($file_name) +{ + ini_set("auto_detect_line_endings", true); + + if(($csv_file = fopen($file_name,'r')) !== FALSE) + { + //Skip Byte-Order Mark + fseek($csv_file, 3); + + while (($data = fgetcsv($csv_file)) !== FALSE) + { + $line_array[] = $data; + } + } + else + { + return FALSE; + } + + return $line_array; +} +?> \ No newline at end of file diff --git a/application/language/en-GB/items_lang.php b/application/language/en-GB/items_lang.php index 2aad67268..23514afab 100644 --- a/application/language/en-GB/items_lang.php +++ b/application/language/en-GB/items_lang.php @@ -34,7 +34,7 @@ $lang["items_error_adding_updating"] = "Error adding/updating item"; $lang["items_error_updating_multiple"] = "Error updating items"; $lang["items_excel_import_failed"] = "The excel import failed"; $lang["items_excel_import_nodata_wrongformat"] = "The uploaded file has no data or is formatted incorrectly"; -$lang["items_excel_import_partially_failed"] = "Item import successful with some failures:"; +$lang["items_excel_import_partially_failed"] = "There were %1 item import failure(s) on line(s): %2. No rows were imported"; $lang["items_excel_import_success"] = "Item import successful"; $lang["items_generate_barcodes"] = "Generate Barcodes"; $lang["items_hsn_code"] = "Harmonised System Nomenclature"; @@ -43,6 +43,7 @@ $lang["items_import_items_excel"] = "Item Import from Excel"; $lang["items_info_provided_by"] = "Information provided by"; $lang["items_inventory"] = "Inventory"; $lang["items_inventory_comments"] = "Comments"; +$lang["items_inventory_CSV_import_quantity"] = "Quantity Imported from CSV"; $lang["items_inventory_data_tracking"] = "Inventory Data Tracking"; $lang["items_inventory_date"] = "Date"; $lang["items_inventory_employee"] = "Employee"; diff --git a/application/language/en-US/items_lang.php b/application/language/en-US/items_lang.php index 727e3db43..7c4c842c0 100644 --- a/application/language/en-US/items_lang.php +++ b/application/language/en-US/items_lang.php @@ -1,4 +1,4 @@ -Stock_location->get_all()->result_array(); + + //Add stock locations to the import file + foreach($stock_locations as $location) + { + add_import_file_column('location_' .$location['location_name'],'../import_items.csv', TRUE); + } + } + + public function down() + { + + } +} +?> diff --git a/application/models/Attribute.php b/application/models/Attribute.php index f6edecfc2..f02f88be5 100644 --- a/application/models/Attribute.php +++ b/application/models/Attribute.php @@ -17,14 +17,14 @@ class Attribute extends CI_Model const SHOW_IN_ITEMS = 1; const SHOW_IN_SALES = 2; const SHOW_IN_RECEIVINGS = 4; - + public static function get_definition_flags() { $class = new ReflectionClass(__CLASS__); - + return array_flip($class->getConstants()); } - + /* Determines if a given definition_id is an attribute */ @@ -33,10 +33,10 @@ class Attribute extends CI_Model $this->db->from('attribute_definitions'); $this->db->where('definition_id', $definition_id); $this->db->where('deleted', $deleted); - + return ($this->db->get()->num_rows() == 1); } - + public function link_exists($item_id, $definition_id = FALSE) { $this->db->where('sale_id'); @@ -50,13 +50,13 @@ class Attribute extends CI_Model else { $this->db->where('definition_id', $definition_id); - + } $this->db->where('item_id', $item_id); - + return ($this->db->get()->num_rows() > 0); } - + /* Determines if a given attribute_value exists in the attribute_values table and returns the attribute_id if it does */ @@ -65,10 +65,10 @@ class Attribute extends CI_Model $this->db->distinct('attribute_id'); $this->db->from('attribute_values'); $this->db->where('attribute_value', $attribute_value); - + return $this->db->get()->row()->attribute_id; } - + /* Gets information about a particular attribute definition */ @@ -78,9 +78,9 @@ class Attribute extends CI_Model $this->db->from('attribute_definitions AS definition'); $this->db->join('attribute_definitions AS parent_definition', 'parent_definition.definition_id = definition.definition_fk', 'left'); $this->db->where('definition.definition_id', $definition_id); - + $query = $this->db->get(); - + if($query->num_rows() == 1) { return $query->row(); @@ -89,17 +89,17 @@ class Attribute extends CI_Model { //Get empty base parent object, as $item_id is NOT an item $item_obj = new stdClass(); - + //Get all the fields from items table foreach($this->db->list_fields('attribute_definitions') as $field) { $item_obj->$field = ''; } - + return $item_obj; } } - + /* Performs a search on attribute definitions */ @@ -108,22 +108,22 @@ class Attribute extends CI_Model $this->db->select('definition_group.definition_name AS definition_group, definition.*'); $this->db->from('attribute_definitions AS definition'); $this->db->join('attribute_definitions AS definition_group', 'definition_group.definition_id = definition.definition_fk', 'left'); - + $this->db->group_start(); $this->db->like('definition.definition_name', $search); $this->db->or_like('definition.definition_type', $search); $this->db->group_end(); $this->db->where('definition.deleted', 0); $this->db->order_by($sort, $order); - + if($rows > 0) { $this->db->limit($rows, $limit_from); } - + return $this->db->get(); } - + public function get_attributes_by_item($item_id) { $this->db->from('attribute_definitions'); @@ -132,51 +132,51 @@ class Attribute extends CI_Model $this->db->where('receiving_id'); $this->db->where('sale_id'); $this->db->where('deleted', 0); - + $results = $this->db->get()->result_array(); - + return $this->_to_array($results, 'definition_id'); } - + public function get_values_by_definitions($definition_ids) { if(count($definition_ids ? : [])) { $this->db->from('attribute_definitions'); - + $this->db->group_start(); $this->db->where_in('definition_fk', array_keys($definition_ids)); $this->db->or_where_in('definition_id', array_keys($definition_ids)); $this->db->where('definition_type !=', GROUP); $this->db->group_end(); - + $this->db->where('deleted', 0); - + $results = $this->db->get()->result_array(); - + return $this->_to_array($results, 'definition_id'); } - + return array(); } - + public function get_definitions_by_type($attribute_type, $definition_id = -1) { $this->db->from('attribute_definitions'); $this->db->where('definition_type', $attribute_type); $this->db->where('deleted', 0); - + if($definition_id != -1) { $this->db->where('definition_id != ', $definition_id); } - + $this->db->where('definition_fk'); $results = $this->db->get()->result_array(); - + return $this->_to_array($results, 'definition_id', 'definition_name'); } - + public function get_definitions_by_flags($definition_flags) { $this->db->from('attribute_definitions'); @@ -185,47 +185,60 @@ class Attribute extends CI_Model $this->db->where('definition_type <>', GROUP); $this->db->order_by('definition_id'); $results = $this->db->get()->result_array(); - + return $this->_to_array($results, 'definition_id', 'definition_name'); } - - public function get_definition_names() + + + /** + * Returns an array of attribute definition names and IDs + * + * @param boolean $groups If FALSE does not return GROUP type attributes in the array + * @return array Array containing definition IDs, attribute names and -1 index with the local language '[SELECT]' line. + */ + public function get_definition_names($groups = TRUE) { $this->db->from('attribute_definitions'); $this->db->where('deleted', 0); + + if($groups === FALSE) + { + $this->db->where_not_in('definition_type',GROUP); + } + $results = $this->db->get()->result_array(); - + $definition_name = array(-1 => $this->lang->line('common_none_selected_text')); - + return $definition_name + $this->_to_array($results, 'definition_id', 'definition_name'); } - + public function get_definition_values($definition_id) { $attribute_values = []; - + if($definition_id > -1) { $this->db->from('attribute_links'); $this->db->join('attribute_values', 'attribute_values.attribute_id = attribute_links.attribute_id'); $this->db->where('definition_id', $definition_id); $this->db->where('item_id'); - + $results = $this->db->get()->result_array(); - + return $this->_to_array($results, 'attribute_id', 'attribute_value'); } - + return $attribute_values; } - + private function _to_array($results, $key, $value = '') { return array_column(array_map(function($result) use ($key, $value) { return [$result[$key], empty($value) ? $result : $result[$value]]; }, $results), 1, 0); } - + /* Gets total of rows */ @@ -233,10 +246,10 @@ class Attribute extends CI_Model { $this->db->from('attribute_definitions'); $this->db->where('deleted', 0); - + return $this->db->count_all_results(); } - + /* Get number of rows */ @@ -244,11 +257,11 @@ class Attribute extends CI_Model { return $this->search($search)->num_rows(); } - + private function check_data_validity($definition, $from, $to) { $success = FALSE; - + if($from === TEXT) { $this->db->select('item_id,attribute_value'); @@ -256,7 +269,7 @@ class Attribute extends CI_Model $this->db->join('attribute_links', 'attribute_values.attribute_id = attribute_links.attribute_id'); $this->db->where('definition_id',$definition); $success = TRUE; - + if($to === DATETIME) { foreach($this->db->get()->result_array() as $row) @@ -282,22 +295,22 @@ class Attribute extends CI_Model } return $success; } - + private function convert_definition_type($definition_id, $from_type, $to_type) { $success = FALSE; - + //From TEXT to DATETIME if($from_type === TEXT) { if($to_type === DATETIME || $to_type === DECIMAL) { $field = ($to_type === DATETIME ? 'attribute_datetime' : 'attribute_decimal'); - + if($this->check_data_validity($definition_id, $from_type, $to_type)) { $this->db->trans_start(); - + $query = 'UPDATE ospos_attribute_values '; $query .= 'INNER JOIN ospos_attribute_links '; $query .= 'ON ospos_attribute_values.attribute_id = ospos_attribute_links.attribute_id '; @@ -305,7 +318,7 @@ class Attribute extends CI_Model $query .= 'attribute_value = NULL '; $query .= 'WHERE definition_id = ' . $this->db->escape($definition_id); $success = $this->db->query($query); - + $this->db->trans_complete(); } } @@ -314,30 +327,30 @@ class Attribute extends CI_Model $success = TRUE; } } - + //From DROPDOWN to TEXT else if($from_type === DROPDOWN) { //From DROPDOWN to TEXT $this->db->trans_start(); - + $this->db->from('ospos_attribute_links'); $this->db->where('definition_id',$definition_id); $this->db->where('item_id',NULL); $success = $this->db->delete(); - + $this->db->trans_complete(); } - + //Any other allowed conversion does not get checked here else { $success = TRUE; } - + return $success; } - + /* Inserts or updates a definition */ @@ -345,23 +358,26 @@ class Attribute extends CI_Model { //Run these queries as a transaction, we want to make sure we do all or nothing $this->db->trans_start(); - + //Definition doesn't exist if($definition_id === -1 || !$this->exists($definition_id)) { $success = $this->db->insert('attribute_definitions', $definition_data); $definition_data['definition_id'] = $this->db->insert_id(); } + //Definition already exists else { - $this->db->select('definition_type'); + $this->db->select('definition_type, definition_name'); $this->db->from('attribute_definitions'); $this->db->where('definition_id',$definition_id); - - $from_definition_type = $this->db->get()->row()->definition_type; + + $row = $this->db->get()->row(); + $from_definition_type = $row->definition_type; + $from_definition_name = $row->definition_name; $to_definition_type = $definition_data['definition_type']; - + if($from_definition_type !== $to_definition_type) { if(!$this->convert_definition_type($definition_id,$from_definition_type,$to_definition_type)) @@ -369,32 +385,35 @@ class Attribute extends CI_Model return FALSE; } } - + $this->db->where('definition_id', $definition_id); $success = $this->db->update('attribute_definitions', $definition_data); $definition_data['definition_id'] = $definition_id; } - + $this->db->trans_complete(); - + $success &= $this->db->trans_status(); - + return $success; } - - public function get_definition_by_name($definition_name, $definition_type) + + public function get_definition_by_name($definition_name, $definition_type = FALSE) { $this->db->from('attribute_definitions'); $this->db->where('definition_name', $definition_name); - $this->db->where('definition_type', $definition_type); - - return $this->db->get()->row_object(); + if($definition_type != FALSE) + { + $this->db->where('definition_type', $definition_type); + } + + return $this->db->get()->result_array(); } - + public function save_link($item_id, $definition_id, $attribute_id) { $this->db->trans_start(); - + if($this->link_exists($item_id, $definition_id)) { $this->db->where('definition_id', $definition_id); @@ -407,30 +426,30 @@ class Attribute extends CI_Model { $this->db->insert('attribute_links', array('attribute_id' => $attribute_id, 'item_id' => $item_id, 'definition_id' => $definition_id)); } - + $this->db->trans_complete(); - + return $this->db->trans_status(); } - + public function delete_link($item_id) { $this->db->where('sale_id'); $this->db->where('receiving_id'); - + return $this->db->delete('attribute_links', array('item_id' => $item_id)); } - + public function get_link_value($item_id, $definition_id) { $this->db->where('item_id', $item_id); $this->db->where('definition_id', $definition_id); $this->db->where('sale_id'); $this->db->where('receiving_id'); - + return $this->db->get('attribute_links')->row_object(); } - + public function get_link_values($item_id, $sale_receiving_fk, $id, $definition_flags) { $this->db->select('GROUP_CONCAT(attribute_value SEPARATOR ", ") AS attribute_values, GROUP_CONCAT(attribute_datetime SEPARATOR ", ") AS attribute_datetimevalues'); @@ -439,7 +458,7 @@ class Attribute extends CI_Model $this->db->join('attribute_definitions', 'attribute_definitions.definition_id = attribute_links.definition_id'); $this->db->where('definition_type <>', GROUP); $this->db->where('deleted', 0); - + if(!empty($id)) { $this->db->where($sale_receiving_fk, $id); @@ -451,26 +470,26 @@ class Attribute extends CI_Model } $this->db->where('item_id', (int) $item_id); $this->db->where('definition_flags & ', $definition_flags); - + $results = $this->db->get(); - + if ($results->num_rows() > 0) { $row_object = $results->row_object(); - + $datetime_values = explode(', ', $row_object->attribute_datetimevalues); $attribute_values = array(); - + foreach (array_filter($datetime_values) as $datetime_value) { $attribute_values[] = to_datetime(strtotime($datetime_value)); } - + return implode(',', $attribute_values) . $row_object->attribute_values; } return ""; } - + public function get_attribute_value($item_id, $definition_id) { $this->db->from('attribute_values'); @@ -479,10 +498,10 @@ class Attribute extends CI_Model $this->db->where('sale_id'); $this->db->where('receiving_id'); $this->db->where('item_id', (int) $item_id); - + return $this->db->get()->row_object(); } - + public function copy_attribute_links($item_id, $sale_receiving_fk, $id) { $this->db->query( @@ -492,7 +511,7 @@ class Attribute extends CI_Model WHERE item_id = ' . $this->db->escape($item_id) . ' AND sale_id IS NULL AND receiving_id IS NULL' ); } - + public function get_suggestions($definition_id, $term) { $suggestions = array(); @@ -510,14 +529,14 @@ class Attribute extends CI_Model $row_array = (array) $row; $suggestions[] = array('value' => $row_array['attribute_id'], 'label' => $row_array['attribute_value']); } - + return $suggestions; } - + public function save_value($attribute_value, $definition_id, $item_id = FALSE, $attribute_id = FALSE, $definition_type = DROPDOWN) { $this->db->trans_start(); - + if(empty($attribute_id) || empty($item_id)) { if($definition_type == TEXT || $definition_type == DROPDOWN) @@ -543,7 +562,7 @@ class Attribute extends CI_Model $this->db->insert('attribute_values', array('attribute_datetime' => date('Y-m-d H:i:s', strtotime($attribute_value)))); $attribute_id = $this->db->insert_id(); } - + $this->db->insert('attribute_links', array( 'attribute_id' => empty($attribute_id) ? NULL : $attribute_id, 'item_id' => empty($item_id) ? NULL : $item_id, @@ -554,29 +573,35 @@ class Attribute extends CI_Model $this->db->where('attribute_id', $attribute_id); $this->db->update('attribute_values', array('attribute_value' => $attribute_value)); } - + $this->db->trans_complete(); - + return $attribute_id; } - + public function delete_value($attribute_value, $definition_id) { return $this->db->query("DELETE atrv, atrl FROM " . $this->db->dbprefix('attribute_values') . " atrv, " . $this->db->dbprefix('attribute_links') . " atrl " . "WHERE atrl.attribute_id = atrv.attribute_id AND atrv.attribute_value = " . $this->db->escape($attribute_value) . " AND atrl.definition_id = " . $this->db->escape($definition_id)); } - + + /** + * Deletes an Attribute definition from the database and associated column in the items_import.csv + * + * @param unknown $definition_id Attribute definition ID to remove. + * @return boolean TRUE if successful and FALSE if there is a failure + */ public function delete_definition($definition_id) { $this->db->where('definition_id', $definition_id); - + return $this->db->update('attribute_definitions', array('deleted' => 1)); } - + public function delete_definition_list($definition_ids) { $this->db->where_in('definition_id', $definition_ids); - + return $this->db->update('attribute_definitions', array('deleted' => 1)); } } diff --git a/application/models/Stock_location.php b/application/models/Stock_location.php index 239e3937c..806dd95f4 100644 --- a/application/models/Stock_location.php +++ b/application/models/Stock_location.php @@ -109,27 +109,27 @@ class Stock_location extends CI_Model { $this->db->trans_start(); - $this->db->insert('stock_locations', $location_data_to_save); - $location_id = $this->db->insert_id(); + $this->db->insert('stock_locations', $location_data_to_save); + $location_id = $this->db->insert_id(); - $this->_insert_new_permission('items', $location_id, $location_name); - $this->_insert_new_permission('sales', $location_id, $location_name); - $this->_insert_new_permission('receivings', $location_id, $location_name); + $this->_insert_new_permission('items', $location_id, $location_name); + $this->_insert_new_permission('sales', $location_id, $location_name); + $this->_insert_new_permission('receivings', $location_id, $location_name); - // insert quantities for existing items - $items = $this->Item->get_all(); - foreach($items->result_array() as $item) - { - $quantity_data = array('item_id' => $item['item_id'], 'location_id' => $location_id, 'quantity' => 0); - $this->db->insert('item_quantities', $quantity_data); - } + // insert quantities for existing items + $items = $this->Item->get_all(); + foreach($items->result_array() as $item) + { + $quantity_data = array('item_id' => $item['item_id'], 'location_id' => $location_id, 'quantity' => 0); + $this->db->insert('item_quantities', $quantity_data); + } - $this->db->trans_complete(); + $this->db->trans_complete(); return $this->db->trans_status(); - } + } - $original_location_name = $this->get_location_name($location_id); + $original_location_name = $this->get_location_name($location_id); if($original_location_name != $location_name) { @@ -167,7 +167,7 @@ class Stock_location extends CI_Model /* Deletes one item - */ + */ public function delete($location_id) { $this->db->trans_start(); diff --git a/import_items.csv b/import_items.csv index cd38d8cd0..98bf23bdb 100644 --- a/import_items.csv +++ b/import_items.csv @@ -1,2 +1,2 @@ -Barcode,Item Name,Category,Supplier ID,Cost Price,Unit Price,Tax 1 Name,Tax 1 Percent,Tax 2 Name ,Tax 2 Percent,Reorder Level,Description,Allow Alt Description,Item has Serial Number,item_image,HSN,location_id,quantity -33333334,Apple iMac,Computers,,800,1200,Tax 1,8,Tax 2,10,1,Best Computer ever,y,,item.jpg,,1,100 +Barcode,Item Name,Category,Supplier ID,Cost Price,Unit Price,Tax 1 Name,Tax 1 Percent,Tax 2 Name ,Tax 2 Percent,Reorder Level,Description,Allow Alt Description,Item has Serial Number,item_image,HSN +33333334,Apple iMac,Computers,,800,1200,Tax 1,8,Tax 2,10,1,Best Computer ever,y,,item.jpg, \ No newline at end of file From e900607725b3385f0704a8dd23b9997050200e39 Mon Sep 17 00:00:00 2001 From: objecttothis <17935339+objecttothis@users.noreply.github.com> Date: Tue, 4 Jun 2019 12:03:46 +0400 Subject: [PATCH 2/3] Delete 20190415111200_stock_location.php Apparently this deletion was undone when we squashed commits. This file is no longer needed due to generating the CSV on the fly. --- .../20190415111200_stock_location.php | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 application/migrations/20190415111200_stock_location.php diff --git a/application/migrations/20190415111200_stock_location.php b/application/migrations/20190415111200_stock_location.php deleted file mode 100644 index 7153d632e..000000000 --- a/application/migrations/20190415111200_stock_location.php +++ /dev/null @@ -1,26 +0,0 @@ -Stock_location->get_all()->result_array(); - - //Add stock locations to the import file - foreach($stock_locations as $location) - { - add_import_file_column('location_' .$location['location_name'],'../import_items.csv', TRUE); - } - } - - public function down() - { - - } -} -?> From 1f18ccd6d6d69d9294b92618b22cdfea702e858d Mon Sep 17 00:00:00 2001 From: objecttothis <17935339+objecttothis@users.noreply.github.com> Date: Tue, 4 Jun 2019 12:04:29 +0400 Subject: [PATCH 3/3] Delete import_items.csv Apparently this deletion was undone when we squashed commits. This file is no longer needed due to generating the CSV on the fly. --- import_items.csv | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 import_items.csv diff --git a/import_items.csv b/import_items.csv deleted file mode 100644 index 98bf23bdb..000000000 --- a/import_items.csv +++ /dev/null @@ -1,2 +0,0 @@ -Barcode,Item Name,Category,Supplier ID,Cost Price,Unit Price,Tax 1 Name,Tax 1 Percent,Tax 2 Name ,Tax 2 Percent,Reorder Level,Description,Allow Alt Description,Item has Serial Number,item_image,HSN -33333334,Apple iMac,Computers,,800,1200,Tax 1,8,Tax 2,10,1,Best Computer ever,y,,item.jpg, \ No newline at end of file