mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-05-29 18:55:53 -04:00
Compare commits
6 Commits
review-pr-
...
refactor-4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62236aec30 | ||
|
|
905b58ca6e | ||
|
|
609b206375 | ||
|
|
6fec2464f8 | ||
|
|
332d8c8c69 | ||
|
|
577cf55b6a |
17
.github/ISSUE_TEMPLATE/bug report.yml
vendored
17
.github/ISSUE_TEMPLATE/bug report.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/build-release.yml
vendored
2
.github/workflows/build-release.yml
vendored
@@ -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
|
||||
|
||||
1
.github/workflows/main.yml
vendored
1
.github/workflows/main.yml
vendored
@@ -28,7 +28,6 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php-version:
|
||||
- '8.1'
|
||||
- '8.2'
|
||||
- '8.3'
|
||||
- '8.4'
|
||||
|
||||
8
.github/workflows/php-linter.yml
vendored
8
.github/workflows/php-linter.yml
vendored
@@ -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:
|
||||
|
||||
3
.github/workflows/phpunit.yml
vendored
3
.github/workflows/phpunit.yml
vendored
@@ -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
|
||||
|
||||
@@ -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)>).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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 can’t 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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = [];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
|
||||
namespace Config;
|
||||
|
||||
/**
|
||||
* @immutable
|
||||
*/
|
||||
class DocTypes
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>>>
|
||||
*/
|
||||
|
||||
@@ -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
40
app/Config/Hostnames.php
Normal 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',
|
||||
];
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
/*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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__ . '/../../';
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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
62
app/Config/WorkerMode.php
Normal 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;
|
||||
}
|
||||
@@ -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')));
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')]);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
37
app/Database/Seeds/TestDatabaseBootstrapSeeder.php
Normal file
37
app/Database/Seeds/TestDatabaseBootstrapSeeder.php
Normal 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}`");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
35
app/Helpers/attribute_helper.php
Normal file
35
app/Helpers/attribute_helper.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -94,4 +94,3 @@ function remove_backup(): void
|
||||
@unlink($backup_path);
|
||||
log_message('info', "Removed $backup_path");
|
||||
}
|
||||
|
||||
|
||||
@@ -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 !== [];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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, '-_', '+/'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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'])));
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
130
app/Traits/Controller/Shared.php
Normal file
130
app/Traits/Controller/Shared.php
Normal 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
|
||||
];
|
||||
}
|
||||
}
|
||||
72
app/Traits/Database/SalesTaxMigration.php
Normal file
72
app/Traits/Database/SalesTaxMigration.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
30
app/Traits/Models/Reports/ReportDateFilter.php
Normal file
30
app/Traits/Models/Reports/ReportDateFilter.php
Normal 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'])));
|
||||
}
|
||||
}
|
||||
}
|
||||
39
app/Traits/Models/Reports/SaleTypeFilter.php
Normal file
39
app/Traits/Models/Reports/SaleTypeFilter.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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"> </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") ?>">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 "» GD: ", extension_loaded('gd') ? '<span style="color: green;">Enabled ✓</span>' : '<span style="color: red;">Disabled ✗</span>', '<br>';
|
||||
echo "» BC Math: ", extension_loaded('bcmath') ? '<span style="color: green;">Enabled ✓</span>' : '<span style="color: red">Disabled ✗</span>', '<br>';
|
||||
echo "» INTL: ", extension_loaded('intl') ? '<span style="color: green;">Enabled ✓</span>' : '<span style="color: red">Disabled ✗</span>', '<br>';
|
||||
echo "» OpenSSL: ", extension_loaded('openssl') ? '<span style="color: green;">Enabled ✓</span>' : '<span style="color: red">Disabled ✗</span>', '<br>';
|
||||
echo "» MBString: ", extension_loaded('mbstring') ? '<span style="color: green;">Enabled ✓</span>' : '<span style="color: red">Disabled ✗</span>', '<br>';
|
||||
echo "» Curl: ", extension_loaded('curl') ? '<span style="color: green;">Enabled ✓</span>' : '<span style="color: red">Disabled ✗</span>', '<br>';
|
||||
echo "» Json: ", extension_loaded('json') ? '<span style="color: green;">Enabled ✓</span>' : '<span style="color: red">Disabled ✗</span>', '<br><br>';
|
||||
echo "» Xml: ", extension_loaded('xml') ? '<span style="color: green;">Enabled ✓</span>' : '<span style="color: red">Disabled ✗</span>', '<br><br>';
|
||||
echo "» GD: ", extension_loaded('gd') ? '<span style="color: green;">Enabled ✓</span>' : '<span style="color: red;">Disabled ✗</span>', '<br>';
|
||||
echo "» BC Math: ", extension_loaded('bcmath') ? '<span style="color: green;">Enabled ✓</span>' : '<span style="color: red;">Disabled ✗</span>', '<br>';
|
||||
echo "» INTL: ", extension_loaded('intl') ? '<span style="color: green;">Enabled ✓</span>' : '<span style="color: red;">Disabled ✗</span>', '<br>';
|
||||
echo "» OpenSSL: ", extension_loaded('openssl') ? '<span style="color: green;">Enabled ✓</span>' : '<span style="color: red;">Disabled ✗</span>', '<br>';
|
||||
echo "» MBString: ", extension_loaded('mbstring') ? '<span style="color: green;">Enabled ✓</span>' : '<span style="color: red;">Disabled ✗</span>', '<br>';
|
||||
echo "» Curl: ", extension_loaded('curl') ? '<span style="color: green;">Enabled ✓</span>' : '<span style="color: red;">Disabled ✗</span>', '<br>';
|
||||
echo "» Json: ", extension_loaded('json') ? '<span style="color: green;">Enabled ✓</span>' : '<span style="color: red;">Disabled ✗</span>', '<br><br>';
|
||||
echo "» Xml: ", extension_loaded('xml') ? '<span style="color: green;">Enabled ✓</span>' : '<span style="color: red;">Disabled ✗</span>', '<br><br>';
|
||||
?>
|
||||
User Configuration:<br>
|
||||
.Browser:
|
||||
|
||||
@@ -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'] ?? [])));
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
<?php
|
||||
/**
|
||||
* @var string $message
|
||||
*/
|
||||
?>
|
||||
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
@@ -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')) ?> —
|
||||
PHP: <?= esc(PHP_VERSION) ?> —
|
||||
CodeIgniter: <?= esc(CodeIgniter::CI_VERSION) ?> --
|
||||
Environment: <?= ENVIRONMENT ?>
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<div class="environment">
|
||||
Displayed at <?= esc(date('H:i:s')) ?> —
|
||||
PHP: <?= esc(PHP_VERSION) ?> —
|
||||
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 →</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 →</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 →</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'])) : ?>
|
||||
— <?= 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'])) : ?>
|
||||
— <?= 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 →</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'])) : ?>
|
||||
— <?= 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'])) : ?>
|
||||
— <?= 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>
|
||||
|
||||
@@ -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"> </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') ?>">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 ?>';
|
||||
}
|
||||
})
|
||||
]
|
||||
|
||||
@@ -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)', {
|
||||
|
||||
@@ -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"> </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"> </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;"><?= ' ' ?></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>
|
||||
|
||||
@@ -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"><?= ' ' ?></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;"><?= ' ' ?></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>
|
||||
|
||||
@@ -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"> </span><?= lang('Common.print') ?>
|
||||
</button>
|
||||
<?= anchor("sales", '<span class="glyphicon glyphicon-shopping-cart"> </span>' . lang('Sales.register'), ['class' => 'btn btn-info btn-sm pull-right', 'id' => 'show_sales_button']) ?>
|
||||
|
||||
@@ -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"><?= ' ' //TODO: Replace the php echo for nbsp with just straight html? ?></td>
|
||||
<td colspan="<?= $quote_columns ?>" style="text-align: center;"><?= ' ' //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
Reference in New Issue
Block a user