Compare commits

..

4 Commits

Author SHA1 Message Date
Ollama
196d1e4d3a fix: Remove deleted filter and primary key from insert
- Remove deleted=0 filter from existence check (allow soft-deleted updates)
- Remove primary key from insert payload to avoid conflicts
- Cleaner approach for upsert logic

Address CodeRabbit review feedback
2026-04-15 17:08:20 +00:00
Ollama
a4c0d081a2 fix: Use primary key only check and atomic insert in saveValue
- Replace exists() with direct primary key check to avoid matching by other identifiers
- Wrap insert + low_sell_item_id update in transaction for atomicity
- Check db insert result and rollback on failure

Address CodeRabbit review feedback
2026-04-15 16:01:48 +00:00
Ollama
e7daa7a9db fix: Remove primary key from update payload
- Unset item_id from data array before update
- Cleaner approach to avoid including PK in update payload

Address CodeRabbit review feedback
2026-04-15 15:29:43 +00:00
Ollama
baf135dd42 refactor: unify Item model save_value signature
- Rename save_value() to saveValue() for PSR compliance
- Remove second parameter (item_id) - now derived from data array
- Check for item_id in data to determine insert vs update
- Update all call sites in Items controller
- Update test file references

Part of #4459
2026-04-15 15:10:41 +00:00
23 changed files with 498 additions and 951 deletions

View File

@@ -246,7 +246,7 @@ class Attributes extends Secure_Controller
$data['definition_group'][''] = lang('Common.none_selected_text');
$data['definition_info'] = $info;
$show_all = Attribute::SHOW_IN_ITEMS | Attribute::SHOW_IN_RECEIVINGS | Attribute::SHOW_IN_SALES | Attribute::SHOW_IN_SEARCH | Attribute::SHOW_IN_CUSTOMERS | Attribute::SHOW_IN_EMPLOYEES | Attribute::SHOW_IN_SUPPLIERS;
$show_all = Attribute::SHOW_IN_ITEMS | Attribute::SHOW_IN_RECEIVINGS | Attribute::SHOW_IN_SALES;
$data['definition_flags'] = $this->get_attributes($show_all);
$selected_flags = $info->definition_flags === '' ? $show_all : $info->definition_flags;
$data['selected_definition_flags'] = $this->get_attributes($selected_flags);

View File

@@ -398,9 +398,6 @@ class Config extends Secure_Controller
$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;
@@ -408,16 +405,12 @@ class Config extends Secure_Controller
$definitionData['definition_id'] = CATEGORY_DEFINITION_ID;
$definitionData['deleted'] = 0;
$attributeSuccess = $this->attribute->saveDefinition($definitionData, CATEGORY_DEFINITION_ID);
$this->attribute->saveDefinition($definitionData, CATEGORY_DEFINITION_ID);
} elseif ($batchSaveData['category_dropdown'] == NO_DEFINITION_ID) {
$attributeSuccess = $this->attribute->deleteDefinition(CATEGORY_DEFINITION_ID);
$this->attribute->deleteDefinition(CATEGORY_DEFINITION_ID);
}
$success = $attributeSuccess && $this->appconfig->batch_save($batchSaveData);
$this->db->transComplete();
$success = $success && $this->db->transStatus();
$success = $this->appconfig->batch_save($batchSaveData);
return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]);
}
@@ -430,35 +423,32 @@ class Config extends Secure_Controller
*/
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');
$number_locale = $this->request->getPost('number_locale');
$save_number_locale = $this->request->getPost('save_number_locale');
$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;
$fmt = new NumberFormatter($number_locale, NumberFormatter::CURRENCY);
if ($number_locale != $save_number_locale) {
$currency_symbol = $fmt->getSymbol(NumberFormatter::CURRENCY_SYMBOL);
$currency_code = $fmt->getTextAttribute(NumberFormatter::CURRENCY_CODE);
$save_number_locale = $number_locale;
} else {
$currency_symbol = empty($this->request->getPost('currency_symbol')) ? $fmt->getSymbol(NumberFormatter::CURRENCY_SYMBOL) : $this->request->getPost('currency_symbol');
$currency_code = empty($this->request->getPost('currency_code')) ? $fmt->getTextAttribute(NumberFormatter::CURRENCY_CODE) : $this->request->getPost('currency_code');
}
if ($this->request->getPost('thousands_separator') == 'false') {
$fmt->setTextAttribute(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, '');
}
$fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL, $currencySymbol);
$numberLocaleExample = $fmt->format(1234567890.12300);
$fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL, $currency_symbol);
$number_local_example = $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,
'success' => $number_local_example != false,
'save_number_locale' => $save_number_locale,
'number_locale_example' => $number_local_example,
'currency_symbol' => $currency_symbol,
'currency_code' => $currency_code,
]);
}

View File

@@ -3,7 +3,7 @@
namespace App\Controllers;
use App\Libraries\Mailchimp_lib;
use App\Models\Attribute;
use App\Models\Customer;
use App\Models\Customer_rewards;
use App\Models\Tax_code;
@@ -15,31 +15,34 @@ use stdClass;
class Customers extends Persons
{
private string $listId;
private Mailchimp_lib $mailchimpLib;
private Customer_rewards $customerRewards;
private string $_list_id;
private Mailchimp_lib $mailchimp_lib;
private Customer_rewards $customer_rewards;
private Customer $customer;
private Tax_code $taxCode;
private array $appConfig;
private Tax_code $tax_code;
private array $config;
public function __construct()
{
parent::__construct('customers');
$this->mailchimpLib = new Mailchimp_lib();
$this->customerRewards = model(Customer_rewards::class);
$this->mailchimp_lib = new Mailchimp_lib();
$this->customer_rewards = model(Customer_rewards::class);
$this->customer = model(Customer::class);
$this->taxCode = model(Tax_code::class);
$this->appConfig = config(OSPOS::class)->settings;
$this->tax_code = model(Tax_code::class);
$this->config = config(OSPOS::class)->settings;
$encrypter = Services::encrypter();
if (!empty($this->appConfig['mailchimp_list_id'])) {
$this->listId = $encrypter->decrypt($this->appConfig['mailchimp_list_id']);
if (!empty($this->config['mailchimp_list_id'])) {
$this->_list_id = $encrypter->decrypt($this->config['mailchimp_list_id']);
} else {
$this->listId = '';
$this->_list_id = '';
}
}
/**
* @return string
*/
public function getIndex(): string
{
$data['table_headers'] = get_customer_manage_table_headers();
@@ -47,13 +50,19 @@ class Customers extends Persons
return view('people/manage', $data);
}
public function getRow(int $rowId): ResponseInterface
/**
* Gets one row for a customer manage table. This is called using AJAX to update one row.
* @return ResponseInterface
*/
public function getRow(int $row_id): ResponseInterface
{
$person = $this->customer->get_info($rowId);
$person = $this->customer->get_info($row_id);
$stats = $this->customer->get_stats($person->person_id);
// Retrieve the total amount the customer spent so far together with min, max and average values
$stats = $this->customer->get_stats($person->person_id); // TODO: This and the next 11 lines are duplicated in search(). Extract a method.
if (empty($stats)) {
// Create object with empty properties.
$stats = new stdClass();
$stats->total = 0;
$stats->min = 0;
@@ -63,11 +72,17 @@ class Customers extends Persons
$stats->quantity = 0;
}
$dataRow = get_customer_data_row($person, $stats);
$data_row = get_customer_data_row($person, $stats);
return $this->response->setJSON($dataRow);
return $this->response->setJSON($data_row);
}
/**
* Returns customer table data rows. This will be called with AJAX.
*
* @return void
*/
public function getSearch(): ResponseInterface
{
$search = $this->request->getGet('search');
@@ -77,13 +92,15 @@ class Customers extends Persons
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$customers = $this->customer->search($search, $limit, $offset, $sort, $order);
$totalRows = $this->customer->get_found_rows($search);
$total_rows = $this->customer->get_found_rows($search);
$dataRows = [];
$data_rows = [];
foreach ($customers->getResult() as $person) {
$stats = $this->customer->get_stats($person->person_id);
// Retrieve the total amount the customer spent so far together with min, max and average values
$stats = $this->customer->get_stats($person->person_id); // TODO: duplicated... see above
if (empty($stats)) {
// Create object with empty properties.
$stats = new stdClass();
$stats->total = 0;
$stats->min = 0;
@@ -93,12 +110,16 @@ class Customers extends Persons
$stats->quantity = 0;
}
$dataRows[] = get_customer_data_row($person, $stats);
$data_rows[] = get_customer_data_row($person, $stats);
}
return $this->response->setJSON(['total' => $totalRows, 'rows' => $dataRows]);
return $this->response->setJSON(['total' => $total_rows, 'rows' => $data_rows]);
}
/**
* Gives search suggestions based on what is being searched for
* @return ResponseInterface
*/
public function getSuggest(): ResponseInterface
{
$search = $this->request->getGet('term');
@@ -107,7 +128,10 @@ class Customers extends Persons
return $this->response->setJSON($suggestions);
}
public function suggestSearch(): ResponseInterface
/**
* @return ResponseInterface
*/
public function suggest_search(): ResponseInterface
{
$search = $this->request->getGet('term');
$suggestions = $this->customer->get_search_suggestions($search, 25, false);
@@ -115,11 +139,16 @@ class Customers extends Persons
return $this->response->setJSON($suggestions);
}
public function getView(int $customerId = NEW_ENTRY): string
/**
* Loads the customer edit form
* @return string
*/
public function getView(int $customer_id = NEW_ENTRY): string
{
if ($customerId == null) $customerId = NEW_ENTRY;
// Set default values
if ($customer_id == null) $customer_id = NEW_ENTRY;
$info = $this->customer->get_info($customerId);
$info = $this->customer->get_info($customer_id);
foreach (get_object_vars($info) as $property => $value) {
$info->$property = $value;
}
@@ -130,27 +159,28 @@ class Customers extends Persons
$data['person_info']->employee_id = $this->employee->get_logged_in_employee_info()->person_id;
}
$employeeInfo = $this->employee->get_info($info->employee_id);
$data['employee'] = $employeeInfo->first_name . ' ' . $employeeInfo->last_name;
$employee_info = $this->employee->get_info($info->employee_id);
$data['employee'] = $employee_info->first_name . ' ' . $employee_info->last_name;
$taxCodeInfo = $this->taxCode->get_info($info->sales_tax_code_id);
$tax_code_info = $this->tax_code->get_info($info->sales_tax_code_id);
if ($taxCodeInfo->tax_code != null) {
$data['sales_tax_code_label'] = $taxCodeInfo->tax_code . ' ' . $taxCodeInfo->tax_code_name;
if ($tax_code_info->tax_code != null) {
$data['sales_tax_code_label'] = $tax_code_info->tax_code . ' ' . $tax_code_info->tax_code_name;
} else {
$data['sales_tax_code_label'] = '';
}
$packages = ['' => lang('Items.none')];
foreach ($this->customerRewards->get_all()->getResultArray() as $row) {
foreach ($this->customer_rewards->get_all()->getResultArray() as $row) {
$packages[$row['package_id']] = $row['package_name'];
}
$data['packages'] = $packages;
$data['selected_package'] = $info->package_id;
$data['use_destination_based_tax'] = $this->appConfig['use_destination_based_tax'];
$data['use_destination_based_tax'] = $this->config['use_destination_based_tax'];
$stats = $this->customer->get_stats($customerId);
// Retrieve the total amount the customer spent so far together with min, max and average values
$stats = $this->customer->get_stats($customer_id);
if (!empty($stats)) {
foreach (get_object_vars($stats) as $property => $value) {
$info->$property = $value;
@@ -158,11 +188,14 @@ class Customers extends Persons
$data['stats'] = $stats;
}
// Retrieve the info from Mailchimp only if there is an email address assigned
if (!empty($info->email)) {
if (($mailchimpInfo = $this->mailchimpLib->getMemberInfo($this->listId, $info->email)) !== false) {
$data['mailchimp_info'] = $mailchimpInfo;
// Collect Mailchimp customer info
if (($mailchimp_info = $this->mailchimp_lib->getMemberInfo($this->_list_id, $info->email)) !== false) {
$data['mailchimp_info'] = $mailchimp_info;
if (($activities = $this->mailchimpLib->getMemberActivity($this->listId, $info->email)) !== false) {
// Collect customer Mailchimp emails activities (stats)
if (($activities = $this->mailchimp_lib->getMemberActivity($this->_list_id, $info->email)) !== false) {
if (array_key_exists('activity', $activities)) {
$open = 0;
$unopen = 0;
@@ -202,25 +235,22 @@ class Customers extends Persons
}
/**
* Gets person attributes for a customer (AJAX)
* Inserts/updates a customer
* @return ResponseInterface
*/
public function getAttributes(int $customerId = NEW_ENTRY): string
public function postSave(int $customer_id = NEW_ENTRY): ResponseInterface
{
return $this->getPersonAttributes($customerId, Attribute::SHOW_IN_CUSTOMERS);
}
public function postSave(int $customerId = NEW_ENTRY): ResponseInterface
{
$firstName = $this->request->getPost('first_name');
$lastName = $this->request->getPost('last_name');
$first_name = $this->request->getPost('first_name');
$last_name = $this->request->getPost('last_name');
$email = strtolower($this->request->getPost('email', FILTER_SANITIZE_EMAIL));
$firstName = $this->nameize($firstName);
$lastName = $this->nameize($lastName);
// Format first and last name properly
$first_name = $this->nameize($first_name);
$last_name = $this->nameize($last_name);
$personData = [
'first_name' => $firstName,
'last_name' => $lastName,
$person_data = [
'first_name' => $first_name,
'last_name' => $last_name,
'gender' => $this->request->getPost('gender', FILTER_SANITIZE_NUMBER_INT),
'email' => $email,
'phone_number' => $this->request->getPost('phone_number'),
@@ -233,9 +263,9 @@ class Customers extends Persons
'comments' => $this->request->getPost('comments')
];
$dateFormatter = date_create_from_format($this->appConfig['dateformat'] . ' ' . $this->appConfig['timeformat'], $this->request->getPost('date'));
$date_formatter = date_create_from_format($this->config['dateformat'] . ' ' . $this->config['timeformat'], $this->request->getPost('date'));
$customerData = [
$customer_data = [
'consent' => $this->request->getPost('consent') != null,
'account_number' => $this->request->getPost('account_number') == '' ? null : $this->request->getPost('account_number'),
'tax_id' => $this->request->getPost('tax_id'),
@@ -244,57 +274,68 @@ class Customers extends Persons
'discount_type' => $this->request->getPost('discount_type') == null ? PERCENT : $this->request->getPost('discount_type', FILTER_SANITIZE_NUMBER_INT),
'package_id' => $this->request->getPost('package_id') == '' ? null : $this->request->getPost('package_id'),
'taxable' => $this->request->getPost('taxable') != null,
'date' => $dateFormatter->format('Y-m-d H:i:s'),
'date' => $date_formatter->format('Y-m-d H:i:s'),
'employee_id' => $this->request->getPost('employee_id', FILTER_SANITIZE_NUMBER_INT),
'sales_tax_code_id' => $this->request->getPost('sales_tax_code_id') == '' ? null : $this->request->getPost('sales_tax_code_id', FILTER_SANITIZE_NUMBER_INT)
];
if ($this->customer->save_customer($personData, $customerData, $customerId)) {
$personId = $customerId == NEW_ENTRY ? $customerData['person_id'] : $customerId;
$this->savePersonAttributes($personId, Attribute::SHOW_IN_CUSTOMERS);
$mailchimpStatus = $this->request->getPost('mailchimp_status');
$this->mailchimpLib->addOrUpdateMember(
$this->listId,
if ($this->customer->save_customer($person_data, $customer_data, $customer_id)) {
// Save customer to Mailchimp selected list // TODO: addOrUpdateMember should be refactored. Potentially pass an array or object instead of 6 parameters.
$mailchimp_status = $this->request->getPost('mailchimp_status');
$this->mailchimp_lib->addOrUpdateMember(
$this->_list_id,
$email,
$firstName,
$lastName,
$mailchimpStatus == null ? "" : $mailchimpStatus,
$first_name,
$last_name,
$mailchimp_status == null ? "" : $mailchimp_status,
['vip' => $this->request->getPost('mailchimp_vip') != null]
);
if ($customerId == NEW_ENTRY) {
// New customer
if ($customer_id == NEW_ENTRY) {
return $this->response->setJSON([
'success' => true,
'message' => lang('Customers.successful_adding') . ' ' . $firstName . ' ' . $lastName,
'id' => $customerData['person_id']
'message' => lang('Customers.successful_adding') . ' ' . $first_name . ' ' . $last_name,
'id' => $customer_data['person_id']
]);
} else {
} else { // Existing customer
return $this->response->setJSON([
'success' => true,
'message' => lang('Customers.successful_updating') . ' ' . $firstName . ' ' . $lastName,
'id' => $customerId
'message' => lang('Customers.successful_updating') . ' ' . $first_name . ' ' . $last_name,
'id' => $customer_id
]);
}
} else {
} else { // Failure
return $this->response->setJSON([
'success' => false,
'message' => lang('Customers.error_adding_updating') . ' ' . $firstName . ' ' . $lastName,
'message' => lang('Customers.error_adding_updating') . ' ' . $first_name . ' ' . $last_name,
'id' => NEW_ENTRY
]);
}
}
/**
* Verifies if an email address already exists. Used in app/Views/customers/form.php
*
* @return ResponseInterface
* @noinspection PhpUnused
*/
public function postCheckEmail(): ResponseInterface
{
$email = strtolower($this->request->getPost('email', FILTER_SANITIZE_EMAIL));
$personId = $this->request->getPost('person_id', FILTER_SANITIZE_NUMBER_INT);
$person_id = $this->request->getPost('person_id', FILTER_SANITIZE_NUMBER_INT);
$exists = $this->customer->check_email_exists($email, $personId);
$exists = $this->customer->check_email_exists($email, $person_id);
return $this->response->setJSON(!$exists ? 'true' : 'false');
}
/**
* Verifies if an account number already exists. Used in app/Views/customers/form.php
*
* @return ResponseInterface
* @noinspection PhpUnused
*/
public function postCheckAccountNumber(): ResponseInterface
{
$exists = $this->customer->check_account_number_exists($this->request->getPost('account_number'), $this->request->getPost('person_id', FILTER_SANITIZE_NUMBER_INT));
@@ -302,22 +343,27 @@ class Customers extends Persons
return $this->response->setJSON(!$exists ? 'true' : 'false');
}
/**
* This deletes customers from the customers table
* @return ResponseInterface
*/
public function postDelete(): ResponseInterface
{
$customersToDelete = $this->request->getPost('ids');
$customersInfo = $this->customer->get_multiple_info($customersToDelete);
$customers_to_delete = $this->request->getPost('ids');
$customers_info = $this->customer->get_multiple_info($customers_to_delete);
$count = 0;
foreach ($customersInfo->getResult() as $info) {
foreach ($customers_info->getResult() as $info) {
if ($this->customer->delete($info->person_id)) {
$this->mailchimpLib->removeMember($this->listId, $info->email);
// remove customer from Mailchimp selected list
$this->mailchimp_lib->removeMember($this->_list_id, $info->email);
$count++;
}
}
if ($count == count($customersToDelete)) {
if ($count == count($customers_to_delete)) {
return $this->response->setJSON([
'success' => true,
'message' => lang('Customers.successful_deleted') . ' ' . $count . ' ' . lang('Customers.one_or_multiple')
@@ -327,6 +373,12 @@ class Customers extends Persons
}
}
/**
* Customers import from csv spreadsheet
*
* @return DownloadResponse The template for Customer CSV imports is returned and download forced.
* @noinspection PhpUnused
*/
public function getCsv(): DownloadResponse
{
$name = 'importCustomers.csv';
@@ -334,17 +386,30 @@ class Customers extends Persons
return $this->response->download($name, $data);
}
/**
* Displays the customer CSV import modal. Used in app/Views/people/manage.php
*
* @return string
* @noinspection PhpUnused
*/
public function getCsvImport(): string
{
return view('customers/form_csv_import');
}
/**
* Imports a CSV file containing customers. Used in app/Views/customers/form_csv_import.php
*
* @return ResponseInterface
* @noinspection PhpUnused
*/
public function postImportCsvFile(): ResponseInterface
{
if ($_FILES['file_path']['error'] != UPLOAD_ERR_OK) {
return $this->response->setJSON(['success' => false, 'message' => lang('Customers.csv_import_failed')]);
} else {
if (($handle = fopen($_FILES['file_path']['tmp_name'], 'r')) !== false) {
// Skip the first row as it's the table description
fgetcsv($handle);
$i = 1;
@@ -355,7 +420,7 @@ class Customers extends Persons
if (sizeof($data) >= 16 && $consent) {
$email = strtolower($data[4]);
$personData = [
$person_data = [
'first_name' => $data[0],
'last_name' => $data[1],
'gender' => $data[2],
@@ -370,7 +435,7 @@ class Customers extends Persons
'comments' => $data[12]
];
$customerData = [
$customer_data = [
'consent' => $consent,
'company_name' => $data[13],
'discount' => $data[15],
@@ -379,13 +444,14 @@ class Customers extends Persons
'date' => date('Y-m-d H:i:s'),
'employee_id' => $this->employee->get_logged_in_employee_info()->person_id
];
$accountNumber = $data[14];
$account_number = $data[14];
// Don't duplicate people with same email
$invalidated = $this->customer->check_email_exists($email);
if ($accountNumber != '') {
$customerData['account_number'] = $accountNumber;
$invalidated &= $this->customer->check_account_number_exists($accountNumber);
if ($account_number != '') {
$customer_data['account_number'] = $account_number;
$invalidated &= $this->customer->check_account_number_exists($account_number);
}
} else {
$invalidated = true;
@@ -394,8 +460,9 @@ class Customers extends Persons
if ($invalidated) {
$failCodes[] = $i;
log_message('error', "Row $i was not imported: Either email or account number already exist or data was invalid.");
} elseif ($this->customer->save_customer($personData, $customerData)) {
$this->mailchimpLib->addOrUpdateMember($this->listId, $personData['email'], $personData['first_name'], '', $personData['last_name']);
} elseif ($this->customer->save_customer($person_data, $customer_data)) {
// Save customer to Mailchimp selected list
$this->mailchimp_lib->addOrUpdateMember($this->_list_id, $person_data['email'], $person_data['first_name'], '', $person_data['last_name']);
} else {
$failCodes[] = $i;
}
@@ -415,4 +482,4 @@ class Customers extends Persons
}
}
}
}
}

View File

@@ -2,15 +2,18 @@
namespace App\Controllers;
use App\Models\Attribute;
use App\Models\Module;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Services;
/**
*
*
* @property module module
*
*/
class Employees extends Persons
{
protected Module $module;
public function __construct()
{
parent::__construct('employees');
@@ -18,25 +21,35 @@ class Employees extends Persons
$this->module = model('Module');
}
/**
* Returns employee table data rows. This will be called with AJAX.
*
* @return void
*/
public function getSearch(): ResponseInterface
{
$search = $this->request->getGet('search');
$limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT);
$limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT);
$offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT);
$sort = $this->sanitizeSortColumn(person_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'people.person_id');
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$sort = $this->sanitizeSortColumn(person_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'people.person_id');
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$employees = $this->employee->search($search, $limit, $offset, $sort, $order);
$totalRows = $this->employee->get_found_rows($search);
$total_rows = $this->employee->get_found_rows($search);
$dataRows = [];
$data_rows = [];
foreach ($employees->getResult() as $person) {
$dataRows[] = get_person_data_row($person);
$data_rows[] = get_person_data_row($person);
}
return $this->response->setJSON(['total' => $totalRows, 'rows' => $dataRows]);
return $this->response->setJSON(['total' => $total_rows, 'rows' => $data_rows]);
}
/**
* AJAX called function gives search suggestions based on what is being searched for.
*
* @return ResponseInterface
*/
public function getSuggest(): ResponseInterface
{
$search = $this->request->getGet('term');
@@ -45,7 +58,10 @@ class Employees extends Persons
return $this->response->setJSON($suggestions);
}
public function suggestSearch(): ResponseInterface
/**
* @return ResponseInterface
*/
public function suggest_search(): ResponseInterface
{
$search = $this->request->getPost('term');
$suggestions = $this->employee->get_search_suggestions($search);
@@ -53,35 +69,39 @@ class Employees extends Persons
return $this->response->setJSON($suggestions);
}
public function getView(int $employeeId = NEW_ENTRY): string
/**
* Loads the employee edit form
* @return string
*/
public function getView(int $employee_id = NEW_ENTRY): string
{
$personInfo = $this->employee->get_info($employeeId);
$currentUser = $this->employee->get_logged_in_employee_info();
$person_info = $this->employee->get_info($employee_id);
$current_user = $this->employee->get_logged_in_employee_info();
if ($employeeId != NEW_ENTRY && !$this->employee->canModifyEmployee($personInfo->person_id, $currentUser->person_id)) {
if ($employee_id != NEW_ENTRY && !$this->employee->canModifyEmployee($person_info->person_id, $current_user->person_id)) {
header('Location: ' . base_url('no_access/employees/employees'));
exit();
}
foreach (get_object_vars($personInfo) as $property => $value) {
$personInfo->$property = $value;
foreach (get_object_vars($person_info) as $property => $value) {
$person_info->$property = $value;
}
$data['person_info'] = $personInfo;
$data['employee_id'] = $employeeId;
$data['person_info'] = $person_info;
$data['employee_id'] = $employee_id;
$modules = [];
foreach ($this->module->get_all_modules()->getResult() as $module) {
$module->grant = $this->employee->has_grant($module->module_id, $personInfo->person_id);
$module->menu_group = $this->employee->get_menu_group($module->module_id, $personInfo->person_id);
$module->grant = $this->employee->has_grant($module->module_id, $person_info->person_id);
$module->menu_group = $this->employee->get_menu_group($module->module_id, $person_info->person_id);
$modules[] = $module;
}
$data['all_modules'] = $modules;
$permissions = [];
foreach ($this->module->get_all_subpermissions()->getResult() as $permission) {
foreach ($this->module->get_all_subpermissions()->getResult() as $permission) { // TODO: subpermissions does not follow naming standards.
$permission->permission_id = str_replace(' ', '_', $permission->permission_id);
$permission->grant = $this->employee->has_grant($permission->permission_id, $personInfo->person_id);
$permission->grant = $this->employee->has_grant($permission->permission_id, $person_info->person_id);
$permissions[] = $permission;
}
@@ -90,18 +110,17 @@ class Employees extends Persons
return view('employees/form', $data);
}
public function getAttributes(int $employeeId = NEW_ENTRY): string
/**
* Inserts/updates an employee
* @return ResponseInterface
*/
public function postSave(int $employee_id = NEW_ENTRY): ResponseInterface
{
return $this->getPersonAttributes($employeeId, Attribute::SHOW_IN_EMPLOYEES);
}
$current_user = $this->employee->get_logged_in_employee_info();
public function postSave(int $employeeId = NEW_ENTRY): ResponseInterface
{
$currentUser = $this->employee->get_logged_in_employee_info();
if ($employeeId != NEW_ENTRY) {
$targetEmployee = $this->employee->get_info($employeeId);
if (!$this->employee->canModifyEmployee($targetEmployee->person_id, $currentUser->person_id)) {
if ($employee_id != NEW_ENTRY) {
$target_employee = $this->employee->get_info($employee_id);
if (!$this->employee->canModifyEmployee($target_employee->person_id, $current_user->person_id)) {
return $this->response->setJSON([
'success' => false,
'message' => lang('Employees.error_updating_admin'),
@@ -110,16 +129,17 @@ class Employees extends Persons
}
}
$firstName = $this->request->getPost('first_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$lastName = $this->request->getPost('last_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$first_name = $this->request->getPost('first_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS); // TODO: duplicated code
$last_name = $this->request->getPost('last_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$email = strtolower($this->request->getPost('email', FILTER_SANITIZE_EMAIL));
$firstName = $this->nameize($firstName);
$lastName = $this->nameize($lastName);
// format first and last name properly
$first_name = $this->nameize($first_name);
$last_name = $this->nameize($last_name);
$personData = [
'first_name' => $firstName,
'last_name' => $lastName,
$person_data = [
'first_name' => $first_name,
'last_name' => $last_name,
'gender' => $this->request->getPost('gender', FILTER_SANITIZE_NUMBER_INT),
'email' => $email,
'phone_number' => $this->request->getPost('phone_number', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
@@ -132,98 +152,108 @@ class Employees extends Persons
'comments' => $this->request->getPost('comments', FILTER_SANITIZE_FULL_SPECIAL_CHARS)
];
$grantsArray = [];
$isAdmin = $this->employee->isAdmin($currentUser->person_id);
$grants_array = [];
$isAdmin = $this->employee->isAdmin($current_user->person_id);
foreach ($this->module->get_all_permissions()->getResult() as $permission) {
$grants = [];
$grant = $this->request->getPost('grant_' . $permission->permission_id) != null ? $this->request->getPost('grant_' . $permission->permission_id, FILTER_SANITIZE_FULL_SPECIAL_CHARS) : '';
if ($grant == $permission->permission_id) {
if (!$isAdmin && !$this->employee->has_grant($permission->permission_id, $currentUser->person_id)) {
if (!$isAdmin && !$this->employee->has_grant($permission->permission_id, $current_user->person_id)) {
continue;
}
$grants['permission_id'] = $permission->permission_id;
$grants['menu_group'] = $this->request->getPost('menu_group_' . $permission->permission_id) != null ? $this->request->getPost('menu_group_' . $permission->permission_id, FILTER_SANITIZE_FULL_SPECIAL_CHARS) : '--';
$grantsArray[] = $grants;
$grants_array[] = $grants;
}
}
// Password has been changed OR first time password set
if (!empty($this->request->getPost('password')) && ENVIRONMENT != 'testing') {
$exploded = explode(":", $this->request->getPost('language', FILTER_SANITIZE_FULL_SPECIAL_CHARS));
$employeeData = [
$employee_data = [
'username' => $this->request->getPost('username', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
'password' => password_hash($this->request->getPost('password'), PASSWORD_DEFAULT),
'hash_version' => 2,
'language_code' => $exploded[0],
'language' => $exploded[1]
];
} else {
} else { // Password not changed
$exploded = explode(":", $this->request->getPost('language', FILTER_SANITIZE_FULL_SPECIAL_CHARS));
$employeeData = [
$employee_data = [
'username' => $this->request->getPost('username', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
'language_code' => $exploded[0],
'language' => $exploded[1]
];
}
if ($this->employee->save_employee($personData, $employeeData, $grantsArray, $employeeId)) {
$personId = $employeeId == NEW_ENTRY ? $employeeData['person_id'] : $employeeId;
$this->savePersonAttributes($personId, Attribute::SHOW_IN_EMPLOYEES);
if ($employeeId == NEW_ENTRY) {
if ($this->employee->save_employee($person_data, $employee_data, $grants_array, $employee_id)) {
// New employee
if ($employee_id == NEW_ENTRY) {
return $this->response->setJSON([
'success' => true,
'message' => lang('Employees.successful_adding') . ' ' . $firstName . ' ' . $lastName,
'id' => $employeeData['person_id']
'message' => lang('Employees.successful_adding') . ' ' . $first_name . ' ' . $last_name,
'id' => $employee_data['person_id']
]);
} else {
$loggedInEmployeeId = session()->get('person_id');
if ($employeeId == $loggedInEmployeeId) {
session()->set('language_code', $employeeData['language_code']);
session()->set('language', $employeeData['language']);
} else { // Existing employee
$logged_in_employee_id = session()->get('person_id');
if ($employee_id == $logged_in_employee_id) {
session()->set('language_code', $employee_data['language_code']);
session()->set('language', $employee_data['language']);
}
return $this->response->setJSON([
'success' => true,
'message' => lang('Employees.successful_updating') . ' ' . $firstName . ' ' . $lastName,
'id' => $employeeId
'message' => lang('Employees.successful_updating') . ' ' . $first_name . ' ' . $last_name,
'id' => $employee_id
]);
}
} else {
} else { // Failure
return $this->response->setJSON([
'success' => false,
'message' => lang('Employees.error_adding_updating') . ' ' . $firstName . ' ' . $lastName,
'message' => lang('Employees.error_adding_updating') . ' ' . $first_name . ' ' . $last_name,
'id' => NEW_ENTRY
]);
}
}
/**
* This deletes employees from the employees table
* @return ResponseInterface
*/
public function postDelete(): ResponseInterface
{
$employeesToDelete = $this->request->getPost('ids', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$currentUser = $this->employee->get_logged_in_employee_info();
$employees_to_delete = $this->request->getPost('ids', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$current_user = $this->employee->get_logged_in_employee_info();
if (!$this->employee->isAdmin($currentUser->person_id)) {
foreach ($employeesToDelete as $empId) {
if ($this->employee->isAdmin((int)$empId)) {
if (!$this->employee->isAdmin($current_user->person_id)) {
foreach ($employees_to_delete as $emp_id) {
if ($this->employee->isAdmin((int)$emp_id)) {
return $this->response->setJSON(['success' => false, 'message' => lang('Employees.error_deleting_admin')]);
}
}
}
if ($this->employee->delete_list($employeesToDelete)) {
if ($this->employee->delete_list($employees_to_delete)) { // TODO: this is passing a string, but delete_list expects an array
return $this->response->setJSON([
'success' => true,
'message' => lang('Employees.successful_deleted') . ' ' . count($employeesToDelete) . ' ' . lang('Employees.one_or_multiple')
'message' => lang('Employees.successful_deleted') . ' ' . count($employees_to_delete) . ' ' . lang('Employees.one_or_multiple')
]);
} else {
return $this->response->setJSON(['success' => false, 'message' => lang('Employees.cannot_be_deleted')]);
}
}
public function getCheckUsername($employeeId): ResponseInterface
/**
* Checks an employee username against the database. Used in app\Views\employees\form.php
*
* @param $employee_id
* @return ResponseInterface
* @noinspection PhpUnused
*/
public function getCheckUsername($employee_id): ResponseInterface
{
$exists = $this->employee->username_exists($employeeId, $this->request->getGet('username'));
$exists = $this->employee->username_exists($employee_id, $this->request->getGet('username'));
return $this->response->setJSON(!$exists ? 'true' : 'false');
}
}
}

View File

@@ -482,9 +482,9 @@ class Items extends Secure_Controller
foreach ($result as &$item) {
if (isset($item['item_number']) && empty($item['item_number']) && $this->config['barcode_generate_if_empty']) {
if (isset($item['item_id'])) {
$save_item = ['item_number' => $item['item_number']];
$this->item->save_value($save_item, $item['item_id']);
}
$save_item = ['item_number' => $item['item_number'], 'item_id' => $item['item_id']];
$this->item->saveValue($save_item);
}
}
}
$data['items'] = $result;
@@ -663,7 +663,12 @@ class Items extends Secure_Controller
$employee_id = $this->employee->get_logged_in_employee_info()->person_id;
if ($this->item->save_value($item_data, $item_id)) {
// For updates, include item_id in data array
if ($item_id !== NEW_ENTRY) {
$item_data['item_id'] = $item_id;
}
if ($this->item->saveValue($item_data)) {
$success = true;
$new_item = false;
@@ -826,8 +831,8 @@ class Items extends Secure_Controller
*/
public function getRemoveLogo($item_id): ResponseInterface
{
$item_data = ['pic_filename' => null];
$result = $this->item->save_value($item_data, $item_id);
$item_data = ['pic_filename' => null, 'item_id' => $item_id];
$result = $this->item->saveValue($item_data);
return $this->response->setJSON(['success' => $result]);
}
@@ -1039,7 +1044,7 @@ class Items extends Secure_Controller
return $value !== null && strlen($value);
});
if (!$isFailedRow && $this->item->save_value($itemData, $itemId)) {
if (!$isFailedRow && $this->item->saveValue($itemData)) {
$this->save_tax_data($row, $itemData);
$this->save_inventory_quantities($row, $itemData, $allowedStockLocations, $employeeId);
$csvAttributeValues = $this->extractAttributeData($row);
@@ -1312,8 +1317,8 @@ class Items extends Secure_Controller
$images = glob(FCPATH . "uploads/item_pics/$item->pic_filename.*");
if (sizeof($images) > 0) {
$new_pic_filename = pathinfo($images[0], PATHINFO_BASENAME);
$item_data = ['pic_filename' => $new_pic_filename];
$this->item->save_value($item_data, $item->item_id);
$item_data = ['pic_filename' => $new_pic_filename, 'item_id' => $item->item_id];
$this->item->saveValue($item_data);
}
}
}

View File

@@ -2,28 +2,28 @@
namespace App\Controllers;
use App\Models\Attribute;
use App\Models\Person;
use CodeIgniter\HTTP\ResponseInterface;
use Config\OSPOS;
use Config\Services;
use function Tamtamchik\NameCase\str_name_case;
abstract class Persons extends Secure_Controller
{
protected Person $person;
protected Attribute $attribute;
protected array $appConfig;
public function __construct(?string $moduleId = null)
/**
* @param string|null $module_id
*/
public function __construct(?string $module_id = null)
{
parent::__construct($moduleId);
parent::__construct($module_id);
$this->person = model(Person::class);
$this->attribute = model(Attribute::class);
$this->appConfig = config(OSPOS::class)->settings;
}
/**
* @return string
*/
public function getIndex(): string
{
$data['table_headers'] = get_people_manage_table_headers();
@@ -31,6 +31,10 @@ abstract class Persons extends Secure_Controller
return view('people/manage', $data);
}
/**
* Gives search suggestions based on what is being searched for
* @return ResponseInterface
*/
public function getSuggest(): ResponseInterface
{
$search = $this->request->getGet('term');
@@ -39,88 +43,34 @@ abstract class Persons extends Secure_Controller
return $this->response->setJSON($suggestions);
}
public function getRow(int $rowId): ResponseInterface
/**
* Gets one row for a person manage table. This is called using AJAX to update one row.
* @return ResponseInterface
*/
public function getRow(int $row_id): ResponseInterface
{
$dataRow = get_person_data_row($this->person->get_info($rowId));
$data_row = get_person_data_row($this->person->get_info($row_id));
return $this->response->setJSON($dataRow);
}
protected function getPersonAttributes(int $personId, int $definitionFlags): string
{
$data['person_id'] = $personId;
$data['config'] = $this->appConfig;
$definitionIds = json_decode($this->request->getGet('definition_ids') ?? '', true);
$data['definition_values'] = $this->attribute->getAttributesByPerson($personId) + $this->attribute->get_values_by_definitions($definitionIds);
$data['definition_names'] = $this->attribute->getDefinitionsByType(true, $definitionFlags);
foreach ($data['definition_values'] as $definitionId => $definitionValue) {
$attributeValue = $this->attribute->getPersonAttributeValue($personId, $definitionId);
$attributeId = (empty($attributeValue) || empty($attributeValue->attribute_id)) ? null : $attributeValue->attribute_id;
$values = &$data['definition_values'][$definitionId];
$values['attribute_id'] = $attributeId;
$values['attribute_value'] = $attributeValue;
$values['selected_value'] = '';
if ($definitionValue['definition_type'] === DROPDOWN) {
$values['values'] = $this->attribute->get_definition_values($definitionId);
$linkValue = $this->getPersonLinkValue($personId, $definitionId);
$values['selected_value'] = (empty($linkValue)) ? '' : $linkValue->attribute_id;
}
if (!empty($definitionIds[$definitionId])) {
$values['selected_value'] = $definitionIds[$definitionId];
}
unset($data['definition_names'][$definitionId]);
}
return view('attributes/person', $data);
}
private function getPersonLinkValue(int $personId, int $definitionId): ?object
{
$builder = $this->db->table('attribute_links');
$builder->where('person_id', $personId);
$builder->where('item_id', null);
$builder->where('sale_id', null);
$builder->where('receiving_id', null);
$builder->where('definition_id', $definitionId);
return $builder->get()->getRowObject();
}
protected function savePersonAttributes(int $personId, int $definitionFlags): void
{
$attributeLinks = $this->request->getPost('attribute_links') ?? [];
$attributeIds = $this->request->getPost('attribute_ids') ?? [];
$this->attribute->deletePersonAttributeLinks($personId);
foreach ($attributeLinks as $definitionId => $attributeId) {
$definitionInfo = $this->attribute->getAttributeInfo((int)$definitionId);
$definitionType = $definitionInfo->definition_type;
if ($definitionType !== DROPDOWN) {
$attributeId = $this->attribute->savePersonAttributeValue(
$attributeId,
(int)$definitionId,
$personId,
$attributeIds[$definitionId] ?? false,
$definitionType
);
}
$this->attribute->savePersonAttributeLink($personId, (int)$definitionId, (int)$attributeId);
}
return $this->response->setJSON($data_row);
}
/**
* Capitalize segments of a name, and put the rest into lower case.
* You can pass the characters you want to use as delimiters as exceptions.
* The function supports UTF-8 strings
*
* Example:
* i.e. <?php echo nameize("john o'grady-smith"); ?>
*
* returns John O'Grady-Smith
*/
protected function nameize(string $input): string
{
$adjustedName = str_name_case($input);
$adjusted_name = str_name_case($input);
// TODO: Use preg_replace to match HTML entities and convert them to lowercase. This is a workaround for https://github.com/tamtamchik/namecase/issues/20
return preg_replace_callback('/&[a-zA-Z0-9#]+;/', function ($matches) {
return strtolower($matches[0]);
}, $adjustedName);
}, $adjusted_name);
}
}
}

View File

@@ -937,10 +937,7 @@ class Sales extends Secure_Controller
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();
$sale_data['mimetype'] = mime_content_type(FCPATH . 'uploads/' . $this->config['company_logo']);
// Generate email attachment: invoice in PDF format
$view = Services::renderer();
@@ -977,7 +974,13 @@ class Sales extends Secure_Controller
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();
$sale_data['img_tag'] = '';
$logo_path = FCPATH . 'uploads/' . $this->config['company_logo'];
if (!empty($this->config['company_logo']) && file_exists($logo_path)) {
$logo_data = base64_encode(file_get_contents($logo_path));
$sale_data['img_tag'] = '<img id="image" src="data:image/png;base64,' . $logo_data . '" alt="company_logo">';
}
$to = $sale_data['customer_email'];
$subject = lang('Sales.receipt');

View File

@@ -2,7 +2,6 @@
namespace App\Controllers;
use App\Models\Attribute;
use App\Models\Supplier;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Services;
@@ -18,6 +17,9 @@ class Suppliers extends Persons
$this->supplier = model(Supplier::class);
}
/**
* @return string
*/
public function getIndex(): string
{
$data['table_headers'] = get_suppliers_manage_table_headers();
@@ -25,14 +27,23 @@ class Suppliers extends Persons
return view('people/manage', $data);
}
public function getRow($rowId): ResponseInterface
/**
* Gets one row for a supplier manage table. This is called using AJAX to update one row.
* @param $row_id
* @return ResponseInterface
*/
public function getRow($row_id): ResponseInterface
{
$dataRow = get_supplier_data_row($this->supplier->get_info($rowId));
$dataRow['category'] = $this->supplier->get_category_name($dataRow['category']);
$data_row = get_supplier_data_row($this->supplier->get_info($row_id));
$data_row['category'] = $this->supplier->get_category_name($data_row['category']);
return $this->response->setJSON($dataRow);
return $this->response->setJSON($data_row);
}
/**
* Returns Supplier table data rows. This will be called with AJAX.
* @return void
**/
public function getSearch(): ResponseInterface
{
$search = $this->request->getGet('search');
@@ -42,19 +53,23 @@ class Suppliers extends Persons
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$suppliers = $this->supplier->search($search, $limit, $offset, $sort, $order);
$totalRows = $this->supplier->get_found_rows($search);
$total_rows = $this->supplier->get_found_rows($search);
$dataRows = [];
$data_rows = [];
foreach ($suppliers->getResult() as $supplier) {
$row = get_supplier_data_row($supplier);
$row['category'] = $this->supplier->get_category_name($row['category']);
$dataRows[] = $row;
$data_rows[] = $row;
}
return $this->response->setJSON(['total' => $totalRows, 'rows' => $dataRows]);
return $this->response->setJSON(['total' => $total_rows, 'rows' => $data_rows]);
}
/**
* Gives search suggestions based on what is being searched for
* @return ResponseInterface
**/
public function getSuggest(): ResponseInterface
{
$search = $this->request->getGet('term');
@@ -63,7 +78,10 @@ class Suppliers extends Persons
return $this->response->setJSON($suggestions);
}
public function suggestSearch(): ResponseInterface
/**
* @return ResponseInterface
*/
public function suggest_search(): ResponseInterface
{
$search = $this->request->getPost('term');
$suggestions = $this->supplier->get_search_suggestions($search, false);
@@ -71,9 +89,15 @@ class Suppliers extends Persons
return $this->response->setJSON($suggestions);
}
public function getView(int $supplierId = NEW_ENTRY): string
/**
* Loads the supplier edit form
*
* @param int $supplier_id
* @return string
*/
public function getView(int $supplier_id = NEW_ENTRY): string
{
$info = $this->supplier->get_info($supplierId);
$info = $this->supplier->get_info($supplier_id);
foreach (get_object_vars($info) as $property => $value) {
$info->$property = $value;
}
@@ -83,23 +107,25 @@ class Suppliers extends Persons
return view("suppliers/form", $data);
}
public function getAttributes(int $supplierId = NEW_ENTRY): string
/**
* Inserts/updates a supplier
*
* @param int $supplier_id
* @return ResponseInterface
*/
public function postSave(int $supplier_id = NEW_ENTRY): ResponseInterface
{
return $this->getPersonAttributes($supplierId, Attribute::SHOW_IN_SUPPLIERS);
}
public function postSave(int $supplierId = NEW_ENTRY): ResponseInterface
{
$firstName = $this->request->getPost('first_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$lastName = $this->request->getPost('last_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$first_name = $this->request->getPost('first_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS); // TODO: Duplicate code
$last_name = $this->request->getPost('last_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$email = strtolower($this->request->getPost('email', FILTER_SANITIZE_EMAIL));
$firstName = $this->nameize($firstName);
$lastName = $this->nameize($lastName);
// Format first and last name properly
$first_name = $this->nameize($first_name);
$last_name = $this->nameize($last_name);
$personData = [
'first_name' => $firstName,
'last_name' => $lastName,
$person_data = [
'first_name' => $first_name,
'last_name' => $last_name,
'gender' => $this->request->getPost('gender'),
'email' => $email,
'phone_number' => $this->request->getPost('phone_number', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
@@ -112,7 +138,7 @@ class Suppliers extends Persons
'comments' => $this->request->getPost('comments', FILTER_SANITIZE_FULL_SPECIAL_CHARS)
];
$supplierData = [
$supplier_data = [
'company_name' => $this->request->getPost('company_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
'agency_name' => $this->request->getPost('agency_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
'category' => $this->request->getPost('category', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
@@ -120,43 +146,47 @@ class Suppliers extends Persons
'tax_id' => $this->request->getPost('tax_id', FILTER_SANITIZE_NUMBER_INT)
];
if ($this->supplier->save_supplier($personData, $supplierData, $supplierId)) {
$personId = $supplierId == NEW_ENTRY ? $supplierData['person_id'] : $supplierId;
$this->savePersonAttributes($personId, Attribute::SHOW_IN_SUPPLIERS);
if ($supplierId == NEW_ENTRY) {
if ($this->supplier->save_supplier($person_data, $supplier_data, $supplier_id)) {
// New supplier
if ($supplier_id == NEW_ENTRY) {
return $this->response->setJSON([
'success' => true,
'message' => lang('Suppliers.successful_adding') . ' ' . $supplierData['company_name'],
'id' => $supplierData['person_id']
'message' => lang('Suppliers.successful_adding') . ' ' . $supplier_data['company_name'],
'id' => $supplier_data['person_id']
]);
} else {
} else { // Existing supplier
return $this->response->setJSON([
'success' => true,
'message' => lang('Suppliers.successful_updating') . ' ' . $supplierData['company_name'],
'id' => $supplierId
'message' => lang('Suppliers.successful_updating') . ' ' . $supplier_data['company_name'],
'id' => $supplier_id
]);
}
} else {
} else { // Failure
return $this->response->setJSON([
'success' => false,
'message' => lang('Suppliers.error_adding_updating') . ' ' . $supplierData['company_name'],
'message' => lang('Suppliers.error_adding_updating') . ' ' . $supplier_data['company_name'],
'id' => NEW_ENTRY
]);
}
}
/**
* This deletes suppliers from the suppliers table
*
* @return ResponseInterface
*/
public function postDelete(): ResponseInterface
{
$suppliersToDelete = $this->request->getPost('ids', FILTER_SANITIZE_NUMBER_INT);
$suppliers_to_delete = $this->request->getPost('ids', FILTER_SANITIZE_NUMBER_INT);
if ($this->supplier->delete_list($suppliersToDelete)) {
if ($this->supplier->delete_list($suppliers_to_delete)) {
return $this->response->setJSON([
'success' => true,
'message' => lang('Suppliers.successful_deleted') . ' ' . count($suppliersToDelete) . ' ' . lang('Suppliers.one_or_multiple')
'message' => lang('Suppliers.successful_deleted') . ' ' . count($suppliers_to_delete) . ' ' . lang('Suppliers.one_or_multiple')
]);
} else {
return $this->response->setJSON(['success' => false, 'message' => lang('Suppliers.cannot_be_deleted')]);
}
}
}
}

View File

@@ -1,61 +0,0 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
use Config\Database;
class AddPersonToAttributeLinks extends Migration
{
public function up(): void
{
helper('migration');
// First, modify the generated unique column to include person_id
// Drop the existing unique constraint
$this->db->query('ALTER TABLE `ospos_attribute_links` DROP INDEX `attribute_links_uq3`');
$this->db->query('ALTER TABLE `ospos_attribute_links` DROP COLUMN `generated_unique_column`');
// Add person_id column
$this->db->query('ALTER TABLE `ospos_attribute_links` ADD COLUMN `person_id` INT(10) NULL AFTER `receiving_id`');
// Add index for person_id
$this->db->query('ALTER TABLE `ospos_attribute_links` ADD KEY `person_id` (`person_id`)');
// Add foreign key constraint for person_id
$this->db->query('ALTER TABLE `ospos_attribute_links` ADD CONSTRAINT `ospos_attribute_links_ibfk_6` FOREIGN KEY (`person_id`) REFERENCES `ospos_people` (`person_id`) ON DELETE CASCADE');
// Recreate the generated unique column with person_id support
// This ensures uniqueness for both item attributes and person attributes
$this->db->query("ALTER TABLE `ospos_attribute_links`
ADD COLUMN `generated_unique_column` VARCHAR(255) GENERATED ALWAYS AS (
CASE
WHEN `sale_id` IS NULL AND `receiving_id` IS NULL AND `item_id` IS NOT NULL THEN CONCAT('item-', `definition_id`, '-', `item_id`)
WHEN `sale_id` IS NULL AND `receiving_id` IS NULL AND `item_id` IS NULL AND `person_id` IS NOT NULL THEN CONCAT('person-', `definition_id`, '-', `person_id`)
ELSE NULL
END
) STORED");
// Re-add unique constraint
$this->db->query('ALTER TABLE `ospos_attribute_links` ADD UNIQUE INDEX `attribute_links_uq3` (`generated_unique_column`)');
}
public function down(): void
{
// Drop person_id related constraints and column
$this->db->query('ALTER TABLE `ospos_attribute_links` DROP INDEX `attribute_links_uq3`');
$this->db->query('ALTER TABLE `ospos_attribute_links` DROP COLUMN `generated_unique_column`');
$this->db->query('ALTER TABLE `ospos_attribute_links` DROP FOREIGN KEY `ospos_attribute_links_ibfk_6`');
$this->db->query('ALTER TABLE `ospos_attribute_links` DROP COLUMN `person_id`');
// Restore original generated column
$this->db->query("ALTER TABLE `ospos_attribute_links`
ADD COLUMN `generated_unique_column` VARCHAR(255) GENERATED ALWAYS AS (
CASE
WHEN `sale_id` IS NULL AND `receiving_id` IS NULL AND `item_id` IS NOT NULL THEN CONCAT(`definition_id`, '-', `item_id`)
ELSE NULL
END
) STORED");
$this->db->query('ALTER TABLE `ospos_attribute_links` ADD UNIQUE INDEX `attribute_links_uq3` (`generated_unique_column`)');
}
}

View File

@@ -1,18 +0,0 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPersonAttributeFlag extends Migration
{
public function up(): void
{
$this->db->query('ALTER TABLE `ospos_attribute_definitions` ADD COLUMN `person_attribute` TINYINT(1) DEFAULT 0 AFTER `definition_flags`');
}
public function down(): void
{
$this->db->query('ALTER TABLE `ospos_attribute_definitions` DROP COLUMN `person_attribute`');
}
}

View File

@@ -22,7 +22,7 @@ function current_language_code(bool $load_system_language = false): string
}
}
return $config['language_code'] ?? DEFAULT_LANGUAGE_CODE;
return $config->language_code ?? DEFAULT_LANGUAGE_CODE;
}
/**
@@ -43,7 +43,7 @@ function current_language(bool $load_system_language = false): string
}
}
return $config['language'] ?? DEFAULT_LANGUAGE;
return $config->language ?? DEFAULT_LANGUAGE_CODE;
}
/**

View File

@@ -23,10 +23,6 @@ return [
"new" => "New Attribute",
"no_attributes_to_display" => "No Attributes to display",
"receipt_visibility" => "Receipt",
"show_in_customers" => "Show in customers",
"show_in_customers_visibility" => "Customers",
"show_in_employees" => "Show in employees",
"show_in_employees_visibility" => "Employees",
"show_in_items" => "Show in items",
"show_in_items_visibility" => "Items",
"show_in_receipt" => "Show in receipt",
@@ -34,7 +30,5 @@ return [
"show_in_receivings_visibility" => "Receivings",
"show_in_sales" => "Show in sales",
"show_in_sales_visibility" => "Sales",
"show_in_suppliers" => "Show in suppliers",
"show_in_suppliers_visibility" => "Suppliers",
"update" => "Update Attribute",
];

View File

@@ -82,40 +82,4 @@ class Email_lib
return $result;
}
/**
* Gets the mime type of the company logo file.
*
* @return string Mime type or empty string if logo doesn't exist
*/
public function getLogoMimeType(): string
{
$logo_path = FCPATH . 'uploads/' . $this->config['company_logo'];
if (!empty($this->config['company_logo']) && file_exists($logo_path)) {
$mimeType = mime_content_type($logo_path);
return $mimeType !== false ? $mimeType : '';
}
return '';
}
/**
* Builds an img tag for the company logo to use in email templates.
*
* @return string HTML img tag with base64-encoded logo, or empty string if no logo
*/
public function buildLogoImgTag(): string
{
$mimeType = $this->getLogoMimeType();
if ($mimeType === '') {
return '';
}
$logo_path = FCPATH . 'uploads/' . $this->config['company_logo'];
$logo_data = base64_encode(file_get_contents($logo_path));
return '<img id="image" src="data:' . $mimeType . ';base64,' . $logo_data . '" alt="company_logo">';
}
}

View File

@@ -27,14 +27,12 @@ class Attribute extends Model
'definition_type',
'definition_unit',
'definition_flags',
'person_attribute',
'deleted',
'attribute_id',
'definition_id',
'item_id',
'sale_id',
'receiving_id',
'person_id',
'attribute_value',
'attribute_date',
'attribute_decimal'
@@ -43,12 +41,7 @@ class Attribute extends Model
public const SHOW_IN_ITEMS = 1; // TODO: These need to be moved to constants.php
public const SHOW_IN_SALES = 2;
public const SHOW_IN_RECEIVINGS = 4;
public const SHOW_IN_SEARCH = 8;
public const SHOW_IN_CUSTOMERS = 16;
public const SHOW_IN_EMPLOYEES = 32;
public const SHOW_IN_SUPPLIERS = 64;
public function deleteDropdownAttributeValue(string $attributeValue, int $definitionId): bool
public function deleteDropdownAttributeValue(string $attribute_value, int $definition_id): bool
{
$attribute_id = $this->getAttributeIdByValue($attribute_value);
$this->deleteAttributeLinksByDefinitionIdAndAttributeId($definition_id, $attribute_id);
@@ -276,7 +269,7 @@ class Attribute extends Model
public function get_definitions_by_flags(int $definition_flags, bool $include_types = false): array
{
$builder = $this->db->table('attribute_definitions');
$builder->where(new RawSql("definition_flags & $definition_flags"));
$builder->where(new RawSql("definition_flags & $definition_flags")); // TODO: we need to heed CI warnings to escape properly
$builder->where('deleted', 0);
$builder->where('definition_type <>', GROUP);
$builder->orderBy('definition_id');
@@ -298,30 +291,11 @@ class Attribute extends Model
}
/**
* Gets attribute definitions filtered by type (person or item)
* Returns an array of attribute definition names and IDs
*
* @param bool $isPersonAttribute True for person attributes, false for item attributes
* @param int $definitionFlags Optional visibility flags to further filter
* @return array
* @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 getDefinitionsByType(bool $isPersonAttribute, int $definitionFlags = 0): array
{
$builder = $this->db->table('attribute_definitions');
$builder->where('person_attribute', $isPersonAttribute ? 1 : 0);
$builder->where('deleted', 0);
$builder->where('definition_type <>', GROUP);
if ($definitionFlags > 0) {
$builder->where(new RawSql("definition_flags & $definitionFlags"));
}
$builder->orderBy('definition_name', 'ASC');
$results = $builder->get()->getResultArray();
return $this->to_array($results, 'definition_id', 'definition_name');
}
public function get_definition_names(bool $groups = true): array
{
$builder = $this->db->table('attribute_definitions');
@@ -1253,227 +1227,4 @@ class Attribute extends Model
$itemsBuilder->update();
}
}
/**
* Gets all attributes connected to a person given the person_id
*
* @param int $personId Person to retrieve attributes for.
* @return array Attributes for the person.
*/
public function getAttributesByPerson(int $personId): array
{
$builder = $this->db->table('attribute_definitions');
$builder->join('attribute_links', 'attribute_links.definition_id = attribute_definitions.definition_id');
$builder->where('person_id', $personId);
$builder->where('item_id', null);
$builder->where('sale_id', null);
$builder->where('receiving_id', null);
$builder->where('deleted', 0);
$builder->orderBy('definition_name', 'ASC');
$results = $builder->get()->getResultArray();
return $this->to_array($results, 'definition_id');
}
/**
* Returns whether an attribute_link row exists given a person_id and optionally a definition_id
*
* @param int $personId ID of the person to check for an associated attribute.
* @param int|bool $definitionId Attribute definition ID to check.
* @return bool Returns true if at least one attribute_link exists or false if no attributes exist for that person and attribute.
*/
public function personAttributeLinkExists(int $personId, int|bool $definitionId = false): bool
{
$builder = $this->db->table('attribute_links');
$builder->where('person_id', $personId);
$builder->where('item_id', null);
$builder->where('sale_id', null);
$builder->where('receiving_id', null);
if ($definitionId) {
$builder->where('definition_id', $definitionId);
} else {
$builder->where('definition_id IS NOT NULL');
$builder->where('attribute_id', null);
}
$results = $builder->countAllResults();
return $results > 0;
}
/**
* Inserts or updates an attribute link for a person
*
* @param int $personId
* @param int $definitionId
* @param int $attributeId
* @return bool True if the attribute link was saved successfully, false otherwise.
*/
public function savePersonAttributeLink(int $personId, int $definitionId, int $attributeId): bool
{
$this->db->transStart();
$builder = $this->db->table('attribute_links');
if ($this->personAttributeLinkExists($personId, $definitionId)) {
$builder->set(['attribute_id' => $attributeId]);
$builder->where('definition_id', $definitionId);
$builder->where('person_id', $personId);
$builder->where('item_id', null);
$builder->where('sale_id', null);
$builder->where('receiving_id', null);
$builder->update();
} else {
$data = [
'attribute_id' => $attributeId,
'person_id' => $personId,
'definition_id' => $definitionId
];
$builder->insert($data);
}
$this->db->transComplete();
return $this->db->transStatus();
}
/**
* Deletes attribute links for a person
*
* @param int $personId
* @param int|bool $definitionId
* @return bool
*/
public function deletePersonAttributeLinks(int $personId, int|bool $definitionId = false): bool
{
$deleteData = ['person_id' => $personId];
$builder = $this->db->table('attribute_links');
$builder->where('item_id', null);
$builder->where('sale_id', null);
$builder->where('receiving_id', null);
if (!empty($definitionId)) {
$deleteData['definition_id'] = $definitionId;
}
return $builder->delete($deleteData);
}
/**
* Gets the attribute value for a person and definition
*
* @param int $personId
* @param int $definitionId
* @return object|null
*/
public function getPersonAttributeValue(int $personId, int $definitionId): ?object
{
$builder = $this->db->table('attribute_values');
$builder->join('attribute_links', 'attribute_links.attribute_id = attribute_values.attribute_id');
$builder->where('person_id', $personId);
$builder->where('item_id', null);
$builder->where('sale_id', null);
$builder->where('receiving_id', null);
$builder->where('definition_id', $definitionId);
$query = $builder->get();
if ($query->getNumRows() == 1) {
return $query->getRow();
}
return $this->getEmptyObject('attribute_values');
}
/**
* Saves an attribute value for a person
*
* @param string $attributeValue
* @param int $definitionId
* @param int $personId
* @param int|bool $attributeId
* @param string $definitionType
* @return int
*/
public function savePersonAttributeValue(string $attributeValue, int $definitionId, int $personId, int|bool $attributeId = false, string $definitionType = DROPDOWN): int
{
$config = config(OSPOS::class)->settings;
$this->db->transStart();
switch ($definitionType) {
case DATE:
$dataType = 'date';
$attributeDateValue = DateTime::createFromFormat($config['dateformat'], $attributeValue);
$attributeValue = $attributeDateValue ? $attributeDateValue->format('Y-m-d') : $attributeValue;
break;
case DECIMAL:
$dataType = 'decimal';
break;
default:
$dataType = 'value';
break;
}
// New Attribute
if (empty($attributeId) || empty($personId) || $attributeId == -1) {
$attributeId = $this->attributeValueExists($attributeValue, $definitionType);
if (!$attributeId) {
$builder = $this->db->table('attribute_values');
$builder->set(["attribute_$dataType" => $attributeValue]);
$builder->insert();
$attributeId = $this->db->insertID();
}
$data = [
'attribute_id' => empty($attributeId) ? null : $attributeId,
'person_id' => $personId,
'definition_id' => $definitionId
];
$builder = $this->db->table('attribute_links');
$builder->set($data);
$builder->insert();
}
// Existing Attribute
else {
$builder = $this->db->table('attribute_values');
$builder->set(["attribute_$dataType" => $attributeValue]);
$builder->where('attribute_id', $attributeId);
$builder->update();
}
$this->db->transComplete();
return $attributeId;
}
/**
* Gets link values for a person given the person_id and visibility flags
*
* @param int $personId
* @param int $definitionFlags
* @return ResultInterface
*/
public function getPersonLinkValues(int $personId, int $definitionFlags): ResultInterface
{
$format = $this->db->escape(dateformat_mysql());
$builder = $this->db->table('attribute_links');
$builder->select("GROUP_CONCAT(attribute_value SEPARATOR ', ') AS attribute_values");
$builder->select("GROUP_CONCAT(DATE_FORMAT(attribute_date, $format) SEPARATOR ', ') AS attribute_dtvalues");
$builder->join('attribute_values', 'attribute_values.attribute_id = attribute_links.attribute_id');
$builder->join('attribute_definitions', 'attribute_definitions.definition_id = attribute_links.definition_id');
$builder->where('definition_type <>', GROUP);
$builder->where('deleted', ACTIVE);
$builder->where('person_id', $personId);
$builder->where('item_id', null);
$builder->where('sale_id', null);
$builder->where('receiving_id', null);
$builder->where(new RawSql("definition_flags & $definitionFlags"));
return $builder->get();
}
}

View File

@@ -65,10 +65,8 @@ class Item extends Model
public function exists(string $item_id, bool $ignore_deleted = false, bool $deleted = false): bool
{
$builder = $this->db->table('items');
$builder->groupStart();
$builder->where('item_id', $item_id);
$builder->orWhere('item_number', $item_id);
$builder->groupEnd();
if (!$ignore_deleted) {
$builder->where('deleted', $deleted);
@@ -392,10 +390,8 @@ class Item extends Model
{
$builder = $this->db->table('items');
$builder->join('suppliers', 'suppliers.person_id = items.supplier_id', 'left');
$builder->groupStart();
$builder->where('item_number', $item_number);
$builder->orWhere('item_id', $item_number);
$builder->groupEnd();
if (!$ignore_deleted) {
$builder->where('items.deleted', $deleted);
@@ -440,32 +436,62 @@ class Item extends Model
/**
* Inserts or updates an item
*
* If the primary key (item_id) is present in the data array and the record exists,
* it will update the existing record. Otherwise, it will insert a new record.
*
* @param array $data The item data to save (passed by reference to set item_id on insert)
* @return bool True on success, false on failure
*/
public function save_value(array &$item_data, int $item_id = NEW_ENTRY): bool // TODO: need to bring this in line with parent or change the name
public function saveValue(array &$data): bool
{
$builder = $this->db->table('items');
$primaryKey = $this->primaryKey;
$id = $data[$primaryKey] ?? NEW_ENTRY;
if ($item_id < 1 || !$this->exists($item_id, true)) {
if ($builder->insert($item_data)) {
$item_data['item_id'] = (int)$this->db->insertID();
if ($item_id < 1) {
$builder = $this->db->table('items');
$builder->where('item_id', $item_data['item_id']);
$builder->update(['low_sell_item_id' => $item_data['item_id']]);
}
return true;
// If id > 0 and record exists by primary key only, update it
if ($id > 0) {
// Check existence strictly by primary key (regardless of soft-delete status)
$builder = $this->db->table('items');
$builder->where($primaryKey, $id);
$exists = $builder->countAllResults() > 0;
if ($exists) {
// Remove primary key from data array for update
$updateData = $data;
unset($updateData[$primaryKey]);
$builder = $this->db->table('items');
$builder->where($primaryKey, $id);
return $builder->update($updateData);
}
return false;
} else {
$item_data['item_id'] = $item_id;
}
// Insert new record with transaction for atomicity
$this->db->transBegin();
// Remove primary key from insert payload if present
$insertData = $data;
unset($insertData[$primaryKey]);
$builder = $this->db->table('items');
$builder->where('item_id', $item_id);
return $builder->update($item_data);
$success = $builder->insert($insertData);
if ($success) {
$data[$primaryKey] = (int)$this->db->insertID();
// Update low_sell_item_id for new items
$builder = $this->db->table('items');
$builder->where($primaryKey, $data[$primaryKey]);
$success = $builder->update(['low_sell_item_id' => $data[$primaryKey]]);
}
if ($success) {
$this->db->transCommit();
return true;
}
$this->db->transRollback();
return false;
}
/**
@@ -1083,9 +1109,9 @@ class Item extends Model
$total_quantity = $old_total_quantity + $items_received;
$average_price = bcdiv(bcadd(bcmul((string)$items_received, (string)$new_price), bcmul((string)$old_total_quantity, (string)$old_price)), (string)$total_quantity);
$data = ['cost_price' => $average_price];
$data = ['cost_price' => $average_price, 'item_id' => $item_id];
return $this->save_value($data, $item_id);
return $this->saveValue($data);
}
/**

View File

@@ -1,169 +0,0 @@
<?php
/**
* @var array $definition_names
* @var array $definition_values
* @var int $person_id
* @var array $config
*/
use App\Models\Attribute;
?>
<div class="form-group form-group-sm">
<?= form_label(lang('Attributes.definition_name'), 'definition_name_label', ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-8">
<?= form_dropdown([
'name' => 'definition_name',
'options' => $definition_names,
'selected' => -1,
'class' => 'form-control',
'id' => 'definition_name'
]) ?>
</div>
</div>
<?php foreach ($definition_values as $definitionId => $definitionValue) { ?>
<div class="form-group form-group-sm">
<?= form_label(esc($definitionValue['definition_name']), esc($definitionValue['definition_name']), ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-8">
<div class="input-group">
<?php
echo form_hidden("attribute_ids[$definitionId]", strval($definitionValue['attribute_id']));
$attributeValue = $definitionValue['attribute_value'];
switch ($definitionValue['definition_type']) {
case DATE:
$value = (empty($attributeValue) || empty($attributeValue->attribute_date)) ? NOW : strtotime($attributeValue->attribute_date);
echo form_input([
'name' => "attribute_links[$definitionId]",
'value' => to_date($value),
'class' => 'form-control input-sm datetime',
'data-definition-id' => $definitionId,
'readonly' => 'true'
]);
break;
case DROPDOWN:
$selectedValue = $definitionValue['selected_value'];
echo form_dropdown([
'name' => "attribute_links[$definitionId]",
'options' => $definitionValue['values'],
'selected' => $selectedValue,
'class' => 'form-control',
'data-definition-id' => $definitionId
]);
break;
case TEXT:
$value = (empty($attributeValue) || empty($attributeValue->attribute_value)) ? $definitionValue['selected_value'] : $attributeValue->attribute_value;
echo form_input([
'name' => "attribute_links[$definitionId]",
'value' => esc($value),
'class' => 'form-control valid_chars',
'data-definition-id' => $definitionId
]);
break;
case DECIMAL:
$value = (empty($attributeValue) || empty($attributeValue->attribute_decimal)) ? $definitionValue['selected_value'] : $attributeValue->attribute_decimal;
echo form_input([
'name' => "attribute_links[$definitionId]",
'value' => to_decimals((float)$value),
'class' => 'form-control valid_chars',
'data-definition-id' => $definitionId
]);
break;
case CHECKBOX:
$value = (empty($attributeValue) || empty($attributeValue->attribute_value)) ? $definitionValue['selected_value'] : $attributeValue->attribute_value;
// Sends 0 if the box is unchecked instead of not sending anything.
echo form_input([
'type' => 'hidden',
'name' => "attribute_links[$definitionId]",
'id' => "attribute_links[$definitionId]",
'value' => 0,
'data-definition-id' => $definitionId
]);
echo form_checkbox([
'name' => "attribute_links[$definitionId]",
'id' => "attribute_links[$definitionId]",
'value' => 1,
'checked' => $value == 1,
'class' => 'checkbox-inline',
'data-definition-id' => $definitionId
]);
break;
}
?>
<span class="input-group-addon input-sm btn btn-default remove_attribute_btn">
<span class="glyphicon glyphicon-trash"></span>
</span>
</div>
</div>
</div>
<?php } ?>
<script type="text/javascript">
(function() {
<?= view('partial/datepicker_locale', ['format' => dateformat_bootstrap($config['dateformat'])]) ?>
var enableDelete = function() {
$('.remove_attribute_btn').click(function() {
$(this).parents('.form-group').remove();
});
};
enableDelete();
$("input[name*='attribute_links']").change(function() {
var definitionId = $(this).data('definition-id');
$("input[name='attribute_ids[" + definitionId + "]']").val('');
}).autocomplete({
source: function(request, response) {
$.get('<?= 'attributes/suggestAttribute/' ?>' + this.element.data('definition-id') + '?term=' + request.term, function(data) {
return response(data);
}, 'json');
},
appendTo: '.modal-content',
select: function(event, ui) {
event.preventDefault();
$(this).val(ui.item.label);
},
delay: 10
});
var getDefinitionValues = function() {
var result = {};
$("[name*='attribute_links']").each(function() {
var definitionId = $(this).data('definition-id');
var element = $(this);
// For checkboxes, use the visible checkbox, not the hidden input
if (element.attr('type') === 'hidden' && element.siblings('input[type="checkbox"]').length > 0) {
// Skip hidden inputs that have a corresponding checkbox
return;
}
// For checkboxes, get the checked state
if (element.attr('type') === 'checkbox') {
result[definitionId] = element.prop('checked') ? '1' : '0';
} else {
result[definitionId] = element.val();
}
});
return result;
};
var refresh = function() {
var definitionId = $("#definition_name option:selected").val();
var attributeValues = getDefinitionValues();
attributeValues[definitionId] = '';
$('#person_attributes').load(window.location.href, {
'definition_ids': JSON.stringify(attributeValues)
}, enableDelete);
};
$('#definition_name').change(function() {
refresh();
});
})();
</script>

View File

@@ -13,17 +13,17 @@ $barcode_lib = new Barcode_lib();
<html lang="<?= current_language_code() ?>">
<head>
<meta charset="utf-8">
<title><?= esc(lang('Items.generate_barcodes')) ?></title>
<link rel="stylesheet" href="<?= esc(base_url('css/barcode_font.css'), 'url') ?>">
<title><?= lang('Items.generate_barcodes') ?></title>
<link rel="stylesheet" href="<?= base_url() ?>css/barcode_font.css">
<style>
.barcode svg {
height: <?= (int) $barcode_config['barcode_height'] ?>px;
width: <?= (int) $barcode_config['barcode_width'] ?>px;
height: <?= $barcode_config['barcode_height'] ?>px;
width: <?= $barcode_config['barcode_width'] ?>px;
}
</style>
</head>
<body class="<?= esc('font_' . $barcode_lib->get_font_name($barcode_config['barcode_font']), 'attr') ?>" style="font-size: <?= (int) $barcode_config['barcode_font_size'] ?>px;">
<table style="border-spacing: <?= (int) $barcode_config['barcode_page_cellspacing'] ?>px; width: <?= (int) $barcode_config['barcode_page_width'] ?>%;">
<body class=<?= 'font_' . $barcode_lib->get_font_name($barcode_config['barcode_font']) ?> style="font-size: <?= $barcode_config['barcode_font_size'] ?>px;">
<table style="border-spacing: <?= $barcode_config['barcode_page_cellspacing'] ?>; width: <?= $barcode_config['barcode_page_width'] ?>%;">
<tr>
<?php
$count = 0;

View File

@@ -44,12 +44,6 @@
<?= view('people/form_basic_info') ?>
<div id="person_attributes">
<script type="text/javascript">
$('#person_attributes').load('<?= "customers/attributes/$person_info->person_id" ?>');
</script>
</div>
<div class="form-group form-group-sm">
<?= form_label(lang('Customers.discount_type'), 'discount_type', ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-8">

View File

@@ -29,12 +29,6 @@
<div class="tab-pane fade in active" id="employee_basic_info">
<fieldset>
<?= view('people/form_basic_info') ?>
<div id="person_attributes">
<script type="text/javascript">
$('#person_attributes').load('<?= "employees/attributes/$person_info->person_id" ?>');
</script>
</div>
</fieldset>
</div>

View File

@@ -45,12 +45,6 @@
<?= view('people/form_basic_info') ?>
<div id="person_attributes">
<script type="text/javascript">
$('#person_attributes').load('<?= "suppliers/attributes/$person_info->person_id" ?>');
</script>
</div>
<div class="form-group form-group-sm">
<?= form_label(lang('Suppliers.account_number'), 'account_number', ['class' => 'control-label col-xs-3']) ?>
<div class="col-xs-8">

11
package-lock.json generated
View File

@@ -28,7 +28,7 @@
"chartist-plugin-tooltips": "^0.0.17",
"clipboard": "^2.0.11",
"coffeescript": "^2.7.0",
"dompurify": "^3.4.0",
"dompurify": "^3.3.2",
"elegant-circles": "github:opensourcepos/elegant-circles#minified",
"es6-promise": "^4.2.8",
"file-saver": "^2.0.5",
@@ -1483,10 +1483,13 @@
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="
},
"node_modules/dompurify": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.0.tgz",
"integrity": "sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz",
"integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"engines": {
"node": ">=20"
},
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}

View File

@@ -49,7 +49,7 @@
"chartist-plugin-tooltips": "^0.0.17",
"clipboard": "^2.0.11",
"coffeescript": "^2.7.0",
"dompurify": "^3.4.0",
"dompurify": "^3.3.2",
"elegant-circles": "github:opensourcepos/elegant-circles#minified",
"es6-promise": "^4.2.8",
"file-saver": "^2.0.5",

View File

@@ -237,7 +237,7 @@ class ItemsCsvImportTest extends CIUnitTestCase
'deleted' => 0
];
$this->assertTrue($this->item->save_value($itemData));
$this->assertTrue($this->item->saveValue($itemData));
$row = $this->db->table('items')
->where('item_number', $itemData['item_number'])
@@ -268,7 +268,7 @@ class ItemsCsvImportTest extends CIUnitTestCase
'deleted' => 0
];
$this->assertTrue($this->item->save_value($itemData));
$this->assertTrue($this->item->saveValue($itemData));
$locationId = 1;
$quantity = 100;
@@ -298,7 +298,7 @@ class ItemsCsvImportTest extends CIUnitTestCase
'deleted' => 0
];
$this->assertTrue($this->item->save_value($itemData));
$this->assertTrue($this->item->saveValue($itemData));
$inventoryData = [
'trans_inventory' => 50,
@@ -329,7 +329,7 @@ class ItemsCsvImportTest extends CIUnitTestCase
'deleted' => 0
];
$this->assertTrue($this->item->save_value($itemData));
$this->assertTrue($this->item->saveValue($itemData));
$taxesData = [
['name' => 'VAT', 'percent' => 20],
@@ -406,7 +406,7 @@ class ItemsCsvImportTest extends CIUnitTestCase
'deleted' => false
];
$this->assertTrue($this->item->save_value($itemData));
$this->assertTrue($this->item->saveValue($itemData));
}
$item1 = $this->item->get_info_by_id_or_number('ITEM-A');
@@ -430,7 +430,7 @@ class ItemsCsvImportTest extends CIUnitTestCase
'deleted' => 0
];
$this->assertTrue($this->item->save_value($originalData));
$this->assertTrue($this->item->saveValue($originalData));
$updatedData = [
'item_id' => $originalData['item_id'],
@@ -443,7 +443,7 @@ class ItemsCsvImportTest extends CIUnitTestCase
'deleted' => 0
];
$this->assertTrue($this->item->save_value($updatedData, $updatedData['item_id']));
$this->assertTrue($this->item->saveValue($updatedData));
$updatedItem = $this->item->get_info($updatedData['item_id']);
$this->assertEquals('Updated Name', $updatedItem->name);
@@ -464,7 +464,7 @@ class ItemsCsvImportTest extends CIUnitTestCase
'deleted' => 0
];
$this->assertTrue($this->item->save_value($originalData));
$this->assertTrue($this->item->saveValue($originalData));
$definitionData = [
'definition_name' => 'Color',
@@ -510,7 +510,7 @@ class ItemsCsvImportTest extends CIUnitTestCase
'deleted' => 0
];
$this->assertTrue($this->item->save_value($itemData));
$this->assertTrue($this->item->saveValue($itemData));
$definitionData = [
'definition_name' => 'Color',
@@ -553,7 +553,7 @@ class ItemsCsvImportTest extends CIUnitTestCase
'deleted' => 0
];
$this->assertTrue($this->item->save_value($itemData));
$this->assertTrue($this->item->saveValue($itemData));
// Mock Attribute DROPDOWN
$definitionData = [
@@ -604,7 +604,7 @@ class ItemsCsvImportTest extends CIUnitTestCase
'deleted' => 0
];
$this->assertTrue($this->item->save_value($itemData));
$this->assertTrue($this->item->saveValue($itemData));
$locationId = 1;
@@ -633,7 +633,7 @@ class ItemsCsvImportTest extends CIUnitTestCase
'deleted' => 0
];
$this->assertTrue($this->item->save_value($itemData));
$this->assertTrue($this->item->saveValue($itemData));
$savedItem = $this->item->get_info($itemData['item_id']);
$this->assertEquals(-1, (int)$savedItem->reorder_level);
@@ -672,7 +672,7 @@ class ItemsCsvImportTest extends CIUnitTestCase
'deleted' => 0
];
$this->assertTrue($this->item->save_value($itemData));
$this->assertTrue($this->item->saveValue($itemData));
$savedItem = $this->item->get_info($itemData['item_id']);
@@ -702,7 +702,7 @@ class ItemsCsvImportTest extends CIUnitTestCase
'deleted' => 0
];
$this->assertTrue($this->item->save_value($itemData));
$this->assertTrue($this->item->saveValue($itemData));
$savedItem = $this->item->get_info($itemData['item_id']);
$this->assertEquals('8471', $savedItem->hsn_code);
@@ -719,7 +719,7 @@ class ItemsCsvImportTest extends CIUnitTestCase
'deleted' => 0
];
$this->assertTrue($this->item->save_value($itemData));
$this->assertTrue($this->item->saveValue($itemData));
$locations = [
'Warehouse' => 100,
@@ -792,7 +792,7 @@ class ItemsCsvImportTest extends CIUnitTestCase
'deleted' => 0
];
$this->assertTrue($this->item->save_value($itemData));
$this->assertTrue($this->item->saveValue($itemData));
$this->assertIsInt($itemData['item_id']);
$this->assertGreaterThan(0, $itemData['item_id']);
@@ -812,7 +812,7 @@ class ItemsCsvImportTest extends CIUnitTestCase
'deleted' => 0
];
$this->assertTrue($this->item->save_value($itemData));
$this->assertTrue($this->item->saveValue($itemData));
$exists = $this->item->exists($itemData['item_id']);
$this->assertTrue($exists);
@@ -858,7 +858,7 @@ class ItemsCsvImportTest extends CIUnitTestCase
'deleted' => 0
];
$this->assertTrue($this->item->save_value($itemData));
$this->assertTrue($this->item->saveValue($itemData));
$taxesData = [];
if (is_numeric($csvRow['Tax 1 Percent']) && $csvRow['Tax 1 Name'] !== '') {
@@ -1032,7 +1032,7 @@ class ItemsCsvImportTest extends CIUnitTestCase
'deleted' => 0
];
$this->assertTrue($this->item->save_value($itemData));
$this->assertTrue($this->item->saveValue($itemData));
$uniqueId = uniqid();
$locations = ['Warehouse' . $uniqueId, 'Store' . $uniqueId];