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
359 changed files with 2884 additions and 5896 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

2
.gitignore vendored
View File

@@ -87,5 +87,3 @@ auth.json
/app/Database/database.sql
/writable/cache/settings
/.env.bak
/.php-cs-fixer.cache
/build

127
AGENTS.md
View File

@@ -1,125 +1,40 @@
# Agent Instructions
This document is the single source of truth for all AI agents working on the Open Source Point of Sale (OSPOS) codebase. Read it fully before making any changes.
## Project Overview
OpenSourcePOS is a web-based Point of Sale system built on **CodeIgniter 4** (PHP 8.2+) with MySQL/MariaDB. Frontend uses Bootstrap 3 (Bootstrap 5 migration in progress) and jQuery, with assets built via Gulp.
## Common Commands
```bash
# PHP dependencies
composer install
# Frontend dependencies and asset build
npm install
npm run build # Runs Gulp: compiles and copies all CSS/JS to public/resources/
# Run full test suite
composer test
# Run a single test file
vendor/bin/phpunit tests/unit/AppTest.php
# Lint / code style check
vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.no-header.php --dry-run
# Apply code style fixes
vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.no-header.php
```
Tests require a MariaDB/MySQL database (see CI config in `.github/workflows/phpunit.yml`).
## Architecture
### Framework & Entry Point
- **Framework**: CodeIgniter 4 — MVC with QueryBuilder ORM, no Eloquent
- **Web root**: `public/``public/index.php` is the only entry point
- **Routes**: `app/Config/Routes.php`
- **App config**: `app/Config/App.php` (version, session, security settings)
- **Environment**: `.env` file (copy from `.env.example`); `CI_ENVIRONMENT` controls dev/prod/test mode
### Directory Layout
```text
app/
├── Config/ # CI4 config classes
├── Controllers/ # ~27 controllers (Sales, Items, Reports, Customers, etc.)
├── Models/ # ~28 models (Sale, Item, Customer, Supplier, etc.)
├── Views/ # PHP view templates
├── Libraries/ # Business logic (Sale_lib, Tax_lib, Receiving_lib, etc.)
├── Plugins/ # Plugin system — each plugin is a subdirectory here
├── Database/ # Migrations (ospos_ prefix) and seeds
├── Language/ # i18n files (IETF BCP 47 locale names)
├── Filters/ # Request/response filters (auth, HTTPS, etc.)
└── Events/ # CI4 event subscribers
public/
└── resources/ # Built CSS/JS (do not edit directly — generated by npm run build)
tests/ # PHPUnit test suite
```
### Key Libraries
`app/Libraries/` holds core business logic:
- `Sale_lib.php` — sale cart state, pricing, discounts, tax calculation
- `Tax_lib.php` — multi-tier tax engine
- `Receiving_lib.php` — purchase orders / receivings
- `Barcode_lib.php` — barcode generation
- `Email_lib.php` — email delivery
- `Token_lib.php` — CSRF/session token management
### Database
- Table prefix: `ospos_` (defined in `app/Config/Database.php`)
- Migrations live in `app/Database/Migrations/` and run automatically on first access
- CodeIgniter QueryBuilder throughout — no raw SQL unless necessary
### Plugin System
Plugins live in `app/Plugins/<PluginName>/` and are auto-discovered by `PluginManager`. Each plugin:
- Extends `BasePlugin` or implements `PluginInterface`
- Registers event hooks (e.g., `item_sale`, `customer_saved`, view hooks like `customer_tabs`)
- Can include its own `Views/`, `Models/`, `Controllers/`, and `Language/` subdirectories
- Configuration stored in `ospos_plugin_config` table
- See `app/Plugins/README.md` for plugin structure, event hooks, and LICENSE requirements
### Frontend Build
`gulpfile.js` (Gulp 5) copies vendor CSS/JS from `node_modules/` into `public/resources/`. Run `npm run build` after installing npm packages or changing gulp tasks. Do not manually edit files under `public/resources/`.
This document provides guidance for AI agents working on the Open Source Point of Sale (OSPOS) codebase.
## Code Style
- **PSR-12** enforced via PHP-CS-Fixer (config: `.php-cs-fixer.no-header.php`)
- `camelCase` for variables and methods; `PascalCase` for classes; `UPPER_CASE` for constants
- PHP 8.2+ features acceptable (named arguments, enums, readonly properties)
- Views in `app/Views/errors/html/` are excluded from the fixer
- Run fixer before committing: `vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.no-header.php`
- Follow PHP CodeIgniter 4 coding standards
- Run PHP-CS-Fixer before committing: `vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.no-header.php`
- Write PHP 8.1+ compatible code with proper type declarations
- Use PSR-12 naming conventions: `camelCase` for variables and functions, `PascalCase` for classes, `UPPER_CASE` for constants
## Development Workflow
## Development
- Create a new git worktree for each issue, based on the latest state of `origin/master`
- Commit fixes to the worktree and push to the remote
- Tests must pass before submitting changes (`composer test`)
- Minimum PHPUnit version: 10.5.16+. Default config: `phpunit.xml.dist`
## Testing
- Run PHPUnit tests: `composer test`
- Tests must pass before submitting changes
## Build
- Install dependencies: `composer install && npm install`
- Build assets: `npm run build` or `gulp`
## Conventions
- Controllers `app/Controllers/`
- Models `app/Models/`
- Views `app/Views/`
- Migrations `app/Database/Migrations/`
- Plugins → `app/Plugins/` (see `app/Plugins/README.md` for plugin structure, event hooks, and LICENSE requirements)
- Controllers go in `app/Controllers/`
- Models go in `app/Models/`
- Views go in `app/Views/`
- Database migrations in `app/Database/Migrations/`
- Use CodeIgniter 4 framework patterns and helpers
- Sanitize user input; escape output using `esc()` helper
## Security
- `app.allowedHostnames` **must** be set in production (host header injection protection)
- HTMLPurifier for HTML sanitization; Laminas Escaper for output escaping
- CSRF tokens managed via `Token_lib` — do not bypass CI4's CSRF filter
- Session storage is database-backed (`ospos_sessions` table) for multi-instance support
- Never commit secrets, credentials, or `.env` files
- Use parameterized queries to prevent SQL injection
- Validate and sanitize all user input
- Validate and sanitize all user input

View File

@@ -1,3 +0,0 @@
# CLAUDE.md
> **MANDATORY INSTRUCTION**: You MUST read `AGENTS.md` in this directory before doing anything else. `AGENTS.md` is the single source of truth for this project — architecture, commands, conventions, security rules, and workflow are all defined there. Do not proceed with any task until you have read and internalized its contents.

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

@@ -78,7 +78,6 @@ class Autoload extends AutoloadConfig
'No_access' => '/App/Controllers/No_access.php',
'Office' => '/App/Controllers/Office.php',
'Persons' => '/App/Controllers/Persons.php',
'Plugins' => '/App/Controllers/Plugins.php',
'Receivings' => '/App/Controllers/Receivings.php',
'Reports' => '/App/Controllers/Reports.php',
'Sales' => '/App/Controllers/Sales.php',
@@ -158,9 +157,9 @@ class Autoload extends AutoloadConfig
'Barcode_lib' => '/App/Libraries/Barcode_lib.php',
'Email_lib' => '/App/Libraries/Email_lib.php',
'Item_lib' => '/App/Libraries/Item_lib.php',
'Mailchimp_lib' => '/App/Libraries/Mailchimp_lib.php',
'MY_Email' => '/App/Libraries/MY_Email.php',
'MY_Migration' => '/App/Libraries/MY_Migration.php',
'PluginManager' => '/App/Libraries/Plugins/PluginManager.php',
'Receving_lib' => '/App/Libraries/Receiving_lib.php',
'Sale_lib' => '/App/Libraries/Sale_lib.php',
'Sms_lib' => '/App/Libraries/Sms_lib.php',
@@ -204,7 +203,6 @@ class Autoload extends AutoloadConfig
'cookie',
'tabular',
'locale',
'security',
'plugin'
'security'
];
}

View File

@@ -173,4 +173,4 @@ const DEFAULT_LANGUAGE_CODE = 'en';
/**
* Admin modules - list of modules required for admin privileges
*/
const ADMIN_MODULES = ['customers', 'employees', 'giftcards', 'items', 'item_kits', 'messages', 'plugins', 'receivings', 'reports', 'sales', 'config', 'suppliers'];
const ADMIN_MODULES = ['customers', 'employees', 'giftcards', 'items', 'item_kits', 'messages', 'receivings', 'reports', 'sales', 'config', 'suppliers'];

View File

@@ -8,7 +8,6 @@ use CodeIgniter\HotReloader\HotReloader;
use App\Events\Db_log;
use App\Events\Load_config;
use App\Events\Method;
use App\Libraries\Plugins\PluginManager;
/*
* --------------------------------------------------------------------
@@ -26,9 +25,6 @@ use App\Libraries\Plugins\PluginManager;
* Example:
* Events::on('create', [$myInstance, 'myMethod']);
*/
Events::on('pre_system', static function (): void {
PluginManager::registerAllNamespaces();
});
Events::on('pre_system', static function (): void {
if (ENVIRONMENT !== 'testing') {
@@ -52,6 +48,7 @@ Events::on('pre_system', static function (): void {
if (CI_DEBUG && ! is_cli()) {
Events::on('DBQuery', 'CodeIgniter\Debug\Toolbar\Collectors\Database::collect');
service('toolbar')->respond();
// Hot Reload route - for framework use on the hot reloader.
if (ENVIRONMENT === 'development') {
service('routes')->get('__hot-reload', static function (): void {
(new HotReloader())->run();
@@ -60,12 +57,8 @@ Events::on('pre_system', static function (): void {
}
});
Events::on('post_controller_constructor', static function (): void {
service('pluginManager');
}, 10);
$config = new Load_config();
Events::on('post_controller_constructor', [$config, 'load_config'], 1);
Events::on('post_controller_constructor', [$config, 'load_config']);
$db_log = new Db_log();
Events::on('DBQuery', [$db_log, 'db_log_queries']);

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

@@ -3,7 +3,6 @@
namespace Config;
use App\Libraries\MY_Language;
use App\Libraries\Plugins\PluginManager;
use Locale;
use HTMLPurifier;
use HTMLPurifier_Config;
@@ -62,24 +61,6 @@ class Services extends BaseService
return new MY_Language($locale);
}
public static function pluginManager(bool $getShared = true): PluginManager
{
if ($getShared) {
return static::getSharedInstance('pluginManager');
}
$manager = new PluginManager();
if ($manager->canLoadPlugins()) {
$manager->discoverPlugins();
$manager->registerPluginEvents();
} else {
log_message('debug', 'PluginManager: skipping init, plugin_config table not found.');
}
return $manager;
}
private static HTMLPurifier $htmlPurifier;
public static function htmlPurifier($getShared = true): object

View File

@@ -3,6 +3,7 @@
namespace App\Controllers;
use App\Libraries\Barcode_lib;
use App\Libraries\Mailchimp_lib;
use App\Libraries\Receiving_lib;
use App\Libraries\Sale_lib;
use App\Libraries\Tax_lib;
@@ -252,6 +253,32 @@ class Config extends Secure_Controller
$data['image_allowed_types'] = array_combine($image_allowed_types, $image_allowed_types);
$data['selected_image_allowed_types'] = explode(',', $this->config['image_allowed_types']);
// Integrations Related fields
$data['mailchimp'] = [];
if (check_encryption()) { // TODO: Hungarian notation
if (!isset($this->encrypter)) {
helper('security');
$this->encrypter = Services::encrypter();
}
$data['mailchimp']['api_key'] = (isset($this->config['mailchimp_api_key']) && !empty($this->config['mailchimp_api_key']))
? $this->encrypter->decrypt($this->config['mailchimp_api_key'])
: '';
$data['mailchimp']['list_id'] = (isset($this->config['mailchimp_list_id']) && !empty($this->config['mailchimp_list_id']))
? $this->encrypter->decrypt($this->config['mailchimp_list_id'])
: '';
// Remove any backup of .env created by check_encryption()
remove_backup();
} else {
$data['mailchimp']['api_key'] = '';
$data['mailchimp']['list_id'] = '';
}
$data['mailchimp']['lists'] = $this->_mailchimp();
return view('configs/manage', $data);
}
@@ -289,6 +316,7 @@ class Config extends Secure_Controller
return $this->response->setJSON(['success' => $success, 'message' => $message]);
}
/**
* @return array
*/
@@ -547,6 +575,76 @@ class Config extends Secure_Controller
return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]);
}
/**
* This function fetches all the available lists from Mailchimp for the given API key
*/
private function _mailchimp(string $api_key = ''): array // TODO: Hungarian notation
{
$mailchimp_lib = new Mailchimp_lib(['api_key' => $api_key]);
$result = [];
$lists = $mailchimp_lib->getLists();
if ($lists !== false) {
if (is_array($lists) && !empty($lists['lists']) && is_array($lists['lists'])) {
foreach ($lists['lists'] as $list) {
$result[$list['id']] = $list['name'] . ' [' . $list['stats']['member_count'] . ']';
}
}
}
return $result;
}
/**
* Gets Mailchimp lists when a valid API key is inserted. Used in app/Views/configs/integrations_config.php
*
* @return ResponseInterface
* @noinspection PhpUnused
*/
public function postCheckMailchimpApiKey(): ResponseInterface
{
$lists = $this->_mailchimp($this->request->getPost('mailchimp_api_key'));
$success = count($lists) > 0;
return $this->response->setJSON([
'success' => $success,
'message' => lang('Config.mailchimp_key_' . ($success ? '' : 'un') . 'successfully'),
'mailchimp_lists' => $lists
]);
}
/**
* Saves Mailchimp configuration. Used in app/Views/configs/integrations_config.php
*
* @throws ReflectionException
* @return ResponseInterface
* @noinspection PhpUnused
*/
public function postSaveMailchimp(): ResponseInterface
{
$api_key = '';
$list_id = '';
if (check_encryption()) {
$api_key_unencrypted = $this->request->getPost('mailchimp_api_key');
if (!empty($api_key_unencrypted)) {
$api_key = $this->encrypter->encrypt($api_key_unencrypted);
}
$list_id_unencrypted = $this->request->getPost('mailchimp_list_id');
if (!empty($list_id_unencrypted)) {
$list_id = $this->encrypter->encrypt($list_id_unencrypted);
}
}
$batch_save_data = ['mailchimp_api_key' => $api_key, 'mailchimp_list_id' => $list_id];
$success = $this->appconfig->batch_save($batch_save_data);
return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]);
}
/**
* Gets all stock locations. Used in app/Views/configs/stock_config.php
*
@@ -914,8 +1012,8 @@ class Config extends Secure_Controller
'work_order_enable' => $this->request->getPost('work_order_enable') != null,
'work_order_format' => $this->request->getPost('work_order_format'),
'last_used_work_order_number' => $this->request->getPost('last_used_work_order_number', FILTER_SANITIZE_NUMBER_INT),
'invoice_type' => Sale_lib::isValidInvoiceType($this->request->getPost('invoice_type'))
? $this->request->getPost('invoice_type')
'invoice_type' => Sale_lib::isValidInvoiceType($this->request->getPost('invoice_type'))
? $this->request->getPost('invoice_type')
: 'invoice'
];
@@ -961,8 +1059,8 @@ class Config extends Secure_Controller
return $fieldType === 'first' ? 'name' : '';
}
$allowed = $fieldType === 'first'
? Item::ALLOWED_SUGGESTIONS_COLUMNS
$allowed = $fieldType === 'first'
? Item::ALLOWED_SUGGESTIONS_COLUMNS
: Item::ALLOWED_SUGGESTIONS_COLUMNS_WITH_EMPTY;
$fallback = $fieldType === 'first' ? 'name' : '';

View File

@@ -2,10 +2,11 @@
namespace App\Controllers;
use App\Libraries\Mailchimp_lib;
use App\Models\Customer;
use App\Models\Customer_rewards;
use App\Models\Tax_code;
use CodeIgniter\Events\Events;
use CodeIgniter\HTTP\DownloadResponse;
use CodeIgniter\HTTP\ResponseInterface;
use Config\OSPOS;
@@ -14,6 +15,8 @@ use stdClass;
class Customers extends Persons
{
private string $_list_id;
private Mailchimp_lib $mailchimp_lib;
private Customer_rewards $customer_rewards;
private Customer $customer;
private Tax_code $tax_code;
@@ -22,11 +25,19 @@ class Customers extends Persons
public function __construct()
{
parent::__construct('customers');
$this->mailchimp_lib = new Mailchimp_lib();
$this->customer_rewards = model(Customer_rewards::class);
$this->customer = model(Customer::class);
$this->tax_code = model(Tax_code::class);
$this->config = config(OSPOS::class)->settings;
$encrypter = Services::encrypter();
if (!empty($this->config['mailchimp_list_id'])) {
$this->_list_id = $encrypter->decrypt($this->config['mailchimp_list_id']);
} else {
$this->_list_id = '';
}
}
/**
@@ -41,12 +52,11 @@ class Customers extends Persons
/**
* Gets one row for a customer manage table. This is called using AJAX to update one row.
* @param int $row_id
* @return ResponseInterface
*/
public function getRow(int $row_id): ResponseInterface
{
$person = $this->customer->getInfo($row_id);
$person = $this->customer->get_info($row_id);
// Retrieve the total amount the customer spent so far together with min, max and average values
$stats = $this->customer->get_stats($person->person_id); // TODO: This and the next 11 lines are duplicated in search(). Extract a method.
@@ -131,16 +141,14 @@ class Customers extends Persons
/**
* Loads the customer edit form
* @param int $customerId
* @return string
*/
public function getView(int $customerId = NEW_ENTRY): string
public function getView(int $customer_id = NEW_ENTRY): string
{
if ($customerId == null) {
$customerId = NEW_ENTRY;
}
// Set default values
if ($customer_id == null) $customer_id = NEW_ENTRY;
$info = $this->customer->getInfo($customerId);
$info = $this->customer->get_info($customer_id);
foreach (get_object_vars($info) as $property => $value) {
$info->$property = $value;
}
@@ -151,7 +159,7 @@ class Customers extends Persons
$data['person_info']->employee_id = $this->employee->get_logged_in_employee_info()->person_id;
}
$employee_info = $this->employee->getInfo($info->employee_id);
$employee_info = $this->employee->get_info($info->employee_id);
$data['employee'] = $employee_info->first_name . ' ' . $employee_info->last_name;
$tax_code_info = $this->tax_code->get_info($info->sales_tax_code_id);
@@ -172,7 +180,7 @@ class Customers extends Persons
$data['use_destination_based_tax'] = $this->config['use_destination_based_tax'];
// Retrieve the total amount the customer spent so far together with min, max and average values
$stats = $this->customer->get_stats($customerId);
$stats = $this->customer->get_stats($customer_id);
if (!empty($stats)) {
foreach (get_object_vars($stats) as $property => $value) {
$info->$property = $value;
@@ -180,29 +188,69 @@ class Customers extends Persons
$data['stats'] = $stats;
}
Events::trigger('customer_loaded', $customerId);
// Retrieve the info from Mailchimp only if there is an email address assigned
if (!empty($info->email)) {
// Collect Mailchimp customer info
if (($mailchimp_info = $this->mailchimp_lib->getMemberInfo($this->_list_id, $info->email)) !== false) {
$data['mailchimp_info'] = $mailchimp_info;
// Collect customer Mailchimp emails activities (stats)
if (($activities = $this->mailchimp_lib->getMemberActivity($this->_list_id, $info->email)) !== false) {
if (array_key_exists('activity', $activities)) {
$open = 0;
$unopen = 0;
$click = 0;
$total = 0;
$lastopen = '';
foreach ($activities['activity'] as $activity) {
if ($activity['action'] == 'sent') {
++$unopen;
} elseif ($activity['action'] == 'open') {
if (empty($lastopen)) {
$lastopen = substr($activity['timestamp'], 0, 10);
}
++$open;
} elseif ($activity['action'] == 'click') {
if (empty($lastopen)) {
$lastopen = substr($activity['timestamp'], 0, 10);
}
++$click;
}
++$total;
}
$data['mailchimp_activity']['total'] = $total;
$data['mailchimp_activity']['open'] = $open;
$data['mailchimp_activity']['unopen'] = $unopen;
$data['mailchimp_activity']['click'] = $click;
$data['mailchimp_activity']['lastopen'] = $lastopen;
}
}
}
}
return view("customers/form", $data);
}
/**
* Inserts/updates a customer
* @param int $customerId
* @return ResponseInterface
*/
public function postSave(int $customerId = NEW_ENTRY): ResponseInterface
public function postSave(int $customer_id = NEW_ENTRY): ResponseInterface
{
$firstName = $this->request->getPost('first_name');
$lastName = $this->request->getPost('last_name');
$first_name = $this->request->getPost('first_name');
$last_name = $this->request->getPost('last_name');
$email = strtolower($this->request->getPost('email', FILTER_SANITIZE_EMAIL));
// Format first and last name properly
$firstName = $this->nameize($firstName);
$lastName = $this->nameize($lastName);
$first_name = $this->nameize($first_name);
$last_name = $this->nameize($last_name);
$personData = [
'first_name' => $firstName,
'last_name' => $lastName,
$person_data = [
'first_name' => $first_name,
'last_name' => $last_name,
'gender' => $this->request->getPost('gender', FILTER_SANITIZE_NUMBER_INT),
'email' => $email,
'phone_number' => $this->request->getPost('phone_number'),
@@ -215,9 +263,9 @@ class Customers extends Persons
'comments' => $this->request->getPost('comments')
];
$dateFormatter = date_create_from_format($this->config['dateformat'] . ' ' . $this->config['timeformat'], $this->request->getPost('date'));
$date_formatter = date_create_from_format($this->config['dateformat'] . ' ' . $this->config['timeformat'], $this->request->getPost('date'));
$customerData = [
$customer_data = [
'consent' => $this->request->getPost('consent') != null,
'account_number' => $this->request->getPost('account_number') == '' ? null : $this->request->getPost('account_number'),
'tax_id' => $this->request->getPost('tax_id'),
@@ -226,32 +274,41 @@ class Customers extends Persons
'discount_type' => $this->request->getPost('discount_type') == null ? PERCENT : $this->request->getPost('discount_type', FILTER_SANITIZE_NUMBER_INT),
'package_id' => $this->request->getPost('package_id') == '' ? null : $this->request->getPost('package_id'),
'taxable' => $this->request->getPost('taxable') != null,
'date' => $dateFormatter->format('Y-m-d H:i:s'),
'date' => $date_formatter->format('Y-m-d H:i:s'),
'employee_id' => $this->request->getPost('employee_id', FILTER_SANITIZE_NUMBER_INT),
'sales_tax_code_id' => $this->request->getPost('sales_tax_code_id') == '' ? null : $this->request->getPost('sales_tax_code_id', FILTER_SANITIZE_NUMBER_INT)
];
if ($this->customer->saveCustomer($personData, $customerData, $customerId)) {
Events::trigger('customer_saved', [$customerData['person_id']]);
if ($this->customer->save_customer($person_data, $customer_data, $customer_id)) {
// Save customer to Mailchimp selected list // TODO: addOrUpdateMember should be refactored. Potentially pass an array or object instead of 6 parameters.
$mailchimp_status = $this->request->getPost('mailchimp_status');
$this->mailchimp_lib->addOrUpdateMember(
$this->_list_id,
$email,
$first_name,
$last_name,
$mailchimp_status == null ? "" : $mailchimp_status,
['vip' => $this->request->getPost('mailchimp_vip') != null]
);
// New customer
if ($customerId == NEW_ENTRY) {
if ($customer_id == NEW_ENTRY) {
return $this->response->setJSON([
'success' => true,
'message' => lang('Customers.successful_adding') . " $firstName $lastName",
'id' => $customerData['person_id']
'message' => lang('Customers.successful_adding') . ' ' . $first_name . ' ' . $last_name,
'id' => $customer_data['person_id']
]);
} else { // Existing customer
return $this->response->setJSON([
'success' => true,
'message' => lang('Customers.successful_updating') . " $firstName $lastName",
'id' => $customerId
'message' => lang('Customers.successful_updating') . ' ' . $first_name . ' ' . $last_name,
'id' => $customer_id
]);
}
} else { // Failure
return $this->response->setJSON([
'success' => false,
'message' => lang('Customers.error_adding_updating') . " $firstName $lastName",
'message' => lang('Customers.error_adding_updating') . ' ' . $first_name . ' ' . $last_name,
'id' => NEW_ENTRY
]);
}
@@ -287,23 +344,26 @@ class Customers extends Persons
}
/**
* This deletes customers from the customer's table
* This deletes customers from the customers table
* @return ResponseInterface
*/
public function postDelete(): ResponseInterface
{
$customersToDelete = $this->request->getPost('ids');
$customers = $this->customer->get_multiple_info($customersToDelete);
$customers_to_delete = $this->request->getPost('ids');
$customers_info = $this->customer->get_multiple_info($customers_to_delete);
$count = 0;
foreach ($customers->getResult() as $customer) {
if ($this->customer->delete($customer->person_id)) {
Events::trigger('customer_deleted', (int)$customer->person_id, (string)$customer->email);
foreach ($customers_info->getResult() as $info) {
if ($this->customer->delete($info->person_id)) {
// remove customer from Mailchimp selected list
$this->mailchimp_lib->removeMember($this->_list_id, $info->email);
$count++;
}
}
if ($count === count($customersToDelete)) {
if ($count == count($customers_to_delete)) {
return $this->response->setJSON([
'success' => true,
'message' => lang('Customers.successful_deleted') . ' ' . $count . ' ' . lang('Customers.one_or_multiple')
@@ -351,17 +411,16 @@ class Customers extends Persons
if (($handle = fopen($_FILES['file_path']['tmp_name'], 'r')) !== false) {
// Skip the first row as it's the table description
fgetcsv($handle);
$rowNumber = 1;
$i = 1;
$failCodes = [];
$customerIds = [];
while (($data = fgetcsv($handle)) !== false) {
$consent = $data[3] == '' ? 0 : 1;
if (sizeof($data) >= 16 && $consent) {
$email = strtolower($data[4]);
$personData = [
$person_data = [
'first_name' => $data[0],
'last_name' => $data[1],
'gender' => $data[2],
@@ -376,7 +435,7 @@ class Customers extends Persons
'comments' => $data[12]
];
$customerData = [
$customer_data = [
'consent' => $consent,
'company_name' => $data[13],
'discount' => $data[15],
@@ -391,7 +450,7 @@ class Customers extends Persons
$invalidated = $this->customer->check_email_exists($email);
if ($account_number != '') {
$customerData['account_number'] = $account_number;
$customer_data['account_number'] = $account_number;
$invalidated &= $this->customer->check_account_number_exists($account_number);
}
} else {
@@ -399,15 +458,16 @@ class Customers extends Persons
}
if ($invalidated) {
$failCodes[] = $rowNumber;
log_message('error', "Row $rowNumber was not imported: Either email or account number already exist or data was invalid.");
} elseif ($this->customer->saveCustomer($personData, $customerData)) {
$customerIds[] = $customerData['person_id'];
$failCodes[] = $i;
log_message('error', "Row $i was not imported: Either email or account number already exist or data was invalid.");
} elseif ($this->customer->save_customer($person_data, $customer_data)) {
// Save customer to Mailchimp selected list
$this->mailchimp_lib->addOrUpdateMember($this->_list_id, $person_data['email'], $person_data['first_name'], '', $person_data['last_name']);
} else {
$failCodes[] = $rowNumber;
$failCodes[] = $i;
}
++$rowNumber;
++$i;
}
if (count($failCodes) > 0) {
@@ -415,8 +475,6 @@ class Customers extends Persons
return $this->response->setJSON(['success' => false, 'message' => $message]);
} else {
Events::trigger('customer_saved', $customerIds);
return $this->response->setJSON(['success' => true, 'message' => lang('Customers.csv_import_success')]);
}
} else {

View File

@@ -75,7 +75,7 @@ class Employees extends Persons
*/
public function getView(int $employee_id = NEW_ENTRY): string
{
$person_info = $this->employee->getInfo($employee_id);
$person_info = $this->employee->get_info($employee_id);
$current_user = $this->employee->get_logged_in_employee_info();
if ($employee_id != NEW_ENTRY && !$this->employee->canModifyEmployee($person_info->person_id, $current_user->person_id)) {
@@ -119,7 +119,7 @@ class Employees extends Persons
$current_user = $this->employee->get_logged_in_employee_info();
if ($employee_id != NEW_ENTRY) {
$target_employee = $this->employee->getInfo($employee_id);
$target_employee = $this->employee->get_info($employee_id);
if (!$this->employee->canModifyEmployee($target_employee->person_id, $current_user->person_id)) {
return $this->response->setJSON([
'success' => false,

View File

@@ -106,7 +106,7 @@ class Expenses extends Secure_Controller
}
} else {
$stored_employee_id = $expense_id == NEW_ENTRY ? $current_employee_id : $data['expenses_info']->employee_id;
$stored_employee = $this->employee->getInfo($stored_employee_id);
$stored_employee = $this->employee->get_info($stored_employee_id);
$data['employees'][$stored_employee_id] = $stored_employee->first_name . ' ' . $stored_employee->last_name;
}
$data['can_assign_employee'] = $can_assign_employee;

View File

@@ -51,7 +51,7 @@ class Home extends Secure_Controller
return $this->response->setStatusCode(403)->setBody(lang('Employees.unauthorized_modify'));
}
$person_info = $this->employee->getInfo($employeeId);
$person_info = $this->employee->get_info($employeeId);
foreach (get_object_vars($person_info) as $property => $value) {
$person_info->$property = $value;
}

View File

@@ -13,7 +13,6 @@ use App\Models\Item_taxes;
use App\Models\Stock_location;
use App\Models\Supplier;
use App\Models\Tax_category;
use CodeIgniter\Events\Events;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Images\Handlers\BaseHandler;
use CodeIgniter\HTTP\DownloadResponse;
@@ -276,7 +275,7 @@ class Items extends Secure_Controller
*/
public function getRow(string $item_ids): ResponseInterface // TODO: An array would be better for parameter.
{
$item_infos = $this->item->getMultipleInfo(explode(':', $item_ids), $this->item_lib->get_item_location());
$item_infos = $this->item->get_multiple_info(explode(':', $item_ids), $this->item_lib->get_item_location());
$result = [];
@@ -492,7 +491,7 @@ class Items extends Secure_Controller
public function getGenerateBarcodes(string $item_ids): string // TODO: Passing these through as a string instead of an array limits the contents of the item_ids. Perhaps a better approach would to serialize as JSON in an array and pass through post variables?
{
$item_ids = explode(':', $item_ids);
$result = $this->item->getMultipleInfo($item_ids, $this->item_lib->get_item_location())->getResultArray();
$result = $this->item->get_multiple_info($item_ids, $this->item_lib->get_item_location())->getResultArray();
$data['barcode_config'] = $this->barcode_lib->get_barcode_config();
foreach ($result as &$item) {
@@ -612,149 +611,148 @@ class Items extends Secure_Controller
}
/**
* @param int $itemId
* @param int $item_id
* @return ResponseInterface
* @throws ReflectionException
*/
public function postSave(int $itemId = NEW_ENTRY): ResponseInterface
public function postSave(int $item_id = NEW_ENTRY): ResponseInterface
{
$uploadData = $this->upload_image();
$uploadSuccess = empty($uploadData['error']);
$upload_data = $this->upload_image();
$upload_success = empty($upload_data['error']);
$rawReceivingQuantity = $this->request->getPost('receiving_quantity');
$raw_receiving_quantity = $this->request->getPost('receiving_quantity');
$receivingQuantity = parse_quantity($rawReceivingQuantity);
$itemType = $this->request->getPost('item_type') === null ? ITEM : intval($this->request->getPost('item_type'));
$receiving_quantity = parse_quantity($raw_receiving_quantity);
$item_type = $this->request->getPost('item_type') === null ? ITEM : intval($this->request->getPost('item_type'));
if ($receivingQuantity === 0.0 && $itemType !== ITEM_TEMP) {
$receivingQuantity = 1;
if ($receiving_quantity === 0.0 && $item_type !== ITEM_TEMP) {
$receiving_quantity = 1;
}
$defaultPackName = lang('Items.default_pack_name');
$default_pack_name = lang('Items.default_pack_name');
$costPrice = parse_decimals($this->request->getPost('cost_price'));
$unitPrice = parse_decimals($this->request->getPost('unit_price'));
$reorderLevel = parse_quantity($this->request->getPost('reorder_level'));
$quantityPerPack = parse_quantity($this->request->getPost('qty_per_pack') ?? '');
$cost_price = parse_decimals($this->request->getPost('cost_price'));
$unit_price = parse_decimals($this->request->getPost('unit_price'));
$reorder_level = parse_quantity($this->request->getPost('reorder_level'));
$qty_per_pack = parse_quantity($this->request->getPost('qty_per_pack') ?? '');
// Save item data
$itemData = [
$item_data = [
'name' => $this->request->getPost('name'),
'description' => $this->request->getPost('description', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
'category' => $this->request->getPost('category'),
'item_type' => $itemType,
'item_type' => $item_type,
'stock_type' => $this->request->getPost('stock_type') === null ? HAS_STOCK : intval($this->request->getPost('stock_type')),
'supplier_id' => empty($this->request->getPost('supplier_id')) ? null : intval($this->request->getPost('supplier_id')),
'item_number' => empty($this->request->getPost('item_number')) ? null : $this->request->getPost('item_number'),
'cost_price' => $costPrice,
'unit_price' => $unitPrice,
'reorder_level' => $reorderLevel,
'receiving_quantity' => $receivingQuantity,
'cost_price' => $cost_price,
'unit_price' => $unit_price,
'reorder_level' => $reorder_level,
'receiving_quantity' => $receiving_quantity,
'allow_alt_description' => $this->request->getPost('allow_alt_description') != null,
'is_serialized' => $this->request->getPost('is_serialized') != null,
'qty_per_pack' => $this->request->getPost('qty_per_pack') == null ? 1 : parse_quantity($quantityPerPack),
'pack_name' => $this->request->getPost('pack_name') == null ? $defaultPackName : $this->request->getPost('pack_name'),
'low_sell_item_id' => $this->request->getPost('low_sell_item_id') === null ? $itemId : intval($this->request->getPost('low_sell_item_id')),
'qty_per_pack' => $this->request->getPost('qty_per_pack') == null ? 1 : parse_quantity($qty_per_pack),
'pack_name' => $this->request->getPost('pack_name') == null ? $default_pack_name : $this->request->getPost('pack_name'),
'low_sell_item_id' => $this->request->getPost('low_sell_item_id') === null ? $item_id : intval($this->request->getPost('low_sell_item_id')),
'deleted' => $this->request->getPost('is_deleted') != null,
'hsn_code' => $this->request->getPost('hsn_code') === null ? '' : $this->request->getPost('hsn_code')
];
if ($itemData['item_type'] == ITEM_TEMP) {
$itemData['stock_type'] = HAS_NO_STOCK;
$itemData['receiving_quantity'] = 0;
$itemData['reorder_level'] = 0;
if ($item_data['item_type'] == ITEM_TEMP) {
$item_data['stock_type'] = HAS_NO_STOCK;
$item_data['receiving_quantity'] = 0;
$item_data['reorder_level'] = 0;
}
$taxCategoryId = $this->request->getPost('tax_category_id');
$tax_category_id = $this->request->getPost('tax_category_id');
if (!isset($taxCategoryId)) {
$itemData['tax_category_id'] = null;
if (!isset($tax_category_id)) {
$item_data['tax_category_id'] = null;
} else {
$itemData['tax_category_id'] = empty($this->request->getPost('tax_category_id')) ? null : intval($this->request->getPost('tax_category_id'));
$item_data['tax_category_id'] = empty($this->request->getPost('tax_category_id')) ? null : intval($this->request->getPost('tax_category_id'));
}
if (!empty($uploadData['orig_name']) && $uploadData['raw_name']) {
$itemData['pic_filename'] = $uploadData['raw_name'] . '.' . $uploadData['file_ext'];
if (!empty($upload_data['orig_name']) && $upload_data['raw_name']) {
$item_data['pic_filename'] = $upload_data['raw_name'] . '.' . $upload_data['file_ext'];
}
$employeeId = $this->employee->get_logged_in_employee_info()->person_id;
$employee_id = $this->employee->get_logged_in_employee_info()->person_id;
if ($this->item->save_value($itemData, $itemId)) {
if ($this->item->save_value($item_data, $item_id)) {
$success = true;
$newItem = false;
$new_item = false;
if ($itemId === NEW_ENTRY) {
$itemId = $itemData['item_id'];
$newItem = true;
if ($item_id === NEW_ENTRY) {
$item_id = $item_data['item_id'];
$new_item = true;
}
$useDestinationBasedTax = (bool)$this->config['use_destination_based_tax'];
$use_destination_based_tax = (bool)$this->config['use_destination_based_tax'];
if (!$useDestinationBasedTax) {
$itemsTaxesData = [];
$taxNames = $this->request->getPost('tax_names');
$taxPercents = $this->request->getPost('tax_percents');
if (!$use_destination_based_tax) {
$items_taxes_data = [];
$tax_names = $this->request->getPost('tax_names');
$tax_percents = $this->request->getPost('tax_percents');
$taxNameIndex = 0;
$tax_name_index = 0;
foreach ($taxPercents as $taxPercent) {
$taxpercentage = parse_tax($taxPercent);
foreach ($tax_percents as $tax_percent) {
$tax_percentage = parse_tax($tax_percent);
if (is_numeric($taxpercentage)) {
$itemsTaxesData[] = ['name' => $taxNames[$taxNameIndex], 'percent' => $taxpercentage];
if (is_numeric($tax_percentage)) {
$items_taxes_data[] = ['name' => $tax_names[$tax_name_index], 'percent' => $tax_percentage];
}
$taxNameIndex++;
$tax_name_index++;
}
$success &= $this->item_taxes->save_value($itemsTaxesData, $itemId);
$success &= $this->item_taxes->save_value($items_taxes_data, $item_id);
}
// Save item quantity
$stockLocations = $this->stock_location->get_undeleted_all()->getResultArray();
foreach ($stockLocations as $location) {
$updatedQuantity = parse_quantity($this->request->getPost('quantity_' . $location['location_id']));
$stock_locations = $this->stock_location->get_undeleted_all()->getResultArray();
foreach ($stock_locations as $location) {
$updated_quantity = parse_quantity($this->request->getPost('quantity_' . $location['location_id']));
if ($itemData['item_type'] == ITEM_TEMP) {
$updatedQuantity = 0;
if ($item_data['item_type'] == ITEM_TEMP) {
$updated_quantity = 0;
}
$locationDetail = [
'item_id' => $itemId,
$location_detail = [
'item_id' => $item_id,
'location_id' => $location['location_id'],
'quantity' => $updatedQuantity
'quantity' => $updated_quantity
];
$itemQuantity = $this->item_quantity->get_item_quantity($itemId, $location['location_id']);
$item_quantity = $this->item_quantity->get_item_quantity($item_id, $location['location_id']);
if ($itemQuantity->quantity != $updatedQuantity || $newItem) {
$success = $success && $this->item_quantity->save_value($locationDetail, $itemId, $location['location_id']);
if ($item_quantity->quantity != $updated_quantity || $new_item) {
$success = $success && $this->item_quantity->save_value($location_detail, $item_id, $location['location_id']);
$inv_data = [
'trans_date' => date('Y-m-d H:i:s'),
'trans_items' => $itemId,
'trans_user' => $employeeId,
'trans_items' => $item_id,
'trans_user' => $employee_id,
'trans_location' => $location['location_id'],
'trans_comment' => lang('Items.manually_editing_of_quantity'),
'trans_inventory' => $updatedQuantity - $itemQuantity->quantity
'trans_inventory' => $updated_quantity - $item_quantity->quantity
];
$success = $success && $this->inventory->insert($inv_data, false);
}
}
$success = $success && $this->saveItemAttributes($itemId);
$success = $success && $this->saveItemAttributes($item_id);
if ($success && $uploadSuccess) {
Events::trigger('item_saved', [$itemId]);
if ($success && $upload_success) {
$message = lang('Items.successful_' . ($new_item ? 'adding' : 'updating')) . ' ' . $item_data['name'];
$message = lang('Items.successful_' . ($newItem ? 'adding' : 'updating')) . ' ' . $itemData['name'];
return $this->response->setJSON(['success' => true, 'message' => $message, 'id' => $itemId]);
return $this->response->setJSON(['success' => true, 'message' => $message, 'id' => $item_id]);
} else {
$message = $uploadSuccess ? lang('Items.error_adding_updating') . ' ' . $itemData['name'] : strip_tags($uploadData['error']);
$message = $upload_success ? lang('Items.error_adding_updating') . ' ' . $item_data['name'] : strip_tags($upload_data['error']);
return $this->response->setJSON(['success' => false, 'message' => $message, 'id' => $itemId]);
return $this->response->setJSON(['success' => false, 'message' => $message, 'id' => $item_id]);
}
} else {
$message = lang('Items.error_adding_updating') . ' ' . $itemData['name'];
$message = lang('Items.error_adding_updating') . ' ' . $item_data['name'];
return $this->response->setJSON(['success' => false, 'message' => $message, 'id' => NEW_ENTRY]);
}
@@ -974,7 +972,7 @@ class Items extends Secure_Controller
}
/**
* Imports items from a CSV-formatted file.
* Imports items from a CSV formatted file.
* @return ResponseInterface
* @noinspection PhpUnused
*/
@@ -999,7 +997,7 @@ class Items extends Secure_Controller
$attributeData = [];
foreach ($attributeDefinitionNames as $definitionName) {
$attributeData[$definitionName] = $this->attribute->getDefinitionByName($definitionName)[0];
$attributeData[$definitionName] = $this->attribute->get_definition_by_name($definitionName)[0];
if ($attributeData[$definitionName]['definition_type'] === DROPDOWN) {
$attributeData[$definitionName]['dropdown_values'] = $this->attribute->get_definition_values($attributeData[$definitionName]['definition_id']);
@@ -1008,7 +1006,6 @@ class Items extends Secure_Controller
$db = db_connect();
$db->transBegin(); // TODO: This section needs to be reworked so that the data array is being created then passed to the Item model because $db doesn't exist in the controller without being instantiated, but database operations should be restricted to the model
$itemIds = [];
foreach ($csvRows as $key => $row) {
$isFailedRow = false;
$itemId = (int)$row['Id'];
@@ -1078,8 +1075,6 @@ class Items extends Secure_Controller
if ($isUpdate) {
$itemData = array_merge($itemData, get_object_vars($this->item->get_info_by_id_or_number($itemId)));
}
$itemIds[] = $itemData['item_id'];
} else {
$failedRow = $key + 2;
$failCodes[] = $failedRow;
@@ -1099,8 +1094,6 @@ class Items extends Secure_Controller
$db->transCommit();
$this->attribute->deleteOrphanedValues();
Events::trigger('item_saved', [$itemIds]);
return $this->response->setJSON(['success' => true, 'message' => lang('Items.csv_import_success')]);
}
} else {

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

@@ -33,7 +33,7 @@ class Messages extends Secure_Controller
public function getView(int $person_id = NEW_ENTRY): string
{
$person = model(Person::class);
$info = $person->getInfo($person_id);
$info = $person->get_info($person_id);
foreach (get_object_vars($info) as $property => $value) {
$info->$property = $value;

View File

@@ -49,7 +49,7 @@ abstract class Persons extends Secure_Controller
*/
public function getRow(int $row_id): ResponseInterface
{
$data_row = get_person_data_row($this->person->getInfo($row_id));
$data_row = get_person_data_row($this->person->get_info($row_id));
return $this->response->setJSON($data_row);
}

View File

@@ -1,169 +0,0 @@
<?php
namespace App\Controllers;
use App\Libraries\Plugins\PluginManager;
use CodeIgniter\HTTP\ResponseInterface;
class Plugins extends Secure_Controller
{
private PluginManager $pluginManager;
public function __construct()
{
parent::__construct('plugins');
$this->pluginManager = service('pluginManager');
}
public function getIndex(): string
{
$data['table_headers'] = get_plugin_manage_table_headers();
return view('plugins/manage', $data);
}
public function getSearch(): ResponseInterface
{
$search = strtolower($this->request->getGet('search') ?? '');
$limit = (int)($this->request->getGet('limit') ?? 0);
$offset = (int)($this->request->getGet('offset') ?? 0);
$sort = $this->sanitizeSortColumn(plugin_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'name');
$order = strtolower($this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?? 'asc');
$pluginData = $this->buildPluginDataArray();
if ($search !== '') {
$pluginData = array_values(array_filter($pluginData, static function (array $p) use ($search): bool {
return str_contains(strtolower($p['name']), $search)
|| str_contains(strtolower($p['description']), $search)
|| str_contains(strtolower($p['id']), $search);
}));
}
$total = count($pluginData);
usort($pluginData, static function (array $a, array $b) use ($sort, $order): int {
$valA = strtolower($a[$sort] ?? $a['name']);
$valB = strtolower($b[$sort] ?? $b['name']);
return $order === 'asc' ? strcmp($valA, $valB) : strcmp($valB, $valA);
});
$pluginData = $limit > 0 ? array_slice($pluginData, $offset, $limit) : array_slice($pluginData, $offset);
return $this->response->setJSON(['total' => $total, 'rows' => array_map('get_plugin_data_row', $pluginData)]);
}
public function getRow(string $pluginId): ResponseInterface
{
$plugin = $this->pluginManager->getPlugin($pluginId);
if (!$plugin) {
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.not_found')]);
}
$enabled = $this->pluginManager->getEnabledPlugins();
$pluginData = [
'id' => $plugin->getPluginId(),
'name' => $plugin->getPluginName(),
'description' => $plugin->getPluginDescription(),
'version' => $plugin->getVersion(),
'enabled' => isset($enabled[$pluginId]),
'has_config' => $plugin->getConfigView() !== null,
];
return $this->response->setJSON(get_plugin_data_row($pluginData));
}
private function buildPluginDataArray(): array
{
$plugins = $this->pluginManager->getAllPlugins();
$enabled = $this->pluginManager->getEnabledPlugins();
$result = [];
foreach ($plugins as $pluginId => $plugin) {
$result[] = [
'id' => $plugin->getPluginId(),
'name' => $plugin->getPluginName(),
'description' => $plugin->getPluginDescription(),
'version' => $plugin->getVersion(),
'enabled' => isset($enabled[$pluginId]),
'has_config' => $plugin->getConfigView() !== null,
];
}
return $result;
}
public function postEnable(string $pluginId): ResponseInterface
{
if ($this->pluginManager->enablePlugin($pluginId)) {
return $this->response->setJSON(['success' => true, 'message' => lang('Plugins.enabled')]);
}
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.enable_failed')]);
}
public function postDisable(string $pluginId): ResponseInterface
{
if ($this->pluginManager->disablePlugin($pluginId)) {
return $this->response->setJSON(['success' => true, 'message' => lang('Plugins.disabled')]);
}
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.disable_failed')]);
}
public function postUninstall(string $pluginId): ResponseInterface
{
if ($this->pluginManager->uninstallPlugin($pluginId)) {
return $this->response->setJSON(['success' => true, 'message' => lang('Plugins.uninstalled')]);
}
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.uninstall_failed')]);
}
public function getConfig(string $pluginId): ResponseInterface
{
$plugin = $this->pluginManager->getPlugin($pluginId);
if (!$plugin) {
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.not_found')]);
}
$configView = $plugin->getConfigView();
if (!$configView) {
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.no_config')]);
}
$settings = $plugin->getSettings();
$data = array_merge(['settings' => $settings, 'plugin' => $plugin], $plugin->getConfigViewData());
// Plugin views may live outside app/Views/ (absolute path from plugin's __DIR__)
if (is_file($configView . '.php')) {
$renderer = \Config\Services::renderer(dirname($configView) . DIRECTORY_SEPARATOR, null, false);
echo $renderer->setData($data)->render(basename($configView));
} else {
echo view($configView, $data);
}
return $this->response;
}
/**
* Save plugin settings by calling the plugin's saveSettings method.
*
* @param string $pluginId The plugin ID for the current plugin
* @return ResponseInterface The JSON response
* @noinspection PhpUnused Called via AJAX
*/
public function postSaveConfig(string $pluginId): ResponseInterface
{
$plugin = $this->pluginManager->getPlugin($pluginId);
if (!$plugin) {
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.not_found')]);
}
$settings = $this->request->getPost();
unset($settings['_method'], $settings[csrf_token()]);
if ($plugin->saveSettings($settings)) {
return $this->response->setJSON(['success' => true, 'message' => lang('Plugins.settings_saved')]);
}
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.settings_save_failed')]);
}
}

View File

@@ -11,7 +11,6 @@ use App\Models\Item_kit;
use App\Models\Receiving;
use App\Models\Stock_location;
use App\Models\Supplier;
use CodeIgniter\Events\Events;
use CodeIgniter\HTTP\ResponseInterface;
use Config\OSPOS;
use Config\Services;
@@ -254,7 +253,7 @@ class Receivings extends Secure_Controller
}
} else {
$stored_employee_id = $receiving_info['employee_id'];
$stored_employee = $this->employee->getInfo($stored_employee_id);
$stored_employee = $this->employee->get_info($stored_employee_id);
$data['employees'][$stored_employee_id] = $stored_employee->first_name . ' ' . $stored_employee->last_name;
}
@@ -343,12 +342,12 @@ class Receivings extends Secure_Controller
}
$employee_id = $this->employee->get_logged_in_employee_info()->person_id;
$employee_info = $this->employee->getInfo($employee_id);
$employee_info = $this->employee->get_info($employee_id);
$data['employee'] = $employee_info->first_name . ' ' . $employee_info->last_name;
$supplier_id = $this->receiving_lib->get_supplier();
if ($supplier_id != -1) {
$supplier_info = $this->supplier->getInfo($supplier_id);
$supplier_info = $this->supplier->get_info($supplier_id);
$data['supplier'] = $supplier_info->company_name; // TODO: duplicated code
$data['first_name'] = $supplier_info->first_name;
$data['last_name'] = $supplier_info->last_name;
@@ -368,7 +367,6 @@ class Receivings extends Secure_Controller
$data['error_message'] = lang('Receivings.transaction_failed');
} else {
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['receiving_id']);
Events::trigger('receiving_complete', (int) substr($data['receiving_id'], 5), $data['mode']);
}
$data['print_after_sale'] = $this->receiving_lib->is_print_after_sale();
@@ -424,12 +422,12 @@ class Receivings extends Secure_Controller
$data['reference'] = $this->receiving_lib->get_reference();
$data['receiving_id'] = 'RECV ' . $receiving_id;
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['receiving_id']);
$employee_info = $this->employee->getInfo($receiving_info['employee_id']);
$employee_info = $this->employee->get_info($receiving_info['employee_id']);
$data['employee'] = $employee_info->first_name . ' ' . $employee_info->last_name;
$supplier_id = $this->receiving_lib->get_supplier(); // TODO: Duplicated code
if ($supplier_id != -1) {
$supplier_info = $this->supplier->getInfo($supplier_id);
$supplier_info = $this->supplier->get_info($supplier_id);
$data['supplier'] = $supplier_info->company_name;
$data['first_name'] = $supplier_info->first_name;
$data['last_name'] = $supplier_info->last_name;
@@ -477,7 +475,7 @@ class Receivings extends Secure_Controller
$supplier_id = $this->receiving_lib->get_supplier();
if ($supplier_id != -1) { // TODO: Duplicated Code... replace -1 with a constant
$supplier_info = $this->supplier->getInfo($supplier_id);
$supplier_info = $this->supplier->get_info($supplier_id);
$data['supplier'] = $supplier_info->company_name;
$data['first_name'] = $supplier_info->first_name;
$data['last_name'] = $supplier_info->last_name;

View File

@@ -1343,7 +1343,7 @@ class Reports extends Secure_Controller
}
}
$customer_info = $this->customer->getInfo($customer_id);
$customer_info = $this->customer->get_info($customer_id);
$customer_name = !empty($customer_info->company_name) // TODO: This variable is not used anywhere in the code. Should it be or can it be deleted?
? "[ $customer_info->company_name ]"
: $customer_info->company_name;
@@ -1470,7 +1470,7 @@ class Reports extends Secure_Controller
}
}
$employee_info = $this->employee->getInfo($employee_id);
$employee_info = $this->employee->get_info($employee_id);
// TODO: Duplicated Code
$data = [
'title' => $employee_info->first_name . ' ' . $employee_info->last_name . ' ' . lang('Reports.report'),
@@ -1736,7 +1736,7 @@ class Reports extends Secure_Controller
];
}
$supplier_info = $this->supplier->getInfo((int) $supplier_id);
$supplier_info = $this->supplier->get_info((int) $supplier_id);
$data = [
'title' => $supplier_info->company_name . ' (' . $supplier_info->first_name . ' ' . $supplier_info->last_name . ') ' . lang('Reports.report'),
'subtitle' => $this->_get_subtitle_report(['start_date' => $start_date, 'end_date' => $end_date]),

View File

@@ -20,7 +20,6 @@ use App\Models\Stock_location;
use App\Models\Tokens\Token_invoice_count;
use App\Models\Tokens\Token_customer;
use App\Models\Tokens\Token_invoice_sequence;
use CodeIgniter\Events\Events;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Services;
use Config\OSPOS;
@@ -162,7 +161,7 @@ class Sales extends Secure_Controller
'only_bank_transfer'=> false,
'only_wallet' => false,
'only_invoices' => $this->config['invoice_enable'] && $this->request->getGet('only_invoices', FILTER_SANITIZE_NUMBER_INT),
'is_valid_receipt' => $this->sale->isValidReceipt($search)
'is_valid_receipt' => $this->sale->is_valid_receipt($search)
];
// Check if any filter is set in the multiselect dropdown
@@ -199,7 +198,7 @@ class Sales extends Secure_Controller
? $this->request->getGet('term')
: null;
if ($this->sale_lib->get_mode() == 'return' && $this->sale->isValidReceipt($receipt)) {
if ($this->sale_lib->get_mode() == 'return' && $this->sale->is_valid_receipt($receipt)) {
// If a valid receipt or invoice was found the search term will be replaced with a receipt number (POS #)
$suggestions[] = $receipt;
}
@@ -234,8 +233,8 @@ class Sales extends Secure_Controller
$customer_id = (int)$this->request->getPost('customer', FILTER_SANITIZE_NUMBER_INT);
if ($this->customer->exists($customer_id)) {
$this->sale_lib->set_customer($customer_id);
$discount = $this->customer->getInfo($customer_id)->discount;
$discount_type = $this->customer->getInfo($customer_id)->discount_type;
$discount = $this->customer->get_info($customer_id)->discount;
$discount_type = $this->customer->get_info($customer_id)->discount_type;
// Apply customer default discount to items that have 0 discount
if ($discount != '') {
@@ -438,9 +437,9 @@ class Sales extends Secure_Controller
}
} elseif ($payment_type === lang('Sales.rewards')) {
$customer_id = $this->sale_lib->get_customer();
$package_id = $this->customer->getInfo($customer_id)->package_id;
$package_id = $this->customer->get_info($customer_id)->package_id;
if (!empty($package_id)) {
$points = $this->customer->getInfo($customer_id)->points;
$points = $this->customer->get_info($customer_id)->points;
$points = ($points == null ? 0 : $points);
$payments = $this->sale_lib->get_payments();
@@ -512,8 +511,8 @@ class Sales extends Secure_Controller
$customer_id = $this->sale_lib->get_customer();
if ($customer_id != NEW_ENTRY) {
// Load the customer discount if any
$customer_discount = $this->customer->getInfo($customer_id)->discount;
$customer_discount_type = $this->customer->getInfo($customer_id)->discount_type;
$customer_discount = $this->customer->get_info($customer_id)->discount;
$customer_discount_type = $this->customer->get_info($customer_id)->discount_type;
if ($customer_discount != '') {
$discount = $customer_discount;
$discount_type = $customer_discount_type;
@@ -526,7 +525,7 @@ class Sales extends Secure_Controller
$quantity = ($mode == 'return') ? -$quantity : $quantity;
$item_location = $this->sale_lib->get_sale_location();
if ($mode == 'return' && $this->sale->isValidReceipt($item_id_or_number_or_item_kit_or_receipt)) {
if ($mode == 'return' && $this->sale->is_valid_receipt($item_id_or_number_or_item_kit_or_receipt)) {
$this->sale_lib->return_entire_sale($item_id_or_number_or_item_kit_or_receipt);
} elseif ($this->item_kit->is_valid_item_kit($item_id_or_number_or_item_kit_or_receipt)) {
// Add kit item to order if one is assigned
@@ -704,7 +703,7 @@ class Sales extends Secure_Controller
$data['show_stock_locations'] = $this->stock_location->show_locations('sales');
$data['comments'] = $this->sale_lib->get_comment();
$employee_id = $this->employee->get_logged_in_employee_info()->person_id;
$employee_info = $this->employee->getInfo($employee_id);
$employee_info = $this->employee->get_info($employee_id);
$data['employee'] = $employee_info->first_name . ' ' . mb_substr($employee_info->last_name, 0, 1);
$data['company_info'] = implode("\n", [$this->config['address'], $this->config['phone']]);
@@ -917,8 +916,6 @@ class Sales extends Secure_Controller
}
$data['receipt_template_view'] = $receipt_template;
Events::trigger('sale_complete', $data['sale_id_num'], $sale_type);
$this->sale_lib->clear_all();
return view('sales/receipt', $data);
}
@@ -1021,7 +1018,7 @@ class Sales extends Secure_Controller
$customer_info = '';
if ($customer_id != NEW_ENTRY) {
$customer_info = $this->customer->getInfo($customer_id);
$customer_info = $this->customer->get_info($customer_id);
$data['customer_id'] = $customer_id;
if (!empty($customer_info->company_name)) {
@@ -1044,11 +1041,11 @@ class Sales extends Secure_Controller
$data['customer_account_number'] = $customer_info->account_number;
$data['customer_discount'] = $customer_info->discount;
$data['customer_discount_type'] = $customer_info->discount_type;
$package_id = $this->customer->getInfo($customer_id)->package_id;
$package_id = $this->customer->get_info($customer_id)->package_id;
if ($package_id != null) {
$package_name = $this->customer_rewards->get_name($package_id);
$points = $this->customer->getInfo($customer_id)->points;
$points = $this->customer->get_info($customer_id)->points;
$data['customer_rewards']['package_id'] = $package_id;
$data['customer_rewards']['points'] = empty($points) ? 0 : $points;
$data['customer_rewards']['package_name'] = $package_name;
@@ -1127,7 +1124,7 @@ class Sales extends Secure_Controller
$data['amount_change'] = $data['amount_due'] * -1;
$employee_info = $this->employee->getInfo($this->sale_lib->get_employee());
$employee_info = $this->employee->get_info($this->sale_lib->get_employee());
$data['employee'] = $employee_info->first_name . ' ' . mb_substr($employee_info->last_name, 0, 1);
$this->_load_customer_data($this->sale_lib->get_customer(), $data);
@@ -1342,7 +1339,7 @@ class Sales extends Secure_Controller
$sale_info = $this->sale->get_info($sale_id)->getRowArray();
$data['selected_customer_id'] = $sale_info['customer_id'];
$data['selected_customer_name'] = $sale_info['customer_name'];
$employee_info = $this->employee->getInfo($sale_info['employee_id']);
$employee_info = $this->employee->get_info($sale_info['employee_id']);
$data['selected_employee_id'] = $sale_info['employee_id'];
$data['selected_employee_name'] = $employee_info->first_name . ' ' . $employee_info->last_name;
$data['sale_info'] = $sale_info;

View File

@@ -99,10 +99,10 @@ class Secure_Controller extends BaseController
}
/**
* @param string $key
* @param $key
* @return mixed|void
*/
public function getConfig(string $key)
public function getConfig($key)
{
if (isset($config[$key])) {
return $config[$key];

View File

@@ -34,7 +34,7 @@ class Suppliers extends Persons
*/
public function getRow($row_id): ResponseInterface
{
$data_row = get_supplier_data_row($this->supplier->getInfo($row_id));
$data_row = get_supplier_data_row($this->supplier->get_info($row_id));
$data_row['category'] = $this->supplier->get_category_name($data_row['category']);
return $this->response->setJSON($data_row);
@@ -97,7 +97,7 @@ class Suppliers extends Persons
*/
public function getView(int $supplier_id = NEW_ENTRY): string
{
$info = $this->supplier->getInfo($supplier_id);
$info = $this->supplier->get_info($supplier_id);
foreach (get_object_vars($info) as $property => $value) {
$info->$property = $value;
}

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

@@ -1,20 +0,0 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class PluginConfigTableCreate extends Migration
{
public function up(): void
{
log_message('info', 'Migrating plugin_config table started');
execute_script(APPPATH . 'Database/Migrations/sqlscripts/3.5.0_PluginConfigTableCreate.sql');
}
public function down(): void
{
$this->forge->dropTable('plugin_config', true);
}
}

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

@@ -1,21 +0,0 @@
CREATE TABLE IF NOT EXISTS `ospos_plugin_config` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`plugin_id` varchar(100) NOT NULL,
`key` varchar(100) NOT NULL,
`value` text NOT NULL,
`is_control` tinyint(1) NOT NULL DEFAULT 0,
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `uq_plugin_key` (`plugin_id`, `key`),
KEY `idx_plugin_id` (`plugin_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT IGNORE INTO `ospos_modules` (`name_lang_key`, `desc_lang_key`, `sort`, `module_id`) VALUES
('module_plugins', 'module_plugins_desc', 111, 'plugins');
INSERT IGNORE INTO `ospos_permissions` (`permission_id`, `module_id`) VALUES
('plugins', 'plugins');
INSERT IGNORE INTO `ospos_grants` (`permission_id`, `person_id`, `menu_group`)
SELECT 'plugins', `person_id`, 'office' FROM `ospos_grants` WHERE `permission_id` = 'config';

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,19 +0,0 @@
<?php
use CodeIgniter\Events\Events;
if (!function_exists('pluginContent')) {
function pluginContent(string $section, array $data = []): string
{
ob_start();
Events::trigger("view:{$section}", $data);
return ob_get_clean() ?: '';
}
}
if (!function_exists('pluginContentExists')) {
function pluginContentExists(string $section): bool
{
return !empty(Events::listeners("view:{$section}"));
}
}

View File

@@ -933,50 +933,6 @@ function get_controller(): string
return end($controller_name_parts);
}
function plugin_headers(): array
{
return [
['name' => lang('Plugins.name'), 'escape' => false],
['description' => lang('Plugins.description')],
['version' => lang('Plugins.version'), 'escape' => false],
['status' => lang('Plugins.status'), 'escape' => false],
];
}
function get_plugin_manage_table_headers(): string
{
return transform_headers(plugin_headers(), false, true);
}
function get_plugin_data_row(array $plugin): array
{
$pluginId = $plugin['id'];
$statusHtml = $plugin['enabled']
? '<span class="label label-success">' . lang('Plugins.active') . '</span>'
: '<span class="label label-default">' . lang('Plugins.inactive') . '</span>';
$editHtml = $plugin['enabled']
? '<button class="btn btn-warning btn-xs plugin-action" data-action="disable" data-plugin-id="' . esc($pluginId) . '">'
. '<span class="glyphicon glyphicon-pause"></span> ' . lang('Plugins.disable') . '</button>'
: '<button class="btn btn-success btn-xs plugin-action" data-action="enable" data-plugin-id="' . esc($pluginId) . '">'
. '<span class="glyphicon glyphicon-play"></span> ' . lang('Plugins.enable') . '</button>';
if ($plugin['has_config'] && $plugin['enabled']) {
$editHtml .= ' <button class="btn btn-primary btn-xs plugin-config" data-plugin-id="' . esc($pluginId) . '">'
. '<span class="glyphicon glyphicon-cog"></span> ' . lang('Plugins.configure') . '</button>';
}
return [
'plugin_id' => $pluginId,
'name' => '<strong>' . esc($plugin['name']) . '</strong><br><small class="text-muted">' . esc($pluginId) . '</small>',
'description' => esc($plugin['description']),
'version' => '<span class="label label-default">' . esc($plugin['version']) . '</span>',
'status' => $statusHtml,
'edit' => $editHtml,
];
}
/**
* Restores filter values from the URL query string.
*

View File

@@ -166,6 +166,8 @@ return [
"info" => "معلومات",
"info_configuration" => "معلومات الشركة",
"input_groups" => "مجموعات الإدخال",
"integrations" => "التكامل",
"integrations_configuration" => "تكامل",
"invoice" => "الفاتورة",
"invoice_configuration" => "إعدادات طباعة الفاتورة",
"invoice_default_comments" => "التعليق الافتراضي على الفاتورة",
@@ -196,6 +198,13 @@ return [
"location_info" => "معلومات تهيئة الأماكن",
"login_form" => "نمط نموذج تسجيل الدخول",
"logout" => "هل تريد عمل نسخة إحتياطية قبل الخروج؟ اضغط [نعم] لعمل النسخة أو [الغاء] للخروج.",
"mailchimp" => "ميل تشامب",
"mailchimp_api_key" => "مفتاح ميل شيمب",
"mailchimp_configuration" => "إعدادات ميل شيمب",
"mailchimp_key_successfully" => "نجاح.",
"mailchimp_key_unsuccessfully" => "فشل.",
"mailchimp_lists" => "إعدادات ميل شيمب",
"mailchimp_tooltip" => "انقر على رمز مفتاح API.",
"message" => "الرسائل",
"message_configuration" => "إعدادات الرسائل",
"msg_msg" => "الرسائل النصية المحفوظة",

View File

@@ -28,6 +28,16 @@ return [
"employee" => "الموظف",
"error_adding_updating" => "خطاء فى إضافة أو تحديث العميل.",
"import_items_csv" => "استيراد العملا ء من ورقة عمل اكسل",
"mailchimp_activity_click" => "النقر على البريد الإلكتروني",
"mailchimp_activity_lastopen" => "آخر رسالة إلكترونية مفتوحة",
"mailchimp_activity_open" => "رسالة إلكترونية مفتوحة",
"mailchimp_activity_total" => "تم ارسال الرسالة الإلكترونية بنجاح",
"mailchimp_activity_unopen" => "رسالة إلكترونية غير مفتوحة",
"mailchimp_email_client" => "بريد الكتروني",
"mailchimp_info" => "ميل تشيمب",
"mailchimp_member_rating" => "التقييم",
"mailchimp_status" => "الحالة",
"mailchimp_vip" => "مهم",
"max" => "الحد الأقصى",
"min" => "الحد الأدنى",
"new" => "عميل جديد",

View File

@@ -32,7 +32,6 @@ return [
"migrate_desc" => "تحديث قاعدة البيانات.",
"office" => "المكتب",
"office_desc" => "اظهار الائحة المكتبية.",
'plugins' => 'الإضافات',
"receivings" => "استلام الأصناف",
"receivings_desc" => "معالجة أوامر الشراء و استلام الأصناف.",
"reports" => "التقارير",

View File

@@ -1,27 +0,0 @@
<?php
return [
'actions' => 'إجراءات',
'active' => 'نشط',
'configure' => 'تكوين',
'description' => 'الوصف',
'disable' => 'تعطيل',
'disable_failed' => 'فشل تعطيل الإضافة',
'disabled' => 'تم تعطيل الإضافة بنجاح',
'enable' => 'تفعيل',
'enable_failed' => 'فشل تفعيل الإضافة',
'enabled' => 'تم تفعيل الإضافة بنجاح',
'inactive' => 'غير نشط',
'management' => 'إدارة الإضافات',
'name' => 'اسم الإضافة',
'no_config' => 'هذه الإضافة لا تحتوي على خيارات تكوين',
'no_plugins_to_display' => 'لا توجد إضافات للعرض',
'not_found' => 'الإضافة غير موجودة',
'plugins' => 'الإضافات',
'settings_save_failed' => 'فشل حفظ إعدادات الإضافة',
'settings_saved' => 'تم حفظ إعدادات الإضافة بنجاح',
'status' => 'الحالة',
'uninstall' => 'إلغاء التثبيت',
'uninstall_failed' => 'فشل إلغاء تثبيت الإضافة',
'uninstalled' => 'تم إلغاء تثبيت الإضافة بنجاح',
'version' => 'الإصدار',
];

View File

@@ -41,6 +41,7 @@ return [
"customer_discount" => "الخصم",
"customer_email" => "البريد الإلكترونى",
"customer_location" => "المكان",
"customer_mailchimp_status" => "حالة بريد ميل تشيمب",
"customer_optional" => "(مطلوب للدفعات المستحقة)",
"customer_required" => "(اجباري)",
"customer_total" => "المجموع",

View File

@@ -166,6 +166,8 @@ return [
"info" => "معلومات",
"info_configuration" => "معلومات الشركة",
"input_groups" => "مجموعات الإدخال",
"integrations" => "التكامل",
"integrations_configuration" => "تكامل",
"invoice" => "الفاتورة",
"invoice_configuration" => "إعدادات طباعة الفاتورة",
"invoice_default_comments" => "التعليق الافتراضي على الفاتورة",
@@ -196,6 +198,13 @@ return [
"location_info" => "معلومات تهيئة الأماكن",
"login_form" => "نمط نموذج تسجيل الدخول",
"logout" => "هل تريد عمل نسخة إحتياطية قبل الخروج؟ اضغط [نعم] لعمل النسخة أو [الغاء] للخروج.",
"mailchimp" => "ميل تشامب",
"mailchimp_api_key" => "مفتاح ميل شيمب",
"mailchimp_configuration" => "إعدادات ميل شيمب",
"mailchimp_key_successfully" => "نجاح.",
"mailchimp_key_unsuccessfully" => "فشل.",
"mailchimp_lists" => "قوائم ميل شيمب",
"mailchimp_tooltip" => "انقر على رمز مفتاح API.",
"message" => "الرسائل",
"message_configuration" => "إعدادات الرسائل",
"msg_msg" => "الرسائل النصية المحفوظة",

View File

@@ -28,6 +28,16 @@ return [
"employee" => "الموظف",
"error_adding_updating" => "خطاء فى إضافة أو تحديث العميل.",
"import_items_csv" => "استيراد العملا ء من ورقة عمل اكسل",
"mailchimp_activity_click" => "النقر على البريد الإلكتروني",
"mailchimp_activity_lastopen" => "آخر رسالة إلكترونية مفتوحة",
"mailchimp_activity_open" => "رسالة إلكترونية مفتوحة",
"mailchimp_activity_total" => "تم ارسال الرسالة الإلكترونية بنجاح",
"mailchimp_activity_unopen" => "رسالة إلكترونية غير مفتوحة",
"mailchimp_email_client" => "بريد الكتروني",
"mailchimp_info" => "ميل تشيمب",
"mailchimp_member_rating" => "التقييم",
"mailchimp_status" => "الحالة",
"mailchimp_vip" => "مهم",
"max" => "الحد الأقصى",
"min" => "الحد الأدنى",
"new" => "عميل جديد",

View File

@@ -32,7 +32,6 @@ return [
"migrate_desc" => "تحديث قاعدة البيانات.",
"office" => "المكتب",
"office_desc" => "اظهار الائحة المكتبية.",
'plugins' => 'الإضافات',
"receivings" => "استلام الأصناف",
"receivings_desc" => "معالجة أوامر الشراء و استلام الأصناف.",
"reports" => "التقارير",

View File

@@ -1,27 +0,0 @@
<?php
return [
'actions' => 'إجراءات',
'active' => 'نشط',
'configure' => 'تكوين',
'description' => 'الوصف',
'disable' => 'تعطيل',
'disable_failed' => 'فشل تعطيل الإضافة',
'disabled' => 'تم تعطيل الإضافة بنجاح',
'enable' => 'تفعيل',
'enable_failed' => 'فشل تفعيل الإضافة',
'enabled' => 'تم تفعيل الإضافة بنجاح',
'inactive' => 'غير نشط',
'management' => 'إدارة الإضافات',
'name' => 'اسم الإضافة',
'no_config' => 'هذه الإضافة لا تحتوي على خيارات تكوين',
'no_plugins_to_display' => 'لا توجد إضافات للعرض',
'not_found' => 'الإضافة غير موجودة',
'plugins' => 'الإضافات',
'settings_save_failed' => 'فشل حفظ إعدادات الإضافة',
'settings_saved' => 'تم حفظ إعدادات الإضافة بنجاح',
'status' => 'الحالة',
'uninstall' => 'إلغاء التثبيت',
'uninstall_failed' => 'فشل إلغاء تثبيت الإضافة',
'uninstalled' => 'تم إلغاء تثبيت الإضافة بنجاح',
'version' => 'الإصدار',
];

View File

@@ -41,6 +41,7 @@ return [
"customer_discount" => "الخصم",
"customer_email" => "البريد الإلكترونى",
"customer_location" => "المكان",
"customer_mailchimp_status" => "حالة بريد ميل تشيمب",
"customer_optional" => "(مطلوب للدفعات المستحقة)",
"customer_required" => "(اجباري)",
"customer_total" => "المجموع",

View File

@@ -166,6 +166,8 @@ return [
"info" => "Məlumat",
"info_configuration" => "Dükan İnformasiyası",
"input_groups" => "",
"integrations" => "İnteqrasiya",
"integrations_configuration" => "Üçüncü tərəf inteqrasiya",
"invoice" => "Faktura",
"invoice_configuration" => "Faktura Çap Parametrləri",
"invoice_default_comments" => "Standart Faktura Şərhləri",
@@ -196,6 +198,13 @@ return [
"location_info" => "Yer Konfiqurasiya Məlumatı",
"login_form" => "",
"logout" => "Çıxışdan əvvəl məlumatlari ehtiyat bazasına köçürmək istəyirsinizmi? Çıxış üçün Bekap və ya [Ləğv] üçün [OK]' düyməsinə basın.",
"mailchimp" => "Mailçimp",
"mailchimp_api_key" => "Mailchimp API Açarı",
"mailchimp_configuration" => "Mailchimp Konfiqurasiyası",
"mailchimp_key_successfully" => "API Açarı etibarlıdır.",
"mailchimp_key_unsuccessfully" => "API Açarı etibarsızdır.",
"mailchimp_lists" => "Mailchimp siyahısı (lar)",
"mailchimp_tooltip" => "API Açarının İşarəsinə basın.",
"message" => "Mesaj",
"message_configuration" => "Mesaj Konfiqurasiyası",
"msg_msg" => "Saxlanılan Mətn Mesajı",

View File

@@ -28,6 +28,16 @@ return [
"employee" => "Əməkdaş",
"error_adding_updating" => "Müştəri əlavəsində ya da yenilənməsində XƏTA.",
"import_items_csv" => "CSVdən müştəri əlavə et",
"mailchimp_activity_click" => "Elektron poçt düyməsi",
"mailchimp_activity_lastopen" => "Son açılan məktub",
"mailchimp_activity_open" => "ıq məktub",
"mailchimp_activity_total" => "Məktub göndərildi",
"mailchimp_activity_unopen" => "ılmamış məktub",
"mailchimp_email_client" => "Müştəriyə Məktub Göndər",
"mailchimp_info" => "Mailchimp-də",
"mailchimp_member_rating" => "Reytinq",
"mailchimp_status" => "Status",
"mailchimp_vip" => "siz silmək üçün heç bir müştəri seçməmisiniz",
"max" => "Ən çox xərclənən",
"min" => "Ən az xərclənən",
"new" => "Yeni Müştəri",

View File

@@ -32,7 +32,6 @@ return [
"migrate_desc" => "ALSAN Məlumat Bazasıni Yenilə.",
"office" => "Ofis",
"office_desc" => "Ofis menyusu siyahısı modulları.",
'plugins' => 'Plaginlər',
"receivings" => "Qəbul Edilənlər",
"receivings_desc" => "Satınalma sifarişləri işləyin.",
"reports" => "Hesabatlar",

View File

@@ -1,27 +0,0 @@
<?php
return [
'actions' => 'Əməliyyatlar',
'active' => 'Aktiv',
'configure' => 'Konfiqurasiya',
'description' => 'Təsvir',
'disable' => 'Deaktiv et',
'disable_failed' => 'Plagini deaktiv etmək alınmadı',
'disabled' => 'Plagin uğurla deaktiv edildi',
'enable' => 'Aktiv et',
'enable_failed' => 'Plagini aktiv etmək alınmadı',
'enabled' => 'Plagin uğurla aktiv edildi',
'inactive' => 'Deaktiv',
'management' => 'Plagin idarəetməsi',
'name' => 'Plagin adı',
'no_config' => 'Bu plaginin konfiqurasiya seçimləri yoxdur',
'no_plugins_to_display' => 'Göstəriləcək plagin yoxdur',
'not_found' => 'Plagin tapılmadı',
'plugins' => 'Plaginlər',
'settings_save_failed' => 'Plagin parametrlərini saxlamaq alınmadı',
'settings_saved' => 'Plagin parametrləri uğurla saxlandı',
'status' => 'Status',
'uninstall' => 'Sil',
'uninstall_failed' => 'Plagini silmək alınmadı',
'uninstalled' => 'Plagin uğurla silindi',
'version' => 'Versiya',
];

View File

@@ -41,6 +41,7 @@ return [
"customer_discount" => "Endirim",
"customer_email" => "E-poçt",
"customer_location" => "Yer",
"customer_mailchimp_status" => "Mailchimp Statusu",
"customer_optional" => "(Ödənişlərdə tələb olunur)",
"customer_required" => "(Vacib)",
"customer_total" => "Cəmi",

View File

@@ -166,6 +166,8 @@ return [
"info" => "Information",
"info_configuration" => "Store Information",
"input_groups" => "",
"integrations" => "",
"integrations_configuration" => "",
"invoice" => "Invoice",
"invoice_configuration" => "Invoice Print Settings",
"invoice_default_comments" => "Default Invoice Comments",
@@ -196,6 +198,13 @@ return [
"location_info" => "Location Configuration Information",
"login_form" => "",
"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",

View File

@@ -28,6 +28,16 @@ return [
"employee" => "Служител",
"error_adding_updating" => "Добавянето или актуализирането на клиента е неуспешно.",
"import_items_csv" => "Импортиране на клиент от CSV",
"mailchimp_activity_click" => "Email click",
"mailchimp_activity_lastopen" => "Последно отворен Имейл",
"mailchimp_activity_open" => "Имейлът е отворен",
"mailchimp_activity_total" => "Имейлът е изпратен",
"mailchimp_activity_unopen" => "Имейлът е неотворен",
"mailchimp_email_client" => "Имейл клиент",
"mailchimp_info" => "Mailchimp (Услуга)",
"mailchimp_member_rating" => "Оценка",
"mailchimp_status" => "Статус",
"mailchimp_vip" => "VIP",
"max" => "Максимално похарчени",
"min" => "Минимално похарчено",
"new" => "Нов клиент",

View File

@@ -32,7 +32,6 @@ return [
"migrate_desc" => "Update the OSPOS Database.",
"office" => "Office",
"office_desc" => "List office menu modules.",
'plugins' => 'Плъгини',
"receivings" => "Receivings",
"receivings_desc" => "Process Purchase Orders.",
"reports" => "Reports",

View File

@@ -1,27 +0,0 @@
<?php
return [
'actions' => 'Действия',
'active' => 'Активен',
'configure' => 'Конфигуриране',
'description' => 'Описание',
'disable' => 'Деактивиране',
'disable_failed' => 'Неуспешно деактивиране на приставката',
'disabled' => 'Приставката е деактивирана успешно',
'enable' => 'Активиране',
'enable_failed' => 'Неуспешно активиране на приставката',
'enabled' => 'Приставката е активирана успешно',
'inactive' => 'Неактивен',
'management' => 'Управление на приставки',
'name' => 'Име на приставката',
'no_config' => 'Тази приставка няма опции за конфигурация',
'no_plugins_to_display' => 'Няма приставки за показване',
'not_found' => 'Приставката не е намерена',
'plugins' => 'Приставки',
'settings_save_failed' => 'Неуспешно запазване на настройките на приставката',
'settings_saved' => 'Настройките на приставката са запазени успешно',
'status' => 'Статус',
'uninstall' => 'Деинсталиране',
'uninstall_failed' => 'Неуспешно деинсталиране на приставката',
'uninstalled' => 'Приставката е деинсталирана успешно',
'version' => 'Версия',
];

View File

@@ -41,7 +41,7 @@ return [
"customer_discount" => "Намаление",
"customer_email" => "Електронна поща",
"customer_location" => "Местоположение",
"mailchimp_customer_status" => "Състояние на Mailchimp",
"customer_mailchimp_status" => "Състояние на Mailchimp",
"customer_optional" => "(Незадължително)",
"customer_required" => "(Задължително)",
"customer_total" => "Обща сума",

View File

@@ -166,6 +166,8 @@ return [
"info" => "Informacije",
"info_configuration" => "Info o web trgovini",
"input_groups" => "Grupe unosa",
"integrations" => "Integracije",
"integrations_configuration" => "Integracije trećih strana",
"invoice" => "Faktura",
"invoice_configuration" => "Podešavanja štamapnja",
"invoice_default_comments" => "Komentar na fakturi",
@@ -196,6 +198,13 @@ return [
"location_info" => "Informacije o konfiguraciji lokacije",
"login_form" => "Stil formulara za prijavu",
"logout" => "Zar ne želite da napravite rezervnu kopiju prije odjave? Kliknite [OK] za sigurnosnu kopiju, [Cancel] da biste se odjavili.",
"mailchimp" => "MeilChimp",
"mailchimp_api_key" => "MailChimp API ključ",
"mailchimp_configuration" => "MailChimp konfiguracija",
"mailchimp_key_successfully" => "API ključ je važeći.",
"mailchimp_key_unsuccessfully" => "API ključ je nevažeći.",
"mailchimp_lists" => "MailChimp lista(e)",
"mailchimp_tooltip" => "Kliknite na ikonu za API ključ.",
"message" => "Poruke",
"message_configuration" => "Konfigurisanje poruke",
"msg_msg" => "Snimljena tekst poruka",

View File

@@ -28,6 +28,16 @@ return [
"employee" => "Zaposlenik",
"error_adding_updating" => "Dodavanje ili ažuriranje kupca nije uspjelo.",
"import_items_csv" => "Uvezi kupce iz CSV datoteke",
"mailchimp_activity_click" => "Klik na e-mail",
"mailchimp_activity_lastopen" => "Zadnji otvoreni e-mail",
"mailchimp_activity_open" => "E-mail otvoren",
"mailchimp_activity_total" => "E-mail poslat",
"mailchimp_activity_unopen" => "E-mail nije otvoren",
"mailchimp_email_client" => "E-mail klijenta",
"mailchimp_info" => "MeilChimp",
"mailchimp_member_rating" => "Ocjena",
"mailchimp_status" => "Status",
"mailchimp_vip" => "VIP",
"max" => "Maks. potrošeno",
"min" => "Min. potrošeno",
"new" => "Novi kupac",

View File

@@ -32,7 +32,6 @@ return [
"migrate_desc" => "Ažurirajte OSPOS bazu podataka.",
"office" => "Administracija",
"office_desc" => "Lista modula kancelarijskog menija.",
'plugins' => 'Dodaci',
"receivings" => "Ulazi",
"receivings_desc" => "Obrada narudžbenica.",
"reports" => "Izvještaji",

View File

@@ -1,27 +0,0 @@
<?php
return [
'actions' => 'Akcije',
'active' => 'Aktivno',
'configure' => 'Konfiguracija',
'description' => 'Opis',
'disable' => 'Onemogući',
'disable_failed' => 'Nije uspjelo onemogućavanje dodatka',
'disabled' => 'Dodatak je uspješno onemogućen',
'enable' => 'Omogući',
'enable_failed' => 'Nije uspjelo omogućavanje dodatka',
'enabled' => 'Dodatak je uspješno omogućen',
'inactive' => 'Neaktivno',
'management' => 'Upravljanje dodacima',
'name' => 'Naziv dodatka',
'no_config' => 'Ovaj dodatak nema opcija konfiguracije',
'no_plugins_to_display' => 'Nema dodataka za prikaz',
'not_found' => 'Dodatak nije pronađen',
'plugins' => 'Dodaci',
'settings_save_failed' => 'Nije uspjelo spremanje postavki dodatka',
'settings_saved' => 'Postavke dodatka su uspješno sačuvane',
'status' => 'Status',
'uninstall' => 'Deinstaliraj',
'uninstall_failed' => 'Nije uspjelo deinstaliranje dodatka',
'uninstalled' => 'Dodatak je uspješno deinstaliran',
'version' => 'Verzija',
];

View File

@@ -41,6 +41,7 @@ return [
"customer_discount" => "Popust",
"customer_email" => "E-mail kupca",
"customer_location" => "Mjesto kupca",
"customer_mailchimp_status" => "Status MailChimp-a",
"customer_optional" => "(Potrebno za odloženo plaćanje)",
"customer_required" => "Obavezno",
"customer_total" => "Ukupno",

View File

@@ -166,8 +166,8 @@ return [
'info' => "زانیاری",
'info_configuration' => "زانیاری فڕۆشتگا",
'input_groups' => "گروپەکانی زانیارییە پێدراوەکان",
'plugins' => "یەکگرتنەکان",
'plugins_configuration' => "یەکگرتنەکانی لایەنی سێیەم",
'integrations' => "یەکگرتنەکان",
'integrations_configuration' => "یەکگرتنەکانی لایەنی سێیەم",
'invoice' => "فاکتۆرە",
'invoice_configuration' => "ڕێکخستنەکانی چاپی فاکتورە",
'invoice_default_comments' => "سەرنجەکانی فاکتۆرەی بنەڕەتیی",
@@ -198,6 +198,13 @@ return [
'location_info' => "زانیاری ڕێکخستنی شوێن",
'login_form' => "ستایلی فۆڕمی چوونەژوورەوە",
'logout' => "دەتەوێت پاڵپشت دروست بکەیت پێش چوونە دەرەوە؟ کرتە بکە لەسەر [باشە] بۆ پاڵپشت دروستکردن یان [هەڵوەشاندنەوە] بۆ چوونە دەرەوە.",
'mailchimp' => "مەیڵچیمپ",
'mailchimp_api_key' => "کلیلی (ئەی پی ئای)ی مەیڵچیمپ",
'mailchimp_configuration' => "ڕێکخستنی مەیڵچیمپ",
'mailchimp_key_successfully' => "کلیلی (ئەی پی ئای) دروستە.",
'mailchimp_key_unsuccessfully' => "کلیلی (ئەی پی ئای) نادروستە.",
'mailchimp_lists' => "لیست(ەکان)ی مەیڵچیمپ",
'mailchimp_tooltip' => "کرتە لەسەر ئایکۆنی کلیلی (ئەی پی ئای) بکە.",
'message' => "نامە",
'message_configuration' => "ڕێکخستنی نامە",
'msg_msg' => "دەقی نامەی پاشەکەوتکراو",

View File

@@ -28,6 +28,16 @@ return [
'employee' => "فەرمانبەر",
'error_adding_updating' => "زیادکردن یان نوێکردنەوەی کڕیار سەرکەوتوو نەبوو.",
'import_items_csv' => "هاوردەکردنی کڕیار لەڕێگایCSV",
'mailchimp_activity_click' => "کرتەی ئیمەیل",
'mailchimp_activity_lastopen' => "دوایین ئیمەیڵی کراوە",
'mailchimp_activity_open' => "ئیمەیڵ کرایەوە",
'mailchimp_activity_total' => "ئیمەیڵ نێردرا",
'mailchimp_activity_unopen' => "ئیمەیڵ نەکراوە",
'mailchimp_email_client' => "کڕیاری ئیمەیل",
'mailchimp_info' => "مەیڵچیمپ",
'mailchimp_member_rating' => "پلەپێدان",
'mailchimp_status' => "دۆخ",
'mailchimp_vip' => "ڤی ئای پی",
'max' => "زۆرترین. خەرجکراو",
'min' => "کەمترین. خەرجکراو",
'new' => "کڕیاری نوێ",

View File

@@ -32,7 +32,6 @@ return [
'migrate_desc' => "داتابەیسی OSPOS نوێ بکەرەوە.",
'office' => "ئۆفیس",
'office_desc' => "لیستی مۆدۆلی پێڕستی ئۆفیس نیشان بدە.",
'plugins' => 'پڵەگینەکان',
'receivings' => "وەرگرتنەکان",
'receivings_desc' => "پرۆسەی داواکاری کڕین.",
'reports' => "ڕاپۆرتەکان",

View File

@@ -1,27 +0,0 @@
<?php
return [
'actions' => 'کردارەکان',
'active' => 'چالاک',
'configure' => 'ڕێکخستن',
'description' => 'وەسف',
'disable' => 'ناچالاک کردن',
'disable_failed' => 'ناکامی لە ناچالاک کردنی پڵەگین',
'disabled' => 'پڵەگین بە سەرکەوتوویی ناچالاک کرا',
'enable' => 'چالاک کردن',
'enable_failed' => 'ناکامی لە چالاک کردنی پڵەگین',
'enabled' => 'پڵەگین بە سەرکەوتوویی چالاک کرا',
'inactive' => 'ناچالاک',
'management' => 'بەڕێوەبردنی پڵەگین',
'name' => 'ناوی پڵەگین',
'no_config' => 'ئەم پڵەگینە هیچ بژاردەیەکی ڕێکخستن نییە',
'no_plugins_to_display' => 'هیچ پڵەگینێک بۆ نیشاندان نییە',
'not_found' => 'پڵەگین نەدۆزرایەوە',
'plugins' => 'پڵەگینەکان',
'settings_save_failed' => 'ناکامی لە پاشەکەوت کردنی ڕێکخستنەکانی پڵەگین',
'settings_saved' => 'ڕێکخستنەکانی پڵەگین بە سەرکەوتوویی پاشەکەوت کران',
'status' => 'باری',
'uninstall' => 'لادانی دامەزراندن',
'uninstall_failed' => 'ناکامی لە لادانی دامەزراندنی پڵەگین',
'uninstalled' => 'پڵەگین بە سەرکەوتوویی لادرا',
'version' => 'وەشان',
];

View File

@@ -41,6 +41,7 @@ return [
'customer_discount' => "داشکاندن",
'customer_email' => "ئیمەیڵ",
'customer_location' => "ناونیشان",
'customer_mailchimp_status' => "دۆخی بەکارهێنان مایلچیمپ",
'customer_optional' => "(پێویستە بۆئەو پارانەی دەبێت بدرێت)",
'customer_required' => "(پێویستە)",
'customer_total' => "کۆی گشتی",

View File

@@ -166,6 +166,8 @@ return [
"info" => "",
"info_configuration" => "",
"input_groups" => "",
"integrations" => "",
"integrations_configuration" => "",
"invoice" => "",
"invoice_configuration" => "",
"invoice_default_comments" => "",
@@ -196,6 +198,13 @@ return [
"location_info" => "",
"login_form" => "",
"logout" => "",
"mailchimp" => "",
"mailchimp_api_key" => "",
"mailchimp_configuration" => "",
"mailchimp_key_successfully" => "",
"mailchimp_key_unsuccessfully" => "",
"mailchimp_lists" => "",
"mailchimp_tooltip" => "",
"message" => "",
"message_configuration" => "",
"msg_msg" => "",

View File

@@ -28,6 +28,16 @@ return [
"employee" => "Zaměstnanec",
"error_adding_updating" => "Chyba při vytváření nebo aktualizaci zákazníka.",
"import_items_csv" => "Import zákazníků z CSV",
"mailchimp_activity_click" => "",
"mailchimp_activity_lastopen" => "Poslední otevřený email",
"mailchimp_activity_open" => "",
"mailchimp_activity_total" => "",
"mailchimp_activity_unopen" => "",
"mailchimp_email_client" => "",
"mailchimp_info" => "",
"mailchimp_member_rating" => "Hodnocení",
"mailchimp_status" => "",
"mailchimp_vip" => "VIP",
"max" => "",
"min" => "",
"new" => "",

View File

@@ -32,7 +32,6 @@ return [
"migrate_desc" => "Aktualizovat databázi OSPOS.",
"office" => "Správa",
"office_desc" => "Seznam modulů pro správu.",
'plugins' => 'Doplňky',
"receivings" => "Příjem zboží",
"receivings_desc" => "",
"reports" => "Sestavy",

View File

@@ -1,27 +0,0 @@
<?php
return [
'actions' => 'Akce',
'active' => 'Aktivní',
'configure' => 'Konfigurovat',
'description' => 'Popis',
'disable' => 'Deaktivovat',
'disable_failed' => 'Deaktivace pluginu se nezdařila',
'disabled' => 'Plugin byl úspěšně deaktivován',
'enable' => 'Aktivovat',
'enable_failed' => 'Aktivace pluginu se nezdařila',
'enabled' => 'Plugin byl úspěšně aktivován',
'inactive' => 'Neaktivní',
'management' => 'Správa pluginů',
'name' => 'Název pluginu',
'no_config' => 'Tento plugin nemá žádné možnosti konfigurace',
'no_plugins_to_display' => 'Žádné pluginy k zobrazení',
'not_found' => 'Plugin nebyl nalezen',
'plugins' => 'Pluginy',
'settings_save_failed' => 'Uložení nastavení pluginu se nezdařilo',
'settings_saved' => 'Nastavení pluginu bylo úspěšně uloženo',
'status' => 'Stav',
'uninstall' => 'Odinstalovat',
'uninstall_failed' => 'Odinstalace pluginu se nezdařila',
'uninstalled' => 'Plugin byl úspěšně odinstalován',
'version' => 'Verze',
];

View File

@@ -41,6 +41,7 @@ return [
"customer_discount" => "Sleva",
"customer_email" => "Email",
"customer_location" => "Místo",
"customer_mailchimp_status" => "Stav mailchimp",
"customer_optional" => "(Volitelné)",
"customer_required" => "(Vyžadováno)",
"customer_total" => "Celkem",

View File

@@ -166,6 +166,8 @@ return [
"info" => "Information",
"info_configuration" => "Store Information",
"input_groups" => "",
"integrations" => "Integrations",
"integrations_configuration" => "Third Party Integrations",
"invoice" => "Invoice",
"invoice_configuration" => "Invoice Print Settings",
"invoice_default_comments" => "Default Invoice Comments",
@@ -196,6 +198,13 @@ return [
"location_info" => "Location Configuration Information",
"login_form" => "",
"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",

View File

@@ -28,6 +28,16 @@ return [
"employee" => "Employee",
"error_adding_updating" => "Customer add or update failed.",
"import_items_csv" => "Customer Import from CSV",
"mailchimp_activity_click" => "Email click",
"mailchimp_activity_lastopen" => "Last open email",
"mailchimp_activity_open" => "Email open",
"mailchimp_activity_total" => "Email sent",
"mailchimp_activity_unopen" => "Email unopen",
"mailchimp_email_client" => "Email client",
"mailchimp_info" => "Mailchimp",
"mailchimp_member_rating" => "Rating",
"mailchimp_status" => "Status",
"mailchimp_vip" => "VIP",
"max" => "Max. spent",
"min" => "Min. spent",
"new" => "New Customer",

View File

@@ -32,7 +32,6 @@ return [
"migrate_desc" => "",
"office" => "",
"office_desc" => "",
'plugins' => 'Plugins',
"receivings" => "",
"receivings_desc" => "",
"reports" => "",

View File

@@ -1,27 +0,0 @@
<?php
return [
'actions' => 'Handlinger',
'active' => 'Aktiv',
'configure' => 'Konfigurer',
'description' => 'Beskrivelse',
'disable' => 'Deaktiver',
'disable_failed' => 'Deaktivering af plugin mislykkedes',
'disabled' => 'Plugin blev deaktiveret',
'enable' => 'Aktiver',
'enable_failed' => 'Aktivering af plugin mislykkedes',
'enabled' => 'Plugin blev aktiveret',
'inactive' => 'Inaktiv',
'management' => 'Plugin-administration',
'name' => 'Plugin-navn',
'no_config' => 'Dette plugin har ingen konfigurationsmuligheder',
'no_plugins_to_display' => 'Ingen plugins at vise',
'not_found' => 'Plugin ikke fundet',
'plugins' => 'Plugins',
'settings_save_failed' => 'Gemning af plugin-indstillinger mislykkedes',
'settings_saved' => 'Plugin-indstillinger blev gemt',
'status' => 'Status',
'uninstall' => 'Afinstaller',
'uninstall_failed' => 'Afinstallation af plugin mislykkedes',
'uninstalled' => 'Plugin blev afinstalleret',
'version' => 'Version',
];

View File

@@ -41,6 +41,7 @@ return [
"customer_discount" => "Rabat",
"customer_email" => "",
"customer_location" => "",
"customer_mailchimp_status" => "",
"customer_optional" => "",
"customer_required" => "",
"customer_total" => "",

View File

@@ -166,6 +166,8 @@ return [
"info" => "Instellungen",
"info_configuration" => "Instellungen",
"input_groups" => "",
"integrations" => "",
"integrations_configuration" => "",
"invoice" => "Rechnungs",
"invoice_configuration" => "Druckereinstellungen",
"invoice_default_comments" => "Rechnungskommentar",
@@ -196,6 +198,13 @@ return [
"location_info" => "Lagerort-Information",
"login_form" => "",
"logout" => "Wollen Sie eine Sicherung machen vor dem Beenden? Klicke [OK] für Sicherung",
"mailchimp" => "",
"mailchimp_api_key" => "",
"mailchimp_configuration" => "",
"mailchimp_key_successfully" => "",
"mailchimp_key_unsuccessfully" => "",
"mailchimp_lists" => "",
"mailchimp_tooltip" => "",
"message" => "Message",
"message_configuration" => "Message Configuration",
"msg_msg" => "Saved Text Message",

View File

@@ -28,6 +28,16 @@ return [
"employee" => "",
"error_adding_updating" => "Fehler beim Hinzufügen/Ändern",
"import_items_csv" => "Importiere Kunden via CSV",
"mailchimp_activity_click" => "",
"mailchimp_activity_lastopen" => "",
"mailchimp_activity_open" => "",
"mailchimp_activity_total" => "",
"mailchimp_activity_unopen" => "",
"mailchimp_email_client" => "",
"mailchimp_info" => "",
"mailchimp_member_rating" => "",
"mailchimp_status" => "",
"mailchimp_vip" => "",
"max" => "",
"min" => "",
"new" => "Neuer Kunde",

View File

@@ -32,7 +32,6 @@ return [
"migrate_desc" => "",
"office" => "",
"office_desc" => "",
'plugins' => 'Plugins',
"receivings" => "Eingänge",
"receivings_desc" => "Hinzufügen, Ändern, Löschen und Suchen",
"reports" => "Berichte",

View File

@@ -1,27 +0,0 @@
<?php
return [
'actions' => 'Aktionen',
'active' => 'Aktiv',
'configure' => 'Konfigurieren',
'description' => 'Beschreibung',
'disable' => 'Deaktivieren',
'disable_failed' => 'Plugin konnte nicht deaktiviert werden',
'disabled' => 'Plugin erfolgreich deaktiviert',
'enable' => 'Aktivieren',
'enable_failed' => 'Plugin konnte nicht aktiviert werden',
'enabled' => 'Plugin erfolgreich aktiviert',
'inactive' => 'Inaktiv',
'management' => 'Plugin-Verwaltung',
'name' => 'Plugin-Name',
'no_config' => 'Dieses Plugin hat keine Konfigurationsoptionen',
'no_plugins_to_display' => 'Keine Plugins anzuzeigen',
'not_found' => 'Plugin nicht gefunden',
'plugins' => 'Plugins',
'settings_save_failed' => 'Plugin-Einstellungen konnten nicht gespeichert werden',
'settings_saved' => 'Plugin-Einstellungen erfolgreich gespeichert',
'status' => 'Status',
'uninstall' => 'Deinstallieren',
'uninstall_failed' => 'Plugin konnte nicht deinstalliert werden',
'uninstalled' => 'Plugin erfolgreich deinstalliert',
'version' => 'Version',
];

View File

@@ -41,6 +41,7 @@ return [
"customer_discount" => "Discount",
"customer_email" => "Customer Email",
"customer_location" => "Customer Location",
"customer_mailchimp_status" => "",
"customer_optional" => "",
"customer_required" => "",
"customer_total" => "Total",

View File

@@ -166,6 +166,8 @@ return [
"info" => "Informationen",
"info_configuration" => "Generelle Einstellungen",
"input_groups" => "",
"integrations" => "Integrationen",
"integrations_configuration" => "Drittanbieter Integrationen",
"invoice" => "Rechnungs",
"invoice_configuration" => "Druckereinstellungen",
"invoice_default_comments" => "Rechnungskommentar",
@@ -196,7 +198,14 @@ return [
"location_info" => "Lagerort-Information",
"login_form" => "",
"logout" => "Wollen Sie vor dem Beenden eine Sicherung erstellen? Klicke [OK] für Sicherung.",
"message" => "Nachricht",
"mailchimp" => "Mailchimp",
"mailchimp_api_key" => "Mailchimp API Schlüssel",
"mailchimp_configuration" => "Mailchimp Konfiguration",
"mailchimp_key_successfully" => "API Key ist gültig.",
"mailchimp_key_unsuccessfully" => "API Key ist ungültig.",
"mailchimp_lists" => "Mailchimp Liste(n)",
"mailchimp_tooltip" => "Icon anklicken um API Key zu erhalten.",
"message" => "Nachricht",
"message_configuration" => "Nachrichtenkonfiguration",
"msg_msg" => "Gespeicherte Nachricht",
"msg_msg_placeholder" => "Wenn Sie eine SMS Vorlage benutzen wollen, geben Sie diese hier ein, ansonsten lassen Sie dieses Feld frei.",

View File

@@ -28,6 +28,16 @@ return [
"employee" => "Mitarbeiter",
"error_adding_updating" => "Fehler beim Hinzufügen/Ändern.",
"import_items_csv" => "Importiere Kunden via CSV",
"mailchimp_activity_click" => "E-Mail klick",
"mailchimp_activity_lastopen" => "Letzte geöffnet E-Mail",
"mailchimp_activity_open" => "E-Mail geöffnet",
"mailchimp_activity_total" => "E-Mail gesendet",
"mailchimp_activity_unopen" => "E-Mail ungeöffnet",
"mailchimp_email_client" => "E-Mail Client",
"mailchimp_info" => "Mailchimp",
"mailchimp_member_rating" => "Bewertung",
"mailchimp_status" => "Status",
"mailchimp_vip" => "VIP",
"max" => "Maximal Ausgegeben",
"min" => "Minimal Ausgegeben",
"new" => "Neuer Kunde",

View File

@@ -32,7 +32,6 @@ return [
"migrate_desc" => "Aktualisiere die OSPOS-Datenbank.",
"office" => "Verwaltung",
"office_desc" => "Auflistung der Module für das Verwaltungs-Menü.",
'plugins' => 'Plugins',
"receivings" => "Eingänge",
"receivings_desc" => "Hinzufügen, Ändern, Löschen und Suchen von Bestellungen.",
"reports" => "Berichte",

View File

@@ -1,27 +0,0 @@
<?php
return [
'actions' => 'Aktionen',
'active' => 'Aktiv',
'configure' => 'Konfigurieren',
'description' => 'Beschreibung',
'disable' => 'Deaktivieren',
'disable_failed' => 'Plugin konnte nicht deaktiviert werden',
'disabled' => 'Plugin erfolgreich deaktiviert',
'enable' => 'Aktivieren',
'enable_failed' => 'Plugin konnte nicht aktiviert werden',
'enabled' => 'Plugin erfolgreich aktiviert',
'inactive' => 'Inaktiv',
'management' => 'Plugin-Verwaltung',
'name' => 'Plugin-Name',
'no_config' => 'Dieses Plugin hat keine Konfigurationsoptionen',
'no_plugins_to_display' => 'Keine Plugins anzuzeigen',
'not_found' => 'Plugin nicht gefunden',
'plugins' => 'Plugins',
'settings_save_failed' => 'Plugin-Einstellungen konnten nicht gespeichert werden',
'settings_saved' => 'Plugin-Einstellungen erfolgreich gespeichert',
'status' => 'Status',
'uninstall' => 'Deinstallieren',
'uninstall_failed' => 'Plugin konnte nicht deinstalliert werden',
'uninstalled' => 'Plugin erfolgreich deinstalliert',
'version' => 'Version',
];

View File

@@ -41,6 +41,7 @@ return [
"customer_discount" => "Rabatt",
"customer_email" => "Kunden eMail",
"customer_location" => "Kunden Stadt",
"customer_mailchimp_status" => "Mailchim Status",
"customer_optional" => "(Benötigt für fällige Zahlungen)",
"customer_required" => "(Benötigt)",
"customer_total" => "Gesamtbetrag",

View File

@@ -166,6 +166,8 @@ return [
"info" => "",
"info_configuration" => "",
"input_groups" => "",
"integrations" => "",
"integrations_configuration" => "",
"invoice" => "",
"invoice_configuration" => "",
"invoice_default_comments" => "",
@@ -196,6 +198,13 @@ return [
"location_info" => "",
"login_form" => "",
"logout" => "",
"mailchimp" => "",
"mailchimp_api_key" => "",
"mailchimp_configuration" => "",
"mailchimp_key_successfully" => "",
"mailchimp_key_unsuccessfully" => "",
"mailchimp_lists" => "",
"mailchimp_tooltip" => "",
"message" => "",
"message_configuration" => "",
"msg_msg" => "",

View File

@@ -28,6 +28,16 @@ return [
"employee" => "",
"error_adding_updating" => "",
"import_items_csv" => "",
"mailchimp_activity_click" => "",
"mailchimp_activity_lastopen" => "",
"mailchimp_activity_open" => "",
"mailchimp_activity_total" => "",
"mailchimp_activity_unopen" => "",
"mailchimp_email_client" => "",
"mailchimp_info" => "",
"mailchimp_member_rating" => "",
"mailchimp_status" => "",
"mailchimp_vip" => "",
"max" => "",
"min" => "",
"new" => "",

View File

@@ -32,7 +32,6 @@ return [
"migrate_desc" => "",
"office" => "",
"office_desc" => "",
'plugins' => 'Πρόσθετα',
"receivings" => "",
"receivings_desc" => "",
"reports" => "",

View File

@@ -1,27 +0,0 @@
<?php
return [
'actions' => 'Ενέργειες',
'active' => 'Ενεργό',
'configure' => 'Διαμόρφωση',
'description' => 'Περιγραφή',
'disable' => 'Απενεργοποίηση',
'disable_failed' => 'Η απενεργοποίηση της προσθήκης απέτυχε',
'disabled' => 'Η προσθήκη απενεργοποιήθηκε επιτυχώς',
'enable' => 'Ενεργοποίηση',
'enable_failed' => 'Η ενεργοποίηση της προσθήκης απέτυχε',
'enabled' => 'Η προσθήκη ενεργοποιήθηκε επιτυχώς',
'inactive' => 'Ανενεργό',
'management' => 'Διαχείριση Προσθηκών',
'name' => 'Όνομα Προσθήκης',
'no_config' => 'Αυτή η προσθήκη δεν έχει επιλογές διαμόρφωσης',
'no_plugins_to_display' => 'Δεν υπάρχουν προσθήκες για εμφάνιση',
'not_found' => 'Η προσθήκη δεν βρέθηκε',
'plugins' => 'Προσθήκες',
'settings_save_failed' => 'Η αποθήκευση των ρυθμίσεων της προσθήκης απέτυχε',
'settings_saved' => 'Οι ρυθμίσεις της προσθήκης αποθηκεύτηκαν επιτυχώς',
'status' => 'Κατάσταση',
'uninstall' => 'Απεγκατάσταση',
'uninstall_failed' => 'Η απεγκατάσταση της προσθήκης απέτυχε',
'uninstalled' => 'Η προσθήκη απεγκαταστάθηκε επιτυχώς',
'version' => 'Έκδοση',
];

View File

@@ -41,6 +41,7 @@ return [
"customer_discount" => "Έκπτωση",
"customer_email" => "Διεύθυνση ηλεκτρονικού ταχυδρομείου",
"customer_location" => "Τοποθεσία",
"customer_mailchimp_status" => "Κατάσταση Mailchimp",
"customer_optional" => "(Απαραίτητο για πληρωμές επί Πιστώσει)",
"customer_required" => "(Απαραίτητο)",
"customer_total" => "Σύνολο",

View File

@@ -166,6 +166,8 @@ return [
"info" => "Information",
"info_configuration" => "Shop 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",
@@ -196,6 +198,13 @@ return [
"location_info" => "Location Configuration Information",
"login_form" => "Login Form Style",
"logout" => "Don't you want to make a backup before logging out? Click [OK] to backup, [Cancel] to logout.",
"mailchimp" => "MailChimp",
"mailchimp_api_key" => "MailChimp API Key",
"mailchimp_configuration" => "MailChimp Configuration",
"mailchimp_key_successfully" => "Valid API Key.",
"mailchimp_key_unsuccessfully" => "Invalid API Key.",
"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",

View File

@@ -28,6 +28,16 @@ return [
"employee" => "Employee",
"error_adding_updating" => "Error adding/updating Customer.",
"import_items_csv" => "Customer Import from CSV",
"mailchimp_activity_click" => "Email click",
"mailchimp_activity_lastopen" => "Last open email",
"mailchimp_activity_open" => "Email open",
"mailchimp_activity_total" => "Email sent",
"mailchimp_activity_unopen" => "Email unopen",
"mailchimp_email_client" => "Email client",
"mailchimp_info" => "MailChimp",
"mailchimp_member_rating" => "Rating",
"mailchimp_status" => "Status",
"mailchimp_vip" => "VIP",
"max" => "Max spent",
"min" => "Min spent",
"new" => "New Customer",

View File

@@ -32,7 +32,6 @@ return [
"migrate_desc" => "Update the OSPOS Database.",
"office" => "Office",
"office_desc" => "List office menu modules.",
'plugins' => 'Plugins',
"receivings" => "Receivings",
"receivings_desc" => "Process Purchase Orders.",
"reports" => "Reports",

View File

@@ -1,27 +0,0 @@
<?php
return [
'actions' => 'Actions',
'active' => 'Active',
'configure' => 'Configure',
'description' => 'Description',
'disable' => 'Disable',
'disable_failed' => 'Failed to disable plugin',
'disabled' => 'Plugin disabled successfully',
'enable' => 'Enable',
'enable_failed' => 'Failed to enable plugin',
'enabled' => 'Plugin enabled successfully',
'inactive' => 'Inactive',
'management' => 'Plugin Management',
'name' => 'Plugin Name',
'no_config' => 'This plugin has no configuration options',
'no_plugins_to_display' => 'No Plugins to display',
'not_found' => 'Plugin not found',
'plugins' => 'Plugins',
'settings_save_failed' => 'Failed to save plugin settings',
'settings_saved' => 'Plugin settings saved successfully',
'status' => 'Status',
'uninstall' => 'Uninstall',
'uninstall_failed' => 'Failed to uninstall plugin',
'uninstalled' => 'Plugin uninstalled successfully',
'version' => 'Version',
];

View File

@@ -42,6 +42,7 @@ return [
"customer_discount" => "Discount",
"customer_email" => "Email",
"customer_location" => "Location",
"customer_mailchimp_status" => "MailChimp Status",
"customer_optional" => "(Required for Due Payments)",
"customer_required" => "(Required)",
"customer_total" => "Total",

View File

@@ -166,6 +166,8 @@ return [
"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",
@@ -196,6 +198,13 @@ return [
"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",

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