mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-05-25 00:44:03 -04:00
Compare commits
138 Commits
issue-4474
...
plugin-sys
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93713f8e4b | ||
|
|
b7384296c1 | ||
|
|
ad901f9c2d | ||
|
|
388c8ad631 | ||
|
|
705c61b48c | ||
|
|
d39067e2e1 | ||
|
|
50eead4da4 | ||
|
|
4c7ac7b5d0 | ||
|
|
bed8a1c34d | ||
|
|
10588867c4 | ||
|
|
139f754a07 | ||
|
|
c08872f83e | ||
|
|
01172fc522 | ||
|
|
f8fd12c5de | ||
|
|
a15a6516a6 | ||
|
|
84a10ec218 | ||
|
|
f650f17181 | ||
|
|
d699d82388 | ||
|
|
b0dddc22a3 | ||
|
|
7afaeef6a3 | ||
|
|
8d6b166673 | ||
|
|
df24ef5193 | ||
|
|
1c2112a78b | ||
|
|
093ec7fb13 | ||
|
|
ad097adccd | ||
|
|
796657118a | ||
|
|
445a506ea8 | ||
|
|
5d608ec873 | ||
|
|
9c89a2e2cb | ||
|
|
2f51c4ef52 | ||
|
|
def0c27a0e | ||
|
|
90c981b6b7 | ||
|
|
6ff28d8a4d | ||
|
|
70fb347fc4 | ||
|
|
2f5c0130f4 | ||
|
|
fdd6a408ec | ||
|
|
ef91e6a9df | ||
|
|
144e73eba6 | ||
|
|
42ba39d290 | ||
|
|
81213f0434 | ||
|
|
e5fdea85f3 | ||
|
|
ca07aac9a0 | ||
|
|
cd91ac3ff3 | ||
|
|
478934321d | ||
|
|
4d266c9b5e | ||
|
|
43bee7bfe4 | ||
|
|
f71af765f8 | ||
|
|
4246a915c4 | ||
|
|
939012dc1b | ||
|
|
d2d0c8bf37 | ||
|
|
6c55526479 | ||
|
|
fe331c34dd | ||
|
|
6630fb56f6 | ||
|
|
2f48e0499f | ||
|
|
cbabe1d56c | ||
|
|
8c1c9d85dc | ||
|
|
97adee0c28 | ||
|
|
32997d48c0 | ||
|
|
7edefe8ee1 | ||
|
|
68e14191f9 | ||
|
|
a381c3ca54 | ||
|
|
058e12244e | ||
|
|
1a9e84bd37 | ||
|
|
9d0b14a8ce | ||
|
|
c796b52c22 | ||
|
|
f863be68f9 | ||
|
|
f1c6fe2981 | ||
|
|
54c476a498 | ||
|
|
ec139c477a | ||
|
|
bae361c637 | ||
|
|
dcfdc212da | ||
|
|
c217fd770c | ||
|
|
6ef6f49693 | ||
|
|
eae6417f97 | ||
|
|
4cfff5388c | ||
|
|
a77b95f0cc | ||
|
|
506cded6e9 | ||
|
|
2639a8b212 | ||
|
|
f6106e7ead | ||
|
|
202c016dd8 | ||
|
|
d47ead8747 | ||
|
|
0e5ba88f6c | ||
|
|
d88cf54f99 | ||
|
|
2f200b47c6 | ||
|
|
f819bc92f8 | ||
|
|
ff7a8d2e88 | ||
|
|
e602eddb47 | ||
|
|
0a313aa09d | ||
|
|
12e3c7e31f | ||
|
|
db180d134e | ||
|
|
edd97a3c78 | ||
|
|
de62e9f3bd | ||
|
|
d73bfd39f6 | ||
|
|
97ca738b2d | ||
|
|
c714dd6f68 | ||
|
|
d9d93e0d9d | ||
|
|
b6f28da058 | ||
|
|
bfb4ad4617 | ||
|
|
00cd13f735 | ||
|
|
43972b8f0e | ||
|
|
ff3c7d1b14 | ||
|
|
9fc918b53d | ||
|
|
56f68f7577 | ||
|
|
65fb6339d7 | ||
|
|
57a19bb35f | ||
|
|
c81cf4a5cc | ||
|
|
1918f3e6e2 | ||
|
|
196e87fa49 | ||
|
|
ebd1c8fa0e | ||
|
|
f842be50b3 | ||
|
|
e94e5e634c | ||
|
|
bcff389b34 | ||
|
|
deb122246c | ||
|
|
e1b76d2e0d | ||
|
|
84ea65b1bd | ||
|
|
a19fe03ecf | ||
|
|
15227523d9 | ||
|
|
1587d4276d | ||
|
|
d6967704e6 | ||
|
|
80dc62948d | ||
|
|
acb3b18584 | ||
|
|
19cdb76bb3 | ||
|
|
07f1a35e9d | ||
|
|
3b0476f2b3 | ||
|
|
2fa324ba4e | ||
|
|
8b14ed81e0 | ||
|
|
0ea3ced674 | ||
|
|
896ed87797 | ||
|
|
eb264ad76d | ||
|
|
10a64e7af9 | ||
|
|
6e99f05d63 | ||
|
|
c430c7afb5 | ||
|
|
519347f4f5 | ||
|
|
62d84411b2 | ||
|
|
6bd4bb545d | ||
|
|
66f7d70749 | ||
|
|
bd8b4fa6c1 | ||
|
|
a9669ddf19 |
@@ -16,6 +16,9 @@ CI_ENVIRONMENT = production
|
||||
# Configure with comma-separated list of domains/subdomains:
|
||||
# app.allowedHostnames = 'yourdomain.com,www.yourdomain.com'
|
||||
#
|
||||
# Or via environment variable (useful for Docker/Compose):
|
||||
# ALLOWED_HOSTNAMES=yourdomain.com,www.yourdomain.com
|
||||
#
|
||||
# For local development:
|
||||
# app.allowedHostnames = 'localhost'
|
||||
#
|
||||
|
||||
1
.github/workflows/build-release.yml
vendored
1
.github/workflows/build-release.yml
vendored
@@ -123,6 +123,7 @@ jobs:
|
||||
.
|
||||
!.git
|
||||
!node_modules
|
||||
include-hidden-files: true
|
||||
retention-days: 1
|
||||
|
||||
docker:
|
||||
|
||||
219
.github/workflows/deploy-core.yml
vendored
Normal file
219
.github/workflows/deploy-core.yml
vendored
Normal file
@@ -0,0 +1,219 @@
|
||||
name: Deploy Core
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
image_tag:
|
||||
description: 'Docker image tag to deploy'
|
||||
type: string
|
||||
required: true
|
||||
sha:
|
||||
description: 'Git commit SHA to deploy'
|
||||
type: string
|
||||
required: true
|
||||
description:
|
||||
description: 'Deployment description'
|
||||
type: string
|
||||
required: true
|
||||
pr_number:
|
||||
description: 'Pull request number (optional)'
|
||||
type: string
|
||||
required: false
|
||||
outputs:
|
||||
deployment_id:
|
||||
description: 'GitHub deployment ID'
|
||||
value: ${{ jobs.deploy.outputs.deployment_id }}
|
||||
status:
|
||||
description: 'Deployment status (success/failure)'
|
||||
value: ${{ jobs.deploy.outputs.status }}
|
||||
|
||||
concurrency:
|
||||
group: deploy-staging
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy to staging
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
environment:
|
||||
name: staging
|
||||
url: ${{ vars.DEPLOY_URL || 'https://dev.opensourcepos.org' }}
|
||||
deployment: false
|
||||
|
||||
outputs:
|
||||
deployment_id: ${{ steps.deployment.outputs.deployment_id }}
|
||||
status: ${{ steps.webhook.outputs.status }}
|
||||
|
||||
steps:
|
||||
- name: Create GitHub Deployment
|
||||
id: deployment
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
IMAGE_TAG: ${{ inputs.image_tag }}
|
||||
REF_SHA: ${{ inputs.sha }}
|
||||
DESCRIPTION: ${{ inputs.description }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
DEPLOYMENT_ID=$(gh api "repos/${GITHUB_REPOSITORY}/deployments" \
|
||||
-X POST \
|
||||
-f ref="${REF_SHA}" \
|
||||
-f environment="staging" \
|
||||
-f description="${DESCRIPTION}" \
|
||||
-F auto_merge=false \
|
||||
-F required_contexts[] \
|
||||
--jq '.id')
|
||||
|
||||
if [ -z "$DEPLOYMENT_ID" ]; then
|
||||
echo "::error::Failed to create deployment"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "deployment_id=$DEPLOYMENT_ID" >> "$GITHUB_OUTPUT"
|
||||
echo "Created deployment: $DEPLOYMENT_ID"
|
||||
|
||||
- name: Set deployment status to in_progress
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
gh api "repos/${GITHUB_REPOSITORY}/deployments/${{ steps.deployment.outputs.deployment_id }}/statuses" \
|
||||
-X POST \
|
||||
-f state="in_progress" \
|
||||
-f description="Deployment in progress..." \
|
||||
-f log_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
|
||||
|
||||
- name: Trigger deployment webhook
|
||||
id: webhook
|
||||
env:
|
||||
DEPLOY_WEBHOOK_URL: ${{ secrets.DEPLOY_WEBHOOK_URL }}
|
||||
DEPLOY_WEBHOOK_SECRET: ${{ secrets.DEPLOY_WEBHOOK_SECRET }}
|
||||
DOCKER_REPO_NAME: ${{ secrets.DOCKER_REPO_NAME }}
|
||||
IMAGE_TAG: ${{ inputs.image_tag }}
|
||||
REF_SHA: ${{ inputs.sha }}
|
||||
DEPLOYMENT_ID: ${{ steps.deployment.outputs.deployment_id }}
|
||||
PR_NUMBER: ${{ inputs.pr_number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "$DEPLOY_WEBHOOK_URL" ]; then
|
||||
echo "::error::DEPLOY_WEBHOOK_URL secret is not configured"
|
||||
echo "Please add the DEPLOY_WEBHOOK_URL secret in your repository settings"
|
||||
echo "status=failure" >> "$GITHUB_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_NAME="${DOCKER_REPO_NAME:-opensourcepos/opensourcepos}"
|
||||
REPO_NAMESPACE="${REPO_NAME%%/*}"
|
||||
REPO_SHORT_NAME="${REPO_NAME#*/}"
|
||||
PUSHED_AT=$(date +%s)
|
||||
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg callback_url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
||||
--argjson pushed_at "$PUSHED_AT" \
|
||||
--arg pusher "$GITHUB_ACTOR" \
|
||||
--arg tag "$IMAGE_TAG" \
|
||||
--arg repo_name "$REPO_NAME" \
|
||||
--arg name "$REPO_SHORT_NAME" \
|
||||
--arg namespace "$REPO_NAMESPACE" \
|
||||
--arg repo_url "https://hub.docker.com/r/${REPO_NAME}/" \
|
||||
--arg deployment_id "$DEPLOYMENT_ID" \
|
||||
--arg repository "$GITHUB_REPOSITORY" \
|
||||
--arg sha "$REF_SHA" \
|
||||
--arg run_id "$GITHUB_RUN_ID" \
|
||||
--arg actor "$GITHUB_ACTOR" \
|
||||
--argjson pr_number "$PR_NUMBER" \
|
||||
'{
|
||||
callback_url: $callback_url,
|
||||
push_data: {pushed_at: $pushed_at, pusher: $pusher, tag: $tag},
|
||||
repository: {repo_name: $repo_name, name: $name, namespace: $namespace, repo_url: $repo_url, status: "Active"},
|
||||
github_deployment: {id: $deployment_id, environment: "staging", repository: $repository, sha: $sha, run_id: $run_id, actor: $actor, pull_request: $pr_number}
|
||||
}')
|
||||
else
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg callback_url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
||||
--argjson pushed_at "$PUSHED_AT" \
|
||||
--arg pusher "$GITHUB_ACTOR" \
|
||||
--arg tag "$IMAGE_TAG" \
|
||||
--arg repo_name "$REPO_NAME" \
|
||||
--arg name "$REPO_SHORT_NAME" \
|
||||
--arg namespace "$REPO_NAMESPACE" \
|
||||
--arg repo_url "https://hub.docker.com/r/${REPO_NAME}/" \
|
||||
--arg deployment_id "$DEPLOYMENT_ID" \
|
||||
--arg repository "$GITHUB_REPOSITORY" \
|
||||
--arg sha "$REF_SHA" \
|
||||
--arg run_id "$GITHUB_RUN_ID" \
|
||||
--arg actor "$GITHUB_ACTOR" \
|
||||
'{
|
||||
callback_url: $callback_url,
|
||||
push_data: {pushed_at: $pushed_at, pusher: $pusher, tag: $tag},
|
||||
repository: {repo_name: $repo_name, name: $name, namespace: $namespace, repo_url: $repo_url, status: "Active"},
|
||||
github_deployment: {id: $deployment_id, environment: "staging", repository: $repository, sha: $sha, run_id: $run_id, actor: $actor}
|
||||
}')
|
||||
fi
|
||||
|
||||
echo "Sending webhook..."
|
||||
echo "Image: ${IMAGE_TAG}"
|
||||
echo "Environment: staging"
|
||||
|
||||
HEADERS=(-H "Content-Type: application/json")
|
||||
|
||||
if [ -n "$DEPLOY_WEBHOOK_SECRET" ]; then
|
||||
SIGNATURE=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$DEPLOY_WEBHOOK_SECRET" | sed 's/.*= //')
|
||||
HEADERS+=(-H "X-Hub-Signature-256: sha256=$SIGNATURE")
|
||||
echo "Using HMAC-SHA256 signature verification"
|
||||
else
|
||||
echo "::warning::DEPLOY_WEBHOOK_SECRET not set - webhook calls will not be signed"
|
||||
echo "For security, configure DEPLOY_WEBHOOK_SECRET in your repository settings"
|
||||
fi
|
||||
|
||||
HTTP_CODE=$(curl -sS --connect-timeout 10 --max-time 120 \
|
||||
-o response.txt -w "%{http_code}" \
|
||||
-X POST \
|
||||
"${HEADERS[@]}" \
|
||||
-d "$PAYLOAD" \
|
||||
"$DEPLOY_WEBHOOK_URL") || HTTP_CODE="000"
|
||||
|
||||
echo "Response code: $HTTP_CODE"
|
||||
if [ -s response.txt ]; then
|
||||
cat response.txt
|
||||
fi
|
||||
|
||||
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
||||
echo "status=success" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "status=failure" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Set deployment status
|
||||
if: always()
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
IMAGE_TAG: ${{ inputs.image_tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
STATE="${{ steps.webhook.outputs.status }}"
|
||||
|
||||
if [ "$STATE" = "success" ]; then
|
||||
DESCRIPTION=$(jq -nr --arg tag "$IMAGE_TAG" \
|
||||
'"Deployed image \($tag) to staging"')
|
||||
|
||||
gh api "repos/${GITHUB_REPOSITORY}/deployments/${{ steps.deployment.outputs.deployment_id }}/statuses" \
|
||||
-X POST \
|
||||
-f state="success" \
|
||||
-f description="$DESCRIPTION"
|
||||
else
|
||||
gh api "repos/${GITHUB_REPOSITORY}/deployments/${{ steps.deployment.outputs.deployment_id }}/statuses" \
|
||||
-X POST \
|
||||
-f state="failure" \
|
||||
-f description="Deployment failed"
|
||||
exit 1
|
||||
fi
|
||||
79
.github/workflows/deploy-pr.yml
vendored
Normal file
79
.github/workflows/deploy-pr.yml
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
name: PR Deploy
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
concurrency:
|
||||
group: staging-deploy
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: Prepare deployment
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.event.review.state == 'approved' &&
|
||||
github.event.pull_request.head.repo.full_name == github.repository
|
||||
outputs:
|
||||
image_tag: ${{ steps.image.outputs.tag }}
|
||||
sha: ${{ github.event.pull_request.head.sha }}
|
||||
pr_number: ${{ github.event.pull_request.number }}
|
||||
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- 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}"
|
||||
echo "tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT"
|
||||
|
||||
deploy:
|
||||
name: Deploy to staging
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/deploy-core.yml
|
||||
with:
|
||||
image_tag: ${{ needs.prepare.outputs.image_tag }}
|
||||
sha: ${{ needs.prepare.outputs.sha }}
|
||||
description: Deploy PR #${{ needs.prepare.outputs.pr_number }} to staging
|
||||
pr_number: ${{ needs.prepare.outputs.pr_number }}
|
||||
secrets: inherit
|
||||
|
||||
comment:
|
||||
name: Comment deployment status
|
||||
needs: [prepare, deploy]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }}
|
||||
PR_NUMBER: ${{ needs.prepare.outputs.pr_number }}
|
||||
REF_SHA: ${{ needs.prepare.outputs.sha }}
|
||||
STATUS: ${{ needs.deploy.outputs.status }}
|
||||
|
||||
steps:
|
||||
- name: Comment on PR
|
||||
run: |
|
||||
if [ "$STATUS" = "success" ]; then
|
||||
BODY=$(jq -nr --arg tag "$IMAGE_TAG" --arg sha "$REF_SHA" --arg url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
||||
'"✅ **Staging deployment completed**\n\n🔗 **URL**: https://dev.opensourcepos.org\n📦 **Image Tag**: `\($tag)`\n🔨 **Commit**: \($sha)\n\nView logs: \($url)"')
|
||||
else
|
||||
BODY=$(jq -nr --arg url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
||||
'"❌ **Staging deployment failed**\n\nCheck the [workflow logs](\($url)) for details."')
|
||||
fi
|
||||
|
||||
gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \
|
||||
-X POST \
|
||||
-f body="$BODY"
|
||||
23
.github/workflows/deploy.yml
vendored
Normal file
23
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
image_tag:
|
||||
description: 'Docker image tag to deploy (e.g., v3.4.0, latest)'
|
||||
required: true
|
||||
default: 'latest'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy to staging
|
||||
uses: ./.github/workflows/deploy-core.yml
|
||||
with:
|
||||
image_tag: ${{ inputs.image_tag }}
|
||||
sha: ${{ github.sha }}
|
||||
description: Deploy image ${{ inputs.image_tag }}
|
||||
secrets: inherit
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -87,3 +87,5 @@ auth.json
|
||||
/app/Database/database.sql
|
||||
/writable/cache/settings
|
||||
/.env.bak
|
||||
/.php-cs-fixer.cache
|
||||
/build
|
||||
|
||||
127
AGENTS.md
127
AGENTS.md
@@ -1,40 +1,125 @@
|
||||
# Agent Instructions
|
||||
|
||||
This document provides guidance for AI agents working on the Open Source Point of Sale (OSPOS) codebase.
|
||||
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/`.
|
||||
|
||||
## Code Style
|
||||
|
||||
- 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
|
||||
- **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`
|
||||
|
||||
## Development
|
||||
## Development Workflow
|
||||
|
||||
- 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
|
||||
|
||||
## 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`
|
||||
- Tests must pass before submitting changes (`composer test`)
|
||||
- Minimum PHPUnit version: 10.5.16+. Default config: `phpunit.xml.dist`
|
||||
|
||||
## Conventions
|
||||
|
||||
- Controllers go in `app/Controllers/`
|
||||
- Models go in `app/Models/`
|
||||
- Views go in `app/Views/`
|
||||
- Database migrations in `app/Database/Migrations/`
|
||||
- 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)
|
||||
- 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
|
||||
|
||||
33
CHANGELOG.md
33
CHANGELOG.md
@@ -1,5 +1,4 @@
|
||||
[unreleased]: https://github.com/opensourcepos/opensourcepos/compare/3.4.0...HEAD
|
||||
[3.4.2]: https://github.com/opensourcepos/opensourcepos/compare/3.4.1...3.4.2
|
||||
[unreleased]: https://github.com/opensourcepos/opensourcepos/compare/3.4.1...HEAD
|
||||
[3.4.1]: https://github.com/opensourcepos/opensourcepos/compare/3.4.0...3.4.1
|
||||
[3.4.0]: https://github.com/opensourcepos/opensourcepos/compare/3.3.9...3.4.0
|
||||
[3.3.9]: https://github.com/opensourcepos/opensourcepos/compare/3.3.8...3.3.9
|
||||
@@ -34,10 +33,36 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [3.4.0] - 2025-02-06
|
||||
## [3.4.1] - 2025-06-05
|
||||
- Feature: PSR-12 Compliant Indentation by @objecttothis in ([#4196](https://github.com/opensourcepos/opensourcepos/pull/4196))
|
||||
- Add .env to dist zip by @jekkos in ([#4199](https://github.com/opensourcepos/opensourcepos/pull/4199))
|
||||
- Add CI4 coding standards linter ([#3708](https://github.com/opensourcepos/opensourcepos/issues/3708)) by @jekkos in ([#4198](https://github.com/opensourcepos/opensourcepos/pull/4198))
|
||||
- Bump canvg from 3.0.10 to 3.0.11 by @dependabot in ([#4189](https://github.com/opensourcepos/opensourcepos/pull/4189))
|
||||
- Bump jspdf and jspdf-autotable by @dependabot in ([#4190](https://github.com/opensourcepos/opensourcepos/pull/4190))
|
||||
- Feature bump ci to 4.6.0 by @objecttothis in ([#4197](https://github.com/opensourcepos/opensourcepos/pull/4197))
|
||||
- Add Kurdish language option to UI by @BudsieBuds in ([#4210](https://github.com/opensourcepos/opensourcepos/pull/4210))
|
||||
- Convert language ku to ckb by @BudsieBuds in ([#4211](https://github.com/opensourcepos/opensourcepos/pull/4211))
|
||||
- Fix PHP 8.4 errors by @BudsieBuds in ([#4215](https://github.com/opensourcepos/opensourcepos/pull/4215))
|
||||
- Add default bootstrap to themes by @BudsieBuds in ([#4219](https://github.com/opensourcepos/opensourcepos/pull/4219))
|
||||
- Update language names by @BudsieBuds in ([#4218](https://github.com/opensourcepos/opensourcepos/pull/4218))
|
||||
- Update install docs by @BudsieBuds in ([#4217](https://github.com/opensourcepos/opensourcepos/pull/4217))
|
||||
- Convert menu icons to SVG by @BudsieBuds in ([#4220](https://github.com/opensourcepos/opensourcepos/pull/4220))
|
||||
- Enhance license handling by @BudsieBuds in ([#4223](https://github.com/opensourcepos/opensourcepos/pull/4223))
|
||||
- Fix datetime rendering ([#4226](https://github.com/opensourcepos/opensourcepos/issues/4226)) by @jekkos in ([#4227](https://github.com/opensourcepos/opensourcepos/pull/4227))
|
||||
- Fix datetime rendering by @jekkos in ([#4228](https://github.com/opensourcepos/opensourcepos/pull/4228))
|
||||
- Fix null error when sending by email a receipt of a sale that has no invoice by @diego-ramos in ([#4229](https://github.com/opensourcepos/opensourcepos/pull/4229))
|
||||
- Update Receivings.php to save form. by @odiea in ([#4231](https://github.com/opensourcepos/opensourcepos/pull/4231))
|
||||
- Update Cashups.php for ajax cashup total to work. by @odiea in ([#4238](https://github.com/opensourcepos/opensourcepos/pull/4238))
|
||||
- Coding style updates for PSR-12 compliance & improved readability by @BudsieBuds in ([#4204](https://github.com/opensourcepos/opensourcepos/pull/4204))
|
||||
- Fix Codeigniter disallowed characters error with payment types that have accents by @diego-ramos in ([#4232](https://github.com/opensourcepos/opensourcepos/pull/4232))
|
||||
- Fixed broken escape string for success & warning messages by @Franchovy in ([#4253](https://github.com/opensourcepos/opensourcepos/pull/4253))
|
||||
- Bugfix constraint migration fix by @objecttothis in ([#4230](https://github.com/opensourcepos/opensourcepos/pull/4230))
|
||||
- Fix item number lookup in sales/receivings ([#4212](https://github.com/opensourcepos/opensourcepos/issues/4212)) by @jekkos in ([#4250](https://github.com/opensourcepos/opensourcepos/pull/4250))
|
||||
|
||||
## [3.4.0] - 2025-03-23
|
||||
|
||||
- Translation updates (Spanish, Indonesian, Swedish, Urdu, Chinese, Thai, French, Dutch)
|
||||
- PHP 8.x support
|
||||
- PHP `8.x` support
|
||||
- Security fixes (XSS, SQLi)
|
||||
- Migration to Gulp as buildsystem
|
||||
- Decimal validation fix
|
||||
|
||||
3
CLAUDE.md
Normal file
3
CLAUDE.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 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.
|
||||
@@ -1,98 +1,85 @@
|
||||
Contributor Covenant Code of Conduct
|
||||
Our Pledge
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||
identity and orientation.
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
Our Standards
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
[comment]: # (Contributor Covenant 2.1 - from https://www.contributor-covenant.org/version/2/1/code_of_conduct/code_of_conduct.md)
|
||||
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall
|
||||
community
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||
any kind
|
||||
* The use of sexualized language or imagery, and sexual attention or advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others’ private information, such as a physical or email address,
|
||||
without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
* Publishing others' private information, such as a physical or email address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
Enforcement Responsibilities
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
Scope
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official email address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
Enforcement
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
[INSERT CONTACT METHOD].
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
Enforcement Guidelines
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
1. Correction
|
||||
Community Impact: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
Consequence: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
2. Warning
|
||||
Community Impact: A violation through a single incident or series of
|
||||
actions.
|
||||
Consequence: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or permanent
|
||||
ban.
|
||||
3. Temporary Ban
|
||||
Community Impact: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
Consequence: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
4. Permanent Ban
|
||||
Community Impact: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
Consequence: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
Attribution
|
||||
This Code of Conduct is adapted from the Contributor Covenant,
|
||||
version 2.1, available at
|
||||
https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
|
||||
Community Impact Guidelines were inspired by
|
||||
Mozilla’s code of conduct enforcement ladder.
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [INSERT CONTACT METHOD]. All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||
[FAQ]: https://www.contributor-covenant.org/faq
|
||||
[translations]: https://www.contributor-covenant.org/translations
|
||||
|
||||
@@ -13,7 +13,8 @@ RUN echo "date.timezone = \"\${PHP_TIMEZONE}\"" > /usr/local/etc/php/conf.d/time
|
||||
|
||||
WORKDIR /app
|
||||
COPY --chown=www-data:www-data . /app
|
||||
RUN chmod 770 /app/writable/uploads /app/writable/logs /app/writable/cache \
|
||||
RUN chmod 750 /app/writable/logs /app/writable/uploads /app/writable/cache /app/public/uploads /app/public/uploads/item_pics \
|
||||
&& chmod 640 /app/writable/uploads/importCustomers.csv \
|
||||
&& ln -s /app/*[^public] /var/www \
|
||||
&& rm -rf /var/www/html \
|
||||
&& ln -nsf /app/public /var/www/html
|
||||
|
||||
131
SECURITY.md
131
SECURITY.md
@@ -5,8 +5,9 @@
|
||||
- [Supported Versions](#supported-versions)
|
||||
- [Security Advisories](#security-advisories)
|
||||
- [Reporting a Vulnerability](#reporting-a-vulnerability)
|
||||
- [Disclosure Process](#disclosure-process)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- END doctoc generated TOC please keep comment here to allow update -->
|
||||
|
||||
# Security Policy
|
||||
|
||||
@@ -21,26 +22,116 @@ We release patches for security vulnerabilities.
|
||||
|
||||
## Security Advisories
|
||||
|
||||
The following security vulnerabilities have been published:
|
||||
|
||||
### High Severity
|
||||
|
||||
| CVE | Vulnerability | CVSS | Published | Fixed In | Credit |
|
||||
|-----|--------------|------|-----------|----------|--------|
|
||||
| [CVE-2025-68434](https://github.com/opensourcepos/opensourcepos/security/advisories/GHSA-wjm4-hfwg-5w5r) | CSRF leading to Admin Creation | 8.8 | 2025-12-17 | 3.4.2 | @Nixon-H, @jekkos |
|
||||
| [CVE-2025-68147](https://github.com/opensourcepos/opensourcepos/security/advisories/GHSA-xgr7-7pvw-fpmh) | Stored XSS in Return Policy | 8.1 | 2025-12-17 | 3.4.2 | @Nixon-H, @jekkos |
|
||||
| [CVE-2025-66924](https://github.com/opensourcepos/opensourcepos/security/advisories/GHSA-gv8j-f6gq-g59m) | Stored XSS in Item Kits | 7.2 | 2026-03-04 | 3.4.2 | @hungnqdz, @omkaryepre |
|
||||
|
||||
### Medium Severity
|
||||
|
||||
| CVE | Vulnerability | CVSS | Published | Fixed In | Credit |
|
||||
|-----|--------------|------|-----------|----------|--------|
|
||||
| [CVE-2025-68658](https://github.com/opensourcepos/opensourcepos/security/advisories/GHSA-32r8-8r9r-9chw) | Stored XSS in Company Name | 4.3 | 2026-01-13 | 3.4.2 | @hungnqdz |
|
||||
|
||||
For a complete list including draft advisories, see our [GitHub Security Advisories page](https://github.com/opensourcepos/opensourcepos/security/advisories).
|
||||
For a complete list of published and draft security advisories with CVE details, see our [GitHub Security Advisories page](https://github.com/opensourcepos/opensourcepos/security/advisories).
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report (suspected) security vulnerabilities to **[jeroen@steganos.dev](mailto:jeroen@steganos.dev)**.
|
||||
**Option 1: GitHub Security Advisory (Preferred)**
|
||||
|
||||
You will receive a response from us within 48 hours. If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days.
|
||||
1. Create a draft security advisory directly on GitHub:
|
||||
- Go to https://github.com/opensourcepos/opensourcepos/security/advisories
|
||||
- Click "New draft security advisory"
|
||||
- Fill in the vulnerability details using our [template below](#vulnerability-template)
|
||||
- Submit as **draft** (not published)
|
||||
|
||||
2. Notify us for triage:
|
||||
- Send an email to **[jeroen@steganos.dev](mailto:jeroen@steganos.dev)** with:
|
||||
- Subject: `[GHSA] Brief description of vulnerability`
|
||||
- Link to the draft advisory
|
||||
- Brief summary
|
||||
|
||||
**Option 2: Email Report**
|
||||
|
||||
Send vulnerability details to **[jeroen@steganos.dev](mailto:jeroen@steganos.dev)**.
|
||||
|
||||
You will receive a response within 48 hours. Confirmed vulnerabilities will be patched within a few days depending on complexity.
|
||||
|
||||
## Disclosure Process
|
||||
|
||||
### Timeline
|
||||
|
||||
| Step | Timeline | Action |
|
||||
|------|----------|--------|
|
||||
| 1. Report received | Day 0 | We acknowledge within 48 hours |
|
||||
| 2. Triage & confirmation | Day 1-3 | We validate the vulnerability |
|
||||
| 3. Fix development | Day 3-7 | We develop and test the fix |
|
||||
| 4. Patch release | Day 7-10 | We release a security patch |
|
||||
| 5. CVE request | Day 7-14 | We request CVE from GitHub (if applicable) |
|
||||
| 6. Advisory published | Day 14 | We publish the advisory with credit |
|
||||
| 7. Public disclosure | Day 14+ | Full disclosure after patch release |
|
||||
|
||||
### CVE Process
|
||||
|
||||
**We request CVE identifiers through GitHub's security advisory system.** This is the preferred and easiest method:
|
||||
|
||||
1. After we confirm and fix the vulnerability, we'll request a CVE through GitHub
|
||||
2. GitHub coordinates with MITRE on our behalf
|
||||
3. The CVE is automatically linked to the advisory
|
||||
4. You'll be credited as the reporter in the published advisory
|
||||
|
||||
**Already have a CVE?** If you've already obtained a CVE from another source (e.g., VulDB, CVE.MITRE.ORG), please include it in your report or advisory. We'll update our advisory to reference the existing CVE.
|
||||
|
||||
### No Bug Bounty Program
|
||||
|
||||
**Important:** Open Source Point of Sale does not offer a bug bounty program.
|
||||
|
||||
- All security research and vulnerability triage is done on a **voluntary basis** in our free time
|
||||
- We do not offer monetary rewards for vulnerability reports
|
||||
- We do credit reporters in published advisories (unless anonymity is requested)
|
||||
- We greatly appreciate the security research community's efforts to help improve project security
|
||||
|
||||
### Security Best Practices for Researchers
|
||||
|
||||
- **Do not** access, modify, or delete data that doesn't belong to you
|
||||
- **Do not** perform denial of service attacks
|
||||
- **Do not** publicly disclose vulnerabilities before we've had time to fix them
|
||||
- **Do** provide sufficient information to reproduce the vulnerability
|
||||
- **Do** allow us reasonable time to fix before public disclosure
|
||||
- **Do** report through official channels (GitHub advisories or email)
|
||||
|
||||
### Vulnerability Template
|
||||
|
||||
When creating a draft advisory, please include:
|
||||
|
||||
```
|
||||
## Summary
|
||||
[Brief description of the vulnerability]
|
||||
|
||||
## Impact
|
||||
- **Confidentiality:** [High/Medium/Low - what data can be exposed]
|
||||
- **Integrity:** [High/Medium/Low - what can be modified]
|
||||
- **Availability:** [High/Medium/Low - service disruption potential]
|
||||
- **Privilege Required:** [None/Low/High - authentication level needed]
|
||||
- **CVSS v3.1:** [Score] ([Vector string])
|
||||
|
||||
## Details
|
||||
[Technical details about the vulnerability]
|
||||
|
||||
**Affected Code:**
|
||||
```php
|
||||
// Path to affected file and vulnerable code
|
||||
```
|
||||
|
||||
**Attack Vector:**
|
||||
[How an attacker can exploit this]
|
||||
|
||||
## Proof of Concept
|
||||
```bash
|
||||
# Steps to reproduce
|
||||
```
|
||||
|
||||
## Patch
|
||||
[Suggested fix or approach]
|
||||
|
||||
## Affected Versions
|
||||
- OpenSourcePOS X.Y.Z and earlier
|
||||
|
||||
## Credit
|
||||
[Your GitHub username or preferred name]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Thank you to all security researchers who have contributed to making Open Source Point of Sale more secure.** Your voluntary efforts help protect thousands of users worldwide and contribute to a safer, more trustworthy free and open-source software ecosystem. We deeply appreciate your responsible disclosure and the time you invest in improving our project.
|
||||
|
||||
If you've reported a vulnerability and would like to discuss CVE coordination or have questions about the process, please reach out to us at [jeroen@steganos.dev](mailto:jeroen@steganos.dev).
|
||||
@@ -58,9 +58,9 @@ class App extends BaseConfig
|
||||
* Allowed Hostnames in the Site URL other than the hostname in the baseURL.
|
||||
* If you want to accept multiple Hostnames, set this.
|
||||
*
|
||||
* E.g.,
|
||||
* When your site URL ($baseURL) is 'http://example.com/', and your site
|
||||
* also accepts 'http://media.example.com/' and 'http://accounts.example.com/':
|
||||
* Or via environment variable (useful for Docker/Compose):
|
||||
* ALLOWED_HOSTNAMES=example.com,www.example.com
|
||||
*
|
||||
* ['media.example.com', 'accounts.example.com']
|
||||
*
|
||||
* @var list<string>
|
||||
@@ -286,7 +286,11 @@ class App extends BaseConfig
|
||||
|
||||
// Solution for CodeIgniter 4 limitation: arrays cannot be set from .env
|
||||
// See: https://github.com/codeigniter4/CodeIgniter4/issues/7311
|
||||
$envAllowedHostnames = getenv('app.allowedHostnames');
|
||||
// Support both: app.allowedHostnames (from .env) and ALLOWED_HOSTNAMES (from environment/Docker)
|
||||
$envAllowedHostnames = getenv('ALLOWED_HOSTNAMES');
|
||||
if ($envAllowedHostnames === false || trim($envAllowedHostnames) === '') {
|
||||
$envAllowedHostnames = getenv('app.allowedHostnames');
|
||||
}
|
||||
if ($envAllowedHostnames !== false && trim($envAllowedHostnames) !== '') {
|
||||
$this->allowedHostnames = array_values(array_filter(
|
||||
array_map('trim', explode(',', $envAllowedHostnames)),
|
||||
@@ -327,7 +331,7 @@ class App extends BaseConfig
|
||||
$errorMessage =
|
||||
'Security: allowedHostnames is not configured. ' .
|
||||
'Host header injection protection is disabled. ' .
|
||||
'Set app.allowedHostnames in your .env file. ' .
|
||||
'Set app.allowedHostnames in your .env file or ALLOWED_HOSTNAMES environment variable. ' .
|
||||
'Example: app.allowedHostnames = "example.com,www.example.com" ' .
|
||||
'Received Host: ' . $httpHost;
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ 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',
|
||||
@@ -157,9 +158,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',
|
||||
@@ -203,6 +204,7 @@ class Autoload extends AutoloadConfig
|
||||
'cookie',
|
||||
'tabular',
|
||||
'locale',
|
||||
'security'
|
||||
'security',
|
||||
'plugin'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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', 'receivings', 'reports', 'sales', 'config', 'suppliers'];
|
||||
const ADMIN_MODULES = ['customers', 'employees', 'giftcards', 'items', 'item_kits', 'messages', 'plugins', 'receivings', 'reports', 'sales', 'config', 'suppliers'];
|
||||
|
||||
@@ -8,6 +8,7 @@ use CodeIgniter\HotReloader\HotReloader;
|
||||
use App\Events\Db_log;
|
||||
use App\Events\Load_config;
|
||||
use App\Events\Method;
|
||||
use App\Libraries\Plugins\PluginManager;
|
||||
|
||||
/*
|
||||
* --------------------------------------------------------------------
|
||||
@@ -25,6 +26,9 @@ use App\Events\Method;
|
||||
* 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') {
|
||||
@@ -48,7 +52,6 @@ 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();
|
||||
@@ -57,8 +60,12 @@ 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']);
|
||||
Events::on('post_controller_constructor', [$config, 'load_config'], 1);
|
||||
|
||||
$db_log = new Db_log();
|
||||
Events::on('DBQuery', [$db_log, 'db_log_queries']);
|
||||
|
||||
@@ -486,10 +486,9 @@ class Mimes
|
||||
/**
|
||||
* Attempts to determine the best mime type for the given file extension.
|
||||
*
|
||||
* @param string $extension
|
||||
* @return array|string|null The mime type found, or none if unable to determine.
|
||||
* @return string|null The mime type found, or none if unable to determine.
|
||||
*/
|
||||
public static function guessTypeFromExtension(string $extension): array|string|null
|
||||
public static function guessTypeFromExtension(string $extension)
|
||||
{
|
||||
$extension = trim(strtolower($extension), '. ');
|
||||
|
||||
@@ -507,7 +506,7 @@ class Mimes
|
||||
*
|
||||
* @return string|null The extension determined, or null if unable to match.
|
||||
*/
|
||||
public static function guessExtensionFromType(string $type, ?string $proposedExtension = null): ?string
|
||||
public static function guessExtensionFromType(string $type, ?string $proposedExtension = null)
|
||||
{
|
||||
$type = trim(strtolower($type), '. ');
|
||||
|
||||
@@ -523,7 +522,7 @@ class Mimes
|
||||
}
|
||||
|
||||
// Reverse check the mime type list if no extension was proposed.
|
||||
// This search is order-sensitive!
|
||||
// This search is order sensitive!
|
||||
foreach (static::$mimes as $ext => $types) {
|
||||
if (in_array($type, (array) $types, true)) {
|
||||
return $ext;
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Config;
|
||||
use App\Models\Appconfig;
|
||||
use CodeIgniter\Cache\CacheInterface;
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use Config\Database;
|
||||
|
||||
/**
|
||||
* This class holds the configuration options stored from the database so that on launch those settings can be cached
|
||||
@@ -14,7 +14,7 @@ use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
*/
|
||||
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,23 +34,35 @@ class OSPOS extends BaseConfig
|
||||
|
||||
if ($cache) {
|
||||
$this->settings = decode_array($cache);
|
||||
} else {
|
||||
try {
|
||||
$appconfig = model(Appconfig::class);
|
||||
foreach ($appconfig->get_all()->getResult() as $app_config) {
|
||||
$this->settings[$app_config->key] = $app_config->value;
|
||||
}
|
||||
$this->cache->save('settings', encode_array($this->settings));
|
||||
} catch (DatabaseException $e) {
|
||||
// Database table doesn't exist yet (migrations haven't run)
|
||||
// Return empty settings to allow migration page to display
|
||||
$this->settings = [
|
||||
'language' => 'english',
|
||||
'language_code' => 'en',
|
||||
'company' => 'Home'
|
||||
];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = Database::connect();
|
||||
|
||||
if (!$db->tableExists('app_config')) {
|
||||
$this->settings = $this->getDefaultSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
$appconfig = model(Appconfig::class);
|
||||
foreach ($appconfig->get_all()->getResult() as $app_config) {
|
||||
$this->settings[$app_config->key] = $app_config->value;
|
||||
}
|
||||
$this->cache->save('settings', encode_array($this->settings));
|
||||
} catch (\Exception $e) {
|
||||
$this->settings = $this->getDefaultSettings();
|
||||
}
|
||||
}
|
||||
|
||||
private function getDefaultSettings(): array
|
||||
{
|
||||
return [
|
||||
'language' => 'english',
|
||||
'language_code' => 'en',
|
||||
'company' => 'Home',
|
||||
'barcode_type' => 'Code39'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,4 +73,4 @@ class OSPOS extends BaseConfig
|
||||
$this->cache->delete('settings');
|
||||
$this->set_settings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace Config;
|
||||
|
||||
use App\Libraries\MY_Language;
|
||||
use App\Libraries\Plugins\PluginManager;
|
||||
use Locale;
|
||||
use HTMLPurifier;
|
||||
use HTMLPurifier_Config;
|
||||
@@ -61,6 +62,24 @@ 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
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace Config;
|
||||
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Session\Handlers\BaseHandler;
|
||||
use CodeIgniter\Session\Handlers\DatabaseHandler;
|
||||
use CodeIgniter\Session\Handlers\FileHandler;
|
||||
@@ -139,7 +138,11 @@ class Session extends BaseConfig
|
||||
$this->driver = FileHandler::class;
|
||||
$this->savePath = WRITEPATH . 'session';
|
||||
}
|
||||
} catch (DatabaseException $e) {
|
||||
} catch (\Exception $e) {
|
||||
// Database not available yet (e.g. fresh install before migrations).
|
||||
// Fall back to file-based sessions so the login/migration page
|
||||
// can still be served. Catches mysqli_sql_exception which is
|
||||
// not a subclass of DatabaseException but is a RuntimeException.
|
||||
$this->driver = FileHandler::class;
|
||||
$this->savePath = WRITEPATH . 'session';
|
||||
}
|
||||
|
||||
@@ -28,12 +28,9 @@ abstract class BaseController extends Controller
|
||||
// protected $session;
|
||||
|
||||
/**
|
||||
* @param RequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param LoggerInterface $logger
|
||||
* @return void
|
||||
*/
|
||||
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger): void
|
||||
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
|
||||
{
|
||||
// Load here all helpers you want to be available in your controllers that extend BaseController.
|
||||
// Caution: Do not put the this below the parent::initController() call below.
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
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;
|
||||
@@ -82,7 +81,7 @@ class Config extends Secure_Controller
|
||||
$npmDev = false;
|
||||
$license = [];
|
||||
|
||||
$license[$i]['title'] = 'Open Source Point Of Sale ' . config('App')->application_version;
|
||||
$license[$i]['title'] = 'Open Source Point of Sale ' . config('App')->application_version;
|
||||
|
||||
if (file_exists('license/LICENSE')) {
|
||||
$license[$i]['text'] = file_get_contents('license/LICENSE', false, null, 0, 3000);
|
||||
@@ -221,6 +220,7 @@ class Config extends Secure_Controller
|
||||
*/
|
||||
public function getIndex(): string
|
||||
{
|
||||
$data['config'] = $this->config;
|
||||
$data['stock_locations'] = $this->stock_location->get_all()->getResultArray();
|
||||
$data['dinner_tables'] = $this->dinner_table->get_all()->getResultArray();
|
||||
$data['customer_rewards'] = $this->customer_rewards->get_all()->getResultArray();
|
||||
@@ -231,6 +231,8 @@ class Config extends Secure_Controller
|
||||
$data['line_sequence_options'] = $this->sale_lib->get_line_sequence_options();
|
||||
$data['register_mode_options'] = $this->sale_lib->get_register_mode_options();
|
||||
$data['invoice_type_options'] = $this->sale_lib->get_invoice_type_options();
|
||||
$data['keyboardShortcutOptions'] = $this->sale_lib->getKeyShortcutsOptions();
|
||||
$data['keyboardShortcuts'] = $this->sale_lib->getKeyShortcuts();
|
||||
$data['rounding_options'] = rounding_mode::get_rounding_options();
|
||||
$data['tax_code_options'] = $this->tax_lib->get_tax_code_options();
|
||||
$data['tax_category_options'] = $this->tax_lib->get_tax_category_options();
|
||||
@@ -250,32 +252,6 @@ class Config extends Secure_Controller
|
||||
$data['image_allowed_types'] = array_combine($image_allowed_types, $image_allowed_types);
|
||||
$data['selected_image_allowed_types'] = explode(',', $this->config['image_allowed_types']);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
@@ -313,7 +289,6 @@ class Config extends Secure_Controller
|
||||
return $this->response->setJSON(['success' => $success, 'message' => $message]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
@@ -398,6 +373,9 @@ class Config extends Secure_Controller
|
||||
|
||||
$this->module->set_show_office_group($this->request->getPost('show_office_group') != null);
|
||||
|
||||
$this->db->transStart();
|
||||
|
||||
$attributeSuccess = true;
|
||||
if ($batchSaveData['category_dropdown']) {
|
||||
$definitionData['definition_name'] = 'ospos_category';
|
||||
$definitionData['definition_flags'] = 0;
|
||||
@@ -405,12 +383,16 @@ class Config extends Secure_Controller
|
||||
$definitionData['definition_id'] = CATEGORY_DEFINITION_ID;
|
||||
$definitionData['deleted'] = 0;
|
||||
|
||||
$this->attribute->saveDefinition($definitionData, CATEGORY_DEFINITION_ID);
|
||||
$attributeSuccess = $this->attribute->saveDefinition($definitionData, CATEGORY_DEFINITION_ID);
|
||||
} elseif ($batchSaveData['category_dropdown'] == NO_DEFINITION_ID) {
|
||||
$this->attribute->deleteDefinition(CATEGORY_DEFINITION_ID);
|
||||
$attributeSuccess = $this->attribute->deleteDefinition(CATEGORY_DEFINITION_ID);
|
||||
}
|
||||
|
||||
$success = $this->appconfig->batch_save($batchSaveData);
|
||||
$success = $attributeSuccess && $this->appconfig->batch_save($batchSaveData);
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
$success = $success && $this->db->transStatus();
|
||||
|
||||
return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]);
|
||||
}
|
||||
@@ -423,32 +405,35 @@ class Config extends Secure_Controller
|
||||
*/
|
||||
public function postCheckNumberLocale(): ResponseInterface
|
||||
{
|
||||
$number_locale = $this->request->getPost('number_locale');
|
||||
$save_number_locale = $this->request->getPost('save_number_locale');
|
||||
$numberLocale = $this->request->getPost('number_locale');
|
||||
$saveNumberLocale = $this->request->getPost('save_number_locale');
|
||||
$postedCurrencySymbol = $this->request->getPost('currency_symbol');
|
||||
$postedCurrencyCode = $this->request->getPost('currency_code');
|
||||
|
||||
$fmt = new NumberFormatter($number_locale, NumberFormatter::CURRENCY);
|
||||
if ($number_locale != $save_number_locale) {
|
||||
$currency_symbol = $fmt->getSymbol(NumberFormatter::CURRENCY_SYMBOL);
|
||||
$currency_code = $fmt->getTextAttribute(NumberFormatter::CURRENCY_CODE);
|
||||
$save_number_locale = $number_locale;
|
||||
} else {
|
||||
$currency_symbol = empty($this->request->getPost('currency_symbol')) ? $fmt->getSymbol(NumberFormatter::CURRENCY_SYMBOL) : $this->request->getPost('currency_symbol');
|
||||
$currency_code = empty($this->request->getPost('currency_code')) ? $fmt->getTextAttribute(NumberFormatter::CURRENCY_CODE) : $this->request->getPost('currency_code');
|
||||
$fmt = new NumberFormatter($numberLocale, NumberFormatter::CURRENCY);
|
||||
|
||||
// Use posted values if provided, otherwise fall back to locale defaults
|
||||
$currencySymbol = $postedCurrencySymbol !== '' ? $postedCurrencySymbol : $fmt->getSymbol(NumberFormatter::CURRENCY_SYMBOL);
|
||||
$currencyCode = $postedCurrencyCode !== '' ? $postedCurrencyCode : $fmt->getTextAttribute(NumberFormatter::CURRENCY_CODE);
|
||||
|
||||
// Update saved locale if it changed
|
||||
if ($numberLocale !== $saveNumberLocale) {
|
||||
$saveNumberLocale = $numberLocale;
|
||||
}
|
||||
|
||||
if ($this->request->getPost('thousands_separator') == 'false') {
|
||||
$fmt->setTextAttribute(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, '');
|
||||
}
|
||||
|
||||
$fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL, $currency_symbol);
|
||||
$number_local_example = $fmt->format(1234567890.12300);
|
||||
$fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL, $currencySymbol);
|
||||
$numberLocaleExample = $fmt->format(1234567890.12300);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'success' => $number_local_example != false,
|
||||
'save_number_locale' => $save_number_locale,
|
||||
'number_locale_example' => $number_local_example,
|
||||
'currency_symbol' => $currency_symbol,
|
||||
'currency_code' => $currency_code,
|
||||
'success' => $numberLocaleExample != false,
|
||||
'save_number_locale' => $saveNumberLocale,
|
||||
'number_locale_example' => $numberLocaleExample,
|
||||
'currency_symbol' => $currencySymbol,
|
||||
'currency_code' => $currencyCode,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -562,76 +547,6 @@ 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
|
||||
*
|
||||
@@ -911,7 +826,9 @@ class Config extends Secure_Controller
|
||||
public function postSaveReceipt(): ResponseInterface
|
||||
{
|
||||
$batch_save_data = [
|
||||
'receipt_template' => $this->request->getPost('receipt_template'),
|
||||
'receipt_template' => Sale_lib::isValidReceiptTemplate($this->request->getPost('receipt_template'))
|
||||
? $this->request->getPost('receipt_template')
|
||||
: 'receipt_default',
|
||||
'receipt_font_size' => $this->request->getPost('receipt_font_size', FILTER_SANITIZE_NUMBER_INT),
|
||||
'print_delay_autoreturn' => $this->request->getPost('print_delay_autoreturn', FILTER_SANITIZE_NUMBER_INT),
|
||||
'email_receipt_check_behaviour' => $this->request->getPost('email_receipt_check_behaviour'),
|
||||
@@ -936,6 +853,44 @@ class Config extends Secure_Controller
|
||||
return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves keyboard shortcut bindings.
|
||||
*
|
||||
* @return ResponseInterface
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function postSaveShortcuts(): ResponseInterface
|
||||
{
|
||||
$allowedShortcuts = array_keys($this->sale_lib->getKeyShortcutsOptions());
|
||||
$currentShortcuts = $this->sale_lib->getKeyShortcuts();
|
||||
$batchSaveData = [];
|
||||
|
||||
foreach ($currentShortcuts as $name => $shortcut) {
|
||||
$postedValue = trim((string)$this->request->getPost('key_' . $name));
|
||||
|
||||
if (!in_array($postedValue, $allowedShortcuts, true)) {
|
||||
$postedValue = $shortcut['value'];
|
||||
}
|
||||
|
||||
$batchSaveData['key_' . $name] = $postedValue;
|
||||
}
|
||||
|
||||
$duplicateValues = array_filter(array_count_values($batchSaveData), static fn(int $count): bool => $count > 1);
|
||||
if (!empty($duplicateValues)) {
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'message' => lang('Config.shortcuts_duplicate_bindings')
|
||||
]);
|
||||
}
|
||||
|
||||
$success = $this->appconfig->batch_save($batchSaveData);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'success' => $success,
|
||||
'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves invoice configuration. Used in app/Views/configs/invoice_config.php.
|
||||
*
|
||||
@@ -959,8 +914,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'
|
||||
];
|
||||
|
||||
@@ -1006,8 +961,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' : '';
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
|
||||
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;
|
||||
@@ -15,8 +14,6 @@ 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;
|
||||
@@ -25,19 +22,11 @@ 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 = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,11 +41,12 @@ 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->get_info($row_id);
|
||||
$person = $this->customer->getInfo($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.
|
||||
@@ -141,14 +131,16 @@ class Customers extends Persons
|
||||
|
||||
/**
|
||||
* Loads the customer edit form
|
||||
* @param int $customerId
|
||||
* @return string
|
||||
*/
|
||||
public function getView(int $customer_id = NEW_ENTRY): string
|
||||
public function getView(int $customerId = NEW_ENTRY): string
|
||||
{
|
||||
// Set default values
|
||||
if ($customer_id == null) $customer_id = NEW_ENTRY;
|
||||
if ($customerId == null) {
|
||||
$customerId = NEW_ENTRY;
|
||||
}
|
||||
|
||||
$info = $this->customer->get_info($customer_id);
|
||||
$info = $this->customer->getInfo($customerId);
|
||||
foreach (get_object_vars($info) as $property => $value) {
|
||||
$info->$property = $value;
|
||||
}
|
||||
@@ -159,7 +151,7 @@ class Customers extends Persons
|
||||
$data['person_info']->employee_id = $this->employee->get_logged_in_employee_info()->person_id;
|
||||
}
|
||||
|
||||
$employee_info = $this->employee->get_info($info->employee_id);
|
||||
$employee_info = $this->employee->getInfo($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);
|
||||
@@ -180,7 +172,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($customer_id);
|
||||
$stats = $this->customer->get_stats($customerId);
|
||||
if (!empty($stats)) {
|
||||
foreach (get_object_vars($stats) as $property => $value) {
|
||||
$info->$property = $value;
|
||||
@@ -188,69 +180,29 @@ class Customers extends Persons
|
||||
$data['stats'] = $stats;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Events::trigger('customer_loaded', $customerId);
|
||||
|
||||
return view("customers/form", $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts/updates a customer
|
||||
* @param int $customerId
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function postSave(int $customer_id = NEW_ENTRY): ResponseInterface
|
||||
public function postSave(int $customerId = NEW_ENTRY): ResponseInterface
|
||||
{
|
||||
$first_name = $this->request->getPost('first_name');
|
||||
$last_name = $this->request->getPost('last_name');
|
||||
$firstName = $this->request->getPost('first_name');
|
||||
$lastName = $this->request->getPost('last_name');
|
||||
$email = strtolower($this->request->getPost('email', FILTER_SANITIZE_EMAIL));
|
||||
|
||||
// Format first and last name properly
|
||||
$first_name = $this->nameize($first_name);
|
||||
$last_name = $this->nameize($last_name);
|
||||
$firstName = $this->nameize($firstName);
|
||||
$lastName = $this->nameize($lastName);
|
||||
|
||||
$person_data = [
|
||||
'first_name' => $first_name,
|
||||
'last_name' => $last_name,
|
||||
$personData = [
|
||||
'first_name' => $firstName,
|
||||
'last_name' => $lastName,
|
||||
'gender' => $this->request->getPost('gender', FILTER_SANITIZE_NUMBER_INT),
|
||||
'email' => $email,
|
||||
'phone_number' => $this->request->getPost('phone_number'),
|
||||
@@ -263,9 +215,9 @@ class Customers extends Persons
|
||||
'comments' => $this->request->getPost('comments')
|
||||
];
|
||||
|
||||
$date_formatter = date_create_from_format($this->config['dateformat'] . ' ' . $this->config['timeformat'], $this->request->getPost('date'));
|
||||
$dateFormatter = date_create_from_format($this->config['dateformat'] . ' ' . $this->config['timeformat'], $this->request->getPost('date'));
|
||||
|
||||
$customer_data = [
|
||||
$customerData = [
|
||||
'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'),
|
||||
@@ -274,41 +226,32 @@ 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' => $date_formatter->format('Y-m-d H:i:s'),
|
||||
'date' => $dateFormatter->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->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]
|
||||
);
|
||||
if ($this->customer->saveCustomer($personData, $customerData, $customerId)) {
|
||||
Events::trigger('customer_saved', [$customerData['person_id']]);
|
||||
|
||||
// New customer
|
||||
if ($customer_id == NEW_ENTRY) {
|
||||
if ($customerId == NEW_ENTRY) {
|
||||
return $this->response->setJSON([
|
||||
'success' => true,
|
||||
'message' => lang('Customers.successful_adding') . ' ' . $first_name . ' ' . $last_name,
|
||||
'id' => $customer_data['person_id']
|
||||
'message' => lang('Customers.successful_adding') . " $firstName $lastName",
|
||||
'id' => $customerData['person_id']
|
||||
]);
|
||||
} else { // Existing customer
|
||||
return $this->response->setJSON([
|
||||
'success' => true,
|
||||
'message' => lang('Customers.successful_updating') . ' ' . $first_name . ' ' . $last_name,
|
||||
'id' => $customer_id
|
||||
'message' => lang('Customers.successful_updating') . " $firstName $lastName",
|
||||
'id' => $customerId
|
||||
]);
|
||||
}
|
||||
} else { // Failure
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'message' => lang('Customers.error_adding_updating') . ' ' . $first_name . ' ' . $last_name,
|
||||
'message' => lang('Customers.error_adding_updating') . " $firstName $lastName",
|
||||
'id' => NEW_ENTRY
|
||||
]);
|
||||
}
|
||||
@@ -344,26 +287,23 @@ class Customers extends Persons
|
||||
}
|
||||
|
||||
/**
|
||||
* This deletes customers from the customers table
|
||||
* This deletes customers from the customer's table
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function postDelete(): ResponseInterface
|
||||
{
|
||||
$customers_to_delete = $this->request->getPost('ids');
|
||||
$customers_info = $this->customer->get_multiple_info($customers_to_delete);
|
||||
$customersToDelete = $this->request->getPost('ids');
|
||||
$customers = $this->customer->get_multiple_info($customersToDelete);
|
||||
|
||||
$count = 0;
|
||||
|
||||
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);
|
||||
|
||||
foreach ($customers->getResult() as $customer) {
|
||||
if ($this->customer->delete($customer->person_id)) {
|
||||
Events::trigger('customer_deleted', (int)$customer->person_id, (string)$customer->email);
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($count == count($customers_to_delete)) {
|
||||
if ($count === count($customersToDelete)) {
|
||||
return $this->response->setJSON([
|
||||
'success' => true,
|
||||
'message' => lang('Customers.successful_deleted') . ' ' . $count . ' ' . lang('Customers.one_or_multiple')
|
||||
@@ -411,16 +351,17 @@ 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);
|
||||
$i = 1;
|
||||
$rowNumber = 1;
|
||||
|
||||
$failCodes = [];
|
||||
$customerIds = [];
|
||||
|
||||
while (($data = fgetcsv($handle)) !== false) {
|
||||
$consent = $data[3] == '' ? 0 : 1;
|
||||
|
||||
if (sizeof($data) >= 16 && $consent) {
|
||||
$email = strtolower($data[4]);
|
||||
$person_data = [
|
||||
$personData = [
|
||||
'first_name' => $data[0],
|
||||
'last_name' => $data[1],
|
||||
'gender' => $data[2],
|
||||
@@ -435,7 +376,7 @@ class Customers extends Persons
|
||||
'comments' => $data[12]
|
||||
];
|
||||
|
||||
$customer_data = [
|
||||
$customerData = [
|
||||
'consent' => $consent,
|
||||
'company_name' => $data[13],
|
||||
'discount' => $data[15],
|
||||
@@ -450,7 +391,7 @@ class Customers extends Persons
|
||||
$invalidated = $this->customer->check_email_exists($email);
|
||||
|
||||
if ($account_number != '') {
|
||||
$customer_data['account_number'] = $account_number;
|
||||
$customerData['account_number'] = $account_number;
|
||||
$invalidated &= $this->customer->check_account_number_exists($account_number);
|
||||
}
|
||||
} else {
|
||||
@@ -458,16 +399,15 @@ class Customers extends Persons
|
||||
}
|
||||
|
||||
if ($invalidated) {
|
||||
$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']);
|
||||
$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'];
|
||||
} else {
|
||||
$failCodes[] = $i;
|
||||
$failCodes[] = $rowNumber;
|
||||
}
|
||||
|
||||
++$i;
|
||||
++$rowNumber;
|
||||
}
|
||||
|
||||
if (count($failCodes) > 0) {
|
||||
@@ -475,6 +415,8 @@ 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 {
|
||||
|
||||
@@ -75,7 +75,7 @@ class Employees extends Persons
|
||||
*/
|
||||
public function getView(int $employee_id = NEW_ENTRY): string
|
||||
{
|
||||
$person_info = $this->employee->get_info($employee_id);
|
||||
$person_info = $this->employee->getInfo($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->get_info($employee_id);
|
||||
$target_employee = $this->employee->getInfo($employee_id);
|
||||
if (!$this->employee->canModifyEmployee($target_employee->person_id, $current_user->person_id)) {
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
|
||||
@@ -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->get_info($stored_employee_id);
|
||||
$stored_employee = $this->employee->getInfo($stored_employee_id);
|
||||
$data['employees'][$stored_employee_id] = $stored_employee->first_name . ' ' . $stored_employee->last_name;
|
||||
}
|
||||
$data['can_assign_employee'] = $can_assign_employee;
|
||||
|
||||
@@ -43,7 +43,7 @@ class Home extends Secure_Controller
|
||||
public function getChangePassword(int $employeeId = NEW_ENTRY): ResponseInterface|string
|
||||
{
|
||||
$loggedInEmployee = $this->employee->get_logged_in_employee_info();
|
||||
$currentPersonId = $loggedInEmployee->person_id;
|
||||
$currentPersonId = (int) $loggedInEmployee->person_id;
|
||||
|
||||
$employeeId = $employeeId === NEW_ENTRY ? $currentPersonId : $employeeId;
|
||||
|
||||
@@ -51,7 +51,7 @@ class Home extends Secure_Controller
|
||||
return $this->response->setStatusCode(403)->setBody(lang('Employees.unauthorized_modify'));
|
||||
}
|
||||
|
||||
$person_info = $this->employee->get_info($employeeId);
|
||||
$person_info = $this->employee->getInfo($employeeId);
|
||||
foreach (get_object_vars($person_info) as $property => $value) {
|
||||
$person_info->$property = $value;
|
||||
}
|
||||
@@ -68,10 +68,11 @@ class Home extends Secure_Controller
|
||||
public function postSave(int $employeeId = NEW_ENTRY): ResponseInterface
|
||||
{
|
||||
$currentUser = $this->employee->get_logged_in_employee_info();
|
||||
$currentPersonId = (int) $currentUser->person_id;
|
||||
|
||||
$employeeId = $employeeId === NEW_ENTRY ? $currentUser->person_id : $employeeId;
|
||||
$employeeId = $employeeId === NEW_ENTRY ? $currentPersonId : $employeeId;
|
||||
|
||||
if (!$this->employee->isAdmin($currentUser->person_id) && $employeeId !== $currentUser->person_id) {
|
||||
if (!$this->employee->isAdmin($currentPersonId) && $employeeId !== $currentPersonId) {
|
||||
return $this->response->setStatusCode(403)->setJSON([
|
||||
'success' => false,
|
||||
'message' => lang('Employees.unauthorized_modify')
|
||||
|
||||
@@ -13,6 +13,7 @@ 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;
|
||||
@@ -154,8 +155,23 @@ class Items extends Secure_Controller
|
||||
{
|
||||
helper('file');
|
||||
|
||||
$pic_filename = rawurldecode($pic_filename);
|
||||
$file_extension = pathinfo($pic_filename, PATHINFO_EXTENSION);
|
||||
// Security: Sanitize filename to prevent path traversal
|
||||
// Use basename() to strip directory components and prevent '../' attacks
|
||||
$pic_filename = basename(rawurldecode($pic_filename));
|
||||
$file_extension = strtolower(pathinfo($pic_filename, PATHINFO_EXTENSION));
|
||||
|
||||
// Validate file extension against system-configured allowed image types
|
||||
// Handle both legacy pipe-separated and current comma-separated formats
|
||||
// Fallback to types that GD library can process for thumbnail generation
|
||||
$allowed_types = $this->config['image_allowed_types'] ?? 'jpg,jpeg,gif,png,webp,bmp,tif,tiff';
|
||||
$allowed_extensions = strpos($allowed_types, '|') !== false
|
||||
? explode('|', $allowed_types)
|
||||
: explode(',', $allowed_types);
|
||||
|
||||
if (!in_array($file_extension, $allowed_extensions, true)) {
|
||||
return $this->response->setStatusCode(400)->setBody('Invalid file type');
|
||||
}
|
||||
|
||||
$images = glob("./uploads/item_pics/$pic_filename");
|
||||
$base_path = './uploads/item_pics/' . pathinfo($pic_filename, PATHINFO_FILENAME);
|
||||
|
||||
@@ -260,7 +276,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->get_multiple_info(explode(':', $item_ids), $this->item_lib->get_item_location());
|
||||
$item_infos = $this->item->getMultipleInfo(explode(':', $item_ids), $this->item_lib->get_item_location());
|
||||
|
||||
$result = [];
|
||||
|
||||
@@ -476,7 +492,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->get_multiple_info($item_ids, $this->item_lib->get_item_location())->getResultArray();
|
||||
$result = $this->item->getMultipleInfo($item_ids, $this->item_lib->get_item_location())->getResultArray();
|
||||
$data['barcode_config'] = $this->barcode_lib->get_barcode_config();
|
||||
|
||||
foreach ($result as &$item) {
|
||||
@@ -596,148 +612,149 @@ class Items extends Secure_Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $item_id
|
||||
* @param int $itemId
|
||||
* @return ResponseInterface
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function postSave(int $item_id = NEW_ENTRY): ResponseInterface
|
||||
public function postSave(int $itemId = NEW_ENTRY): ResponseInterface
|
||||
{
|
||||
$upload_data = $this->upload_image();
|
||||
$upload_success = empty($upload_data['error']);
|
||||
$uploadData = $this->upload_image();
|
||||
$uploadSuccess = empty($uploadData['error']);
|
||||
|
||||
$raw_receiving_quantity = $this->request->getPost('receiving_quantity');
|
||||
$rawReceivingQuantity = $this->request->getPost('receiving_quantity');
|
||||
|
||||
$receiving_quantity = parse_quantity($raw_receiving_quantity);
|
||||
$item_type = $this->request->getPost('item_type') === null ? ITEM : intval($this->request->getPost('item_type'));
|
||||
$receivingQuantity = parse_quantity($rawReceivingQuantity);
|
||||
$itemType = $this->request->getPost('item_type') === null ? ITEM : intval($this->request->getPost('item_type'));
|
||||
|
||||
if ($receiving_quantity === 0.0 && $item_type !== ITEM_TEMP) {
|
||||
$receiving_quantity = 1;
|
||||
if ($receivingQuantity === 0.0 && $itemType !== ITEM_TEMP) {
|
||||
$receivingQuantity = 1;
|
||||
}
|
||||
|
||||
$default_pack_name = lang('Items.default_pack_name');
|
||||
$defaultPackName = lang('Items.default_pack_name');
|
||||
|
||||
$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') ?? '');
|
||||
$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') ?? '');
|
||||
|
||||
// Save item data
|
||||
$item_data = [
|
||||
$itemData = [
|
||||
'name' => $this->request->getPost('name'),
|
||||
'description' => $this->request->getPost('description', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||
'category' => $this->request->getPost('category'),
|
||||
'item_type' => $item_type,
|
||||
'item_type' => $itemType,
|
||||
'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' => $cost_price,
|
||||
'unit_price' => $unit_price,
|
||||
'reorder_level' => $reorder_level,
|
||||
'receiving_quantity' => $receiving_quantity,
|
||||
'cost_price' => $costPrice,
|
||||
'unit_price' => $unitPrice,
|
||||
'reorder_level' => $reorderLevel,
|
||||
'receiving_quantity' => $receivingQuantity,
|
||||
'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($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')),
|
||||
'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')),
|
||||
'deleted' => $this->request->getPost('is_deleted') != null,
|
||||
'hsn_code' => $this->request->getPost('hsn_code') === null ? '' : $this->request->getPost('hsn_code')
|
||||
];
|
||||
|
||||
if ($item_data['item_type'] == ITEM_TEMP) {
|
||||
$item_data['stock_type'] = HAS_NO_STOCK;
|
||||
$item_data['receiving_quantity'] = 0;
|
||||
$item_data['reorder_level'] = 0;
|
||||
if ($itemData['item_type'] == ITEM_TEMP) {
|
||||
$itemData['stock_type'] = HAS_NO_STOCK;
|
||||
$itemData['receiving_quantity'] = 0;
|
||||
$itemData['reorder_level'] = 0;
|
||||
}
|
||||
|
||||
$tax_category_id = $this->request->getPost('tax_category_id');
|
||||
$taxCategoryId = $this->request->getPost('tax_category_id');
|
||||
|
||||
if (!isset($tax_category_id)) {
|
||||
$item_data['tax_category_id'] = null;
|
||||
if (!isset($taxCategoryId)) {
|
||||
$itemData['tax_category_id'] = null;
|
||||
} else {
|
||||
$item_data['tax_category_id'] = empty($this->request->getPost('tax_category_id')) ? null : intval($this->request->getPost('tax_category_id'));
|
||||
$itemData['tax_category_id'] = empty($this->request->getPost('tax_category_id')) ? null : intval($this->request->getPost('tax_category_id'));
|
||||
}
|
||||
|
||||
if (!empty($upload_data['orig_name']) && $upload_data['raw_name']) {
|
||||
$item_data['pic_filename'] = $upload_data['raw_name'] . '.' . $upload_data['file_ext'];
|
||||
if (!empty($uploadData['orig_name']) && $uploadData['raw_name']) {
|
||||
$itemData['pic_filename'] = $uploadData['raw_name'] . '.' . $uploadData['file_ext'];
|
||||
}
|
||||
|
||||
$employee_id = $this->employee->get_logged_in_employee_info()->person_id;
|
||||
$employeeId = $this->employee->get_logged_in_employee_info()->person_id;
|
||||
|
||||
if ($this->item->save_value($item_data, $item_id)) {
|
||||
if ($this->item->save_value($itemData, $itemId)) {
|
||||
$success = true;
|
||||
$new_item = false;
|
||||
$newItem = false;
|
||||
|
||||
if ($item_id === NEW_ENTRY) {
|
||||
$item_id = $item_data['item_id'];
|
||||
$new_item = true;
|
||||
if ($itemId === NEW_ENTRY) {
|
||||
$itemId = $itemData['item_id'];
|
||||
$newItem = true;
|
||||
}
|
||||
|
||||
$use_destination_based_tax = (bool)$this->config['use_destination_based_tax'];
|
||||
$useDestinationBasedTax = (bool)$this->config['use_destination_based_tax'];
|
||||
|
||||
if (!$use_destination_based_tax) {
|
||||
$items_taxes_data = [];
|
||||
$tax_names = $this->request->getPost('tax_names');
|
||||
$tax_percents = $this->request->getPost('tax_percents');
|
||||
if (!$useDestinationBasedTax) {
|
||||
$itemsTaxesData = [];
|
||||
$taxNames = $this->request->getPost('tax_names');
|
||||
$taxPercents = $this->request->getPost('tax_percents');
|
||||
|
||||
$tax_name_index = 0;
|
||||
$taxNameIndex = 0;
|
||||
|
||||
foreach ($tax_percents as $tax_percent) {
|
||||
$tax_percentage = parse_tax($tax_percent);
|
||||
foreach ($taxPercents as $taxPercent) {
|
||||
$taxpercentage = parse_tax($taxPercent);
|
||||
|
||||
if (is_numeric($tax_percentage)) {
|
||||
$items_taxes_data[] = ['name' => $tax_names[$tax_name_index], 'percent' => $tax_percentage];
|
||||
if (is_numeric($taxpercentage)) {
|
||||
$itemsTaxesData[] = ['name' => $taxNames[$taxNameIndex], 'percent' => $taxpercentage];
|
||||
}
|
||||
|
||||
$tax_name_index++;
|
||||
$taxNameIndex++;
|
||||
}
|
||||
$success &= $this->item_taxes->save_value($items_taxes_data, $item_id);
|
||||
$success &= $this->item_taxes->save_value($itemsTaxesData, $itemId);
|
||||
}
|
||||
|
||||
// Save item quantity
|
||||
$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']));
|
||||
$stockLocations = $this->stock_location->get_undeleted_all()->getResultArray();
|
||||
foreach ($stockLocations as $location) {
|
||||
$updatedQuantity = parse_quantity($this->request->getPost('quantity_' . $location['location_id']));
|
||||
|
||||
if ($item_data['item_type'] == ITEM_TEMP) {
|
||||
$updated_quantity = 0;
|
||||
if ($itemData['item_type'] == ITEM_TEMP) {
|
||||
$updatedQuantity = 0;
|
||||
}
|
||||
|
||||
$location_detail = [
|
||||
'item_id' => $item_id,
|
||||
$locationDetail = [
|
||||
'item_id' => $itemId,
|
||||
'location_id' => $location['location_id'],
|
||||
'quantity' => $updated_quantity
|
||||
'quantity' => $updatedQuantity
|
||||
];
|
||||
|
||||
$item_quantity = $this->item_quantity->get_item_quantity($item_id, $location['location_id']);
|
||||
$itemQuantity = $this->item_quantity->get_item_quantity($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']);
|
||||
if ($itemQuantity->quantity != $updatedQuantity || $newItem) {
|
||||
$success = $success && $this->item_quantity->save_value($locationDetail, $itemId, $location['location_id']);
|
||||
|
||||
$inv_data = [
|
||||
'trans_date' => date('Y-m-d H:i:s'),
|
||||
'trans_items' => $item_id,
|
||||
'trans_user' => $employee_id,
|
||||
'trans_items' => $itemId,
|
||||
'trans_user' => $employeeId,
|
||||
'trans_location' => $location['location_id'],
|
||||
'trans_comment' => lang('Items.manually_editing_of_quantity'),
|
||||
'trans_inventory' => $updated_quantity - $item_quantity->quantity
|
||||
'trans_inventory' => $updatedQuantity - $itemQuantity->quantity
|
||||
];
|
||||
|
||||
$success = $success && $this->inventory->insert($inv_data, false);
|
||||
}
|
||||
}
|
||||
$success = $success && $this->saveItemAttributes($item_id);
|
||||
$success = $success && $this->saveItemAttributes($itemId);
|
||||
|
||||
if ($success && $upload_success) {
|
||||
$message = lang('Items.successful_' . ($new_item ? 'adding' : 'updating')) . ' ' . $item_data['name'];
|
||||
if ($success && $uploadSuccess) {
|
||||
Events::trigger('item_saved', [$itemId]);
|
||||
|
||||
return $this->response->setJSON(['success' => true, 'message' => $message, 'id' => $item_id]);
|
||||
$message = lang('Items.successful_' . ($newItem ? 'adding' : 'updating')) . ' ' . $itemData['name'];
|
||||
return $this->response->setJSON(['success' => true, 'message' => $message, 'id' => $itemId]);
|
||||
} else {
|
||||
$message = $upload_success ? lang('Items.error_adding_updating') . ' ' . $item_data['name'] : strip_tags($upload_data['error']);
|
||||
$message = $uploadSuccess ? lang('Items.error_adding_updating') . ' ' . $itemData['name'] : strip_tags($uploadData['error']);
|
||||
|
||||
return $this->response->setJSON(['success' => false, 'message' => $message, 'id' => $item_id]);
|
||||
return $this->response->setJSON(['success' => false, 'message' => $message, 'id' => $itemId]);
|
||||
}
|
||||
} else {
|
||||
$message = lang('Items.error_adding_updating') . ' ' . $item_data['name'];
|
||||
$message = lang('Items.error_adding_updating') . ' ' . $itemData['name'];
|
||||
|
||||
return $this->response->setJSON(['success' => false, 'message' => $message, 'id' => NEW_ENTRY]);
|
||||
}
|
||||
@@ -957,7 +974,7 @@ class Items extends Secure_Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports items from a CSV formatted file.
|
||||
* Imports items from a CSV-formatted file.
|
||||
* @return ResponseInterface
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
@@ -982,7 +999,7 @@ class Items extends Secure_Controller
|
||||
$attributeData = [];
|
||||
|
||||
foreach ($attributeDefinitionNames as $definitionName) {
|
||||
$attributeData[$definitionName] = $this->attribute->get_definition_by_name($definitionName)[0];
|
||||
$attributeData[$definitionName] = $this->attribute->getDefinitionByName($definitionName)[0];
|
||||
|
||||
if ($attributeData[$definitionName]['definition_type'] === DROPDOWN) {
|
||||
$attributeData[$definitionName]['dropdown_values'] = $this->attribute->get_definition_values($attributeData[$definitionName]['definition_id']);
|
||||
@@ -991,6 +1008,7 @@ 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'];
|
||||
@@ -1040,20 +1058,28 @@ class Items extends Secure_Controller
|
||||
});
|
||||
|
||||
if (!$isFailedRow && $this->item->save_value($itemData, $itemId)) {
|
||||
$this->save_tax_data($row, $itemData);
|
||||
$this->save_inventory_quantities($row, $itemData, $allowedStockLocations, $employeeId);
|
||||
if (!$this->save_tax_data($row, $itemData)) {
|
||||
$isFailedRow = true;
|
||||
}
|
||||
if (!$this->save_inventory_quantities($row, $itemData, $allowedStockLocations, $employeeId)) {
|
||||
$isFailedRow = true;
|
||||
}
|
||||
$csvAttributeValues = $this->extractAttributeData($row);
|
||||
$isFailedRow = !$this->attribute->saveCSVRowAttributeData($csvAttributeValues, $itemData, $attributeData);
|
||||
if (!$this->attribute->saveCSVRowAttributeData($csvAttributeValues, $itemData, $attributeData)) {
|
||||
$isFailedRow = true;
|
||||
}
|
||||
if ($isFailedRow) {
|
||||
$failedRow = $key + 2;
|
||||
$failCodes[] = $failedRow;
|
||||
log_message('error', "CSV Item import failed on line $failedRow while saving attributes.");
|
||||
log_message('error', "CSV Item import failed on line $failedRow while saving item.");
|
||||
continue;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -1073,6 +1099,8 @@ 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 {
|
||||
@@ -1237,13 +1265,15 @@ class Items extends Secure_Controller
|
||||
* @param array $item_data
|
||||
* @param array $allowed_locations
|
||||
* @param int $employee_id
|
||||
* @return bool Returns true on success, false on failure
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
private function save_inventory_quantities(array $row, array $item_data, array $allowed_locations, int $employee_id): void
|
||||
private function save_inventory_quantities(array $row, array $item_data, array $allowed_locations, int $employee_id): bool
|
||||
{
|
||||
// Quantities & Inventory Section
|
||||
$comment = lang('Items.inventory_CSV_import_quantity');
|
||||
$is_update = (bool)$row['Id'];
|
||||
$success = true;
|
||||
|
||||
foreach ($allowed_locations as $location_id => $location_name) {
|
||||
$item_quantity_data = ['item_id' => $item_data['item_id'], 'location_id' => $location_id];
|
||||
@@ -1257,20 +1287,22 @@ class Items extends Secure_Controller
|
||||
|
||||
if (!empty($row["location_$location_name"]) || $row["location_$location_name"] === '0') {
|
||||
$item_quantity_data['quantity'] = $row["location_$location_name"];
|
||||
$this->item_quantity->save_value($item_quantity_data, $item_data['item_id'], $location_id);
|
||||
$success &= $this->item_quantity->save_value($item_quantity_data, $item_data['item_id'], $location_id);
|
||||
|
||||
$csv_data['trans_inventory'] = $row["location_$location_name"];
|
||||
$this->inventory->insert($csv_data, false);
|
||||
$success &= (bool)$this->inventory->insert($csv_data, false);
|
||||
} elseif ($is_update) {
|
||||
return;
|
||||
continue;
|
||||
} else {
|
||||
$item_quantity_data['quantity'] = 0;
|
||||
$this->item_quantity->save_value($item_quantity_data, $item_data['item_id'], $location_id);
|
||||
$success &= $this->item_quantity->save_value($item_quantity_data, $item_data['item_id'], $location_id);
|
||||
|
||||
$csv_data['trans_inventory'] = 0;
|
||||
$this->inventory->insert($csv_data, false);
|
||||
$success &= (bool)$this->inventory->insert($csv_data, false);
|
||||
}
|
||||
}
|
||||
|
||||
return (bool)$success;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1278,8 +1310,9 @@ class Items extends Secure_Controller
|
||||
*
|
||||
* @param array $row
|
||||
* @param array $item_data
|
||||
* @return bool Returns true on success, false on failure
|
||||
*/
|
||||
private function save_tax_data(array $row, array $item_data): void
|
||||
private function save_tax_data(array $row, array $item_data): bool
|
||||
{
|
||||
$items_taxes_data = [];
|
||||
|
||||
@@ -1291,9 +1324,11 @@ class Items extends Secure_Controller
|
||||
$items_taxes_data[] = ['name' => $row['Tax 2 Name'], 'percent' => $row['Tax 2 Percent']];
|
||||
}
|
||||
|
||||
if (isset($items_taxes_data)) {
|
||||
$this->item_taxes->save_value($items_taxes_data, $item_data['item_id']);
|
||||
if (!empty($items_taxes_data)) {
|
||||
return $this->item_taxes->save_value($items_taxes_data, $item_data['item_id']);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -49,6 +49,13 @@ class Login extends BaseController
|
||||
return view('login', $data);
|
||||
}
|
||||
|
||||
if (!$data['is_latest'] || $data['is_new_install']) {
|
||||
set_time_limit(3600);
|
||||
|
||||
$migration->setNamespace('App')->latest();
|
||||
return redirect()->to('login');
|
||||
}
|
||||
|
||||
$rules = ['username' => 'required|login_check[data]'];
|
||||
$messages = [
|
||||
'username' => [
|
||||
@@ -62,13 +69,6 @@ class Login extends BaseController
|
||||
|
||||
return view('login', $data);
|
||||
}
|
||||
|
||||
if (!$data['is_latest']) {
|
||||
set_time_limit(3600);
|
||||
|
||||
$migration->setNamespace('App')->latest();
|
||||
return redirect()->to('login');
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->to('home');
|
||||
@@ -79,18 +79,18 @@ class Login extends BaseController
|
||||
try {
|
||||
$migration = new MY_Migration(config('Migrations'));
|
||||
$migration->migrate_to_ci4();
|
||||
|
||||
|
||||
set_time_limit(3600);
|
||||
$migration->setNamespace('App')->latest();
|
||||
|
||||
|
||||
return $this->response->setJSON([
|
||||
'success' => true,
|
||||
'message' => 'Migration completed successfully'
|
||||
]);
|
||||
|
||||
|
||||
} catch (\Exception $e) {
|
||||
log_message('error', 'Migration failed: ' . $e->getMessage());
|
||||
|
||||
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'message' => 'Migration failed: ' . $e->getMessage()
|
||||
|
||||
@@ -33,7 +33,7 @@ class Messages extends Secure_Controller
|
||||
public function getView(int $person_id = NEW_ENTRY): string
|
||||
{
|
||||
$person = model(Person::class);
|
||||
$info = $person->get_info($person_id);
|
||||
$info = $person->getInfo($person_id);
|
||||
|
||||
foreach (get_object_vars($info) as $property => $value) {
|
||||
$info->$property = $value;
|
||||
|
||||
@@ -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->get_info($row_id));
|
||||
$data_row = get_person_data_row($this->person->getInfo($row_id));
|
||||
|
||||
return $this->response->setJSON($data_row);
|
||||
}
|
||||
|
||||
169
app/Controllers/Plugins.php
Normal file
169
app/Controllers/Plugins.php
Normal file
@@ -0,0 +1,169 @@
|
||||
<?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')]);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ 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;
|
||||
@@ -253,7 +254,7 @@ class Receivings extends Secure_Controller
|
||||
}
|
||||
} else {
|
||||
$stored_employee_id = $receiving_info['employee_id'];
|
||||
$stored_employee = $this->employee->get_info($stored_employee_id);
|
||||
$stored_employee = $this->employee->getInfo($stored_employee_id);
|
||||
$data['employees'][$stored_employee_id] = $stored_employee->first_name . ' ' . $stored_employee->last_name;
|
||||
}
|
||||
|
||||
@@ -342,12 +343,12 @@ class Receivings extends Secure_Controller
|
||||
}
|
||||
|
||||
$employee_id = $this->employee->get_logged_in_employee_info()->person_id;
|
||||
$employee_info = $this->employee->get_info($employee_id);
|
||||
$employee_info = $this->employee->getInfo($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->get_info($supplier_id);
|
||||
$supplier_info = $this->supplier->getInfo($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;
|
||||
@@ -367,6 +368,7 @@ 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();
|
||||
@@ -422,12 +424,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->get_info($receiving_info['employee_id']);
|
||||
$employee_info = $this->employee->getInfo($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->get_info($supplier_id);
|
||||
$supplier_info = $this->supplier->getInfo($supplier_id);
|
||||
$data['supplier'] = $supplier_info->company_name;
|
||||
$data['first_name'] = $supplier_info->first_name;
|
||||
$data['last_name'] = $supplier_info->last_name;
|
||||
@@ -475,7 +477,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->get_info($supplier_id);
|
||||
$supplier_info = $this->supplier->getInfo($supplier_id);
|
||||
$data['supplier'] = $supplier_info->company_name;
|
||||
$data['first_name'] = $supplier_info->first_name;
|
||||
$data['last_name'] = $supplier_info->last_name;
|
||||
|
||||
@@ -1246,13 +1246,15 @@ class Reports extends Secure_Controller
|
||||
public function get_payment_type(): array
|
||||
{
|
||||
return [
|
||||
'all' => lang('Common.none_selected_text'),
|
||||
'cash' => lang('Sales.cash'),
|
||||
'due' => lang('Sales.due'),
|
||||
'check' => lang('Sales.check'),
|
||||
'credit' => lang('Sales.credit'),
|
||||
'debit' => lang('Sales.debit'),
|
||||
'invoices' => lang('Sales.invoice')
|
||||
'all' => lang('Common.none_selected_text'),
|
||||
'cash' => lang('Sales.cash'),
|
||||
'due' => lang('Sales.due'),
|
||||
'check' => lang('Sales.check'),
|
||||
'credit' => lang('Sales.credit'),
|
||||
'debit' => lang('Sales.debit'),
|
||||
'bank_transfer' => lang('Sales.bank_transfer'),
|
||||
'wallet' => lang('Sales.wallet'),
|
||||
'invoices' => lang('Sales.invoice')
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1341,7 +1343,7 @@ class Reports extends Secure_Controller
|
||||
}
|
||||
}
|
||||
|
||||
$customer_info = $this->customer->get_info($customer_id);
|
||||
$customer_info = $this->customer->getInfo($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;
|
||||
@@ -1468,7 +1470,7 @@ class Reports extends Secure_Controller
|
||||
}
|
||||
}
|
||||
|
||||
$employee_info = $this->employee->get_info($employee_id);
|
||||
$employee_info = $this->employee->getInfo($employee_id);
|
||||
// TODO: Duplicated Code
|
||||
$data = [
|
||||
'title' => $employee_info->first_name . ' ' . $employee_info->last_name . ' ' . lang('Reports.report'),
|
||||
@@ -1734,7 +1736,7 @@ class Reports extends Secure_Controller
|
||||
];
|
||||
}
|
||||
|
||||
$supplier_info = $this->supplier->get_info((int) $supplier_id);
|
||||
$supplier_info = $this->supplier->getInfo((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]),
|
||||
|
||||
@@ -20,6 +20,7 @@ use App\Models\Stock_location;
|
||||
use App\Models\Tokens\Token_invoice_count;
|
||||
use App\Models\Tokens\Token_customer;
|
||||
use App\Models\Tokens\Token_invoice_sequence;
|
||||
use CodeIgniter\Events\Events;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Config\Services;
|
||||
use Config\OSPOS;
|
||||
@@ -93,6 +94,8 @@ class Sales extends Secure_Controller
|
||||
'only_check' => lang('Sales.check_filter'),
|
||||
'only_creditcard' => lang('Sales.credit_filter'),
|
||||
'only_debit' => lang('Sales.debit'),
|
||||
'only_bank_transfer'=> lang('Sales.bank_transfer'),
|
||||
'only_wallet' => lang('Sales.wallet'),
|
||||
'only_invoices' => lang('Sales.invoice_filter'),
|
||||
'selected_customer' => lang('Sales.selected_customer')
|
||||
];
|
||||
@@ -156,8 +159,10 @@ class Sales extends Secure_Controller
|
||||
'selected_customer' => false,
|
||||
'only_creditcard' => false,
|
||||
'only_debit' => false,
|
||||
'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->is_valid_receipt($search)
|
||||
'is_valid_receipt' => $this->sale->isValidReceipt($search)
|
||||
];
|
||||
|
||||
// Check if any filter is set in the multiselect dropdown
|
||||
@@ -194,7 +199,7 @@ class Sales extends Secure_Controller
|
||||
? $this->request->getGet('term')
|
||||
: null;
|
||||
|
||||
if ($this->sale_lib->get_mode() == 'return' && $this->sale->is_valid_receipt($receipt)) {
|
||||
if ($this->sale_lib->get_mode() == 'return' && $this->sale->isValidReceipt($receipt)) {
|
||||
// If a valid receipt or invoice was found the search term will be replaced with a receipt number (POS #)
|
||||
$suggestions[] = $receipt;
|
||||
}
|
||||
@@ -229,8 +234,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->get_info($customer_id)->discount;
|
||||
$discount_type = $this->customer->get_info($customer_id)->discount_type;
|
||||
$discount = $this->customer->getInfo($customer_id)->discount;
|
||||
$discount_type = $this->customer->getInfo($customer_id)->discount_type;
|
||||
|
||||
// Apply customer default discount to items that have 0 discount
|
||||
if ($discount != '') {
|
||||
@@ -433,9 +438,9 @@ class Sales extends Secure_Controller
|
||||
}
|
||||
} elseif ($payment_type === lang('Sales.rewards')) {
|
||||
$customer_id = $this->sale_lib->get_customer();
|
||||
$package_id = $this->customer->get_info($customer_id)->package_id;
|
||||
$package_id = $this->customer->getInfo($customer_id)->package_id;
|
||||
if (!empty($package_id)) {
|
||||
$points = $this->customer->get_info($customer_id)->points;
|
||||
$points = $this->customer->getInfo($customer_id)->points;
|
||||
$points = ($points == null ? 0 : $points);
|
||||
|
||||
$payments = $this->sale_lib->get_payments();
|
||||
@@ -507,8 +512,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->get_info($customer_id)->discount;
|
||||
$customer_discount_type = $this->customer->get_info($customer_id)->discount_type;
|
||||
$customer_discount = $this->customer->getInfo($customer_id)->discount;
|
||||
$customer_discount_type = $this->customer->getInfo($customer_id)->discount_type;
|
||||
if ($customer_discount != '') {
|
||||
$discount = $customer_discount;
|
||||
$discount_type = $customer_discount_type;
|
||||
@@ -521,7 +526,7 @@ class Sales extends Secure_Controller
|
||||
$quantity = ($mode == 'return') ? -$quantity : $quantity;
|
||||
$item_location = $this->sale_lib->get_sale_location();
|
||||
|
||||
if ($mode == 'return' && $this->sale->is_valid_receipt($item_id_or_number_or_item_kit_or_receipt)) {
|
||||
if ($mode == 'return' && $this->sale->isValidReceipt($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
|
||||
@@ -699,7 +704,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->get_info($employee_id);
|
||||
$employee_info = $this->employee->getInfo($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']]);
|
||||
@@ -904,6 +909,16 @@ class Sales extends Secure_Controller
|
||||
return $this->_reload($data);
|
||||
} else {
|
||||
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']);
|
||||
|
||||
// Validate receipt template to prevent path traversal
|
||||
$receipt_template = $this->config['receipt_template'] ?? '';
|
||||
if (!Sale_lib::isValidReceiptTemplate($receipt_template)) {
|
||||
$receipt_template = 'receipt_default';
|
||||
}
|
||||
$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);
|
||||
}
|
||||
@@ -937,7 +952,10 @@ class Sales extends Secure_Controller
|
||||
new Token_customer((array)$sale_data)
|
||||
];
|
||||
$text = $this->token_lib->render($text, $tokens);
|
||||
$sale_data['mimetype'] = mime_content_type(FCPATH . 'uploads/' . $this->config['company_logo']);
|
||||
$sale_data['mimetype'] = $this->email_lib->getLogoMimeType();
|
||||
|
||||
// Build img_tag for email views that need it (receipt_email.php)
|
||||
$sale_data['img_tag'] = $this->email_lib->buildLogoImgTag();
|
||||
|
||||
// Generate email attachment: invoice in PDF format
|
||||
$view = Services::renderer();
|
||||
@@ -974,13 +992,7 @@ class Sales extends Secure_Controller
|
||||
|
||||
if (!empty($sale_data['customer_email'])) {
|
||||
$sale_data['barcode'] = $this->barcode_lib->generate_receipt_barcode($sale_data['sale_id']);
|
||||
$sale_data['img_tag'] = '';
|
||||
|
||||
$logo_path = FCPATH . 'uploads/' . $this->config['company_logo'];
|
||||
if (!empty($this->config['company_logo']) && file_exists($logo_path)) {
|
||||
$logo_data = base64_encode(file_get_contents($logo_path));
|
||||
$sale_data['img_tag'] = '<img id="image" src="data:image/png;base64,' . $logo_data . '" alt="company_logo">';
|
||||
}
|
||||
$sale_data['img_tag'] = $this->email_lib->buildLogoImgTag();
|
||||
|
||||
$to = $sale_data['customer_email'];
|
||||
$subject = lang('Sales.receipt');
|
||||
@@ -1009,7 +1021,7 @@ class Sales extends Secure_Controller
|
||||
$customer_info = '';
|
||||
|
||||
if ($customer_id != NEW_ENTRY) {
|
||||
$customer_info = $this->customer->get_info($customer_id);
|
||||
$customer_info = $this->customer->getInfo($customer_id);
|
||||
$data['customer_id'] = $customer_id;
|
||||
|
||||
if (!empty($customer_info->company_name)) {
|
||||
@@ -1032,11 +1044,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->get_info($customer_id)->package_id;
|
||||
$package_id = $this->customer->getInfo($customer_id)->package_id;
|
||||
|
||||
if ($package_id != null) {
|
||||
$package_name = $this->customer_rewards->get_name($package_id);
|
||||
$points = $this->customer->get_info($customer_id)->points;
|
||||
$points = $this->customer->getInfo($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;
|
||||
@@ -1115,7 +1127,7 @@ class Sales extends Secure_Controller
|
||||
|
||||
$data['amount_change'] = $data['amount_due'] * -1;
|
||||
|
||||
$employee_info = $this->employee->get_info($this->sale_lib->get_employee());
|
||||
$employee_info = $this->employee->getInfo($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);
|
||||
|
||||
@@ -1162,6 +1174,13 @@ class Sales extends Secure_Controller
|
||||
}
|
||||
$data['invoice_view'] = $invoice_type;
|
||||
|
||||
// Validate receipt template to prevent path traversal
|
||||
$receipt_template = $this->config['receipt_template'] ?? '';
|
||||
if (!Sale_lib::isValidReceiptTemplate($receipt_template)) {
|
||||
$receipt_template = 'receipt_default';
|
||||
}
|
||||
$data['receipt_template_view'] = $receipt_template;
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
@@ -1256,6 +1275,7 @@ class Sales extends Secure_Controller
|
||||
|
||||
$data['quote_number'] = $this->sale_lib->get_quote_number();
|
||||
$data['work_order_number'] = $this->sale_lib->get_work_order_number();
|
||||
$data['keyboardShortcuts'] = $this->sale_lib->getKeyShortcuts();
|
||||
|
||||
// TODO: the if/else set below should be converted to a switch
|
||||
if ($this->sale_lib->get_mode() == 'sale_invoice') { // TODO: Duplicated code.
|
||||
@@ -1322,7 +1342,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->get_info($sale_info['employee_id']);
|
||||
$employee_info = $this->employee->getInfo($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;
|
||||
@@ -1644,7 +1664,9 @@ class Sales extends Secure_Controller
|
||||
*/
|
||||
public function getSalesKeyboardHelp(): string
|
||||
{
|
||||
return view('sales/help');
|
||||
return view('sales/help', [
|
||||
'keyboardShortcuts' => $this->sale_lib->getKeyShortcuts()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -99,10 +99,10 @@ class Secure_Controller extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $key
|
||||
* @param string $key
|
||||
* @return mixed|void
|
||||
*/
|
||||
public function getConfig($key)
|
||||
public function getConfig(string $key)
|
||||
{
|
||||
if (isset($config[$key])) {
|
||||
return $config[$key];
|
||||
|
||||
@@ -34,7 +34,7 @@ class Suppliers extends Persons
|
||||
*/
|
||||
public function getRow($row_id): ResponseInterface
|
||||
{
|
||||
$data_row = get_supplier_data_row($this->supplier->get_info($row_id));
|
||||
$data_row = get_supplier_data_row($this->supplier->getInfo($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->get_info($supplier_id);
|
||||
$info = $this->supplier->getInfo($supplier_id);
|
||||
foreach (get_object_vars($info) as $property => $value) {
|
||||
$info->$property = $value;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
FROM alpine:3.14
|
||||
MAINTAINER jekkos
|
||||
LABEL maintainer="jekkos"
|
||||
|
||||
ADD database.sql /docker-entrypoint-initdb.d/database.sql
|
||||
VOLUME /docker-entrypoint-initdb.d
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class Migration_Upgrade_To_3_1_1 extends Migration
|
||||
@@ -17,7 +18,37 @@ class Migration_Upgrade_To_3_1_1 extends Migration
|
||||
public function up(): void
|
||||
{
|
||||
helper('migration');
|
||||
execute_script(APPPATH . 'Database/Migrations/sqlscripts/3.0.2_to_3.1.1.sql');
|
||||
|
||||
// MariaDB blocks CONVERT TO CHARACTER SET on tables with FK constraints.
|
||||
// Drop all FKs across affected tables before running the SQL script, recreate after.
|
||||
$fkColumns = [
|
||||
['modules', 'module_id'],
|
||||
['stock_locations', 'location_id'],
|
||||
['permissions', 'permission_id'],
|
||||
['people', 'person_id'],
|
||||
['suppliers', 'supplier_id'],
|
||||
['items', 'item_id'],
|
||||
['item_kits', 'item_kit_id'],
|
||||
['sales', 'sale_id'],
|
||||
['receivings', 'receiving_id'],
|
||||
['employees', 'employee_id'],
|
||||
['customers', 'person_id'],
|
||||
];
|
||||
|
||||
$constraints = [];
|
||||
foreach ($fkColumns as [$table, $column]) {
|
||||
foreach (dropAllForeignKeyConstraints($table, $column) as $c) {
|
||||
$constraints[$c['constraintName']] = $c;
|
||||
}
|
||||
}
|
||||
|
||||
if (!execute_script(APPPATH . 'Database/Migrations/sqlscripts/3.0.2_to_3.1.1.sql')) {
|
||||
throw new DatabaseException('Migration script 3.0.2_to_3.1.1.sql failed. Check logs for details.');
|
||||
}
|
||||
|
||||
$droppedTables = ['sales_suspended', 'sales_suspended_items', 'sales_suspended_items_taxes', 'sales_suspended_payments'];
|
||||
$toRecreate = array_filter($constraints, fn($c) => !in_array($c['tableName'], $droppedTables, true));
|
||||
recreateForeignKeyConstraints(array_values($toRecreate));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
46
app/Database/Migrations/20260506000000_AddShortcutKeys.php
Normal file
46
app/Database/Migrations/20260506000000_AddShortcutKeys.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddShortcutKeys extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$shortcutValues = [
|
||||
['key' => 'key_cancel', 'value' => '27 | ESC'],
|
||||
['key' => 'key_items', 'value' => '49 | ALT + 1'],
|
||||
['key' => 'key_customers', 'value' => '50 | ALT + 2'],
|
||||
['key' => 'key_suspend', 'value' => '51 | ALT + 3'],
|
||||
['key' => 'key_suspended', 'value' => '52 | ALT + 4'],
|
||||
['key' => 'key_amount', 'value' => '53 | ALT + 5'],
|
||||
['key' => 'key_payment', 'value' => '54 | ALT + 6'],
|
||||
['key' => 'key_complete', 'value' => '55 | ALT + 7'],
|
||||
['key' => 'key_finish', 'value' => '56 | ALT + 8'],
|
||||
['key' => 'key_help', 'value' => '57 | ALT + 9'],
|
||||
];
|
||||
|
||||
$this->db->table('app_config')->ignore(true)->insertBatch($shortcutValues);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$shortcutKeys = [
|
||||
'key_cancel',
|
||||
'key_items',
|
||||
'key_customers',
|
||||
'key_suspend',
|
||||
'key_suspended',
|
||||
'key_amount',
|
||||
'key_payment',
|
||||
'key_complete',
|
||||
'key_finish',
|
||||
'key_help',
|
||||
];
|
||||
|
||||
$this->db->table('app_config')
|
||||
->whereIn('key', $shortcutKeys)
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -327,19 +327,6 @@ INSERT INTO `ospos_sales_items` (sale_id, item_id, description, serialnumber, li
|
||||
INSERT INTO `ospos_sales_payments` (sale_id, payment_type, payment_amount) SELECT sale_id, payment_type, payment_amount FROM `ospos_sales_suspended_payments`;
|
||||
INSERT INTO `ospos_sales_items_taxes` (sale_id, item_id, line, name, percent) SELECT sale_id, item_id, line, name, percent FROM `ospos_sales_suspended_items_taxes`;
|
||||
|
||||
ALTER TABLE `ospos_sales_suspended_payments` DROP FOREIGN KEY `ospos_sales_suspended_payments_ibfk_1`;
|
||||
|
||||
ALTER TABLE `ospos_sales_suspended_items_taxes` DROP FOREIGN KEY `ospos_sales_suspended_items_taxes_ibfk_1`;
|
||||
ALTER TABLE `ospos_sales_suspended_items_taxes` DROP FOREIGN KEY `ospos_sales_suspended_items_taxes_ibfk_2`;
|
||||
|
||||
ALTER TABLE `ospos_sales_suspended_items` DROP FOREIGN KEY `ospos_sales_suspended_items_ibfk_1`;
|
||||
ALTER TABLE `ospos_sales_suspended_items` DROP FOREIGN KEY `ospos_sales_suspended_items_ibfk_2`;
|
||||
ALTER TABLE `ospos_sales_suspended_items` DROP FOREIGN KEY `ospos_sales_suspended_items_ibfk_3`;
|
||||
|
||||
ALTER TABLE `ospos_sales_suspended` DROP FOREIGN KEY `ospos_sales_suspended_ibfk_1`;
|
||||
ALTER TABLE `ospos_sales_suspended` DROP FOREIGN KEY `ospos_sales_suspended_ibfk_2`;
|
||||
ALTER TABLE `ospos_sales_suspended` DROP FOREIGN KEY `ospos_sales_suspended_ibfk_3`;
|
||||
|
||||
DROP TABLE `ospos_sales_suspended_payments`, `ospos_sales_suspended_items_taxes`, `ospos_sales_suspended_items`, `ospos_sales_suspended`;
|
||||
|
||||
--
|
||||
|
||||
@@ -140,7 +140,7 @@ CREATE TABLE IF NOT EXISTS `ospos_expense_categories` (
|
||||
`category_name` varchar(255) DEFAULT NULL,
|
||||
`category_description` varchar(255) NOT NULL,
|
||||
`deleted` int(1) NOT NULL DEFAULT '0'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
|
||||
|
||||
|
||||
-- Table structure for table `ospos_expenses`
|
||||
@@ -154,7 +154,7 @@ CREATE TABLE IF NOT EXISTS `ospos_expenses` (
|
||||
`description` varchar(255) NOT NULL,
|
||||
`employee_id` int(10) NOT NULL,
|
||||
`deleted` int(1) NOT NULL DEFAULT '0'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
|
||||
|
||||
|
||||
-- Indexes for table `ospos_expense_categories`
|
||||
|
||||
@@ -75,7 +75,7 @@ CREATE TABLE `ospos_cash_up` (
|
||||
`open_employee_id` int(10) NOT NULL,
|
||||
`close_employee_id` int(10) NOT NULL,
|
||||
`deleted` int(1) NOT NULL DEFAULT '0'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
|
||||
|
||||
-- Indexes for table `ospos_cash_up`
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS `ospos_tax_codes` (
|
||||
`state` varchar(255) NOT NULL DEFAULT '',
|
||||
`deleted` int(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`tax_code_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
|
||||
|
||||
ALTER TABLE `ospos_customers`
|
||||
ADD COLUMN `tax_id` varchar(32) NOT NULL DEFAULT '' AFTER `taxable`,
|
||||
@@ -59,7 +59,7 @@ CREATE TABLE `ospos_sales_taxes` (
|
||||
`rounding_code` tinyint(2) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`sales_taxes_id`),
|
||||
KEY `print_sequence` (`sale_id`,`print_sequence`,`tax_group`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `ospos_tax_jurisdictions` (
|
||||
`jurisdiction_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
@@ -71,7 +71,7 @@ CREATE TABLE IF NOT EXISTS `ospos_tax_jurisdictions` (
|
||||
`cascade_sequence` tinyint(2) NOT NULL DEFAULT 0,
|
||||
`deleted` int(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`jurisdiction_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci AUTO_INCREMENT=1;
|
||||
|
||||
ALTER TABLE `ospos_suppliers`
|
||||
ADD COLUMN `tax_id` varchar(32) DEFAULT NULL AFTER `account_number`;
|
||||
@@ -89,7 +89,7 @@ CREATE TABLE IF NOT EXISTS `ospos_tax_rates` (
|
||||
`tax_rate` decimal(15,4) NOT NULL DEFAULT 0.0000,
|
||||
`tax_rounding_code` tinyint(2) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`tax_rate_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
|
||||
|
||||
-- Add support for sales tax report
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ CREATE TABLE `ospos_sales_payments` (
|
||||
`reference_code` varchar(40) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (`payment_id`),
|
||||
KEY `payment_sale` (`sale_id`, `payment_type`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
|
||||
|
||||
INSERT INTO ospos_sales_payments (sale_id, payment_type, payment_amount, payment_user)
|
||||
SELECT payments.sale_id, payments.payment_type, payments.payment_amount, sales.employee_id
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
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';
|
||||
@@ -22,7 +22,7 @@ function current_language_code(bool $load_system_language = false): string
|
||||
}
|
||||
}
|
||||
|
||||
return $config->language_code ?? DEFAULT_LANGUAGE_CODE;
|
||||
return $config['language_code'] ?? DEFAULT_LANGUAGE_CODE;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,7 +43,7 @@ function current_language(bool $load_system_language = false): string
|
||||
}
|
||||
}
|
||||
|
||||
return $config->language ?? DEFAULT_LANGUAGE_CODE;
|
||||
return $config['language'] ?? DEFAULT_LANGUAGE;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -272,6 +272,9 @@ function get_payment_options(): array
|
||||
$payments[lang('Sales.upi')] = lang('Sales.upi');
|
||||
}
|
||||
|
||||
$payments[lang('Sales.bank_transfer')] = lang('Sales.bank_transfer');
|
||||
$payments[lang('Sales.wallet')] = lang('Sales.wallet');
|
||||
|
||||
return $payments;
|
||||
}
|
||||
|
||||
|
||||
@@ -172,6 +172,7 @@ function dropAllForeignKeyConstraints(string $table, string $column): array {
|
||||
WHERE kcu.TABLE_SCHEMA = DATABASE()
|
||||
AND ((kcu.REFERENCED_TABLE_NAME = '" . $db->getPrefix() . "$table' AND kcu.REFERENCED_COLUMN_NAME = '$column')
|
||||
OR (kcu.TABLE_NAME = '" . $db->getPrefix() . "$table' AND kcu.COLUMN_NAME = '$column'))
|
||||
AND rc.CONSTRAINT_NAME IS NOT NULL
|
||||
");
|
||||
|
||||
$deletedConstraints = [];
|
||||
|
||||
19
app/Helpers/plugin_helper.php
Normal file
19
app/Helpers/plugin_helper.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?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}"));
|
||||
}
|
||||
}
|
||||
@@ -933,6 +933,50 @@ 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.
|
||||
*
|
||||
|
||||
@@ -166,8 +166,6 @@ return [
|
||||
"info" => "معلومات",
|
||||
"info_configuration" => "معلومات الشركة",
|
||||
"input_groups" => "مجموعات الإدخال",
|
||||
"integrations" => "التكامل",
|
||||
"integrations_configuration" => "تكامل",
|
||||
"invoice" => "الفاتورة",
|
||||
"invoice_configuration" => "إعدادات طباعة الفاتورة",
|
||||
"invoice_default_comments" => "التعليق الافتراضي على الفاتورة",
|
||||
@@ -198,13 +196,6 @@ 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" => "الرسائل النصية المحفوظة",
|
||||
|
||||
@@ -28,16 +28,6 @@ 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" => "عميل جديد",
|
||||
|
||||
@@ -32,6 +32,7 @@ return [
|
||||
"migrate_desc" => "تحديث قاعدة البيانات.",
|
||||
"office" => "المكتب",
|
||||
"office_desc" => "اظهار الائحة المكتبية.",
|
||||
'plugins' => 'الإضافات',
|
||||
"receivings" => "استلام الأصناف",
|
||||
"receivings_desc" => "معالجة أوامر الشراء و استلام الأصناف.",
|
||||
"reports" => "التقارير",
|
||||
|
||||
27
app/Language/ar-EG/Plugins.php
Normal file
27
app/Language/ar-EG/Plugins.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?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' => 'الإصدار',
|
||||
];
|
||||
@@ -41,7 +41,6 @@ return [
|
||||
"customer_discount" => "الخصم",
|
||||
"customer_email" => "البريد الإلكترونى",
|
||||
"customer_location" => "المكان",
|
||||
"customer_mailchimp_status" => "حالة بريد ميل تشيمب",
|
||||
"customer_optional" => "(مطلوب للدفعات المستحقة)",
|
||||
"customer_required" => "(اجباري)",
|
||||
"customer_total" => "المجموع",
|
||||
|
||||
@@ -166,8 +166,6 @@ return [
|
||||
"info" => "معلومات",
|
||||
"info_configuration" => "معلومات الشركة",
|
||||
"input_groups" => "مجموعات الإدخال",
|
||||
"integrations" => "التكامل",
|
||||
"integrations_configuration" => "تكامل",
|
||||
"invoice" => "الفاتورة",
|
||||
"invoice_configuration" => "إعدادات طباعة الفاتورة",
|
||||
"invoice_default_comments" => "التعليق الافتراضي على الفاتورة",
|
||||
@@ -198,13 +196,6 @@ 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" => "الرسائل النصية المحفوظة",
|
||||
|
||||
@@ -28,16 +28,6 @@ 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" => "عميل جديد",
|
||||
|
||||
@@ -32,6 +32,7 @@ return [
|
||||
"migrate_desc" => "تحديث قاعدة البيانات.",
|
||||
"office" => "المكتب",
|
||||
"office_desc" => "اظهار الائحة المكتبية.",
|
||||
'plugins' => 'الإضافات',
|
||||
"receivings" => "استلام الأصناف",
|
||||
"receivings_desc" => "معالجة أوامر الشراء و استلام الأصناف.",
|
||||
"reports" => "التقارير",
|
||||
|
||||
27
app/Language/ar-LB/Plugins.php
Normal file
27
app/Language/ar-LB/Plugins.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?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' => 'الإصدار',
|
||||
];
|
||||
@@ -41,7 +41,6 @@ return [
|
||||
"customer_discount" => "الخصم",
|
||||
"customer_email" => "البريد الإلكترونى",
|
||||
"customer_location" => "المكان",
|
||||
"customer_mailchimp_status" => "حالة بريد ميل تشيمب",
|
||||
"customer_optional" => "(مطلوب للدفعات المستحقة)",
|
||||
"customer_required" => "(اجباري)",
|
||||
"customer_total" => "المجموع",
|
||||
|
||||
@@ -166,8 +166,6 @@ 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",
|
||||
@@ -198,13 +196,6 @@ 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ı",
|
||||
|
||||
@@ -28,16 +28,6 @@ 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" => "Açıq məktub",
|
||||
"mailchimp_activity_total" => "Məktub göndərildi",
|
||||
"mailchimp_activity_unopen" => "Açı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",
|
||||
|
||||
@@ -32,6 +32,7 @@ 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",
|
||||
|
||||
27
app/Language/az/Plugins.php
Normal file
27
app/Language/az/Plugins.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?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',
|
||||
];
|
||||
@@ -41,7 +41,6 @@ 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",
|
||||
|
||||
@@ -166,8 +166,6 @@ 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",
|
||||
@@ -198,13 +196,6 @@ 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",
|
||||
|
||||
@@ -28,16 +28,6 @@ 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" => "Нов клиент",
|
||||
|
||||
@@ -32,6 +32,7 @@ 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",
|
||||
|
||||
27
app/Language/bg/Plugins.php
Normal file
27
app/Language/bg/Plugins.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?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' => 'Версия',
|
||||
];
|
||||
@@ -41,7 +41,7 @@ return [
|
||||
"customer_discount" => "Намаление",
|
||||
"customer_email" => "Електронна поща",
|
||||
"customer_location" => "Местоположение",
|
||||
"customer_mailchimp_status" => "Състояние на Mailchimp",
|
||||
"mailchimp_customer_status" => "Състояние на Mailchimp",
|
||||
"customer_optional" => "(Незадължително)",
|
||||
"customer_required" => "(Задължително)",
|
||||
"customer_total" => "Обща сума",
|
||||
|
||||
@@ -166,8 +166,6 @@ 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",
|
||||
@@ -198,13 +196,6 @@ 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",
|
||||
|
||||
@@ -28,16 +28,6 @@ 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",
|
||||
|
||||
@@ -32,6 +32,7 @@ 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",
|
||||
|
||||
27
app/Language/bs/Plugins.php
Normal file
27
app/Language/bs/Plugins.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?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',
|
||||
];
|
||||
@@ -41,7 +41,6 @@ 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",
|
||||
|
||||
@@ -166,8 +166,8 @@ return [
|
||||
'info' => "زانیاری",
|
||||
'info_configuration' => "زانیاری فڕۆشتگا",
|
||||
'input_groups' => "گروپەکانی زانیارییە پێدراوەکان",
|
||||
'integrations' => "یەکگرتنەکان",
|
||||
'integrations_configuration' => "یەکگرتنەکانی لایەنی سێیەم",
|
||||
'plugins' => "یەکگرتنەکان",
|
||||
'plugins_configuration' => "یەکگرتنەکانی لایەنی سێیەم",
|
||||
'invoice' => "فاکتۆرە",
|
||||
'invoice_configuration' => "ڕێکخستنەکانی چاپی فاکتورە",
|
||||
'invoice_default_comments' => "سەرنجەکانی فاکتۆرەی بنەڕەتیی",
|
||||
@@ -198,13 +198,6 @@ 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' => "دەقی نامەی پاشەکەوتکراو",
|
||||
|
||||
@@ -28,16 +28,6 @@ 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' => "کڕیاری نوێ",
|
||||
|
||||
@@ -32,6 +32,7 @@ return [
|
||||
'migrate_desc' => "داتابەیسی OSPOS نوێ بکەرەوە.",
|
||||
'office' => "ئۆفیس",
|
||||
'office_desc' => "لیستی مۆدۆلی پێڕستی ئۆفیس نیشان بدە.",
|
||||
'plugins' => 'پڵەگینەکان',
|
||||
'receivings' => "وەرگرتنەکان",
|
||||
'receivings_desc' => "پرۆسەی داواکاری کڕین.",
|
||||
'reports' => "ڕاپۆرتەکان",
|
||||
|
||||
27
app/Language/ckb/Plugins.php
Normal file
27
app/Language/ckb/Plugins.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?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' => 'وەشان',
|
||||
];
|
||||
@@ -41,7 +41,6 @@ return [
|
||||
'customer_discount' => "داشکاندن",
|
||||
'customer_email' => "ئیمەیڵ",
|
||||
'customer_location' => "ناونیشان",
|
||||
'customer_mailchimp_status' => "دۆخی بەکارهێنان مایلچیمپ",
|
||||
'customer_optional' => "(پێویستە بۆئەو پارانەی دەبێت بدرێت)",
|
||||
'customer_required' => "(پێویستە)",
|
||||
'customer_total' => "کۆی گشتی",
|
||||
|
||||
@@ -166,8 +166,6 @@ return [
|
||||
"info" => "",
|
||||
"info_configuration" => "",
|
||||
"input_groups" => "",
|
||||
"integrations" => "",
|
||||
"integrations_configuration" => "",
|
||||
"invoice" => "",
|
||||
"invoice_configuration" => "",
|
||||
"invoice_default_comments" => "",
|
||||
@@ -198,13 +196,6 @@ 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" => "",
|
||||
|
||||
@@ -28,16 +28,6 @@ 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" => "",
|
||||
|
||||
@@ -32,6 +32,7 @@ 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",
|
||||
|
||||
27
app/Language/cs/Plugins.php
Normal file
27
app/Language/cs/Plugins.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?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',
|
||||
];
|
||||
@@ -41,7 +41,6 @@ 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",
|
||||
|
||||
@@ -166,8 +166,6 @@ 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",
|
||||
@@ -198,13 +196,6 @@ 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",
|
||||
|
||||
@@ -28,16 +28,6 @@ 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",
|
||||
|
||||
@@ -32,6 +32,7 @@ return [
|
||||
"migrate_desc" => "",
|
||||
"office" => "",
|
||||
"office_desc" => "",
|
||||
'plugins' => 'Plugins',
|
||||
"receivings" => "",
|
||||
"receivings_desc" => "",
|
||||
"reports" => "",
|
||||
|
||||
27
app/Language/da/Plugins.php
Normal file
27
app/Language/da/Plugins.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?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',
|
||||
];
|
||||
@@ -41,7 +41,6 @@ return [
|
||||
"customer_discount" => "Rabat",
|
||||
"customer_email" => "",
|
||||
"customer_location" => "",
|
||||
"customer_mailchimp_status" => "",
|
||||
"customer_optional" => "",
|
||||
"customer_required" => "",
|
||||
"customer_total" => "",
|
||||
|
||||
@@ -166,8 +166,6 @@ return [
|
||||
"info" => "Instellungen",
|
||||
"info_configuration" => "Instellungen",
|
||||
"input_groups" => "",
|
||||
"integrations" => "",
|
||||
"integrations_configuration" => "",
|
||||
"invoice" => "Rechnungs",
|
||||
"invoice_configuration" => "Druckereinstellungen",
|
||||
"invoice_default_comments" => "Rechnungskommentar",
|
||||
@@ -198,13 +196,6 @@ 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",
|
||||
|
||||
@@ -28,16 +28,6 @@ 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",
|
||||
|
||||
@@ -32,6 +32,7 @@ return [
|
||||
"migrate_desc" => "",
|
||||
"office" => "",
|
||||
"office_desc" => "",
|
||||
'plugins' => 'Plugins',
|
||||
"receivings" => "Eingänge",
|
||||
"receivings_desc" => "Hinzufügen, Ändern, Löschen und Suchen",
|
||||
"reports" => "Berichte",
|
||||
|
||||
27
app/Language/de-CH/Plugins.php
Normal file
27
app/Language/de-CH/Plugins.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?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',
|
||||
];
|
||||
@@ -41,7 +41,6 @@ return [
|
||||
"customer_discount" => "Discount",
|
||||
"customer_email" => "Customer Email",
|
||||
"customer_location" => "Customer Location",
|
||||
"customer_mailchimp_status" => "",
|
||||
"customer_optional" => "",
|
||||
"customer_required" => "",
|
||||
"customer_total" => "Total",
|
||||
|
||||
@@ -166,8 +166,6 @@ return [
|
||||
"info" => "Informationen",
|
||||
"info_configuration" => "Generelle Einstellungen",
|
||||
"input_groups" => "",
|
||||
"integrations" => "Integrationen",
|
||||
"integrations_configuration" => "Drittanbieter Integrationen",
|
||||
"invoice" => "Rechnungs",
|
||||
"invoice_configuration" => "Druckereinstellungen",
|
||||
"invoice_default_comments" => "Rechnungskommentar",
|
||||
@@ -198,14 +196,7 @@ return [
|
||||
"location_info" => "Lagerort-Information",
|
||||
"login_form" => "",
|
||||
"logout" => "Wollen Sie vor dem Beenden eine Sicherung erstellen? Klicke [OK] für Sicherung.",
|
||||
"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" => "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.",
|
||||
|
||||
@@ -28,16 +28,6 @@ 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",
|
||||
|
||||
@@ -32,6 +32,7 @@ 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",
|
||||
|
||||
27
app/Language/de-DE/Plugins.php
Normal file
27
app/Language/de-DE/Plugins.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?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',
|
||||
];
|
||||
@@ -41,7 +41,6 @@ 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",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user