Compare commits

..

28 Commits

Author SHA1 Message Date
Ollama
61cc600909 fix: address PR review comments
- Add missing 'branch' output to build job (fixes master detection)
- Guard Docker publish against fork PRs (secrets unavailable)
- Use consistent 7-char SHA from build outputs in release job
- Add pipefail to install script test to preserve exit status
2026-05-22 14:53:16 +02:00
Ollama
ccc594c377 fix: centralize version tag logic across workflows
Previously, version tags were generated inconsistently:
- build-release.yml: {VERSION}-{BRANCH}-{SHA-6}
- deploy-pr.yml: pr-{PR_NUMBER}-{SHA-7}

Additionally, deploy-pr.yml was broken because Docker images weren't
built for PR events, causing deployment failures.

Changes:
- Create shared .github/scripts/get-version.sh for version tag generation
- Standardize SHA length to 7 characters across all workflows
- Enable Docker builds for pull_request events in build-release.yml
- Update deploy-pr.yml to use shared script

Tag formats are now:
- PRs: pr-{NUMBER}-{SHA-7}
- Master push: {VERSION} (+ latest tag)
- Feature branches: {VERSION}-{BRANCH}-{SHA-7}
2026-05-19 09:03:46 +02:00
Ollama
1caea1b762 fix: escape special characters in passwords for sed
Base64 passwords can contain /, +, = which break sed with |
delimiter. Also escape & and \ characters for safety.
2026-05-19 08:57:32 +02:00
Ollama
b77f094650 fix: address remaining CodeRabbit issues
- Add SQL injection validation for DB_NAME, DB_USER, DB_HOST
- Honor OSPOS_DIR during extraction (was hardcoded to /var/www)
- Fix SSL_EMAIL alone enabling Let's Encrypt (use APACHE_SERVER_NAME as domain)
- Quote OSPOS_DIR variables to prevent word splitting
2026-05-19 08:57:32 +02:00
Ollama
1a9a080358 fix: address CodeRabbit review comments
- Remove duplicate php-gd package
- Disable directory listing (Options -Indexes)
- Propagate MYSQL_ROOT_PASS to all mysql commands
- Fix allowedHostnames sed pattern to match .env.example format
2026-05-19 08:57:32 +02:00
Ollama
71fe6ac6e4 docs: add missing env vars and testing note to INSTALL.md 2026-05-19 08:57:32 +02:00
Ollama
a854132637 chore: remove debug output after fix verification 2026-05-19 08:57:32 +02:00
Ollama
6f4448d09b fix: use | delimiter in sed to handle special characters in passwords
The / delimiter was breaking when passwords contain special chars like !
2026-05-19 08:57:32 +02:00
Ollama
cb847414b9 fix: copy hidden files from extraction directory
Hidden files like .env were not being copied because shell glob *
doesn't match hidden files. Use 'ospos-temp/.' to copy all files.
2026-05-19 08:57:32 +02:00
Ollama
e76421e49c fix: add debug output to trace .env configuration 2026-05-19 08:57:32 +02:00
Ollama
f76acbb693 fix: add verification output for .env configuration 2026-05-19 08:57:32 +02:00
Ollama
acbb3c1639 fix: generate encryption key for OSPOS
The release .env has an empty encryption key which causes
HTTP 500 on startup. Generate a random key if the value is empty.
2026-05-19 08:57:32 +02:00
Ollama
44e3b4c4dc ci: improve debugging for .env and PHP errors 2026-05-19 08:57:32 +02:00
Ollama
1d40e7d5f5 fix: match quoted values in .env file for sed substitutions
The .env file from release uses quoted values like 'localhost'
but sed patterns were looking for unquoted values, causing
database credentials to not be updated.
2026-05-19 08:57:32 +02:00
Ollama
e98bf38af6 fix: configure .env even when .env.example doesn't exist
Release zip contains .env directly, not .env.example. The sed commands
to update database credentials were being skipped because the file check
looked for .env.example first, which doesn't exist in published releases.
2026-05-19 08:57:32 +02:00
Ollama
65fdea0807 ci: add more detailed debugging for HTTP 500 errors 2026-05-19 08:57:32 +02:00
Ollama
8172dc23f7 ci: add debug logging for HTTP 500 errors 2026-05-19 08:57:32 +02:00
Ollama
98d703dc27 fix: extract release zip directly to OSPOS directory
The release zip files extract at root level without a subdirectory
2026-05-19 08:57:32 +02:00
Ollama
c589367479 fix: remove hardcoded OSPOS_VERSION from CI workflow
Let the install script use the latest release by default
2026-05-19 08:57:32 +02:00
Ollama
1747332282 fix: use correct release asset URL from GitHub API
Releases use opensourcepos.VERSION.HASH.zip naming format, not
opensourcepos-VERSION.zip. This fix fetches the actual asset URL
from the GitHub API and extracts the correct directory name.
2026-05-19 08:57:32 +02:00
Ollama
f78a24862e fix: add ondrej/php PPA when PHP version not in default repos
The install script was failing on Ubuntu 22.04 because PHP 8.2 is not
available in default repositories. This fix checks if the requested
PHP version is available, and if not, adds the ondrej/php PPA which
provides all supported PHP versions.
2026-05-19 08:57:32 +02:00
Ollama
f2a57ff5bb ci: add install script test workflow
- Tests install script on Ubuntu 22.04 runner
- Verifies Apache, MariaDB, and OSPOS services
- Matrix tests default and custom DB_PASS scenarios
- Uploads install logs as artifacts
2026-05-19 08:57:32 +02:00
Ollama
4f7c4cf0f6 Add interactive SSL configuration prompt
- Prompts user for SSL preferences during installation
- Asks for domain name and email interactively
- Falls back to environment variables for non-interactive mode
- Shows SSL status in final output (Let's Encrypt / self-signed / none)
- Updates INSTALL.md with interactive/non-interactive examples

Interactive mode (recommended):
  curl -sSL https://opensourcepos.org/install | sudo bash
  # Prompts for SSL, domain, and email

Non-interactive mode:
  curl -sSL https://opensourcepos.org/install | \
    APACHE_SERVER_NAME=pos.example.com \
    SSL_EMAIL=admin@example.com \
    sudo -E bash
2026-05-19 08:57:32 +02:00
Ollama
8282066ed6 Add automatic SSL/TLS certificate setup
- Adds Let's Encrypt support for production (with auto-renewal via certbot.timer)
- Falls back to self-signed certificate for development/testing
- New SSL_EMAIL environment variable enables production SSL
- HTTPS redirect automatically configured for all sites
- Updates INSTALL.md with SSL documentation and examples

Production usage:
  SSL_EMAIL=admin@example.com APACHE_SERVER_NAME=pos.example.com

Development usage (self-signed cert):
  APACHE_SERVER_NAME=localhost (default)
2026-05-19 08:57:32 +02:00
Ollama
6b69959f7d Download latest stable release instead of master branch
- Fetches latest release version from GitHub API
- Downloads pre-built release zip instead of cloning repo
- Renamed OSPOS_BRANCH to OSPOS_VERSION for clarity
- Supports installing specific version via OSPOS_VERSION
- Removed need for composer install (release is pre-built)
- More stable for production deployments
2026-05-19 08:57:32 +02:00
Ollama
b13e4d2ce9 Update Cloud Install section to recommend one-line installer
- Keep DigitalOcean referral link ($100 credit)
- Simplify instructions to 3 steps: create droplet, SSH, run installer
- Move one-line installation section into Cloud Install
- Add security reminder to change password and configure SSL
- Retain link to wiki for manual installation options
2026-05-19 08:57:32 +02:00
jekkos
641b05f900 Update INSTALL.md with opensourcepos.org short URL
- Preferred install URL: https://opensourcepos.org/install
- Falls back to direct GitHub URL if redirect unavailable
- More professional and easier to remember
2026-05-19 08:57:32 +02:00
jekkos
1db7d6f552 Add one-line Ubuntu installation script
- Creates scripts/install-ubuntu.sh for automated fresh Ubuntu server setup
- Installs Apache, MariaDB, PHP 8.2 with required extensions
- Downloads and configures OSPOS from GitHub
- Sets up Apache virtual host with proper permissions
- Generates secure random database password
- Supports environment variables for customization
- Updates INSTALL.md with curl pipe to bash instructions

This provides an alternative to cloud-specific instructions and
allows users to quickly set up OSPOS on any fresh Ubuntu server.
2026-05-19 08:57:32 +02:00
25 changed files with 4973 additions and 5253 deletions

94
.github/scripts/get-version.sh vendored Normal file
View File

@@ -0,0 +1,94 @@
#!/bin/bash
set -euo pipefail
# Shared version tag generation script for GitHub Actions workflows
# Usage: ./get-version.sh [FORMAT] [SHA_LENGTH]
#
# Formats:
# docker-tag - Docker image tag (default)
# archive - Archive filename suffix
# all - Output all version variables for GITHUB_OUTPUT
#
# Environment variables:
# GITHUB_REF - Git ref (e.g., refs/heads/master, refs/pull/123/merge)
# GITHUB_SHA - Git commit SHA
# GITHUB_EVENT_NAME - Event that triggered workflow (push, pull_request, etc.)
# GITHUB_EVENT_PATH - Path to event JSON (for PR number extraction)
# GITHUB_OUTPUT - Path to GITHUB_OUTPUT file (when format=all)
# Ensure we're in a git repository with source files
cd "${GITHUB_WORKSPACE:-.}"
# Get version from App.php
VERSION=$(grep "application_version" app/Config/App.php | sed "s/.*= '\(.*\)';/\1/g")
# Standardize SHA length (default: 7 chars)
SHA_LENGTH="${2:-7}"
SHA="${GITHUB_SHA:0:$SHA_LENGTH}"
# Initialize variables
IMAGE_TAG=""
BRANCH=""
# Detect event type and generate appropriate tag
if [[ "$GITHUB_EVENT_NAME" == "pull_request" || "$GITHUB_EVENT_NAME" == "pull_request_review" ]]; then
# Extract PR number from event JSON
if [[ -f "${GITHUB_EVENT_PATH:-}" ]]; then
PR_NUMBER=$(jq -r '.pull_request.number // .number // empty' < "$GITHUB_EVENT_PATH" 2>/dev/null || true)
if [[ -n "$PR_NUMBER" ]]; then
# PR-based tag (for PR deployments)
IMAGE_TAG="pr-${PR_NUMBER}-${SHA}"
BRANCH="pr-${PR_NUMBER}"
fi
fi
# Fallback if we couldn't extract PR number
if [[ -z "$IMAGE_TAG" ]]; then
# Try to extract from GITHUB_REF
PR_NUMBER=$(echo "$GITHUB_REF" | grep -oP 'pull/\K[0-9]+' || true)
if [[ -n "$PR_NUMBER" ]]; then
IMAGE_TAG="pr-${PR_NUMBER}-${SHA}"
BRANCH="pr-${PR_NUMBER}"
else
# Last resort: use SHA only
IMAGE_TAG="${SHA}"
BRANCH="unknown"
fi
fi
else
# Branch-based tag (for push events)
BRANCH="${GITHUB_REF#refs/heads/}"
BRANCH=$(echo "$BRANCH" | sed 's/feature\///' | tr '/' '_')
if [[ "$BRANCH" == "master" ]]; then
# Master builds: use version as tag
IMAGE_TAG="${VERSION}"
else
# Feature branch builds: version + branch + sha
IMAGE_TAG="${VERSION}-${BRANCH}-${SHA}"
fi
fi
# Output format based on first argument
case "${1:-docker-tag}" in
docker-tag)
echo "$IMAGE_TAG"
;;
archive)
echo "${VERSION}.${SHA}"
;;
all)
{
echo "version=${VERSION}"
echo "version-tag=${IMAGE_TAG}"
echo "short-sha=${SHA}"
echo "branch=${BRANCH}"
} >> "$GITHUB_OUTPUT"
echo "::debug::version=${VERSION}, version-tag=${IMAGE_TAG}, short-sha=${SHA}, branch=${BRANCH}"
;;
*)
echo "::error::Unknown format: $1" >&2
exit 1
;;
esac

View File

@@ -22,6 +22,7 @@ jobs:
version: ${{ steps.version.outputs.version }}
version-tag: ${{ steps.version.outputs.version-tag }}
short-sha: ${{ steps.version.outputs.short-sha }}
branch: ${{ steps.version.outputs.branch }}
steps:
- name: Checkout
@@ -75,16 +76,13 @@ jobs:
- name: Get version info
id: version
run: |
VERSION=$(grep "application_version" app/Config/App.php | sed "s/.*= '\(.*\)';/\1/g")
BRANCH=$(echo "${GITHUB_REF#refs/heads/}" | sed 's/feature\///' | tr '/' '_')
TAG=$(echo "${GITHUB_TAG:-$BRANCH}" | tr '/' '_')
SHORT_SHA=$(git rev-parse --short=6 HEAD)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "version-tag=$VERSION-$BRANCH-$SHORT_SHA" >> $GITHUB_OUTPUT
echo "short-sha=$SHORT_SHA" >> $GITHUB_OUTPUT
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
chmod +x .github/scripts/get-version.sh
.github/scripts/get-version.sh all 7
env:
GITHUB_TAG: ${{ github.ref_name }}
GITHUB_EVENT_PATH: ${{ github.event_path }}
GITHUB_SHA: ${{ github.sha }}
GITHUB_REF: ${{ github.ref }}
GITHUB_EVENT_NAME: ${{ github.event_name }}
- name: Create .env file
run: |
@@ -130,7 +128,7 @@ jobs:
name: Build Docker Image
runs-on: ubuntu-22.04
needs: build
if: github.event_name == 'push'
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
steps:
- name: Download build context
@@ -154,14 +152,14 @@ jobs:
- name: Determine Docker tags
id: tags
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:master" >> $GITHUB_OUTPUT
TAG="${{ needs.build.outputs.version-tag }}"
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "tags=${{ secrets.DOCKER_USERNAME }}/opensourcepos:${TAG}" >> $GITHUB_OUTPUT
elif [ "${{ needs.build.outputs.branch }}" = "master" ]; then
echo "tags=${{ secrets.DOCKER_USERNAME }}/opensourcepos:${TAG},${{ secrets.DOCKER_USERNAME }}/opensourcepos:latest" >> $GITHUB_OUTPUT
else
echo "tags=${{ secrets.DOCKER_USERNAME }}/opensourcepos:${{ needs.build.outputs.version-tag }}" >> $GITHUB_OUTPUT
echo "tags=${{ secrets.DOCKER_USERNAME }}/opensourcepos:${TAG}" >> $GITHUB_OUTPUT
fi
env:
GITHUB_REF: ${{ github.ref }}
- name: Build and push Docker images
uses: docker/build-push-action@v5
@@ -194,7 +192,7 @@ jobs:
id: version
run: |
VERSION="${{ needs.build.outputs.version }}"
SHORT_SHA=$(git rev-parse --short=6 HEAD)
SHORT_SHA="${{ needs.build.outputs.short-sha }}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "short-sha=$SHORT_SHA" >> $GITHUB_OUTPUT

View File

@@ -33,12 +33,15 @@ jobs:
- name: Get image tag
id: image
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_SHA: ${{ github.event.pull_request.head.sha }}
run: |
IMAGE_TAG="pr-${PR_NUMBER}-${PR_SHA:0:7}"
chmod +x .github/scripts/get-version.sh
IMAGE_TAG=$(.github/scripts/get-version.sh docker-tag 7)
echo "tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT"
env:
GITHUB_EVENT_PATH: ${{ github.event_path }}
GITHUB_SHA: ${{ github.event.pull_request.head.sha }}
GITHUB_REF: ${{ github.ref }}
GITHUB_EVENT_NAME: ${{ github.event_name }}
deploy:
name: Deploy to staging

View File

@@ -0,0 +1,131 @@
name: Install Script Test
on:
push:
paths:
- 'scripts/install-ubuntu.sh'
- '.github/workflows/install-script-test.yml'
pull_request:
paths:
- 'scripts/install-ubuntu.sh'
- '.github/workflows/install-script-test.yml'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
install-test:
name: Test Install Script (${{ matrix.scenario }})
runs-on: ubuntu-22.04
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- scenario: default
db_pass: ''
- scenario: custom-password
db_pass: 'TestPass123!'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Make install script executable
run: chmod +x scripts/install-ubuntu.sh
- name: Run install script
env:
DB_PASS: ${{ matrix.db_pass }}
run: |
set -o pipefail
echo "Running install script with scenario: ${{ matrix.scenario }}"
sudo -E bash scripts/install-ubuntu.sh 2>&1 | tee install-output.log
echo "Install completed successfully"
- name: Wait for services to stabilize
run: sleep 10
- name: Verify Apache is running
run: |
echo "Checking Apache status..."
sudo systemctl status apache2 --no-pager
sudo systemctl is-active apache2
- name: Verify MariaDB is running
run: |
echo "Checking MariaDB status..."
sudo systemctl status mariadb --no-pager
sudo systemctl is-active mariadb
- name: Verify Apache HTTP response
run: |
echo "Testing HTTP response on port 80..."
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/)
echo "HTTP Response Code: $HTTP_CODE"
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "302" ]; then
echo "Apache is responding correctly"
elif [ "$HTTP_CODE" = "500" ]; then
echo "HTTP 500 - Application error. Checking .env configuration..."
sudo cat /var/www/ospos/.env 2>/dev/null | grep -E "database\.default\.(hostname|database|username|password)|encryption\.key|CI_ENVIRONMENT" | head -10
sudo cat /var/www/ospos/writable/logs/*.log 2>/dev/null | tail -20 || true
curl -s http://localhost/ | head -50
exit 1
else
echo "Unexpected HTTP code: $HTTP_CODE"
exit 1
fi
- name: Verify OSPOS login page
run: |
echo "Checking OSPOS login page..."
curl -s http://localhost/ | grep -qi "login\|password\|username" && echo "Login page content found" || {
echo "Login page verification failed"
curl -s http://localhost/ | head -50
exit 1
}
- name: Verify database exists
env:
DB_PASS: ${{ matrix.db_pass != '' && matrix.db_pass || '' }}
run: |
echo "Verifying database..."
# Extract the generated password from install output if using default
if [ -z "${{ matrix.db_pass }}" ]; then
GENERATED_PASS=$(grep -oP 'Database Password: \K[^\s]+' install-output.log || true)
if [ -n "$GENERATED_PASS" ]; then
DB_PASS="$GENERATED_PASS"
fi
fi
# Check database exists
sudo mysql -u root -e "SHOW DATABASES LIKE 'ospos';" | grep -q ospos && echo "Database 'ospos' exists" || {
echo "Database 'ospos' not found"
sudo mysql -u root -e "SHOW DATABASES;"
exit 1
}
# Check tables exist
TABLE_COUNT=$(sudo mysql -u root ospos -e "SHOW TABLES;" | wc -l)
echo "Found $TABLE_COUNT tables in database"
if [ "$TABLE_COUNT" -gt 5 ]; then
echo "Database tables verified"
else
echo "Not enough tables found"
exit 1
fi
- name: Upload install log
uses: actions/upload-artifact@v4
if: always()
with:
name: install-log-${{ matrix.scenario }}
path: install-output.log
retention-days: 7

View File

@@ -102,5 +102,73 @@ Do **not** use below command on live deployments unless you want to tear everyth
## Cloud install
If you choose DigitalOcean:
[Through this link](https://m.do.co/c/ac38c262507b), you will get a [**free $100, 60-day credit**](https://m.do.co/c/ac38c262507b). [Check the wiki](https://github.com/opensourcepos/opensourcepos/wiki/Getting-Started-installations) for further instructions on how to install the necessary components.
### Recommended: DigitalOcean
Sign up through [our referral link](https://m.do.co/c/ac38c262507b) to get a [**$100, 60-day credit**](https://m.do.co/c/ac38c262507b).
1. Create an Ubuntu 20.04+ or 22.04+ droplet
2. SSH into your server: `ssh root@<your-droplet-ip>`
3. Run the one-line installer:
```bash
curl -sSL https://opensourcepos.org/install | sudo bash
```
The installer will:
- Install Apache, MariaDB, PHP 8.2 and required extensions
- Download the **latest stable release** of OSPOS from GitHub
- Create a database with secure random password
- Configure OSPOS and Apache
- **Set up SSL/TLS certificates** (interactive prompt or environment variables)
- Display login credentials after completion
**Interactive Mode (Recommended for first-time users):**
When run without environment variables, the installer will prompt you:
1. Whether to configure SSL (recommended for production)
2. Your domain name (e.g., `pos.example.com`)
3. Your email for Let's Encrypt (for production SSL)
```bash
curl -sSL https://opensourcepos.org/install | sudo bash
# Script will ask:
# - Configure SSL? (y/n)
# - Domain name: pos.example.com
# - Email for Let's Encrypt: admin@example.com
```
**Non-Interactive Mode (for automation):**
```bash
# Development (no SSL)
curl -sSL https://opensourcepos.org/install | APACHE_SERVER_NAME=localhost sudo -E bash
# Production with Let's Encrypt SSL
curl -sSL https://opensourcepos.org/install | APACHE_SERVER_NAME=pos.example.com SSL_EMAIL=admin@example.com sudo -E bash
# Custom database password
curl -sSL https://opensourcepos.org/install | DB_PASS=securepassword APACHE_SERVER_NAME=pos.example.com SSL_EMAIL=admin@example.com sudo -E bash
```
**Environment variables:**
- `DB_HOST` - Database host (default: localhost)
- `DB_NAME` - Database name (default: ospos)
- `DB_USER` - Database user (default: ospos)
- `DB_PASS` - Database password (default: auto-generated)
- `MYSQL_ROOT_PASS` - MariaDB root password (default: empty/no password)
- `OSPOS_DIR` - Installation directory (default: /var/www/ospos)
- `OSPOS_VERSION` - OSPOS version to install (default: latest stable release)
- `PHP_VERSION` - PHP version (default: 8.2)
- `APACHE_SERVER_NAME` - Server hostname (default: localhost, or set interactively)
- `SSL_EMAIL` - Email for Let's Encrypt. When set, enables production SSL with auto-renewal
- `SSL_DOMAIN` - Alternative to `APACHE_SERVER_NAME` for SSL certificate domain
> **Testing:** This installer is tested with each commit via our CI workflow. A fresh Ubuntu container is spawned, the script runs to completion, and basic sanity checks verify the installation. For production deployments, we recommend testing on a staging server first. If you encounter issues, please [open an issue](https://github.com/opensourcepos/opensourcepos/issues/new?template=bug_report.yml) with your server version and error output.
> **Note:** If the short URL is unavailable, use the direct GitHub URL:
> ```bash
> curl -sSL https://raw.githubusercontent.com/opensourcepos/opensourcepos/master/scripts/install-ubuntu.sh | sudo bash
> ```
For other cloud providers or manual installation, see the [detailed installation guide](https://github.com/opensourcepos/opensourcepos/wiki/Getting-Started-installations) in the wiki.
**Important:** Change the default password after first login!

View File

@@ -5,7 +5,6 @@ namespace Config;
use App\Models\Appconfig;
use CodeIgniter\Cache\CacheInterface;
use CodeIgniter\Config\BaseConfig;
use Config\Database;
/**
* This class holds the configuration options stored from the database so that on launch those settings can be cached
@@ -14,7 +13,7 @@ use Config\Database;
*/
class OSPOS extends BaseConfig
{
public array $settings = [];
public array $settings;
public string $commit_sha1 = 'dev'; // TODO: Travis scripts need to be updated to replace this with the commit hash on build
private CacheInterface $cache;
@@ -34,37 +33,28 @@ class OSPOS extends BaseConfig
if ($cache) {
$this->settings = decode_array($cache);
return;
}
try {
$db = Database::connect();
if (!$db->tableExists('app_config')) {
$this->settings = $this->getDefaultSettings();
return;
} else {
try {
$appconfig = model(Appconfig::class);
foreach ($appconfig->get_all()->getResult() as $app_config) {
$this->settings[$app_config->key] = $app_config->value;
}
$this->cache->save('settings', encode_array($this->settings));
} catch (\Exception $e) {
// Database table doesn't exist yet (migrations haven't run)
// or database connection failed. Return empty settings to
// allow migration page to display. Catches mysqli_sql_exception
// which is not a subclass of DatabaseException.
$this->settings = [
'language' => 'english',
'language_code' => 'en',
'company' => 'Home',
'barcode_type' => 'Code39'
];
}
$appconfig = model(Appconfig::class);
foreach ($appconfig->get_all()->getResult() as $app_config) {
$this->settings[$app_config->key] = $app_config->value;
}
$this->cache->save('settings', encode_array($this->settings));
} catch (\Exception $e) {
$this->settings = $this->getDefaultSettings();
}
}
private function getDefaultSettings(): array
{
return [
'language' => 'english',
'language_code' => 'en',
'company' => 'Home',
'barcode_type' => 'Code39'
];
}
/**
* @return void
*/
@@ -73,4 +63,4 @@ class OSPOS extends BaseConfig
$this->cache->delete('settings');
$this->set_settings();
}
}
}

View File

@@ -1,4 +1,4 @@
<?php
<?php
use CodeIgniter\Router\RouteCollection;
@@ -12,40 +12,6 @@ $routes->get('login', 'Login::index');
$routes->post('login', 'Login::index');
$routes->post('migrate', 'Login::migrate');
$routes->get('sales', 'Sales::getIndex');
$routes->get('sales/customerDisplay', 'Sales::getCustomerDisplay');
$routes->get('sales/itemSearch', 'Sales::getItemSearch');
$routes->post('sales/selectCustomer', 'Sales::postSelectCustomer');
$routes->post('sales/changeMode', 'Sales::postChangeMode');
$routes->post('sales/setComment', 'Sales::postSetComment');
$routes->post('sales/setInvoiceNumber', 'Sales::postSetInvoiceNumber');
$routes->post('sales/setPaymentType', 'Sales::postSetPaymentType');
$routes->post('sales/setPrintAfterSale', 'Sales::postSetPrintAfterSale');
$routes->post('sales/setPriceWorkOrders', 'Sales::postSetPriceWorkOrders');
$routes->post('sales/setEmailReceipt', 'Sales::postSetEmailReceipt');
$routes->post('sales/addPayment', 'Sales::postAddPayment');
$routes->post('sales/add', 'Sales::postAdd');
$routes->post('sales/editItem/(:segment)', 'Sales::postEditItem/$1');
$routes->post('sales/deleteItem/(:segment)', 'Sales::getDeleteItem/$1');
$routes->post('sales/deletePayment/(:segment)', 'Sales::getDeletePayment/$1');
$routes->post('sales/removeCustomer', 'Sales::getRemoveCustomer');
$routes->post('sales/complete', 'Sales::postComplete');
$routes->post('sales/cancel', 'Sales::postCancel');
$routes->post('sales/suspend', 'Sales::postSuspend');
$routes->post('sales/unsuspend', 'Sales::postUnsuspend');
$routes->post('sales/checkInvoiceNumber', 'Sales::postCheckInvoiceNumber');
$routes->post('sales/changeItemNumber', 'Sales::postChangeItemNumber');
$routes->post('sales/changeItemName', 'Sales::postChangeItemName');
$routes->post('sales/changeItemDescription', 'Sales::postChangeItemDescription');
$routes->get('sales/suspended', 'Sales::getSuspended');
$routes->get('sales/discardSuspendedSale', 'Sales::getDiscardSuspendedSale');
$routes->get('sales/sales_keyboard_help', 'Sales::getSalesKeyboardHelp');
$routes->get('sales/receipt/(:num)', 'Sales::getReceipt/$1');
$routes->get('sales/invoice/(:num)', 'Sales::getInvoice/$1');
$routes->get('sales/edit/(:num)', 'Sales::getEdit/$1');
$routes->post('sales/delete/(:num)', 'Sales::postDelete/$1');
$routes->post('sales/save/(:num)', 'Sales::postSave/$1');
$routes->add('no_access/index/(:segment)', 'No_access::index/$1');
$routes->add('no_access/index/(:segment)/(:segment)', 'No_access::index/$1/$2');
@@ -73,4 +39,4 @@ $routes->add('reports/specific_(:any)/(:any)/(:any)/(:any)', 'Reports::Specific_
$routes->add('reports/specific_customers', 'Reports::specific_customer_input');
$routes->add('reports/specific_employees', 'Reports::specific_employee_input');
$routes->add('reports/specific_discounts', 'Reports::specific_discount_input');
$routes->add('reports/specific_suppliers', 'Reports::specific_supplier_input');
$routes->add('reports/specific_suppliers', 'Reports::specific_supplier_input');

View File

File diff suppressed because it is too large Load Diff

View File

@@ -49,13 +49,6 @@ class Login extends BaseController
return view('login', $data);
}
if (!$data['is_latest'] || $data['is_new_install']) {
set_time_limit(3600);
$migration->setNamespace('App')->latest();
return redirect()->to('login');
}
$rules = ['username' => 'required|login_check[data]'];
$messages = [
'username' => [
@@ -69,6 +62,13 @@ class Login extends BaseController
return view('login', $data);
}
if (!$data['is_latest']) {
set_time_limit(3600);
$migration->setNamespace('App')->latest();
return redirect()->to('login');
}
}
return redirect()->to('home');
@@ -79,18 +79,18 @@ class Login extends BaseController
try {
$migration = new MY_Migration(config('Migrations'));
$migration->migrate_to_ci4();
set_time_limit(3600);
$migration->setNamespace('App')->latest();
return $this->response->setJSON([
'success' => true,
'message' => 'Migration completed successfully'
]);
} catch (\Exception $e) {
log_message('error', 'Migration failed: ' . $e->getMessage());
return $this->response->setJSON([
'success' => false,
'message' => 'Migration failed: ' . $e->getMessage()

View File

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,6 @@
namespace App\Database\Migrations;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\Migration;
class Migration_Upgrade_To_3_1_1 extends Migration
@@ -18,37 +17,7 @@ class Migration_Upgrade_To_3_1_1 extends Migration
public function up(): void
{
helper('migration');
// MariaDB blocks CONVERT TO CHARACTER SET on tables with FK constraints.
// Drop all FKs across affected tables before running the SQL script, recreate after.
$fkColumns = [
['modules', 'module_id'],
['stock_locations', 'location_id'],
['permissions', 'permission_id'],
['people', 'person_id'],
['suppliers', 'supplier_id'],
['items', 'item_id'],
['item_kits', 'item_kit_id'],
['sales', 'sale_id'],
['receivings', 'receiving_id'],
['employees', 'employee_id'],
['customers', 'person_id'],
];
$constraints = [];
foreach ($fkColumns as [$table, $column]) {
foreach (dropAllForeignKeyConstraints($table, $column) as $c) {
$constraints[$c['constraintName']] = $c;
}
}
if (!execute_script(APPPATH . 'Database/Migrations/sqlscripts/3.0.2_to_3.1.1.sql')) {
throw new DatabaseException('Migration script 3.0.2_to_3.1.1.sql failed. Check logs for details.');
}
$droppedTables = ['sales_suspended', 'sales_suspended_items', 'sales_suspended_items_taxes', 'sales_suspended_payments'];
$toRecreate = array_filter($constraints, fn($c) => !in_array($c['tableName'], $droppedTables, true));
recreateForeignKeyConstraints(array_values($toRecreate));
execute_script(APPPATH . 'Database/Migrations/sqlscripts/3.0.2_to_3.1.1.sql');
}
/**

View File

@@ -327,6 +327,19 @@ INSERT INTO `ospos_sales_items` (sale_id, item_id, description, serialnumber, li
INSERT INTO `ospos_sales_payments` (sale_id, payment_type, payment_amount) SELECT sale_id, payment_type, payment_amount FROM `ospos_sales_suspended_payments`;
INSERT INTO `ospos_sales_items_taxes` (sale_id, item_id, line, name, percent) SELECT sale_id, item_id, line, name, percent FROM `ospos_sales_suspended_items_taxes`;
ALTER TABLE `ospos_sales_suspended_payments` DROP FOREIGN KEY `ospos_sales_suspended_payments_ibfk_1`;
ALTER TABLE `ospos_sales_suspended_items_taxes` DROP FOREIGN KEY `ospos_sales_suspended_items_taxes_ibfk_1`;
ALTER TABLE `ospos_sales_suspended_items_taxes` DROP FOREIGN KEY `ospos_sales_suspended_items_taxes_ibfk_2`;
ALTER TABLE `ospos_sales_suspended_items` DROP FOREIGN KEY `ospos_sales_suspended_items_ibfk_1`;
ALTER TABLE `ospos_sales_suspended_items` DROP FOREIGN KEY `ospos_sales_suspended_items_ibfk_2`;
ALTER TABLE `ospos_sales_suspended_items` DROP FOREIGN KEY `ospos_sales_suspended_items_ibfk_3`;
ALTER TABLE `ospos_sales_suspended` DROP FOREIGN KEY `ospos_sales_suspended_ibfk_1`;
ALTER TABLE `ospos_sales_suspended` DROP FOREIGN KEY `ospos_sales_suspended_ibfk_2`;
ALTER TABLE `ospos_sales_suspended` DROP FOREIGN KEY `ospos_sales_suspended_ibfk_3`;
DROP TABLE `ospos_sales_suspended_payments`, `ospos_sales_suspended_items_taxes`, `ospos_sales_suspended_items`, `ospos_sales_suspended`;
--

View File

@@ -140,7 +140,7 @@ CREATE TABLE IF NOT EXISTS `ospos_expense_categories` (
`category_name` varchar(255) DEFAULT NULL,
`category_description` varchar(255) NOT NULL,
`deleted` int(1) NOT NULL DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Table structure for table `ospos_expenses`
@@ -154,7 +154,7 @@ CREATE TABLE IF NOT EXISTS `ospos_expenses` (
`description` varchar(255) NOT NULL,
`employee_id` int(10) NOT NULL,
`deleted` int(1) NOT NULL DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Indexes for table `ospos_expense_categories`

View File

@@ -75,7 +75,7 @@ CREATE TABLE `ospos_cash_up` (
`open_employee_id` int(10) NOT NULL,
`close_employee_id` int(10) NOT NULL,
`deleted` int(1) NOT NULL DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Indexes for table `ospos_cash_up`

View File

@@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS `ospos_tax_codes` (
`state` varchar(255) NOT NULL DEFAULT '',
`deleted` int(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`tax_code_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
ALTER TABLE `ospos_customers`
ADD COLUMN `tax_id` varchar(32) NOT NULL DEFAULT '' AFTER `taxable`,
@@ -59,7 +59,7 @@ CREATE TABLE `ospos_sales_taxes` (
`rounding_code` tinyint(2) NOT NULL DEFAULT 0,
PRIMARY KEY (`sales_taxes_id`),
KEY `print_sequence` (`sale_id`,`print_sequence`,`tax_group`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE IF NOT EXISTS `ospos_tax_jurisdictions` (
`jurisdiction_id` int(11) NOT NULL AUTO_INCREMENT,
@@ -71,7 +71,7 @@ CREATE TABLE IF NOT EXISTS `ospos_tax_jurisdictions` (
`cascade_sequence` tinyint(2) NOT NULL DEFAULT 0,
`deleted` int(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`jurisdiction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci AUTO_INCREMENT=1;
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;
ALTER TABLE `ospos_suppliers`
ADD COLUMN `tax_id` varchar(32) DEFAULT NULL AFTER `account_number`;
@@ -89,7 +89,7 @@ CREATE TABLE IF NOT EXISTS `ospos_tax_rates` (
`tax_rate` decimal(15,4) NOT NULL DEFAULT 0.0000,
`tax_rounding_code` tinyint(2) NOT NULL DEFAULT 0,
PRIMARY KEY (`tax_rate_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Add support for sales tax report

View File

@@ -12,7 +12,7 @@ CREATE TABLE `ospos_sales_payments` (
`reference_code` varchar(40) NOT NULL DEFAULT '',
PRIMARY KEY (`payment_id`),
KEY `payment_sale` (`sale_id`, `payment_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO ospos_sales_payments (sale_id, payment_type, payment_amount, payment_user)
SELECT payments.sale_id, payments.payment_type, payments.payment_amount, sales.employee_id

View File

@@ -272,6 +272,9 @@ function get_payment_options(): array
$payments[lang('Sales.upi')] = lang('Sales.upi');
}
$payments[lang('Sales.bank_transfer')] = lang('Sales.bank_transfer');
$payments[lang('Sales.wallet')] = lang('Sales.wallet');
return $payments;
}
@@ -365,74 +368,6 @@ function to_currency_no_money(?string $number): string
return to_decimals($number, 'currency_decimals');
}
/**
* Build the secondary currency rendering context from app config values.
*
* @param array $config
* @return array{show:bool,rate:float,symbol:string,code:string,decimals:int}
*/
function secondary_currency_context(array $config): array
{
$rate = (float) ($config['secondary_currency_rate'] ?? 0);
$symbol = trim((string) ($config['secondary_currency_symbol'] ?? ''));
$code = trim((string) ($config['secondary_currency_code'] ?? ''));
$decimals = (int) ($config['secondary_currency_decimals'] ?? ($config['currency_decimals'] ?? DEFAULT_PRECISION));
return [
'show' => (($config['secondary_currency_enabled'] ?? false) == 1) && $rate > 0,
'rate' => $rate,
'symbol' => $symbol,
'code' => $code,
'decimals' => $decimals,
];
}
/**
* Render a value in the secondary currency.
*
* @param float|int|string|null $number
* @param array{show:bool,rate:float,symbol:string,code:string,decimals:int} $secondaryCurrency
* @return string
*/
function to_secondary_currency(float|int|string|null $number, array $secondaryCurrency): string
{
if (!isset($number) || !$secondaryCurrency['show']) {
return '';
}
$config = config(OSPOS::class)->settings;
$amount = (float) $number * (float) $secondaryCurrency['rate'];
$fmt = new NumberFormatter($config['number_locale'], NumberFormatter::CURRENCY);
$fmt->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $secondaryCurrency['decimals']);
$fmt->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $secondaryCurrency['decimals']);
if (empty($config['thousands_separator'])) {
$fmt->setTextAttribute(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, '');
}
$fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL, $secondaryCurrency['symbol'] !== '' ? $secondaryCurrency['symbol'] : ($secondaryCurrency['code'] !== '' ? $secondaryCurrency['code'] : ''));
return $fmt->format($amount);
}
/**
* Render the secondary and primary currency amounts together.
*
* @param float|int|string|null $number
* @param array{show:bool,rate:float,symbol:string,code:string,decimals:int} $secondaryCurrency
* @return string
*/
function to_secondary_currency_dual(float|int|string|null $number, array $secondaryCurrency): string
{
$secondary = to_secondary_currency($number, $secondaryCurrency);
if ($secondary === '') {
return to_currency((string) $number);
}
return $secondary . '<br>' . to_currency((string) $number);
}
/**
* @param string|null $number
* @return string

View File

@@ -172,7 +172,6 @@ function dropAllForeignKeyConstraints(string $table, string $column): array {
WHERE kcu.TABLE_SCHEMA = DATABASE()
AND ((kcu.REFERENCED_TABLE_NAME = '" . $db->getPrefix() . "$table' AND kcu.REFERENCED_COLUMN_NAME = '$column')
OR (kcu.TABLE_NAME = '" . $db->getPrefix() . "$table' AND kcu.COLUMN_NAME = '$column'))
AND rc.CONSTRAINT_NAME IS NOT NULL
");
$deletedConstraints = [];

View File

@@ -1,344 +1,336 @@
<?php
return [
"address" => "Company Address",
"address_required" => "Company address is a required field.",
"all_set" => "All file permissions are set correctly!",
"allow_duplicate_barcodes" => "Allow Duplicate Barcodes",
"apostrophe" => "apostrophe",
"backup_button" => "Backup",
"backup_database" => "Backup Database",
"barcode" => "Barcode",
"barcode_company" => "Company Name",
"barcode_configuration" => "Barcode Configuration",
"barcode_content" => "Barcode Content",
"barcode_first_row" => "Row 1",
"barcode_font" => "Font",
"barcode_formats" => "Input Formats",
"barcode_generate_if_empty" => "Generate if empty.",
"barcode_height" => "Height (px)",
"barcode_id" => "Item Id/Name",
"barcode_info" => "Barcode Configuration Information",
"barcode_layout" => "Barcode Layout",
"barcode_name" => "Name",
"barcode_number" => "Barcode",
"barcode_number_in_row" => "Number in row",
"barcode_page_cellspacing" => "Display page cellspacing.",
"barcode_page_width" => "Display page width",
"barcode_price" => "Price",
"barcode_second_row" => "Row 2",
"barcode_third_row" => "Row 3",
"barcode_tooltip" => "Warning: This feature can cause duplicate items to be imported or created. Do not use if you do not want duplicate barcodes.",
"barcode_type" => "Barcode Type",
"barcode_width" => "Width (px)",
"bottom" => "Bottom",
"cash_button" => "",
"cash_button_1" => "",
"cash_button_2" => "",
"cash_button_3" => "",
"cash_button_4" => "",
"cash_button_5" => "",
"cash_button_6" => "",
"cash_decimals" => "Cash Decimals",
"cash_decimals_tooltip" => "If Cash Decimals and Currency Decimals are the same then no cash triggered rounding will take place, unless Cash Rounding is set to Half Five.",
"cash_rounding" => "Cash Rounding",
"category_dropdown" => "Show Category as a dropdown",
"center" => "Center",
"change_apperance_tooltip" => "",
"comma" => "comma",
"company" => "Company Name",
"company_avatar" => "",
"company_change_image" => "Change Image",
"company_logo" => "Company Logo",
"company_remove_image" => "Remove Image",
"company_required" => "Company name is a required field",
"company_select_image" => "Select Image",
"company_website_url" => "Company website is not a valid URL (http://...).",
"country_codes" => "Country Codes",
"country_codes_tooltip" => "Comma separated list of country codes for nominatim address lookup.",
"currency_code" => "Currency Code",
"currency_decimals" => "Currency Decimals",
"currency_symbol" => "Currency Symbol",
"current_employee_only" => "",
"customer_reward" => "Reward",
"customer_reward_duplicate" => "Reward must be unique.",
"customer_reward_enable" => "Enable Customer Rewards",
"customer_reward_invalid_chars" => "Reward can not contain '_'",
"customer_reward_required" => "Reward is a required field",
"customer_sales_tax_support" => "",
"date_or_time_format" => "Date and Time Filter",
"datetimeformat" => "Date and Time Format",
"decimal_point" => "Decimal Point",
"default_barcode_font_size_number" => "Default Barcode Font Size must be a number.",
"default_barcode_font_size_required" => "Default Barcode Font Size is a required field.",
"default_barcode_height_number" => "Default Barcode Height must be a number.",
"default_barcode_height_required" => "Default Barcode Height is a required field.",
"default_barcode_num_in_row_number" => "Default Barcode Number in Row must be a number.",
"default_barcode_num_in_row_required" => "Default Barcode Number in Row is a required field.",
"default_barcode_page_cellspacing_number" => "Default Barcode Page Cellspacing must be a number.",
"default_barcode_page_cellspacing_required" => "Default Barcode Page Cellspacing is a required field.",
"default_barcode_page_width_number" => "Default Barcode Page Width must be a number.",
"default_barcode_page_width_required" => "Default Barcode Page Width is a required field.",
"default_barcode_width_number" => "Default Barcode Width must be a number.",
"default_barcode_width_required" => "Default Barcode Width is a required field.",
"default_item_columns" => "Default Visible Item Columns",
"default_origin_tax_code" => "Default Origin Tax Code",
"default_receivings_discount" => "Default Receivings Discount",
"default_receivings_discount_number" => "Default Receivings Discount must be a number.",
"default_receivings_discount_required" => "Default Receivings Discount is a required field.",
"default_sales_discount" => "Default Sales Discount",
"default_sales_discount_number" => "Default Sales Discount must be a number.",
"default_sales_discount_required" => "Default Sales Discount is a required field.",
"default_tax_category" => "Default Tax Category",
"default_tax_code" => "Default Tax Code",
"default_tax_jurisdiction" => "Default Tax Jurisdiction",
"default_tax_name_number" => "Default Tax Name must be a string.",
"default_tax_name_required" => "Default Tax Name is a required field.",
"default_tax_rate" => "Default Tax Rate %",
"default_tax_rate_1" => "Tax 1 Rate",
"default_tax_rate_2" => "Tax 2 Rate",
"default_tax_rate_3" => "",
"default_tax_rate_number" => "Default Tax Rate must be a number.",
"default_tax_rate_required" => "Default Tax Rate is a required field.",
"derive_sale_quantity" => "Allow Derived Sale Quantity",
"derive_sale_quantity_tooltip" => "If checked then a new item type will be provided for items ordered by extended amount",
"dinner_table" => "Table",
"dinner_table_duplicate" => "Table must be unique.",
"dinner_table_enable" => "Enable Dinner Tables",
"dinner_table_invalid_chars" => "Table Name can not contain '_'.",
"dinner_table_required" => "Table is a required field.",
"dot" => "dot",
"email" => "Email",
"email_configuration" => "Email Configuration",
"email_mailpath" => "Path to Sendmail",
"email_protocol" => "Protocol",
"email_receipt_check_behaviour" => "Email Receipt checkbox",
"email_receipt_check_behaviour_always" => "Always checked",
"email_receipt_check_behaviour_last" => "Remember last selection",
"email_receipt_check_behaviour_never" => "Always unchecked",
"email_smtp_crypto" => "SMTP Encryption",
"email_smtp_host" => "SMTP Server",
"email_smtp_pass" => "SMTP Password",
"email_smtp_port" => "SMTP Port",
"email_smtp_timeout" => "SMTP Timeout (s)",
"email_smtp_user" => "SMTP Username",
"enable_avatar" => "",
"enable_avatar_tooltip" => "",
"enable_dropdown_tooltip" => "",
"enable_new_look" => "",
"enable_right_bar" => "",
"enable_right_bar_tooltip" => "",
"enforce_privacy" => "Enforce privacy",
"enforce_privacy_tooltip" => "Protect Customers privacy enforcing data scrambling in case of their data being deleted",
"fax" => "Fax",
"file_perm" => "There are problems with file permissions. Please fix and reload this page.",
"financial_year" => "Fiscal Year Start",
"financial_year_apr" => "1st of April",
"financial_year_aug" => "1st of August",
"financial_year_dec" => "1st of December",
"financial_year_feb" => "1st of February",
"financial_year_jan" => "1st of January",
"financial_year_jul" => "1st of July",
"financial_year_jun" => "1st of June",
"financial_year_mar" => "1st of March",
"financial_year_may" => "1st of May",
"financial_year_nov" => "1st of November",
"financial_year_oct" => "1st of October",
"financial_year_sep" => "1st of September",
"floating_labels" => "Floating Labels",
"gcaptcha_enable" => "Login Page reCAPTCHA",
"gcaptcha_secret_key" => "reCAPTCHA Secret Key",
"gcaptcha_secret_key_required" => "reCAPTCHA Secret Key is a required field",
"gcaptcha_site_key" => "reCAPTCHA Site Key",
"gcaptcha_site_key_required" => "reCAPTCHA Site Key is a required field",
"gcaptcha_tooltip" => "Protect the Login page with Google reCAPTCHA, click the icon for an API key pair.",
"general" => "General",
"general_configuration" => "General Configuration",
"giftcard_number" => "Gift Card Number",
"giftcard_random" => "Generate Random",
"giftcard_series" => "Generate in Series",
"image_allowed_file_types" => "Allowed file types",
"image_max_height_tooltip" => "Maximum allowed height of image uploads in pixels (px).",
"image_max_size_tooltip" => "Maximum allowed file size of image uploads in kilobytes (kb).",
"image_max_width_tooltip" => "Maximum allowed width of image uploads in pixels (px).",
"image_restrictions" => "Image Upload Restrictions",
"include_hsn" => "Include Support for HSN Codes",
"info" => "Information",
"info_configuration" => "Store Information",
"input_groups" => "Input Groups",
"integrations" => "Integrations",
"integrations_configuration" => "Third Party Integrations",
"invoice" => "Invoice",
"invoice_configuration" => "Invoice Print Settings",
"invoice_default_comments" => "Default Invoice Comments",
"invoice_email_message" => "Invoice Email Template",
"invoice_enable" => "Enable Invoicing",
"invoice_printer" => "Invoice Printer",
"invoice_type" => "Invoice Type",
"is_readable" => "is readable, but the permissions are incorrectly set. Please set it to 640 or 660 and refresh.",
"is_writable" => "is writable, but the permissions are incorrectly set. Please set it to 750 and refresh.",
"item_markup" => "",
"jsprintsetup_required" => "Warning: This functionality will only work if you have the FireFox jsPrintSetup addon installed. Save anyway?",
"language" => "Language",
"last_used_invoice_number" => "Last used Invoice Number",
"last_used_quote_number" => "Last used Quote Number",
"last_used_work_order_number" => "Last used W/O Number",
"left" => "Left",
"license" => "License",
"license_configuration" => "License Statement",
"line_sequence" => "Line Sequence",
"lines_per_page" => "Lines per Page",
"lines_per_page_number" => "Lines per Page must be a number.",
"lines_per_page_required" => "Lines per Page is a required field.",
"locale" => "Localization",
"locale_configuration" => "Localization Configuration",
"locale_info" => "Localization Configuration Information",
"location" => "Stock",
"location_configuration" => "Stock Locations",
"location_info" => "Location Configuration Information",
"login_form" => "Login Form Style",
"logout" => "Do you want to make a backup before logging out? Click [OK] to backup or [Cancel] to logout.",
"mailchimp" => "MailChimp",
"mailchimp_api_key" => "MailChimp API Key",
"mailchimp_configuration" => "MailChimp Configuration",
"mailchimp_key_successfully" => "API Key is valid.",
"mailchimp_key_unsuccessfully" => "API Key is invalid.",
"mailchimp_lists" => "MailChimp List(s)",
"mailchimp_tooltip" => "Click the icon for an API Key.",
"message" => "Message",
"message_configuration" => "Message Configuration",
"msg_msg" => "Saved Text Message",
"msg_msg_placeholder" => "If you wish to use a SMS template save your message here, otherwise leave the box blank.",
"msg_pwd" => "SMS-API Password",
"msg_pwd_required" => "SMS-API Password is a required field",
"msg_src" => "SMS-API Sender ID",
"msg_src_required" => "SMS-API Sender ID is a required field",
"msg_uid" => "SMS-API Username",
"msg_uid_required" => "SMS-API Username is a required field",
"multi_pack_enabled" => "Multiple Packages per Item",
"no_risk" => "No security/vulnerability risks.",
"none" => "none",
"notify_alignment" => "Notification Popup Position",
"number_format" => "Number Format",
"number_locale" => "Localization",
"number_locale_invalid" => "The entered locale is invalid. Check the link in the tooltip to find a valid locale.",
"number_locale_required" => "Number Locale is a required field.",
"number_locale_tooltip" => "Find a suitable locale through this link.",
"os_timezone" => "OSPOS Timezone:",
"ospos_info" => "OSPOS Installation Info",
"payment_options_order" => "Payment Options Order",
"perm_risk" => "Incorrect permissions leaves this software at risk.",
"phone" => "Company Phone",
"phone_required" => "Company Phone is a required field.",
"print_bottom_margin" => "Margin Bottom",
"print_bottom_margin_number" => "Margin Bottom must be a number.",
"print_bottom_margin_required" => "Margin Bottom is a required field.",
"print_delay_autoreturn" => "Autoreturn to Sale delay",
"print_delay_autoreturn_number" => "Autoreturn to Sale delay is a required field.",
"print_delay_autoreturn_required" => "Autoreturn to Sale delay must be a number.",
"print_footer" => "Print Browser Footer",
"print_header" => "Print Browser Header",
"print_left_margin" => "Margin Left",
"print_left_margin_number" => "Margin Left must be a number.",
"print_left_margin_required" => "Margin Left is a required field.",
"print_receipt_check_behaviour" => "Print Receipt checkbox",
"print_receipt_check_behaviour_always" => "Always checked",
"print_receipt_check_behaviour_last" => "Remember last selection",
"print_receipt_check_behaviour_never" => "Always unchecked",
"print_right_margin" => "Margin Right",
"print_right_margin_number" => "Margin Right must be a number.",
"print_right_margin_required" => "Margin Right is a required field.",
"print_silently" => "Show Print Dialog",
"print_top_margin" => "Margin Top",
"print_top_margin_number" => "Margin Top must be a number.",
"print_top_margin_required" => "Margin Top is a required field.",
"quantity_decimals" => "Quantity Decimals",
"quick_cash_enable" => "",
"quote_default_comments" => "Default Quote Comments",
"receipt" => "Receipt",
"receipt_category" => "",
"receipt_configuration" => "Receipt Print Settings",
"receipt_default" => "Default",
"receipt_font_size" => "Font Size",
"receipt_font_size_number" => "Font Size must be a number.",
"receipt_font_size_required" => "Font Size is a required field.",
"receipt_info" => "Receipt Configuration Information",
"receipt_printer" => "Ticket Printer",
"receipt_short" => "Short",
<?php
return [
"address" => "Company Address",
"address_required" => "Company address is a required field.",
"all_set" => "All file permissions are set correctly!",
"allow_duplicate_barcodes" => "Allow Duplicate Barcodes",
"apostrophe" => "apostrophe",
"backup_button" => "Backup",
"backup_database" => "Backup Database",
"barcode" => "Barcode",
"barcode_company" => "Company Name",
"barcode_configuration" => "Barcode Configuration",
"barcode_content" => "Barcode Content",
"barcode_first_row" => "Row 1",
"barcode_font" => "Font",
"barcode_formats" => "Input Formats",
"barcode_generate_if_empty" => "Generate if empty.",
"barcode_height" => "Height (px)",
"barcode_id" => "Item Id/Name",
"barcode_info" => "Barcode Configuration Information",
"barcode_layout" => "Barcode Layout",
"barcode_name" => "Name",
"barcode_number" => "Barcode",
"barcode_number_in_row" => "Number in row",
"barcode_page_cellspacing" => "Display page cellspacing.",
"barcode_page_width" => "Display page width",
"barcode_price" => "Price",
"barcode_second_row" => "Row 2",
"barcode_third_row" => "Row 3",
"barcode_tooltip" => "Warning: This feature can cause duplicate items to be imported or created. Do not use if you do not want duplicate barcodes.",
"barcode_type" => "Barcode Type",
"barcode_width" => "Width (px)",
"bottom" => "Bottom",
"cash_button" => "",
"cash_button_1" => "",
"cash_button_2" => "",
"cash_button_3" => "",
"cash_button_4" => "",
"cash_button_5" => "",
"cash_button_6" => "",
"cash_decimals" => "Cash Decimals",
"cash_decimals_tooltip" => "If Cash Decimals and Currency Decimals are the same then no cash triggered rounding will take place, unless Cash Rounding is set to Half Five.",
"cash_rounding" => "Cash Rounding",
"category_dropdown" => "Show Category as a dropdown",
"center" => "Center",
"change_apperance_tooltip" => "",
"comma" => "comma",
"company" => "Company Name",
"company_avatar" => "",
"company_change_image" => "Change Image",
"company_logo" => "Company Logo",
"company_remove_image" => "Remove Image",
"company_required" => "Company name is a required field",
"company_select_image" => "Select Image",
"company_website_url" => "Company website is not a valid URL (http://...).",
"country_codes" => "Country Codes",
"country_codes_tooltip" => "Comma separated list of country codes for nominatim address lookup.",
"currency_code" => "Currency Code",
"currency_decimals" => "Currency Decimals",
"currency_symbol" => "Currency Symbol",
"current_employee_only" => "",
"customer_reward" => "Reward",
"customer_reward_duplicate" => "Reward must be unique.",
"customer_reward_enable" => "Enable Customer Rewards",
"customer_reward_invalid_chars" => "Reward can not contain '_'",
"customer_reward_required" => "Reward is a required field",
"customer_sales_tax_support" => "",
"date_or_time_format" => "Date and Time Filter",
"datetimeformat" => "Date and Time Format",
"decimal_point" => "Decimal Point",
"default_barcode_font_size_number" => "Default Barcode Font Size must be a number.",
"default_barcode_font_size_required" => "Default Barcode Font Size is a required field.",
"default_barcode_height_number" => "Default Barcode Height must be a number.",
"default_barcode_height_required" => "Default Barcode Height is a required field.",
"default_barcode_num_in_row_number" => "Default Barcode Number in Row must be a number.",
"default_barcode_num_in_row_required" => "Default Barcode Number in Row is a required field.",
"default_barcode_page_cellspacing_number" => "Default Barcode Page Cellspacing must be a number.",
"default_barcode_page_cellspacing_required" => "Default Barcode Page Cellspacing is a required field.",
"default_barcode_page_width_number" => "Default Barcode Page Width must be a number.",
"default_barcode_page_width_required" => "Default Barcode Page Width is a required field.",
"default_barcode_width_number" => "Default Barcode Width must be a number.",
"default_barcode_width_required" => "Default Barcode Width is a required field.",
"default_item_columns" => "Default Visible Item Columns",
"default_origin_tax_code" => "Default Origin Tax Code",
"default_receivings_discount" => "Default Receivings Discount",
"default_receivings_discount_number" => "Default Receivings Discount must be a number.",
"default_receivings_discount_required" => "Default Receivings Discount is a required field.",
"default_sales_discount" => "Default Sales Discount",
"default_sales_discount_number" => "Default Sales Discount must be a number.",
"default_sales_discount_required" => "Default Sales Discount is a required field.",
"default_tax_category" => "Default Tax Category",
"default_tax_code" => "Default Tax Code",
"default_tax_jurisdiction" => "Default Tax Jurisdiction",
"default_tax_name_number" => "Default Tax Name must be a string.",
"default_tax_name_required" => "Default Tax Name is a required field.",
"default_tax_rate" => "Default Tax Rate %",
"default_tax_rate_1" => "Tax 1 Rate",
"default_tax_rate_2" => "Tax 2 Rate",
"default_tax_rate_3" => "",
"default_tax_rate_number" => "Default Tax Rate must be a number.",
"default_tax_rate_required" => "Default Tax Rate is a required field.",
"derive_sale_quantity" => "Allow Derived Sale Quantity",
"derive_sale_quantity_tooltip" => "If checked then a new item type will be provided for items ordered by extended amount",
"dinner_table" => "Table",
"dinner_table_duplicate" => "Table must be unique.",
"dinner_table_enable" => "Enable Dinner Tables",
"dinner_table_invalid_chars" => "Table Name can not contain '_'.",
"dinner_table_required" => "Table is a required field.",
"dot" => "dot",
"email" => "Email",
"email_configuration" => "Email Configuration",
"email_mailpath" => "Path to Sendmail",
"email_protocol" => "Protocol",
"email_receipt_check_behaviour" => "Email Receipt checkbox",
"email_receipt_check_behaviour_always" => "Always checked",
"email_receipt_check_behaviour_last" => "Remember last selection",
"email_receipt_check_behaviour_never" => "Always unchecked",
"email_smtp_crypto" => "SMTP Encryption",
"email_smtp_host" => "SMTP Server",
"email_smtp_pass" => "SMTP Password",
"email_smtp_port" => "SMTP Port",
"email_smtp_timeout" => "SMTP Timeout (s)",
"email_smtp_user" => "SMTP Username",
"enable_avatar" => "",
"enable_avatar_tooltip" => "",
"enable_dropdown_tooltip" => "",
"enable_new_look" => "",
"enable_right_bar" => "",
"enable_right_bar_tooltip" => "",
"enforce_privacy" => "Enforce privacy",
"enforce_privacy_tooltip" => "Protect Customers privacy enforcing data scrambling in case of their data being deleted",
"fax" => "Fax",
"file_perm" => "There are problems with file permissions. Please fix and reload this page.",
"financial_year" => "Fiscal Year Start",
"financial_year_apr" => "1st of April",
"financial_year_aug" => "1st of August",
"financial_year_dec" => "1st of December",
"financial_year_feb" => "1st of February",
"financial_year_jan" => "1st of January",
"financial_year_jul" => "1st of July",
"financial_year_jun" => "1st of June",
"financial_year_mar" => "1st of March",
"financial_year_may" => "1st of May",
"financial_year_nov" => "1st of November",
"financial_year_oct" => "1st of October",
"financial_year_sep" => "1st of September",
"floating_labels" => "Floating Labels",
"gcaptcha_enable" => "Login Page reCAPTCHA",
"gcaptcha_secret_key" => "reCAPTCHA Secret Key",
"gcaptcha_secret_key_required" => "reCAPTCHA Secret Key is a required field",
"gcaptcha_site_key" => "reCAPTCHA Site Key",
"gcaptcha_site_key_required" => "reCAPTCHA Site Key is a required field",
"gcaptcha_tooltip" => "Protect the Login page with Google reCAPTCHA, click the icon for an API key pair.",
"general" => "General",
"general_configuration" => "General Configuration",
"giftcard_number" => "Gift Card Number",
"giftcard_random" => "Generate Random",
"giftcard_series" => "Generate in Series",
"image_allowed_file_types" => "Allowed file types",
"image_max_height_tooltip" => "Maximum allowed height of image uploads in pixels (px).",
"image_max_size_tooltip" => "Maximum allowed file size of image uploads in kilobytes (kb).",
"image_max_width_tooltip" => "Maximum allowed width of image uploads in pixels (px).",
"image_restrictions" => "Image Upload Restrictions",
"include_hsn" => "Include Support for HSN Codes",
"info" => "Information",
"info_configuration" => "Store Information",
"input_groups" => "Input Groups",
"integrations" => "Integrations",
"integrations_configuration" => "Third Party Integrations",
"invoice" => "Invoice",
"invoice_configuration" => "Invoice Print Settings",
"invoice_default_comments" => "Default Invoice Comments",
"invoice_email_message" => "Invoice Email Template",
"invoice_enable" => "Enable Invoicing",
"invoice_printer" => "Invoice Printer",
"invoice_type" => "Invoice Type",
"is_readable" => "is readable, but the permissions are incorrectly set. Please set it to 640 or 660 and refresh.",
"is_writable" => "is writable, but the permissions are incorrectly set. Please set it to 750 and refresh.",
"item_markup" => "",
"jsprintsetup_required" => "Warning: This functionality will only work if you have the FireFox jsPrintSetup addon installed. Save anyway?",
"language" => "Language",
"last_used_invoice_number" => "Last used Invoice Number",
"last_used_quote_number" => "Last used Quote Number",
"last_used_work_order_number" => "Last used W/O Number",
"left" => "Left",
"license" => "License",
"license_configuration" => "License Statement",
"line_sequence" => "Line Sequence",
"lines_per_page" => "Lines per Page",
"lines_per_page_number" => "Lines per Page must be a number.",
"lines_per_page_required" => "Lines per Page is a required field.",
"locale" => "Localization",
"locale_configuration" => "Localization Configuration",
"locale_info" => "Localization Configuration Information",
"location" => "Stock",
"location_configuration" => "Stock Locations",
"location_info" => "Location Configuration Information",
"login_form" => "Login Form Style",
"logout" => "Do you want to make a backup before logging out? Click [OK] to backup or [Cancel] to logout.",
"mailchimp" => "MailChimp",
"mailchimp_api_key" => "MailChimp API Key",
"mailchimp_configuration" => "MailChimp Configuration",
"mailchimp_key_successfully" => "API Key is valid.",
"mailchimp_key_unsuccessfully" => "API Key is invalid.",
"mailchimp_lists" => "MailChimp List(s)",
"mailchimp_tooltip" => "Click the icon for an API Key.",
"message" => "Message",
"message_configuration" => "Message Configuration",
"msg_msg" => "Saved Text Message",
"msg_msg_placeholder" => "If you wish to use a SMS template save your message here, otherwise leave the box blank.",
"msg_pwd" => "SMS-API Password",
"msg_pwd_required" => "SMS-API Password is a required field",
"msg_src" => "SMS-API Sender ID",
"msg_src_required" => "SMS-API Sender ID is a required field",
"msg_uid" => "SMS-API Username",
"msg_uid_required" => "SMS-API Username is a required field",
"multi_pack_enabled" => "Multiple Packages per Item",
"no_risk" => "No security/vulnerability risks.",
"none" => "none",
"notify_alignment" => "Notification Popup Position",
"number_format" => "Number Format",
"number_locale" => "Localization",
"number_locale_invalid" => "The entered locale is invalid. Check the link in the tooltip to find a valid locale.",
"number_locale_required" => "Number Locale is a required field.",
"number_locale_tooltip" => "Find a suitable locale through this link.",
"os_timezone" => "OSPOS Timezone:",
"ospos_info" => "OSPOS Installation Info",
"payment_options_order" => "Payment Options Order",
"perm_risk" => "Incorrect permissions leaves this software at risk.",
"phone" => "Company Phone",
"phone_required" => "Company Phone is a required field.",
"print_bottom_margin" => "Margin Bottom",
"print_bottom_margin_number" => "Margin Bottom must be a number.",
"print_bottom_margin_required" => "Margin Bottom is a required field.",
"print_delay_autoreturn" => "Autoreturn to Sale delay",
"print_delay_autoreturn_number" => "Autoreturn to Sale delay is a required field.",
"print_delay_autoreturn_required" => "Autoreturn to Sale delay must be a number.",
"print_footer" => "Print Browser Footer",
"print_header" => "Print Browser Header",
"print_left_margin" => "Margin Left",
"print_left_margin_number" => "Margin Left must be a number.",
"print_left_margin_required" => "Margin Left is a required field.",
"print_receipt_check_behaviour" => "Print Receipt checkbox",
"print_receipt_check_behaviour_always" => "Always checked",
"print_receipt_check_behaviour_last" => "Remember last selection",
"print_receipt_check_behaviour_never" => "Always unchecked",
"print_right_margin" => "Margin Right",
"print_right_margin_number" => "Margin Right must be a number.",
"print_right_margin_required" => "Margin Right is a required field.",
"print_silently" => "Show Print Dialog",
"print_top_margin" => "Margin Top",
"print_top_margin_number" => "Margin Top must be a number.",
"print_top_margin_required" => "Margin Top is a required field.",
"quantity_decimals" => "Quantity Decimals",
"quick_cash_enable" => "",
"quote_default_comments" => "Default Quote Comments",
"receipt" => "Receipt",
"receipt_category" => "",
"receipt_configuration" => "Receipt Print Settings",
"receipt_default" => "Default",
"receipt_font_size" => "Font Size",
"receipt_font_size_number" => "Font Size must be a number.",
"receipt_font_size_required" => "Font Size is a required field.",
"receipt_info" => "Receipt Configuration Information",
"receipt_printer" => "Ticket Printer",
"receipt_short" => "Short",
"receipt_show_company_name" => "Show Company Name",
"receipt_show_description" => "Show Description",
"receipt_show_serialnumber" => "Show Serial Number",
"receipt_show_secondary_currency" => "Show Secondary Currency",
"receipt_show_tax_ind" => "Show Tax Indicator",
"receipt_show_taxes" => "Show Taxes",
"receipt_show_total_discount" => "Show Total Discount",
"receipt_template" => "Receipt Template",
"secondary_currency" => "Secondary Currency",
"secondary_currency_decimals" => "Secondary Currency Decimals",
"secondary_currency_code" => "Secondary Currency Code",
"secondary_currency_enable" => "Enable Secondary Currency",
"secondary_currency_enable_tooltip" => "Show secondary currency fields and print/display values across the app.",
"secondary_currency_rate" => "Secondary Currency Rate",
"secondary_currency_settings" => "Secondary Currency Settings",
"secondary_currency_symbol" => "Secondary Currency Symbol",
"receiving_calculate_average_price" => "Calc avg. Price (Receiving)",
"recv_invoice_format" => "Receivings Invoice Format",
"register_mode_default" => "Default Register Mode",
"report_an_issue" => "Report an issue",
"return_policy_required" => "Return policy is a required field.",
"reward" => "Reward",
"reward_configuration" => "Reward Configuration",
"right" => "Right",
"sales_invoice_format" => "Sales Invoice Format",
"sales_quote_format" => "Sales Quote Format",
"mailpath_invalid" => "Invalid sendmail path. Only letters, numbers, dashes, underscores, slashes and dots are allowed.",
"saved_successfully" => "Configuration save successful.",
"saved_unsuccessfully" => "Configuration save failed.",
"security_issue" => "Security Vulnerability Warning",
"recv_invoice_format" => "Receivings Invoice Format",
"register_mode_default" => "Default Register Mode",
"report_an_issue" => "Report an issue",
"return_policy_required" => "Return policy is a required field.",
"reward" => "Reward",
"reward_configuration" => "Reward Configuration",
"right" => "Right",
"sales_invoice_format" => "Sales Invoice Format",
"sales_quote_format" => "Sales Quote Format",
"mailpath_invalid" => "Invalid sendmail path. Only letters, numbers, dashes, underscores, slashes and dots are allowed.",
"saved_successfully" => "Configuration save successful.",
"saved_unsuccessfully" => "Configuration save failed.",
"security_issue" => "Security Vulnerability Warning",
"server_notice" => "Please use the below info for issue reporting.",
"service_charge" => "",
"customer_display" => "Customer Display",
"show_due_enable" => "",
"show_office_group" => "Show office icon",
"statistics" => "Send Statistics",
"statistics_tooltip" => "Send statistics for development and feature improvement purposes.",
"stock_location" => "Stock location",
"stock_location_duplicate" => "Stock Location must be unique.",
"stock_location_invalid_chars" => "Stock Location can not contain '_'.",
"stock_location_required" => "Stock location is a required field.",
"suggestions_fifth_column" => "",
"suggestions_first_column" => "Column 1",
"suggestions_fourth_column" => "",
"suggestions_layout" => "Search Suggestions Layout",
"suggestions_second_column" => "Column 2",
"suggestions_third_column" => "Column 3",
"system_conf" => "Setup & Conf",
"system_info" => "System Info",
"table" => "Table",
"table_configuration" => "Table Configuration",
"takings_printer" => "Receipt Printer",
"tax" => "Tax",
"tax_category" => "Tax Category",
"tax_category_duplicate" => "The entered tax category already exists.",
"tax_category_invalid_chars" => "The entered tax category is invalid.",
"tax_category_required" => "The tax category is required.",
"tax_category_used" => "Tax category cannot be deleted because it is being used.",
"tax_configuration" => "Tax Configuration",
"tax_decimals" => "Tax Decimals",
"tax_id" => "Tax Id",
"tax_included" => "Tax Included",
"theme" => "Theme",
"theme_preview" => "Preview Theme:",
"thousands_separator" => "Thousands Separator",
"timezone" => "Timezone",
"timezone_error" => "OSPOS Timezone is Different from your Local Timezone.",
"top" => "Top",
"use_destination_based_tax" => "Use Destination Based Tax",
"user_timezone" => "Local Timezone:",
"website" => "Website",
"wholesale_markup" => "",
"work_order_enable" => "Work Order Support",
"work_order_format" => "Work Order Format",
];
"show_office_group" => "Show office icon",
"statistics" => "Send Statistics",
"statistics_tooltip" => "Send statistics for development and feature improvement purposes.",
"stock_location" => "Stock location",
"stock_location_duplicate" => "Stock Location must be unique.",
"stock_location_invalid_chars" => "Stock Location can not contain '_'.",
"stock_location_required" => "Stock location is a required field.",
"suggestions_fifth_column" => "",
"suggestions_first_column" => "Column 1",
"suggestions_fourth_column" => "",
"suggestions_layout" => "Search Suggestions Layout",
"suggestions_second_column" => "Column 2",
"suggestions_third_column" => "Column 3",
"shortcuts" => "Shortcuts",
"shortcuts_configuration" => "Sales Keyboard Shortcut Configuration",
"shortcuts_duplicate_bindings" => "Shortcut bindings must be unique.",
"shortcuts_save_error" => "Unable to save shortcut settings.",
"system_conf" => "Setup & Conf",
"system_info" => "System Info",
"table" => "Table",
"table_configuration" => "Table Configuration",
"takings_printer" => "Receipt Printer",
"tax" => "Tax",
"tax_category" => "Tax Category",
"tax_category_duplicate" => "The entered tax category already exists.",
"tax_category_invalid_chars" => "The entered tax category is invalid.",
"tax_category_required" => "The tax category is required.",
"tax_category_used" => "Tax category cannot be deleted because it is being used.",
"tax_configuration" => "Tax Configuration",
"tax_decimals" => "Tax Decimals",
"tax_id" => "Tax Id",
"tax_included" => "Tax Included",
"theme" => "Theme",
"theme_preview" => "Preview Theme:",
"thousands_separator" => "Thousands Separator",
"timezone" => "Timezone",
"timezone_error" => "OSPOS Timezone is Different from your Local Timezone.",
"top" => "Top",
"use_destination_based_tax" => "Use Destination Based Tax",
"user_timezone" => "Local Timezone:",
"website" => "Website",
"wholesale_markup" => "",
"work_order_enable" => "Work Order Support",
"work_order_format" => "Work Order Format",
];

View File

@@ -7,9 +7,9 @@ return [
"account_number" => "Account #",
"add_payment" => "Add Payment",
"amount_due" => "Amount Due",
"amount_due_lbp" => "Amount Due LBP",
"amount_tendered" => "Amount Tendered",
"authorized_signature" => "Authorized Signature",
"bank_transfer" => "Bank Transfer",
"cancel_sale" => "Cancel",
"cash" => "Cash",
"cash_1" => "",
@@ -20,8 +20,6 @@ return [
"cash_deposit" => "Cash Deposit",
"cash_filter" => "Cash",
"change_due" => "Change Due",
"change" => "Change",
"currency_rate" => "Currency Rate",
"change_price" => "Change Selling Price",
"check" => "Check",
"check_balance" => "Check remainder",
@@ -43,7 +41,6 @@ return [
"customer_address" => "Address",
"customer_discount" => "Discount",
"customer_email" => "Email",
"customer_name" => "Customer Name",
"customer_location" => "Location",
"customer_mailchimp_status" => "MailChimp Status",
"customer_optional" => "(Required for Due Payments)",
@@ -77,6 +74,12 @@ return [
"employee" => "Employee",
"entry" => "Entry",
"error_editing_item" => "Error editing item",
"negative_price_invalid" => "Price cannot be negative.",
"negative_quantity_invalid" => "Quantity cannot be negative.",
"negative_discount_invalid" => "Discount cannot be negative.",
"discount_percent_exceeds_100" => "Percentage discount cannot exceed 100%.",
"discount_exceeds_item_total" => "Discount cannot exceed the item total.",
"negative_total_invalid" => "Sale total cannot be negative. Check item discounts and quantities.",
"find_or_scan_item" => "Find or Scan Item",
"find_or_scan_item_or_receipt" => "Find or Scan Item or Receipt",
"giftcard" => "Gift Card",
@@ -107,7 +110,6 @@ return [
"item_name" => "Item Name",
"item_number" => "Item #",
"item_out_of_stock" => "Item is out of stock.",
"items" => "Items",
"key_browser" => "Helpful Shortcuts",
"key_cancel" => "Cancels Current Quote/Invoice/Sale",
"key_customer_search" => "Customer Search",
@@ -149,9 +151,7 @@ return [
"payment_type" => "Type",
"payments" => "",
"payments_total" => "Payments Total",
"loyalty_reward_points" => "Loyalty Reward Points",
"price" => "Price",
"price_with_currency" => "Price (%s)",
"print_after_sale" => "Print after Sale",
"quantity" => "Quantity",
"quantity_less_than_reorder_level" => "Warning: Desired Quantity is below Reorder Level for that Item.",
@@ -167,13 +167,10 @@ return [
"receipt_number" => "Sale #",
"receipt_sent" => "Receipt sent to",
"receipt_unsent" => "Receipt failed to be sent to",
"rate" => "Rate",
"refund" => "Refund Type",
"register" => "Sales Register",
"remove_customer" => "Remove Customer",
"remove_discount" => "",
"customer_display" => "Customer Display",
"summary" => "Summary",
"return" => "Return",
"rewards" => "Reward Points",
"rewards_balance" => "Reward Points Balance",
@@ -185,7 +182,6 @@ return [
"sales_total" => "",
"select_customer" => "Select Customer",
"selected_customer" => "Selected Customer",
"walk_in_customer" => "Walk-in Customer",
"send_invoice" => "Send Invoice",
"send_quote" => "Send Quote",
"send_receipt" => "Send Receipt",
@@ -216,7 +212,6 @@ return [
"tax_percent" => "Tax %",
"taxed_ind" => "T",
"total" => "Total",
"total_lbp" => "Total LBP",
"total_tax_exclusive" => "Tax excluded",
"transaction_failed" => "Sales Transaction failed.",
"unable_to_add_item" => "Item add to Sale failed",
@@ -229,6 +224,7 @@ return [
"update" => "Update",
"upi" => "UPI",
"visa" => "",
"wallet" => "Wallet",
"wholesale" => "",
"work_order" => "Work Order",
"work_order_number" => "Work Order Number",
@@ -236,5 +232,3 @@ return [
"work_order_sent" => "Work Order sent to",
"work_order_unsent" => "Work Order failed to be sent to",
];

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,249 +0,0 @@
<?php
/**
* @var array $config
* @var string $companyName
* @var string $companyDetails
*/
helper('url');
?>
<!doctype html>
<html lang="<?= esc(service('request')->getLocale()) ?>">
<head>
<meta charset="utf-8">
<title><?= lang('Sales.customer_display') ?></title>
<link rel="shortcut icon" type="image/x-icon" href="<?= base_url('images/favicon.ico') ?>">
<link rel="stylesheet" href="<?= base_url('resources/bootswatch/' . (empty($config['theme']) ? 'flatly' : esc($config['theme'])) . '/bootstrap.min.css') ?>">
<link rel="stylesheet" href="<?= base_url('resources/opensourcepos-8e34d6a398.min.css') ?>">
<style>
html, body {
margin: 0;
padding: 0;
background: #f8f8f8;
color: #333;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
body {
width: 100%;
overflow: hidden;
}
.customer-display-header {
background: #1f3143;
color: #fff;
text-align: center;
font-size: 15px;
font-weight: 700;
letter-spacing: 0.02em;
padding: 6px 12px;
border-bottom: 1px solid #102131;
}
.customer-display-shell {
width: 100%;
max-width: 1280px;
margin: 0 auto;
padding: 12px 18px 18px;
box-sizing: border-box;
}
.customer-display-company {
text-align: center;
margin-bottom: 18px;
}
.customer-display-company img {
display: block;
margin: 0 auto 6px;
max-height: 84px;
max-width: 240px;
}
.customer-display-company .company-name {
font-size: 20px;
font-weight: 600;
line-height: 1.2;
margin-top: 12px;
}
.customer-display-company .company-details {
font-size: 13px;
line-height: 1.35;
white-space: pre-line;
}
.customer-display-company .company-phone {
font-size: 13px;
line-height: 1.35;
margin-top: 4px;
}
.customer-display-main-row {
display: flex;
gap: 14px;
align-items: flex-start;
margin-top: 6px;
}
.customer-display-cart-column {
flex: 1 1 auto;
min-width: 0;
}
.customer-display-summary-column {
flex: 0 0 320px;
width: 320px;
}
.customer-display-summary-panel,
.customer-display-info-panel,
.customer-display-items-panel {
margin-bottom: 0;
}
.customer-display-summary-panel .panel-heading,
.customer-display-info-panel .panel-heading,
.customer-display-items-panel .panel-heading {
font-weight: 600;
}
.customer-display-summary-panel .table,
.customer-display-info-table {
margin-bottom: 0;
font-size: 13px;
}
.customer-display-summary-panel .table > tbody > tr > th,
.customer-display-info-table > tbody > tr > th {
background: #f8fbfd;
width: 56%;
font-weight: 700;
}
.customer-display-summary-panel .table > tbody > tr > td,
.customer-display-info-table > tbody > tr > td {
width: 44%;
text-align: right;
white-space: nowrap;
font-weight: 600;
}
.customer-display-summary-panel .rate-row th,
.customer-display-summary-panel .rate-row td {
color: #c00000;
}
.customer-display-summary-panel .summary-section-row th {
background: #eaf2f8;
color: #1f3b5b;
font-weight: 700;
}
.customer-display-summary-panel .summary-subtable {
width: 100%;
}
.customer-display-summary-panel .summary-subtable > tbody > tr > th {
background: #fdfefe;
font-weight: 600;
}
.customer-display-summary-panel .summary-subtable > tbody > tr > td {
font-weight: 600;
}
.register-wrap {
width: 100%;
}
#register {
width: 100%;
margin: 0;
table-layout: fixed;
background: #fff;
}
#register th,
#register td {
text-align: center;
vertical-align: middle;
padding: 6px 5px;
word-wrap: break-word;
}
#register thead th {
font-size: 12px;
font-weight: 600;
color: #333;
}
#register tbody td {
font-size: 15px;
}
#register tbody td.item-name-cell {
font-size: 16px;
text-align: left;
}
#register tbody td.price-cell {
font-size: 15px;
}
#register tbody td.serial-cell {
font-size: 12px;
color: #2F4F4F;
}
.customer-display-summary-panel .table > tbody > tr > th,
.customer-display-info-table > tbody > tr > th {
border-top: 1px solid #e5e5e5;
}
.customer-display-summary-panel .table > tbody > tr > td,
.customer-display-info-table > tbody > tr > td {
border-top: 1px solid #e5e5e5;
}
.customer-display-summary-panel .panel-body,
.customer-display-info-panel .panel-body,
.customer-display-items-panel .panel-body {
padding: 12px 15px;
}
.customer-display-summary-column .panel-body {
padding-top: 8px;
}
.customer-display-summary-column .customer-name-value,
.customer-display-summary-column .giftcard-value,
.customer-display-summary-column .reward-value {
text-align: right;
}
.customer-display-footer {
margin-top: 14px;
text-align: center;
font-size: 12px;
color: #777;
}
</style>
</head>
<body>
<div class="customer-display-header">Open Source Point of Sale</div>
<div class="customer-display-shell">
<div class="customer-display-company">
<?php if (!empty($config['company_logo'])) { ?>
<img src="<?= base_url('uploads/' . esc($config['company_logo'], 'url')) ?>" alt="company_logo">
<?php } ?>
<div class="company-name"><?= esc($companyName) ?></div>
<div class="company-phone">Phone: <?= esc((string)($config['phone'] ?? '')) ?></div>
<?php if ($companyDetails !== '') { ?>
<div class="company-details"><?= nl2br(esc($companyDetails)) ?></div>
<?php } ?>
</div>
<div class="customer-display-main-row">

View File

@@ -1,224 +0,0 @@
<?php
/**
* @var array $cart
* @var array $config
* @var float $rate
* @var float $total
* @var float $subtotal
* @var float $prediscount_subtotal
* @var array $taxes
* @var array $payments
* @var float $amount_change
*/
$priceWithCurrencyLabel = lang('Sales.price_with_currency');
?>
<?= view('partial/customer_display_header') ?>
<div class="customer-display-cart-column">
<div class="register-wrap">
<div class="panel panel-default customer-display-items-panel">
<div class="panel-heading"><?= lang('Sales.items') ?></div>
<div class="panel-body table-responsive">
<table class="table table-striped table-condensed" id="register">
<thead>
<tr>
<th style="width: <?= (int) $cartItemWidth ?>%;"><?= lang('Sales.item_name') ?></th>
<?php if ($cartHasCustomerDisplay) { ?>
<th style="width: <?= (int) $cartPriceWidth ?>%;"><?= sprintf($priceWithCurrencyLabel, esc($customerDisplayCurrencyLabel)) ?></th>
<?php } ?>
<th style="width: <?= (int) $cartOriginalWidth ?>%;"><?= sprintf($priceWithCurrencyLabel, esc($originalCurrencyLabel)) ?></th>
<th style="width: <?= (int) $cartQuantityWidth ?>%;"><?= lang('Sales.quantity') ?></th>
<th style="width: <?= (int) $cartDiscountWidth ?>%;"><?= lang('Sales.discount') ?></th>
<th style="width: <?= (int) $cartTotalWidth ?>%;"><?= lang('Sales.total') ?></th>
</tr>
</thead>
<tbody id="cart_contents">
<?php if (count($cart) == 0) { ?>
<tr>
<td colspan="<?= (int) $cartColspan ?>">
<div class="alert alert-dismissible alert-info"><?= lang('Sales.no_items_in_cart') ?></div>
</td>
</tr>
<?php } else { ?>
<?php foreach (array_reverse($cart, true) as $line => $item) { ?>
<tr>
<td class="item-name-cell">
<?= esc($item['name']) ?><br>
<?= !empty($item['attribute_values']) ? esc($item['attribute_values']) : '' ?>
</td>
<?php if ($cartHasCustomerDisplay) { ?>
<td class="price-cell">
<?= to_secondary_currency((float)$item['price'], $secondaryCurrency) ?>
</td>
<?php } ?>
<td class="price-cell">
<?= to_currency($item['price']) ?>
</td>
<td class="price-cell">
<?= to_quantity_decimals($item['quantity']) ?>
</td>
<td class="price-cell">
<?= to_decimals($item['discount'], 0) ?>
</td>
<td class="price-cell">
<?= $item['item_type'] == ITEM_AMOUNT_ENTRY ? to_currency_no_money($item['discounted_total']) : to_currency($item['discounted_total']) ?>
</td>
</tr>
<tr>
<td colspan="<?= $cartHasCustomerDisplay ? 3 : 2 ?>"></td>
<td class="serial-cell">
<?= $item['is_serialized'] == 1 ? lang('Sales.serial') : '' ?>
</td>
<td colspan="2" class="serial-cell">
<?php if ($item['is_serialized'] == 1) {
echo esc($item['serialnumber']);
} ?>
</td>
</tr>
<?php } ?>
<?php } ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="customer-display-summary-column">
<div class="panel panel-primary customer-display-summary-panel">
<div class="panel-heading"><?= lang('Sales.summary') ?></div>
<div class="panel-body">
<table class="table table-condensed summary-subtable">
<tbody>
<tr>
<th><?= lang('Sales.total') ?></th>
<td><?= to_currency($total) ?></td>
</tr>
<?php if ($showCustomerDisplay): ?>
<tr>
<th><?= lang('Sales.total') ?> <?= esc($customerDisplayCurrencyLabel) ?></th>
<td><?= to_secondary_currency((float)$total, $secondaryCurrency) ?></td>
</tr>
<tr class="rate-row">
<th><?= lang('Sales.rate') ?></th>
<td><?= number_format((float) $rate, 2) ?></td>
</tr>
<?php endif; ?>
</tbody>
</table>
<table class="table table-condensed summary-subtable" style="margin-top: 10px;">
<tbody>
<tr class="summary-section-row">
<th colspan="2"><?= lang('Sales.customer') ?></th>
</tr>
<tr>
<th><?= lang('Sales.customer_name') ?></th>
<td class="customer-name-value"><?= esc($customerName ?? lang('Sales.walk_in_customer')) ?></td>
</tr>
<tr>
<th><?= lang('Sales.giftcard_balance') ?></th>
<td class="giftcard-value"><?= to_currency((float) ($giftcardRemainder ?? 0)) ?></td>
</tr>
<tr>
<th><?= lang('Sales.loyalty_reward_points') ?></th>
<td class="reward-value"><?= esc((string)($customerRewardPoints ?? 0)) ?></td>
</tr>
</tbody>
</table>
<table class="table table-condensed summary-subtable" style="margin-top: 10px;">
<tbody>
<tr class="summary-section-row">
<th colspan="2"><?= lang('Sales.change') ?></th>
</tr>
<tr>
<th><?= lang('Sales.payments_total') ?></th>
<td><?= to_currency($payments_total) ?></td>
</tr>
<tr>
<th><?= lang('Sales.amount_due') ?></th>
<td><?= to_currency($amount_due) ?></td>
</tr>
<?php if ($showCustomerDisplay): ?>
<tr>
<th><?= lang('Sales.amount_due') ?> <?= esc($customerDisplayCurrencyLabel) ?></th>
<td><?= to_secondary_currency((float)$amount_due, $secondaryCurrency) ?></td>
</tr>
<?php endif; ?>
<tr>
<th><?= lang('Sales.change_due') ?></th>
<td><?= to_currency($paymentChangeDue ?? 0) ?></td>
</tr>
<?php if ($showCustomerDisplay): ?>
<tr>
<th><?= lang('Sales.change_due') ?> <?= esc($customerDisplayCurrencyLabel) ?></th>
<td><?= to_secondary_currency((float)($paymentChangeDue ?? 0), $secondaryCurrency) ?></td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="customer-display-footer"></div>
</div>
<script>
const customerDisplayId = new URLSearchParams(window.location.search).get('displayId') || '';
const customerDisplayStorageSuffix = customerDisplayId !== '' ? '_' + customerDisplayId : '';
const customerDisplayStorageKeys = {
open: 'customerDisplayOpen' + customerDisplayStorageSuffix,
dirtyAt: 'customerDisplayDirtyAt' + customerDisplayStorageSuffix
};
localStorage.setItem(customerDisplayStorageKeys.open, '1');
let lastDirtyAt = localStorage.getItem(customerDisplayStorageKeys.dirtyAt) || '';
let refreshTimer = null;
const scheduleRefresh = function(dirtyAt) {
if (refreshTimer !== null) {
clearTimeout(refreshTimer);
}
refreshTimer = setTimeout(function() {
if (localStorage.getItem(customerDisplayStorageKeys.open) !== '1') {
return;
}
if (localStorage.getItem(customerDisplayStorageKeys.dirtyAt) === dirtyAt) {
window.location.reload();
}
}, 700);
};
const checkForRefresh = function() {
const dirtyAt = localStorage.getItem(customerDisplayStorageKeys.dirtyAt) || '';
if (dirtyAt !== '' && dirtyAt !== lastDirtyAt) {
lastDirtyAt = dirtyAt;
scheduleRefresh(dirtyAt);
}
};
window.addEventListener('storage', function(event) {
if (event.key === customerDisplayStorageKeys.dirtyAt) {
checkForRefresh();
}
});
setInterval(checkForRefresh, 500);
window.addEventListener('beforeunload', function() {
localStorage.removeItem(customerDisplayStorageKeys.open);
});
</script>
</body>
</html>

View File

File diff suppressed because it is too large Load Diff

381
scripts/install-ubuntu.sh Normal file
View File

@@ -0,0 +1,381 @@
#!/bin/bash
set -e
COLOR_RED='\033[0;31m'
COLOR_GREEN='\033[0;32m'
COLOR_YELLOW='\033[1;33m'
COLOR_BLUE='\033[0;34m'
COLOR_RESET='\033[0m'
echo -e "${COLOR_BLUE}╔══════════════════════════════════════════════════════════╗${COLOR_RESET}"
echo -e "${COLOR_BLUE}║ Open Source Point of Sale - Ubuntu Installer ║${COLOR_RESET}"
echo -e "${COLOR_BLUE}║ Version 3.4+ ║${COLOR_RESET}"
echo -e "${COLOR_BLUE}╚══════════════════════════════════════════════════════════╝${COLOR_RESET}"
echo ""
if [ "$EUID" -ne 0 ]; then
echo -e "${COLOR_RED}Please run this script as root or with sudo${COLOR_RESET}"
exit 1
fi
export DEBIAN_FRONTEND=noninteractive
DB_HOST="${DB_HOST:-localhost}"
DB_NAME="${DB_NAME:-ospos}"
DB_USER="${DB_USER:-ospos}"
DB_PASS="${DB_PASS:-$(openssl rand -base64 24)}"
OSPOS_DIR="${OSPOS_DIR:-/var/www/ospos}"
OSPOS_VERSION="${OSPOS_VERSION:-}"
PHP_VERSION="${PHP_VERSION:-8.2}"
APACHE_SERVER_NAME="${APACHE_SERVER_NAME:-}"
SSL_EMAIL="${SSL_EMAIL:-}"
SSL_DOMAIN="${SSL_DOMAIN:-}"
MYSQL_ROOT_PASS="${MYSQL_ROOT_PASS:-}"
# Validate database variables contain only safe characters (alphanumeric, underscore, hyphen, dot)
validate_db_vars() {
local var_name="$1"
local var_value="$2"
local pattern='^[a-zA-Z0-9_\-\.]+$'
if [[ ! "$var_value" =~ $pattern ]]; then
echo -e "${COLOR_RED}Error: ${var_name} contains invalid characters. Only alphanumeric, underscore, hyphen, and dot are allowed.${COLOR_RESET}"
exit 1
fi
}
# Validate critical database variables
validate_db_vars "DB_NAME" "$DB_NAME"
validate_db_vars "DB_USER" "$DB_USER"
validate_db_vars "DB_HOST" "$DB_HOST"
# Check if running interactively
INTERACTIVE=false
if [ -t 0 ]; then
INTERACTIVE=true
fi
echo -e "${COLOR_YELLOW}Configuration:${COLOR_RESET}"
echo -e " Database Name: ${DB_NAME}"
echo -e " Database User: ${DB_USER}"
echo -e " Database Host: ${DB_HOST}"
echo -e " Install Directory: ${OSPOS_DIR}"
echo -e " PHP Version: ${PHP_VERSION}"
if [ -n "$OSPOS_VERSION" ]; then
echo -e " OSPOS Version: ${OSPOS_VERSION}"
else
echo -e " OSPOS Version: latest"
fi
if [ -n "$APACHE_SERVER_NAME" ]; then
echo -e " Server Name: ${APACHE_SERVER_NAME}"
fi
echo ""
if [ -d "$OSPOS_DIR" ]; then
echo -e "${COLOR_RED}Installation directory $OSPOS_DIR already exists${COLOR_RESET}"
echo -e "${COLOR_YELLOW}Remove it or set OSPOS_DIR environment variable${COLOR_RESET}"
exit 1
fi
echo -e "${COLOR_GREEN}[1/9] Updating system packages...${COLOR_RESET}"
apt-get update -qq
echo -e "${COLOR_GREEN}[2/9] Installing Apache, PHP, and dependencies...${COLOR_RESET}"
# Add PHP repository for newer PHP versions if not available in default repos
if ! apt-cache policy php${PHP_VERSION} 2>/dev/null | grep -q "Candidate:"; then
echo -e "${COLOR_YELLOW}PHP ${PHP_VERSION} not in default repos, adding ondrej/php PPA...${COLOR_RESET}"
apt-get install -y -qq software-properties-common
add-apt-repository -y ppa:ondrej/php
apt-get update -qq
fi
apt-get install -y -qq \
apache2 \
mariadb-server \
mariadb-client \
php${PHP_VERSION} \
php${PHP_VERSION}-mysql \
php${PHP_VERSION}-gd \
php${PHP_VERSION}-bcmath \
php${PHP_VERSION}-intl \
php${PHP_VERSION}-mbstring \
php${PHP_VERSION}-curl \
php${PHP_VERSION}-xml \
php${PHP_VERSION}-zip \
git \
curl \
unzip \
openssl
echo -e "${COLOR_GREEN}[3/9] Starting MariaDB...${COLOR_RESET}"
systemctl start mariadb
systemctl enable mariadb
if [ -z "$MYSQL_ROOT_PASS" ]; then
echo -e "${COLOR_BLUE}Securing MariaDB installation...${COLOR_RESET}"
mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '';"
mysql -e "FLUSH PRIVILEGES;"
else
mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '${MYSQL_ROOT_PASS}';"
fi
echo -e "${COLOR_GREEN}[4/9] Creating database and user...${COLOR_RESET}"
if [ -n "$MYSQL_ROOT_PASS" ]; then
mysql -u root -p"${MYSQL_ROOT_PASS}" <<EOF
CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS '${DB_USER}'@'${DB_HOST}' IDENTIFIED BY '${DB_PASS}';
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'${DB_HOST}';
FLUSH PRIVILEGES;
EOF
else
mysql -u root <<EOF
CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS '${DB_USER}'@'${DB_HOST}' IDENTIFIED BY '${DB_PASS}';
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'${DB_HOST}';
FLUSH PRIVILEGES;
EOF
fi
echo -e "${COLOR_GREEN}[5/9] Downloading OSPOS...${COLOR_RESET}"
mkdir -p "$(dirname "$OSPOS_DIR")"
cd "$(dirname "$OSPOS_DIR")"
if [ -z "$OSPOS_VERSION" ]; then
OSPOS_VERSION=$(curl -sS https://api.github.com/repos/opensourcepos/opensourcepos/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [ -z "$OSPOS_VERSION" ]; then
echo -e "${COLOR_RED}Failed to get latest release version${COLOR_RESET}"
exit 1
fi
fi
echo -e "${COLOR_BLUE}Downloading OSPOS version ${OSPOS_VERSION}...${COLOR_RESET}"
ASSET_URL=$(curl -sS "https://api.github.com/repos/opensourcepos/opensourcepos/releases/tags/${OSPOS_VERSION}" | grep '"browser_download_url"' | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
if [ -z "$ASSET_URL" ]; then
echo -e "${COLOR_RED}Failed to find release asset for ${OSPOS_VERSION}${COLOR_RESET}"
exit 1
fi
curl -sSL "$ASSET_URL" -o ospos.zip
if [ ! -f ospos.zip ] || [ ! -s ospos.zip ]; then
echo -e "${COLOR_RED}Failed to download OSPOS release ${OSPOS_VERSION}${COLOR_RESET}"
rm -f ospos.zip
exit 1
fi
unzip -q ospos.zip -d ospos-temp
mkdir -p "${OSPOS_DIR}"
cp -r ospos-temp/. "${OSPOS_DIR}/"
rm -rf ospos-temp ospos.zip
echo -e "${COLOR_GREEN}Downloaded OSPOS ${OSPOS_VERSION}${COLOR_RESET}"
echo -e "${COLOR_GREEN}[6/9] Setting up OSPOS...${COLOR_RESET}"
cd "${OSPOS_DIR}"
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer 2>/dev/null
if [ -f "composer.json" ]; then
echo -e "${COLOR_BLUE}Installing dependencies...${COLOR_RESET}"
composer install --no-dev --optimize-autoloader --no-interaction --quiet 2>/dev/null
fi
echo -e "${COLOR_GREEN}[7/9] Configuring OSPOS...${COLOR_RESET}"
if [ -f ".env.example" ]; then
cp .env.example .env
fi
if [ -f ".env" ]; then
# Escape special characters in password for sed
ESCAPED_DB_PASS=$(printf '%s\n' "$DB_PASS" | sed 's/[&/\]/\\&/g')
sed -i "s|database\.default\.hostname = 'localhost'|database.default.hostname = '${DB_HOST}'|" .env
sed -i "s|database\.default\.database = 'ospos'|database.default.database = '${DB_NAME}'|" .env
sed -i "s|database\.default\.username = 'admin'|database.default.username = '${DB_USER}'|" .env
sed -i "s|database\.default\.password = 'pointofsale'|database.default.password = '${ESCAPED_DB_PASS}'|" .env
sed -i "s|CI_ENVIRONMENT = development|CI_ENVIRONMENT = production|" .env
if grep -q "encryption\.key = ''" .env; then
ENCRYPTION_KEY=$(openssl rand -base64 32)
ESCAPED_KEY=$(printf '%s\n' "$ENCRYPTION_KEY" | sed 's/[&/\]/\\&/g')
sed -i "s|encryption\.key = ''|encryption.key = '${ESCAPED_KEY}'|" .env
fi
fi
echo -e "${COLOR_GREEN}[8/9] Importing database schema...${COLOR_RESET}"
if [ -n "$MYSQL_ROOT_PASS" ]; then
mysql -u root -p"${MYSQL_ROOT_PASS}" ${DB_NAME} < app/Database/database.sql
else
mysql -u root ${DB_NAME} < app/Database/database.sql
fi
# Interactive SSL configuration
if $INTERACTIVE && [ -z "$SSL_EMAIL" ] && [ -z "$APACHE_SERVER_NAME" ]; then
echo ""
echo -e "${COLOR_BLUE}╔══════════════════════════════════════════════════════════╗${COLOR_RESET}"
echo -e "${COLOR_BLUE}║ SSL/TLS Configuration ║${COLOR_RESET}"
echo -e "${COLOR_BLUE}╚══════════════════════════════════════════════════════════╝${COLOR_RESET}"
echo ""
echo -e "${COLOR_YELLOW}SSL provides secure HTTPS access to your OSPOS installation.${COLOR_RESET}"
echo -e "${COLOR_YELLOW}For production, we recommend Let's Encrypt (free SSL certificate).${COLOR_RESET}"
echo ""
read -p "Configure SSL? (y/n) [n]: " CONFIGURE_SSL
CONFIGURE_SSL=${CONFIGURE_SSL:-n}
if [[ "$CONFIGURE_SSL" =~ ^[Yy]$ ]]; then
read -p "Enter your domain name (e.g., pos.example.com): " SSL_DOMAIN
SSL_DOMAIN=${SSL_DOMAIN:-localhost}
APACHE_SERVER_NAME=$SSL_DOMAIN
read -p "Enter your email for Let's Encrypt notifications: " SSL_EMAIL
if [ -z "$SSL_EMAIL" ]; then
echo -e "${COLOR_YELLOW}No email provided. Using self-signed certificate (not recommended for production).${COLOR_RESET}"
SSL_TYPE="self-signed"
else
SSL_TYPE="letsencrypt"
fi
else
APACHE_SERVER_NAME="localhost"
SSL_TYPE="none"
fi
fi
# Set default server name if not provided
if [ -z "$APACHE_SERVER_NAME" ]; then
APACHE_SERVER_NAME="localhost"
fi
# If SSL_EMAIL is set without SSL_DOMAIN, use APACHE_SERVER_NAME
if [ -n "$SSL_EMAIL" ] && [ -z "$SSL_DOMAIN" ] && [ "$APACHE_SERVER_NAME" != "localhost" ]; then
SSL_DOMAIN="$APACHE_SERVER_NAME"
fi
echo -e "${COLOR_GREEN}[9/9] Configuring Apache...${COLOR_RESET}"
cat > /etc/apache2/sites-available/ospos.conf <<EOF
<VirtualHost *:80>
ServerName ${APACHE_SERVER_NAME}
DocumentRoot ${OSPOS_DIR}/public
<Directory ${OSPOS_DIR}/public>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog \${APACHE_LOG_DIR}/ospos_error.log
CustomLog \${APACHE_LOG_DIR}/ospos_access.log combined
</VirtualHost>
EOF
a2enmod rewrite
a2dissite 000-default.conf
a2ensite ospos.conf
chown -R www-data:www-data "${OSPOS_DIR}"
chmod -R 750 "${OSPOS_DIR}/writable"
systemctl restart apache2
systemctl enable apache2
# Configure SSL if requested
if [ -n "$SSL_EMAIL" ] && [ -n "$SSL_DOMAIN" ]; then
# Let's Encrypt SSL
echo -e "${COLOR_BLUE}Installing Certbot for Let's Encrypt...${COLOR_RESET}"
apt-get install -y -qq certbot python3-certbot-apache
echo -e "${COLOR_BLUE}Obtaining SSL certificate for ${SSL_DOMAIN}...${COLOR_RESET}"
certbot --apache -d ${SSL_DOMAIN} --non-interactive --agree-tos --email ${SSL_EMAIL} --redirect
echo -e "${COLOR_BLUE}Setting up auto-renewal...${COLOR_RESET}"
systemctl enable certbot.timer
systemctl start certbot.timer
PROTOCOL="https"
FINAL_URL="https://${SSL_DOMAIN}/"
elif [ -n "$SSL_DOMAIN" ]; then
# Self-signed SSL
echo -e "${COLOR_BLUE}Generating self-signed SSL certificate...${COLOR_RESET}"
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout /etc/ssl/private/ospos-selfsigned.key \
-out /etc/ssl/certs/ospos-selfsigned.crt \
-subj "/C=US/ST=State/L=City/O=Organization/CN=${SSL_DOMAIN}" 2>/dev/null
cat > /etc/apache2/sites-available/ospos-ssl.conf <<EOF
<VirtualHost *:443>
ServerName ${SSL_DOMAIN}
DocumentRoot ${OSPOS_DIR}/public
SSLEngine on
SSLCertificateFile /etc/ssl/certs/ospos-selfsigned.crt
SSLCertificateKeyFile /etc/ssl/private/ospos-selfsigned.key
<Directory ${OSPOS_DIR}/public>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog \${APACHE_LOG_DIR}/ospos_ssl_error.log
CustomLog \${APACHE_LOG_DIR}/ospos_ssl_access.log combined
</VirtualHost>
EOF
a2enmod ssl
a2ensite ospos-ssl.conf
cat > /etc/apache2/sites-available/ospos.conf <<EOF
<VirtualHost *:80>
ServerName ${SSL_DOMAIN}
Redirect permanent / https://${SSL_DOMAIN}/
</VirtualHost>
EOF
a2dissite ospos.conf
a2ensite ospos.conf
PROTOCOL="https"
FINAL_URL="https://${SSL_DOMAIN}/"
echo -e "${COLOR_YELLOW}Note: Your browser will show a security warning for self-signed${COLOR_RESET}"
echo -e "${COLOR_YELLOW} certificates. For production, re-run with an email for Let's Encrypt.${COLOR_RESET}"
else
PROTOCOL="http"
FINAL_URL="http://${APACHE_SERVER_NAME}/"
fi
systemctl restart apache2
# Configure allowed hostnames
if [ -f "${OSPOS_DIR}/.env" ]; then
sed -i "s|app\.allowedHostnames = ''|app.allowedHostnames = '${APACHE_SERVER_NAME}'|" ${OSPOS_DIR}/.env
fi
echo ""
echo -e "${COLOR_GREEN}╔══════════════════════════════════════════════════════════╗${COLOR_RESET}"
echo -e "${COLOR_GREEN}║ Installation Complete! ║${COLOR_RESET}"
echo -e "${COLOR_GREEN}╚══════════════════════════════════════════════════════════╝${COLOR_RESET}"
echo ""
echo -e "${COLOR_YELLOW}Database Credentials:${COLOR_RESET}"
echo -e " Database: ${DB_NAME}"
echo -e " Username: ${DB_USER}"
echo -e " Password: ${DB_PASS}"
echo ""
echo -e "${COLOR_YELLOW}Login Credentials:${COLOR_RESET}"
echo -e " URL: ${FINAL_URL}"
if [ -n "$SSL_EMAIL" ]; then
echo -e " SSL: Let's Encrypt (auto-renewal enabled)"
elif [ -n "$SSL_DOMAIN" ]; then
echo -e " SSL: Self-signed certificate"
else
echo -e " SSL: Not configured (HTTP only)"
fi
echo -e " Username: admin"
echo -e " Password: pointofsale"
echo ""
echo -e "${COLOR_RED}IMPORTANT: Change the default password after first login!${COLOR_RESET}"
echo ""
echo -e "${COLOR_BLUE}Configuration file: ${OSPOS_DIR}/.env${COLOR_RESET}"
echo ""