Compare commits

..

6 Commits

Author SHA1 Message Date
Ollama
62236aec30 refactor: Extract duplicated code into reusable components
- Created app/Traits/Controller/Shared.php with helper methods for supplier info, sale mode labels, company info, and tax code data
- Created app/Traits/Models/Reports/ReportDateFilter.php for date filtering logic across reports
- Created app/Traits/Models/Reports/SaleTypeFilter.php for sale type filtering pattern
- Created app/Traits/Database/SalesTaxMigration.php for migration tax handling
- Refactored Sales.php to use Shared trait for mode labels and company info
- Refactored Taxes.php to use Shared trait for tax code initialization
- Refactored Receivings.php to use Shared trait for supplier info building
- Refactored Summary_report.php, Summary_payments.php, Summary_sales_taxes.php, Summary_expenses_categories.php to use ReportDateFilter and SaleTypeFilter traits
- Refactored Detailed_sales.php to use SaleTypeFilter trait
- Refactored both tax migrations to use SalesTaxMigration trait
- Removed 39 TODO: Duplicated code comments across 19 files

Closes #4490
2026-04-15 12:49:31 +00:00
Ollama
905b58ca6e [Fix]: Add missing return statements to Sales Controller functions
- Fix postComplete(): Add return keyword for error redirect paths
  (lines 799, 843, 871) when duplicate invoice/work_order/quote numbers
- Fix postChangeItemNumber(): Add return statement returning JSON response
- Fix postChangeItemName(): Add return statement returning JSON response
- Fix postChangeItemDescription(): Add return statement returning JSON response

All 4 functions declared return types but were missing return statements,
causing potential runtime errors in certain code paths.

Resolves #4492
2026-04-15 06:49:12 +00:00
dependabot[bot]
609b206375 Bump lodash from 4.17.23 to 4.18.1 (#4462)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.23 to 4.18.1.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.18.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: objecttothis <17935339+objecttothis@users.noreply.github.com>
2026-04-14 01:21:43 +04:00
objecttothis
6fec2464f8 Update to CodeIgniter 4.7.2 (#4485)
- Merge Config and Core File Changes 4.6.3 > 4.6.4
- Merge Config and Core File Changes 4.6.4 > 4.7.0
- Added app\Config\WorkerMode.php
- Merge Config and Core File Changes Not previously merged
- Added app\Config\Hostnames.php
- Corrected incorrect CSS property used in invoice.php view.
- Corrected unknown CSS properties used in register.php view.
- Used shorthand CSS in debug.css
- Corrected indentation in barcode_sheet.php view.
- Corrected indentation in footer.php view.
- Corrected indentation in invoice_email.php view.
- Replaced obsolete attributes with CSS style attributes in barcode_sheet.php
- Replaced obsolete attribute in error_exception.php
- Replaced obsolete attribute in invoice_email.php
- Replaced obsolete attribute in quote_email.php
- Replaced obsolete attributes in work_order_email.php
- Fixed indentation in system_info.php
- Replaced <strong> tag outside <p> tags, which isn't allowed, with style attributes.
- Simplified js return logic and indentation fixes in tax_categories.php
- Simplified js return logic in tax_codes.php
- Simplified js return logic in tax_jurisdictions.php
- Removed unnecessary labels in manage views.
- Rewrite JavaScript function and PHP to be more readable in bar.php, hbar.php, line.php and pie.php
- Added type declarations, return types and an import to app\Config\Services
- Updated Attribute.php parameter type
- Updated Receiving_lib.php parameter type
- Updated Receivings.php parameter types and updated PHPdocs
- Updated tabular_helper.php parameter types and updated PHPdocs
- Added type declarations and corrected PHPdocs in url_helper.php
- Added return types to functions
- Revert $objectSrc value in ContentSecurityPolicy.php
- Correct return type in Customer->get_stats()
- Correct return type in Item->get_info_by_id_or_number()
- Correct misspelling in border-spacing
- Added missing css style semicolons
- Resolve operator precedence ambiguity.
- Resolve column mismatch.
- Added missing escaping in view.
- Updated requirement for PHP 8.2
- Resolve unresolved conflicts
- Added PHP 8.2 requirement to the README.md
- Fixed bugs in display of UI
- Fixed duplicated `>` in app\Views\Expenses\manage.php
- Removed excess whitespace at the end of some lines in table_filter_persistence.php
- Added missing `>` in app\Views\Expenses\manage.php
- Corrected grammar in PHPdoc in table_filter_persistence.php
- Remove bug causing `\` to be injected into the new giftcard value
- Fix bug causing DROPDOWN Attribute Values to not save correctly
- Added check for null in $normalizedItemId

- Removing < PHP 8.2 from linting and tests
- Update Linter to not include PHP 8.2 and 8.1
- Remove PHP 8.1 unit test cycle.
- Update Bug Report Template
- Update Composer files for CodeIgniter 4.7.2
- Updated INSTALL.md to reflect changes.

---------

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-14 01:05:10 +04:00
jekkos
332d8c8c69 fix: change docker image tag to master 2026-04-10 23:58:38 +02:00
objecttothis
577cf55b6a [Feature]: Case-sensitive attribute updates and CSV Import attribute deletion capability (#4384)
PSR and Readability Changes
- Removed unused import
- Corrected PHPdoc to include the correct return type
- Refactored out a function to get attribute data from the row in a CSV item import.
- refactored snake_case variables and function names to camelCase
- Refactored the naming of saveAttributeData() to better reflect the functions purpose.
- Improved PHPdocs
- Remove whitespace
- Remove unneeded comment
- Refactored abbreviated variable name for clarity
- Removed $csvHeaders as it is unused
- Corrected spacing and curly brace location
- Refactored Stock Locations validation inside general validation

Bugfixes
- Fixed bug causing attribute_id and item_id to not be properly assigned when empty() returns true.
- Fixed bug causing CSV Item import to not update barcode when changed in the import file.
- Fixed saveAttributeValue() logic causing attribute_value to be updated to a value that already exists for a different attribute_id
- Fixed bug preventing Category as dropdown functionality from working
- Fixed bug preventing barcodes from updating. in Item CSV Imports
- Corrected bug in stock_location->save_value()
- Corrected incorrect helper file references.
- Removed duplicate call to save attribute link
- Rollback transaction on failure before returning false
- Rollback transaction and return 0 on failure to save attribute link.
- Account for '0' being an acceptable TEXT or DECIMAL attributeValue.
- Corrected Business logic
- Resolved incorrect array key
- Account for 0 in column values
- Correct check empty attribute check
- Previously 0 would have been skipped even though that's a valid value for an attribute.
- Removed unused foreach loop index variables
- Corrected CodeIgniter Framework version to specific version

UnitTest Seeder and tests
- Created a seeder to automatically prepare the test database.
- Modified the Unit Test setup to properly seed the test database.
- Wrote a unit test to test deleting an attribute from an item through the CSV.
- Corrected errors in unit tests preventing them from passing. save_value() returns a bool, not the itemId
- Fix Unit Tests that were failing
- Corrected the logic in itemUpdate test
- Replaced precision test with one reflecting testing of actual value.
- This test does not test cash rounding rules. That should go into a different test.
- Correct expected value in test.
- Update app/Database/Seeds/TestDatabaseBootstrapSeeder.php
- Added check to testImportDeleteAttributeFromExistingItem
- Correct mocking of dropdowns
- Remove code depending on removed database.sql
- Removed FQN in seeder() call
- Added checks in Database seeder
- Moved the function to the attribute model where it belongs which allows testability.

Case Change Capability (CSV Import and Form)
- CSV Import and view Case Changes of `attribute_value`
- Store attribute even when just case is different.
- Add getAttributeValueByAttributeId() to assist in comparing the value
- Corrected Capitalization in File Handling Logic

CSV Import Attribute Link Deletion Capability
- Validation checks bypass magic word cells.
- Delete the attribute link for an item if the CSV contains `_DELETE_`
- Added calls to deleteOrphanedValues()
- Items CSV Import Attribute Delete
- Exclude the itemId in the check to see if the barcode number exists

Error Checking and Reporting Improvements
- Fail the import if an invalid stock location is found in the CSV
- Return false if deleteAttributeLinks fails
- Match sanitization of description field to Form submission import
- Fold errors into result and return value
- Populated $allowedStockLocations before sending it to the validation function
- Added logic to not ignore failed saveItemAttributes calls
- Add error checking to failed row insert
- Reworked &= to && logic so that it short-circuits the function call after if success is already false.
- Add transaction to storeCSVAttributeValue function to prevent deleting the attribute links before confirming the new value successfully saved.
- Modified generate_message in Db_log.php to be defensive.

Attribute Improvements
- Move ATTRIBUTE_VALUE_TYPES to the helper
- Normalize AttributeId in saveAttributeLink()
- normalize itemId in saveAttributeLink()
- Account for '0' in column values for allow_alt_description
- Remove duplicate saveAttributeValue call
- Correct return value of function
- Like other save_value() functions, the location_data variable is passed by reference.
- Unlike other save_value() functions, the location_data variable is not being updated with the primary key id.
- Added updateAttributeValue() function as part of logic fix.
- Added attribute_helper.php
- Simplified logic to store attribute values

---------

Signed-off-by: objec <objecttothis@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-04-09 11:13:22 +04:00
111 changed files with 2845 additions and 2532 deletions

View File

@@ -12,11 +12,11 @@ body:
attributes:
value: |
## Thanks for taking the time to fill out this bug report! 🐜
Bug reports help us identify and fix issues. Please provide as much detail as possible.
> ⚠️ **Important:** Submit a separate bug report for each problem you encounter.
>
>
> 🚫 Do not include personal identifying information such as email addresses or encryption keys.
# ─────────────────────────────────────────────────────────────────────────────
@@ -28,7 +28,7 @@ body:
label: 🐛 Bug Description
description: A clear and concise description of what the bug is.
placeholder: |
Example: When I try to print a receipt, the application crashes
Example: When I try to print a receipt, the application crashes
with an error message saying "Unable to connect to printer".
validations:
required: true
@@ -86,8 +86,7 @@ body:
- PHP 8.2
- PHP 8.1
- PHP 7.4
- PHP 7.3
- PHP 7.2
- Other
default: 0
validations:
required: true
@@ -141,7 +140,7 @@ body:
label: 📊 System Information Report
description: |
Copy and paste the system information from OSPOS:
**Navigation:** Configuration → Setup & Conf → System Info
placeholder: |
Paste the System Information Report here...
@@ -155,7 +154,7 @@ body:
label: 📜 Relevant Log Output
description: |
Please copy and paste any relevant log output.
**Log locations:**
- OSPOS logs: `writable/logs/`
- Web server logs: `/var/log/apache2/` or `/var/log/nginx/`
@@ -185,4 +184,4 @@ body:
- label: I have searched existing issues to ensure this bug has not already been reported
required: true
- label: I have provided all the information requested above
required: true
required: true

View File

@@ -155,7 +155,7 @@ jobs:
run: |
BRANCH=$(echo "${GITHUB_REF#refs/heads/}" | tr '/' '_')
if [ "$BRANCH" = "master" ]; then
echo "tags=${{ secrets.DOCKER_USERNAME }}/opensourcepos:${{ needs.build.outputs.version-tag }},${{ secrets.DOCKER_USERNAME }}/opensourcepos:latest" >> $GITHUB_OUTPUT
echo "tags=${{ secrets.DOCKER_USERNAME }}/opensourcepos:${{ needs.build.outputs.version-tag }},${{ secrets.DOCKER_USERNAME }}/opensourcepos:master" >> $GITHUB_OUTPUT
else
echo "tags=${{ secrets.DOCKER_USERNAME }}/opensourcepos:${{ needs.build.outputs.version-tag }}" >> $GITHUB_OUTPUT
fi

View File

@@ -28,7 +28,6 @@ jobs:
fail-fast: false
matrix:
php-version:
- '8.1'
- '8.2'
- '8.3'
- '8.4'

View File

@@ -12,14 +12,6 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: PHP Lint 8.0
uses: dbfx/github-phplint/8.0@master
with:
folder-to-exclude: "! -path \"./vendor/*\" ! -path \"./folder/excluded/*\""
- name: PHP Lint 8.1
uses: dbfx/github-phplint/8.1@master
with:
folder-to-exclude: "! -path \"./vendor/*\" ! -path \"./folder/excluded/*\""
- name: PHP Lint 8.2
uses: dbfx/github-phplint/8.2@master
with:

View File

@@ -34,7 +34,6 @@ jobs:
fail-fast: false
matrix:
php-version:
- '8.1'
- '8.2'
- '8.3'
- '8.4'
@@ -119,4 +118,4 @@ jobs:
- name: Stop MariaDB
if: always()
run: docker stop mysql && docker rm mysql
run: docker stop mysql && docker rm mysql

View File

@@ -1,6 +1,6 @@
## Server Requirements
- PHP version `8.1` to `8.4` are supported, PHP version `≤7.4` is NOT supported. Please note that PHP needs to have the extensions `php-json`, `php-gd`, `php-bcmath`, `php-intl`, `php-openssl`, `php-mbstring`, `php-curl` and `php-xml` installed and enabled. An unstable master build can be downloaded in the releases section.
- PHP version `8.2` to `8.4` are supported, PHP version `≤ 8.1` is NOT supported. Please note that PHP needs to have the extensions `php-json`, `php-gd`, `php-bcmath`, `php-intl`, `php-openssl`, `php-mbstring`, `php-curl` and `php-xml` installed and enabled. An unstable master build can be downloaded in the releases section.
- MySQL `5.7` is supported, also MariaDB replacement `10.x` is supported and might offer better performance.
- Apache `2.4` is supported. Nginx should work fine too, see [wiki page here](https://github.com/opensourcepos/opensourcepos/wiki/Local-Deployment-using-LEMP).
- Raspberry PI based installations proved to work, see [wiki page here](<https://github.com/opensourcepos/opensourcepos/wiki/Installing-on-Raspberry-PI---Orange-PI-(Headless-OSPOS)>).

View File

@@ -102,7 +102,7 @@ NOTE: If you're running non-release code, please make sure you always run the la
- If you have suhosin installed and face an issue with CSRF, please make sure you read [issue #1492](https://github.com/opensourcepos/opensourcepos/issues/1492).
- PHP `≥ 8.1` is required to run this app.
- PHP `≥ 8.2` is required to run this app.
## 🏃 Keep the Machine Running

View File

@@ -55,21 +55,13 @@ class App extends BaseConfig
public string $baseURL; // Defined in the constructor
/**
* Allowed Hostnames for the Site URL.
*
* Security: This is used to validate the HTTP Host header to prevent
* Host Header Injection attacks. If the Host header doesn't match
* an entry in this list, the request will use the first allowed hostname.
*
* IMPORTANT: This MUST be configured for production deployments.
* If empty in production, the application will fail to start.
* In development, it will fall back to 'localhost' with a warning.
*
* Configure via .env file (comma-separated list):
* app.allowedHostnames = 'example.com,www.example.com'
*
* For local development:
* app.allowedHostnames = 'localhost'
* Allowed Hostnames in the Site URL other than the hostname in the baseURL.
* If you want to accept multiple Hostnames, set this.
*
* E.g.,
* When your site URL ($baseURL) is 'http://example.com/', and your site
* also accepts 'http://media.example.com/' and 'http://accounts.example.com/':
* ['media.example.com', 'accounts.example.com']
*
* @var list<string>
*/
@@ -125,7 +117,7 @@ class App extends BaseConfig
| DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!!
|
*/
public string $permittedURIChars = 'a-z 0-9~%.:_\-=';
public string $permittedURIChars = 'a-z 0-9~%.:_\-';
/**
* --------------------------------------------------------------------------
@@ -286,12 +278,12 @@ class App extends BaseConfig
* @see http://www.html5rocks.com/en/tutorials/security/content-security-policy/
* @see http://www.w3.org/TR/CSP/
*/
public bool $CSPEnabled = false; // TODO: Currently CSP3 tags are not supported so enabling this causes problems with script-src-elem, style-src-attr and style-src-elem
public bool $CSPEnabled = false;
public function __construct()
{
parent::__construct();
// Solution for CodeIgniter 4 limitation: arrays cannot be set from .env
// See: https://github.com/codeigniter4/CodeIgniter4/issues/7311
$envAllowedHostnames = getenv('app.allowedHostnames');
@@ -301,9 +293,9 @@ class App extends BaseConfig
static fn (string $hostname): bool => $hostname !== ''
));
}
$this->https_on = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') || (isset($_ENV['FORCE_HTTPS']) && $_ENV['FORCE_HTTPS'] == 'true');
$host = $this->getValidHost();
$this->baseURL = $this->https_on ? 'https' : 'http';
$this->baseURL .= '://' . $host . '/';
@@ -312,39 +304,39 @@ class App extends BaseConfig
/**
* Validates and returns a trusted hostname.
*
*
* Security: Prevents Host Header Injection attacks (GHSA-jchf-7hr6-h4f3)
* by validating the HTTP_HOST against a whitelist of allowed hostnames.
*
*
* In production: Fails fast if allowedHostnames is not configured.
* In development: Allows localhost fallback with an error log.
*
*
* @return string A validated hostname
* @throws \RuntimeException If allowedHostnames is not configured in production
*/
private function getValidHost(): string
{
$httpHost = $_SERVER['HTTP_HOST'] ?? 'localhost';
// Determine environment
// CodeIgniter's test bootstrap sets $_SERVER['CI_ENVIRONMENT'] = 'testing'
// Check $_SERVER first, then $_ENV, then fall back to 'production'
$environment = $_SERVER['CI_ENVIRONMENT'] ?? $_ENV['CI_ENVIRONMENT'] ?? getenv('CI_ENVIRONMENT') ?: 'production';
if (empty($this->allowedHostnames)) {
$errorMessage =
$errorMessage =
'Security: allowedHostnames is not configured. ' .
'Host header injection protection is disabled. ' .
'Set app.allowedHostnames in your .env file. ' .
'Example: app.allowedHostnames = "example.com,www.example.com" ' .
'Received Host: ' . $httpHost;
// Production: Fail explicitly to prevent silent security vulnerabilities
// Testing and development: Allow localhost fallback
if ($environment === 'production') {
throw new \RuntimeException($errorMessage);
}
log_message('error', $errorMessage . ' Using localhost fallback (development only).');
return 'localhost';
}
@@ -354,7 +346,7 @@ class App extends BaseConfig
}
// Host not in whitelist - use first configured hostname as fallback
log_message('warning',
log_message('warning',
'Security: Rejected HTTP_HOST "' . $httpHost . '" - not in allowedHostnames whitelist. ' .
'Using fallback: ' . $this->allowedHostnames[0]
);

View File

@@ -17,8 +17,6 @@ use CodeIgniter\Config\AutoloadConfig;
*
* NOTE: This class is required prior to Autoloader instantiation,
* and does not extend BaseConfig.
*
* @immutable
*/
class Autoload extends AutoloadConfig
{

View File

@@ -1,23 +1,38 @@
<?php
/*
* The environment testing is reserved for PHPUnit testing. It has special
* conditions built into the framework at various places to assist with that.
* You cant use it for your development.
*/
/*
|--------------------------------------------------------------------------
| ERROR DISPLAY
| ERROR DISPLAY
|--------------------------------------------------------------------------
*/
| In development, we want to show as many errors as possible to help
| make sure they don't make it to production. And save us hours of
| painful debugging.
*/
error_reporting(E_ALL);
ini_set('display_errors', '1');
/*
|--------------------------------------------------------------------------
| DEBUG BACKTRACES
| DEBUG BACKTRACES
|--------------------------------------------------------------------------
*/
| If true, this constant will tell the error screens to display debug
| backtraces along with the other error information. If you would
| prefer to not see this, set this value to false.
*/
defined('SHOW_DEBUG_BACKTRACE') || define('SHOW_DEBUG_BACKTRACE', true);
/*
|--------------------------------------------------------------------------
| DEBUG MODE
| DEBUG MODE
|--------------------------------------------------------------------------
*/
defined('CI_DEBUG') || define('CI_DEBUG', true);
| Debug mode is an experimental flag that can allow changes throughout
| the system. It's not widely used currently, and may not survive
| release of the framework.
*/
defined('CI_DEBUG') || define('CI_DEBUG', true);

View File

@@ -6,6 +6,22 @@ use CodeIgniter\Config\BaseConfig;
class CURLRequest extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* CURLRequest Share Connection Options
* --------------------------------------------------------------------------
*
* Share connection options between requests.
*
* @var list<int>
*
* @see https://www.php.net/manual/en/curl.constants.php#constant.curl-lock-data-connect
*/
public array $shareConnectionOptions = [
CURL_LOCK_DATA_CONNECT,
CURL_LOCK_DATA_DNS,
];
/**
* --------------------------------------------------------------------------
* CURLRequest Share Options

View File

@@ -3,6 +3,7 @@
namespace Config;
use CodeIgniter\Cache\CacheInterface;
use CodeIgniter\Cache\Handlers\ApcuHandler;
use CodeIgniter\Cache\Handlers\DummyHandler;
use CodeIgniter\Cache\Handlers\FileHandler;
use CodeIgniter\Cache\Handlers\MemcachedHandler;
@@ -78,7 +79,7 @@ class Cache extends BaseConfig
* Your file storage preferences can be specified below, if you are using
* the File driver.
*
* @var array<string, int|string|null>
* @var array{storePath?: string, mode?: int}
*/
public array $file = [
'storePath' => WRITEPATH . 'cache/',
@@ -95,7 +96,7 @@ class Cache extends BaseConfig
*
* @see https://codeigniter.com/user_guide/libraries/caching.html#memcached
*
* @var array<string, bool|int|string>
* @var array{host?: string, port?: int, weight?: int, raw?: bool}
*/
public array $memcached = [
'host' => '127.0.0.1',
@@ -108,17 +109,28 @@ class Cache extends BaseConfig
* -------------------------------------------------------------------------
* Redis settings
* -------------------------------------------------------------------------
*
* Your Redis server can be specified below, if you are using
* the Redis or Predis drivers.
*
* @var array<string, int|string|null>
* @var array{
* host?: string,
* password?: string|null,
* port?: int,
* timeout?: int,
* async?: bool,
* persistent?: bool,
* database?: int
* }
*/
public array $redis = [
'host' => '127.0.0.1',
'password' => null,
'port' => 6379,
'timeout' => 0,
'database' => 0,
'host' => '127.0.0.1',
'password' => null,
'port' => 6379,
'timeout' => 0,
'async' => false, // specific to Predis and ignored by the native Redis extension
'persistent' => false,
'database' => 0,
];
/**
@@ -132,6 +144,7 @@ class Cache extends BaseConfig
* @var array<string, class-string<CacheInterface>>
*/
public array $validHandlers = [
'apcu' => ApcuHandler::class,
'dummy' => DummyHandler::class,
'file' => FileHandler::class,
'memcached' => MemcachedHandler::class,
@@ -158,4 +171,28 @@ class Cache extends BaseConfig
* @var bool|list<string>
*/
public $cacheQueryString = false;
/**
* --------------------------------------------------------------------------
* Web Page Caching: Cache Status Codes
* --------------------------------------------------------------------------
*
* HTTP status codes that are allowed to be cached. Only responses with
* these status codes will be cached by the PageCache filter.
*
* Default: [] - Cache all status codes (backward compatible)
*
* Recommended: [200] - Only cache successful responses
*
* You can also use status codes like:
* [200, 404, 410] - Cache successful responses and specific error codes
* [200, 201, 202, 203, 204] - All 2xx successful responses
*
* WARNING: Using [] may cache temporary error pages (404, 500, etc).
* Consider restricting to [200] for production applications to avoid
* caching errors that should be temporary.
*
* @var list<int>
*/
public array $cacheStatusCodes = [];
}

View File

@@ -30,6 +30,11 @@ class ContentSecurityPolicy extends BaseConfig
*/
public ?string $reportURI = null;
/**
* Specifies a reporting endpoint to which violation reports ought to be sent.
*/
public ?string $reportTo = null;
/**
* Instructs user agents to rewrite URL schemes, changing
* HTTP to HTTPS. This directive is for websites with
@@ -38,12 +43,12 @@ class ContentSecurityPolicy extends BaseConfig
public bool $upgradeInsecureRequests = false;
// -------------------------------------------------------------------------
// Sources allowed
// CSP DIRECTIVES SETTINGS
// NOTE: once you set a policy to 'none', it cannot be further restricted
// -------------------------------------------------------------------------
/**
* Will default to self if not overridden
* Will default to `'self'` if not overridden
*
* @var list<string>|string|null
*/
@@ -64,6 +69,21 @@ class ContentSecurityPolicy extends BaseConfig
'www.google.com www.gstatic.com'
];
/**
* Specifies valid sources for JavaScript <script> elements.
*
* @var list<string>|string
*/
public array|string $scriptSrcElem = 'self';
/**
* Specifies valid sources for JavaScript inline event
* handlers and JavaScript URLs.
*
* @var list<string>|string
*/
public array|string $scriptSrcAttr = 'self';
/**
* Lists allowed stylesheets' URLs.
*
@@ -76,6 +96,21 @@ class ContentSecurityPolicy extends BaseConfig
'https://fonts.googleapis.com',
];
/**
* Specifies valid sources for stylesheets <link> elements.
*
* @var list<string>|string
*/
public array|string $styleSrcElem = 'self';
/**
* Specifies valid sources for stylesheets inline
* style attributes and `<style>` elements.
*
* @var list<string>|string
*/
public array|string $styleSrcAttr = 'self';
/**
* Defines the origins from which images can be loaded.
*
@@ -169,6 +204,11 @@ class ContentSecurityPolicy extends BaseConfig
*/
public $manifestSrc;
/**
* @var list<string>|string
*/
public array|string $workerSrc = [];
/**
* Limits the kinds of plugins a page may invoke.
*
@@ -184,17 +224,17 @@ class ContentSecurityPolicy extends BaseConfig
public $sandbox;
/**
* Nonce tag for style
* Nonce placeholder for style tags.
*/
public string $styleNonceTag = '{csp-style-nonce}';
/**
* Nonce tag for script
* Nonce placeholder for script tags.
*/
public string $scriptNonceTag = '{csp-script-nonce}';
/**
* Replace nonce tag automatically
* Replace nonce tag automatically?
*/
public bool $autoNonce = true;
}

View File

@@ -85,7 +85,7 @@ class Cookie extends BaseConfig
* (empty string) means default SameSite attribute set by browsers (`Lax`)
* will be set on cookies. If set to `None`, `$secure` must also be set.
*
* @phpstan-var 'None'|'Lax'|'Strict'|''
* @var ''|'Lax'|'None'|'Strict'
*/
public string $samesite = 'Lax';

View File

@@ -42,6 +42,8 @@ class Database extends Config
'strictOn' => false,
'failover' => [],
'port' => 3306,
'numberNative' => false,
'foundRows' => false,
'dateFormat' => [
'date' => 'Y-m-d',
'datetime' => 'Y-m-d H:i:s',
@@ -55,26 +57,27 @@ class Database extends Config
* @var array<string, mixed>
*/
public array $tests = [
'DSN' => '',
'hostname' => 'localhost',
'username' => 'admin',
'password' => 'pointofsale',
'database' => 'ospos',
'DBDriver' => 'MySQLi',
'DBPrefix' => 'ospos_',
'pConnect' => false,
'DBDebug' => (ENVIRONMENT !== 'production'),
'charset' => 'utf8mb4',
'DBCollat' => 'utf8mb4_general_ci',
'swapPre' => '',
'encrypt' => false,
'compress' => false,
'strictOn' => false,
'failover' => [],
'port' => 3306,
'foreignKeys' => true,
'busyTimeout' => 1000,
'dateFormat' => [
'DSN' => '',
'hostname' => 'localhost',
'username' => 'admin',
'password' => 'pointofsale',
'database' => 'ospos',
'DBDriver' => 'MySQLi',
'DBPrefix' => 'ospos_',
'pConnect' => false,
'DBDebug' => (ENVIRONMENT !== 'production'),
'charset' => 'utf8mb4',
'DBCollat' => 'utf8mb4_general_ci',
'swapPre' => '',
'encrypt' => false,
'compress' => false,
'strictOn' => false,
'failover' => [],
'port' => 3306,
'foreignKeys' => true,
'busyTimeout' => 1000,
'synchronous' => null,
'dateFormat' => [
'date' => 'Y-m-d',
'datetime' => 'Y-m-d H:i:s',
'time' => 'H:i:s',

View File

@@ -2,9 +2,6 @@
namespace Config;
/**
* @immutable
*/
class DocTypes
{
/**

View File

@@ -30,6 +30,11 @@ class Email extends BaseConfig
*/
public string $SMTPHost = 'mail.mxserver.com';
/**
* Which SMTP authentication method to use: login, plain
*/
public string $SMTPAuthMethod = 'login';
/**
* SMTP Username
*/

View File

@@ -23,6 +23,23 @@ class Encryption extends BaseConfig
*/
public string $key = '';
/**
* --------------------------------------------------------------------------
* Previous Encryption Keys
* --------------------------------------------------------------------------
*
* When rotating encryption keys, add old keys here to maintain ability
* to decrypt data encrypted with previous keys. Encryption always uses
* the current $key. Decryption tries current key first, then falls back
* to previous keys if decryption fails.
*
* In .env file, use comma-separated string:
* encryption.previousKeys = hex2bin:9be8c64fcea509867...,hex2bin:3f5a1d8e9c2b7a4f6...
*
* @var list<string>|string
*/
public array|string $previousKeys = '';
/**
* --------------------------------------------------------------------------
* Encryption Driver to Use

View File

@@ -65,7 +65,10 @@ class Filters extends BaseFilters
* List of filter aliases that are always
* applied before and after every request.
*
* @var array<string, array<string, array<string, string>>>|array<string, list<string>>
* @var array{
* before: array<string, array{except: list<string>|string}>|list<string>,
* after: array<string, array{except: list<string>|string}>|list<string>
* }
*/
public array $globals = [
'before' => [
@@ -100,7 +103,7 @@ class Filters extends BaseFilters
* before or after URI patterns.
*
* Example:
* isLoggedIn' => ['before' => ['account/*', 'profiles/*']]
* 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']]
*
* @var array<string, array<string, list<string>>>
*/

View File

@@ -61,4 +61,13 @@ class Format extends BaseConfig
'application/xml' => 0,
'text/xml' => 0,
];
/**
* --------------------------------------------------------------------------
* Maximum depth for JSON encoding.
* --------------------------------------------------------------------------
*
* This value determines how deep the JSON encoder will traverse nested structures.
*/
public int $jsonEncodeDepth = 512;
}

40
app/Config/Hostnames.php Normal file
View File

@@ -0,0 +1,40 @@
<?php
namespace Config;
class Hostnames
{
// List of known two-part TLDs for subdomain extraction
public const TWO_PART_TLDS = [
'co.uk', 'org.uk', 'gov.uk', 'ac.uk', 'sch.uk', 'ltd.uk', 'plc.uk',
'com.au', 'net.au', 'org.au', 'edu.au', 'gov.au', 'asn.au', 'id.au',
'co.jp', 'ac.jp', 'go.jp', 'or.jp', 'ne.jp', 'gr.jp',
'co.nz', 'org.nz', 'govt.nz', 'ac.nz', 'net.nz', 'geek.nz', 'maori.nz', 'school.nz',
'co.in', 'net.in', 'org.in', 'ind.in', 'ac.in', 'gov.in', 'res.in',
'com.cn', 'net.cn', 'org.cn', 'gov.cn', 'edu.cn',
'com.sg', 'net.sg', 'org.sg', 'gov.sg', 'edu.sg', 'per.sg',
'co.za', 'org.za', 'gov.za', 'ac.za', 'net.za',
'co.kr', 'or.kr', 'go.kr', 'ac.kr', 'ne.kr', 'pe.kr',
'co.th', 'or.th', 'go.th', 'ac.th', 'net.th', 'in.th',
'com.my', 'net.my', 'org.my', 'edu.my', 'gov.my', 'mil.my', 'name.my',
'com.mx', 'org.mx', 'net.mx', 'edu.mx', 'gob.mx',
'com.br', 'net.br', 'org.br', 'gov.br', 'edu.br', 'art.br', 'eng.br',
'co.il', 'org.il', 'ac.il', 'gov.il', 'net.il', 'muni.il',
'co.id', 'or.id', 'ac.id', 'go.id', 'net.id', 'web.id', 'my.id',
'com.hk', 'edu.hk', 'gov.hk', 'idv.hk', 'net.hk', 'org.hk',
'com.tw', 'net.tw', 'org.tw', 'edu.tw', 'gov.tw', 'idv.tw',
'com.sa', 'net.sa', 'org.sa', 'gov.sa', 'edu.sa', 'sch.sa', 'med.sa',
'co.ae', 'net.ae', 'org.ae', 'gov.ae', 'ac.ae', 'sch.ae',
'com.tr', 'net.tr', 'org.tr', 'gov.tr', 'edu.tr', 'av.tr', 'gen.tr',
'co.ke', 'or.ke', 'go.ke', 'ac.ke', 'sc.ke', 'me.ke', 'mobi.ke', 'info.ke',
'com.ng', 'org.ng', 'gov.ng', 'edu.ng', 'net.ng', 'sch.ng', 'name.ng',
'com.pk', 'net.pk', 'org.pk', 'gov.pk', 'edu.pk', 'fam.pk',
'com.eg', 'edu.eg', 'gov.eg', 'org.eg', 'net.eg',
'com.cy', 'net.cy', 'org.cy', 'gov.cy', 'ac.cy',
'com.lk', 'org.lk', 'edu.lk', 'gov.lk', 'net.lk', 'int.lk',
'com.bd', 'net.bd', 'org.bd', 'ac.bd', 'gov.bd', 'mil.bd',
'com.ar', 'net.ar', 'org.ar', 'gov.ar', 'edu.ar', 'mil.ar',
'gob.cl', 'com.pl', 'net.pl', 'org.pl', 'gov.pl', 'edu.pl',
'co.ir', 'ac.ir', 'org.ir', 'id.ir', 'gov.ir', 'sch.ir', 'net.ir',
];
}

View File

@@ -16,6 +16,8 @@ class Images extends BaseConfig
/**
* The path to the image library.
* Required for ImageMagick, GraphicsMagick, or NetPBM.
*
* @deprecated 4.7.0 No longer used.
*/
public string $libraryPath = '/usr/local/bin/convert';

View File

@@ -4,6 +4,7 @@ namespace Config;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Log\Handlers\FileHandler;
use CodeIgniter\Log\Handlers\HandlerInterface;
class Logger extends BaseConfig
{
@@ -73,7 +74,7 @@ class Logger extends BaseConfig
* Handlers are executed in the order defined in this array, starting with
* the handler on top and continuing down.
*
* @var array<class-string, array<string, int|list<string>|string>>
* @var array<class-string<HandlerInterface>, array<string, int|list<string>|string>>
*/
public array $handlers = [
/*

View File

@@ -47,4 +47,19 @@ class Migrations extends BaseConfig
* - Y_m_d_His_
*/
public string $timestampFormat = 'YmdHis_';
/**
* --------------------------------------------------------------------------
* Enable/Disable Migration Lock
* --------------------------------------------------------------------------
*
* Locking is disabled by default.
*
* When enabled, it will prevent multiple migration processes
* from running at the same time by using a lock mechanism.
*
* This is useful in production environments to avoid conflicts
* or race conditions during concurrent deployments.
*/
public bool $lock = false;
}

View File

@@ -3,8 +3,6 @@
namespace Config;
/**
* Mimes
*
* This file contains an array of mime types. It is used by the
* Upload class to help identify allowed file types.
*
@@ -15,8 +13,6 @@ namespace Config;
*
* When working with mime types, please make sure you have the ´fileinfo´
* extension enabled to reliably detect the media types.
*
* @immutable
*/
class Mimes
{
@@ -482,13 +478,16 @@ class Mimes
'application/sla',
'application/vnd.ms-pki.stl',
'application/x-navistyle',
'model/stl',
'application/octet-stream',
],
];
/**
* Attempts to determine the best mime type for the given file extension.
*
* @return string|null The mime type found, or none if unable to determine.
* @param string $extension
* @return array|string|null The mime type found, or none if unable to determine.
*/
public static function guessTypeFromExtension(string $extension): array|string|null
{
@@ -524,7 +523,7 @@ class Mimes
}
// Reverse check the mime type list if no extension was proposed.
// This search is order sensitive!
// This search is order-sensitive!
foreach (static::$mimes as $ext => $types) {
if (in_array($type, (array) $types, true)) {
return $ext;

View File

@@ -9,8 +9,6 @@ use CodeIgniter\Modules\Modules as BaseModules;
*
* NOTE: This class is required prior to Autoloader instantiation,
* and does not extend BaseConfig.
*
* @immutable
*/
class Modules extends BaseModules
{

View File

@@ -8,7 +8,7 @@ namespace Config;
* NOTE: This class does not extend BaseConfig for performance reasons.
* So you cannot replace the property values with Environment Variables.
*
* @immutable
* WARNING: Do not use these options when running the app in the Worker Mode.
*/
class Optimize
{

View File

@@ -15,8 +15,6 @@ namespace Config;
*
* NOTE: This class is required prior to Autoloader instantiation,
* and does not extend BaseConfig.
*
* @immutable
*/
class Paths
{
@@ -77,4 +75,16 @@ class Paths
* is used when no value is provided to `Services::renderer()`.
*/
public string $viewDirectory = __DIR__ . '/../Views';
/**
* ---------------------------------------------------------------
* ENVIRONMENT DIRECTORY NAME
* ---------------------------------------------------------------
*
* This variable must contain the name of the directory where
* the .env file is located.
* Please consider security implications when changing this
* value - the directory should not be publicly accessible.
*/
public string $envDirectory = __DIR__ . '/../../';
}

View File

@@ -96,6 +96,15 @@ class Routing extends BaseRouting
*/
public bool $autoRoute = true;
/**
* If TRUE, the system will look for attributes on controller
* class and methods that can run before and after the
* controller/method.
*
* If FALSE, will ignore any attributes.
*/
public bool $useControllerAttributes = true;
/**
* For Defined Routes.
* If TRUE, will enable the use of the 'prioritize' option

View File

@@ -13,9 +13,9 @@ class Security extends BaseConfig
*
* Protection Method for Cross Site Request Forgery protection.
*
* @var string|false 'cookie', 'session', or false
* @var string 'cookie' or 'session'
*/
public string|false $csrfProtection = 'session';
public string $csrfProtection = 'session';
/**
* --------------------------------------------------------------------------

View File

@@ -2,6 +2,7 @@
namespace Config;
use App\Libraries\MY_Language;
use Locale;
use HTMLPurifier;
use HTMLPurifier_Config;
@@ -38,9 +39,11 @@ class Services extends BaseService
/**
* Responsible for loading the language string translations.
*
* @param string|null $locale
* @param bool $getShared
* @return MY_Language
*/
public static function language(?string $locale = null, bool $getShared = true)
public static function language(?string $locale = null, bool $getShared = true): MY_Language
{
if ($getShared) {
return static::getSharedInstance('language', $locale)->setLocale($locale);
@@ -55,12 +58,12 @@ class Services extends BaseService
// Use '?:' for empty string check
$locale = $locale ?: $requestLocale;
return new \App\Libraries\MY_Language($locale);
return new MY_Language($locale);
}
private static $htmlPurifier;
private static HTMLPurifier $htmlPurifier;
public static function htmlPurifier($getShared = true)
public static function htmlPurifier($getShared = true): object
{
if ($getShared) {
return static::getSharedInstance('htmlPurifier');

View File

@@ -3,10 +3,10 @@
namespace Config;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Session\Handlers\BaseHandler;
use CodeIgniter\Session\Handlers\DatabaseHandler;
use CodeIgniter\Session\Handlers\FileHandler;
use Config\Database;
class Session extends BaseConfig
{
@@ -139,7 +139,7 @@ class Session extends BaseConfig
$this->driver = FileHandler::class;
$this->savePath = WRITEPATH . 'session';
}
} catch (\CodeIgniter\Database\Exceptions\DatabaseException $e) {
} catch (DatabaseException $e) {
$this->driver = FileHandler::class;
$this->savePath = WRITEPATH . 'session';
}

View File

@@ -119,4 +119,29 @@ class Toolbar extends BaseConfig
public array $watchedExtensions = [
'php', 'css', 'js', 'html', 'svg', 'json', 'env',
];
/**
* --------------------------------------------------------------------------
* Ignored HTTP Headers
* --------------------------------------------------------------------------
*
* CodeIgniter Debug Toolbar normally injects HTML and JavaScript into every
* HTML response. This is correct for full page loads, but it breaks requests
* that expect only a clean HTML fragment.
*
* Libraries like HTMX, Unpoly, and Hotwire (Turbo) update parts of the page or
* manage navigation on the client side. Injecting the Debug Toolbar into their
* responses can cause invalid HTML, duplicated scripts, or JavaScript errors
* (such as infinite loops or "Maximum call stack size exceeded").
*
* Any request containing one of the following headers is treated as a
* client-managed or partial request, and the Debug Toolbar injection is skipped.
*
* @var array<string, string|null>
*/
public array $disableOnHeaders = [
'X-Requested-With' => 'xmlhttprequest', // AJAX requests
'HX-Request' => 'true', // HTMX requests
'X-Up-Version' => null, // Unpoly partial requests
];
}

View File

@@ -230,9 +230,13 @@ class UserAgents extends BaseConfig
*/
public array $robots = [
'googlebot' => 'Googlebot',
'google-pagerenderer' => 'Google Page Renderer',
'google-read-aloud' => 'Google Read Aloud',
'google-safety' => 'Google Safety Bot',
'msnbot' => 'MSNBot',
'baiduspider' => 'Baiduspider',
'bingbot' => 'Bing',
'bingpreview' => 'BingPreview',
'slurp' => 'Inktomi Slurp',
'yahoo' => 'Yahoo',
'ask jeeves' => 'Ask Jeeves',
@@ -248,5 +252,11 @@ class UserAgents extends BaseConfig
'ia_archiver' => 'Alexa Crawler',
'MJ12bot' => 'Majestic-12',
'Uptimebot' => 'Uptimebot',
'duckduckbot' => 'DuckDuckBot',
'sogou' => 'Sogou Spider',
'exabot' => 'Exabot',
'bot' => 'Generic Bot',
'crawler' => 'Generic Crawler',
'spider' => 'Generic Spider',
];
}

View File

@@ -59,4 +59,21 @@ class View extends BaseView
* @var list<class-string<ViewDecoratorInterface>>
*/
public array $decorators = [];
/**
* Subdirectory within app/Views for namespaced view overrides.
*
* Namespaced views will be searched in:
*
* app/Views/{$appOverridesFolder}/{Namespace}/{view_path}.{php|html...}
*
* This allows application-level overrides for package or module views
* without modifying vendor source files.
*
* Examples:
* 'overrides' -> app/Views/overrides/Example/Blog/post/card.php
* 'vendor' -> app/Views/vendor/Example/Blog/post/card.php
* '' -> app/Views/Example/Blog/post/card.php (direct mapping)
*/
public string $appOverridesFolder = 'overrides';
}

62
app/Config/WorkerMode.php Normal file
View File

@@ -0,0 +1,62 @@
<?php
namespace Config;
/**
* This configuration controls how CodeIgniter behaves when running
* in worker mode (with FrankenPHP).
*/
class WorkerMode
{
/**
* Persistent Services
*
* List of service names that should persist across requests.
* These services will NOT be reset between requests.
*
* Services not in this list will be reset for each request to prevent
* state leakage.
*
* Recommended persistent services:
* - `autoloader`: PSR-4 autoloading configuration
* - `locator`: File locator
* - `exceptions`: Exception handler
* - `commands`: CLI commands registry
* - `codeigniter`: Main application instance
* - `superglobals`: Superglobals wrapper
* - `routes`: Router configuration
* - `cache`: Cache instance
*
* @var list<string>
*/
public array $persistentServices = [
'autoloader',
'locator',
'exceptions',
'commands',
'codeigniter',
'superglobals',
'routes',
'cache',
];
/**
* Reset Event Listeners
*
* List of event names whose listeners should be removed between requests.
* Use this if you register event listeners inside other event callbacks
* (rather than at the top level of Config/Events.php), which would cause
* them to accumulate across requests in worker mode.
*
* @var list<string>
*/
public array $resetEventListeners = [];
/**
* Force Garbage Collection
*
* Whether to force garbage collection after each request.
* Helps prevent memory leaks at a small performance cost.
*/
public bool $forceGarbageCollection = true;
}

View File

@@ -132,7 +132,7 @@ class Attributes extends Secure_Controller
$definition_name = $definition_data['definition_name'];
if ($this->attribute->save_definition($definition_data, $definition_id)) {
if ($this->attribute->saveDefinition($definition_data, $definition_id)) {
// New definition
if ($definition_id == NO_DEFINITION_ID) {
$definition_values = json_decode(html_entity_decode($this->request->getPost('definition_values')));

View File

@@ -3,56 +3,46 @@
namespace App\Controllers;
use CodeIgniter\Controller;
use CodeIgniter\HTTP\CLIRequest;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Psr\Log\LoggerInterface;
/**
* Class BaseController
*
* BaseController provides a convenient place for loading components
* and performing functions that are needed by all your controllers.
* Extend this class in any new controllers:
* class Home extends BaseController
*
* For security be sure to declare any new methods as protected or private.
* Extend this class in any new controllers:
* ```
* class Home extends BaseController
* ```
*
* For security, be sure to declare any new methods as protected or private.
*/
abstract class BaseController extends Controller
{
/**
* Instance of the main Request object.
*
* @var CLIRequest|IncomingRequest
*/
protected $request;
/**
* An array of helpers to be loaded automatically upon
* class instantiation. These helpers will be available
* to all other controllers that extend BaseController.
*
* @var list<string>
*/
protected $helpers = [];
/**
* Be sure to declare properties for any property fetch you initialized.
* The creation of dynamic property is deprecated in PHP 8.2.
*/
// protected $session;
/**
* @param RequestInterface $request
* @param ResponseInterface $response
* @param LoggerInterface $logger
* @return void
*/
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger): void
{
// Do Not Edit This Line
// Load here all helpers you want to be available in your controllers that extend BaseController.
// Caution: Do not put the this below the parent::initController() call below.
// $this->helpers = ['form', 'url'];
// Caution: Do not edit this line.
parent::initController($request, $response, $logger);
// Preload any models, libraries, etc, here.
// E.g.: $this->session = service('session');
// $this->session = service('session');
}
}

View File

@@ -3,7 +3,6 @@
namespace App\Controllers;
use App\Libraries\Barcode_lib;
use App\Libraries\Image_lib;
use App\Libraries\Mailchimp_lib;
use App\Libraries\Receiving_lib;
use App\Libraries\Sale_lib;
@@ -251,10 +250,6 @@ class Config extends Secure_Controller
$data['image_allowed_types'] = array_combine($image_allowed_types, $image_allowed_types);
$data['selected_image_allowed_types'] = explode(',', $this->config['image_allowed_types']);
$exif_fields = ['Make', 'Model', 'Orientation', 'Copyright', 'Software', 'DateTime', 'GPS'];
$data['exif_fields'] = array_combine($exif_fields, $exif_fields);
$data['selected_exif_fields'] = array_filter(explode(',', $this->config['exif_fields_to_keep'] ?? ''));
// Integrations Related fields
$data['mailchimp'] = [];
@@ -360,15 +355,6 @@ class Config extends Secure_Controller
$file->move(FCPATH . 'uploads/', $file_info['raw_name'] . '.' . $file_info['file_ext'], true);
$exif_fields_to_keep = array_filter(explode(',', $this->appconfig->get_value('exif_fields_to_keep', 'Copyright,Orientation,Software')));
if (!empty($exif_fields_to_keep)) {
$image_lib = new Image_lib();
$filepath = FCPATH . 'uploads/' . $file_info['raw_name'] . '.' . $file_info['file_ext'];
if (!$image_lib->stripEXIF($filepath, $exif_fields_to_keep)) {
log_message('warning', 'EXIF stripping failed for: ' . $filepath);
}
}
return ($file_info);
}
@@ -381,7 +367,7 @@ class Config extends Secure_Controller
*/
public function postSaveGeneral(): ResponseInterface
{
$batch_save_data = [
$batchSaveData = [
'theme' => $this->request->getPost('theme'),
'login_form' => $this->request->getPost('login_form'),
'default_sales_discount_type' => $this->request->getPost('default_sales_discount_type') != null,
@@ -396,8 +382,7 @@ class Config extends Secure_Controller
'image_max_width' => $this->request->getPost('image_max_width', FILTER_SANITIZE_NUMBER_INT),
'image_max_height' => $this->request->getPost('image_max_height', FILTER_SANITIZE_NUMBER_INT),
'image_max_size' => $this->request->getPost('image_max_size', FILTER_SANITIZE_NUMBER_INT),
'image_allowed_types' => implode(',', $this->request->getPost('image_allowed_types') ?? []),
'exif_fields_to_keep' => implode(',', $this->request->getPost('exif_fields_to_keep') ?? []),
'image_allowed_types' => implode(',', $this->request->getPost('image_allowed_types')),
'gcaptcha_enable' => $this->request->getPost('gcaptcha_enable') != null,
'gcaptcha_secret_key' => $this->request->getPost('gcaptcha_secret_key'),
'gcaptcha_site_key' => $this->request->getPost('gcaptcha_site_key'),
@@ -413,19 +398,19 @@ class Config extends Secure_Controller
$this->module->set_show_office_group($this->request->getPost('show_office_group') != null);
if ($batch_save_data['category_dropdown'] == 1) {
$definition_data['definition_name'] = 'ospos_category';
$definition_data['definition_flags'] = 0;
$definition_data['definition_type'] = 'DROPDOWN';
$definition_data['definition_id'] = CATEGORY_DEFINITION_ID;
$definition_data['deleted'] = 0;
if ($batchSaveData['category_dropdown']) {
$definitionData['definition_name'] = 'ospos_category';
$definitionData['definition_flags'] = 0;
$definitionData['definition_type'] = 'DROPDOWN';
$definitionData['definition_id'] = CATEGORY_DEFINITION_ID;
$definitionData['deleted'] = 0;
$this->attribute->save_definition($definition_data, CATEGORY_DEFINITION_ID);
} elseif ($batch_save_data['category_dropdown'] == NO_DEFINITION_ID) {
$this->attribute->saveDefinition($definitionData, CATEGORY_DEFINITION_ID);
} elseif ($batchSaveData['category_dropdown'] == NO_DEFINITION_ID) {
$this->attribute->deleteDefinition(CATEGORY_DEFINITION_ID);
}
$success = $this->appconfig->batch_save($batch_save_data);
$success = $this->appconfig->batch_save($batchSaveData);
return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]);
}

View File

@@ -91,7 +91,7 @@ class Expenses extends Secure_Controller
*/
public function getView(int $expense_id = NEW_ENTRY): string
{
$data = []; // TODO: Duplicated code
$data = [];
$data['expenses_info'] = $this->expense->get_info($expense_id);
$expense_id = $data['expenses_info']->expense_id;

View File

@@ -35,12 +35,12 @@ class Home extends Secure_Controller
}
/**
* Load "change employee password" form
* Load the "change employee password" form
*
* @param int $employeeId
* @return ResponseInterface|string
* @noinspection PhpUnused
*/
public function getChangePassword(int $employeeId = NEW_ENTRY)
public function getChangePassword(int $employeeId = NEW_ENTRY): ResponseInterface|string
{
$loggedInEmployee = $this->employee->get_logged_in_employee_info();
$currentPersonId = $loggedInEmployee->person_id;

View File

@@ -3,10 +3,7 @@
namespace App\Controllers;
use App\Libraries\Barcode_lib;
use App\Libraries\Image_lib;
use App\Libraries\Item_lib;
use App\Models\Appconfig;
use App\Models\Attribute;
use App\Models\Inventory;
use App\Models\Item;
@@ -16,7 +13,6 @@ use App\Models\Item_taxes;
use App\Models\Stock_location;
use App\Models\Supplier;
use App\Models\Tax_category;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Images\Handlers\BaseHandler;
use CodeIgniter\HTTP\DownloadResponse;
@@ -41,7 +37,6 @@ class Items extends Secure_Controller
private Stock_location $stock_location;
private Supplier $supplier;
private Tax_category $tax_category;
private Appconfig $appconfig;
private array $config;
@@ -65,7 +60,6 @@ class Items extends Secure_Controller
$this->stock_location = model(Stock_location::class);
$this->supplier = model(Supplier::class);
$this->tax_category = model(Tax_category::class);
$this->appconfig = model(Appconfig::class);
$this->config = config(OSPOS::class)->settings;
}
@@ -77,7 +71,7 @@ class Items extends Secure_Controller
$this->session->set('allow_temp_items', 0);
$data['table_headers'] = get_items_manage_table_headers();
// Restore stock_location from URL or session
$stockLocation = $this->request->getGet('stock_location', FILTER_SANITIZE_NUMBER_INT);
$data['stock_location'] = $stockLocation
@@ -512,7 +506,7 @@ class Items extends Secure_Controller
$data['definition_names'] = $this->attribute->get_definition_names();
foreach ($data['definition_values'] as $definition_id => $definition_value) {
$attribute_value = $this->attribute->get_attribute_value($item_id, $definition_id);
$attribute_value = $this->attribute->getAttributeValue($item_id, $definition_id);
$attribute_id = (empty($attribute_value) || empty($attribute_value->attribute_id)) ? null : $attribute_value->attribute_id;
$values = &$data['definition_values'][$definition_id];
$values['attribute_id'] = $attribute_id;
@@ -548,7 +542,7 @@ class Items extends Secure_Controller
$data['definition_names'] = $this->attribute->get_definition_names();
foreach ($data['definition_values'] as $definition_id => $definition_value) {
$attribute_value = $this->attribute->get_attribute_value($item_id, $definition_id);
$attribute_value = $this->attribute->getAttributeValue($item_id, $definition_id);
$attribute_id = (empty($attribute_value) || empty($attribute_value->attribute_id)) ? null : $attribute_value->attribute_id;
$values = &$data['definition_values'][$definition_id];
$values['attribute_id'] = $attribute_id;
@@ -717,7 +711,7 @@ class Items extends Secure_Controller
$item_quantity = $this->item_quantity->get_item_quantity($item_id, $location['location_id']);
if ($item_quantity->quantity != $updated_quantity || $new_item) {
$success &= $this->item_quantity->save_value($location_detail, $item_id, $location['location_id']);
$success = $success && $this->item_quantity->save_value($location_detail, $item_id, $location['location_id']);
$inv_data = [
'trans_date' => date('Y-m-d H:i:s'),
@@ -728,10 +722,10 @@ class Items extends Secure_Controller
'trans_inventory' => $updated_quantity - $item_quantity->quantity
];
$success &= $this->inventory->insert($inv_data, false);
$success = $success && $this->inventory->insert($inv_data, false);
}
}
$this->saveItemAttributes($item_id);
$success = $success && $this->saveItemAttributes($item_id);
if ($success && $upload_success) {
$message = lang('Items.successful_' . ($new_item ? 'adding' : 'updating')) . ' ' . $item_data['name'];
@@ -781,7 +775,7 @@ class Items extends Secure_Controller
$filename = $file->getClientName();
$info = pathinfo($filename);
// Sanitize filename to remove problematic characters like spaces
$sanitized_name = preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $info['filename']);
@@ -792,16 +786,6 @@ class Items extends Secure_Controller
];
$file->move(FCPATH . 'uploads/item_pics/', $file_info['raw_name'] . '.' . $file_info['file_ext'], true);
$exif_fields_to_keep = array_filter(explode(',', $this->appconfig->get_value('exif_fields_to_keep', 'Copyright,Orientation,Software')));
if (!empty($exif_fields_to_keep)) {
$image_lib = new Image_lib();
$filepath = FCPATH . 'uploads/item_pics/' . $file_info['raw_name'] . '.' . $file_info['file_ext'];
if (!$image_lib->stripEXIF($filepath, $exif_fields_to_keep)) {
log_message('warning', 'EXIF stripping failed for: ' . $filepath);
}
}
return ($file_info);
}
@@ -954,7 +938,7 @@ class Items extends Secure_Controller
*/
public function getGenerateCsvFile(): DownloadResponse
{
helper('importfile_helper');
helper('importfile');
$name = 'import_items.csv';
$allowed_locations = $this->stock_location->get_allowed_locations();
$allowed_attributes = $this->attribute->get_definition_names();
@@ -973,14 +957,13 @@ class Items extends Secure_Controller
}
/**
* Imports items from CSV formatted file.
* Imports items from a CSV formatted file.
* @return ResponseInterface
* @throws ReflectionException
* @noinspection PhpUnused
*/
public function postImportCsvFile(): ResponseInterface
{
helper('importfile_helper');
helper('importfile');
try {
if ($_FILES['file_path']['error'] !== UPLOAD_ERR_OK) {
return $this->response->setJSON(['success' => false, 'message' => lang('Items.csv_import_failed')]);
@@ -989,33 +972,33 @@ class Items extends Secure_Controller
set_time_limit(240);
$failCodes = [];
$csv_rows = get_csv_file($_FILES['file_path']['tmp_name']);
$employee_id = $this->employee->get_logged_in_employee_info()->person_id;
$allowed_stock_locations = $this->stock_location->get_allowed_locations();
$attribute_definition_names = $this->attribute->get_definition_names();
$csvRows = get_csv_file($_FILES['file_path']['tmp_name']);
$employeeId = $this->employee->get_logged_in_employee_info()->person_id;
$allowedStockLocations = $this->stock_location->get_allowed_locations();
$attributeDefinitionNames = $this->attribute->get_definition_names();
unset($attribute_definition_names[NEW_ENTRY]); // Removes the common_none_selected_text from the array
unset($attributeDefinitionNames[NEW_ENTRY]); // Removes the common_none_selected_text from the array
$attribute_data = [];
$attributeData = [];
foreach ($attribute_definition_names as $definition_name) {
$attribute_data[$definition_name] = $this->attribute->get_definition_by_name($definition_name)[0];
foreach ($attributeDefinitionNames as $definitionName) {
$attributeData[$definitionName] = $this->attribute->get_definition_by_name($definitionName)[0];
if ($attribute_data[$definition_name]['definition_type'] === DROPDOWN) {
$attribute_data[$definition_name]['dropdown_values'] = $this->attribute->get_definition_values($attribute_data[$definition_name]['definition_id']);
if ($attributeData[$definitionName]['definition_type'] === DROPDOWN) {
$attributeData[$definitionName]['dropdown_values'] = $this->attribute->get_definition_values($attributeData[$definitionName]['definition_id']);
}
}
$db = db_connect();
$db->transBegin(); // TODO: This section needs to be reworked so that the data array is being created then passed to the Item model because $db doesn't exist in the controller without being instantiated, but database operations should be restricted to the model
foreach ($csv_rows as $key => $row) {
$is_failed_row = false;
$item_id = (int)$row['Id'];
$is_update = ($item_id > 0);
$item_data = [
'item_id' => $item_id,
foreach ($csvRows as $key => $row) {
$isFailedRow = false;
$itemId = (int)$row['Id'];
$isUpdate = ($itemId > 0);
$itemData = [
'item_id' => $itemId,
'name' => $row['Item Name'],
'description' => $row['Description'],
'description' => filter_var($row['Description'], FILTER_SANITIZE_FULL_SPECIAL_CHARS),
'category' => $row['Category'],
'cost_price' => $row['Cost Price'],
'unit_price' => $row['Unit Price'],
@@ -1025,25 +1008,26 @@ class Items extends Secure_Controller
'pic_filename' => $row['Image']
];
if (!empty($row['supplier ID'])) {
$item_data['supplier_id'] = $this->supplier->exists($row['Supplier ID']) ? $row['Supplier ID'] : null;
if (!empty($row['Supplier ID'])) {
$itemData['supplier_id'] = $this->supplier->exists($row['Supplier ID']) ? $row['Supplier ID'] : null;
}
if ($is_update) {
$item_data['allow_alt_description'] = empty($row['Allow Alt Description']) ? null : $row['Allow Alt Description'];
$item_data['is_serialized'] = empty($row['Item has Serial Number']) ? null : $row['Item has Serial Number'];
if ($isUpdate) {
$itemData['allow_alt_description'] = $row['Allow Alt Description'] === '' ? null : $row['Allow Alt Description'];
$itemData['is_serialized'] = $row['Item has Serial Number'] === '' ? null : $row['Item has Serial Number'];
} else {
$item_data['allow_alt_description'] = empty($row['Allow Alt Description']) ? '0' : '1';
$item_data['is_serialized'] = empty($row['Item has Serial Number']) ? '0' : '1';
$itemData['allow_alt_description'] = $row['Allow Alt Description'] === '' ? '0' : '1';
$itemData['is_serialized'] = $row['Item has Serial Number'] === '' ? '0' : '1';
}
if (!empty($row['Barcode']) && !$is_update) {
$item_data['item_number'] = $row['Barcode'];
$is_failed_row = $this->item->item_number_exists($item_data['item_number']);
if (!empty($row['Barcode'])) {
$itemData['item_number'] = $row['Barcode'];
$isFailedRow = $this->item->item_number_exists($itemData['item_number'], $itemId);
}
if (!$is_failed_row) {
$invalidLocations = $this->validateCSVStockLocations($row, $allowedStockLocations);
if (!$isFailedRow) {
$allowedStockLocations = $this->stock_location->get_allowed_locations();
$isFailedRow = $this->validateCSVData($row, $itemData, $allowedStockLocations, $attributeDefinitionNames, $attributeData);
if (!empty($invalidLocations)) {
$isFailedRow = true;
log_message('error', 'CSV import: Invalid stock location(s) found: ' . implode(', ', $invalidLocations));
@@ -1051,28 +1035,35 @@ class Items extends Secure_Controller
}
// Remove false, null, '' and empty strings but keep 0
$item_data = array_filter($item_data, function ($value) {
$itemData = array_filter($itemData, function ($value) {
return $value !== null && strlen($value);
});
if (!$is_failed_row && $this->item->save_value($item_data, $item_id)) {
$this->save_tax_data($row, $item_data);
$this->save_inventory_quantities($row, $item_data, $allowed_stock_locations, $employee_id);
$is_failed_row = $this->save_attribute_data($row, $item_data, $attribute_data); // TODO: $is_failed_row never gets used after this.
if (!$isFailedRow && $this->item->save_value($itemData, $itemId)) {
$this->save_tax_data($row, $itemData);
$this->save_inventory_quantities($row, $itemData, $allowedStockLocations, $employeeId);
$csvAttributeValues = $this->extractAttributeData($row);
$isFailedRow = !$this->attribute->saveCSVRowAttributeData($csvAttributeValues, $itemData, $attributeData);
if ($isFailedRow) {
$failedRow = $key + 2;
$failCodes[] = $failedRow;
log_message('error', "CSV Item import failed on line $failedRow while saving attributes.");
continue;
}
if ($is_update) {
$item_data = array_merge($item_data, get_object_vars($this->item->get_info_by_id_or_number($item_id)));
if ($isUpdate) {
$itemData = array_merge($itemData, get_object_vars($this->item->get_info_by_id_or_number($itemId)));
}
} else {
$failed_row = $key + 2;
$failCodes[] = $failed_row;
log_message('error', "CSV Item import failed on line $failed_row. This item was not imported.");
$failedRow = $key + 2;
$failCodes[] = $failedRow;
log_message('error', "CSV Item import failed on line $failedRow. This item was not imported.");
}
unset($csv_rows[$key]);
unset($csvRows[$key]);
}
$csv_rows = null;
$csvRows = null;
if (count($failCodes) > 0) {
$message = lang('Items.csv_import_partially_failed', [count($failCodes), implode(', ', $failCodes)]);
@@ -1080,6 +1071,7 @@ class Items extends Secure_Controller
return $this->response->setJSON(['success' => false, 'message' => $message]);
} else {
$db->transCommit();
$this->attribute->deleteOrphanedValues();
return $this->response->setJSON(['success' => true, 'message' => lang('Items.csv_import_success')]);
}
@@ -1093,6 +1085,20 @@ class Items extends Secure_Controller
}
private function extractAttributeData(array $row): array
{
$attributeData = [];
foreach ($row as $key => $value) {
if (str_starts_with($key, 'attribute_')) {
$definitionName = substr($key, 10);
$attributeData[$definitionName] = $value;
}
}
return $attributeData;
}
/**
* Validates that stock location columns in CSV row are valid locations
*
@@ -1121,87 +1127,99 @@ class Items extends Secure_Controller
* Checks the entire line of data in an import file for errors
*
* @param array $row
* @param array $item_data
* @param array $allowed_locations
* @param array $definition_names
* @param array $attribute_data
* @param array $itemData
* @param array $allowedStockLocations
* @param array $definitionNames
* @param array $attributeData
* @return bool Returns false if all data checks out and true when there is an error in the data
*/
private function data_error_check(array $row, array $item_data, array $allowed_locations, array $definition_names, array $attribute_data): bool // TODO: Long function and large number of parameters in the declaration... perhaps refactoring is needed
private function validateCSVData(array $row, array $itemData, array $allowedStockLocations, array $definitionNames, array $attributeData): bool // TODO: Long function and large number of parameters in the declaration... perhaps refactoring is needed
{
$item_id = $row['Id'];
$is_update = (bool)$item_id;
$itemId = $row['Id'];
$isUpdate = (bool)$itemId;
// Check for empty required fields
$check_for_empty = [
'name' => $item_data['name'],
'category' => $item_data['category'],
'unit_price' => $item_data['unit_price']
$valuesToCheckForEmpty = [
'name' => $itemData['name'],
'category' => $itemData['category'],
'unit_price' => $itemData['unit_price']
];
foreach ($check_for_empty as $key => $val) {
if (empty($val) && !$is_update) {
foreach ($valuesToCheckForEmpty as $key => $value) {
if (($value === null || $value === '') && !$isUpdate) {
log_message('error', "Empty required value in $key.");
return true;
}
}
if (!$is_update) {
$item_data['cost_price'] = empty($item_data['cost_price']) ? 0 : $item_data['cost_price']; // Allow for zero wholesale price
if (!$isUpdate) {
$itemData['cost_price'] = empty($itemData['cost_price']) ? 0 : $itemData['cost_price']; // Allow for zero wholesale price
} else {
if (!$this->item->exists($item_id)) {
log_message('error', "non-existent item_id: '$item_id' when either existing item_id or no item_id is required.");
if (!$this->item->exists($itemId)) {
log_message('error', "non-existent item_id: '$itemId' when either existing item_id or no item_id is required.");
return true;
}
}
// Build array of fields to check for numerics
$check_for_numeric_values = [
'cost_price' => $item_data['cost_price'],
'unit_price' => $item_data['unit_price'],
'reorder_level' => $item_data['reorder_level'],
$valuesToCheckForNumeric = [
'cost_price' => $itemData['cost_price'],
'unit_price' => $itemData['unit_price'],
'reorder_level' => $itemData['reorder_level'],
'supplier_id' => $row['Supplier ID'],
'Tax 1 Percent' => $row['Tax 1 Percent'],
'Tax 2 Percent' => $row['Tax 2 Percent']
];
foreach ($allowed_locations as $location_name) {
$check_for_numeric_values[] = $row["location_$location_name"];
foreach ($allowedStockLocations as $location_name) {
$valuesToCheckForNumeric[] = $row["location_$location_name"];
}
// Check for non-numeric values which require numeric
foreach ($check_for_numeric_values as $key => $value) {
foreach ($valuesToCheckForNumeric as $key => $value) {
if (!is_numeric($value) && !empty($value)) {
log_message('error', "non-numeric: '$value' for '$key' when numeric is required");
return true;
}
}
// Check stock locations
$invalidLocations = $this->validateCSVStockLocations($row, $allowedStockLocations);
if (!empty($invalidLocations)) {
log_message('error', 'CSV import: Invalid stock location(s) found: ' . implode(', ', $invalidLocations));
return true;
}
// Check Attribute Data
foreach ($definition_names as $definition_name) {
if (!empty($row["attribute_$definition_name"])) {
$definition_type = $attribute_data[$definition_name]['definition_type'];
$attribute_value = $row["attribute_$definition_name"];
foreach ($definitionNames as $definitionName) {
$attributeColumn = "attribute_$definitionName";
if (array_key_exists($attributeColumn, $row) && $row[$attributeColumn] != '') {
$definitionType = $attributeData[$definitionName]['definition_type'];
$attributeValue = $row[$attributeColumn];
switch ($definition_type) {
if (strcasecmp($attributeValue, '_DELETE_') === 0) {
continue;
}
switch ($definitionType) {
case DROPDOWN:
$dropdown_values = $attribute_data[$definition_name]['dropdown_values'];
$dropdown_values[] = '';
$dropdownValues = $attributeData[$definitionName]['dropdown_values'];
$dropdownValues[] = '';
if (!empty($attribute_value) && !in_array($attribute_value, $dropdown_values)) {
log_message('error', "Value: '$attribute_value' is not an acceptable DROPDOWN value");
if (!empty($attributeValue) && !in_array($attributeValue, $dropdownValues)) {
log_message('error', "Value: '$attributeValue' is not an acceptable DROPDOWN value");
return true;
}
break;
case DECIMAL:
if (!is_numeric($attribute_value) && !empty($attribute_value)) {
log_message('error', "'$attribute_value' is not an acceptable DECIMAL value");
if (!is_numeric($attributeValue) && !empty($attributeValue)) {
log_message('error', "'$attributeValue' is not an acceptable DECIMAL value");
return true;
}
break;
case DATE:
if (!valid_date($attribute_value) && !empty($attribute_value)) {
log_message('error', "'$attribute_value' is not an acceptable DATE value. The value must match the set locale.");
if (!valid_date($attributeValue) && !empty($attributeValue)) {
log_message('error', "'$attributeValue' is not an acceptable DATE value. The value must match the set locale.");
return true;
}
break;
@@ -1212,59 +1230,6 @@ class Items extends Secure_Controller
return false;
}
/**
* Saves attribute data found in the CSV import.
*
* @param array $row
* @param array $item_data
* @param array $definitions
* @return bool
*/
private function save_attribute_data(array $row, array $item_data, array $definitions): bool
{
foreach ($definitions as $definition) {
$attribute_name = $definition['definition_name'];
$attribute_value = $row["attribute_$attribute_name"];
// Create attribute value
if (!empty($attribute_value) || $attribute_value === '0') {
if ($definition['definition_type'] === CHECKBOX) {
$checkbox_is_unchecked = (strcasecmp($attribute_value, 'false') === 0 || $attribute_value === '0');
$attribute_value = $checkbox_is_unchecked ? '0' : '1';
$attribute_id = $this->store_attribute_value($attribute_value, $definition, $item_data['item_id']);
} elseif (!empty($attribute_value)) {
$attribute_id = $this->store_attribute_value($attribute_value, $definition, $item_data['item_id']);
} else {
return true;
}
if (!$attribute_id) {
return true;
}
}
}
return false;
}
/**
* Saves the attribute_value and attribute_link if necessary
*/
private function store_attribute_value(string $value, array $attribute_data, int $item_id)
{
$attribute_id = $this->attribute->attributeValueExists($value, $attribute_data['definition_type']);
$this->attribute->deleteAttributeLinks($item_id, $attribute_data['definition_id']);
if (!$attribute_id) {
$attribute_id = $this->attribute->saveAttributeValue($value, $attribute_data['definition_id'], $item_id, false, $attribute_data['definition_type']);
} elseif (!$this->attribute->saveAttributeLink($item_id, $attribute_data['definition_id'], $attribute_id)) {
return false;
}
return $attribute_id;
}
/**
* Saves inventory quantities for the row in the appropriate stock locations.
*
@@ -1358,10 +1323,11 @@ class Items extends Secure_Controller
* Saves item attributes for a given item.
*
* @param int $itemId The item for which attributes need to be saved to.
* @return void
* @return bool Returns true when item attributes are successfully saved and false on error.
*/
public function saveItemAttributes(int $itemId): void
public function saveItemAttributes(int $itemId): bool
{
$success = true;
$attributeLinks = $this->request->getPost('attribute_links') ?? [];
$attributeIds = $this->request->getPost('attribute_ids');
@@ -1373,16 +1339,18 @@ class Items extends Secure_Controller
switch ($definitionType) {
case DROPDOWN:
$attributeId = $attributeValue;
$success = $success && $this->attribute->saveAttributeLink($itemId, $definitionId, $attributeId);
break;
case DECIMAL:
$attributeValue = parse_decimals($attributeValue);
// Fall through to save the attribute value
// no break
default:
$attributeId = $this->attribute->saveAttributeValue($attributeValue, $definitionId, $itemId, $attributeIds[$definitionId], $definitionType);
$success = $success && ($attributeId > 0);
break;
}
$this->attribute->saveAttributeLink($itemId, $definitionId, $attributeId);
}
return $success && $this->attribute->deleteOrphanedValues();
}
}

View File

@@ -11,6 +11,7 @@ use App\Models\Item_kit;
use App\Models\Receiving;
use App\Models\Stock_location;
use App\Models\Supplier;
use App\Traits\Controller\Shared;
use CodeIgniter\HTTP\ResponseInterface;
use Config\OSPOS;
use Config\Services;
@@ -18,6 +19,7 @@ use ReflectionException;
class Receivings extends Secure_Controller
{
use Shared;
private Receiving_lib $receiving_lib;
private Token_lib $token_lib;
private Barcode_lib $barcode_lib;
@@ -190,11 +192,11 @@ class Receivings extends Secure_Controller
/**
* Edit line item in current receiving. Used in app/Views/receivings/receiving.php
*
* @param string|int|null $item_id
* @param int|string|null $item_id
* @return string
* @noinspection PhpUnused
*/
public function postEditItem($item_id): string
public function postEditItem(int|string|null $item_id): string
{
$data = [];
@@ -208,7 +210,7 @@ class Receivings extends Secure_Controller
$quantity = parse_quantity($this->request->getPost('quantity'));
$raw_receiving_quantity = parse_quantity($this->request->getPost('receiving_quantity'));
$description = $this->request->getPost('description', FILTER_SANITIZE_FULL_SPECIAL_CHARS); // TODO: Duplicated code
$description = $this->request->getPost('description', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$serialnumber = $this->request->getPost('serialnumber', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?? '';
$discount_type = $this->request->getPost('discount_type', FILTER_SANITIZE_NUMBER_INT);
$discount = $discount_type
@@ -242,7 +244,7 @@ class Receivings extends Secure_Controller
}
$receiving_info = $this->receiving->get_info($receiving_id)->getRowArray();
$current_employee_id = $this->employee->get_logged_in_employee_info()->person_id;
$can_assign_employee = $this->employee->has_grant('employees', $current_employee_id);
@@ -280,8 +282,10 @@ class Receivings extends Secure_Controller
}
/**
* @throws ReflectionException
* @param int $receiving_id
* @param bool $update_inventory
* @return ResponseInterface
* @throws ReflectionException
*/
public function postDelete(int $receiving_id = -1, bool $update_inventory = true): ResponseInterface
{
@@ -423,19 +427,10 @@ class Receivings extends Secure_Controller
$employee_info = $this->employee->get_info($receiving_info['employee_id']);
$data['employee'] = $employee_info->first_name . ' ' . $employee_info->last_name;
$supplier_id = $this->receiving_lib->get_supplier(); // TODO: Duplicated code
$supplier_id = $this->receiving_lib->get_supplier();
if ($supplier_id != -1) {
$supplier_info = $this->supplier->get_info($supplier_id);
$data['supplier'] = $supplier_info->company_name;
$data['first_name'] = $supplier_info->first_name;
$data['last_name'] = $supplier_info->last_name;
$data['supplier_email'] = $supplier_info->email;
$data['supplier_address'] = $supplier_info->address_1;
if (!empty($supplier_info->zip) or !empty($supplier_info->city)) {
$data['supplier_location'] = $supplier_info->zip . ' ' . $supplier_info->city;
} else {
$data['supplier_location'] = '';
}
$this->buildSupplierInfo($supplier_info, $data);
}
$data['print_after_sale'] = false;
@@ -472,18 +467,9 @@ class Receivings extends Secure_Controller
$supplier_id = $this->receiving_lib->get_supplier();
if ($supplier_id != -1) { // TODO: Duplicated Code... replace -1 with a constant
if ($supplier_id != -1) {
$supplier_info = $this->supplier->get_info($supplier_id);
$data['supplier'] = $supplier_info->company_name;
$data['first_name'] = $supplier_info->first_name;
$data['last_name'] = $supplier_info->last_name;
$data['supplier_email'] = $supplier_info->email;
$data['supplier_address'] = $supplier_info->address_1;
if (!empty($supplier_info->zip) or !empty($supplier_info->city)) {
$data['supplier_location'] = $supplier_info->zip . ' ' . $supplier_info->city;
} else {
$data['supplier_location'] = '';
}
$this->buildSupplierInfo($supplier_info, $data);
}
$data['print_after_sale'] = $this->receiving_lib->is_print_after_sale();

View File

@@ -128,8 +128,8 @@ class Reports extends Secure_Controller
* @param string $location_id
* @return string
*/
public function summary_sales(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string // TODO: Perhaps these need to be passed as an array? Too many parameters in the signature.
{ // TODO: Duplicated code
public function summary_sales(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string
{
$this->clearCache();
$inputs = [
@@ -176,7 +176,7 @@ class Reports extends Secure_Controller
* @return string
*/
public function summary_categories(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string
{ // TODO: Duplicated code
{
$this->clearCache();
$inputs = [
@@ -493,7 +493,7 @@ class Reports extends Secure_Controller
* @return string
*/
public function summary_sales_taxes(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string
{ // TODO: Duplicated code
{
$this->clearCache();
$inputs = [

View File

@@ -20,6 +20,7 @@ use App\Models\Stock_location;
use App\Models\Tokens\Token_invoice_count;
use App\Models\Tokens\Token_customer;
use App\Models\Tokens\Token_invoice_sequence;
use App\Traits\Controller\Shared;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Services;
use Config\OSPOS;
@@ -28,6 +29,7 @@ use stdClass;
class Sales extends Secure_Controller
{
use Shared;
protected $helpers = ['file'];
private Barcode_lib $barcode_lib;
private Email_lib $email_lib;
@@ -425,7 +427,7 @@ class Sales extends Secure_Controller
$new_giftcard_value = $giftcard->get_giftcard_value($giftcard_num) - $this->sale_lib->get_amount_due();
$new_giftcard_value = max($new_giftcard_value, 0);
$this->sale_lib->set_giftcard_remainder($new_giftcard_value);
$new_giftcard_value = str_replace('$', '\$', to_currency($new_giftcard_value));
$new_giftcard_value = to_currency($new_giftcard_value);
$data['warning'] = lang('Giftcards.remaining_balance', [$giftcard_num, $new_giftcard_value]);
$amount_tendered = min($this->sale_lib->get_amount_due(), $giftcard->get_giftcard_value($giftcard_num));
@@ -731,7 +733,7 @@ class Sales extends Secure_Controller
$data["customer_comments"] = $customer_info->comments;
$data['tax_id'] = $customer_info->tax_id;
}
$tax_details = $this->tax_lib->get_taxes($data['cart']); // TODO: Duplicated code
$tax_details = $this->tax_lib->get_taxes($data['cart']);
$data['taxes'] = $tax_details[0];
$data['discount'] = $this->sale_lib->get_discount();
$data['payments'] = $this->sale_lib->get_payments();
@@ -743,7 +745,7 @@ class Sales extends Secure_Controller
$data['payments_total'] = $totals['payment_total'];
$data['payments_cover_total'] = $totals['payments_cover_total'];
$data['cash_rounding'] = $this->session->get('cash_rounding');
$data['cash_mode'] = $this->session->get('cash_mode'); // TODO: Duplicated code
$data['cash_mode'] = $this->session->get('cash_mode');
$data['prediscount_subtotal'] = $totals['prediscount_subtotal'];
$data['cash_total'] = $totals['cash_total'];
$data['non_cash_total'] = $totals['total'];
@@ -796,7 +798,7 @@ class Sales extends Secure_Controller
if ($sale_id == NEW_ENTRY && $this->sale->check_invoice_number_exists($invoice_number)) {
$data['error'] = lang('Sales.invoice_number_duplicate', [$invoice_number]);
$this->_reload($data);
return $this->_reload($data);
} else {
$data['invoice_number'] = $invoice_number;
$data['sale_status'] = COMPLETED;
@@ -817,6 +819,7 @@ class Sales extends Secure_Controller
if ($data['sale_id_num'] == NEW_ENTRY) {
$data['error_message'] = lang('Sales.transaction_failed');
return $this->_reload($data);
} else {
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']);
$this->sale_lib->clear_all();
@@ -840,7 +843,7 @@ class Sales extends Secure_Controller
if ($sale_id == NEW_ENTRY && $this->sale->check_work_order_number_exists($work_order_number)) {
$data['error'] = lang('Sales.work_order_number_duplicate');
$this->_reload($data);
return $this->_reload($data);
} else {
$data['work_order_number'] = $work_order_number;
$data['sale_status'] = SUSPENDED;
@@ -868,7 +871,7 @@ class Sales extends Secure_Controller
if ($sale_id == NEW_ENTRY && $this->sale->check_quote_number_exists($quote_number)) {
$data['error'] = lang('Sales.quote_number_duplicate');
$this->_reload($data);
return $this->_reload($data);
} else {
$data['quote_number'] = $quote_number;
$data['sale_status'] = SUSPENDED;
@@ -900,6 +903,7 @@ class Sales extends Secure_Controller
if ($data['sale_id_num'] == NEW_ENTRY) {
$data['error_message'] = lang('Sales.transaction_failed');
return $this->_reload($data);
} else {
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']);
$this->sale_lib->clear_all();
@@ -1096,7 +1100,7 @@ class Sales extends Secure_Controller
$data['subtotal'] = $totals['subtotal'];
$data['payments_total'] = $totals['payment_total'];
$data['payments_cover_total'] = $totals['payments_cover_total'];
$data['cash_mode'] = $this->session->get('cash_mode'); // TODO: Duplicated code.
$data['cash_mode'] = $this->session->get('cash_mode');.
$data['prediscount_subtotal'] = $totals['prediscount_subtotal'];
$data['cash_total'] = $totals['cash_total'];
$data['non_cash_total'] = $totals['total'];
@@ -1124,35 +1128,15 @@ class Sales extends Secure_Controller
$data['quote_number'] = $sale_info['quote_number'];
$data['sale_status'] = $sale_info['sale_status'];
$data['company_info'] = implode("\n", [$this->config['address'], $this->config['phone']]); // TODO: Duplicated code.
if ($this->config['account_number']) {
$data['company_info'] .= "\n" . lang('Sales.account_number') . ": " . $this->config['account_number'];
}
if ($this->config['tax_id'] != '') {
$data['company_info'] .= "\n" . lang('Sales.tax_id') . ": " . $this->config['tax_id'];
}
$data['company_info'] = $this->buildCompanyInfo();
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']);
$data['print_after_sale'] = false;
$data['price_work_orders'] = false;
if ($this->sale_lib->get_mode() == 'sale_invoice') { // TODO: Duplicated code.
$data['mode_label'] = lang('Sales.invoice');
$data['customer_required'] = lang('Sales.customer_required');
} elseif ($this->sale_lib->get_mode() == 'sale_quote') {
$data['mode_label'] = lang('Sales.quote');
$data['customer_required'] = lang('Sales.customer_required');
} elseif ($this->sale_lib->get_mode() == 'sale_work_order') {
$data['mode_label'] = lang('Sales.work_order');
$data['customer_required'] = lang('Sales.customer_required');
} elseif ($this->sale_lib->get_mode() == 'return') {
$data['mode_label'] = lang('Sales.return');
$data['customer_required'] = lang('Sales.customer_optional');
} else {
$data['mode_label'] = lang('Sales.receipt');
$data['customer_required'] = lang('Sales.customer_optional');
}
$modeData = $this->getSaleModeLabel($this->sale_lib->get_mode());
$data['mode_label'] = $modeData['mode_label'];
$data['customer_required'] = $modeData['customer_required'];
$invoice_type = $this->config['invoice_type'];
if (!Sale_lib::isValidInvoiceType($invoice_type)) {
@@ -1190,7 +1174,7 @@ class Sales extends Secure_Controller
$data['stock_locations'] = $this->stock_location->get_allowed_locations('sales');
$data['stock_location'] = $this->sale_lib->get_sale_location();
$data['tax_exclusive_subtotal'] = $this->sale_lib->get_subtotal(true, true);
$tax_details = $this->tax_lib->get_taxes($data['cart']); // TODO: Duplicated code.
$tax_details = $this->tax_lib->get_taxes($data['cart']);.
$data['taxes'] = $tax_details[0];
$data['discount'] = $this->sale_lib->get_discount();
$data['payments'] = $this->sale_lib->get_payments();
@@ -1208,7 +1192,7 @@ class Sales extends Secure_Controller
// cash_mode indicates whether this sale is going to be processed using cash_rounding
$cash_mode = $this->session->get('cash_mode');
$data['cash_mode'] = $cash_mode;
$data['prediscount_subtotal'] = $totals['prediscount_subtotal']; // TODO: Duplicated code.
$data['prediscount_subtotal'] = $totals['prediscount_subtotal'];.
$data['cash_total'] = $totals['cash_total'];
$data['non_cash_total'] = $totals['total'];
$data['cash_amount_due'] = $totals['cash_amount_due'];
@@ -1255,23 +1239,9 @@ class Sales extends Secure_Controller
$data['quote_number'] = $this->sale_lib->get_quote_number();
$data['work_order_number'] = $this->sale_lib->get_work_order_number();
// TODO: the if/else set below should be converted to a switch
if ($this->sale_lib->get_mode() == 'sale_invoice') { // TODO: Duplicated code.
$data['mode_label'] = lang('Sales.invoice');
$data['customer_required'] = lang('Sales.customer_required');
} elseif ($this->sale_lib->get_mode() == 'sale_quote') {
$data['mode_label'] = lang('Sales.quote');
$data['customer_required'] = lang('Sales.customer_required');
} elseif ($this->sale_lib->get_mode() == 'sale_work_order') {
$data['mode_label'] = lang('Sales.work_order');
$data['customer_required'] = lang('Sales.customer_required');
} elseif ($this->sale_lib->get_mode() == 'return') {
$data['mode_label'] = lang('Sales.return');
$data['customer_required'] = lang('Sales.customer_optional');
} else {
$data['mode_label'] = lang('Sales.receipt');
$data['customer_required'] = lang('Sales.customer_optional');
}
$modeData = $this->getSaleModeLabel($this->sale_lib->get_mode());
$data['mode_label'] = $modeData['mode_label'];
$data['customer_required'] = $modeData['customer_required'];
return view("sales/register", $data);
}
@@ -1693,10 +1663,11 @@ class Sales extends Secure_Controller
$this->item->update_item_number($item_id, $item_number);
$cart = $this->sale_lib->get_cart();
$x = $this->search_cart_for_item_id($item_id, $cart);
if ($x != null) {
if ($x !== null) {
$cart[$x]['item_number'] = $item_number;
}
$this->sale_lib->set_cart($cart);
return $this->response->setJSON(['success' => true]);
}
/**
@@ -1715,11 +1686,12 @@ class Sales extends Secure_Controller
$cart = $this->sale_lib->get_cart();
$x = $this->search_cart_for_item_id($item_id, $cart);
if ($x != null) {
if ($x !== null) {
$cart[$x]['name'] = $name;
}
$this->sale_lib->set_cart($cart);
return $this->response->setJSON(['success' => true]);
}
/**
@@ -1738,11 +1710,12 @@ class Sales extends Secure_Controller
$cart = $this->sale_lib->get_cart();
$x = $this->search_cart_for_item_id($item_id, $cart);
if ($x != null) {
if ($x !== null) {
$cart[$x]['description'] = $description;
}
$this->sale_lib->set_cart($cart);
return $this->response->setJSON(['success' => true]);
}
/**

View File

@@ -8,12 +8,14 @@ use App\Models\Tax;
use App\Models\Tax_category;
use App\Models\Tax_code;
use App\Models\Tax_jurisdiction;
use App\Traits\Controller\Shared;
use CodeIgniter\HTTP\ResponseInterface;
use Config\OSPOS;
use Config\Services;
class Taxes extends Secure_Controller
{
use Shared;
private array $config;
private Tax_lib $tax_lib;
private Tax $tax;
@@ -140,44 +142,26 @@ class Taxes extends Secure_Controller
{
$tax_code_info = $this->tax->get_info($tax_code);
$default_tax_category_id = 1; // Tax category id is always the default tax category // TODO: Replace 1 with constant
$default_tax_category = $this->tax->get_tax_category($default_tax_category_id); // TODO: this variable is never used in the code.
$default_tax_category_id = 1; // Tax category id is always the default tax category // TODO: This variable is not used anywhere in the code
$default_tax_category = $this->tax->get_tax_category($default_tax_category_id); // TODO: This variable is not used anywhere in the code
$tax_rate_info = $this->tax->get_rate_info($tax_code, $default_tax_category_id);
$data['rounding_options'] = Rounding_mode::get_rounding_options();
$data['html_rounding_options'] = $this->get_html_rounding_options();
if ($this->config['tax_included']) {
$data['default_tax_type'] = Tax_lib::TAX_TYPE_INCLUDED;
} else {
$data['default_tax_type'] = Tax_lib::TAX_TYPE_EXCLUDED;
}
$data['rounding_options'] = Rounding_mode::get_rounding_options();
$data['html_rounding_options'] = $this->get_html_rounding_options();
if ($tax_code == NEW_ENTRY) { // TODO: Duplicated code
$data['tax_code'] = '';
$data['tax_code_name'] = '';
$data['tax_code_type'] = '0';
$data['city'] = '';
$data['state'] = '';
$data['tax_rate'] = '0.0000';
$data['rate_tax_code'] = '';
$data['rate_tax_category_id'] = 1;
$data['tax_category'] = '';
$data['add_tax_category'] = '';
$data['rounding_code'] = '0';
if ($tax_code == NEW_ENTRY) {
$taxData = $this->initDefaultTaxCodeData();
$data = array_merge($data, $taxData);
} else {
$data['tax_code'] = $tax_code;
$data['tax_code_name'] = $tax_code_info->tax_code_name;
$data['tax_code_type'] = $tax_code_info->tax_code_type;
$data['city'] = $tax_code_info->city;
$data['state'] = $tax_code_info->state;
$data['rate_tax_code'] = $tax_code_info->rate_tax_code;
$data['rate_tax_category_id'] = $tax_code_info->rate_tax_category_id;
$data['tax_category'] = $tax_code_info->tax_category;
$data['add_tax_category'] = '';
$data['tax_rate'] = $tax_rate_info->tax_rate;
$data['rounding_code'] = $tax_rate_info->rounding_code;
$taxData = $this->buildTaxCodeData($tax_code_info, $tax_rate_info);
$data = array_merge($data, $taxData);
}
$tax_rates = [];
@@ -300,7 +284,7 @@ class Taxes extends Secure_Controller
*/
public function getView_tax_jurisdictions(int $tax_code = NEW_ENTRY): string // TODO: This appears to be called no where in the code.
{
$tax_code_info = $this->tax->get_info($tax_code); // TODO: Duplicated code
$tax_code_info = $this->tax->get_info($tax_code);
$default_tax_category_id = 1; // Tax category id is always the default tax category
$default_tax_category = $this->tax->get_tax_category($default_tax_category_id); // TODO: This variable is not used anywhere in the code

View File

@@ -4,16 +4,15 @@ namespace App\Database\Migrations;
use App\Libraries\Tax_lib;
use App\Models\Appconfig;
use App\Traits\Database\SalesTaxMigration;
use CodeIgniter\Database\Migration;
use CodeIgniter\Database\ResultInterface;
/**
* @property tax_lib tax_lib
* @property appconfig appconfig
*/
class Migration_Sales_Tax_Data extends Migration
{
public const ROUND_UP = 5; // TODO: These need to be moved to constants.php
use SalesTaxMigration;
public const ROUND_UP = 5;
public const ROUND_DOWN = 6;
public const HALF_FIVE = 7;
public const YES = '1';
@@ -327,79 +326,18 @@ class Migration_Sales_Tax_Data extends Migration
}
}
/**
* @param string $string
* @return string
*/
public function clean(string $string): string // TODO: $string is not a good name for this variable
public function clean(string $string): string
{
$string = str_replace(' ', '-', $string); // Replaces all spaces with hyphens.
return preg_replace('/[^A-Za-z0-9\-]/', '', $string); // Removes special chars.
return $this->cleanIdentifier($string);
}
/**
* @param array $sales_taxes
* @return void
*/
public function apply_invoice_taxing(array &$sales_taxes): void
{
if (!empty($sales_taxes)) { // TODO: Duplicated code
$sort = [];
foreach ($sales_taxes as $key => $value) {
$sort['print_sequence'][$key] = $value['print_sequence'];
}
array_multisort($sort['print_sequence'], SORT_ASC, $sales_taxes);
}
$decimals = totals_decimals();
foreach ($sales_taxes as $row_number => $sales_tax) {
$sales_taxes[$row_number]['sale_tax_amount'] = $this->get_sales_tax_for_amount($sales_tax['sale_tax_basis'], $sales_tax['tax_rate'], $sales_tax['rounding_code'], $decimals);
}
$this->applyInvoiceTaxing($sales_taxes);
}
/**
* @param array $sales_taxes
* @return void
*/
public function round_sales_taxes(array &$sales_taxes): void
{
if (!empty($sales_taxes)) {
$sort = [];
foreach ($sales_taxes as $k => $v) {
$sort['print_sequence'][$k] = $v['print_sequence'];
}
array_multisort($sort['print_sequence'], SORT_ASC, $sales_taxes);
}
$decimals = totals_decimals();
foreach ($sales_taxes as $row_number => $sales_tax) {
$sale_tax_amount = (float)$sales_tax['sale_tax_amount'];
$rounding_code = $sales_tax['rounding_code'];
$rounded_sale_tax_amount = $sale_tax_amount;
if (
$rounding_code == PHP_ROUND_HALF_UP
|| $rounding_code == PHP_ROUND_HALF_DOWN
|| $rounding_code == PHP_ROUND_HALF_EVEN
|| $rounding_code == PHP_ROUND_HALF_ODD
) {
$rounded_sale_tax_amount = round($sale_tax_amount, $decimals, $rounding_code);
} elseif ($rounding_code == Migration_Sales_Tax_Data::ROUND_UP) {
$fig = (int) str_pad('1', $decimals, '0');
$rounded_sale_tax_amount = (ceil($sale_tax_amount * $fig) / $fig);
} elseif ($rounding_code == Migration_Sales_Tax_Data::ROUND_DOWN) {
$fig = (int) str_pad('1', $decimals, '0');
$rounded_sale_tax_amount = (floor($sale_tax_amount * $fig) / $fig);
} elseif ($rounding_code == Migration_Sales_Tax_Data::HALF_FIVE) {
$rounded_sale_tax_amount = round($sale_tax_amount / 5) * 5;
}
$sales_taxes[$row_number]['sale_tax_amount'] = $rounded_sale_tax_amount;
}
$this->roundSalesTaxes($sales_taxes, self::ROUND_UP, self::ROUND_DOWN, self::HALF_FIVE);
}
}

View File

@@ -2,25 +2,22 @@
namespace App\Database\Migrations;
use App\Traits\Database\SalesTaxMigration;
use CodeIgniter\Database\Migration;
use App\Libraries\Tax_lib;
use App\Models\Appconfig;
use CodeIgniter\Database\ResultInterface;
/**
*
*
* @property appconfig appconfig
* @property tax_lib tax_lib
*/
class Migration_TaxAmount extends Migration
{
use SalesTaxMigration;
public const ROUND_UP = 5;
public const ROUND_DOWN = 6;
public const HALF_FIVE = 7;
public const YES = '1';
public const VAT_TAX = '0';
public const SALES_TAX = '1'; // TODO: It appears that this constant is never used
public const SALES_TAX = '1';
private Appconfig $appconfig;
public function __construct()
@@ -305,79 +302,18 @@ class Migration_TaxAmount extends Migration
}
}
/**
* @param string $string
* @return string
*/
public function clean(string $string): string // TODO: This can probably go into the migration helper as it's used it more than one migration. Also, $string needs to be refactored to a different name.
public function clean(string $string): string
{
$string = str_replace(' ', '-', $string); // Replaces all spaces with hyphens.
return preg_replace('/[^A-Za-z0-9\-]/', '', $string); // Removes special chars.
return $this->cleanIdentifier($string);
}
/**
* @param array $sales_taxes
* @return void
*/
public function apply_invoice_taxing(array &$sales_taxes): void
{
if (!empty($sales_taxes)) { // TODO: Duplicated code
$sort = [];
foreach ($sales_taxes as $k => $v) {
$sort['print_sequence'][$k] = $v['print_sequence'];
}
array_multisort($sort['print_sequence'], SORT_ASC, $sales_taxes);
}
$decimals = totals_decimals();
foreach ($sales_taxes as $row_number => $sales_tax) {
$sales_taxes[$row_number]['sale_tax_amount'] = $this->get_sales_tax_for_amount($sales_tax['sale_tax_basis'], $sales_tax['tax_rate'], $sales_tax['rounding_code'], $decimals);
}
$this->applyInvoiceTaxing($sales_taxes);
}
/**
* @param array $sales_taxes
* @return void
*/
public function round_sales_taxes(array &$sales_taxes): void
{
if (!empty($sales_taxes)) {
$sort = [];
foreach ($sales_taxes as $k => $v) {
$sort['print_sequence'][$k] = $v['print_sequence'];
}
array_multisort($sort['print_sequence'], SORT_ASC, $sales_taxes);
}
$decimals = totals_decimals();
foreach ($sales_taxes as $row_number => $sales_tax) {
$sale_tax_amount = (float)$sales_tax['sale_tax_amount'];
$rounding_code = $sales_tax['rounding_code'];
$rounded_sale_tax_amount = $sale_tax_amount;
if (
$rounding_code == PHP_ROUND_HALF_UP // TODO: This block of if/elseif statements can be converted to a switch.
|| $rounding_code == PHP_ROUND_HALF_DOWN
|| $rounding_code == PHP_ROUND_HALF_EVEN
|| $rounding_code == PHP_ROUND_HALF_ODD
) {
$rounded_sale_tax_amount = round($sale_tax_amount, $decimals, $rounding_code);
} elseif ($rounding_code == Migration_TaxAmount::ROUND_UP) {
$fig = (int) str_pad('1', $decimals, '0');
$rounded_sale_tax_amount = (ceil($sale_tax_amount * $fig) / $fig);
} elseif ($rounding_code == Migration_TaxAmount::ROUND_DOWN) {
$fig = (int) str_pad('1', $decimals, '0');
$rounded_sale_tax_amount = (floor($sale_tax_amount * $fig) / $fig);
} elseif ($rounding_code == Migration_TaxAmount::HALF_FIVE) {
$rounded_sale_tax_amount = round($sale_tax_amount / 5) * 5;
}
$sales_taxes[$row_number]['sale_tax_amount'] = $rounded_sale_tax_amount;
}
$this->roundSalesTaxes($sales_taxes, self::ROUND_UP, self::ROUND_DOWN, self::HALF_FIVE);
}
}

View File

@@ -20,7 +20,7 @@ class Migration_database_optimizations extends Migration
$attribute = model(Attribute::class);
$attribute->delete_orphaned_values();
$attribute->deleteOrphanedValues();
$this->migrate_duplicate_attribute_values(DECIMAL);
$this->migrate_duplicate_attribute_values(DATE);

View File

@@ -1,49 +0,0 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
use Config\Database;
class MigrationEXIFStrippingOptions extends Migration
{
/**
* Perform a migration step.
*/
public function up(): void
{
log_message('info', 'Migrating EXIF Stripping Options');
$db = Database::connect();
$configs = [
[
'key' => 'exif_fields_to_keep',
'value' => 'Copyright,Orientation,Software'
]
];
foreach ($configs as $config) {
$existing = $db->table('app_config')
->where('key', $config['key'])
->get()
->getRow();
if ($existing === null) {
$db->table('app_config')->insert($config);
}
}
}
/**
* Revert a migration step.
*/
public function down(): void
{
$db = Database::connect();
$db->table('app_config')
->where('key', 'exif_fields_to_keep')
->delete();
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
use Config\Database;
class TestDatabaseBootstrapSeeder extends Seeder
{
public function run(): void
{
if (ENVIRONMENT !== 'testing') {
throw new \RuntimeException('TestDatabaseBootstrapSeeder can only run in the testing environment.');
}
$config = config('Database');
$group = $config->tests;
$dbName = $group['database'];
if ($dbName === '' || !str_contains(strtolower($dbName), 'test')) {
throw new \RuntimeException("Refusing to reset non-test database: {$dbName}");
}
$serverConn = Database::connect([
'hostname' => $group['hostname'],
'username' => $group['username'],
'password' => $group['password'],
'DBDriver' => $group['DBDriver'],
'database' => null,
'charset' => $group['charset'] ?? 'utf8mb4',
'DBCollat' => $group['DBCollat'] ?? 'utf8mb4_general_ci',
], false);
$serverConn->query("DROP DATABASE IF EXISTS `{$dbName}`");
$serverConn->query("CREATE DATABASE IF NOT EXISTS `{$dbName}`");
}
}

View File

@@ -36,21 +36,26 @@ class Db_log
private function generate_message(): string
{
$db = Database::connect();
$last_query = $db->getLastQuery();
$affected_rows = $db->affectedRows();
$execution_time = $this->convert_time($last_query->getDuration());
$lastQuery = $db->getLastQuery();
if ($lastQuery === null) {
return '';
}
$affectedRows = $db->affectedRows();
$executionTime = $this->convert_time($lastQuery->getDuration());
$message = '*** Query: ' . date('Y-m-d H:i:s T') . ' *******************'
. "\n" . $last_query->getQuery()
. "\n Affected rows: $affected_rows"
. "\n Execution Time: " . $execution_time['time'] . ' ' . $execution_time['unit'];
. "\n" . $lastQuery->getQuery()
. "\n Affected rows: $affectedRows"
. "\n Execution Time: " . $executionTime['time'] . ' ' . $executionTime['unit'];
$long_query = ($execution_time['unit'] === 's') && ($execution_time['time'] > 0.5);
if ($long_query) {
$longQuery = ($executionTime['unit'] === 's') && ($executionTime['time'] > 0.5);
if ($longQuery) {
$message .= ' [LONG RUNNING QUERY]';
}
return $this->config->db_log_only_long && !$long_query ? '' : $message;
return $this->config->db_log_only_long && !$longQuery ? '' : $message;
}
/**

View File

@@ -0,0 +1,35 @@
<?php
/**
* Translates the attribute type to the corresponding database column name.
*
* Maps attribute type constants to their corresponding attribute_values table columns.
* Defaults to 'attribute_value' for TEXT, DROPDOWN and CHECKBOX attribute types.
*
* @param string $input The attribute type constant (DATE, DECIMAL, etc.)
* @return string The database column name for storing this attribute type
*/
function getAttributeDataType(string $input): string
{
$columnMap = [
DATE => 'attribute_date',
DECIMAL => 'attribute_decimal',
];
return $columnMap[$input] ?? 'attribute_value';
}
/**
* Validates that the provided data type is an allowed attribute value type.
*
* @param string $dataType
* @return void
*/
function validateAttributeValueType(string $dataType): void
{
$attributeValueTypes = ['attribute_value', 'attribute_decimal', 'attribute_date'];
if (!in_array($dataType, $attributeValueTypes, true)) {
throw new InvalidArgumentException('Invalid data type');
}
}

View File

@@ -1,10 +1,10 @@
<?php
/**
* @param array $stock_locations
* @param array $attributes
* @return string
*/
function generate_import_items_csv(array $stock_locations, array $attributes): string
{
$csv_headers = pack('CCC', 0xef, 0xbb, 0xbf); // Encode the Byte-Order Mark (BOM) so that UTF-8 File headers display properly in Microsoft Excel

View File

@@ -94,4 +94,3 @@ function remove_backup(): void
@unlink($backup_path);
log_message('info', "Removed $backup_path");
}

View File

@@ -5,6 +5,7 @@ use App\Models\Employee;
use App\Models\Item_taxes;
use App\Models\Tax_category;
use CodeIgniter\Database\ResultInterface;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\Session\Session;
use Config\OSPOS;
use Config\Services;
@@ -577,8 +578,8 @@ function item_kit_headers(): array
['item_kit_number' => lang('Item_kits.item_kit_number')],
['name' => lang('Item_kits.name')],
['description' => lang('Item_kits.description')],
['total_cost_price' => lang('Items.cost_price'), 'sortable' => FALSE],
['total_unit_price' => lang('Items.unit_price'), 'sortable' => FALSE]
['total_cost_price' => lang('Items.cost_price'), 'sortable' => false],
['total_unit_price' => lang('Items.unit_price'), 'sortable' => false]
];
}
@@ -654,7 +655,7 @@ function expand_attribute_values(array $definition_names, array $row): array
foreach ($definition_names as $definition_id => $definitionInfo) {
if (isset($indexed_values[$definition_id])) {
$raw_value = $indexed_values[$definition_id];
// Format DECIMAL attributes according to locale
if (is_array($definitionInfo) && isset($definitionInfo['type']) && $definitionInfo['type'] === DECIMAL) {
$attribute_values["$definition_id"] = to_decimals($raw_value);
@@ -742,7 +743,7 @@ function get_expense_category_manage_table_headers(): string
}
/**
* Gets the html data row for the expenses category
* Gets the html data row for the expense category
*/
function get_expense_category_data_row(object $expense_category): array
{
@@ -841,7 +842,7 @@ function get_expenses_data_last_row(object $expense): array
}
/**
* Get the expenses payments summary
* Get the expense payments summary
*/
function get_expenses_manage_payments_summary(array $payments, ResultInterface $expenses): string // TODO: $expenses is passed but never used.
{
@@ -933,22 +934,22 @@ function get_controller(): string
}
/**
* Restores filter values from URL query string.
*
* @param CodeIgniter\HTTP\IncomingRequest $request The request object
* Restores filter values from the URL query string.
*
* @param IncomingRequest $request The request object
* @return array Array with 'start_date', 'end_date', and 'selected_filters' keys
*/
function restoreTableFilters($request): array
function restoreTableFilters(IncomingRequest $request): array
{
$startDate = $request->getGet('start_date', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$endDate = $request->getGet('end_date', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
$urlFilters = $request->getGet('filters', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
return array_filter([
'start_date' => $startDate ?: null,
'end_date' => $endDate ?: null,
'selected_filters' => $urlFilters ?? []
], function($value) {
], function ($value) {
return $value !== null && $value !== [];
});
}

View File

@@ -7,7 +7,7 @@ if (!function_exists('base64url_encode')) {
* @param string $data
* @return string
*/
function base64url_encode($data)
function base64url_encode(string $data): string
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
@@ -20,7 +20,7 @@ if (!function_exists('base64url_decode')) {
* @param string $data
* @return string|false
*/
function base64url_decode($data)
function base64url_decode(string $data): false|string
{
$remainder = strlen($data) % 4;
if ($remainder) {
@@ -28,4 +28,4 @@ if (!function_exists('base64url_decode')) {
}
return base64_decode(strtr($data, '-_', '+/'));
}
}
}

View File

@@ -329,6 +329,4 @@ return [
"wholesale_markup" => "",
"work_order_enable" => "Work Order Support",
"work_order_format" => "Work Order Format",
"exif_fields_to_keep" => "EXIF Fields to Keep",
"exif_fields_to_keep_tooltip" => "Select EXIF fields to preserve in uploaded images. Fields not selected will be removed. Leave empty to disable EXIF stripping. Keeps beneficial metadata while removing privacy-sensitive data like GPS location.",
];

View File

@@ -1,220 +0,0 @@
<?php
namespace App\Libraries;
use lsolesen\pel\PelIfd;
use lsolesen\pel\PelJpeg;
use lsolesen\pel\PelTag;
class Image_lib
{
private array $exif_to_pel_tags = [
'Make' => PelTag::MAKE,
'Model' => PelTag::MODEL,
'Orientation' => PelTag::ORIENTATION,
'Copyright' => PelTag::COPYRIGHT,
'Software' => PelTag::SOFTWARE,
'DateTime' => PelTag::DATE_TIME,
];
public function stripEXIF(string $filepath, array $fields_to_keep = []): bool
{
if (!file_exists($filepath)) {
return false;
}
$mimetype = mime_content_type($filepath);
$allowed_types = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($mimetype, $allowed_types)) {
return false;
}
if ($mimetype === 'image/jpeg' || $mimetype === 'image/jpg') {
return $this->stripExifJpeg($filepath, $fields_to_keep);
}
if ($mimetype === 'image/png') {
return $this->stripExifPng($filepath);
}
if ($mimetype === 'image/gif') {
return $this->stripExifGif($filepath);
}
if ($mimetype === 'image/webp') {
return $this->stripExifWebp($filepath);
}
return false;
}
private function stripExifJpeg(string $filepath, array $fields_to_keep = []): bool
{
try {
$data = file_get_contents($filepath);
if ($data === false) {
return false;
}
$jpeg = new PelJpeg($data);
$exif = $jpeg->getExif();
if ($exif === null) {
return true;
}
$tiff = $exif->getTiff();
if ($tiff === null) {
return true;
}
$ifd0 = $tiff->getIfd();
if ($ifd0 !== null) {
$this->removeExifFields($ifd0, $fields_to_keep);
$subIfd = $ifd0->getSubIfd(PelTag::EXIF_IFD_POINTER);
if ($subIfd !== null) {
$this->removeExifFields($subIfd, $fields_to_keep);
}
if (!in_array('GPS', $fields_to_keep)) {
$ifd0->removeEntry(PelTag::GPS_INFO_IFD_POINTER);
}
}
$jpeg->saveFile($filepath);
return true;
} catch (\Exception $e) {
return $this->stripExifFallback($filepath);
}
}
private function removeExifFields(PelIfd $ifd, array $fields_to_keep): void
{
$tags_to_remove = array_diff(array_keys($this->exif_to_pel_tags), $fields_to_keep);
foreach ($tags_to_remove as $field_name) {
$pel_tag = $this->exif_to_pel_tags[$field_name];
$entry = $ifd->getEntry($pel_tag);
if ($entry !== null) {
$ifd->removeEntry($pel_tag);
}
}
}
private function stripExifPng(string $filepath): bool
{
$image = @imagecreatefrompng($filepath);
if ($image === false) {
return false;
}
imagealphablending($image, false);
imagesavealpha($image, true);
$result = imagepng($image, $filepath, 9);
imagedestroy($image);
return $result;
}
private function stripExifGif(string $filepath): bool
{
$content = file_get_contents($filepath);
if ($content === false) {
return false;
}
if (strpos($content, "\x21\xF9\x04") !== false || strpos($content, 'NETSCAPE2.0') !== false) {
return true;
}
$image = @imagecreatefromgif($filepath);
if ($image === false) {
return false;
}
$result = imagegif($image, $filepath);
imagedestroy($image);
return $result;
}
private function stripExifWebp(string $filepath): bool
{
if (!function_exists('imagecreatefromwebp')) {
return false;
}
$image = @imagecreatefromwebp($filepath);
if ($image === false) {
return false;
}
$result = imagewebp($image, $filepath, 100);
imagedestroy($image);
return $result;
}
private function stripExifFallback(string $filepath): bool
{
$content = file_get_contents($filepath);
if ($content === false) {
return false;
}
$markers = [];
$offset = 0;
while ($offset < strlen($content)) {
if ($offset + 4 > strlen($content)) {
break;
}
$marker = ord($content[$offset + 1]);
if (ord($content[$offset]) !== 0xFF) {
break;
}
if ($marker >= 0xE0 && $marker <= 0xEF) {
$marker_len = ord($content[$offset + 2]) * 256 + ord($content[$offset + 3]);
$markers[] = [$offset, $marker_len + 2];
$offset += $marker_len + 2;
} elseif ($marker === 0xD8 || $marker === 0xD9) {
$offset += 2;
} elseif ($marker === 0x00 || $marker === 0xD0 || $marker === 0xD1 || $marker === 0xD2 || $marker === 0xD3 || $marker === 0xD4 || $marker === 0xD5 || $marker === 0xD6 || $marker === 0xD7) {
$offset += 2;
} elseif ($marker === 0x01) {
$offset += 2;
} else {
if ($offset + 4 > strlen($content)) {
break;
}
$marker_len = ord($content[$offset + 2]) * 256 + ord($content[$offset + 3]);
$offset += $marker_len + 2;
}
}
if (empty($markers)) {
return true;
}
$marker_names = [];
foreach ($markers as $marker_info) {
$marker_byte = ord($content[$marker_info[0] + 1]);
$marker_names[] = 'APP' . ($marker_byte - 0xE0);
}
log_message('warning', "stripExifFallback: Removing all APP markers from {$filepath}: " . implode(', ', $marker_names));
$new_content = $content;
foreach (array_reverse($markers) as $marker_info) {
$new_content = substr_replace($new_content, '', $marker_info[0], $marker_info[1]);
}
return file_put_contents($filepath, $new_content) !== false;
}
}

View File

@@ -6,8 +6,7 @@ use CodeIgniter\Language\Language;
class MY_Language extends Language
{
public function getLine(string $line, array $args = [])
public function getLine(string $line, array $args = []): array|string
{
// If no file is given, just parse the line
if (! str_contains($line, '.')) {
@@ -20,7 +19,7 @@ class MY_Language extends Language
$output = $this->getTranslationOutput($this->locale, $file, $parsedLine);
if ($output === NULL && strpos($this->locale, '-')) {
if ($output === null && strpos($this->locale, '-')) {
[$locale] = explode('-', $this->locale, 2);
[$file, $parsedLine] = $this->parseLine($line, $locale);
@@ -29,7 +28,7 @@ class MY_Language extends Language
}
// If still not found, try English
if ($output === NULL || $output === "") {
if ($output === null || $output === "") {
[$file, $parsedLine] = $this->parseLine($line, 'en');
$output = $this->getTranslationOutput('en', $file, $parsedLine);

View File

@@ -394,7 +394,7 @@ class Receiving_lib
/**
* @param $line int|string The item_number or item_id of the item to be removed from the receiving.
*/
public function delete_item($line): void
public function delete_item(int|string $line): void
{
$items = $this->get_cart();
unset($items[$line]);

View File

@@ -9,6 +9,7 @@ use CodeIgniter\Model;
use CodeIgniter\Database\RawSql;
use Config\OSPOS;
use DateTime;
use InvalidArgumentException;
use stdClass;
use ReflectionClass;
@@ -471,7 +472,7 @@ class Attribute extends Model
}
} elseif ($from_type === DROPDOWN) {
if (in_array($to_type, [TEXT, CHECKBOX], true)) {
if ($to_type === CHECKBOX) { // TODO: Duplicated code.
if ($to_type === CHECKBOX) {
$checkbox_attribute_values = $this->checkbox_attribute_values($definition_id);
$this->db->transStart();
@@ -498,7 +499,7 @@ class Attribute extends Model
}
$this->delete_orphaned_links($definition_id);
$this->delete_orphaned_values();
$this->deleteOrphanedValues();
return $success;
}
@@ -517,7 +518,6 @@ class Attribute extends Model
if (!$one_attribute_id) {
$one_attribute_id = $this->saveAttributeValue('1', $definition_id, false, false, CHECKBOX);
$one_attribute_id = $this->saveAttributeValue('1', $definition_id, false, false, CHECKBOX);
}
return [$zero_attribute_id, $one_attribute_id];
@@ -526,43 +526,43 @@ class Attribute extends Model
/**
* Inserts or updates a definition
*/
public function save_definition(array &$definition_data, int $definition_id = NO_DEFINITION_ID): bool
public function saveDefinition(array &$definitionData, int $definitionId = NO_DEFINITION_ID): bool
{
$this->db->transStart();
// Definition doesn't exist
if ($definition_id === NO_DEFINITION_ID || !$this->exists($definition_id)) {
if ($this->exists($definition_id, true)) {
$success = $this->undelete($definition_id);
// Insert definition
if ($definitionId === NO_DEFINITION_ID || !$this->exists($definitionId)) {
if ($this->exists($definitionId, true)) {
$success = $this->undelete($definitionId);
} else {
$builder = $this->db->table('attribute_definitions');
$success = $builder->insert($definition_data);
$definition_data['definition_id'] = $this->db->insertID();
$success = $builder->insert($definitionData);
$definitionData['definition_id'] = $definitionId !== CATEGORY_DEFINITION_ID ? $this->db->insertID() : $definitionId;
}
}
// Definition already exists
// Update definition
else {
$builder = $this->db->table('attribute_definitions');
$builder->select('definition_type');
$builder->where('definition_id', $definition_id);
$builder->where('definition_id', $definitionId);
$builder->where('deleted', ACTIVE);
$query = $builder->get();
$row = $query->getRow();
$from_definition_type = $row->definition_type;
$to_definition_type = $definition_data['definition_type'];
$to_definition_type = $definitionData['definition_type'];
// Update the definition values
$builder->where('definition_id', $definition_id);
// Update definition values
$builder->where('definition_id', $definitionId);
$success = $builder->update($definitionData);
$definitionData['definition_id'] = $definitionId;
$success = $builder->update($definition_data);
$definition_data['definition_id'] = $definition_id;
if ($from_definition_type !== $to_definition_type) {
if (!$this->convert_definition_data($definition_id, $from_definition_type, $to_definition_type)) {
return false;
}
if ($from_definition_type !== $to_definition_type
&& !$this->convert_definition_data($definitionId, $from_definition_type, $to_definition_type)) {
$this->db->transRollback();
return false;
}
}
@@ -575,10 +575,10 @@ class Attribute extends Model
/**
* @param string $definition_name
* @param $definition_type
* @param string|bool $definition_type
* @return array
*/
public function get_definition_by_name(string $definition_name, $definition_type = false): array
public function get_definition_by_name(string $definition_name, string|bool $definition_type = false): array
{
$builder = $this->db->table('attribute_definitions');
$builder->where('definition_name', $definition_name);
@@ -601,24 +601,48 @@ class Attribute extends Model
*/
public function saveAttributeLink(int $itemId, int $definitionId, int $attributeId): bool
{
$normalizedItemId = empty($itemId) ? null : $itemId;
$normalizedAttributeId = empty($attributeId) ? null : $attributeId;
$this->db->transStart();
$definitionType = $this->getAttributeInfo($definitionId)->definition_type ?? '';
$builder = $this->db->table('attribute_links');
if ($this->attributeLinkExists($itemId, $definitionId)) {
$builder->set(['attribute_id' => $attributeId]);
if ($definitionType === DROPDOWN && $normalizedItemId === null) {
$builder->where('item_id', $normalizedItemId);
$builder->where('definition_id', $definitionId);
$builder->where('item_id', $itemId);
$builder->where('attribute_id', $normalizedAttributeId);
$builder->where('sale_id', null);
$builder->where('receiving_id', null);
$builder->update();
$dropdownAttributeLinkExists = $builder->countAllResults(false) !== 0;
if (!$dropdownAttributeLinkExists) {
$data = [
'attribute_id' => $normalizedAttributeId,
'item_id' => $normalizedItemId,
'definition_id' => $definitionId
];
$builder->insert($data);
}
} else {
$data = [
'attribute_id' => $attributeId,
'item_id' => $itemId,
'definition_id' => $definitionId
];
$builder->insert($data);
if ($this->attributeLinkExists($normalizedItemId, $definitionId)) {
$builder->set(['attribute_id' => $normalizedAttributeId]);
$builder->where('definition_id', $definitionId);
$builder->where('item_id', $normalizedItemId);
$builder->where('sale_id', null);
$builder->where('receiving_id', null);
$builder->update();
} else {
$data = [
'attribute_id' => $normalizedAttributeId,
'item_id' => $normalizedItemId,
'definition_id' => $definitionId
];
$builder->insert($data);
}
}
$this->db->transComplete();
@@ -627,24 +651,28 @@ class Attribute extends Model
}
/**
* @param int $item_id
* @param int|bool $definition_id
* @return bool
* Deletes attribute links for a given item and optionally a given definition. Does not delete links where sale_id
* or receiving_id has a value. If a definitionId is not provided, deletes all attribute links for the item that do
* not have a sale_id or receiving_id value.
*
* @param int $itemId The item ID to delete links for.
* @param int|bool $definitionId The definition ID to delete links for. (optional)
* @return bool true if successful, false otherwise
*/
public function deleteAttributeLinks(int $item_id, int|bool $definition_id = false): bool
public function deleteAttributeLinks(int $itemId, int|bool $definitionId = false): bool
{
$delete_data = ['item_id' => $item_id];
$deleteData = ['item_id' => $itemId];
// Exclude rows where sale_id or receiving_id has a value
$builder = $this->db->table('attribute_links');
$builder->where('sale_id', null);
$builder->where('receiving_id', null);
if (!empty($definition_id)) {
$delete_data += ['definition_id' => $definition_id];
if (!empty($definitionId)) {
$deleteData += ['definition_id' => $definitionId];
}
return $builder->delete($delete_data);
return $builder->delete($deleteData);
}
/**
@@ -703,7 +731,7 @@ class Attribute extends Model
* @param int $definition_id
* @return object|null
*/
public function get_attribute_value(int $item_id, int $definition_id): ?object
public function getAttributeValue(int $item_id, int $definition_id): ?object
{
$builder = $this->db->table('attribute_values');
$builder->join('attribute_links', 'attribute_links.attribute_id = attribute_values.attribute_id');
@@ -720,6 +748,31 @@ class Attribute extends Model
return $this->getEmptyObject('attribute_values');
}
/**
* Gets a single attribute value by attribute ID.
*
* @param int $attributeId The attribute ID to look up
* @param string $dataType The column name to retrieve (attribute_value, attribute_date, or attribute_decimal)
* @return string|float|null The attribute value. Note: MySQL returns values as follows:
* - attribute_value (TEXT): string
* - attribute_date (DATE): string in 'Y-m-d' format
* - attribute_decimal (DECIMAL): string or float depending on CodeIgniter configuration
* Returns null if the attribute_id is not found.
*/
public function getAttributeValueByAttributeId(int $attributeId, string $dataType): string|float|null
{
helper('attribute');
validateAttributeValueType($dataType);
$builder = $this->db->table('attribute_values');
$builder->select($dataType);
$builder->where('attribute_id', $attributeId);
$builder->limit(1);
$row = $builder->get()->getRow();
return $row ? $row->$dataType : null;
}
/**
* Initializes an empty object based on database definitions
* @param string $table_name
@@ -806,67 +859,155 @@ class Attribute extends Model
}
/**
* @param string $attribute_value
* @param int $definition_id
* @param $item_id
* @param $attribute_id
* @param string $definition_type
* @return int
* Saves an attribute value and creates an attribute link between the attribute value and item if necessary.
* If the attribute value already exists, it will simply create a link to the existing attribute value.
* If the attribute value exists but only has capitalization differences, it will update the existing attribute
* value to match the new capitalization.
* @param string $attributeValue The attribute value to be saved.
* @param int $definitionId The ID of the attribute definition this value is associated with.
* @param int|bool $itemId The ID of the item to link this attribute value to. If false, NULL will be inserted into
* the database for that itemId indicating it is a dropdown value and not linked to a specific item.
* @param int|bool $attributeId The ID of the attribute value if it already exists and is being updated. If false,
* a new attribute value will be created.
* @param string $definitionType The type of the attribute definition which will dictate which column the attribute
* value is saved to.
* @return int The attribute ID of the saved attribute value.
*/
public function saveAttributeValue(string $attribute_value, int $definition_id, int|bool $item_id = false, int|bool $attribute_id = false, string $definition_type = DROPDOWN): int
public function saveAttributeValue(string $attributeValue, int $definitionId, int|bool $itemId = false, int|bool $attributeId = false, string $definitionType = DROPDOWN): int
{
$config = config(OSPOS::class)->settings;
helper('attribute');
$dataType = getAttributeDataType($definitionType);
if ($definitionType === DATE) {
$config = config(OSPOS::class)->settings;
$date = DateTime::createFromFormat($config['dateformat'], $attributeValue);
if ($date !== false) {
$attributeValue = $date->format('Y-m-d');
}
}
$this->db->transStart();
switch ($definition_type) {
case DATE:
$data_type = 'date';
$attribute_date_value = DateTime::createFromFormat($config['dateformat'], $attribute_value);
$attribute_value = $attribute_date_value->format('Y-m-d');
break;
case DECIMAL:
$data_type = 'decimal';
break;
default:
$data_type = 'value';
break;
}
$existingAttributeId = $this->attributeValueExists($attributeValue, $definitionType);
// New Attribute
if (empty($attribute_id) || empty($item_id) || $attribute_id == -1) {
$attribute_id = $this->attributeValueExists($attribute_value, $definition_type);
// Update
if ($existingAttributeId) {
$attributeId = $existingAttributeId;
$storedValue = $this->getAttributeValueByAttributeId($attributeId, $dataType);
if (!$attribute_id) {
$builder = $this->db->table('attribute_values');
$builder->set(["attribute_$data_type" => $attribute_value]);
$builder->insert();
$attribute_id = $this->db->insertID();
if ($dataType === 'attribute_value'
&& is_string($storedValue)
&& strcasecmp($storedValue, $attributeValue) === 0
&& $storedValue !== $attributeValue
) {
$this->updateAttributeValue($attributeId, $dataType, $attributeValue);
}
$data = [
'attribute_id' => empty($attribute_id) ? null : $attribute_id,
'item_id' => empty($item_id) ? null : $item_id,
'definition_id' => $definition_id
];
$builder = $this->db->table('attribute_links');
$builder->set($data);
$builder->insert();
}
// Existing Attribute
else {
} else {
// Insert
$builder = $this->db->table('attribute_values');
$builder->set(["attribute_$data_type" => $attribute_value]);
$builder->where('attribute_id', $attribute_id);
$builder->update();
$builder->set([$dataType => $attributeValue]);
$builder->insert();
$attributeId = $this->db->insertID();
}
if (!empty($definitionId)) {
$success = $this->saveAttributeLink($itemId, $definitionId, $attributeId);
if (!$success) {
$this->db->transRollback();
return 0;
}
}
$this->db->transComplete();
return $attribute_id;
return $attributeId;
}
/**
* Saves attribute data found in one row of a CSV import file. Loops through all attribute definitions and checks
* if there is data for that attribute in the row. If there is, it saves the attribute value and link to the item.
*
* @param array $attributeValues Attribute name/value pairs from one row of the CSV import file
* @param array $itemData Contains data for the item being imported/updated from the CSV file.
* @param array $definitions Contains all attribute definitions in the system.
* @return bool Returns true if all attribute data saves correctly and false if there is an error saving any of
* the attribute data.
*/
public function saveCSVRowAttributeData(array $attributeValues, array $itemData, array $definitions): bool
{
helper('attribute');
foreach ($definitions as $definition) {
$attributeName = $definition['definition_name'];
$attributeValue = $attributeValues[$attributeName] ?? null;
if (isset($attributeValue) && strcasecmp($attributeValue, '_DELETE_') === 0) {
if (!$this->deleteAttributeLinks($itemData['item_id'], $definition['definition_id'])) {
return false;
}
continue;
}
// Create attribute value
if (!empty($attributeValue) || $attributeValue === '0') {
if ($definition['definition_type'] === CHECKBOX) {
$checkbox_is_unchecked = (strcasecmp($attributeValue, 'false') === 0 || $attributeValue === '0');
$attributeValue = $checkbox_is_unchecked ? '0' : '1';
$attribute_id = $this->storeCSVAttributeValue($attributeValue, $definition, $itemData['item_id']);
} elseif (!empty($attributeValue) || $attributeValue === '0') {
$attribute_id = $this->storeCSVAttributeValue($attributeValue, $definition, $itemData['item_id']);
} else {
return false;
}
if (!$attribute_id) {
return false;
}
}
}
return true;
}
/**
* Saves the attribute_value and attribute_link in a CSV file if necessary
*
* @param string $value
* @param array $attributeData
* @param int $itemId
* @return bool|int
*/
private function storeCSVAttributeValue(string $value, array $attributeData, int $itemId): bool|int
{
$this->db->transStart();
$attributeId = $this->attributeValueExists($value, $attributeData['definition_type']);
$this->deleteAttributeLinks($itemId, $attributeData['definition_id']);
if (!$attributeId) {
$attributeId = $this->saveAttributeValue($value, $attributeData['definition_id'], $itemId, false, $attributeData['definition_type']);
} else {
helper('attribute');
$dataType = getAttributeDataType($attributeData['definition_type']);
$storedValue = $this->getAttributeValueByAttributeId($attributeId, $dataType);
// Update the attribute value if only the case has changed and only for text values.
if ($dataType === 'attribute_value'
&& is_string($storedValue)
&& strcasecmp($storedValue, $value) === 0
&& $storedValue !== $value) {
$attributeId = $this->saveAttributeValue($value, $attributeData['definition_id'], $itemId, $attributeId, $attributeData['definition_type']);
} elseif (!$this->saveAttributeLink($itemId, $attributeData['definition_id'], $attributeId)) {
return false;
}
}
$this->db->transComplete();
if (!$this->db->transStatus()) {
return false;
}
return $attributeId;
}
/**
@@ -899,15 +1040,14 @@ class Attribute extends Model
return $builder->update(['deleted' => DELETED]);
}
/**
* Deletes attribute links by definition ID
*
* @param int|array $definition_id
*/
/**
* Deletes attribute links by definition ID
*
* @param int|array $definition_id
*/
public function deleteAttributeLinksByDefinitionId(int|array $definition_id): void
{
if(!is_array($definition_id))
{
if (!is_array($definition_id)) {
$definition_id = [$definition_id];
}
@@ -951,7 +1091,7 @@ class Attribute extends Model
*
* @return boolean true is returned if the delete was successful or false if there were any failures
*/
public function delete_orphaned_values(): bool
public function deleteOrphanedValues(): bool
{
$subquery = $this->db->table('attribute_links')
->distinct()
@@ -1039,7 +1179,7 @@ class Attribute extends Model
*
* @param int $definitionId
* @param int $attributeId
* @return \CodeIgniter\Database\BaseBuilder
* @return void
*/
private function deleteAttributeLinksByDefinitionIdAndAttributeId(int $definitionId, int $attributeId): void
{
@@ -1050,4 +1190,41 @@ class Attribute extends Model
$builder->where('attribute_id', $attributeId);
$builder->delete();
}
/**
* Updates the attribute_value, attribute_date, or attribute_decimal column in the attribute_values table based on
* the provided data type for a specific attribute ID.
*
* @param int $attributeId
* @param string $dataType
* @param mixed $attributeValue
* @return void
*/
private function updateAttributeValue(int $attributeId, string $dataType, mixed $attributeValue): void
{
helper('attribute');
validateAttributeValueType($dataType);
// Update the attribute_values table
$builder = $this->db->table('attribute_values');
$builder->set([$dataType => $attributeValue]);
$builder->where('attribute_id', $attributeId);
$builder->update();
// Check if this attribute_id is linked to definition_id = -1 (category dropdown) using COUNT
$linkBuilder = $this->db->table('attribute_links');
$linkBuilder->selectCount('attribute_id', 'cnt');
$linkBuilder->where('attribute_id', $attributeId);
$linkBuilder->where('definition_id', CATEGORY_DEFINITION_ID);
$countRow = $linkBuilder->get()->getRow();
$isCategoryDropdownAttribute = $countRow && $countRow->cnt > 0;
// Update the items.category column to match new capitalization.
if ($isCategoryDropdownAttribute) {
$itemsBuilder = $this->db->table('items');
$itemsBuilder->set(['category' => $attributeValue]);
$itemsBuilder->where('category', $attributeValue);
$itemsBuilder->update();
}
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use CodeIgniter\Database\ResultInterface;
use Config\OSPOS;
use stdClass;
/**
* Customer class
@@ -128,7 +129,7 @@ class Customer extends Person
/**
* Gets stats about a particular customer
*/
public function get_stats(int $customer_id)
public function get_stats(int $customer_id): ?stdClass
{
$db_prefix = $this->db->getPrefix();
$totals_decimals = totals_decimals();
@@ -422,7 +423,7 @@ class Customer extends Person
$builder->orLike('phone_number', $search);
$builder->orLike('account_number', $search);
$builder->orLike('company_name', $search);
$builder->orLike('CONCAT(first_name, " ", last_name)', $search); // TODO: Duplicated code.
$builder->orLike('CONCAT(first_name, " ", last_name)', $search);
$builder->groupEnd();
$builder->where('deleted', 0);

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use CodeIgniter\Database\ResultInterface;
use CodeIgniter\Session\Session;
use stdClass;
/**
* Employee class
@@ -407,7 +408,7 @@ class Employee extends Person
/**
* Gets information about the currently logged in employee.
*/
public function get_logged_in_employee_info()
public function get_logged_in_employee_info(): float|false|array|int|string|stdClass|null
{
if ($this->is_logged_in()) {
return $this->get_info($this->session->get('person_id'));

View File

@@ -352,7 +352,7 @@ class Item extends Model
/**
* Gets information about a particular item by item id or number
*/
public function get_info_by_id_or_number(string $item_id, bool $include_deleted = true)
public function get_info_by_id_or_number(string $item_id, bool $include_deleted = true): stdClass|string
{
$builder = $this->db->table('items');
$builder->groupStart();
@@ -547,9 +547,9 @@ class Item extends Model
public function get_search_suggestion_format(?string $seed = null): string
{
$config = config(OSPOS::class)->settings;
$suggestionsFirstColumn = $this->suggestionColumnIsAllowed($config['suggestions_first_column'])
? $config['suggestions_first_column']
? $config['suggestions_first_column']
: 'name';
$seed .= ',' . $suggestionsFirstColumn;
@@ -573,14 +573,14 @@ class Item extends Model
$config = config(OSPOS::class)->settings;
$label = '';
$label1 = $this->suggestionColumnIsAllowed($config['suggestions_first_column'])
? $config['suggestions_first_column']
$label1 = $this->suggestionColumnIsAllowed($config['suggestions_first_column'])
? $config['suggestions_first_column']
: 'name';
$label2 = $this->suggestionColumnIsAllowed($config['suggestions_second_column'])
? $config['suggestions_second_column']
$label2 = $this->suggestionColumnIsAllowed($config['suggestions_second_column'])
? $config['suggestions_second_column']
: '';
$label3 = $this->suggestionColumnIsAllowed($config['suggestions_third_column'])
? $config['suggestions_third_column']
$label3 = $this->suggestionColumnIsAllowed($config['suggestions_third_column'])
? $config['suggestions_third_column']
: '';
$this->format_result_numbers($result_row);

View File

@@ -116,7 +116,7 @@ class Module extends Model
public function get_allowed_office_modules(int $person_id): ResultInterface
{
$menus = ['office', 'both'];
$builder = $this->db->table('modules'); // TODO: Duplicated code
$builder = $this->db->table('modules');
$builder->join('permissions', 'permissions.permission_id = modules.module_id');
$builder->join('grants', 'permissions.permission_id = grants.permission_id');
$builder->where('person_id', $person_id);

View File

@@ -3,32 +3,21 @@
namespace App\Models\Reports;
use App\Models\Sale;
use App\Traits\Models\Reports\SaleTypeFilter;
/**
*
*
* @property sale sale
*
*/
class Detailed_sales extends Report
{
/**
* @param array $inputs
* @return void
*/
use SaleTypeFilter;
public function create(array $inputs): void
{
// Create our temp tables to work with the data in our report
$sale = model(Sale::class);
$sale->create_temp_table($inputs);
}
/**
* @return array
*/
public function getDataColumns(): array
{
return [ // TODO: Duplicated code
return [
'summary' => [
['id' => lang('Reports.sale_id')],
['type_code' => lang('Reports.code_type')],
@@ -119,47 +108,11 @@ class Detailed_sales extends Report
MAX(payment_type) AS payment_type,
MAX(comment) AS comment');
if ($inputs['location_id'] != 'all') { // TODO: Duplicated code
if ($inputs['location_id'] != 'all') {
$builder->where('item_location', $inputs['location_id']);
}
switch ($inputs['sale_type']) {
case 'complete':
$builder->where('sale_status', COMPLETED);
$builder->groupStart();
$builder->where('sale_type', SALE_TYPE_POS);
$builder->orWhere('sale_type', SALE_TYPE_INVOICE);
$builder->orWhere('sale_type', SALE_TYPE_RETURN);
$builder->groupEnd();
break;
case 'sales':
$builder->where('sale_status', COMPLETED);
$builder->groupStart();
$builder->where('sale_type', SALE_TYPE_POS);
$builder->orWhere('sale_type', SALE_TYPE_INVOICE);
$builder->groupEnd();
break;
case 'quotes':
$builder->where('sale_status', SUSPENDED);
$builder->where('sale_type', SALE_TYPE_QUOTE);
break;
case 'work_orders':
$builder->where('sale_status', SUSPENDED);
$builder->where('sale_type', SALE_TYPE_WORK_ORDER);
break;
case 'canceled':
$builder->where('sale_status', CANCELED);
break;
case 'returns':
$builder->where('sale_status', COMPLETED);
$builder->where('sale_type', SALE_TYPE_RETURN);
break;
}
$this->applySaleTypeFilter($builder, $inputs['sale_type'], false);
$builder->groupBy('sale_id');
$builder->orderBy('MAX(sale_time)');
@@ -209,56 +162,16 @@ class Detailed_sales extends Report
return $data;
}
/**
* @param array $inputs
* @return array
*/
public function getSummaryData(array $inputs): array
{
$builder = $this->db->table('sales_items_temp');
$builder->select('SUM(subtotal) AS subtotal, SUM(tax) AS tax, SUM(total) AS total, SUM(cost) AS cost, SUM(profit) AS profit');
if ($inputs['location_id'] != 'all') { // TODO: Duplicated code
if ($inputs['location_id'] != 'all') {
$builder->where('item_location', $inputs['location_id']);
}
switch ($inputs['sale_type']) {
case 'complete':
$builder->where('sale_status', COMPLETED);
$builder->groupStart();
$builder->where('sale_type', SALE_TYPE_POS);
$builder->orWhere('sale_type', SALE_TYPE_INVOICE);
$builder->orWhere('sale_type', SALE_TYPE_RETURN);
$builder->groupEnd();
break;
case 'sales':
$builder->where('sale_status', COMPLETED);
$builder->groupStart();
$builder->where('sale_type', SALE_TYPE_POS);
$builder->orWhere('sale_type', SALE_TYPE_INVOICE);
$builder->groupEnd();
break;
case 'quotes':
$builder->where('sale_status', SUSPENDED);
$builder->where('sale_type', SALE_TYPE_QUOTE);
break;
case 'work_orders':
$builder->where('sale_status', SUSPENDED);
$builder->where('sale_type', SALE_TYPE_WORK_ORDER);
break;
case 'canceled':
$builder->where('sale_status', CANCELED);
break;
case 'returns':
$builder->where('sale_status', COMPLETED);
$builder->where('sale_type', SALE_TYPE_RETURN);
break;
}
$this->applySaleTypeFilter($builder, $inputs['sale_type'], false);
return $builder->get()->getRowArray();
}

View File

@@ -93,7 +93,7 @@ class Specific_customer extends Report
MAX(payment_type) AS payment_type,
MAX(comment) AS comment');
$builder->where('customer_id', $inputs['customer_id']); // TODO: Duplicated code
$builder->where('customer_id', $inputs['customer_id']);
if ($inputs['payment_type'] == 'invoices') {
$builder->where('sale_type', SALE_TYPE_INVOICE);
@@ -139,7 +139,7 @@ class Specific_customer extends Report
break;
}
$builder->groupBy('sale_id'); // TODO: Duplicated code
$builder->groupBy('sale_id');
$builder->orderBy('MAX(sale_time)');
$data = [];

View File

@@ -27,7 +27,7 @@ class Specific_discount extends Report
* @return array
*/
public function getDataColumns(): array
{ // TODO: Duplicated code
{
return [
'summary' => [
['id' => lang('Reports.sale_id')],
@@ -95,7 +95,7 @@ class Specific_discount extends Report
MAX(payment_type) AS payment_type,
MAX(comment) AS comment');
$builder->where('discount >=', $inputs['discount']); // TODO: Duplicated code
$builder->where('discount >=', $inputs['discount']);
$builder->where('discount_type', $inputs['discount_type']);
switch ($inputs['sale_type']) {
@@ -136,7 +136,7 @@ class Specific_discount extends Report
break;
}
$builder->groupBy('sale_id'); // TODO: Duplicated code
$builder->groupBy('sale_id');
$builder->orderBy('MAX(sale_time)');
$data = [];
@@ -168,7 +168,7 @@ class Specific_discount extends Report
$builder = $this->db->table('sales_items_temp');
$builder->select('SUM(subtotal) AS subtotal, SUM(tax) AS tax, SUM(total) AS total, SUM(cost) AS cost, SUM(profit) AS profit');
$builder->where('discount >=', $inputs['discount']); // TODO: Duplicated code
$builder->where('discount >=', $inputs['discount']);
$builder->where('discount_type', $inputs['discount_type']);
// TODO: this needs to be converted to a switch statement

View File

@@ -93,7 +93,7 @@ class Specific_employee extends Report
MAX(payment_type) AS payment_type,
MAX(comment) AS comment');
$builder->where('employee_id', $inputs['employee_id']); // TODO: Duplicated code
$builder->where('employee_id', $inputs['employee_id']);
switch ($inputs['sale_type']) {
case 'complete':
@@ -164,7 +164,7 @@ class Specific_employee extends Report
{
$builder = $this->db->table('sales_items_temp');
$builder->select('SUM(subtotal) AS subtotal, SUM(tax) AS tax, SUM(total) AS total, SUM(cost) AS cost, SUM(profit) AS profit');
$builder->where('employee_id', $inputs['employee_id']); // TODO: Duplicated code
$builder->where('employee_id', $inputs['employee_id']);
// TODO: this needs to be converted to a switch statement
if ($inputs['sale_type'] == 'complete') {

View File

@@ -77,7 +77,7 @@ class Specific_supplier extends Report
MAX(discount_type) AS discount_type,
MAX(discount) AS discount');
$builder->where('supplier_id', $inputs['supplier_id']); // TODO: Duplicated code
$builder->where('supplier_id', $inputs['supplier_id']);
switch ($inputs['sale_type']) {
case 'complete':

View File

@@ -2,14 +2,14 @@
namespace App\Models\Reports;
use App\Traits\Models\Reports\ReportDateFilter;
use Config\OSPOS;
class Summary_expenses_categories extends Summary_report
{
/**
* @return array[]
*/
protected function _get_data_columns(): array // TODO: Hungarian notation
use ReportDateFilter;
protected function _get_data_columns(): array
{
return [
['category_name' => lang('Reports.expenses_category')],
@@ -19,10 +19,6 @@ class Summary_expenses_categories extends Summary_report
];
}
/**
* @param array $inputs
* @return array
*/
public function getData(array $inputs): array
{
$config = config(OSPOS::class)->settings;
@@ -31,8 +27,7 @@ class Summary_expenses_categories extends Summary_report
$builder->select('expense_categories.category_name AS category_name, COUNT(expenses.expense_id) AS count, SUM(expenses.amount) AS total_amount, SUM(expenses.tax_amount) AS total_tax_amount');
$builder->join('expense_categories AS expense_categories', 'expense_categories.expense_category_id = expenses.expense_category_id', 'LEFT');
// TODO: convert this to ternary notation
if (empty($config['date_or_time_format'])) { // TODO: Duplicated code
if (empty($config['date_or_time_format'])) {
$builder->where('DATE(expenses.date) BETWEEN ' . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date']));
} else {
$builder->where('expenses.date BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date'])));
@@ -46,10 +41,6 @@ class Summary_expenses_categories extends Summary_report
return $builder->get()->getResultArray();
}
/**
* @param array $inputs
* @return array
*/
public function getSummaryData(array $inputs): array
{
$config = config(OSPOS::class)->settings;
@@ -57,7 +48,52 @@ class Summary_expenses_categories extends Summary_report
$builder = $this->db->table('expenses AS expenses');
$builder->select('SUM(expenses.amount) AS expenses_total_amount, SUM(expenses.tax_amount) AS expenses_total_tax_amount');
if (empty($config['date_or_time_format'])) { // TODO: Duplicated code
if (empty($config['date_or_time_format'])) {
$builder->where('DATE(expenses.date) BETWEEN ' . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date']));
} else {
$builder->where('expenses.date BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date']));
}
$builder->where('expenses.deleted', 0);
return $builder->get()->getRowArray();
}
}
/**
* @param array $inputs
* @return array
*/
public function getData(array $inputs): array
{
$config = config(OSPOS::class)->settings;
$builder = $this->db->table('expenses AS expenses');
$builder->select('expense_categories.category_name AS category_name, COUNT(expenses.expense_id) AS count, SUM(expenses.amount) AS total_amount, SUM(expenses.tax_amount) AS total_tax_amount');
$builder->join('expense_categories AS expense_categories', 'expense_categories.expense_category_id = expenses.expense_category_id', 'LEFT');
if (empty($config['date_or_time_format'])) {
$builder->where('DATE(expenses.date) BETWEEN ' . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date']));
} else {
$builder->where('expenses.date BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date'])));
}
$builder->where('expenses.deleted', 0);
$builder->groupBy('expense_categories.category_name');
$builder->orderBy('expense_categories.category_name');
return $builder->get()->getResultArray();
}
public function getSummaryData(array $inputs): array
{
$config = config(OSPOS::class)->settings;
$builder = $this->db->table('expenses AS expenses');
$builder->select('SUM(expenses.amount) AS expenses_total_amount, SUM(expenses.tax_amount) AS expenses_total_tax_amount');
if (empty($config['date_or_time_format'])) {
$builder->where('DATE(expenses.date) BETWEEN ' . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date']));
} else {
$builder->where('expenses.date BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date'])));

View File

@@ -2,14 +2,14 @@
namespace App\Models\Reports;
use App\Traits\Models\Reports\ReportDateFilter;
use Config\OSPOS;
class Summary_payments extends Summary_report
{
/**
* @return array[]
*/
protected function _get_data_columns(): array // TODO: Hungarian notation
use ReportDateFilter;
protected function _get_data_columns(): array
{
return [
['trans_group' => lang('Reports.trans_group')],
@@ -22,13 +22,9 @@ class Summary_payments extends Summary_report
];
}
/**
* @param array $inputs
* @return array
*/
public function getData(array $inputs): array
{
$cash_payment = lang('Sales.cash'); // TODO: This is never used. Should it be?
$cash_payment = lang('Sales.cash');
$config = config(OSPOS::class)->settings;
$separator[] = [
@@ -41,14 +37,7 @@ class Summary_payments extends Summary_report
'trans_due' => ''
];
$where = ''; // TODO: Duplicated code
// TODO: this needs to be converted to ternary notation
if (empty($config['date_or_time_format'])) {
$where .= 'DATE(sale_time) BETWEEN ' . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date']);
} else {
$where .= 'sale_time BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date']));
}
$where = $this->buildDateWhereClause($inputs);
$this->create_summary_payments_temp_tables($where);

View File

@@ -2,25 +2,20 @@
namespace App\Models\Reports;
use Config\OSPOS;
use App\Traits\Models\Reports\ReportDateFilter;
use App\Traits\Models\Reports\SaleTypeFilter;
use CodeIgniter\Database\BaseBuilder;
use Config\OSPOS;
abstract class Summary_report extends Report
{
/**
* Private interface implementing the core basic functionality for all reports
*/
private function __common_select(array $inputs, &$builder): void // TODO: Hungarian notation
use ReportDateFilter;
use SaleTypeFilter;
private function __common_select(array $inputs, &$builder): void
{
$config = config(OSPOS::class)->settings;
// TODO: convert to using QueryBuilder. Use App/Models/Reports/Summary_taxes.php getData() as a reference template
$where = ''; // TODO: Duplicated code
if (empty($config['date_or_time_format'])) {
$where .= 'DATE(sale_time) BETWEEN ' . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date']);
} else {
$where .= 'sale_time BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date']));
}
$where = $this->buildDateWhereClause($inputs);
$decimals = totals_decimals();
@@ -110,48 +105,16 @@ abstract class Summary_report extends Report
{
$config = config(OSPOS::class)->settings;
// TODO: Probably going to need to rework these since you can't reference $builder without it's instantiation.
if (empty($config['date_or_time_format'])) { // TODO: Duplicated code
$builder->where('DATE(sales.sale_time) BETWEEN ' . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date']));
} else {
$builder->where('sales.sale_time BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date'])));
}
$this->applyDateFilter($builder, $inputs);
if ($inputs['location_id'] != 'all') {
$builder->where('sales_items.item_location', $inputs['location_id']);
}
if ($inputs['sale_type'] == 'complete') {
$builder->where('sales.sale_status', COMPLETED);
$builder->groupStart();
$builder->where('sales.sale_type', SALE_TYPE_POS);
$builder->orWhere('sales.sale_type', SALE_TYPE_INVOICE);
$builder->orWhere('sales.sale_type', SALE_TYPE_RETURN);
$builder->groupEnd();
} elseif ($inputs['sale_type'] == 'sales') {
$builder->where('sales.sale_status', COMPLETED);
$builder->groupStart();
$builder->where('sales.sale_type', SALE_TYPE_POS);
$builder->orWhere('sales.sale_type', SALE_TYPE_INVOICE);
$builder->groupEnd();
} elseif ($inputs['sale_type'] == 'quotes') {
$builder->where('sales.sale_status', SUSPENDED);
$builder->where('sales.sale_type', SALE_TYPE_QUOTE);
} elseif ($inputs['sale_type'] == 'work_orders') {
$builder->where('sales.sale_status', SUSPENDED);
$builder->where('sales.sale_type', SALE_TYPE_WORK_ORDER);
} elseif ($inputs['sale_type'] == 'canceled') {
$builder->where('sales.sale_status', CANCELED);
} elseif ($inputs['sale_type'] == 'returns') {
$builder->where('sales.sale_status', COMPLETED);
$builder->where('sales.sale_type', SALE_TYPE_RETURN);
}
$this->applySaleTypeFilter($builder, $inputs['sale_type']);
}
/**
* Protected class interface implemented by derived classes where required
*/
abstract protected function _get_data_columns(): array; // TODO: hungarian notation
abstract protected function _get_data_columns(): array;
/**
* @param array $inputs

View File

@@ -2,10 +2,13 @@
namespace App\Models\Reports;
use App\Traits\Models\Reports\ReportDateFilter;
use Config\OSPOS;
class Summary_sales_taxes extends Summary_report
{
use ReportDateFilter;
private array $config;
public function __construct()
@@ -14,10 +17,7 @@ class Summary_sales_taxes extends Summary_report
$this->config = config(OSPOS::class)->settings;
}
/**
* @return array[]
*/
protected function _get_data_columns(): array // TODO: hungarian notation
protected function _get_data_columns(): array
{
return [
['reporting_authority' => lang('Reports.authority')],
@@ -28,35 +28,16 @@ class Summary_sales_taxes extends Summary_report
];
}
/**
* @param array $inputs
* @param object $builder
* @return void
*/
protected function _where(array $inputs, object &$builder): void // TODO: hungarian notation
protected function _where(array $inputs, object &$builder): void
{
$builder->where('sales.sale_status', COMPLETED);
if (empty($this->config['date_or_time_format'])) { // TODO: Duplicated code
$builder->where('DATE(sales.sale_time) BETWEEN ' . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date']));
} else {
$builder->where('sales.sale_time BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date'])));
}
$this->applyDateFilter($builder, $inputs, 'sales', 'sale_time');
}
/**
* @param array $inputs
* @return array
*/
public function getData(array $inputs): array
{
$builder = $this->db->table('sales_taxes');
if (empty($this->config['date_or_time_format'])) {
$builder->where('DATE(sale_time) BETWEEN ' . $inputs['start_date'] . ' AND ' . $inputs['end_date']);
} else {
$builder->where('sale_time BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date'])));
}
$this->applyDateFilter($builder, $inputs, 'sales_taxes', 'sale_time');
$builder->select('reporting_authority, jurisdiction_name, tax_category, tax_rate, SUM(sale_tax_amount) AS tax');
$builder->join('sales', 'sales_taxes.sale_id = sales.sale_id', 'left');

View File

@@ -185,6 +185,7 @@ class Stock_location extends Model
$builder = $this->db->table('stock_locations');
$builder->insert($location_data_to_save);
$location_id = $this->db->insertID();
$location_data['location_id'] = $location_id;
$this->_insert_new_permission('items', $location_id, $location_name); // TODO: need to refactor out the hungarian notation.
$this->_insert_new_permission('sales', $location_id, $location_name);

View File

@@ -0,0 +1,130 @@
<?php
namespace App\Traits\Controller;
use Config\OSPOS;
/**
* Shared trait for common controller functionality
*/
trait Shared
{
/**
* Build supplier info array for views
*
* @param object $supplier_info Supplier info object
* @param array $data Data array to populate
* @return void
*/
protected function buildSupplierInfo(object $supplier_info, array &$data): void
{
$data['supplier'] = $supplier_info->company_name;
$data['first_name'] = $supplier_info->first_name;
$data['last_name'] = $supplier_info->last_name;
$data['supplier_email'] = $supplier_info->email;
$data['supplier_address'] = $supplier_info->address_1;
if (!empty($supplier_info->zip) || !empty($supplier_info->city)) {
$data['supplier_location'] = $supplier_info->zip . ' ' . $supplier_info->city;
} else {
$data['supplier_location'] = '';
}
}
/**
* Get mode label and customer required text based on sale mode
*
* @param string $mode The sale mode
* @return array{mode_label: string, customer_required: string}
*/
protected function getSaleModeLabel(string $mode): array
{
return match ($mode) {
'sale_invoice' => [
'mode_label' => lang('Sales.invoice'),
'customer_required' => lang('Sales.customer_required')
],
'sale_quote' => [
'mode_label' => lang('Sales.quote'),
'customer_required' => lang('Sales.customer_required')
],
'sale_work_order' => [
'mode_label' => lang('Sales.work_order'),
'customer_required' => lang('Sales.customer_required')
],
'return' => [
'mode_label' => lang('Sales.return'),
'customer_required' => lang('Sales.customer_optional')
],
default => [
'mode_label' => lang('Sales.receipt'),
'customer_required' => lang('Sales.customer_optional')
]
};
}
/**
* Build company info string from config
*
* @return string
*/
protected function buildCompanyInfo(): string
{
$config = config(OSPOS::class)->settings;
$company_info = implode("\n", [$config['address'], $config['phone']]);
if (!empty($config['account_number'])) {
$company_info .= "\n" . lang('Sales.account_number') . ": " . $config['account_number'];
}
if (!empty($config['tax_id'])) {
$company_info .= "\n" . lang('Sales.tax_id') . ": " . $config['tax_id'];
}
return $company_info;
}
/**
* Initialize default tax code data for new entry
*
* @return array
*/
protected function initDefaultTaxCodeData(): array
{
return [
'tax_code' => '',
'tax_code_name' => '',
'tax_code_type' => '0',
'city' => '',
'state' => '',
'tax_rate' => '0.0000',
'rate_tax_code' => '',
'rate_tax_category_id' => 1,
'tax_category' => '',
'add_tax_category' => '',
'rounding_code' => '0'
];
}
/**
* Populate tax code data from existing tax code info
*
* @param object $tax_code_info Tax code info object
* @param object $tax_rate_info Tax rate info object
* @return array
*/
protected function buildTaxCodeData(object $tax_code_info, object $tax_rate_info): array
{
return [
'tax_code' => $tax_code_info->tax_code,
'tax_code_name' => $tax_code_info->tax_code_name,
'tax_code_type' => $tax_code_info->tax_code_type,
'city' => $tax_code_info->city,
'state' => $tax_code_info->state,
'rate_tax_code' => $tax_code_info->rate_tax_code,
'rate_tax_category_id' => $tax_code_info->rate_tax_category_id,
'tax_category' => $tax_code_info->tax_category,
'add_tax_category' => '',
'tax_rate' => $tax_rate_info->tax_rate,
'rounding_code' => $tax_rate_info->rounding_code
];
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Traits\Database;
trait SalesTaxMigration
{
protected function cleanIdentifier(string $string): string
{
$string = str_replace(' ', '-', $string);
return preg_replace('/[^A-Za-z0-9\-]/', '', $string);
}
protected function applyInvoiceTaxing(array &$salesTaxes): void
{
if (!empty($salesTaxes)) {
$sort = [];
foreach ($salesTaxes as $k => $v) {
$sort['print_sequence'][$k] = $v['print_sequence'];
}
array_multisort($sort['print_sequence'], SORT_ASC, $salesTaxes);
}
$decimals = totals_decimals();
foreach ($salesTaxes as $rowNumber => $salesTax) {
$salesTaxes[$rowNumber]['sale_tax_amount'] = $this->getSalesTaxForAmount(
$salesTax['sale_tax_basis'],
$salesTax['tax_rate'],
$salesTax['rounding_code'],
$decimals
);
}
}
protected function roundSalesTaxes(array &$salesTaxes, int $halfUp = 1, int $roundUp = 5, int $roundDown = 6, int $halfFive = 7): void
{
if (!empty($salesTaxes)) {
$sort = [];
foreach ($salesTaxes as $k => $v) {
$sort['print_sequence'][$k] = $v['print_sequence'];
}
array_multisort($sort['print_sequence'], SORT_ASC, $salesTaxes);
}
$decimals = totals_decimals();
foreach ($salesTaxes as $rowNumber => $salesTax) {
$saleTaxAmount = (float)$salesTax['sale_tax_amount'];
$roundingCode = $salesTax['rounding_code'];
$roundedSaleTaxAmount = $saleTaxAmount;
if (
$roundingCode == PHP_ROUND_HALF_UP
|| $roundingCode == PHP_ROUND_HALF_DOWN
|| $roundingCode == PHP_ROUND_HALF_EVEN
|| $roundingCode == PHP_ROUND_HALF_ODD
) {
$roundedSaleTaxAmount = round($saleTaxAmount, $decimals, $roundingCode);
} elseif ($roundingCode == $roundUp) {
$fig = (int) str_pad('1', $decimals, '0');
$roundedSaleTaxAmount = (ceil($saleTaxAmount * $fig) / $fig);
} elseif ($roundingCode == $roundDown) {
$fig = (int) str_pad('1', $decimals, '0');
$roundedSaleTaxAmount = (floor($saleTaxAmount * $fig) / $fig);
} elseif ($roundingCode == $halfFive) {
$roundedSaleTaxAmount = round($saleTaxAmount / 5) * 5;
}
$salesTaxes[$rowNumber]['sale_tax_amount'] = $roundedSaleTaxAmount;
}
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Traits\Models\Reports;
use CodeIgniter\Database\BaseBuilder;
use Config\OSPOS;
trait ReportDateFilter
{
protected function buildDateWhereClause(array $inputs, string $dateColumn = 'sale_time'): string
{
$config = config(OSPOS::class)->settings;
if (empty($config['date_or_time_format'])) {
return "DATE({$dateColumn}) BETWEEN " . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date']);
}
return "{$dateColumn} BETWEEN " . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date']));
}
protected function applyDateFilter(BaseBuilder $builder, array $inputs, string $tablePrefix = 'sales', string $column = 'sale_time'): void
{
$config = config(OSPOS::class)->settings;
if (empty($config['date_or_time_format'])) {
$builder->where("DATE({$tablePrefix}.{$column}) BETWEEN " . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date']));
} else {
$builder->where("{$tablePrefix}.{$column} BETWEEN " . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date'])));
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Traits\Models\Reports;
use CodeIgniter\Database\BaseBuilder;
trait SaleTypeFilter
{
protected function applySaleTypeFilter(BaseBuilder $builder, string $saleType, bool $usePrefix = true): void
{
$prefix = $usePrefix ? 'sales.' : '';
if ($saleType === 'complete') {
$builder->where("{$prefix}sale_status", COMPLETED);
$builder->groupStart();
$builder->where("{$prefix}sale_type", SALE_TYPE_POS);
$builder->orWhere("{$prefix}sale_type", SALE_TYPE_INVOICE);
$builder->orWhere("{$prefix}sale_type", SALE_TYPE_RETURN);
$builder->groupEnd();
} elseif ($saleType === 'sales') {
$builder->where("{$prefix}sale_status", COMPLETED);
$builder->groupStart();
$builder->where("{$prefix}sale_type", SALE_TYPE_POS);
$builder->orWhere("{$prefix}sale_type", SALE_TYPE_INVOICE);
$builder->groupEnd();
} elseif ($saleType === 'quotes') {
$builder->where("{$prefix}sale_status", SUSPENDED);
$builder->where("{$prefix}sale_type", SALE_TYPE_QUOTE);
} elseif ($saleType === 'work_orders') {
$builder->where("{$prefix}sale_status", SUSPENDED);
$builder->where("{$prefix}sale_type", SALE_TYPE_WORK_ORDER);
} elseif ($saleType === 'canceled') {
$builder->where("{$prefix}sale_status", CANCELED);
} elseif ($saleType === 'returns') {
$builder->where("{$prefix}sale_status", COMPLETED);
$builder->where("{$prefix}sale_type", SALE_TYPE_RETURN);
}
}
}

View File

@@ -11,34 +11,31 @@ $barcode_lib = new Barcode_lib();
<!doctype html>
<html lang="<?= current_language_code() ?>">
<head>
<meta charset="utf-8">
<title><?= lang('Items.generate_barcodes') ?></title>
<link rel="stylesheet" href="<?= base_url() ?>css/barcode_font.css">
<style>
.barcode svg {
height: <?= $barcode_config['barcode_height'] ?>px;
width: <?= $barcode_config['barcode_width'] ?>px;
}
</style>
</head>
<body class=<?= 'font_' . $barcode_lib->get_font_name($barcode_config['barcode_font']) ?> style="font-size: <?= $barcode_config['barcode_font_size'] ?>px;">
<table cellspacing="<?= $barcode_config['barcode_page_cellspacing'] ?>" width="<?= $barcode_config['barcode_page_width'] . '%' ?>">
<tr>
<?php
$count = 0;
foreach ($items as $item) {
if ($count % $barcode_config['barcode_num_in_row'] == 0 && $count != 0) {
echo '</tr><tr>';
}
echo '<td>' . $barcode_lib->display_barcode($item, $barcode_config) . '</td>';
$count++;
<head>
<meta charset="utf-8">
<title><?= lang('Items.generate_barcodes') ?></title>
<link rel="stylesheet" href="<?= base_url() ?>css/barcode_font.css">
<style>
.barcode svg {
height: <?= $barcode_config['barcode_height'] ?>px;
width: <?= $barcode_config['barcode_width'] ?>px;
}
?>
</tr>
</table>
</body>
</style>
</head>
<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;
foreach ($items as $item) {
if ($count % $barcode_config['barcode_num_in_row'] == 0 && $count != 0) {
echo '</tr><tr>';
}
echo '<td>' . $barcode_lib->display_barcode($item, $barcode_config) . '</td>';
$count++;
}
?>
</tr>
</table>
</body>
</html>

View File

@@ -47,7 +47,7 @@
<?= view('partial/print_receipt', ['print_after_sale' => false, 'selected_printer' => 'takings_printer']) ?>
<div id="title_bar" class="print_hide btn-toolbar">
<button onclick="javascript:printdoc()" class="btn btn-info btn-sm pull-right">
<button onclick="printdoc()" class="btn btn-info btn-sm pull-right">
<span class="glyphicon glyphicon-print">&nbsp;</span><?= lang('Common.print') ?>
</button>
<button class="btn btn-info btn-sm pull-right modal-dlg" data-btn-submit="<?= lang('Common.submit') ?>" data-href="<?= "$controller_name/view" ?>" title="<?= lang(ucfirst($controller_name) . ".new") ?>">

View File

@@ -3,8 +3,6 @@
* @var array $themes
* @var array $image_allowed_types
* @var array $selected_image_allowed_types
* @var array $exif_fields
* @var array $selected_exif_fields
* @var bool $show_office_group
* @var string $controller_name
* @var array $config
@@ -270,26 +268,6 @@
</div>
</div>
<div class="form-group form-group-sm">
<?= form_label(lang('Config.exif_fields_to_keep'), 'exif_fields_to_keep', ['class' => 'control-label col-xs-2']) ?>
<div class="col-xs-4">
<?= form_multiselect([
'name' => 'exif_fields_to_keep[]',
'options' => $exif_fields,
'selected' => $selected_exif_fields,
'id' => 'exif_fields_to_keep',
'class' => 'selectpicker show-menu-arrow',
'data-none-selected-text' => lang('Common.none_selected_text'),
'data-selected-text-format' => 'count > 1',
'data-style' => 'btn-default btn-sm',
'data-width' => '100%'
]) ?>
<label class="control-label">
<span class="glyphicon glyphicon-info-sign" data-toggle="tooltip" data-placement="right" title="<?= lang('Config.exif_fields_to_keep_tooltip') ?>"></span>
</label>
</div>
</div>
<div class="form-group form-group-sm">
<?= form_label(lang('Config.gcaptcha_enable'), 'gcaptcha_enable', ['class' => 'control-label col-xs-2']) ?>
<div class="col-xs-1">

View File

@@ -25,11 +25,9 @@ use Config\OSPOS;
<div class="container">
<div class="row">
<div class="col-sm-2" style="text-align: left;"><br>
<strong>
<p style="min-height: 14.7em;">General Info</p>
<p style="min-height: 10.5em;">User Setup</p><br>
<p>Permissions</p>
</strong>
<p style="min-height: 14.7em; font-weight: bold;">General Info</p>
<p style="min-height: 10.5em; font-weight: bold;">User Setup</p><br>
<p style="font-weight: bold;">Permissions</p>
</div>
<div class="col-sm-8" id="issuetemplate" style="text-align: left;"><br>
<?= lang('Config.ospos_info') . ':' ?>
@@ -38,14 +36,14 @@ use Config\OSPOS;
<div id="TimeError"></div>
Extensions & Modules:<br>
<?php
echo "&#187; GD: ", extension_loaded('gd') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red;">Disabled &#x2717</span>', '<br>';
echo "&#187; BC Math: ", extension_loaded('bcmath') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red">Disabled &#x2717</span>', '<br>';
echo "&#187; INTL: ", extension_loaded('intl') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red">Disabled &#x2717</span>', '<br>';
echo "&#187; OpenSSL: ", extension_loaded('openssl') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red">Disabled &#x2717</span>', '<br>';
echo "&#187; MBString: ", extension_loaded('mbstring') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red">Disabled &#x2717</span>', '<br>';
echo "&#187; Curl: ", extension_loaded('curl') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red">Disabled &#x2717</span>', '<br>';
echo "&#187; Json: ", extension_loaded('json') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red">Disabled &#x2717</span>', '<br><br>';
echo "&#187; Xml: ", extension_loaded('xml') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red">Disabled &#x2717</span>', '<br><br>';
echo "&#187; GD: ", extension_loaded('gd') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red;">Disabled &#x2717</span>', '<br>';
echo "&#187; BC Math: ", extension_loaded('bcmath') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red;">Disabled &#x2717</span>', '<br>';
echo "&#187; INTL: ", extension_loaded('intl') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red;">Disabled &#x2717</span>', '<br>';
echo "&#187; OpenSSL: ", extension_loaded('openssl') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red;">Disabled &#x2717</span>', '<br>';
echo "&#187; MBString: ", extension_loaded('mbstring') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red;">Disabled &#x2717</span>', '<br>';
echo "&#187; Curl: ", extension_loaded('curl') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red;">Disabled &#x2717</span>', '<br>';
echo "&#187; Json: ", extension_loaded('json') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red;">Disabled &#x2717</span>', '<br><br>';
echo "&#187; Xml: ", extension_loaded('xml') ? '<span style="color: green;">Enabled &#x2713</span>' : '<span style="color: red;">Disabled &#x2717</span>', '<br><br>';
?>
User Configuration:<br>
.Browser:

View File

@@ -57,7 +57,7 @@ if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE) {
$args = implode(', ', array_map(static fn ($value): string => match (true) {
is_object($value) => 'Object(' . $value::class . ')',
is_array($value) => $value !== [] ? '[...]' : '[]',
$value === null => 'null', // Return the lowercased version
$value === null => 'null', // return the lowercased version
default => var_export($value, true),
}, array_values($error['args'] ?? [])));

View File

@@ -3,7 +3,7 @@
--main-text-color: #555;
--dark-text-color: #222;
--light-text-color: #c7c7c7;
--brand-primary-color: #E06E3F;
--brand-primary-color: #DC4814;
--light-bg-color: #ededee;
--dark-bg-color: #404040;
}
@@ -71,7 +71,7 @@ p.lead {
text-align: center;
padding: calc(4px + 0.2083vw);
width: 100%;
margin-top: -2.14rem;
top: 0;
position: fixed;
}
@@ -101,11 +101,9 @@ p.lead {
}
.tabs {
list-style: none;
list-style-position: inside;
margin: 0;
list-style: none inside none;
padding: 0;
margin-bottom: -1px;
margin: 0 0 -1px;
}
.tabs li {
display: inline;

View File

@@ -1,6 +1,5 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><?= lang('Errors.badRequest') ?></title>
@@ -16,7 +15,6 @@
left: 50%;
margin-left: -73px;
}
body {
height: 100%;
background: #fafafa;
@@ -24,7 +22,6 @@
color: #777;
font-weight: 300;
}
h1 {
font-weight: lighter;
letter-spacing: normal;
@@ -33,7 +30,6 @@
margin-bottom: 0;
color: #222;
}
.wrap {
max-width: 1024px;
margin: 5rem auto;
@@ -44,12 +40,10 @@
border-radius: 0.5rem;
position: relative;
}
pre {
white-space: normal;
margin-top: 1.5rem;
}
code {
background: #fafafa;
border: 1px solid #efefef;
@@ -57,11 +51,9 @@
border-radius: 5px;
display: block;
}
p {
margin-top: 1.5rem;
}
.footer {
margin-top: 2rem;
border-top: 1px solid #efefef;
@@ -69,7 +61,6 @@
font-size: 85%;
color: #999;
}
a:active,
a:link,
a:visited {
@@ -77,19 +68,17 @@
}
</style>
</head>
<body>
<div class="wrap">
<h1>400</h1>
<div class="wrap">
<h1>400</h1>
<p>
<?php if (ENVIRONMENT !== 'production') : ?>
<?= nl2br(esc($message)) ?>
<?php else : ?>
<?= lang('Errors.sorryBadRequest') ?>
<?php endif; ?>
</p>
</div>
<p>
<?php if (ENVIRONMENT !== 'production') : ?>
<?= nl2br(esc($message)) ?>
<?php else : ?>
<?= lang('Errors.sorryBadRequest') ?>
<?php endif; ?>
</p>
</div>
</body>
</html>

View File

@@ -1,10 +1,4 @@
<?php
/**
* @var string $message
*/
?>
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">

View File

@@ -14,421 +14,424 @@ $errorId = uniqid('error', true);
?>
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex">
<title><?= esc($title) ?></title>
<style>
<?= preg_replace('#[\r\n\t ]+#', ' ', file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'debug.css')) ?>
</style>
<title><?= esc($title) ?></title>
<style>
<?= preg_replace('#[\r\n\t ]+#', ' ', file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'debug.css')) ?>
</style>
<script type="text/javascript">
<?= file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'debug.js') ?>
</script>
</head>
<body onload="init()">
<script type="text/javascript">
<?= file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'debug.js') ?>
</script>
</head>
<body onload="init()">
<!-- Header -->
<div class="header">
<div class="environment">
Displayed at <?= esc(date('H:i:sa')) ?> &mdash;
PHP: <?= esc(PHP_VERSION) ?> &mdash;
CodeIgniter: <?= esc(CodeIgniter::CI_VERSION) ?> --
Environment: <?= ENVIRONMENT ?>
<!-- Header -->
<div class="header">
<div class="environment">
Displayed at <?= esc(date('H:i:s')) ?> &mdash;
PHP: <?= esc(PHP_VERSION) ?> &mdash;
CodeIgniter: <?= esc(CodeIgniter::CI_VERSION) ?> --
Environment: <?= ENVIRONMENT ?>
</div>
<div class="container">
<h1><?= esc($title), esc($exception->getCode() ? ' #' . $exception->getCode() : '') ?></h1>
<p>
<?= nl2br(esc($exception->getMessage())) ?>
<a href="https://www.duckduckgo.com/?q=<?= urlencode($title . ' ' . preg_replace('#\'.*\'|".*"#Us', '', $exception->getMessage())) ?>"
rel="noreferrer" target="_blank">search &rarr;</a>
</p>
</div>
</div>
<!-- Source -->
<div class="container">
<h1><?= esc($title), esc($exception->getCode() ? ' #' . $exception->getCode() : '') ?></h1>
<p>
<?= nl2br(esc($exception->getMessage())) ?>
<a href="https://www.duckduckgo.com/?q=<?= urlencode($title . ' ' . preg_replace('#\'.*\'|".*"#Us', '', $exception->getMessage())) ?>"
rel="noreferrer" target="_blank">search &rarr;</a>
</p>
<p><b><?= esc(clean_path($file)) ?></b> at line <b><?= esc($line) ?></b></p>
<?php if (is_file($file)) : ?>
<div class="source">
<?= static::highlightFile($file, $line, 15); ?>
</div>
<?php endif; ?>
</div>
</div>
<!-- Source -->
<div class="container">
<p><b><?= esc(clean_path($file)) ?></b> at line <b><?= esc($line) ?></b></p>
<?php if (is_file($file)) : ?>
<div class="source">
<?= static::highlightFile($file, $line, 15); ?>
</div>
<?php endif; ?>
</div>
<div class="container">
<?php
$last = $exception;
while ($prevException = $last->getPrevious()) {
$last = $prevException;
?>
<pre>
Caused by:
<?= esc($prevException::class), esc($prevException->getCode() ? ' #' . $prevException->getCode() : '') ?>
<?= nl2br(esc($prevException->getMessage())) ?>
<a href="https://www.duckduckgo.com/?q=<?= urlencode($prevException::class . ' ' . preg_replace('#\'.*\'|".*"#Us', '', $prevException->getMessage())) ?>" rel="noreferrer" target="_blank">search &rarr;</a>
<?= esc(clean_path($prevException->getFile()) . ':' . $prevException->getLine()) ?>
</pre>
<?php } ?>
</div>
<?php if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE) : ?>
<div class="container">
<ul class="tabs" id="tabs">
<li><a href="#backtrace">Backtrace</a></li>
<li><a href="#server">Server</a></li>
<li><a href="#request">Request</a></li>
<li><a href="#response">Response</a></li>
<li><a href="#files">Files</a></li>
<li><a href="#memory">Memory</a></li>
</ul>
<div class="tab-content">
<!-- Backtrace -->
<div class="content" id="backtrace">
<ol class="trace">
<?php foreach ($trace as $index => $row) : ?>
<li>
<p>
<!-- Trace info -->
<?php if (isset($row['file']) && is_file($row['file'])) : ?>
<?php
if (isset($row['function']) && in_array($row['function'], ['include', 'include_once', 'require', 'require_once'], true)) {
echo esc($row['function'] . ' ' . clean_path($row['file']));
} else {
echo esc(clean_path($row['file']) . ' : ' . $row['line']);
}
?>
<?php else: ?>
{PHP internal code}
<?php endif; ?>
<!-- Class/Method -->
<?php if (isset($row['class'])) : ?>
&nbsp;&nbsp;&mdash;&nbsp;&nbsp;<?= esc($row['class'] . $row['type'] . $row['function']) ?>
<?php if (! empty($row['args'])) : ?>
<?php $argsId = $errorId . 'args' . $index ?>
( <a href="#" onclick="return toggle('<?= esc($argsId, 'attr') ?>');">arguments</a> )
<div class="args" id="<?= esc($argsId, 'attr') ?>">
<table cellspacing="0">
<?php
$params = null;
// Reflection by name is not available for closure function
if (! str_ends_with($row['function'], '}')) {
$mirror = isset($row['class']) ? new ReflectionMethod($row['class'], $row['function']) : new ReflectionFunction($row['function']);
$params = $mirror->getParameters();
}
foreach ($row['args'] as $key => $value) : ?>
<tr>
<td><code><?= esc(isset($params[$key]) ? '$' . $params[$key]->name : "#{$key}") ?></code></td>
<td><pre><?= esc(print_r($value, true)) ?></pre></td>
</tr>
<?php endforeach ?>
</table>
</div>
<?php else : ?>
()
<?php endif; ?>
<?php endif; ?>
<?php if (! isset($row['class']) && isset($row['function'])) : ?>
&nbsp;&nbsp;&mdash;&nbsp;&nbsp; <?= esc($row['function']) ?>()
<?php endif; ?>
</p>
<!-- Source? -->
<?php if (isset($row['file']) && is_file($row['file']) && isset($row['class'])) : ?>
<div class="source">
<?= static::highlightFile($row['file'], $row['line']) ?>
</div>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ol>
</div>
<!-- Server -->
<div class="content" id="server">
<?php foreach (['_SERVER', '_SESSION'] as $var) : ?>
<?php
if (empty($GLOBALS[$var]) || ! is_array($GLOBALS[$var])) {
continue;
} ?>
<h3>$<?= esc($var) ?></h3>
<table>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($GLOBALS[$var] as $key => $value) : ?>
<tr>
<td><?= esc($key) ?></td>
<td>
<?php if (is_string($value)) : ?>
<?= esc($value) ?>
<?php else: ?>
<pre><?= esc(print_r($value, true)) ?></pre>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endforeach ?>
<!-- Constants -->
<?php $constants = get_defined_constants(true); ?>
<?php if (! empty($constants['user'])) : ?>
<h3>Constants</h3>
<table>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($constants['user'] as $key => $value) : ?>
<tr>
<td><?= esc($key) ?></td>
<td>
<?php if (is_string($value)) : ?>
<?= esc($value) ?>
<?php else: ?>
<pre><?= esc(print_r($value, true)) ?></pre>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Request -->
<div class="content" id="request">
<?php $request = service('request'); ?>
<table>
<tbody>
<tr>
<td style="width: 10em">Path</td>
<td><?= esc($request->getUri()) ?></td>
</tr>
<tr>
<td>HTTP Method</td>
<td><?= esc($request->getMethod()) ?></td>
</tr>
<tr>
<td>IP Address</td>
<td><?= esc($request->getIPAddress()) ?></td>
</tr>
<tr>
<td style="width: 10em">Is AJAX Request?</td>
<td><?= $request->isAJAX() ? 'yes' : 'no' ?></td>
</tr>
<tr>
<td>Is CLI Request?</td>
<td><?= $request->isCLI() ? 'yes' : 'no' ?></td>
</tr>
<tr>
<td>Is Secure Request?</td>
<td><?= $request->isSecure() ? 'yes' : 'no' ?></td>
</tr>
<tr>
<td>User Agent</td>
<td><?= esc($request->getUserAgent()->getAgentString()) ?></td>
</tr>
</tbody>
</table>
<?php $empty = true; ?>
<?php foreach (['_GET', '_POST', '_COOKIE'] as $var) : ?>
<?php
if (empty($GLOBALS[$var]) || ! is_array($GLOBALS[$var])) {
continue;
} ?>
<?php $empty = false; ?>
<h3>$<?= esc($var) ?></h3>
<table style="width: 100%">
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($GLOBALS[$var] as $key => $value) : ?>
<tr>
<td><?= esc($key) ?></td>
<td>
<?php if (is_string($value)) : ?>
<?= esc($value) ?>
<?php else: ?>
<pre><?= esc(print_r($value, true)) ?></pre>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endforeach ?>
<?php if ($empty) : ?>
<div class="alert">
No $_GET, $_POST, or $_COOKIE Information to show.
</div>
<?php endif; ?>
<?php $headers = $request->headers(); ?>
<?php if (! empty($headers)) : ?>
<h3>Headers</h3>
<table>
<thead>
<tr>
<th>Header</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($headers as $name => $value) : ?>
<tr>
<td><?= esc($name, 'html') ?></td>
<td>
<?php
if ($value instanceof Header) {
echo esc($value->getValueLine(), 'html');
} else {
foreach ($value as $i => $header) {
echo ' ('. $i+1 . ') ' . esc($header->getValueLine(), 'html');
}
}
?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Response -->
<div class="container">
<?php
$response = service('response');
$response->setStatusCode(http_response_code());
?>
<div class="content" id="response">
<table>
<tr>
<td style="width: 15em">Response Status</td>
<td><?= esc($response->getStatusCode() . ' - ' . $response->getReasonPhrase()) ?></td>
</tr>
</table>
$last = $exception;
<?php $headers = $response->headers(); ?>
<?php if (! empty($headers)) : ?>
<h3>Headers</h3>
while ($prevException = $last->getPrevious()) {
$last = $prevException;
?>
<pre>
Caused by:
<?= esc($prevException::class), esc($prevException->getCode() ? ' #' . $prevException->getCode() : '') ?>
<?= nl2br(esc($prevException->getMessage())) ?>
<a href="https://www.duckduckgo.com/?q=<?= urlencode($prevException::class . ' ' . preg_replace('#\'.*\'|".*"#Us', '', $prevException->getMessage())) ?>"
rel="noreferrer" target="_blank">search &rarr;</a>
<?= esc(clean_path($prevException->getFile()) . ':' . $prevException->getLine()) ?>
</pre>
<?php
}
?>
</div>
<?php if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE) : ?>
<div class="container">
<ul class="tabs" id="tabs">
<li><a href="#backtrace">Backtrace</a></li>
<li><a href="#server">Server</a></li>
<li><a href="#request">Request</a></li>
<li><a href="#response">Response</a></li>
<li><a href="#files">Files</a></li>
<li><a href="#memory">Memory</a></li>
</ul>
<div class="tab-content">
<!-- Backtrace -->
<div class="content" id="backtrace">
<ol class="trace">
<?php foreach ($trace as $index => $row) : ?>
<li>
<p>
<!-- Trace info -->
<?php if (isset($row['file']) && is_file($row['file'])) : ?>
<?php
if (isset($row['function']) && in_array($row['function'], ['include', 'include_once', 'require', 'require_once'], true)) {
echo esc($row['function'] . ' ' . clean_path($row['file']));
} else {
echo esc(clean_path($row['file']) . ' : ' . $row['line']);
}
?>
<?php else: ?>
{PHP internal code}
<?php endif; ?>
<!-- Class/Method -->
<?php if (isset($row['class'])) : ?>
&nbsp;&nbsp;&mdash;&nbsp;&nbsp;<?= esc($row['class'] . $row['type'] . $row['function']) ?>
<?php if (! empty($row['args'])) : ?>
<?php $argsId = $errorId . 'args' . $index ?>
( <a href="#" onclick="return toggle('<?= esc($argsId, 'attr') ?>');">arguments</a> )
<div class="args" id="<?= esc($argsId, 'attr') ?>">
<table style="border-spacing: 0;">
<?php
$params = null;
// Reflection by name is not available for closure function
if (! str_ends_with($row['function'], '}')) {
$mirror = isset($row['class']) ? new ReflectionMethod($row['class'], $row['function']) : new ReflectionFunction($row['function']);
$params = $mirror->getParameters();
}
foreach ($row['args'] as $key => $value) : ?>
<tr>
<td><code><?= esc(isset($params[$key]) ? '$' . $params[$key]->name : "#{$key}") ?></code></td>
<td><pre><?= esc(print_r($value, true)) ?></pre></td>
</tr>
<?php endforeach ?>
</table>
</div>
<?php else : ?>
()
<?php endif; ?>
<?php endif; ?>
<?php if (! isset($row['class']) && isset($row['function'])) : ?>
&nbsp;&nbsp;&mdash;&nbsp;&nbsp; <?= esc($row['function']) ?>()
<?php endif; ?>
</p>
<!-- Source? -->
<?php if (isset($row['file']) && is_file($row['file']) && isset($row['class'])) : ?>
<div class="source">
<?= static::highlightFile($row['file'], $row['line']) ?>
</div>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ol>
</div>
<!-- Server -->
<div class="content" id="server">
<?php foreach (['_SERVER', '_SESSION'] as $var) : ?>
<?php
if (empty($GLOBALS[$var]) || ! is_array($GLOBALS[$var])) {
continue;
} ?>
<h3>$<?= esc($var) ?></h3>
<table>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($GLOBALS[$var] as $key => $value) : ?>
<tr>
<td><?= esc($key) ?></td>
<td>
<?php if (is_string($value)) : ?>
<?= esc($value) ?>
<?php else: ?>
<pre><?= esc(print_r($value, true)) ?></pre>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endforeach ?>
<!-- Constants -->
<?php $constants = get_defined_constants(true); ?>
<?php if (! empty($constants['user'])) : ?>
<h3>Constants</h3>
<table>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($constants['user'] as $key => $value) : ?>
<tr>
<td><?= esc($key) ?></td>
<td>
<?php if (is_string($value)) : ?>
<?= esc($value) ?>
<?php else: ?>
<pre><?= esc(print_r($value, true)) ?></pre>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Request -->
<div class="content" id="request">
<?php $request = service('request'); ?>
<table>
<thead>
<tr>
<th>Header</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($headers as $name => $value) : ?>
<tr>
<td><?= esc($name, 'html') ?></td>
<td>
<?php
if ($value instanceof Header) {
echo esc($response->getHeaderLine($name), 'html');
} else {
foreach ($value as $i => $header) {
echo ' ('. $i+1 . ') ' . esc($header->getValueLine(), 'html');
}
}
?>
</td>
<td style="width: 10em">Path</td>
<td><?= esc($request->getUri()) ?></td>
</tr>
<?php endforeach; ?>
<tr>
<td>HTTP Method</td>
<td><?= esc($request->getMethod()) ?></td>
</tr>
<tr>
<td>IP Address</td>
<td><?= esc($request->getIPAddress()) ?></td>
</tr>
<tr>
<td style="width: 10em">Is AJAX Request?</td>
<td><?= $request->isAJAX() ? 'yes' : 'no' ?></td>
</tr>
<tr>
<td>Is CLI Request?</td>
<td><?= $request->isCLI() ? 'yes' : 'no' ?></td>
</tr>
<tr>
<td>Is Secure Request?</td>
<td><?= $request->isSecure() ? 'yes' : 'no' ?></td>
</tr>
<tr>
<td>User Agent</td>
<td><?= esc($request->getUserAgent()->getAgentString()) ?></td>
</tr>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Files -->
<div class="content" id="files">
<?php $files = get_included_files(); ?>
<?php $empty = true; ?>
<?php foreach (['_GET', '_POST', '_COOKIE'] as $var) : ?>
<?php
if (empty($GLOBALS[$var]) || ! is_array($GLOBALS[$var])) {
continue;
} ?>
<ol>
<?php foreach ($files as $file) :?>
<li><?= esc(clean_path($file)) ?></li>
<?php endforeach ?>
</ol>
</div>
<?php $empty = false; ?>
<!-- Memory -->
<div class="content" id="memory">
<h3>$<?= esc($var) ?></h3>
<table>
<tbody>
<table style="width: 100%">
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($GLOBALS[$var] as $key => $value) : ?>
<tr>
<td><?= esc($key) ?></td>
<td>
<?php if (is_string($value)) : ?>
<?= esc($value) ?>
<?php else: ?>
<pre><?= esc(print_r($value, true)) ?></pre>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endforeach ?>
<?php if ($empty) : ?>
<div class="alert">
No $_GET, $_POST, or $_COOKIE Information to show.
</div>
<?php endif; ?>
<?php $headers = $request->headers(); ?>
<?php if (! empty($headers)) : ?>
<h3>Headers</h3>
<table>
<thead>
<tr>
<th>Header</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($headers as $name => $value) : ?>
<tr>
<td><?= esc($name, 'html') ?></td>
<td>
<?php
if ($value instanceof Header) {
echo esc($value->getValueLine(), 'html');
} else {
foreach ($value as $i => $header) {
echo ' (' . ($i + 1) . ') ' . esc($header->getValueLine(), 'html');
}
}
?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<!-- Response -->
<?php
$response = service('response');
$response->setStatusCode(http_response_code());
?>
<div class="content" id="response">
<table>
<tr>
<td>Memory Usage</td>
<td><?= esc(static::describeMemory(memory_get_usage(true))) ?></td>
<td style="width: 15em">Response Status</td>
<td><?= esc($response->getStatusCode() . ' - ' . $response->getReasonPhrase()) ?></td>
</tr>
<tr>
<td style="width: 12em">Peak Memory Usage:</td>
<td><?= esc(static::describeMemory(memory_get_peak_usage(true))) ?></td>
</tr>
<tr>
<td>Memory Limit:</td>
<td><?= esc(ini_get('memory_limit')) ?></td>
</tr>
</tbody>
</table>
</table>
</div>
<?php $headers = $response->headers(); ?>
<?php if (! empty($headers)) : ?>
<h3>Headers</h3>
</div> <!-- /tab-content -->
<table>
<thead>
<tr>
<th>Header</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<?php foreach ($headers as $name => $value) : ?>
<tr>
<td><?= esc($name, 'html') ?></td>
<td>
<?php
if ($value instanceof Header) {
echo esc($response->getHeaderLine($name), 'html');
} else {
foreach ($value as $i => $header) {
echo ' (' . ($i + 1) . ') ' . esc($header->getValueLine(), 'html');
}
}
?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div> <!-- /container -->
<?php endif; ?>
<?php endif; ?>
</div>
</body>
<!-- Files -->
<div class="content" id="files">
<?php $files = get_included_files(); ?>
<ol>
<?php foreach ($files as $file) :?>
<li><?= esc(clean_path($file)) ?></li>
<?php endforeach ?>
</ol>
</div>
<!-- Memory -->
<div class="content" id="memory">
<table>
<tbody>
<tr>
<td>Memory Usage</td>
<td><?= esc(static::describeMemory(memory_get_usage(true))) ?></td>
</tr>
<tr>
<td style="width: 12em">Peak Memory Usage:</td>
<td><?= esc(static::describeMemory(memory_get_peak_usage(true))) ?></td>
</tr>
<tr>
<td>Memory Limit:</td>
<td><?= esc(ini_get('memory_limit')) ?></td>
</tr>
</tbody>
</table>
</div>
</div> <!-- /tab-content -->
</div> <!-- /container -->
<?php endif; ?>
</body>
</html>

View File

@@ -49,13 +49,13 @@
});
});
</script
<?= view('partial/table_filter_persistence') ?>>
</script>
<?= view('partial/table_filter_persistence') ?>
<?= view('partial/print_receipt', ['print_after_sale' => false, 'selected_printer' => 'takings_printer']) ?>
<div id="title_bar" class="print_hide btn-toolbar">
<button onclick="javascript:printdoc()" class="btn btn-info btn-sm pull-right">
<button onclick="printdoc()" class="btn btn-info btn-sm pull-right">
<span class="glyphicon glyphicon-print">&nbsp;</span><?= lang('Common.print') ?>
</button>
<button class="btn btn-info btn-sm pull-right modal-dlg" data-btn-submit="<?= lang('Common.submit') ?>" data-href="<?= "$controller_name/view" ?>" title="<?= lang(ucfirst($controller_name) . '.new') ?>">

View File

@@ -4,21 +4,20 @@ use Config\OSPOS;
?>
</div>
</div>
</div>
<div id="footer">
<div class="jumbotron push-spaces">
<strong>
<?= lang('Common.copyrights', [date('Y')]) ?> ·
<a href="https://opensourcepos.org" target="_blank"><?= lang('Common.website') ?></a> ·
<?= esc(config('App')->application_version) ?> -
<a target="_blank" href="https://github.com/opensourcepos/opensourcepos/commit/<?= esc(config(OSPOS::class)->commit_sha1) ?>">
<?= esc(substr(config(OSPOS::class)->commit_sha1, 0, 6)); ?>
</a>
</strong>.
<div id="footer">
<div class="jumbotron push-spaces">
<strong>
<?= lang('Common.copyrights', [date('Y')]) ?> ·
<a href="https://opensourcepos.org" target="_blank"><?= lang('Common.website') ?></a> ·
<?= esc(config('App')->application_version) ?> -
<a target="_blank" href="https://github.com/opensourcepos/opensourcepos/commit/<?= esc(config(OSPOS::class)->commit_sha1) ?>">
<?= esc(substr(config(OSPOS::class)->commit_sha1, 0, 6)); ?>
</a>
</strong>.
</div>
</div>
</div>
</body>
</body>
</html>

View File

@@ -1,12 +1,12 @@
<?php
/**
* Table Filter Persistence
*
* This partial updates the URL when filters change, allowing users to
*
* This partially updates the URL when filters change, allowing users to
* share/bookmark filtered views and maintain state on back navigation.
*
*
* Filter restoration from URL is handled server-side in the controller.
*
*
* @param array $options Additional filter options
* - 'additional_params': Array of additional parameter names to track (e.g., ['stock_location'])
* - 'filter_select_id': Filter multiselect element ID (default: 'filters')
@@ -20,10 +20,10 @@ $filter_select_id = $options['filter_select_id'] ?? 'filters';
$(document).ready(function() {
var additional_params = <?= json_encode($additional_params) ?>;
var filter_select_id = '<?= esc($filter_select_id) ?>';
function update_url() {
var params = new URLSearchParams();
// Add dates
if (typeof start_date !== 'undefined') {
params.set('start_date', start_date);
@@ -31,7 +31,7 @@ $filter_select_id = $options['filter_select_id'] ?? 'filters';
if (typeof end_date !== 'undefined') {
params.set('end_date', end_date);
}
// Add filters
var filters = $('#' + filter_select_id).val();
if (filters) {
@@ -39,7 +39,7 @@ $filter_select_id = $options['filter_select_id'] ?? 'filters';
params.append('filters[]', filter);
});
}
// Add additional params
additional_params.forEach(function(param) {
var element = $('#' + param);
@@ -54,7 +54,7 @@ $filter_select_id = $options['filter_select_id'] ?? 'filters';
}
}
});
// Update URL without page reload
var new_url = window.location.pathname;
var params_str = params.toString();
@@ -63,22 +63,22 @@ $filter_select_id = $options['filter_select_id'] ?? 'filters';
}
window.history.replaceState({}, '', new_url);
}
// Update URL when filter dropdown changes
$('#' + filter_select_id).on('hidden.bs.select', function(e) {
update_url();
});
// Update URL when stock location changes (if exists)
if ($('#stock_location').length) {
$("#stock_location").change(function() {
update_url();
});
}
// Update URL when daterangepicker changes
$("#daterangepicker").on('apply.daterangepicker', function(ev, picker) {
update_url();
});
});
</script>
</script>

View File

@@ -47,24 +47,26 @@
offset: 60,
// The label interpolation function enables you to modify the values
// used for the labels on each axis.
labelInterpolationFnc: function(value) {
<?php
<?php
$currency_symbol = esc($config['currency_symbol'], 'js');
$currency_prefix = '';
$currency_suffix = '';
if ($show_currency) {
if (is_right_side_currency_symbol()) {
?>
return value + '<?= esc($config['currency_symbol'], 'js') ?>';
<?php } else { ?>
return '<?= esc($config['currency_symbol'], 'js') ?>' + value;
<?php
$currency_suffix = $currency_symbol;
} else {
$currency_prefix = $currency_symbol;
}
} else {
?>
return value;
<?php } ?>
}
?>
labelInterpolationFnc: function(value) {
return '<?= $currency_prefix ?>' + value + '<?= $currency_suffix ?>';
}
},
// Plugins configuration
// Plugin configuration
plugins: [
Chartist.plugins.ctAxisTitle({
axisX: {

View File

@@ -44,30 +44,32 @@
position: 'end',
// The label interpolation function enables you to modify the values
// used for the labels on each axis.
labelInterpolationFnc: function(value) {
<?php
<?php
$currency_symbol = esc($config['currency_symbol'], 'js');
$currency_prefix = '';
$currency_suffix = '';
if ($show_currency) {
if (is_right_side_currency_symbol()) {
?>
return value + '<?= esc($config['currency_symbol'], 'js') ?>';
<?php } else { ?>
return '<?= esc($config['currency_symbol'], 'js') ?>' + value;
<?php
$currency_suffix = $currency_symbol;
} else {
$currency_prefix = $currency_symbol;
}
} else {
?>
return value;
<?php } ?>
}
?>
labelInterpolationFnc: function(value) {
return '<?= $currency_prefix ?>' + value + '<?= $currency_suffix ?>';
}
},
// Y-Axis specific configuration
axisY: {
// Lets offset the chart a bit from the labels
// Let's offset the chart a bit from the labels
offset: 120
},
// Plugins configuration
// Plugin configuration
plugins: [
Chartist.plugins.ctAxisTitle({
axisX: {

View File

@@ -63,24 +63,26 @@
},
// The label interpolation function enables you to modify the values
// used for the labels on each axis.
labelInterpolationFnc: function(value) {
<?php
<?php
$currency_symbol = esc($config['currency_symbol'], 'js');
$currency_prefix = '';
$currency_suffix = '';
if ($show_currency) {
if (is_right_side_currency_symbol()) {
?>
return value + '<?= esc($config['currency_symbol'], 'js') ?>';
<?php } else { ?>
return '<?= esc($config['currency_symbol'], 'js') ?>' + value;
<?php
$currency_suffix = $currency_symbol;
} else {
$currency_prefix = $currency_symbol;
}
} else {
?>
return value;
<?php } ?>
}
?>
labelInterpolationFnc: function(value) {
return '<?= $currency_prefix ?>' + value + '<?= $currency_suffix ?>';
}
},
// Plugins configuration
// Plugin configuration
plugins: [
Chartist.plugins.ctAxisTitle({
axisX: {
@@ -104,41 +106,45 @@
}
}),
<?php
$currency_symbol = esc($config['currency_symbol'], 'js');
$currency_prefix = '';
$currency_suffix = '';
if ($show_currency) {
if (is_right_side_currency_symbol()) {
$currency_suffix = $currency_symbol;
} else {
$currency_prefix = $currency_symbol;
}
}
?>
Chartist.plugins.ctPointLabels({
textAnchor: 'middle',
labelInterpolationFnc: function(value) {
<?php
if ($show_currency) {
if (is_right_side_currency_symbol()) {
?>
return value + '<?= esc($config['currency_symbol'], 'js') ?>';
<?php } else { ?>
return '<?= esc($config['currency_symbol'], 'js') ?>' + value;
<?php
}
} else {
?>
return value;
<?php } ?>
return '<?= $currency_prefix ?>' + value + '<?= $currency_suffix ?>';
}
}),
<?php
$currency_symbol = esc($config['currency_symbol'], 'js');
$currency_prefix = '';
$currency_suffix = '';
if ($show_currency) {
if (is_right_side_currency_symbol()) {
$currency_suffix = $currency_symbol;
} else {
$currency_prefix = $currency_symbol;
}
}
?>
Chartist.plugins.tooltip({
pointClass: 'ct-tooltip-point',
transformTooltipTextFnc: function(value) {
<?php
if ($show_currency) {
if (is_right_side_currency_symbol()) {
?>
return value + '<?= esc($config['currency_symbol'], 'js') ?>';
<?php } else { ?>
return '<?= esc($config['currency_symbol'], 'js') ?>' + value;
<?php
}
} else {
?>
return value;
<?php } ?>
return '<?= $currency_prefix ?>' + value + '<?= $currency_suffix ?>';
}
})
]

View File

@@ -31,26 +31,27 @@
labelPosition: 'outside',
labelDirection: 'explode',
<?php
$currency_symbol = esc($config['currency_symbol'], 'js');
$currency_prefix = '';
$currency_suffix = '';
if ($show_currency) {
if (is_right_side_currency_symbol()) {
$currency_suffix = $currency_symbol;
} else {
$currency_prefix = $currency_symbol;
}
}
?>
plugins: [
Chartist.plugins.tooltip({
transformTooltipTextFnc: function(value) {
<?php
if ($show_currency) {
if (is_right_side_currency_symbol()) {
?>
return value + '<?= esc($config['currency_symbol'], 'js') ?>';
<?php } else { ?>
return '<?= esc($config['currency_symbol'], 'js') ?>' + value;
<?php
}
} else {
?>
return value;
<?php } ?>
return '<?= $currency_prefix ?>' + value + '<?= $currency_suffix ?>';
}
})
]
};
] };
var responsiveOptions = [
['screen and (min-width: 640px)', {

View File

@@ -63,7 +63,7 @@ if (isset($error_message)) {
/* This line will allow to print and go back to sales automatically.
* echo anchor('sales', '<span class="glyphicon glyphicon-print">&nbsp;</span>' . lang('Common.print'), ['class' => 'btn btn-info btn-sm', 'id' => 'show_print_button', 'onclick' => 'window.print();'));
*/
?>
?>
<?php if (isset($customer_email) && !empty($customer_email)): ?>
<a href="javascript:void(0);">
<div class="btn btn-info btn-sm" id="show_email_button"><?= '<span class="glyphicon glyphicon-envelope">&nbsp;</span>' . lang('Sales.send_invoice') ?></div>
@@ -115,10 +115,10 @@ if (isset($error_message)) {
<tr>
<th><?= lang('Sales.item_number') ?></th>
<?php
$invoice_columns = 6;
if ($include_hsn) {
$invoice_columns += 1;
?>
$invoice_columns = 6;
if ($include_hsn) {
$invoice_columns += 1;
?>
<th><?= lang('Sales.hsn') ?></th>
<?php } ?>
<th><?= lang('Sales.item_name') ?></th>
@@ -126,9 +126,9 @@ if (isset($error_message)) {
<th><?= lang('Sales.price') ?></th>
<th><?= lang('Sales.discount') ?></th>
<?php
if ($discount > 0) {
$invoice_columns += 1;
?>
if ($discount > 0) {
$invoice_columns += 1;
?>
<th><?= lang('Sales.customer_discount') ?></th>
<?php } ?>
<th><?= lang('Sales.total') ?></th>
@@ -137,7 +137,7 @@ if (isset($error_message)) {
<?php
foreach ($cart as $line => $item) {
if ($item['print_option'] == PRINT_YES) {
?>
?>
<tr class="item-row">
<td><?= esc($item['item_number']) ?></td>
<?php if ($include_hsn): ?>
@@ -146,7 +146,7 @@ if (isset($error_message)) {
<td class="item-name"><?= ($item['is_serialized'] || $item['allow_alt_description']) && !empty($item['description']) ? esc($item['description']) : esc($item['name'] . ' ' . $item['attribute_values']) ?></td>
<td style="text-align: center;"><?= to_quantity_decimals($item['quantity']) ?></td>
<td><?= to_currency($item['price']) ?></td>
<td style="height: center;"><?= ($item['discount_type'] == FIXED) ? to_currency($item['discount']) : to_decimals($item['discount']) . '%' ?></td>
<td style="text-align: center;"><?= ($item['discount_type'] == FIXED) ? to_currency($item['discount']) : to_decimals($item['discount']) . '%' ?></td>
<?php if ($discount > 0): ?>
<td style="text-align: center;"><?= to_currency($item['discounted_total'] / $item['quantity']) ?></td>
<?php endif; ?>
@@ -155,13 +155,13 @@ if (isset($error_message)) {
<?php if ($item['is_serialized']) { ?>
<tr class="item-row">
<td class="item-description" colspan="<?= $invoice_columns - 1 ?>"></td>
<td style="text-align: center;"><?= esc($item['serialnumber']) // TODO: serialnumber does not match variable naming conventions for this project ?></td>
<td style="text-align: center;"><?= esc($item['serialnumber']) // TODO: `serialnumber` does not match variable naming conventions for this project. Should be `serialNumber`?></td>
</tr>
<?php
}
}
}
?>
?>
<tr>
<td class="blank" colspan="<?= $invoice_columns ?>" style="text-align: center;"><?= '&nbsp;' ?></td>
@@ -188,13 +188,13 @@ if (isset($error_message)) {
</tr>
<?php
$only_sale_check = false;
$show_giftcard_remainder = false;
foreach ($payments as $payment_id => $payment) {
$only_sale_check |= $payment['payment_type'] == lang('Sales.check');
$splitpayment = explode(':', $payment['payment_type']); // TODO: $splitpayment does not meet variable naming standards for this project
$show_giftcard_remainder |= $splitpayment[0] == lang('Sales.giftcard');
?>
$only_sale_check = false;
$show_giftcard_remainder = false;
foreach ($payments as $payment_id => $payment) {
$only_sale_check |= $payment['payment_type'] == lang('Sales.check');
$splitpayment = explode(':', $payment['payment_type']); // TODO: $splitpayment does not meet variable naming standards for this project
$show_giftcard_remainder |= $splitpayment[0] == lang('Sales.giftcard');
?>
<tr>
<td colspan="<?= $invoice_columns - 3 ?>" class="blank"> </td>
<td colspan="2" class="total-line"><?= esc($splitpayment[0]) ?></td>

View File

@@ -20,171 +20,167 @@
?>
<!doctype html>
<head>
<meta charset="utf-8">
<title><?= lang('Sales.email_receipt') ?></title>
<link rel="stylesheet" href="<?= base_url('css/invoice_email.css') ?>">
</head>
<body>
<?php
if (isset($error_message)) {
echo '<div class="alert alert-dismissible alert-danger">' . esc($error_message) . '</div>';
exit;
}
?>
<div id="page-wrap">
<div id="header"><?= lang('Sales.invoice') ?></div>
<table id="info">
<tr>
<td id="logo">
<?php if ($config['company_logo'] != '') { ?>
<img id="image" src="data:<?= esc($mimetype, 'attr') ?>;base64,<?= base64_encode(file_get_contents('uploads/' . esc($config['company_logo']))) ?>" alt="company_logo">
<?php } ?>
</td>
<td id="customer-title" id="customer">
<?php if (isset($customer)) {
echo nl2br(esc($customer_info));
} ?>
</td>
</tr>
<tr>
<td id="company-title" id="company">
<?= esc($config['company']) ?><br>
<?= nl2br(esc($company_info)) ?>
</td>
<td id="meta">
<table id="meta-content" align="right">
<tr>
<td class="meta-head"><?= lang('Sales.invoice_number') ?></td>
<td><?= esc($invoice_number) ?></td>
</tr>
<tr>
<td class="meta-head"><?= lang('Common.date') ?></td>
<td><?= esc($transaction_date) ?></td>
</tr>
<?php if ($amount_due > 0) { ?>
<tr>
<td class="meta-head"><?= lang('Sales.amount_due') ?></td>
<td class="due"><?= to_currency($total) ?></td>
</tr>
<?php } ?>
</table>
</td>
</tr>
</table>
<table id="items">
<tr>
<th><?= lang('Sales.item_number') ?></th>
<th><?= lang('Sales.item_name') ?></th>
<th><?= lang('Sales.quantity') ?></th>
<th><?= lang('Sales.price') ?></th>
<th><?= lang('Sales.discount') ?></th>
<?php
$invoice_columns = 6;
if ($discount > 0) {
$invoice_columns = $invoice_columns + 1;
?>
<th><?= lang('Sales.customer_discount') ?></th>
<?php } ?>
<th><?= lang('Sales.total') ?></th>
</tr>
<?php
foreach ($cart as $line => $item) {
if ($item['print_option'] == PRINT_YES) {
?>
<tr class="item-row">
<td><?= esc($item['item_number']) ?></td>
<td class="item-name"><?= esc($item['name']) ?></td>
<td><?= to_quantity_decimals($item['quantity']) ?></td>
<td><?= to_currency($item['price']) ?></td>
<td><?= ($item['discount_type'] == FIXED) ? to_currency($item['discount']) : to_decimals($item['discount']) . '%' ?></td>
<?php if ($item['discount'] > 0): ?>
<td><?= to_currency($item['discounted_total'] / $item['quantity']) ?></td>
<?php endif; ?>
<td class="total-line"><?= to_currency($item['discounted_total']) ?></td>
</tr>
<?php
}
<head>
<meta charset="utf-8">
<title><?= lang('Sales.email_receipt') ?></title>
<link rel="stylesheet" href="<?= base_url('css/invoice_email.css') ?>">
</head>
<body>
<?php
if (isset($error_message)) {
echo '<div class="alert alert-dismissible alert-danger">' . esc($error_message) . '</div>';
exit;
}
?>
?>
<tr>
<td colspan="<?= $invoice_columns ?>" align="center"><?= '&nbsp;' ?></td>
</tr>
<div id="page-wrap">
<div id="header"><?= lang('Sales.invoice') ?></div>
<table id="info">
<tr>
<td id="logo">
<?php if ($config['company_logo'] != '') { ?>
<img id="image" src="data:<?= esc($mimetype, 'attr') ?>;base64,<?= base64_encode(file_get_contents('uploads/' . esc($config['company_logo']))) ?>" alt="company_logo">
<?php } ?>
</td>
<td id="customer-title" id="customer">
<?php if (isset($customer)) {
echo nl2br(esc($customer_info));
} ?>
</td>
</tr>
<tr>
<td id="company-title" id="company">
<?= esc($config['company']) ?><br>
<?= nl2br(esc($company_info)) ?>
</td>
<td id="meta">
<table id="meta-content" style="text-align: right;">
<tr>
<td class="meta-head"><?= lang('Sales.invoice_number') ?></td>
<td><?= esc($invoice_number) ?></td>
</tr>
<tr>
<td class="meta-head"><?= lang('Common.date') ?></td>
<td><?= esc($transaction_date) ?></td>
</tr>
<?php if ($amount_due > 0) { ?>
<tr>
<td class="meta-head"><?= lang('Sales.amount_due') ?></td>
<td class="due"><?= to_currency($total) ?></td>
</tr>
<?php } ?>
</table>
</td>
</tr>
</table>
<tr>
<td colspan="<?= $invoice_columns - 3 ?>" class="blank"> </td>
<td colspan="2" class="total-line"><?= lang('Sales.sub_total') ?></td>
<td id="subtotal" class="total-value"><?= to_currency($subtotal) ?></td>
</tr>
<table id="items">
<tr>
<th><?= lang('Sales.item_number') ?></th>
<th><?= lang('Sales.item_name') ?></th>
<th><?= lang('Sales.quantity') ?></th>
<th><?= lang('Sales.price') ?></th>
<th><?= lang('Sales.discount') ?></th>
<?php
$invoice_columns = 6;
if ($discount > 0) {
$invoice_columns = $invoice_columns + 1;
?>
<th><?= lang('Sales.customer_discount') ?></th>
<?php } ?>
<th><?= lang('Sales.total') ?></th>
</tr>
<?php
foreach ($cart as $line => $item) {
if ($item['print_option'] == PRINT_YES) {
?>
<tr class="item-row">
<td><?= esc($item['item_number']) ?></td>
<td class="item-name"><?= esc($item['name']) ?></td>
<td><?= to_quantity_decimals($item['quantity']) ?></td>
<td><?= to_currency($item['price']) ?></td>
<td><?= ($item['discount_type'] == FIXED) ? to_currency($item['discount']) : to_decimals($item['discount']) . '%' ?></td>
<?php if ($discount > 0): ?>
<td><?= to_currency($item['discounted_total'] / $item['quantity']) ?></td>
<?php endif; ?>
<td class="total-line"><?= to_currency($item['discounted_total']) ?></td>
</tr>
<?php
}
}
?>
<tr>
<td colspan="<?= $invoice_columns ?>" style="text-align: center;"><?= '&nbsp;' ?></td>
</tr>
<?php foreach ($taxes as $tax_group_index => $tax) { ?>
<tr>
<td colspan="<?= $invoice_columns - 3 ?>" class="blank"> </td>
<td colspan="2" class="total-line"><?= (float)$tax['tax_rate'] . '% ' . esc($tax['tax_group']) ?></td>
<td id="taxes" class="total-value"><?= to_currency_tax($tax['sale_tax_amount']) ?></td>
<td colspan="2" class="total-line"><?= lang('Sales.sub_total') ?></td>
<td id="subtotal" class="total-value"><?= to_currency($subtotal) ?></td>
</tr>
<?php } ?>
<tr>
<td colspan="<?= $invoice_columns - 3 ?>" class="blank"> </td>
<td colspan="2" class="total-line"><?= lang('Sales.total') ?></td>
<td id="total" class="total-value"><?= to_currency($total) ?></td>
</tr>
<?php foreach ($taxes as $tax_group_index => $tax) { ?>
<tr>
<td colspan="<?= $invoice_columns - 3 ?>" class="blank"> </td>
<td colspan="2" class="total-line"><?= (float)$tax['tax_rate'] . '% ' . esc($tax['tax_group']) ?></td>
<td id="taxes" class="total-value"><?= to_currency_tax($tax['sale_tax_amount']) ?></td>
</tr>
<?php } ?>
<?php
$only_sale_check = false;
$show_giftcard_remainder = false;
foreach ($payments as $payment_id => $payment) {
$only_sale_check |= $payment['payment_type'] == lang('Sales.check');
$splitpayment = explode(':', $payment['payment_type']); // TODO: $splitpayment does not meet the variable naming conventions for this project
$show_giftcard_remainder |= $splitpayment[0] == lang('Sales.giftcard');
?>
<tr>
<td colspan="<?= $invoice_columns - 3 ?>" class="blank"> </td>
<td colspan="2" class="total-line"><?= esc($splitpayment[0]) ?></td>
<td class="total-value"><?= to_currency(-$payment['payment_amount']) ?></td>
<td colspan="2" class="total-line"><?= lang('Sales.total') ?></td>
<td id="total" class="total-value"><?= to_currency($total) ?></td>
</tr>
<?php } ?>
<?php if (isset($cur_giftcard_value) && $show_giftcard_remainder) { ?>
<tr>
<td colspan="<?= $invoice_columns - 3 ?>" class="blank"> </td>
<td colspan="2" class="total-line"><?= lang('Sales.giftcard_balance') ?></td>
<td class="total-value" id="giftcard"><?= to_currency($cur_giftcard_value) ?></td>
</tr>
<?php } ?>
<?php
$only_sale_check = false;
$show_giftcard_remainder = false;
<?php if (!empty($payments)) { ?>
<tr>
<td colspan="<?= $invoice_columns - 3 ?>" class="blank"> </td>
<td colspan="2" class="total-line"><?= lang($amount_change >= 0 ? ($only_sale_check ? 'Sales.check_balance' : 'Sales.change_due') : 'Sales.amount_due') ?></td>
<td class="total-value"><?= to_currency($amount_change) ?></td>
</tr>
<?php } ?>
</table>
foreach ($payments as $payment_id => $payment) {
$only_sale_check |= $payment['payment_type'] == lang('Sales.check');
$splitpayment = explode(':', $payment['payment_type']); // TODO: $splitpayment does not meet the variable naming conventions for this project
$show_giftcard_remainder |= $splitpayment[0] == lang('Sales.giftcard');
?>
<tr>
<td colspan="<?= $invoice_columns - 3 ?>" class="blank"> </td>
<td colspan="2" class="total-line"><?= esc($splitpayment[0]) ?></td>
<td class="total-value"><?= to_currency(-$payment['payment_amount']) ?></td>
</tr>
<?php } ?>
<div id="terms">
<div id="sale_return_policy">
<h5>
<span><?= nl2br($config['payment_message']) ?></span>
<span><?= lang('Sales.comments') . ': ' . (empty($comments) ? esc($config['invoice_default_comments']) : esc($comments)) ?></span>
</h5>
<?= nl2br(esc($config['return_policy'])) ?>
</div>
<div id="barcode">
<img alt="<?= esc($sale_id) ?>" src="data:image/svg+xml;base64,<?= base64_encode($barcode) ?>"><br>
<?= esc($sale_id) ?>
<?php if (isset($cur_giftcard_value) && $show_giftcard_remainder) { ?>
<tr>
<td colspan="<?= $invoice_columns - 3 ?>" class="blank"> </td>
<td colspan="2" class="total-line"><?= lang('Sales.giftcard_balance') ?></td>
<td class="total-value" id="giftcard"><?= to_currency($cur_giftcard_value) ?></td>
</tr>
<?php } ?>
<?php if (!empty($payments)) { ?>
<tr>
<td colspan="<?= $invoice_columns - 3 ?>" class="blank"> </td>
<td colspan="2" class="total-line"><?= lang($amount_change >= 0 ? ($only_sale_check ? 'Sales.check_balance' : 'Sales.change_due') : 'Sales.amount_due') ?></td>
<td class="total-value"><?= to_currency($amount_change) ?></td>
</tr>
<?php } ?>
</table>
<div id="terms">
<div id="sale_return_policy">
<h5>
<span><?= nl2br(esc($config['payment_message'])) ?></span>
<span><?= lang('Sales.comments') . ': ' . (empty($comments) ? esc($config['invoice_default_comments']) : esc($comments)) ?></span>
</h5>
<?= nl2br(esc($config['return_policy'])) ?>
</div>
<div id="barcode">
<img alt="<?= esc($sale_id) ?>" src="data:image/svg+xml;base64,<?= base64_encode($barcode) ?>"><br>
<?= esc($sale_id) ?>
</div>
</div>
</div>
</div>
</body>
</body>
</html>

View File

@@ -73,7 +73,7 @@
<?= view('partial/print_receipt', ['print_after_sale' => false, 'selected_printer' => 'takings_printer']) ?>
<div id="title_bar" class="print_hide btn-toolbar">
<button onclick="javascript:printdoc()" class="btn btn-info btn-sm pull-right">
<button onclick="printdoc()" class="btn btn-info btn-sm pull-right">
<span class="glyphicon glyphicon-print">&nbsp;</span><?= lang('Common.print') ?>
</button>
<?= anchor("sales", '<span class="glyphicon glyphicon-shopping-cart">&nbsp;</span>' . lang('Sales.register'), ['class' => 'btn btn-info btn-sm pull-right', 'id' => 'show_sales_button']) ?>

View File

@@ -58,7 +58,7 @@
</div>
</td>
<td id="meta">
<table id="meta-content" align="right">
<table id="meta-content" style="text-align: right;">
<tr>
<td class="meta-head"><?= lang('Sales.quote_number') ?> </td>
<td><?= esc($quote_number) ?></td>
@@ -116,7 +116,7 @@
?>
<tr>
<td colspan="<?= $quote_columns ?>" align="center"><?= '&nbsp;' //TODO: Replace the php echo for nbsp with just straight html? ?></td>
<td colspan="<?= $quote_columns ?>" style="text-align: center;"><?= '&nbsp;' //TODO: Replace the php echo for nbsp with just straight html? ?></td>
</tr>
<tr>

Some files were not shown because too many files have changed in this diff Show More