Compare commits

..

3 Commits

Author SHA1 Message Date
WebShells
1a6cfffc27 Fix locale parsing 2026-05-20 23:37:54 +03:00
WebShells
5e0541c53e Restore secondary display sales flow 2026-05-19 21:33:54 +03:00
objecttothis
b0dddc22a3 Bugfixes to get Migration working on MySQL and MariaDB (#4551)
* Bugfixes to get Migration working on MySQL

Signed-off-by: objec <objecttothis@gmail.com>

* MariaDB compatibility fixes

- Drop foreign key constraints before making charset changes
- Fix dropAllForeignKeyConstraints helper function.
- Added `IF EXISTS` to DROP statements
- Do not try to readd FK constraints for tables which were dropped.
- MariaDB 11.8.x changes the default implicit collation to uca1400 which breaks the IndiaGST migration, et. al. Explicitly declare utf8_general_ci in affected migrations.

Signed-off-by: objec <objecttothis@gmail.com>

* Fix changes which break MySQL migrations

- MySQL does not support IF EXISTS in foreign key constraints. Since the PHP is now handling dropping those constraints, these lines are redundant. Remove them.

Signed-off-by: objec <objecttothis@gmail.com>

* Resolve code review recommendations

- Add try/catch around DB connect statement
- Heed result of execute_script function and throw an exception on failure.

Signed-off-by: objec <objecttothis@gmail.com>

* Refactor out duplicate code

Signed-off-by: objec <objecttothis@gmail.com>

* Initialize array variable causing potential issues

Signed-off-by: objec <objecttothis@gmail.com>

---------

Signed-off-by: objec <objecttothis@gmail.com>
2026-05-19 16:02:05 +04:00
25 changed files with 5243 additions and 4963 deletions

View File

@@ -1,94 +0,0 @@
#!/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,7 +22,6 @@ 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
@@ -76,13 +75,16 @@ jobs:
- name: Get version info
id: version
run: |
chmod +x .github/scripts/get-version.sh
.github/scripts/get-version.sh all 7
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
env:
GITHUB_EVENT_PATH: ${{ github.event_path }}
GITHUB_SHA: ${{ github.sha }}
GITHUB_REF: ${{ github.ref }}
GITHUB_EVENT_NAME: ${{ github.event_name }}
GITHUB_TAG: ${{ github.ref_name }}
- name: Create .env file
run: |
@@ -128,7 +130,7 @@ jobs:
name: Build Docker Image
runs-on: ubuntu-22.04
needs: build
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
if: github.event_name == 'push'
steps:
- name: Download build context
@@ -152,14 +154,14 @@ jobs:
- name: Determine Docker tags
id: tags
run: |
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
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
else
echo "tags=${{ secrets.DOCKER_USERNAME }}/opensourcepos:${TAG}" >> $GITHUB_OUTPUT
echo "tags=${{ secrets.DOCKER_USERNAME }}/opensourcepos:${{ needs.build.outputs.version-tag }}" >> $GITHUB_OUTPUT
fi
env:
GITHUB_REF: ${{ github.ref }}
- name: Build and push Docker images
uses: docker/build-push-action@v5
@@ -192,7 +194,7 @@ jobs:
id: version
run: |
VERSION="${{ needs.build.outputs.version }}"
SHORT_SHA="${{ needs.build.outputs.short-sha }}"
SHORT_SHA=$(git rev-parse --short=6 HEAD)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "short-sha=$SHORT_SHA" >> $GITHUB_OUTPUT

View File

@@ -33,15 +33,12 @@ jobs:
- name: Get image tag
id: image
run: |
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 }}
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}"
echo "tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT"
deploy:
name: Deploy to staging

View File

@@ -1,131 +0,0 @@
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,73 +102,5 @@ Do **not** use below command on live deployments unless you want to tear everyth
## Cloud install
### 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!
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.

View File

@@ -5,6 +5,7 @@ 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
@@ -13,7 +14,7 @@ use CodeIgniter\Config\BaseConfig;
*/
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;
@@ -33,26 +34,35 @@ class OSPOS extends BaseConfig
if ($cache) {
$this->settings = decode_array($cache);
} 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'
];
}
return;
}
try {
$db = Database::connect();
if (!$db->tableExists('app_config')) {
$this->settings = $this->getDefaultSettings();
return;
}
$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'
];
}
/**
@@ -63,4 +73,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,6 +12,40 @@ $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');
@@ -39,4 +73,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,6 +49,13 @@ 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' => [
@@ -62,13 +69,6 @@ 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,6 +2,7 @@
namespace App\Database\Migrations;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\Migration;
class Migration_Upgrade_To_3_1_1 extends Migration
@@ -17,7 +18,37 @@ class Migration_Upgrade_To_3_1_1 extends Migration
public function up(): void
{
helper('migration');
execute_script(APPPATH . 'Database/Migrations/sqlscripts/3.0.2_to_3.1.1.sql');
// 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));
}
/**

View File

@@ -327,19 +327,6 @@ 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;
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
-- 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;
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
-- 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;
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
-- 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;
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
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;
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
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 AUTO_INCREMENT=1;
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci 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;
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
-- 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;
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
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,9 +272,6 @@ 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;
}
@@ -368,6 +365,74 @@ 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,6 +172,7 @@ 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,336 +1,344 @@
<?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",
"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",
];
"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",
];

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,6 +20,8 @@ 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",
@@ -41,6 +43,7 @@ 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)",
@@ -74,12 +77,6 @@ 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",
@@ -110,6 +107,7 @@ 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",
@@ -151,7 +149,9 @@ 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,10 +167,13 @@ 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",
@@ -182,6 +185,7 @@ 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",
@@ -212,6 +216,7 @@ 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",
@@ -224,7 +229,6 @@ return [
"update" => "Update",
"upi" => "UPI",
"visa" => "",
"wallet" => "Wallet",
"wholesale" => "",
"work_order" => "Work Order",
"work_order_number" => "Work Order Number",
@@ -232,3 +236,5 @@ 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

@@ -0,0 +1,249 @@
<?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

@@ -0,0 +1,224 @@
<?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

View File

@@ -1,381 +0,0 @@
#!/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 ""