mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-05-25 08:44:42 -04:00
Compare commits
31 Commits
refactor-4
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5450404cb2 | ||
|
|
b7384296c1 | ||
|
|
b0dddc22a3 | ||
|
|
8d6b166673 | ||
|
|
093ec7fb13 | ||
|
|
9c89a2e2cb | ||
|
|
2f51c4ef52 | ||
|
|
def0c27a0e | ||
|
|
90c981b6b7 | ||
|
|
6ff28d8a4d | ||
|
|
70fb347fc4 | ||
|
|
2f5c0130f4 | ||
|
|
fdd6a408ec | ||
|
|
ef91e6a9df | ||
|
|
144e73eba6 | ||
|
|
42ba39d290 | ||
|
|
81213f0434 | ||
|
|
7edefe8ee1 | ||
|
|
68e14191f9 | ||
|
|
a381c3ca54 | ||
|
|
058e12244e | ||
|
|
f1c6fe2981 | ||
|
|
ff7a8d2e88 | ||
|
|
e602eddb47 | ||
|
|
0a313aa09d | ||
|
|
12e3c7e31f | ||
|
|
de62e9f3bd | ||
|
|
97ca738b2d | ||
|
|
c714dd6f68 | ||
|
|
b6f28da058 | ||
|
|
165c3351eb |
@@ -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
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -106,7 +106,7 @@ NOTE: If you're running non-release code, please make sure you always run the la
|
||||
|
||||
## 🏃 Keep the Machine Running
|
||||
|
||||
If you like our project, please consider buying us a coffee through the button below so we can keep adding features.
|
||||
If you like our project, please consider buying us a coffee through the button below so we can keep adding features. Please star the project if you like it!
|
||||
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=MUN6AEG7NY6H8)\
|
||||
Or refer to the [FUNDING.yml](.github/FUNDING.yml) file.
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
@@ -82,7 +82,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 +221,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 +232,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();
|
||||
@@ -398,6 +401,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 +411,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 +433,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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -911,7 +924,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 +951,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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -154,8 +154,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);
|
||||
|
||||
@@ -1040,14 +1055,20 @@ 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;
|
||||
}
|
||||
|
||||
@@ -1237,13 +1258,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 +1280,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 +1303,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 +1317,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()
|
||||
|
||||
@@ -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')
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -93,6 +93,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 +158,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 +198,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;
|
||||
}
|
||||
@@ -521,7 +525,7 @@ class Sales extends Secure_Controller
|
||||
$quantity = ($mode == 'return') ? -$quantity : $quantity;
|
||||
$item_location = $this->sale_lib->get_sale_location();
|
||||
|
||||
if ($mode == 'return' && $this->sale->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
|
||||
@@ -904,6 +908,14 @@ 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;
|
||||
|
||||
$this->sale_lib->clear_all();
|
||||
return view('sales/receipt', $data);
|
||||
}
|
||||
@@ -937,7 +949,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 +989,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');
|
||||
@@ -1162,6 +1171,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 +1272,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.
|
||||
@@ -1644,7 +1661,9 @@ class Sales extends Secure_Controller
|
||||
*/
|
||||
public function getSalesKeyboardHelp(): string
|
||||
{
|
||||
return view('sales/help');
|
||||
return view('sales/help', [
|
||||
'keyboardShortcuts' => $this->sale_lib->getKeyShortcuts()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -9,6 +9,7 @@ return [
|
||||
"amount_due" => "Amount Due",
|
||||
"amount_tendered" => "Amount Tendered",
|
||||
"authorized_signature" => "Authorised Signature",
|
||||
"bank_transfer" => "Bank Transfer",
|
||||
"cancel_sale" => "Cancel",
|
||||
"cash" => "Cash",
|
||||
"cash_1" => "",
|
||||
@@ -223,6 +224,7 @@ return [
|
||||
"update" => "Update",
|
||||
"upi" => "UPI",
|
||||
"visa" => "",
|
||||
"wallet" => "Wallet",
|
||||
"wholesale" => "",
|
||||
"work_order" => "Work Order",
|
||||
"work_order_number" => "Work Order Number",
|
||||
|
||||
@@ -302,6 +302,10 @@ return [
|
||||
"suggestions_layout" => "Search Suggestions Layout",
|
||||
"suggestions_second_column" => "Column 2",
|
||||
"suggestions_third_column" => "Column 3",
|
||||
"shortcuts" => "Shortcuts",
|
||||
"shortcuts_configuration" => "Sales Keyboard Shortcut Configuration",
|
||||
"shortcuts_duplicate_bindings" => "Shortcut bindings must be unique.",
|
||||
"shortcuts_save_error" => "Unable to save shortcut settings.",
|
||||
"system_conf" => "Setup & Conf",
|
||||
"system_info" => "System Info",
|
||||
"table" => "Table",
|
||||
|
||||
@@ -9,6 +9,7 @@ return [
|
||||
"amount_due" => "Amount Due",
|
||||
"amount_tendered" => "Amount Tendered",
|
||||
"authorized_signature" => "Authorized Signature",
|
||||
"bank_transfer" => "Bank Transfer",
|
||||
"cancel_sale" => "Cancel",
|
||||
"cash" => "Cash",
|
||||
"cash_1" => "",
|
||||
@@ -223,6 +224,7 @@ return [
|
||||
"update" => "Update",
|
||||
"upi" => "UPI",
|
||||
"visa" => "",
|
||||
"wallet" => "Wallet",
|
||||
"wholesale" => "",
|
||||
"work_order" => "Work Order",
|
||||
"work_order_number" => "Work Order Number",
|
||||
|
||||
@@ -26,7 +26,7 @@ return [
|
||||
"cost_price_required" => "Precio al Por Mayor es un campo requerido.",
|
||||
"count" => "Actualizar Inventario",
|
||||
"csv_import_failed" => "Falló la importación de Hoja de Cálculo",
|
||||
"csv_import_invalid_location" => "Ubicación(es) de stock inválida(s) encontrada(s): {0}. Solo ubicaciones de stock válidas son permitidas.",
|
||||
"csv_import_invalid_location" => "Se encontraron ubicaciones de stock no válidas: {0}. Solo se permiten ubicaciones de stock válidas.",
|
||||
"csv_import_nodata_wrongformat" => "El archivo subido no tiene datos o el formato es incorrecto.",
|
||||
"csv_import_partially_failed" => "Hubo {0} falla(s) en la importación de producto(s) en la(s) línea(s): {1}. Ninguna fila ha sido importada.",
|
||||
"csv_import_success" => "Se importaron los articulos exitosamente.",
|
||||
|
||||
@@ -9,6 +9,7 @@ return [
|
||||
"amount_due" => "Monto Adeudado",
|
||||
"amount_tendered" => "Cantidad Recibida",
|
||||
"authorized_signature" => "Firma Autorizada",
|
||||
"bank_transfer" => "Transferencia Bancaria",
|
||||
"cancel_sale" => "Cancelar Venta",
|
||||
"cash" => "Efectivo",
|
||||
"cash_1" => "1",
|
||||
@@ -222,6 +223,7 @@ return [
|
||||
"update" => "Editar",
|
||||
"upi" => "PIN UPI",
|
||||
"visa" => "Tarjeta Visa",
|
||||
"wallet" => "Monedero",
|
||||
"wholesale" => "Precio al por mayor",
|
||||
"work_order" => "Orden trabajo",
|
||||
"work_order_number" => "Numero Orden Trabajo",
|
||||
|
||||
@@ -9,6 +9,7 @@ return [
|
||||
"amount_due" => "Monto de adeudo",
|
||||
"amount_tendered" => "Cantidad Recibida",
|
||||
"authorized_signature" => "Firma Autorizada",
|
||||
"bank_transfer" => "Transferencia Bancaria",
|
||||
"cancel_sale" => "Cancelar",
|
||||
"cash" => "Efectivo",
|
||||
"cash_1" => "",
|
||||
@@ -222,6 +223,7 @@ return [
|
||||
"update" => "Actualizar",
|
||||
"upi" => "UPI",
|
||||
"visa" => "",
|
||||
"wallet" => "Monedero",
|
||||
"wholesale" => "",
|
||||
"work_order" => "Orden de trabajo",
|
||||
"work_order_number" => "Número de orden de trabajo",
|
||||
|
||||
@@ -9,6 +9,7 @@ return [
|
||||
"amount_due" => "Montant à Payer",
|
||||
"amount_tendered" => "Montant Présenté",
|
||||
"authorized_signature" => "Signature autorisée",
|
||||
"bank_transfer" => "Virement Bancaire",
|
||||
"cancel_sale" => "Annuler la Vente",
|
||||
"cash" => "Espèce",
|
||||
"cash_1" => "",
|
||||
@@ -222,6 +223,7 @@ return [
|
||||
"update" => "Éditer",
|
||||
"upi" => "UPI",
|
||||
"visa" => "",
|
||||
"wallet" => "Portefeuille",
|
||||
"wholesale" => "",
|
||||
"work_order" => "Commande de travail",
|
||||
"work_order_number" => "Numéro de commande",
|
||||
|
||||
@@ -38,7 +38,7 @@ return [
|
||||
"february" => "",
|
||||
"march" => "",
|
||||
"april" => "",
|
||||
"mayl" => "",
|
||||
"may" => "",
|
||||
"june" => "",
|
||||
"july" => "",
|
||||
"august" => "",
|
||||
@@ -46,4 +46,4 @@ return [
|
||||
"october" => "",
|
||||
"november" => "",
|
||||
"december" => "",
|
||||
];
|
||||
];
|
||||
|
||||
@@ -38,7 +38,7 @@ return [
|
||||
"february" => "",
|
||||
"march" => "",
|
||||
"april" => "",
|
||||
"mayl" => "",
|
||||
"may" => "",
|
||||
"june" => "",
|
||||
"july" => "",
|
||||
"august" => "",
|
||||
@@ -46,4 +46,4 @@ return [
|
||||
"october" => "",
|
||||
"november" => "",
|
||||
"december" => "",
|
||||
];
|
||||
];
|
||||
|
||||
@@ -38,7 +38,7 @@ return [
|
||||
"february" => "ഫെബ്രുവരി",
|
||||
"march" => "മാർച്ച്",
|
||||
"april" => "ഏപ്രിൽ",
|
||||
"mayl" => "മേയ്",
|
||||
"may" => "മേയ്",
|
||||
"june" => "ജൂൺ",
|
||||
"july" => "ജൂലൈ",
|
||||
"august" => "ആഗസ്റ്റ്",
|
||||
@@ -46,4 +46,4 @@ return [
|
||||
"october" => "ഒക്ടോബർ",
|
||||
"november" => "നവംബർ",
|
||||
"december" => "ഡിസംബർ",
|
||||
];
|
||||
];
|
||||
|
||||
@@ -38,7 +38,7 @@ return [
|
||||
"february" => "Februar",
|
||||
"march" => "Mars",
|
||||
"april" => "April",
|
||||
"mayl" => "Mai",
|
||||
"may" => "Mai",
|
||||
"june" => "Juni",
|
||||
"july" => "Juli",
|
||||
"august" => "August",
|
||||
@@ -46,4 +46,4 @@ return [
|
||||
"october" => "Oktober",
|
||||
"november" => "November",
|
||||
"december" => "Desember",
|
||||
];
|
||||
];
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"all" => "ทั้งหมด",
|
||||
"columns" => "คอลัมน์",
|
||||
"hide_show_pagination" => "ซ่อน/แสดง รายการหน้า",
|
||||
"loading" => "กำลังดำเนินการ รอสักครู่",
|
||||
"page_from_to" => "แสดง {0} ถึง {1} จาก {2} รายการ",
|
||||
"refresh" => "Refresh ข้อมูล",
|
||||
"rows_per_page" => "{0} รายการ/หน้า",
|
||||
"toggle" => "ซ่อน/แสดง",
|
||||
'all' => "ทั้งหมด",
|
||||
'columns' => "คอลัมน์",
|
||||
'hide_show_pagination' => "ซ่อน/แสดง รายการหน้า",
|
||||
'loading' => "กำลังดำเนินการ รอสักครู่ ...",
|
||||
'page_from_to' => "แสดง {0} ถึง {1} จาก {2} รายการ",
|
||||
'refresh' => "Refresh ข้อมูล",
|
||||
'rows_per_page' => "{0} รายการ/หน้า",
|
||||
'toggle' => "ซ่อน/แสดง",
|
||||
];
|
||||
|
||||
@@ -9,7 +9,9 @@ return [
|
||||
"login" => "ลงชื่อเข้าใช้",
|
||||
"logout" => "ออกจากระบบ",
|
||||
"migration_needed" => "การย้ายฐานข้อมูลไปยัง {0} จะเริ่มต้นหลังจากเข้าสู่ระบบ",
|
||||
"migration_required" => "",
|
||||
"migration_required" => "จําเป็นต้องมีการปรับปรุงฐานข้อมูล",
|
||||
"migration_auth_message" => "ผู้ดูแลระบบจำเป็นต้องมีสิทธิ์ในการปรับปรุงฐานข้อมูลเวอร์ชั่น {0} กรุณาเข้าระบบเพื่อดำเนินการต่อ",
|
||||
"migration_complete_redirect" => "ทำการปรับปรุงฐานข้อมูลเรียบร้อย กำลังดำเนินการไปหน้าเข้าสู่ระบบ ...",
|
||||
"migration_auth_message" => "",
|
||||
"migration_initializing" => "",
|
||||
"migration_running" => "",
|
||||
@@ -17,7 +19,6 @@ return [
|
||||
"migration_complete_login" => "",
|
||||
"migration_failed" => "",
|
||||
"migration_error_connection" => "",
|
||||
"migration_complete_redirect" => "",
|
||||
"password" => "รหัสผ่าน",
|
||||
"required_username" => "จำเป็นต้องระบุชื่อผู้ใช้งาน",
|
||||
"username" => "ชื่อผู้ใช้",
|
||||
|
||||
@@ -1,232 +1,232 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"customers_available_points" => "คะแนนที่มี",
|
||||
"rewards_package" => "คะแนนสะสม",
|
||||
"rewards_remaining_balance" => "คะแนนสะสมคงเหลือ ",
|
||||
"account_number" => "บัญชี #",
|
||||
"add_payment" => "เพิ่มบิล",
|
||||
"amount_due" => "ยอดค้างชำระ",
|
||||
"amount_tendered" => "ชำระเข้ามา",
|
||||
"authorized_signature" => "ลายเซ็นผู้มีอำนาจ",
|
||||
"cancel_sale" => "ยกเลิกการขาย",
|
||||
"cash" => "เงินสด",
|
||||
"cash_1" => "",
|
||||
"cash_2" => "",
|
||||
"cash_3" => "",
|
||||
"cash_4" => "",
|
||||
"cash_adjustment" => "การปรับเงินสดขาย",
|
||||
"cash_deposit" => "ฝากเงินสด",
|
||||
"cash_filter" => "เงินสด",
|
||||
"change_due" => "เงินทอน",
|
||||
"change_price" => "เปลี่ยนราคาขาย",
|
||||
"check" => "โอนเงิน/พร้อมเพย์/เช็ค",
|
||||
"check_balance" => "เช็คยอดคงเหลือ",
|
||||
"check_filter" => "ตรวจสอบ",
|
||||
"close" => "",
|
||||
"comment" => "หมายเหตุ",
|
||||
"comments" => "หมายเหตุ",
|
||||
"company_name" => "",
|
||||
"complete" => "",
|
||||
"complete_sale" => "จบการขาย",
|
||||
"confirm_cancel_sale" => "แน่ใจหรือไม่ที่จะล้างการขายนี้? ทุกรายการจะถูกลบทั้งหมด",
|
||||
"confirm_delete" => "โปรดยืนยันการลบรายการขายที่เลือกไว้ ?",
|
||||
"confirm_restore" => "คุณแน่ใจหรือไม่ว่าต้องการยกเลิกการขายที่เลือกไว้?",
|
||||
"credit" => "เครดิตการ์ด",
|
||||
"credit_deposit" => "เงินฝากเครดิต",
|
||||
"credit_filter" => "บัตรเครติด",
|
||||
"current_table" => "",
|
||||
"customer" => "ลูกค้า",
|
||||
"customer_address" => "Customer Address",
|
||||
"customer_discount" => "ส่วนลด",
|
||||
"customer_email" => "Customer Email",
|
||||
"customer_location" => "Customer Location",
|
||||
"customer_mailchimp_status" => "สถานะของระบบส่งเมล์เมล์ชิม",
|
||||
"customer_optional" => "(ต้องระบุวันที่ชำระเงิน)",
|
||||
"customer_required" => "(ต้องระบุ)",
|
||||
"customer_total" => "Total",
|
||||
"customer_total_spent" => "",
|
||||
"daily_sales" => "",
|
||||
"date" => "วันที่ขาย",
|
||||
"date_range" => "ระหว่างวันที่",
|
||||
"date_required" => "กรุณากรอกวันที่ให้ถูกต้อง",
|
||||
"date_type" => "กรุณากรอกข้อมูลในช่องวันที่",
|
||||
"debit" => "บัตรประชารัฐ/เดบิตการ์ด",
|
||||
"debit_filter" => "",
|
||||
"delete" => "อนุญาตให้ลบ",
|
||||
"delete_confirmation" => "แน่ใจหรือไม่ที่จะลบรายการขายนี้, ลบแล้วไม่สามารถเรียกกลับคืนใด้",
|
||||
"delete_entire_sale" => "ลบการขายทั้งหมด",
|
||||
"delete_successful" => "คุณลบการขายสำเร็จ",
|
||||
"delete_unsuccessful" => "คุณลบการขายไม่สำเร็จ",
|
||||
"description_abbrv" => "รายละเอียด",
|
||||
"discard" => "ยกเลิก",
|
||||
"discard_quote" => "",
|
||||
"discount" => "ส่วนลด %",
|
||||
"discount_included" => "% ส่วนลด",
|
||||
"discount_short" => "%",
|
||||
"due" => "วันครบกำหนด",
|
||||
"due_filter" => "วันที่ครบกำหนด",
|
||||
"edit" => "แก้ไข",
|
||||
"edit_item" => "แก้ไขสินค้า",
|
||||
"edit_sale" => "แก้ไขการขาย",
|
||||
"email_receipt" => "อีเมลบิล",
|
||||
"employee" => "พนักงาน",
|
||||
"entry" => "การนำเข้า",
|
||||
"error_editing_item" => "แก้ไขสินค้าล้มเหลว",
|
||||
"negative_price_invalid" => "",
|
||||
"negative_quantity_invalid" => "",
|
||||
"negative_discount_invalid" => "",
|
||||
"discount_percent_exceeds_100" => "",
|
||||
"discount_exceeds_item_total" => "",
|
||||
"negative_total_invalid" => "",
|
||||
"find_or_scan_item" => "ค้นหาสินค้า",
|
||||
"find_or_scan_item_or_receipt" => "ค้นหา หรือ แสกนรายการ หรือ ใบเสร็จ",
|
||||
"giftcard" => "บัตรของขวัญ",
|
||||
"giftcard_balance" => "ยอดคงเหลือบัตรของขวัญ",
|
||||
"giftcard_filter" => "",
|
||||
"giftcard_number" => "เลขที่บัตรของขวัญ",
|
||||
"group_by_category" => "กลุ่มตามหมวดหมู่",
|
||||
"group_by_type" => "กลุ่มตามประเภท",
|
||||
"hsn" => "HSN",
|
||||
"id" => "เลขที่ขาย",
|
||||
"include_prices" => "รวมในราคา?",
|
||||
"invoice" => "ใบแจ้งหนี้",
|
||||
"invoice_confirm" => "ใบแจ้งหนี้นี้จะถูกส่งไปที่",
|
||||
"invoice_enable" => "เลขที่ใบแจ้งหนี้",
|
||||
"invoice_filter" => "ใบแจ้งหนี้",
|
||||
"invoice_no_email" => "ลูกค้ารายนี้ไม่มีที่อยู่อีเมล",
|
||||
"invoice_number" => "เลขใบแจ้งหนี้ #",
|
||||
"invoice_number_duplicate" => "ใบแจ้งหนี้หมายเลข {0} จะต้องไม่ซ้ำกัน",
|
||||
"invoice_sent" => "ส่งใบแจ้งหนี้ไปที่",
|
||||
"invoice_total" => "ยอดรวมในใบแจ้งหนี้",
|
||||
"invoice_type_custom_invoice" => "ใบแจ้งหนี้ที่กำหนดเอง (custom_invoice.php)",
|
||||
"invoice_type_custom_tax_invoice" => "ใบกำกับภาษีที่กำหนดเอง (custom_tax_invoice.php)",
|
||||
"invoice_type_invoice" => "ใบแจ้งหนี้ (invoice.php)",
|
||||
"invoice_type_tax_invoice" => "ใบกำกับภาษี (tax_invoice.php)",
|
||||
"invoice_unsent" => "ไม่สามารถส่งใบแจ้งหนี้ถึง",
|
||||
"invoice_update" => "คำนวณใหม่",
|
||||
"item_insufficient_of_stock" => "จำนวนสินค้าไม่เพียงพอ",
|
||||
"item_name" => "ชื่อสินค้า",
|
||||
"item_number" => "สินค้า #",
|
||||
"item_out_of_stock" => "สินค้าจำหน่ายหมด",
|
||||
"key_browser" => "ความช่วยเหลือ",
|
||||
"key_cancel" => "ยกเลิกใบเสนอราคา/ใบแจ้งหนี้ /ใบการขาย นี้",
|
||||
"key_customer_search" => "ค้นหาลูกค้า",
|
||||
"key_finish_quote" => "จบใบเสนอราคา/ใบแจ้งหนี้โดยไม่ต้องชำระเงิน",
|
||||
"key_finish_sale" => "เพิ่มการชำระเงินและใบแจ้งหนี้ /ใบรายการขาย",
|
||||
"key_full" => "เปิดแบบเต็มหน้าจอ",
|
||||
"key_function" => "ฟังก์ชั่น",
|
||||
"key_help" => "คำสั่งลัดงานขาย",
|
||||
"key_help_modal" => "เปิดหน้าต่างคำสั่งลัดงานขาย",
|
||||
"key_in" => "ขยายเข้า",
|
||||
"key_item_search" => "ค้นหารายการขาย",
|
||||
"key_out" => "ขยายออก",
|
||||
"key_payment" => "เพิ่มการชำระเงิน",
|
||||
"key_print" => "พิมพ์หน้านี้",
|
||||
"key_restore" => "คืนการแสดงผลแบบดั้งเดิม/ขยาย",
|
||||
"key_search" => "ค้นหาตารางรายงาน",
|
||||
"key_suspend" => "พักรายการขายปัจจุบัน",
|
||||
"key_suspended" => "แสดงรายการขายที่พักไว้",
|
||||
"key_system" => "ทางลัดระบบ",
|
||||
"key_tendered" => "แก้ไขจำนวนเงินรับมา",
|
||||
"key_title" => "ทางลัดคียบอร์ดงานขาย",
|
||||
"mc" => "",
|
||||
"mode" => "รูปแบบการลงทะเบียน",
|
||||
"must_enter_numeric" => "จำนวนที่ถุกประมูลต้องใส่ข้อมุลที่เปนตัวเลข",
|
||||
"must_enter_numeric_giftcard" => "เลขที่บัตรของขวัญ ต้องใส่ตัวเลขเท่านั้น",
|
||||
"new_customer" => "ลูกค้าใหม่",
|
||||
"new_item" => "สินค้าใหม่",
|
||||
"no_description" => "ไม่ระบุรายละเอียด",
|
||||
"no_filter" => "ทั้งหมด",
|
||||
"no_items_in_cart" => "ไม่พบสินค้าในตระกร้า",
|
||||
"no_sales_to_display" => "ไม่มีการขายที่จะแสดง",
|
||||
"none_selected" => "คุณยังไม่ได้เลือกการขายที่จะลบ",
|
||||
"nontaxed_ind" => " . ",
|
||||
"not_authorized" => "การกระทำนี้ไม่ได้รับอนุญาต",
|
||||
"one_or_multiple" => "การขาย",
|
||||
"payment" => "รูปแบบชำระเงิน",
|
||||
"payment_amount" => "จำนวน",
|
||||
"payment_not_cover_total" => "จำนวนเงินที่ชำระต้องมากกว่าหรือเท่ากับยอดรวม",
|
||||
"payment_type" => "ชำระโดย",
|
||||
"payments" => "",
|
||||
"payments_total" => "ยอดชำระแล้ว",
|
||||
"price" => "ราคา",
|
||||
"print_after_sale" => "พิมพ์บิลหลังการขาย",
|
||||
"quantity" => "จำนวน",
|
||||
"quantity_less_than_reorder_level" => "คำเตือน ถ้าจำนวนของไม่เพียงพอกับความต้องการหรือไม่ตรงกับยอดในบันชี ก็สามารถทำการขายได้ แต่ต้องเชคปริมานสินค้าคงคลัง",
|
||||
"quantity_less_than_zero" => "คำเตือน: ถ้าจำนวนของไม่เพียงพอกับความต้องการหรือไม่ตรงกับยอดในบัญชี ก็สามารถทำการขายได้ แต่ต้องตรวจสอบปริมาญสินค้าคงคลังก่อน",
|
||||
"quantity_of_items" => "ปริมาณของ {0} รายการ",
|
||||
"quote" => "ใบเสนอราคา",
|
||||
"quote_number" => "หมายเลขอ้างอิง",
|
||||
"quote_number_duplicate" => "หมายเลขอ้างอิงต้องไม่ซ้ำกัน",
|
||||
"quote_sent" => "ส่งการอ้างอิงถึง",
|
||||
"quote_unsent" => "ส่งการอ้างอิงถึงผิดพลาด",
|
||||
"receipt" => "บิลขาย",
|
||||
"receipt_no_email" => "ลูกค้านี้ไม่มีที่อยู่อีเมล์",
|
||||
"receipt_number" => "จุดขาย#",
|
||||
"receipt_sent" => "ส่งใบเสร็จไปที่",
|
||||
"receipt_unsent" => "ไม่สามารถส่งใบเสร็จไปที่",
|
||||
"refund" => "ประเภทการยกเลิกการขาย",
|
||||
"register" => "ลงทะเบียนขาย",
|
||||
"remove_customer" => "ลบลูกค้า",
|
||||
"remove_discount" => "",
|
||||
"return" => "คืน",
|
||||
"rewards" => "คะแนนสะสม",
|
||||
"rewards_balance" => "คะแนนสะสมคงเหลือ",
|
||||
"sale" => "ขาย",
|
||||
"sale_by_invoice" => "การขายโดยใบแจ้งหนี้",
|
||||
"sale_for_customer" => "ลูกค้า:",
|
||||
"sale_time" => "เวลา",
|
||||
"sales_tax" => "ภาษีการขาย",
|
||||
"sales_total" => "",
|
||||
"select_customer" => "เลือกลูกค้า (Optional)",
|
||||
"send_invoice" => "ส่งใบแจ้งหนี้",
|
||||
"send_quote" => "ส่งใบเสนอราคา",
|
||||
"send_receipt" => "ส่งใบเสร็จ",
|
||||
"send_work_order" => "ส่งคำสั่งงาน",
|
||||
"serial" => "หมายเลขซีเรียล",
|
||||
"service_charge" => "",
|
||||
"show_due" => "",
|
||||
"show_invoice" => "ใบแจ้งหนี้",
|
||||
"show_receipt" => "ใบเสร็จ",
|
||||
"start_typing_customer_name" => "เริ่มต้นพิมพ์ชื่อลูกค้า...",
|
||||
"start_typing_item_name" => "เริ่มต้นพิมพ์ชื่อสินค้า หรือ สแกนบาร์โค๊ด...",
|
||||
"stock" => "คลังสินค้า",
|
||||
"stock_location" => "ที่เก็บ",
|
||||
"sub_total" => "ยอดรวมย่อย",
|
||||
"successfully_deleted" => "ลบการขายสมยูรณ์",
|
||||
"successfully_restored" => "คุณกู้คืนสำเร็จแล้ว",
|
||||
"successfully_suspended_sale" => "การขายของคุณถูกระงับเรียบร้อย",
|
||||
"successfully_updated" => "อัพเดทการขายสมบูรณ์",
|
||||
"suspend_sale" => "พักรายการ",
|
||||
"suspended_doc_id" => "รหัสเอกสาร",
|
||||
"suspended_sale_id" => "รหัสการขายที่ถูกพัก",
|
||||
"suspended_sales" => "การขายที่พักไว้",
|
||||
"table" => "โต๊ะ",
|
||||
"takings" => "การขายประจำวัน",
|
||||
"tax" => "ภาษี",
|
||||
"tax_id" => "รหัสภาษี",
|
||||
"tax_invoice" => "ใบกำกับภาษี",
|
||||
"tax_percent" => "ภาษี %",
|
||||
"taxed_ind" => "ภ",
|
||||
"total" => "ยอดรวม",
|
||||
"total_tax_exclusive" => "ยอดไม่รวมภาษี",
|
||||
"transaction_failed" => "การดำเนินการขายล้มเหลว",
|
||||
"unable_to_add_item" => "เพิ่มรายการไปยังการขายล้มเหลว",
|
||||
"unsuccessfully_deleted" => "ลบการขายไม่สำเร็จ",
|
||||
"unsuccessfully_restored" => "การคืนค่ารายการขายล้มเหลว",
|
||||
"unsuccessfully_suspended_sale" => "การขายของคุณถูกระงับเรียบร้อย",
|
||||
"unsuccessfully_updated" => "อัพเดทการขายไม่สมบูรณ์",
|
||||
"unsuspend" => "ยกเลิกการระงับ",
|
||||
"unsuspend_and_delete" => "ยกเลิกการระงับ และ ลบ",
|
||||
"update" => "แก้ไข",
|
||||
"upi" => "ยูพีไอ",
|
||||
"visa" => "",
|
||||
"wholesale" => "",
|
||||
"work_order" => "คำสั่งงาน",
|
||||
"work_order_number" => "หมายเลขคำสั่งงาน",
|
||||
"work_order_number_duplicate" => "หมายเลขคำสั่งงานต้องไม่ซ้ำกัน",
|
||||
"work_order_sent" => "คำสั่งงานส่งถึง",
|
||||
"work_order_unsent" => "ส่งคำสั่งงานล้มเหลว",
|
||||
"selected_customer" => "ลูกค้าที่เลือก",
|
||||
'customers_available_points' => "คะแนนที่มี",
|
||||
'rewards_package' => "คะแนนสะสม",
|
||||
'rewards_remaining_balance' => "คะแนนสะสมคงเหลือ ",
|
||||
'account_number' => "บัญชี #",
|
||||
'add_payment' => "เพิ่มบิล",
|
||||
'amount_due' => "ยอดค้างชำระ",
|
||||
'amount_tendered' => "ชำระเข้ามา",
|
||||
'authorized_signature' => "ลายเซ็นผู้มีอำนาจ",
|
||||
'cancel_sale' => "ยกเลิกการขาย",
|
||||
'cash' => "เงินสด",
|
||||
'cash_1' => "",
|
||||
'cash_2' => "",
|
||||
'cash_3' => "",
|
||||
'cash_4' => "",
|
||||
'cash_adjustment' => "การปรับเงินสดขาย",
|
||||
'cash_deposit' => "ฝากเงินสด",
|
||||
'cash_filter' => "เงินสด",
|
||||
'change_due' => "เงินทอน",
|
||||
'change_price' => "เปลี่ยนราคาขาย",
|
||||
'check' => "โอนเงิน/พร้อมเพย์/เช็ค",
|
||||
'check_balance' => "เช็คยอดคงเหลือ",
|
||||
'check_filter' => "ตรวจสอบ",
|
||||
'close' => "",
|
||||
'comment' => "หมายเหตุ",
|
||||
'comments' => "หมายเหตุ",
|
||||
'company_name' => "",
|
||||
'complete' => "",
|
||||
'complete_sale' => "จบการขาย",
|
||||
'confirm_cancel_sale' => "แน่ใจหรือไม่ที่จะล้างการขายนี้? ทุกรายการจะถูกลบทั้งหมด",
|
||||
'confirm_delete' => "โปรดยืนยันการลบรายการขายที่เลือกไว้ ?",
|
||||
'confirm_restore' => "คุณแน่ใจหรือไม่ว่าต้องการยกเลิกการขายที่เลือกไว้?",
|
||||
'credit' => "เครดิตการ์ด",
|
||||
'credit_deposit' => "เงินฝากเครดิต",
|
||||
'credit_filter' => "บัตรเครติด",
|
||||
'current_table' => "",
|
||||
'customer' => "ลูกค้า",
|
||||
'customer_address' => "Customer Address",
|
||||
'customer_discount' => "ส่วนลด",
|
||||
'customer_email' => "Customer Email",
|
||||
'customer_location' => "Customer Location",
|
||||
'customer_mailchimp_status' => "สถานะของระบบส่งเมล์เมล์ชิม",
|
||||
'customer_optional' => "(ต้องระบุวันที่ชำระเงิน)",
|
||||
'customer_required' => "(ต้องระบุ)",
|
||||
'customer_total' => "Total",
|
||||
'customer_total_spent' => "",
|
||||
'daily_sales' => "",
|
||||
'date' => "วันที่ขาย",
|
||||
'date_range' => "ระหว่างวันที่",
|
||||
'date_required' => "กรุณากรอกวันที่ให้ถูกต้อง",
|
||||
'date_type' => "กรุณากรอกข้อมูลในช่องวันที่",
|
||||
'debit' => "บัตรประชารัฐ/เดบิตการ์ด",
|
||||
'debit_filter' => "",
|
||||
'delete' => "อนุญาตให้ลบ",
|
||||
'delete_confirmation' => "แน่ใจหรือไม่ที่จะลบรายการขายนี้, ลบแล้วไม่สามารถเรียกกลับคืนใด้",
|
||||
'delete_entire_sale' => "ลบการขายทั้งหมด",
|
||||
'delete_successful' => "คุณลบการขายสำเร็จ",
|
||||
'delete_unsuccessful' => "คุณลบการขายไม่สำเร็จ",
|
||||
'description_abbrv' => "รายละเอียด",
|
||||
'discard' => "ยกเลิก",
|
||||
'discard_quote' => "",
|
||||
'discount' => "ส่วนลด %",
|
||||
'discount_included' => "% ส่วนลด",
|
||||
'discount_short' => "%",
|
||||
'due' => "วันครบกำหนด",
|
||||
'due_filter' => "วันที่ครบกำหนด",
|
||||
'edit' => "แก้ไข",
|
||||
'edit_item' => "แก้ไขสินค้า",
|
||||
'edit_sale' => "แก้ไขการขาย",
|
||||
'email_receipt' => "อีเมลบิล",
|
||||
'employee' => "พนักงาน",
|
||||
'entry' => "การนำเข้า",
|
||||
'error_editing_item' => "แก้ไขสินค้าล้มเหลว",
|
||||
'negative_price_invalid' => "ราคาไม่สามารถเป็นค่าติดลบได้",
|
||||
'negative_quantity_invalid' => "จำนวนไม่สามารถเป็นค่าติดลบได้",
|
||||
'negative_discount_invalid' => "ส่วนลดไม่สามารถเป็นค่าติดลบได้",
|
||||
'discount_percent_exceeds_100' => "ส่วนลดเปอร์เซ็นต์มีค่าได้ไม่เกิน 100%",
|
||||
'discount_exceeds_item_total' => "ส่วนลดต้องไม่เกินจำนวนรายการขายทั้งหมด",
|
||||
'negative_total_invalid' => "",
|
||||
'find_or_scan_item' => "ค้นหาสินค้า",
|
||||
'find_or_scan_item_or_receipt' => "ค้นหา หรือ แสกนรายการ หรือ ใบเสร็จ",
|
||||
'giftcard' => "บัตรของขวัญ",
|
||||
'giftcard_balance' => "ยอดคงเหลือบัตรของขวัญ",
|
||||
'giftcard_filter' => "",
|
||||
'giftcard_number' => "เลขที่บัตรของขวัญ",
|
||||
'group_by_category' => "กลุ่มตามหมวดหมู่",
|
||||
'group_by_type' => "กลุ่มตามประเภท",
|
||||
'hsn' => "HSN",
|
||||
'id' => "เลขที่ขาย",
|
||||
'include_prices' => "รวมในราคา?",
|
||||
'invoice' => "ใบแจ้งหนี้",
|
||||
'invoice_confirm' => "ใบแจ้งหนี้นี้จะถูกส่งไปที่",
|
||||
'invoice_enable' => "เลขที่ใบแจ้งหนี้",
|
||||
'invoice_filter' => "ใบแจ้งหนี้",
|
||||
'invoice_no_email' => "ลูกค้ารายนี้ไม่มีที่อยู่อีเมล",
|
||||
'invoice_number' => "เลขใบแจ้งหนี้ #",
|
||||
'invoice_number_duplicate' => "ใบแจ้งหนี้หมายเลข {0} จะต้องไม่ซ้ำกัน",
|
||||
'invoice_sent' => "ส่งใบแจ้งหนี้ไปที่",
|
||||
'invoice_total' => "ยอดรวมในใบแจ้งหนี้",
|
||||
'invoice_type_custom_invoice' => "ใบแจ้งหนี้ที่กำหนดเอง (custom_invoice.php)",
|
||||
'invoice_type_custom_tax_invoice' => "ใบกำกับภาษีที่กำหนดเอง (custom_tax_invoice.php)",
|
||||
'invoice_type_invoice' => "ใบแจ้งหนี้ (invoice.php)",
|
||||
'invoice_type_tax_invoice' => "ใบกำกับภาษี (tax_invoice.php)",
|
||||
'invoice_unsent' => "ไม่สามารถส่งใบแจ้งหนี้ถึง",
|
||||
'invoice_update' => "คำนวณใหม่",
|
||||
'item_insufficient_of_stock' => "จำนวนสินค้าไม่เพียงพอ",
|
||||
'item_name' => "ชื่อสินค้า",
|
||||
'item_number' => "สินค้า #",
|
||||
'item_out_of_stock' => "สินค้าจำหน่ายหมด",
|
||||
'key_browser' => "ความช่วยเหลือ",
|
||||
'key_cancel' => "ยกเลิกใบเสนอราคา/ใบแจ้งหนี้ /ใบการขาย นี้",
|
||||
'key_customer_search' => "ค้นหาลูกค้า",
|
||||
'key_finish_quote' => "จบใบเสนอราคา/ใบแจ้งหนี้โดยไม่ต้องชำระเงิน",
|
||||
'key_finish_sale' => "เพิ่มการชำระเงินและใบแจ้งหนี้ /ใบรายการขาย",
|
||||
'key_full' => "เปิดแบบเต็มหน้าจอ",
|
||||
'key_function' => "ฟังก์ชั่น",
|
||||
'key_help' => "คำสั่งลัดงานขาย",
|
||||
'key_help_modal' => "เปิดหน้าต่างคำสั่งลัดงานขาย",
|
||||
'key_in' => "ขยายเข้า",
|
||||
'key_item_search' => "ค้นหารายการขาย",
|
||||
'key_out' => "ขยายออก",
|
||||
'key_payment' => "เพิ่มการชำระเงิน",
|
||||
'key_print' => "พิมพ์หน้านี้",
|
||||
'key_restore' => "คืนการแสดงผลแบบดั้งเดิม/ขยาย",
|
||||
'key_search' => "ค้นหาตารางรายงาน",
|
||||
'key_suspend' => "พักรายการขายปัจจุบัน",
|
||||
'key_suspended' => "แสดงรายการขายที่พักไว้",
|
||||
'key_system' => "ทางลัดระบบ",
|
||||
'key_tendered' => "แก้ไขจำนวนเงินรับมา",
|
||||
'key_title' => "ทางลัดคียบอร์ดงานขาย",
|
||||
'mc' => "",
|
||||
'mode' => "รูปแบบการลงทะเบียน",
|
||||
'must_enter_numeric' => "จำนวนที่ถุกประมูลต้องใส่ข้อมุลที่เปนตัวเลข",
|
||||
'must_enter_numeric_giftcard' => "เลขที่บัตรของขวัญ ต้องใส่ตัวเลขเท่านั้น",
|
||||
'new_customer' => "ลูกค้าใหม่",
|
||||
'new_item' => "สินค้าใหม่",
|
||||
'no_description' => "ไม่ระบุรายละเอียด",
|
||||
'no_filter' => "ทั้งหมด",
|
||||
'no_items_in_cart' => "ไม่พบสินค้าในตระกร้า",
|
||||
'no_sales_to_display' => "ไม่มีการขายที่จะแสดง",
|
||||
'none_selected' => "คุณยังไม่ได้เลือกการขายที่จะลบ",
|
||||
'nontaxed_ind' => " . ",
|
||||
'not_authorized' => "การกระทำนี้ไม่ได้รับอนุญาต",
|
||||
'one_or_multiple' => "การขาย",
|
||||
'payment' => "รูปแบบชำระเงิน",
|
||||
'payment_amount' => "จำนวน",
|
||||
'payment_not_cover_total' => "จำนวนเงินที่ชำระต้องมากกว่าหรือเท่ากับยอดรวม",
|
||||
'payment_type' => "ชำระโดย",
|
||||
'payments' => "",
|
||||
'payments_total' => "ยอดชำระแล้ว",
|
||||
'price' => "ราคา",
|
||||
'print_after_sale' => "พิมพ์บิลหลังการขาย",
|
||||
'quantity' => "จำนวน",
|
||||
'quantity_less_than_reorder_level' => "คำเตือน ถ้าจำนวนของไม่เพียงพอกับความต้องการหรือไม่ตรงกับยอดในบันชี ก็สามารถทำการขายได้ แต่ต้องเชคปริมานสินค้าคงคลัง",
|
||||
'quantity_less_than_zero' => "คำเตือน: ถ้าจำนวนของไม่เพียงพอกับความต้องการหรือไม่ตรงกับยอดในบัญชี ก็สามารถทำการขายได้ แต่ต้องตรวจสอบปริมาญสินค้าคงคลังก่อน",
|
||||
'quantity_of_items' => "ปริมาณของ {0} รายการ",
|
||||
'quote' => "ใบเสนอราคา",
|
||||
'quote_number' => "หมายเลขอ้างอิง",
|
||||
'quote_number_duplicate' => "หมายเลขอ้างอิงต้องไม่ซ้ำกัน",
|
||||
'quote_sent' => "ส่งการอ้างอิงถึง",
|
||||
'quote_unsent' => "ส่งการอ้างอิงถึงผิดพลาด",
|
||||
'receipt' => "บิลขาย",
|
||||
'receipt_no_email' => "ลูกค้านี้ไม่มีที่อยู่อีเมล์",
|
||||
'receipt_number' => "จุดขาย#",
|
||||
'receipt_sent' => "ส่งใบเสร็จไปที่",
|
||||
'receipt_unsent' => "ไม่สามารถส่งใบเสร็จไปที่",
|
||||
'refund' => "ประเภทการยกเลิกการขาย",
|
||||
'register' => "ลงทะเบียนขาย",
|
||||
'remove_customer' => "ลบลูกค้า",
|
||||
'remove_discount' => "",
|
||||
'return' => "คืน",
|
||||
'rewards' => "คะแนนสะสม",
|
||||
'rewards_balance' => "คะแนนสะสมคงเหลือ",
|
||||
'sale' => "ขาย",
|
||||
'sale_by_invoice' => "การขายโดยใบแจ้งหนี้",
|
||||
'sale_for_customer' => "ลูกค้า:",
|
||||
'sale_time' => "เวลา",
|
||||
'sales_tax' => "ภาษีการขาย",
|
||||
'sales_total' => "",
|
||||
'select_customer' => "เลือกลูกค้า (Optional)",
|
||||
'send_invoice' => "ส่งใบแจ้งหนี้",
|
||||
'send_quote' => "ส่งใบเสนอราคา",
|
||||
'send_receipt' => "ส่งใบเสร็จ",
|
||||
'send_work_order' => "ส่งคำสั่งงาน",
|
||||
'serial' => "หมายเลขซีเรียล",
|
||||
'service_charge' => "",
|
||||
'show_due' => "",
|
||||
'show_invoice' => "ใบแจ้งหนี้",
|
||||
'show_receipt' => "ใบเสร็จ",
|
||||
'start_typing_customer_name' => "เริ่มต้นพิมพ์ชื่อลูกค้า...",
|
||||
'start_typing_item_name' => "เริ่มต้นพิมพ์ชื่อสินค้า หรือ สแกนบาร์โค๊ด...",
|
||||
'stock' => "คลังสินค้า",
|
||||
'stock_location' => "ที่เก็บ",
|
||||
'sub_total' => "ยอดรวมย่อย",
|
||||
'successfully_deleted' => "ลบการขายสมยูรณ์",
|
||||
'successfully_restored' => "คุณกู้คืนสำเร็จแล้ว",
|
||||
'successfully_suspended_sale' => "การขายของคุณถูกระงับเรียบร้อย",
|
||||
'successfully_updated' => "อัพเดทการขายสมบูรณ์",
|
||||
'suspend_sale' => "พักรายการ",
|
||||
'suspended_doc_id' => "รหัสเอกสาร",
|
||||
'suspended_sale_id' => "รหัสการขายที่ถูกพัก",
|
||||
'suspended_sales' => "การขายที่พักไว้",
|
||||
'table' => "โต๊ะ",
|
||||
'takings' => "การขายประจำวัน",
|
||||
'tax' => "ภาษี",
|
||||
'tax_id' => "รหัสภาษี",
|
||||
'tax_invoice' => "ใบกำกับภาษี",
|
||||
'tax_percent' => "ภาษี %",
|
||||
'taxed_ind' => "ภ",
|
||||
'total' => "ยอดรวม",
|
||||
'total_tax_exclusive' => "ยอดไม่รวมภาษี",
|
||||
'transaction_failed' => "การดำเนินการขายล้มเหลว",
|
||||
'unable_to_add_item' => "เพิ่มรายการไปยังการขายล้มเหลว",
|
||||
'unsuccessfully_deleted' => "ลบการขายไม่สำเร็จ",
|
||||
'unsuccessfully_restored' => "การคืนค่ารายการขายล้มเหลว",
|
||||
'unsuccessfully_suspended_sale' => "การขายของคุณถูกระงับเรียบร้อย",
|
||||
'unsuccessfully_updated' => "อัพเดทการขายไม่สมบูรณ์",
|
||||
'unsuspend' => "ยกเลิกการระงับ",
|
||||
'unsuspend_and_delete' => "ยกเลิกการระงับ และ ลบ",
|
||||
'update' => "แก้ไข",
|
||||
'upi' => "ยูพีไอ",
|
||||
'visa' => "",
|
||||
'wholesale' => "",
|
||||
'work_order' => "คำสั่งงาน",
|
||||
'work_order_number' => "หมายเลขคำสั่งงาน",
|
||||
'work_order_number_duplicate' => "หมายเลขคำสั่งงานต้องไม่ซ้ำกัน",
|
||||
'work_order_sent' => "คำสั่งงานส่งถึง",
|
||||
'work_order_unsent' => "ส่งคำสั่งงานล้มเหลว",
|
||||
'selected_customer' => "ลูกค้าที่เลือก",
|
||||
];
|
||||
|
||||
@@ -82,4 +82,40 @@ class Email_lib
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the mime type of the company logo file.
|
||||
*
|
||||
* @return string Mime type or empty string if logo doesn't exist
|
||||
*/
|
||||
public function getLogoMimeType(): string
|
||||
{
|
||||
$logo_path = FCPATH . 'uploads/' . $this->config['company_logo'];
|
||||
|
||||
if (!empty($this->config['company_logo']) && file_exists($logo_path)) {
|
||||
$mimeType = mime_content_type($logo_path);
|
||||
return $mimeType !== false ? $mimeType : '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an img tag for the company logo to use in email templates.
|
||||
*
|
||||
* @return string HTML img tag with base64-encoded logo, or empty string if no logo
|
||||
*/
|
||||
public function buildLogoImgTag(): string
|
||||
{
|
||||
$mimeType = $this->getLogoMimeType();
|
||||
|
||||
if ($mimeType === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$logo_path = FCPATH . 'uploads/' . $this->config['company_logo'];
|
||||
$logo_data = base64_encode(file_get_contents($logo_path));
|
||||
|
||||
return '<img id="image" src="data:' . $mimeType . ';base64,' . $logo_data . '" alt="company_logo">';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Database\MigrationRunner;
|
||||
use Config\Database;
|
||||
use stdClass;
|
||||
@@ -26,7 +25,7 @@ class MY_Migration extends MigrationRunner
|
||||
public function get_latest_migration(): int
|
||||
{
|
||||
$migrations = $this->findMigrations();
|
||||
return basename(end($migrations)->version);
|
||||
return (int) basename(end($migrations)->version);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,9 +41,11 @@ class MY_Migration extends MigrationRunner
|
||||
$builder = $db->table('migrations');
|
||||
$builder->select('version')->orderBy('version', 'DESC')->limit(1);
|
||||
$result = $builder->get()->getRow();
|
||||
return $result ? $result->version : 0;
|
||||
return $result ? (int) $result->version : 0;
|
||||
}
|
||||
} catch (DatabaseException $e) {
|
||||
} catch (\Exception $e) {
|
||||
// Database not available yet (e.g. fresh install before schema).
|
||||
// Catches mysqli_sql_exception which is not a DatabaseException.
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -76,8 +77,9 @@ class MY_Migration extends MigrationRunner
|
||||
$result = $builder->get()->getRow();
|
||||
return $result ? $result->version : false;
|
||||
}
|
||||
} catch (DatabaseException $e) {
|
||||
// Database doesn't exist yet or connection failed
|
||||
} catch (\Exception $e) {
|
||||
// Database not available yet (e.g. fresh install before schema).
|
||||
// Catches mysqli_sql_exception which is not a DatabaseException.
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -23,6 +23,19 @@ use ReflectionException;
|
||||
*/
|
||||
class Sale_lib
|
||||
{
|
||||
private const KEY_SHORTCUT_DEFAULTS = [
|
||||
'cancel' => ['value' => '27 | ESC', 'code' => 27, 'label' => 'ESC'],
|
||||
'items' => ['value' => '49 | ALT + 1', 'code' => 49, 'label' => 'ALT + 1'],
|
||||
'customers' => ['value' => '50 | ALT + 2', 'code' => 50, 'label' => 'ALT + 2'],
|
||||
'suspend' => ['value' => '51 | ALT + 3', 'code' => 51, 'label' => 'ALT + 3'],
|
||||
'suspended' => ['value' => '52 | ALT + 4', 'code' => 52, 'label' => 'ALT + 4'],
|
||||
'amount' => ['value' => '53 | ALT + 5', 'code' => 53, 'label' => 'ALT + 5'],
|
||||
'payment' => ['value' => '54 | ALT + 6', 'code' => 54, 'label' => 'ALT + 6'],
|
||||
'complete' => ['value' => '55 | ALT + 7', 'code' => 55, 'label' => 'ALT + 7'],
|
||||
'finish' => ['value' => '56 | ALT + 8', 'code' => 56, 'label' => 'ALT + 8'],
|
||||
'help' => ['value' => '57 | ALT + 9', 'code' => 57, 'label' => 'ALT + 9'],
|
||||
];
|
||||
|
||||
private Attribute $attribute;
|
||||
private Customer $customer;
|
||||
private Dinner_table $dinner_table;
|
||||
@@ -95,6 +108,11 @@ class Sale_lib
|
||||
'custom_tax_invoice'
|
||||
];
|
||||
|
||||
private const ALLOWED_RECEIPT_TEMPLATES = [
|
||||
'receipt_default',
|
||||
'receipt_short'
|
||||
];
|
||||
|
||||
public function get_invoice_type_options(): array
|
||||
{
|
||||
$invoice_types = [];
|
||||
@@ -105,11 +123,54 @@ class Sale_lib
|
||||
return $invoice_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the available keyboard shortcut choices for the configuration screen.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getKeyShortcutsOptions(): array
|
||||
{
|
||||
$keyShortcuts = [];
|
||||
|
||||
foreach (self::KEY_SHORTCUT_DEFAULTS as $shortcut) {
|
||||
$keyShortcuts[$shortcut['value']] = $shortcut['label'];
|
||||
}
|
||||
|
||||
return $keyShortcuts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns parsed shortcut bindings from app_config with sensible defaults.
|
||||
*
|
||||
* @return array<string, array{value:string,code:int,label:string}>
|
||||
*/
|
||||
public function getKeyShortcuts(): array
|
||||
{
|
||||
$keyboardShortcuts = [];
|
||||
|
||||
foreach (self::KEY_SHORTCUT_DEFAULTS as $name => $default) {
|
||||
$value = $this->config["key_$name"] ?? $default['value'];
|
||||
$parts = array_map('trim', explode('|', $value, 2));
|
||||
$keyboardShortcuts[$name] = [
|
||||
'value' => $value,
|
||||
'code' => (int)($parts[0] ?? $default['code']),
|
||||
'label' => $parts[1] ?? $default['label']
|
||||
];
|
||||
}
|
||||
|
||||
return $keyboardShortcuts;
|
||||
}
|
||||
|
||||
public static function isValidInvoiceType(string $invoice_type): bool
|
||||
{
|
||||
return in_array($invoice_type, self::ALLOWED_INVOICE_TYPES, true);
|
||||
}
|
||||
|
||||
public static function isValidReceiptTemplate(string $receipt_template): bool
|
||||
{
|
||||
return in_array($receipt_template, self::ALLOWED_RECEIPT_TEMPLATES, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
|
||||
@@ -601,6 +601,10 @@ class Attribute extends Model
|
||||
*/
|
||||
public function saveAttributeLink(int $itemId, int $definitionId, int $attributeId): bool
|
||||
{
|
||||
if ($attributeId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$normalizedItemId = empty($itemId) ? null : $itemId;
|
||||
$normalizedAttributeId = empty($attributeId) ? null : $attributeId;
|
||||
|
||||
|
||||
@@ -65,8 +65,10 @@ class Item extends Model
|
||||
public function exists(string $item_id, bool $ignore_deleted = false, bool $deleted = false): bool
|
||||
{
|
||||
$builder = $this->db->table('items');
|
||||
$builder->groupStart();
|
||||
$builder->where('item_id', $item_id);
|
||||
$builder->orWhere('item_number', $item_id);
|
||||
$builder->groupEnd();
|
||||
|
||||
if (!$ignore_deleted) {
|
||||
$builder->where('deleted', $deleted);
|
||||
@@ -389,9 +391,10 @@ class Item extends Model
|
||||
public function get_item_id(string $item_number, bool $ignore_deleted = false, bool $deleted = false): bool|int
|
||||
{
|
||||
$builder = $this->db->table('items');
|
||||
$builder->join('suppliers', 'suppliers.person_id = items.supplier_id', 'left');
|
||||
$builder->groupStart();
|
||||
$builder->where('item_number', $item_number);
|
||||
$builder->orWhere('item_id', $item_number);
|
||||
$builder->groupEnd();
|
||||
|
||||
if (!$ignore_deleted) {
|
||||
$builder->where('items.deleted', $deleted);
|
||||
|
||||
@@ -294,7 +294,9 @@ class Receiving extends Model
|
||||
lang('Sales.check') => lang('Sales.check'),
|
||||
lang('Sales.debit') => lang('Sales.debit'),
|
||||
lang('Sales.credit') => lang('Sales.credit'),
|
||||
lang('Sales.due') => lang('Sales.due')
|
||||
lang('Sales.due') => lang('Sales.due'),
|
||||
lang('Sales.bank_transfer') => lang('Sales.bank_transfer'),
|
||||
lang('Sales.wallet') => lang('Sales.wallet')
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -33,14 +33,16 @@ class Summary_sales_taxes extends Summary_report
|
||||
* @param object $builder
|
||||
* @return void
|
||||
*/
|
||||
protected function _where(array $inputs, object &$builder): void // TODO: hungarian notation
|
||||
protected function _where(array $inputs, object &$builder): void
|
||||
{
|
||||
$builder->where('sales.sale_status', COMPLETED);
|
||||
|
||||
if (empty($this->config['date_or_time_format'])) { // TODO: Duplicated code
|
||||
$builder->where('DATE(sales.sale_time) BETWEEN ' . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date']));
|
||||
if (empty($this->config['date_or_time_format'])) {
|
||||
$builder->where('DATE(sales.sale_time) >=', $inputs['start_date']);
|
||||
$builder->where('DATE(sales.sale_time) <=', $inputs['end_date']);
|
||||
} else {
|
||||
$builder->where('sales.sale_time BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date'])));
|
||||
$builder->where('sales.sale_time >=', $inputs['start_date']);
|
||||
$builder->where('sales.sale_time <=', $inputs['end_date']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,9 +55,11 @@ class Summary_sales_taxes extends Summary_report
|
||||
$builder = $this->db->table('sales_taxes');
|
||||
|
||||
if (empty($this->config['date_or_time_format'])) {
|
||||
$builder->where('DATE(sale_time) BETWEEN ' . $inputs['start_date'] . ' AND ' . $inputs['end_date']);
|
||||
$builder->where('DATE(sale_time) >=', $inputs['start_date']);
|
||||
$builder->where('DATE(sale_time) <=', $inputs['end_date']);
|
||||
} else {
|
||||
$builder->where('sale_time BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date'])));
|
||||
$builder->where('sale_time >=', $inputs['start_date']);
|
||||
$builder->where('sale_time <=', $inputs['end_date']);
|
||||
}
|
||||
|
||||
$builder->select('reporting_authority, jurisdiction_name, tax_category, tax_rate, SUM(sale_tax_amount) AS tax');
|
||||
|
||||
@@ -277,6 +277,14 @@ class Sale extends Model
|
||||
$builder->like('payment_type', lang('Sales.debit'));
|
||||
}
|
||||
|
||||
if ($filters['only_bank_transfer']) {
|
||||
$builder->like('payment_type', lang('Sales.bank_transfer'));
|
||||
}
|
||||
|
||||
if ($filters['only_wallet']) {
|
||||
$builder->like('payment_type', lang('Sales.wallet'));
|
||||
}
|
||||
|
||||
$builder->groupBy('payment_type');
|
||||
|
||||
$payments = $builder->get()->getResultArray();
|
||||
@@ -319,7 +327,7 @@ class Sale extends Model
|
||||
{
|
||||
$suggestions = [];
|
||||
|
||||
if (!$this->is_valid_receipt($search)) {
|
||||
if (!$this->isValidReceipt($search)) {
|
||||
$builder = $this->db->table('sales');
|
||||
$builder->distinct()->select('first_name, last_name');
|
||||
$builder->join('people', 'people.person_id = sales.customer_id');
|
||||
@@ -400,21 +408,21 @@ class Sale extends Model
|
||||
/**
|
||||
* Checks if valid receipt
|
||||
*/
|
||||
public function is_valid_receipt(string|null &$receipt_sale_id): bool // TODO: like the others, maybe this should be an array rather than a delimited string... either that or the parameter name needs to be changed. $receipt_sale_id implies that it's an int.
|
||||
public function isValidReceipt(string|null &$receiptSaleId): bool // TODO: like the others, maybe this should be an array rather than a delimited string... either that or the parameter name needs to be changed. $receipt_sale_id implies that it's an int.
|
||||
{
|
||||
$config = config(OSPOS::class)->settings;
|
||||
|
||||
if (!empty($receipt_sale_id)) {
|
||||
if (!empty($receiptSaleId)) {
|
||||
// POS #
|
||||
$pieces = explode(' ', $receipt_sale_id);
|
||||
$pieces = explode(' ', trim($receiptSaleId));
|
||||
|
||||
if (count($pieces) == 2 && preg_match('/(POS)/i', $pieces[0])) {
|
||||
return $this->exists($pieces[1]);
|
||||
if (count($pieces) == 2 && strtoupper($pieces[0]) === 'POS' && ctype_digit($pieces[1])) {
|
||||
return $this->exists((int)$pieces[1]);
|
||||
} elseif ($config['invoice_enable']) {
|
||||
$sale_info = $this->get_sale_by_invoice_number($receipt_sale_id);
|
||||
$saleInfo = $this->get_sale_by_invoice_number($receiptSaleId);
|
||||
|
||||
if ($sale_info->getNumRows() > 0) {
|
||||
$receipt_sale_id = 'POS ' . $sale_info->getRow()->sale_id;
|
||||
if ($saleInfo->getNumRows() > 0) {
|
||||
$receiptSaleId = 'POS ' . $saleInfo->getRow()->sale_id;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1509,5 +1517,13 @@ class Sale extends Model
|
||||
if ($filters['only_check']) {
|
||||
$builder->like('payments.payment_type', lang('Sales.check'));
|
||||
}
|
||||
|
||||
if ($filters['only_bank_transfer']) {
|
||||
$builder->like('payments.payment_type', lang('Sales.bank_transfer'));
|
||||
}
|
||||
|
||||
if ($filters['only_wallet']) {
|
||||
$builder->like('payments.payment_type', lang('Sales.wallet'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,12 +102,12 @@
|
||||
<script type="text/javascript">
|
||||
// Validation and submit handling
|
||||
$(document).ready(function() {
|
||||
const values = [];
|
||||
const definition_id = <?= esc($definition_id, 'js') ?>;
|
||||
const is_new = definition_id == 0;
|
||||
var values = [];
|
||||
var definition_id = <?= esc($definition_id, 'js') ?>;
|
||||
var is_new = definition_id == 0;
|
||||
|
||||
const disable_definition_types = function() {
|
||||
const definition_type = $("#definition_type option:selected").text();
|
||||
var disable_definition_types = function() {
|
||||
var definition_type = $("#definition_type option:selected").text();
|
||||
|
||||
if (definition_type == "DATE" || (definition_type == "GROUP" && !is_new) || definition_type == "DECIMAL") {
|
||||
$('#definition_type').prop("disabled", true);
|
||||
@@ -121,7 +121,7 @@
|
||||
}
|
||||
disable_definition_types();
|
||||
|
||||
const disable_category_dropdown = function() {
|
||||
var disable_category_dropdown = function() {
|
||||
if (definition_id == -1) {
|
||||
$('#definition_name').prop("disabled", true);
|
||||
$('#definition_type').prop("disabled", true);
|
||||
@@ -131,11 +131,11 @@
|
||||
}
|
||||
disable_category_dropdown();
|
||||
|
||||
const show_hide_fields = function(event) {
|
||||
const is_dropdown = $('#definition_type').val() !== '1';
|
||||
const is_decimal = $('#definition_type').val() !== '2';
|
||||
const is_no_group = $('#definition_type').val() !== '0';
|
||||
const is_category_dropdown = definition_id == -1;
|
||||
var show_hide_fields = function(event) {
|
||||
var is_dropdown = $('#definition_type').val() !== '1';
|
||||
var is_decimal = $('#definition_type').val() !== '2';
|
||||
var is_no_group = $('#definition_type').val() !== '0';
|
||||
var is_category_dropdown = definition_id == -1;
|
||||
|
||||
$('#definition_value, #definition_list_group').parents('.form-group').toggleClass('hidden', is_dropdown);
|
||||
$('#definition_unit').parents('.form-group').toggleClass('hidden', is_decimal);
|
||||
@@ -150,12 +150,12 @@
|
||||
show_hide_fields();
|
||||
|
||||
$('.selectpicker').each(function() {
|
||||
const $selectpicker = $(this);
|
||||
var $selectpicker = $(this);
|
||||
$.fn.selectpicker.call($selectpicker, $selectpicker.data());
|
||||
});
|
||||
|
||||
const remove_attribute_value = function() {
|
||||
const value = $(this).parents("li").text();
|
||||
var remove_attribute_value = function() {
|
||||
var value = $(this).parents("li").text();
|
||||
|
||||
if (is_new) {
|
||||
values.splice($.inArray(value, values), 1);
|
||||
@@ -168,8 +168,8 @@
|
||||
$(this).parents("li").remove();
|
||||
};
|
||||
|
||||
const add_attribute_value = function(value) {
|
||||
const is_event = typeof(value) !== 'string';
|
||||
var add_attribute_value = function(value) {
|
||||
var is_event = typeof(value) !== 'string';
|
||||
|
||||
if ($("#definition_value").val().match(/(\||_)/g) != null) {
|
||||
return;
|
||||
@@ -206,7 +206,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
const definition_values = <?= json_encode(array_values($definition_values)) ?>;
|
||||
var definition_values = <?= json_encode(array_values($definition_values)) ?>;
|
||||
$.each(definition_values, function(index, element) {
|
||||
add_attribute_value(element);
|
||||
});
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
(function() {
|
||||
<?= view('partial/datepicker_locale', ['format' => dateformat_bootstrap($config['dateformat'])]) ?>
|
||||
|
||||
const enable_delete = function() {
|
||||
var enable_delete = function() {
|
||||
$('.remove_attribute_btn').click(function() {
|
||||
$(this).parents('.form-group').remove();
|
||||
});
|
||||
@@ -113,7 +113,7 @@
|
||||
enable_delete();
|
||||
|
||||
$("input[name*='attribute_links']").change(function() {
|
||||
const definition_id = $(this).data('definition-id');
|
||||
var definition_id = $(this).data('definition-id');
|
||||
$("input[name='attribute_ids[" + definition_id + "]']").val('');
|
||||
}).autocomplete({
|
||||
source: function(request, response) {
|
||||
@@ -129,11 +129,11 @@
|
||||
delay: 10
|
||||
});
|
||||
|
||||
const definition_values = function() {
|
||||
const result = {};
|
||||
var definition_values = function() {
|
||||
var result = {};
|
||||
$("[name*='attribute_links'").each(function() {
|
||||
const definition_id = $(this).data('definition-id');
|
||||
const element = $(this);
|
||||
var definition_id = $(this).data('definition-id');
|
||||
var element = $(this);
|
||||
|
||||
// For checkboxes, use the visible checkbox, not the hidden input
|
||||
if (element.attr('type') === 'hidden' && element.siblings('input[type="checkbox"]').length > 0) {
|
||||
@@ -151,9 +151,9 @@
|
||||
return result;
|
||||
};
|
||||
|
||||
const refresh = function() {
|
||||
const definition_id = $("#definition_name option:selected").val();
|
||||
let attribute_values = definition_values();
|
||||
var refresh = function() {
|
||||
var definition_id = $("#definition_name option:selected").val();
|
||||
var attribute_values = definition_values();
|
||||
attribute_values[definition_id] = '';
|
||||
$('#attributes').load('<?= "items/attributes/$item_id" ?>', {
|
||||
'definition_ids': JSON.stringify(attribute_values)
|
||||
|
||||
@@ -11,31 +11,34 @@ $barcode_lib = new Barcode_lib();
|
||||
|
||||
<!doctype html>
|
||||
<html lang="<?= current_language_code() ?>">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><?= lang('Items.generate_barcodes') ?></title>
|
||||
<link rel="stylesheet" href="<?= base_url() ?>css/barcode_font.css">
|
||||
<style>
|
||||
.barcode svg {
|
||||
height: <?= $barcode_config['barcode_height'] ?>px;
|
||||
width: <?= $barcode_config['barcode_width'] ?>px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class=<?= 'font_' . $barcode_lib->get_font_name($barcode_config['barcode_font']) ?> style="font-size: <?= $barcode_config['barcode_font_size'] ?>px;">
|
||||
<table style="border-spacing: <?= $barcode_config['barcode_page_cellspacing'] ?>; width: <?= $barcode_config['barcode_page_width'] ?>%;">
|
||||
<tr>
|
||||
<?php
|
||||
$count = 0;
|
||||
foreach ($items as $item) {
|
||||
if ($count % $barcode_config['barcode_num_in_row'] == 0 && $count != 0) {
|
||||
echo '</tr><tr>';
|
||||
}
|
||||
echo '<td>' . $barcode_lib->display_barcode($item, $barcode_config) . '</td>';
|
||||
$count++;
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><?= esc(lang('Items.generate_barcodes')) ?></title>
|
||||
<link rel="stylesheet" href="<?= esc(base_url('css/barcode_font.css'), 'url') ?>">
|
||||
<style>
|
||||
.barcode svg {
|
||||
height: <?= (int) $barcode_config['barcode_height'] ?>px;
|
||||
width: <?= (int) $barcode_config['barcode_width'] ?>px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="<?= esc('font_' . $barcode_lib->get_font_name($barcode_config['barcode_font']), 'attr') ?>" style="font-size: <?= (int) $barcode_config['barcode_font_size'] ?>px;">
|
||||
<table style="border-spacing: <?= (int) $barcode_config['barcode_page_cellspacing'] ?>px; width: <?= (int) $barcode_config['barcode_page_width'] ?>%;">
|
||||
<tr>
|
||||
<?php
|
||||
$count = 0;
|
||||
foreach ($items as $item) {
|
||||
if ($count % $barcode_config['barcode_num_in_row'] == 0 && $count != 0) {
|
||||
echo '</tr><tr>';
|
||||
}
|
||||
?>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
echo '<td>' . $barcode_lib->display_barcode($item, $barcode_config) . '</td>';
|
||||
$count++;
|
||||
}
|
||||
?>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -308,7 +308,7 @@
|
||||
);
|
||||
});
|
||||
|
||||
const submit_form = function() {
|
||||
var submit_form = function() {
|
||||
$(this).ajaxSubmit({
|
||||
success: function(response) {
|
||||
dialog_support.hide();
|
||||
|
||||
@@ -204,6 +204,7 @@
|
||||
<?= form_label(lang('Config.barcode_number_in_row'), 'barcode_num_in_row', ['class' => 'control-label col-xs-2 required']) ?>
|
||||
<div class="col-xs-2">
|
||||
<?= form_input([
|
||||
'type' => 'number',
|
||||
'name' => 'barcode_num_in_row',
|
||||
'id' => 'barcode_num_in_row',
|
||||
'class' => 'form-control input-sm required',
|
||||
@@ -217,6 +218,9 @@
|
||||
<div class="col-sm-2">
|
||||
<div class="input-group">
|
||||
<?= form_input([
|
||||
'type' => 'number',
|
||||
'min' => '0',
|
||||
'max' => '100',
|
||||
'name' => 'barcode_page_width',
|
||||
'id' => 'barcode_page_width',
|
||||
'class' => 'form-control input-sm required',
|
||||
@@ -232,6 +236,7 @@
|
||||
<div class="col-sm-2">
|
||||
<div class="input-group">
|
||||
<?= form_input([
|
||||
'type' => 'number',
|
||||
'name' => 'barcode_page_cellspacing',
|
||||
'id' => 'barcode_page_cellspacing',
|
||||
'class' => 'form-control input-sm required',
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
<?= form_dropdown(
|
||||
'protocol',
|
||||
[
|
||||
'mail' => 'mail',
|
||||
'sendmail' => 'sendmail',
|
||||
'smtp' => 'smtp'
|
||||
'mail' => 'Mail',
|
||||
'sendmail' => 'Sendmail',
|
||||
'smtp' => 'SMTP'
|
||||
],
|
||||
$config['protocol'],
|
||||
'class="form-control input-sm" id="protocol"'
|
||||
@@ -55,6 +55,7 @@
|
||||
<?= form_label(lang('Config.email_smtp_port'), 'smtp_port', ['class' => 'control-label col-xs-2']) ?>
|
||||
<div class="col-xs-2">
|
||||
<?= form_input([
|
||||
'type' => 'number',
|
||||
'name' => 'smtp_port',
|
||||
'id' => 'smtp_port',
|
||||
'class' => 'form-control input-sm',
|
||||
@@ -83,6 +84,7 @@
|
||||
<?= form_label(lang('Config.email_smtp_timeout'), 'smtp_timeout', ['class' => 'control-label col-xs-2']) ?>
|
||||
<div class="col-xs-2">
|
||||
<?= form_input([
|
||||
'type' => 'number',
|
||||
'name' => 'smtp_timeout',
|
||||
'id' => 'smtp_timeout',
|
||||
'class' => 'form-control input-sm',
|
||||
@@ -139,7 +141,7 @@
|
||||
<script type="text/javascript">
|
||||
// Validation and submit handling
|
||||
$(document).ready(function() {
|
||||
const check_protocol = function() {
|
||||
var check_protocol = function() {
|
||||
if ($('#protocol').val() == 'sendmail') {
|
||||
$('#mailpath').prop('disabled', false);
|
||||
$('#smtp_host, #smtp_user, #smtp_pass, #smtp_port, #smtp_timeout, #smtp_crypto').prop('disabled', true);
|
||||
|
||||
@@ -467,8 +467,8 @@
|
||||
<script type="text/javascript">
|
||||
// Validation and submit handling
|
||||
$(document).ready(function() {
|
||||
const enable_disable_gcaptcha_enable = (function() {
|
||||
const gcaptcha_enable = $("#gcaptcha_enable").is(":checked");
|
||||
var enable_disable_gcaptcha_enable = (function() {
|
||||
var gcaptcha_enable = $("#gcaptcha_enable").is(":checked");
|
||||
if (gcaptcha_enable) {
|
||||
$("#gcaptcha_site_key, #gcaptcha_secret_key").prop("disabled", !gcaptcha_enable).addClass("required");
|
||||
$("#config_gcaptcha_site_key, #config_gcaptcha_secret_key").addClass("required");
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
<span class="glyphicon glyphicon-phone-alt"></span>
|
||||
</span>
|
||||
<?= form_input([
|
||||
'type' => 'tel',
|
||||
'name' => 'phone',
|
||||
'id' => 'phone',
|
||||
'class' => 'form-control input-sm required',
|
||||
@@ -122,6 +123,7 @@
|
||||
<span class="glyphicon glyphicon-phone-alt"></span>
|
||||
</span>
|
||||
<?= form_input([
|
||||
'type' => 'tel',
|
||||
'name' => 'fax',
|
||||
'id' => 'fax',
|
||||
'class' => 'form-control input-sm',
|
||||
|
||||
@@ -198,9 +198,9 @@
|
||||
<script type="text/javascript">
|
||||
// Validation and submit handling
|
||||
$(document).ready(function() {
|
||||
const enable_disable_invoice_enable = (function() {
|
||||
const invoice_enabled = $("#invoice_enable").is(":checked");
|
||||
const work_order_enabled = $("#work_order_enable").is(":checked");
|
||||
var enable_disable_invoice_enable = (function() {
|
||||
var invoice_enabled = $("#invoice_enable").is(":checked");
|
||||
var work_order_enabled = $("#work_order_enable").is(":checked");
|
||||
$("#sales_invoice_format, #recv_invoice_format, #invoice_default_comments, #invoice_email_message, select[name='invoice_type'], #sales_quote_format, select[name='line_sequence'], #last_used_invoice_number, #last_used_quote_number, #quote_default_comments, #work_order_enable, #work_order_format, #last_used_work_order_number").prop("disabled", !invoice_enabled);
|
||||
if (invoice_enabled) {
|
||||
$("#work_order_format, #last_used_work_order_number").prop("disabled", !work_order_enabled);
|
||||
@@ -210,9 +210,9 @@
|
||||
return arguments.callee;
|
||||
})();
|
||||
|
||||
const enable_disable_work_order_enable = (function() {
|
||||
const work_order_enabled = $("#work_order_enable").is(":checked");
|
||||
const invoice_enabled = $("#invoice_enable").is(":checked");
|
||||
var enable_disable_work_order_enable = (function() {
|
||||
var work_order_enabled = $("#work_order_enable").is(":checked");
|
||||
var invoice_enabled = $("#invoice_enable").is(":checked");
|
||||
if (invoice_enabled) {
|
||||
$("#work_order_format, #last_used_work_order_number").prop("disabled", !work_order_enabled);
|
||||
}
|
||||
|
||||
@@ -292,7 +292,7 @@
|
||||
$('span').tooltip();
|
||||
|
||||
$('#currency_symbol, #thousands_separator, #currency_code').change(function() {
|
||||
const data = {
|
||||
var data = {
|
||||
number_locale: $('#number_locale').val()
|
||||
};
|
||||
data['save_number_locale'] = $("input[name='save_number_locale']").val();
|
||||
@@ -336,7 +336,7 @@
|
||||
}
|
||||
},
|
||||
dataFilter: function(data) {
|
||||
const response = JSON.parse(data);
|
||||
var response = JSON.parse(data);
|
||||
$("input[name='save_number_locale']").val(response.save_number_locale);
|
||||
$('#number_locale_example').text(response.number_locale_example);
|
||||
$('#currency_symbol').val(response.currency_symbol);
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
<li role="presentation">
|
||||
<a data-toggle="tab" href="#invoice_tab" title="<?= lang('Config.invoice_configuration') ?>"><?= lang('Config.invoice') ?></a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a data-toggle="tab" href="#shortcuts_tab" title="<?= lang('Config.shortcuts_configuration') ?>"><?= lang('Config.shortcuts') ?></a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a data-toggle="tab" href="#reward_tab" title="<?= lang('Config.reward_configuration') ?>"><?= lang('Config.reward') ?></a>
|
||||
</li>
|
||||
@@ -65,6 +68,9 @@
|
||||
<div class="tab-pane" id="invoice_tab">
|
||||
<?= view('configs/invoice_config') ?>
|
||||
</div>
|
||||
<div class="tab-pane" id="shortcuts_tab">
|
||||
<?= view('configs/shortcuts_config') ?>
|
||||
</div>
|
||||
<div class="tab-pane" id="reward_tab">
|
||||
<?= view('configs/reward_config') ?>
|
||||
</div>
|
||||
|
||||
@@ -338,9 +338,9 @@
|
||||
// Validation and submit handling
|
||||
$(document).ready(function() {
|
||||
if (window.localStorage && window.jsPrintSetup) {
|
||||
const printers = (jsPrintSetup.getPrintersList() && jsPrintSetup.getPrintersList().split(',')) || [];
|
||||
var printers = (jsPrintSetup.getPrintersList() && jsPrintSetup.getPrintersList().split(',')) || [];
|
||||
$('#receipt_printer, #invoice_printer, #takings_printer').each(function() {
|
||||
const $this = $(this)
|
||||
var $this = $(this)
|
||||
$(printers).each(function(key, value) {
|
||||
$this.append($('<option>', {
|
||||
value: value
|
||||
@@ -360,7 +360,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
const dialog_confirmed = window.jsPrintSetup;
|
||||
var dialog_confirmed = window.jsPrintSetup;
|
||||
|
||||
$('#receipt_config_form').validate($.extend(form_support.handler, {
|
||||
submitHandler: function(form) {
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
// Validation and submit handling
|
||||
$(document).ready(function() {
|
||||
|
||||
const enable_disable_customer_reward_enable = (function() {
|
||||
const customer_reward_enable = $("#customer_reward_enable").is(":checked");
|
||||
var enable_disable_customer_reward_enable = (function() {
|
||||
var customer_reward_enable = $("#customer_reward_enable").is(":checked");
|
||||
$("input[name*='customer_reward']:not(input[name=customer_reward_enable])").prop("disabled", !customer_reward_enable);
|
||||
$("input[name*='reward_points_']:not(input[name=customer_reward_enable])").prop("disabled", !customer_reward_enable);
|
||||
if (customer_reward_enable) {
|
||||
@@ -57,9 +57,9 @@
|
||||
|
||||
$("#customer_reward_enable").change(enable_disable_customer_reward_enable);
|
||||
|
||||
let table_count = <?= sizeof($customer_rewards) ?>;
|
||||
var table_count = <?= sizeof($customer_rewards) ?>;
|
||||
|
||||
const hide_show_remove = function() {
|
||||
var hide_show_remove = function() {
|
||||
if ($("input[name*='customer_rewards']:enabled").length > 1) {
|
||||
$(".remove_customer_rewards").show();
|
||||
} else {
|
||||
@@ -67,27 +67,27 @@
|
||||
}
|
||||
};
|
||||
|
||||
const add_customer_reward = function() {
|
||||
let id = $(this).parent().find('input').attr('id');
|
||||
var add_customer_reward = function() {
|
||||
var id = $(this).parent().find('input').attr('id');
|
||||
id = id.replace(/.*?_(\d+)$/g, "$1");
|
||||
const previous_id = 'customer_reward_' + id;
|
||||
const previous_id_next = 'reward_points_' + id;
|
||||
const block = $(this).parent().clone(true);
|
||||
const new_block = block.insertAfter($(this).parent());
|
||||
const new_block_id = 'customer_reward_' + ++id;
|
||||
const new_block_id_next = 'reward_points_' + id;
|
||||
var previous_id = 'customer_reward_' + id;
|
||||
var previous_id_next = 'reward_points_' + id;
|
||||
var block = $(this).parent().clone(true);
|
||||
var new_block = block.insertAfter($(this).parent());
|
||||
var new_block_id = 'customer_reward_' + ++id;
|
||||
var new_block_id_next = 'reward_points_' + id;
|
||||
$(new_block).find('label').html("<?= lang('Config.customer_reward') ?> " + ++table_count).attr('for', new_block_id).attr('class', 'control-label col-xs-2');
|
||||
$(new_block).find("input[id='" + previous_id + "']").attr('id', new_block_id).removeAttr('disabled').attr('name', new_block_id).attr('class', 'form-control input-sm').val('');
|
||||
$(new_block).find("input[id='" + previous_id_next + "']").attr('id', new_block_id_next).removeAttr('disabled').attr('name', new_block_id_next).attr('class', 'form-control input-sm').val('');
|
||||
hide_show_remove();
|
||||
};
|
||||
|
||||
const remove_customer_reward = function() {
|
||||
var remove_customer_reward = function() {
|
||||
$(this).parent().remove();
|
||||
hide_show_remove();
|
||||
};
|
||||
|
||||
const init_add_remove_tables = function() {
|
||||
var init_add_remove_tables = function() {
|
||||
$('.add_customer_reward').click(add_customer_reward);
|
||||
$('.remove_customer_reward').click(remove_customer_reward);
|
||||
hide_show_remove();
|
||||
@@ -96,10 +96,10 @@
|
||||
};
|
||||
init_add_remove_tables();
|
||||
|
||||
const duplicate_found = false;
|
||||
var duplicate_found = false;
|
||||
// Run validator once for all fields
|
||||
$.validator.addMethod('customer_reward', function(value, element) {
|
||||
let value_count = 0;
|
||||
var value_count = 0;
|
||||
$("input[name*='customer_reward']:not(input[name=customer_reward_enable])").each(function() {
|
||||
value_count = $(this).val() == value ? value_count + 1 : value_count;
|
||||
});
|
||||
|
||||
88
app/Views/configs/shortcuts_config.php
Normal file
88
app/Views/configs/shortcuts_config.php
Normal file
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/**
|
||||
* @var array $config
|
||||
* @var array $keyboardShortcutOptions
|
||||
* @var array $keyboardShortcuts
|
||||
*/
|
||||
|
||||
$keyboardShortcuts ??= [];
|
||||
$keyboardShortcutOptions ??= [];
|
||||
$config ??= [];
|
||||
|
||||
$shortcutLabels = [
|
||||
'cancel' => lang('Sales.key_cancel'),
|
||||
'items' => lang('Sales.key_item_search'),
|
||||
'customers' => lang('Sales.key_customer_search'),
|
||||
'suspend' => lang('Sales.key_suspend'),
|
||||
'suspended' => lang('Sales.key_suspended'),
|
||||
'amount' => lang('Sales.key_tendered'),
|
||||
'payment' => lang('Sales.key_payment'),
|
||||
'complete' => lang('Sales.key_finish_sale'),
|
||||
'finish' => lang('Sales.key_finish_quote'),
|
||||
'help' => lang('Sales.key_help_modal')
|
||||
];
|
||||
?>
|
||||
|
||||
<?= form_open('config/saveShortcuts', ['id' => 'shortcuts_config_form', 'class' => 'form-horizontal']) ?>
|
||||
<div id="config_wrapper">
|
||||
<div class="row">
|
||||
<fieldset id="config_info">
|
||||
<div class="col-md-8">
|
||||
<div id="required_fields_message"><?= esc(lang('Common.fields_required_message')) ?></div>
|
||||
<ul id="shortcuts_error_message_box" class="error_message_box"></ul>
|
||||
|
||||
<?php foreach ($shortcutLabels as $name => $label): ?>
|
||||
<div class="form-group form-group-sm">
|
||||
<?= form_label($label, 'key_' . $name, ['class' => 'control-label col-xs-3']) ?>
|
||||
<div class="col-xs-4">
|
||||
<?php $keyboardShortcutSelectedValue = $keyboardShortcuts[$name]['value'] ?? ''; ?>
|
||||
<?= form_dropdown(
|
||||
'key_' . $name,
|
||||
$keyboardShortcutOptions,
|
||||
$keyboardShortcutSelectedValue,
|
||||
'class="form-control input-sm"'
|
||||
) ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<div class="col-xs-12 clearfix">
|
||||
<?= form_submit([
|
||||
'name' => 'submit_shortcuts',
|
||||
'id' => 'submit_shortcuts',
|
||||
'value' => lang('Common.submit'),
|
||||
'class' => 'btn btn-primary btn-sm pull-right'
|
||||
]) ?>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<?= form_close() ?>
|
||||
|
||||
<script type="text/javascript">
|
||||
$('#shortcuts_config_form').validate($.extend(form_support.handler, {
|
||||
submitHandler: function(form) {
|
||||
$(form).ajaxSubmit({
|
||||
success: function(response) {
|
||||
$.notify({
|
||||
message: response.message
|
||||
}, {
|
||||
type: response.success ? 'success' : 'danger'
|
||||
});
|
||||
},
|
||||
error: function(xhr) {
|
||||
const rawMessage = xhr.responseJSON?.message ?? xhr.responseText ?? <?= json_encode(lang('Config.shortcuts_save_error')) ?>;
|
||||
$.notify({
|
||||
message: DOMPurify.sanitize(rawMessage)
|
||||
}, {
|
||||
type: 'danger'
|
||||
});
|
||||
},
|
||||
dataType: 'json'
|
||||
});
|
||||
},
|
||||
|
||||
errorLabelContainer: '#shortcuts_error_message_box'
|
||||
}));
|
||||
</script>
|
||||
@@ -29,9 +29,9 @@
|
||||
<script type="text/javascript">
|
||||
// Validation and submit handling
|
||||
$(document).ready(function() {
|
||||
let location_count = <?= sizeof($stock_locations) ?>;
|
||||
var location_count = <?= sizeof($stock_locations) ?>;
|
||||
|
||||
const hide_show_remove = function() {
|
||||
var hide_show_remove = function() {
|
||||
if ($("input[name*='stock_location']:enabled").length > 1) {
|
||||
$(".remove_stock_location").show();
|
||||
} else {
|
||||
@@ -39,31 +39,31 @@
|
||||
}
|
||||
};
|
||||
|
||||
const add_stock_location = function() {
|
||||
const block = $(this).parent().clone(true);
|
||||
const new_block = block.insertAfter($(this).parent());
|
||||
const new_block_id = 'stock_location[]';
|
||||
var add_stock_location = function() {
|
||||
var block = $(this).parent().clone(true);
|
||||
var new_block = block.insertAfter($(this).parent());
|
||||
var new_block_id = 'stock_location[]';
|
||||
$(new_block).find('label').html("<?= lang('Config.stock_location') ?> " + ++location_count).attr('for', new_block_id).attr('class', 'control-label col-xs-2');
|
||||
$(new_block).find('input').attr('id', new_block_id).removeAttr('disabled').attr('name', new_block_id).attr('class', 'form-control input-sm').val('');
|
||||
hide_show_remove();
|
||||
};
|
||||
|
||||
const remove_stock_location = function() {
|
||||
var remove_stock_location = function() {
|
||||
$(this).parent().remove();
|
||||
hide_show_remove();
|
||||
};
|
||||
|
||||
const init_add_remove_locations = function() {
|
||||
var init_add_remove_locations = function() {
|
||||
$('.add_stock_location').click(add_stock_location);
|
||||
$('.remove_stock_location').click(remove_stock_location);
|
||||
hide_show_remove();
|
||||
};
|
||||
init_add_remove_locations();
|
||||
|
||||
const duplicate_found = false;
|
||||
var duplicate_found = false;
|
||||
// Run validator once for all fields
|
||||
$.validator.addMethod('stock_location', function(value, element) {
|
||||
let value_count = 0;
|
||||
var value_count = 0;
|
||||
$("input[name*='stock_location']").each(function() {
|
||||
value_count = $(this).val() == value ? value_count + 1 : value_count;
|
||||
});
|
||||
|
||||
@@ -25,8 +25,8 @@ use Config\OSPOS;
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-sm-2" style="text-align: left;"><br>
|
||||
<p style="min-height: 14.7em; font-weight: bold;">General Info</p>
|
||||
<p style="min-height: 10.5em; font-weight: bold;">User Setup</p><br>
|
||||
<p style="min-height: 17.7em; font-weight: bold;">General Info</p>
|
||||
<p style="min-height: 12.2em; font-weight: bold;">User Setup</p><br>
|
||||
<p style="font-weight: bold;">Permissions</p>
|
||||
</div>
|
||||
<div class="col-sm-8" id="issuetemplate" style="text-align: left;"><br>
|
||||
@@ -42,7 +42,7 @@ use Config\OSPOS;
|
||||
echo "» OpenSSL: ", extension_loaded('openssl') ? '<span style="color: green;">Enabled ✓</span>' : '<span style="color: red;">Disabled ✗</span>', '<br>';
|
||||
echo "» MBString: ", extension_loaded('mbstring') ? '<span style="color: green;">Enabled ✓</span>' : '<span style="color: red;">Disabled ✗</span>', '<br>';
|
||||
echo "» Curl: ", extension_loaded('curl') ? '<span style="color: green;">Enabled ✓</span>' : '<span style="color: red;">Disabled ✗</span>', '<br>';
|
||||
echo "» Json: ", extension_loaded('json') ? '<span style="color: green;">Enabled ✓</span>' : '<span style="color: red;">Disabled ✗</span>', '<br><br>';
|
||||
echo "» Json: ", extension_loaded('json') ? '<span style="color: green;">Enabled ✓</span>' : '<span style="color: red;">Disabled ✗</span>', '<br>';
|
||||
echo "» Xml: ", extension_loaded('xml') ? '<span style="color: green;">Enabled ✓</span>' : '<span style="color: red;">Disabled ✗</span>', '<br><br>';
|
||||
?>
|
||||
User Configuration:<br>
|
||||
@@ -198,7 +198,7 @@ use Config\OSPOS;
|
||||
<div style="text-align: center;">
|
||||
<a class="copy" data-clipboard-action="copy" data-clipboard-target="#issuetemplate">Copy Info</a> | <a href="https://github.com/opensourcepos/opensourcepos/issues/new" target="_blank"> <?= lang('Config.report_an_issue') ?></a>
|
||||
<script type="text/javascript">
|
||||
const clipboard = new ClipboardJS('.copy');
|
||||
var clipboard = new ClipboardJS('.copy');
|
||||
|
||||
clipboard.on('success', function(e) {
|
||||
document.getSelection().removeAllRanges();
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
// Validation and submit handling
|
||||
$(document).ready(function() {
|
||||
|
||||
const enable_disable_dinner_table_enable = (function() {
|
||||
const dinner_table_enable = $("#dinner_table_enable").is(":checked");
|
||||
var enable_disable_dinner_table_enable = (function() {
|
||||
var dinner_table_enable = $("#dinner_table_enable").is(":checked");
|
||||
$("input[name*='dinner_table']:not(input[name=dinner_table_enable])").prop("disabled", !dinner_table_enable);
|
||||
if (dinner_table_enable) {
|
||||
$(".add_dinner_table, .remove_dinner_table").show();
|
||||
@@ -56,9 +56,9 @@
|
||||
|
||||
$("#dinner_table_enable").change(enable_disable_dinner_table_enable);
|
||||
|
||||
let table_count = <?= sizeof($dinner_tables) ?>;
|
||||
var table_count = <?= sizeof($dinner_tables) ?>;
|
||||
|
||||
const hide_show_remove = function() {
|
||||
var hide_show_remove = function() {
|
||||
if ($("input[name*='dinner_tables']:enabled").length > 1) {
|
||||
$(".remove_dinner_tables").show();
|
||||
} else {
|
||||
@@ -66,23 +66,23 @@
|
||||
}
|
||||
};
|
||||
|
||||
const add_dinner_table = function() {
|
||||
let id = $(this).parent().find('input').attr('id');
|
||||
var add_dinner_table = function() {
|
||||
var id = $(this).parent().find('input').attr('id');
|
||||
id = id.replace(/.*?_(\d+)$/g, "$1");
|
||||
const block = $(this).parent().clone(true);
|
||||
const new_block = block.insertAfter($(this).parent());
|
||||
const new_block_id = 'dinner_table_' + ++id;
|
||||
var block = $(this).parent().clone(true);
|
||||
var new_block = block.insertAfter($(this).parent());
|
||||
var new_block_id = 'dinner_table_' + ++id;
|
||||
$(new_block).find('label').html("<?= lang('Config.dinner_table') ?> " + ++table_count).attr('for', new_block_id).attr('class', 'control-label col-xs-2');
|
||||
$(new_block).find('input').attr('id', new_block_id).removeAttr('disabled').attr('name', new_block_id).attr('class', 'form-control input-sm').val('');
|
||||
hide_show_remove();
|
||||
};
|
||||
|
||||
const remove_dinner_table = function() {
|
||||
var remove_dinner_table = function() {
|
||||
$(this).parent().remove();
|
||||
hide_show_remove();
|
||||
};
|
||||
|
||||
const init_add_remove_tables = function() {
|
||||
var init_add_remove_tables = function() {
|
||||
$('.add_dinner_table').click(add_dinner_table);
|
||||
$('.remove_dinner_table').click(remove_dinner_table);
|
||||
hide_show_remove();
|
||||
@@ -91,10 +91,10 @@
|
||||
};
|
||||
init_add_remove_tables();
|
||||
|
||||
const duplicate_found = false;
|
||||
var duplicate_found = false;
|
||||
// Run validator once for all fields
|
||||
$.validator.addMethod('dinner_table', function(value, element) {
|
||||
let value_count = 0;
|
||||
var value_count = 0;
|
||||
$("input[name*='dinner_table']:not(input[name=dinner_table_enable])").each(function() {
|
||||
value_count = $(this).val() == value ? value_count + 1 : value_count;
|
||||
});
|
||||
|
||||
@@ -51,6 +51,10 @@
|
||||
</div>
|
||||
<div class="col-xs-1 input-group">
|
||||
<?= form_input([
|
||||
'type' => 'number',
|
||||
'step' => 'any',
|
||||
'min' => '0',
|
||||
'max' => '100',
|
||||
'name' => 'default_tax_1_rate',
|
||||
'id' => 'default_tax_1_rate',
|
||||
'class' => 'form-control input-sm',
|
||||
@@ -72,6 +76,10 @@
|
||||
</div>
|
||||
<div class="col-xs-1 input-group">
|
||||
<?= form_input([
|
||||
'type' => 'number',
|
||||
'step' => 'any',
|
||||
'min' => '0',
|
||||
'max' => '100',
|
||||
'name' => 'default_tax_2_rate',
|
||||
'id' => 'default_tax_2_rate',
|
||||
'class' => 'form-control input-sm',
|
||||
@@ -143,8 +151,8 @@
|
||||
<script type="text/javascript">
|
||||
// Validation and submit handling
|
||||
$(document).ready(function() {
|
||||
const enable_disable_use_destination_based_tax = (function() {
|
||||
const use_destination_based_tax = $("#use_destination_based_tax").is(":checked");
|
||||
var enable_disable_use_destination_based_tax = (function() {
|
||||
var use_destination_based_tax = $("#use_destination_based_tax").is(":checked");
|
||||
$("select[name='default_tax_code']").prop("disabled", !use_destination_based_tax);
|
||||
$("select[name='default_tax_category']").prop("disabled", !use_destination_based_tax);
|
||||
$("select[name='default_tax_jurisdiction']").prop("disabled", !use_destination_based_tax);
|
||||
|
||||
@@ -453,7 +453,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
const fill_value = function(event, ui) {
|
||||
var fill_value = function(event, ui) {
|
||||
event.preventDefault();
|
||||
$("input[name='sales_tax_code_id']").val(ui.item.value);
|
||||
$("input[name='sales_tax_code_name']").val(ui.item.label);
|
||||
|
||||
@@ -167,10 +167,10 @@
|
||||
});
|
||||
|
||||
$.validator.addMethod('module', function(value, element) {
|
||||
let result = $('#permission_list input').is(':checked');
|
||||
var result = $('#permission_list input').is(':checked');
|
||||
$('.module').each(function(index, element) {
|
||||
const parent = $(element);
|
||||
const checked = $(element).is(':checked');
|
||||
var parent = $(element).parent();
|
||||
var checked = $(element).is(':checked');
|
||||
if ($('ul', parent).length > 0 && result) {
|
||||
result &= !checked || (checked && $('ul > li > input:checked', parent).length > 0);
|
||||
}
|
||||
@@ -179,10 +179,10 @@
|
||||
}, "<?= lang('Employees.subpermission_required') ?>");
|
||||
|
||||
$('ul#permission_list > li > input.module').each(function() {
|
||||
const $this = $(this);
|
||||
var $this = $(this);
|
||||
$('ul > li > input,select', $this.parent()).each(function() {
|
||||
const $that = $(this);
|
||||
const updateInputs = function(checked) {
|
||||
var $that = $(this);
|
||||
var updateInputs = function(checked) {
|
||||
$that.prop('disabled', !checked);
|
||||
!checked && $that.prop('checked', false);
|
||||
}
|
||||
|
||||
@@ -101,9 +101,11 @@ p.lead {
|
||||
}
|
||||
|
||||
.tabs {
|
||||
list-style: none inside none;
|
||||
list-style: none;
|
||||
list-style-position: inside;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin: 0 0 -1px;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
.tabs li {
|
||||
display: inline;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
const tabLinks = new Array();
|
||||
const contentDivs = new Array();
|
||||
var tabLinks = new Array();
|
||||
var contentDivs = new Array();
|
||||
|
||||
function init()
|
||||
{
|
||||
// Grab the tab links and content divs from the page
|
||||
const tabListItems = document.getElementById('tabs').childNodes;
|
||||
var tabListItems = document.getElementById('tabs').childNodes;
|
||||
console.log(tabListItems);
|
||||
for (let i = 0; i < tabListItems.length; i ++)
|
||||
for (var i = 0; i < tabListItems.length; i ++)
|
||||
{
|
||||
if (tabListItems[i].nodeName == "LI")
|
||||
{
|
||||
const tabLink = getFirstChildWithTagName(tabListItems[i], 'A');
|
||||
const id = getHash(tabLink.getAttribute('href'));
|
||||
var tabLink = getFirstChildWithTagName(tabListItems[i], 'A');
|
||||
var id = getHash(tabLink.getAttribute('href'));
|
||||
tabLinks[id] = tabLink;
|
||||
contentDivs[id] = document.getElementById(id);
|
||||
}
|
||||
@@ -19,9 +19,9 @@ function init()
|
||||
|
||||
// Assign onclick events to the tab links, and
|
||||
// highlight the first tab
|
||||
let i = 0;
|
||||
var i = 0;
|
||||
|
||||
for (const id in tabLinks)
|
||||
for (var id in tabLinks)
|
||||
{
|
||||
tabLinks[id].onclick = showTab;
|
||||
tabLinks[id].onfocus = function () {
|
||||
@@ -35,26 +35,26 @@ function init()
|
||||
}
|
||||
|
||||
// Hide all content divs except the first
|
||||
let j = 0;
|
||||
var i = 0;
|
||||
|
||||
for (const id in contentDivs)
|
||||
for (var id in contentDivs)
|
||||
{
|
||||
if (j != 0)
|
||||
if (i != 0)
|
||||
{
|
||||
console.log(contentDivs[id]);
|
||||
contentDivs[id].className = 'content hide';
|
||||
}
|
||||
j ++;
|
||||
i ++;
|
||||
}
|
||||
}
|
||||
|
||||
function showTab()
|
||||
{
|
||||
const selectedId = getHash(this.getAttribute('href'));
|
||||
var selectedId = getHash(this.getAttribute('href'));
|
||||
|
||||
// Highlight the selected tab, and dim all others.
|
||||
// Also show the selected content div, and hide all others.
|
||||
for (const id in contentDivs)
|
||||
for (var id in contentDivs)
|
||||
{
|
||||
if (id == selectedId)
|
||||
{
|
||||
@@ -74,7 +74,7 @@ function showTab()
|
||||
|
||||
function getFirstChildWithTagName(element, tagName)
|
||||
{
|
||||
for (let i = 0; i < element.childNodes.length; i ++)
|
||||
for (var i = 0; i < element.childNodes.length; i ++)
|
||||
{
|
||||
if (element.childNodes[i].nodeName == tagName)
|
||||
{
|
||||
@@ -85,29 +85,28 @@ function getFirstChildWithTagName(element, tagName)
|
||||
|
||||
function getHash(url)
|
||||
{
|
||||
const hashPos = url.lastIndexOf('#');
|
||||
var hashPos = url.lastIndexOf('#');
|
||||
return url.substring(hashPos + 1);
|
||||
}
|
||||
|
||||
function toggle(elem)
|
||||
{
|
||||
elem = document.getElementById(elem);
|
||||
let disp;
|
||||
|
||||
if (elem.style && elem.style['display'])
|
||||
{
|
||||
// Only works with the "style" attr
|
||||
disp = elem.style['display'];
|
||||
var disp = elem.style['display'];
|
||||
}
|
||||
else if (elem.currentStyle)
|
||||
{
|
||||
// For MSIE, naturally
|
||||
disp = elem.currentStyle['display'];
|
||||
var disp = elem.currentStyle['display'];
|
||||
}
|
||||
else if (window.getComputedStyle)
|
||||
{
|
||||
// For most other browsers
|
||||
disp = document.defaultView.getComputedStyle(elem, null).getPropertyValue('display');
|
||||
var disp = document.defaultView.getComputedStyle(elem, null).getPropertyValue('display');
|
||||
}
|
||||
|
||||
// Toggle the state of the "display" style
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
!$(this).val() && $(this).val('');
|
||||
});
|
||||
|
||||
const fill_value = function(event, ui) {
|
||||
var fill_value = function(event, ui) {
|
||||
event.preventDefault();
|
||||
$(this).val((ui.item ? ui.item.label : ""));
|
||||
$("input[name='person_id']").val(ui.item.value);
|
||||
|
||||
@@ -235,7 +235,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
const fill_value = function(event, ui) {
|
||||
var fill_value = function(event, ui) {
|
||||
event.preventDefault();
|
||||
$("input[name='kit_item_id']").val(ui.item.value);
|
||||
$("input[name='item_name']").val(DOMPurify.sanitize(ui.item.label));
|
||||
|
||||
@@ -467,7 +467,7 @@
|
||||
!$(this).val() && $(this).val('');
|
||||
});
|
||||
|
||||
const fill_tax_category_value = function(event, ui) {
|
||||
var fill_tax_category_value = function(event, ui) {
|
||||
event.preventDefault();
|
||||
$("input[name='tax_category_id']").val(ui.item.value);
|
||||
$("input[name='tax_category']").val(ui.item.label);
|
||||
@@ -483,7 +483,7 @@
|
||||
focus: fill_tax_category_value
|
||||
});
|
||||
|
||||
const fill_low_sell_value = function(event, ui) {
|
||||
var fill_low_sell_value = function(event, ui) {
|
||||
event.preventDefault();
|
||||
$("input[name='low_sell_item_id']").val(ui.item.value);
|
||||
$("input[name='low_sell_item_name']").val(ui.item.label);
|
||||
@@ -517,7 +517,7 @@
|
||||
return value.match(/(\||_)/g) == null;
|
||||
}, "<?= lang('Attributes.attribute_value_invalid_chars') ?>");
|
||||
|
||||
const init_validation = function() {
|
||||
var init_validation = function() {
|
||||
$('#item_form').validate($.extend({
|
||||
submitHandler: function(form, event) { // Event is not used as a parameter here
|
||||
$(form).ajaxSubmit({
|
||||
|
||||
@@ -178,10 +178,10 @@
|
||||
delay: 10
|
||||
});
|
||||
|
||||
let confirm_message = false;
|
||||
var confirm_message = false;
|
||||
$('#tax_percent_name_2, #tax_name_2').prop('disabled', true),
|
||||
$('#tax_percent_name_1, #tax_name_1').blur(function() {
|
||||
const disabled = !($('#tax_percent_name_1').val() + $('#tax_name_1').val());
|
||||
var disabled = !($('#tax_percent_name_1').val() + $('#tax_name_1').val());
|
||||
$('#tax_percent_name_2, #tax_name_2').prop('disabled', disabled);
|
||||
confirm_message = disabled ? '' : "<?= lang('Items.confirm_bulk_edit_wipe_taxes') ?>";
|
||||
});
|
||||
|
||||
@@ -115,27 +115,27 @@ use App\Models\Inventory;
|
||||
});
|
||||
|
||||
function display_stock(location_id) {
|
||||
const item_quantities = <?= json_encode(esc($item_quantities, 'raw')) ?>;
|
||||
var item_quantities = <?= json_encode(esc($item_quantities, 'raw')) ?>;
|
||||
document.getElementById("quantity").value = parseFloat(item_quantities[location_id]).toFixed(<?= quantity_decimals() ?>);
|
||||
|
||||
const inventory_data = <?= json_encode(esc($inventory_array, 'raw')) ?>;
|
||||
const employee_data = <?= json_encode(esc($employee_name, 'raw')) ?>;
|
||||
var inventory_data = <?= json_encode(esc($inventory_array, 'raw')) ?>;
|
||||
var employee_data = <?= json_encode(esc($employee_name, 'raw')) ?>;
|
||||
|
||||
const table = document.getElementById("inventory_result");
|
||||
var table = document.getElementById("inventory_result");
|
||||
|
||||
// Remove old query from tbody
|
||||
const rowCount = table.rows.length;
|
||||
for (let index = rowCount; index > 0; index--) {
|
||||
var rowCount = table.rows.length;
|
||||
for (var index = rowCount; index > 0; index--) {
|
||||
table.deleteRow(index - 1);
|
||||
}
|
||||
|
||||
// Add new query to tbody
|
||||
for (let index = 0; index < inventory_data.length; index++) {
|
||||
const data = inventory_data[index];
|
||||
for (var index = 0; index < inventory_data.length; index++) {
|
||||
var data = inventory_data[index];
|
||||
if (data['trans_location'] == location_id) {
|
||||
const tr = document.createElement('tr');
|
||||
var tr = document.createElement('tr');
|
||||
|
||||
let td = document.createElement('td');
|
||||
var td = document.createElement('td');
|
||||
td.appendChild(document.createTextNode(data['trans_date']));
|
||||
tr.appendChild(td);
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
});
|
||||
|
||||
function fill_quantity(val) {
|
||||
const item_quantities = <?= json_encode(esc($item_quantities, 'raw')) ?>;
|
||||
var item_quantities = <?= json_encode(esc($item_quantities, 'raw')) ?>;
|
||||
document.getElementById('quantity').value = parseFloat(item_quantities[val]).toFixed(<?= quantity_decimals() ?>);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -30,7 +30,7 @@ use App\Models\Employee;
|
||||
// Set the beginning of time as starting date
|
||||
$('#daterangepicker').data('daterangepicker').setStartDate("<?= date($config['dateformat'], mktime(0, 0, 0, 01, 01, 2010)) ?>");
|
||||
// Update the hidden inputs with the selected dates before submitting the search data
|
||||
start_date = "<?= date('Y-m-d', mktime(0, 0, 0, 01, 01, 2010)) ?>";
|
||||
var start_date = "<?= date('Y-m-d', mktime(0, 0, 0, 01, 01, 2010)) ?>";
|
||||
|
||||
// Override dates from server if provided
|
||||
<?php if (isset($start_date) && $start_date): ?>
|
||||
|
||||
@@ -5,9 +5,14 @@
|
||||
* @var bool $is_new_install
|
||||
* @var string $latest_version
|
||||
* @var bool $gcaptcha_enabled
|
||||
* @var CodeIgniter\HTTP\IncomingRequest $request
|
||||
* @var array $config
|
||||
* @var $validation
|
||||
*/
|
||||
|
||||
use Config\Services;
|
||||
|
||||
$request = Services::request();
|
||||
?>
|
||||
|
||||
<!doctype html>
|
||||
@@ -154,11 +159,6 @@
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<?php
|
||||
use Config\Services;
|
||||
$request = Services::request();
|
||||
?>
|
||||
|
||||
<?php if (ENVIRONMENT == 'development' || get_cookie('debug') == 'true' || $request->getGet('debug') == 'true') : ?>
|
||||
<!-- inject:login:debug:js -->
|
||||
<!-- endinject -->
|
||||
|
||||
@@ -3,7 +3,7 @@ use Config\OSPOS;
|
||||
|
||||
$config = config(OSPOS::class)->settings; ?>
|
||||
|
||||
const pickerconfig = function(config) {
|
||||
var pickerconfig = function(config) {
|
||||
return $.extend({
|
||||
format: "<?= $this->data["format"] ?? dateformat_bootstrap($config['dateformat']) . ' ' . dateformat_bootstrap($config['timeformat'])?>",
|
||||
<?php
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
<?php if (empty($config['date_or_time_format'])) { ?>
|
||||
$('#daterangepicker').css("width", "180");
|
||||
let start_date = "<?= date('Y-m-d') ?>";
|
||||
let end_date = "<?= date('Y-m-d') ?>";
|
||||
var start_date = "<?= date('Y-m-d') ?>";
|
||||
var end_date = "<?= date('Y-m-d') ?>";
|
||||
|
||||
$('#daterangepicker').daterangepicker({
|
||||
"ranges": {
|
||||
@@ -112,8 +112,8 @@
|
||||
});
|
||||
<?php } else { ?>
|
||||
$('#daterangepicker').css("width", "305");
|
||||
let start_date = "<?= date('Y-m-d H:i:s', mktime(0, 0, 0, date("m"), date("d"), date("Y"))) ?>";
|
||||
let end_date = "<?= date('Y-m-d H:i:s', mktime(23, 59, 59, date("m"), date("d"), date("Y"))) ?>";
|
||||
var start_date = "<?= date('Y-m-d H:i:s', mktime(0, 0, 0, date("m"), date("d"), date("Y"))) ?>";
|
||||
var end_date = "<?= date('Y-m-d H:i:s', mktime(23, 59, 59, date("m"), date("d"), date("Y"))) ?>";
|
||||
$('#daterangepicker').daterangepicker({
|
||||
"ranges": {
|
||||
"<?= lang('Datepicker.today') ?>": [
|
||||
|
||||
@@ -12,14 +12,16 @@ $request = Services::request();
|
||||
?>
|
||||
|
||||
<!doctype html>
|
||||
<html lang="<?= $request->getLocale() ?>">
|
||||
<html lang="<?= current_language_code() ?>">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<base href="<?= base_url() ?>">
|
||||
<title><?= esc($config['company']) . ' | ' . lang('Common.powered_by') . ' OSPOS ' . esc(config('App')->application_version) ?></title>
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<link rel="shortcut icon" type="image/x-icon" href="images/favicon.ico">
|
||||
<link rel="stylesheet" href="<?= 'resources/bootswatch/' . (empty($config['theme']) ? 'flatly' : esc($config['theme'])) . '/bootstrap.min.css' ?>">
|
||||
<?php $theme = (empty($config['theme']) ? 'flatly' : esc($config['theme'])); ?>
|
||||
<link rel="stylesheet" href="resources/bootswatch/<?= "$theme" ?>/bootstrap.min.css">
|
||||
|
||||
<?php if (ENVIRONMENT == 'development' || get_cookie('debug') == 'true' || $request->getGet('debug') == 'true') : ?>
|
||||
<!-- inject:debug:css -->
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
|
||||
<script type="text/javascript">
|
||||
// Live clock
|
||||
const clock_tick = function clock_tick() {
|
||||
var clock_tick = function clock_tick() {
|
||||
setInterval('update_clock();', 1000);
|
||||
}
|
||||
|
||||
// Start the clock immediately
|
||||
clock_tick();
|
||||
|
||||
const update_clock = function update_clock() {
|
||||
var update_clock = function update_clock() {
|
||||
document.getElementById('liveclock').innerHTML = moment().format("<?= dateformat_momentjs($config['dateformat'] . ' ' . $config['timeformat']) ?>");
|
||||
}
|
||||
|
||||
@@ -32,11 +32,11 @@
|
||||
}
|
||||
});
|
||||
|
||||
const csrf_token = function() {
|
||||
return "<?= esc(csrf_hash(), 'js') ?>";
|
||||
var csrf_token = function() {
|
||||
return "<?= csrf_hash() ?>";
|
||||
};
|
||||
|
||||
const csrf_form_base = function() {
|
||||
var csrf_form_base = function() {
|
||||
return {
|
||||
<?= esc(config('Security')->tokenName, 'js') ?>: function() {
|
||||
return csrf_token()
|
||||
@@ -44,14 +44,14 @@
|
||||
}
|
||||
};
|
||||
|
||||
const setup_csrf_token = function() {
|
||||
var setup_csrf_token = function() {
|
||||
$('input[name="<?= esc(config('Security')->tokenName, 'js') ?>"]').val(csrf_token());
|
||||
};
|
||||
|
||||
const ajax = $.ajax;
|
||||
var ajax = $.ajax;
|
||||
|
||||
$.ajax = function() {
|
||||
let args = arguments[0];
|
||||
var args = arguments[0];
|
||||
if (args['type'] && args['type'].toLowerCase() == 'post' && csrf_token()) {
|
||||
if (typeof args['data'] === 'string') {
|
||||
args['data'] += '&' + $.param(csrf_form_base());
|
||||
@@ -80,7 +80,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
const submit = $.fn.submit;
|
||||
var submit = $.fn.submit;
|
||||
|
||||
$.fn.submit = function() {
|
||||
setup_csrf_token();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script type="text/javascript">
|
||||
(function(lang, $) {
|
||||
|
||||
const lines = {
|
||||
var lines = {
|
||||
'common_submit': "<?= lang('Common.submit') ?>",
|
||||
'common_close': "<?= lang('Common.close') ?>"
|
||||
};
|
||||
|
||||
@@ -29,11 +29,11 @@
|
||||
jsPrintSetup.setOption('footerStrRight', '');
|
||||
<?php } ?>
|
||||
|
||||
const printers = jsPrintSetup.getPrintersList().split(',');
|
||||
var printers = jsPrintSetup.getPrintersList().split(',');
|
||||
// Get right printer here..
|
||||
for (const index in printers) {
|
||||
const default_ticket_printer = window.localStorage && localStorage['<?= esc($selected_printer, 'js') ?>'];
|
||||
const selected_printer = printers[index];
|
||||
for (var index in printers) {
|
||||
var default_ticket_printer = window.localStorage && localStorage['<?= esc($selected_printer, 'js') ?>'];
|
||||
var selected_printer = printers[index];
|
||||
if (selected_printer == default_ticket_printer) {
|
||||
// Select Epson label printer
|
||||
jsPrintSetup.setPrinter(selected_printer);
|
||||
|
||||
@@ -18,11 +18,11 @@ $filter_select_id = $options['filter_select_id'] ?? 'filters';
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
const additional_params = <?= json_encode($additional_params) ?>;
|
||||
const filter_select_id = '<?= esc($filter_select_id) ?>';
|
||||
var additional_params = <?= json_encode($additional_params) ?>;
|
||||
var filter_select_id = '<?= esc($filter_select_id) ?>';
|
||||
|
||||
function update_url() {
|
||||
const params = new URLSearchParams();
|
||||
var params = new URLSearchParams();
|
||||
|
||||
// Add dates
|
||||
if (typeof start_date !== 'undefined') {
|
||||
@@ -33,7 +33,7 @@ $filter_select_id = $options['filter_select_id'] ?? 'filters';
|
||||
}
|
||||
|
||||
// Add filters
|
||||
const filters = $('#' + filter_select_id).val();
|
||||
var filters = $('#' + filter_select_id).val();
|
||||
if (filters) {
|
||||
filters.forEach(function(filter) {
|
||||
params.append('filters[]', filter);
|
||||
@@ -42,9 +42,9 @@ $filter_select_id = $options['filter_select_id'] ?? 'filters';
|
||||
|
||||
// Add additional params
|
||||
additional_params.forEach(function(param) {
|
||||
const element = $('#' + param);
|
||||
var element = $('#' + param);
|
||||
if (element.length) {
|
||||
const value = element.val();
|
||||
var value = element.val();
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
value.forEach(function(v) {
|
||||
params.append(param + '[]', v);
|
||||
@@ -56,8 +56,8 @@ $filter_select_id = $options['filter_select_id'] ?? 'filters';
|
||||
});
|
||||
|
||||
// Update URL without page reload
|
||||
const new_url = window.location.pathname;
|
||||
const params_str = params.toString();
|
||||
var new_url = window.location.pathname;
|
||||
var params_str = params.toString();
|
||||
if (params_str) {
|
||||
new_url += '?' + params_str;
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ function safeRemoveItem(key) {
|
||||
}
|
||||
|
||||
// Load saved column visibility from localStorage
|
||||
const savedVisibility = JSON.parse(safeGetItem('columnVisibility')) || { cost: false, profit: false };
|
||||
let visibleColumns = savedVisibility;
|
||||
var savedVisibility = JSON.parse(safeGetItem('columnVisibility')) || { cost: false, profit: false };
|
||||
var visibleColumns = savedVisibility;
|
||||
|
||||
// Function to save column visibility to localStorage
|
||||
function saveColumnVisibility(visibility) {
|
||||
@@ -56,13 +56,13 @@ $('#table').bootstrapTable('refreshOptions', {
|
||||
});
|
||||
|
||||
// Initialize visibility settings from localStorage
|
||||
let summaryVisibility = JSON.parse(safeGetItem('summaryVisibility')) || { cost: false, profit: false };
|
||||
var summaryVisibility = JSON.parse(safeGetItem('summaryVisibility')) || { cost: false, profit: false };
|
||||
|
||||
// Function to apply visibility for cost and profit rows
|
||||
function applySummaryVisibility() {
|
||||
const rows = $('#report_summary .summary_row');
|
||||
const costRow = rows.eq(rows.length - 2); // Second-to-last row
|
||||
const profitRow = rows.eq(rows.length - 1); // Last row
|
||||
var rows = $('#report_summary .summary_row');
|
||||
var costRow = rows.eq(rows.length - 2); // Second-to-last row
|
||||
var profitRow = rows.eq(rows.length - 1); // Last row
|
||||
|
||||
if (summaryVisibility.cost === false) {
|
||||
costRow.hide(); // Hide the cost row
|
||||
@@ -90,7 +90,7 @@ $('#toggleCostProfitButton').click(function () {
|
||||
applySummaryVisibility();
|
||||
|
||||
// Initialize dialog (if editable)
|
||||
const init_dialog = function () {
|
||||
var init_dialog = function () {
|
||||
<?php if (isset($editable)): ?>
|
||||
table_support.submit_handler('<?php echo site_url("reports/get_detailed_{$editable}_row") ?>');
|
||||
dialog_support.init("a.modal-dlg");
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
pageSize: <?= $config['lines_per_page'] ?>,
|
||||
uniqueId: 'people.person_id',
|
||||
enableActions: function() {
|
||||
const email_disabled = $("td input:checkbox:checked").parents("tr").find("td a[href^='mailto:']").length == 0;
|
||||
var email_disabled = $("td input:checkbox:checked").parents("tr").find("td a[href^='mailto:']").length == 0;
|
||||
$("#email").prop('disabled', email_disabled);
|
||||
}
|
||||
});
|
||||
|
||||
$("#email").click(function(event) {
|
||||
const recipients = $.map($("tr.selected a[href^='mailto:']"), function(element) {
|
||||
var recipients = $.map($("tr.selected a[href^='mailto:']"), function(element) {
|
||||
return $(element).attr('href').replace(/^mailto:/, '');
|
||||
});
|
||||
location.href = "mailto:" + recipients.join(",");
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
|
||||
$('#datetime').datetimepicker(pickerconfig);
|
||||
|
||||
const fill_value = function(event, ui) {
|
||||
var fill_value = function(event, ui) {
|
||||
event.preventDefault();
|
||||
$("input[name='supplier_id']").val(ui.item.value);
|
||||
$("input[name='supplier_name']").val(ui.item.label);
|
||||
|
||||
@@ -533,7 +533,7 @@ if (isset($success)) {
|
||||
});
|
||||
|
||||
$('[name="discount_toggle"]').change(function() {
|
||||
const input = $("<input>").attr("type", "hidden").attr("name", "discount_type").val(($(this).prop('checked')) ? 1 : 0);
|
||||
var input = $("<input>").attr("type", "hidden").attr("name", "discount_type").val(($(this).prop('checked')) ? 1 : 0);
|
||||
$('#cart_' + $(this).attr('data-line')).append($(input));
|
||||
$('#cart_' + $(this).attr('data-line')).submit();
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<script type="text/javascript">
|
||||
// Labels and data series
|
||||
const data = {
|
||||
var data = {
|
||||
labels: <?= esc(json_encode($labels_1), 'js') ?>,
|
||||
series: [{
|
||||
name: '<?= esc($yaxis_title, 'js') ?>',
|
||||
@@ -20,7 +20,7 @@
|
||||
};
|
||||
|
||||
// We are setting a few options for our chart and override the defaults
|
||||
const options = {
|
||||
var options = {
|
||||
|
||||
// Specify a fixed width for the chart as a string (i.e. '100px' or '50%')
|
||||
width: '100%',
|
||||
@@ -98,7 +98,7 @@
|
||||
]
|
||||
};
|
||||
|
||||
const responsiveOptions = [
|
||||
var responsiveOptions = [
|
||||
['screen and (min-width: 640px)', {
|
||||
height: '80%',
|
||||
chartPadding: {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<script type="text/javascript">
|
||||
// Labels and data series
|
||||
const data = {
|
||||
var data = {
|
||||
labels: <?= json_encode(esc($labels_1, 'js')) ?>,
|
||||
series: [{
|
||||
name: '<?= esc($yaxis_title, 'js') ?>',
|
||||
@@ -20,7 +20,7 @@
|
||||
};
|
||||
|
||||
// We are setting a few options for our chart and override the defaults
|
||||
const options = {
|
||||
var options = {
|
||||
|
||||
// Specify a fixed width for the chart as a string (i.e. '100px' or '50%')
|
||||
width: '100%',
|
||||
@@ -101,7 +101,7 @@
|
||||
]
|
||||
};
|
||||
|
||||
const responsiveOptions = [
|
||||
var responsiveOptions = [
|
||||
['screen and (min-width: 640px)', {
|
||||
height: '80%',
|
||||
chartPadding: {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<script type="text/javascript">
|
||||
// Labels and data series
|
||||
const data = {
|
||||
var data = {
|
||||
labels: <?= json_encode(esc($labels_1, 'js')) ?>,
|
||||
series: [{
|
||||
name: '<?= esc($yaxis_title, 'js') ?>',
|
||||
@@ -20,7 +20,7 @@
|
||||
};
|
||||
|
||||
// We are setting a few options for our chart and override the defaults
|
||||
const options = {
|
||||
var options = {
|
||||
|
||||
// Specify a fixed width for the chart as a string (i.e. '100px' or '50%')
|
||||
width: '100%',
|
||||
@@ -150,7 +150,7 @@
|
||||
]
|
||||
};
|
||||
|
||||
const responsiveOptions = [
|
||||
var responsiveOptions = [
|
||||
['screen and (min-width: 640px)', {
|
||||
height: '80%',
|
||||
chartPadding: {
|
||||
@@ -172,7 +172,7 @@
|
||||
// If the draw event was triggered from drawing a point on the line chart
|
||||
if (data.type === 'point') {
|
||||
// We are creating a new path SVG element that draws a triangle around the point coordinates
|
||||
const circle = new Chartist.Svg('circle', {
|
||||
var circle = new Chartist.Svg('circle', {
|
||||
cx: [data.x],
|
||||
cy: [data.y],
|
||||
r: [5],
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
|
||||
<script type="text/javascript">
|
||||
// Labels and data series
|
||||
const data = {
|
||||
var data = {
|
||||
labels: <?= json_encode(esc($labels_1, 'js')) ?>,
|
||||
series: <?= json_encode(esc($series_data_1, 'js')) ?>
|
||||
};
|
||||
|
||||
// We are setting a few options for our chart and override the defaults
|
||||
const options = {
|
||||
var options = {
|
||||
|
||||
// Specify a fixed width for the chart as a string (i.e. '100px' or '50%')
|
||||
width: '100%',
|
||||
@@ -53,7 +53,7 @@
|
||||
})
|
||||
] };
|
||||
|
||||
const responsiveOptions = [
|
||||
var responsiveOptions = [
|
||||
['screen and (min-width: 640px)', {
|
||||
height: '80%',
|
||||
chartPadding: 20
|
||||
@@ -72,9 +72,9 @@
|
||||
// Generate random colours for the pie sliced because Chartist is currently limited to 15 colours
|
||||
chart.on('draw', function(data) {
|
||||
if (data.type === 'slice') {
|
||||
const r = Math.floor(Math.random() * 256);
|
||||
const g = Math.floor(Math.random() * 256);
|
||||
const b = Math.floor(Math.random() * 256);
|
||||
var r = Math.floor(Math.random() * 256);
|
||||
var g = Math.floor(Math.random() * 256);
|
||||
var b = Math.floor(Math.random() * 256);
|
||||
|
||||
data.element.attr({
|
||||
style: 'fill: #' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)
|
||||
|
||||
@@ -87,7 +87,7 @@ if (isset($error)) {
|
||||
<?= view('partial/daterangepicker') ?>
|
||||
|
||||
$("#generate_report").click(function() {
|
||||
let specific_input_data = $('#specific_input_data').val();
|
||||
var specific_input_data = $('#specific_input_data').val();
|
||||
if (!$(".discount_percent").is(":visible")) {
|
||||
specific_input_data = $('#discount_fixed').val();
|
||||
}
|
||||
@@ -97,7 +97,7 @@ if (isset($error)) {
|
||||
});
|
||||
|
||||
function check_discount_type() {
|
||||
const discount_type = $("#discount_type_id").val();
|
||||
var discount_type = $("#discount_type_id").val();
|
||||
|
||||
if (discount_type == 1) {
|
||||
$(".discount_percent").hide();
|
||||
|
||||
@@ -39,13 +39,13 @@
|
||||
$(document).ready(function () {
|
||||
<?= view('partial/bootstrap_tables_locale') ?>
|
||||
|
||||
const details_data = <?= json_encode(esc($details_data)) ?>;
|
||||
var details_data = <?= json_encode(esc($details_data)) ?>;
|
||||
<?php if ($config['customer_reward_enable'] && !empty($details_data_rewards)) { ?>
|
||||
const details_data_rewards = <?= json_encode(esc($details_data_rewards)) ?>;
|
||||
var details_data_rewards = <?= json_encode(esc($details_data_rewards)) ?>;
|
||||
<?php } ?>
|
||||
<?= view('partial/visibility_js') ?>
|
||||
|
||||
const init_dialog = function () {
|
||||
var init_dialog = function () {
|
||||
<?php if (isset($editable)) { ?>
|
||||
table_support.submit_handler('<?= esc(site_url("reports/get_detailed_$editable" . '_row')) ?>');
|
||||
dialog_support.init("a.modal-dlg");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user