mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-05-25 08:44:42 -04:00
Compare commits
32 Commits
refactor-4
...
WebShells-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19184b50c6 | ||
|
|
1a6cfffc27 | ||
|
|
5e0541c53e | ||
|
|
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
|
||||
use CodeIgniter\Router\RouteCollection;
|
||||
|
||||
@@ -12,6 +12,40 @@ $routes->get('login', 'Login::index');
|
||||
$routes->post('login', 'Login::index');
|
||||
$routes->post('migrate', 'Login::migrate');
|
||||
|
||||
$routes->get('sales', 'Sales::getIndex');
|
||||
$routes->get('sales/customerDisplay', 'Sales::getCustomerDisplay');
|
||||
$routes->get('sales/itemSearch', 'Sales::getItemSearch');
|
||||
$routes->post('sales/selectCustomer', 'Sales::postSelectCustomer');
|
||||
$routes->post('sales/changeMode', 'Sales::postChangeMode');
|
||||
$routes->post('sales/setComment', 'Sales::postSetComment');
|
||||
$routes->post('sales/setInvoiceNumber', 'Sales::postSetInvoiceNumber');
|
||||
$routes->post('sales/setPaymentType', 'Sales::postSetPaymentType');
|
||||
$routes->post('sales/setPrintAfterSale', 'Sales::postSetPrintAfterSale');
|
||||
$routes->post('sales/setPriceWorkOrders', 'Sales::postSetPriceWorkOrders');
|
||||
$routes->post('sales/setEmailReceipt', 'Sales::postSetEmailReceipt');
|
||||
$routes->post('sales/addPayment', 'Sales::postAddPayment');
|
||||
$routes->post('sales/add', 'Sales::postAdd');
|
||||
$routes->post('sales/editItem/(:segment)', 'Sales::postEditItem/$1');
|
||||
$routes->post('sales/deleteItem/(:segment)', 'Sales::getDeleteItem/$1');
|
||||
$routes->post('sales/deletePayment/(:segment)', 'Sales::getDeletePayment/$1');
|
||||
$routes->post('sales/removeCustomer', 'Sales::getRemoveCustomer');
|
||||
$routes->post('sales/complete', 'Sales::postComplete');
|
||||
$routes->post('sales/cancel', 'Sales::postCancel');
|
||||
$routes->post('sales/suspend', 'Sales::postSuspend');
|
||||
$routes->post('sales/unsuspend', 'Sales::postUnsuspend');
|
||||
$routes->post('sales/checkInvoiceNumber', 'Sales::postCheckInvoiceNumber');
|
||||
$routes->post('sales/changeItemNumber', 'Sales::postChangeItemNumber');
|
||||
$routes->post('sales/changeItemName', 'Sales::postChangeItemName');
|
||||
$routes->post('sales/changeItemDescription', 'Sales::postChangeItemDescription');
|
||||
$routes->get('sales/suspended', 'Sales::getSuspended');
|
||||
$routes->get('sales/discardSuspendedSale', 'Sales::getDiscardSuspendedSale');
|
||||
$routes->get('sales/sales_keyboard_help', 'Sales::getSalesKeyboardHelp');
|
||||
$routes->get('sales/receipt/(:num)', 'Sales::getReceipt/$1');
|
||||
$routes->get('sales/invoice/(:num)', 'Sales::getInvoice/$1');
|
||||
$routes->get('sales/edit/(:num)', 'Sales::getEdit/$1');
|
||||
$routes->post('sales/delete/(:num)', 'Sales::postDelete/$1');
|
||||
$routes->post('sales/save/(:num)', 'Sales::postSave/$1');
|
||||
|
||||
$routes->add('no_access/index/(:segment)', 'No_access::index/$1');
|
||||
$routes->add('no_access/index/(:segment)/(:segment)', 'No_access::index/$1/$2');
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
@@ -238,6 +241,28 @@ class Config extends Secure_Controller
|
||||
$data['show_office_group'] = $this->module->get_show_office_group();
|
||||
$data['currency_code'] = $this->config['currency_code'] ?? '';
|
||||
$data['dbVersion'] = mysqli_get_server_info($this->db->getConnection());
|
||||
$data['scale_export_formats'] = [
|
||||
'txt' => 'TXT',
|
||||
'csv' => 'CSV',
|
||||
];
|
||||
$data['scale_export_charsets'] = [
|
||||
'windows-1256' => 'Windows-1256',
|
||||
'utf-8' => 'UTF-8',
|
||||
'windows-1252' => 'Windows-1252',
|
||||
];
|
||||
$data['scale_export_delimiters'] = [
|
||||
';' => ';',
|
||||
',' => ',',
|
||||
"\t" => 'Tab',
|
||||
];
|
||||
$data['scale_export_fields_options'] = [
|
||||
'legacy_code' => lang('Items.item_number'),
|
||||
'item_number' => lang('Items.item_number'),
|
||||
'repeat_item_number' => lang('Items.item_number'),
|
||||
'name' => lang('Items.name'),
|
||||
'unit_price' => lang('Items.unit_price'),
|
||||
'legacy_tail' => lang('Items.item_number'),
|
||||
];
|
||||
|
||||
// Load all the license statements, they are already XSS cleaned in the private function
|
||||
$data['licenses'] = $this->_licenses();
|
||||
@@ -391,6 +416,7 @@ class Config extends Secure_Controller
|
||||
'suggestions_third_column' => $this->validateSuggestionsColumn($this->request->getPost('suggestions_third_column'), 'other'),
|
||||
'giftcard_number' => $this->request->getPost('giftcard_number'),
|
||||
'derive_sale_quantity' => $this->request->getPost('derive_sale_quantity') != null,
|
||||
'customer_display_enabled' => $this->request->getPost('customer_display_enabled') != null,
|
||||
'multi_pack_enabled' => $this->request->getPost('multi_pack_enabled') != null,
|
||||
'include_hsn' => $this->request->getPost('include_hsn') != null,
|
||||
'category_dropdown' => $this->request->getPost('category_dropdown') != null
|
||||
@@ -398,6 +424,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 +434,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 +456,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,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -461,13 +497,36 @@ class Config extends Secure_Controller
|
||||
*/
|
||||
public function postSaveLocale(): ResponseInterface
|
||||
{
|
||||
$exploded = explode(":", $this->request->getPost('language'));
|
||||
$language = trim((string) $this->request->getPost('language'));
|
||||
$languageCode = 'en';
|
||||
$languageName = 'english';
|
||||
|
||||
if ($language !== '' && str_contains($language, ':')) {
|
||||
$exploded = array_map('trim', explode(':', $language, 2));
|
||||
|
||||
if (count($exploded) === 2) {
|
||||
$languageCode = htmlspecialchars($exploded[0]);
|
||||
$languageName = htmlspecialchars($exploded[1]);
|
||||
}
|
||||
}
|
||||
|
||||
$currency_symbol = $this->request->getPost('currency_symbol');
|
||||
$secondaryCurrencyCode = strtoupper(trim((string) $this->request->getPost('secondary_currency_code')));
|
||||
|
||||
if (!preg_match('/^[A-Z]{3}$/', $secondaryCurrencyCode)) {
|
||||
$secondaryCurrencyCode = '';
|
||||
}
|
||||
|
||||
$batch_save_data = [
|
||||
'currency_symbol' => htmlspecialchars($currency_symbol ?? ''),
|
||||
'currency_code' => $this->request->getPost('currency_code'),
|
||||
'language_code' => $exploded[0],
|
||||
'language' => $exploded[1],
|
||||
'secondary_currency_enabled' => $this->request->getPost('secondary_currency_enabled') != null,
|
||||
'secondary_currency_symbol' => htmlspecialchars($this->request->getPost('secondary_currency_symbol') ?? ''),
|
||||
'secondary_currency_code' => $secondaryCurrencyCode,
|
||||
'secondary_currency_rate' => $this->request->getPost('secondary_currency_rate', FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION),
|
||||
'secondary_currency_decimals' => $this->request->getPost('secondary_currency_decimals', FILTER_SANITIZE_NUMBER_INT),
|
||||
'language_code' => $languageCode,
|
||||
'language' => $languageName,
|
||||
'timezone' => $this->request->getPost('timezone'),
|
||||
'dateformat' => $this->request->getPost('dateformat'),
|
||||
'timeformat' => $this->request->getPost('timeformat'),
|
||||
@@ -921,6 +980,7 @@ class Config extends Secure_Controller
|
||||
'receipt_show_tax_ind' => $this->request->getPost('receipt_show_tax_ind') != null,
|
||||
'receipt_show_total_discount' => $this->request->getPost('receipt_show_total_discount') != null,
|
||||
'receipt_show_description' => $this->request->getPost('receipt_show_description') != null,
|
||||
'receipt_show_secondary_currency' => $this->request->getPost('receipt_show_secondary_currency') != null,
|
||||
'receipt_show_serialnumber' => $this->request->getPost('receipt_show_serialnumber') != null,
|
||||
'print_silently' => $this->request->getPost('print_silently') != null,
|
||||
'print_header' => $this->request->getPost('print_header') != null,
|
||||
@@ -936,6 +996,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.
|
||||
*
|
||||
@@ -1015,3 +1113,6 @@ class Config extends Secure_Controller
|
||||
return in_array($column, $allowed, true) ? $column : $fallback;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ class Expenses extends Secure_Controller
|
||||
*/
|
||||
public function getView(int $expense_id = NEW_ENTRY): string
|
||||
{
|
||||
$data = [];
|
||||
$data = []; // TODO: Duplicated code
|
||||
|
||||
$data['expenses_info'] = $this->expense->get_info($expense_id);
|
||||
$expense_id = $data['expenses_info']->expense_id;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -11,7 +11,6 @@ use App\Models\Item_kit;
|
||||
use App\Models\Receiving;
|
||||
use App\Models\Stock_location;
|
||||
use App\Models\Supplier;
|
||||
use App\Traits\Controller\Shared;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Config\OSPOS;
|
||||
use Config\Services;
|
||||
@@ -19,7 +18,6 @@ use ReflectionException;
|
||||
|
||||
class Receivings extends Secure_Controller
|
||||
{
|
||||
use Shared;
|
||||
private Receiving_lib $receiving_lib;
|
||||
private Token_lib $token_lib;
|
||||
private Barcode_lib $barcode_lib;
|
||||
@@ -210,7 +208,7 @@ class Receivings extends Secure_Controller
|
||||
$quantity = parse_quantity($this->request->getPost('quantity'));
|
||||
$raw_receiving_quantity = parse_quantity($this->request->getPost('receiving_quantity'));
|
||||
|
||||
$description = $this->request->getPost('description', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
$description = $this->request->getPost('description', FILTER_SANITIZE_FULL_SPECIAL_CHARS); // TODO: Duplicated code
|
||||
$serialnumber = $this->request->getPost('serialnumber', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?? '';
|
||||
$discount_type = $this->request->getPost('discount_type', FILTER_SANITIZE_NUMBER_INT);
|
||||
$discount = $discount_type
|
||||
@@ -427,10 +425,19 @@ class Receivings extends Secure_Controller
|
||||
$employee_info = $this->employee->get_info($receiving_info['employee_id']);
|
||||
$data['employee'] = $employee_info->first_name . ' ' . $employee_info->last_name;
|
||||
|
||||
$supplier_id = $this->receiving_lib->get_supplier();
|
||||
$supplier_id = $this->receiving_lib->get_supplier(); // TODO: Duplicated code
|
||||
if ($supplier_id != -1) {
|
||||
$supplier_info = $this->supplier->get_info($supplier_id);
|
||||
$this->buildSupplierInfo($supplier_info, $data);
|
||||
$data['supplier'] = $supplier_info->company_name;
|
||||
$data['first_name'] = $supplier_info->first_name;
|
||||
$data['last_name'] = $supplier_info->last_name;
|
||||
$data['supplier_email'] = $supplier_info->email;
|
||||
$data['supplier_address'] = $supplier_info->address_1;
|
||||
if (!empty($supplier_info->zip) or !empty($supplier_info->city)) {
|
||||
$data['supplier_location'] = $supplier_info->zip . ' ' . $supplier_info->city;
|
||||
} else {
|
||||
$data['supplier_location'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
$data['print_after_sale'] = false;
|
||||
@@ -467,9 +474,18 @@ class Receivings extends Secure_Controller
|
||||
|
||||
$supplier_id = $this->receiving_lib->get_supplier();
|
||||
|
||||
if ($supplier_id != -1) {
|
||||
if ($supplier_id != -1) { // TODO: Duplicated Code... replace -1 with a constant
|
||||
$supplier_info = $this->supplier->get_info($supplier_id);
|
||||
$this->buildSupplierInfo($supplier_info, $data);
|
||||
$data['supplier'] = $supplier_info->company_name;
|
||||
$data['first_name'] = $supplier_info->first_name;
|
||||
$data['last_name'] = $supplier_info->last_name;
|
||||
$data['supplier_email'] = $supplier_info->email;
|
||||
$data['supplier_address'] = $supplier_info->address_1;
|
||||
if (!empty($supplier_info->zip) or !empty($supplier_info->city)) {
|
||||
$data['supplier_location'] = $supplier_info->zip . ' ' . $supplier_info->city;
|
||||
} else {
|
||||
$data['supplier_location'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
$data['print_after_sale'] = $this->receiving_lib->is_print_after_sale();
|
||||
|
||||
@@ -128,8 +128,8 @@ class Reports extends Secure_Controller
|
||||
* @param string $location_id
|
||||
* @return string
|
||||
*/
|
||||
public function summary_sales(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string
|
||||
{
|
||||
public function summary_sales(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string // TODO: Perhaps these need to be passed as an array? Too many parameters in the signature.
|
||||
{ // TODO: Duplicated code
|
||||
$this->clearCache();
|
||||
|
||||
$inputs = [
|
||||
@@ -176,7 +176,7 @@ class Reports extends Secure_Controller
|
||||
* @return string
|
||||
*/
|
||||
public function summary_categories(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string
|
||||
{
|
||||
{ // TODO: Duplicated code
|
||||
$this->clearCache();
|
||||
|
||||
$inputs = [
|
||||
@@ -493,7 +493,7 @@ class Reports extends Secure_Controller
|
||||
* @return string
|
||||
*/
|
||||
public function summary_sales_taxes(string $start_date, string $end_date, string $sale_type, string $location_id = 'all'): string
|
||||
{
|
||||
{ // TODO: Duplicated code
|
||||
$this->clearCache();
|
||||
|
||||
$inputs = [
|
||||
@@ -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')
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ use App\Models\Stock_location;
|
||||
use App\Models\Tokens\Token_invoice_count;
|
||||
use App\Models\Tokens\Token_customer;
|
||||
use App\Models\Tokens\Token_invoice_sequence;
|
||||
use App\Traits\Controller\Shared;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Config\Services;
|
||||
use Config\OSPOS;
|
||||
@@ -29,7 +28,6 @@ use stdClass;
|
||||
|
||||
class Sales extends Secure_Controller
|
||||
{
|
||||
use Shared;
|
||||
protected $helpers = ['file'];
|
||||
private Barcode_lib $barcode_lib;
|
||||
private Email_lib $email_lib;
|
||||
@@ -68,12 +66,168 @@ class Sales extends Secure_Controller
|
||||
$this->employee = model(Employee::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the shared secondary currency context to a view data array.
|
||||
*
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
private function _append_secondary_currency(array &$data): void
|
||||
{
|
||||
$secondaryCurrency = secondary_currency_context($this->config);
|
||||
$data['secondaryCurrency'] = $secondaryCurrency;
|
||||
|
||||
if (!$secondaryCurrency['show']) {
|
||||
return;
|
||||
}
|
||||
|
||||
$displayFields = [
|
||||
'total' => 'secondaryTotalDisplay',
|
||||
'amount_due' => 'secondaryAmountDueDisplay',
|
||||
'cash_amount_due' => 'secondaryCashAmountDueDisplay',
|
||||
'non_cash_total' => 'secondaryNonCashTotalDisplay',
|
||||
'non_cash_amount_due' => 'secondaryNonCashAmountDueDisplay'
|
||||
];
|
||||
|
||||
foreach ($displayFields as $sourceField => $targetField) {
|
||||
if (array_key_exists($sourceField, $data)) {
|
||||
$data[$targetField] = to_secondary_currency((float) $data[$sourceField], $secondaryCurrency);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getIndex(): ResponseInterface|string
|
||||
{
|
||||
$this->session->set('allow_temp_items', 1);
|
||||
return $this->_reload(); // TODO: Hungarian Notation
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the customer display popup.
|
||||
*
|
||||
* @return ResponseInterface|string
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function getCustomerDisplay(): ResponseInterface|string
|
||||
{
|
||||
if (($this->config['customer_display_enabled'] ?? false) != 1) {
|
||||
return $this->response->setStatusCode(404)->setBody('');
|
||||
}
|
||||
|
||||
if ($this->session->get('sale_id') == '') {
|
||||
$this->session->set('sale_id', NEW_ENTRY);
|
||||
}
|
||||
|
||||
$secondaryCurrency = secondary_currency_context($this->config);
|
||||
$secondaryCurrencyEnabled = (($this->config['secondary_currency_enabled'] ?? false) == 1);
|
||||
$cashRounding = $this->sale_lib->reset_cash_rounding();
|
||||
$showCustomerDisplay = $secondaryCurrencyEnabled && !empty($secondaryCurrency['rate']) && (float) $secondaryCurrency['rate'] > 0;
|
||||
$companyLines = preg_split("/\r\n|\r|\n/", (string) ($this->config['company'] ?? '')) ?: [];
|
||||
$companyName = array_shift($companyLines) ?? '';
|
||||
$companyDetails = trim(implode("\n", $companyLines));
|
||||
$secondaryCurrencySymbol = trim((string) ($this->config['secondary_currency_symbol'] ?? ''));
|
||||
$secondaryCurrencyCode = trim((string) ($this->config['secondary_currency_code'] ?? ''));
|
||||
$originalCurrencySymbol = trim((string) ($this->config['currency_symbol'] ?? ''));
|
||||
$customerDisplayCurrencyLabel = $secondaryCurrencyCode !== '' ? $secondaryCurrencyCode : ($secondaryCurrencySymbol !== '' ? $secondaryCurrencySymbol : 'LBP');
|
||||
$originalCurrencyLabel = $originalCurrencySymbol !== '' ? $originalCurrencySymbol : '$';
|
||||
$cartHasCustomerDisplay = $showCustomerDisplay;
|
||||
$cartColspan = $cartHasCustomerDisplay ? 6 : 5;
|
||||
$cartItemWidth = $cartHasCustomerDisplay ? 32 : 44;
|
||||
$cartPriceWidth = $cartHasCustomerDisplay ? 18 : 0;
|
||||
$cartOriginalWidth = $cartHasCustomerDisplay ? 18 : 26;
|
||||
$cartQuantityWidth = $cartHasCustomerDisplay ? 12 : 10;
|
||||
$cartDiscountWidth = $cartHasCustomerDisplay ? 10 : 9;
|
||||
$cartTotalWidth = $cartHasCustomerDisplay ? 10 : 11;
|
||||
|
||||
$data = [
|
||||
'cash_rounding' => $cashRounding,
|
||||
'cart' => $this->sale_lib->get_cart()
|
||||
];
|
||||
$customer_info = $this->_load_customer_data($this->sale_lib->get_customer(), $data, true);
|
||||
$data += [
|
||||
'customer_name' => $data['customer'] ?? lang('Sales.walk_in_customer'),
|
||||
'customer_reward_points' => (int) ($data['customer_rewards']['points'] ?? 0),
|
||||
'customer_reward_package' => $data['customer_rewards']['package_name'] ?? '',
|
||||
'giftcard_remainder' => $this->sale_lib->get_giftcard_remainder(),
|
||||
'rewards_remainder' => $this->sale_lib->get_rewards_remainder(),
|
||||
'customerName' => $data['customer'] ?? lang('Sales.walk_in_customer'),
|
||||
'customerRewardPoints' => (int) ($data['customer_rewards']['points'] ?? 0),
|
||||
'giftcardRemainder' => $this->sale_lib->get_giftcard_remainder()
|
||||
];
|
||||
|
||||
$tax_details = $this->tax_lib->get_taxes($data['cart']);
|
||||
$data += [
|
||||
'tax_exclusive_subtotal' => $this->sale_lib->get_subtotal(true, true),
|
||||
'taxes' => $tax_details[0],
|
||||
'discount' => $this->sale_lib->get_discount(),
|
||||
'payments' => $this->sale_lib->get_payments()
|
||||
];
|
||||
|
||||
$totals = $this->sale_lib->get_totals($tax_details[0]);
|
||||
$data += [
|
||||
'item_count' => $totals['item_count'],
|
||||
'total_units' => $totals['total_units'],
|
||||
'subtotal' => $totals['subtotal'],
|
||||
'total' => $totals['total'],
|
||||
'payments_total' => $totals['payment_total'],
|
||||
'payments_cover_total' => $totals['payments_cover_total'],
|
||||
'prediscount_subtotal' => $totals['prediscount_subtotal'],
|
||||
'cash_total' => $totals['cash_total'],
|
||||
'non_cash_total' => $totals['total'],
|
||||
'cash_amount_due' => $totals['cash_amount_due'],
|
||||
'non_cash_amount_due' => $totals['amount_due'],
|
||||
'cash_mode' => $this->session->get('cash_mode'),
|
||||
'selected_payment_type' => $this->sale_lib->get_payment_type(),
|
||||
'comment' => $this->sale_lib->get_comment(),
|
||||
'email_receipt' => $this->sale_lib->is_email_receipt(),
|
||||
'config' => $this->config,
|
||||
'mode' => $this->sale_lib->get_mode(),
|
||||
'rate' => (float) ($secondaryCurrency['rate'] ?? $this->config['secondary_currency_rate'] ?? 0),
|
||||
'secondaryCurrency' => $secondaryCurrency,
|
||||
'secondaryCurrencyEnabled' => $secondaryCurrencyEnabled,
|
||||
'showCustomerDisplay' => $showCustomerDisplay,
|
||||
'companyName' => $companyName,
|
||||
'companyDetails' => $companyDetails,
|
||||
'secondaryCurrencySymbol' => $secondaryCurrencySymbol,
|
||||
'secondaryCurrencyCode' => $secondaryCurrencyCode,
|
||||
'originalCurrencySymbol' => $originalCurrencySymbol,
|
||||
'customerDisplayCurrencyLabel' => $customerDisplayCurrencyLabel,
|
||||
'originalCurrencyLabel' => $originalCurrencyLabel,
|
||||
'cartHasCustomerDisplay' => $cartHasCustomerDisplay,
|
||||
'cartColspan' => $cartColspan,
|
||||
'cartItemWidth' => $cartItemWidth,
|
||||
'cartPriceWidth' => $cartPriceWidth,
|
||||
'cartOriginalWidth' => $cartOriginalWidth,
|
||||
'cartQuantityWidth' => $cartQuantityWidth,
|
||||
'cartDiscountWidth' => $cartDiscountWidth,
|
||||
'cartTotalWidth' => $cartTotalWidth,
|
||||
'items_module_allowed' => $this->employee->has_grant('items', $this->employee->get_logged_in_employee_info()->person_id),
|
||||
'change_price' => $this->employee->has_grant('sales_change_price', $this->employee->get_logged_in_employee_info()->person_id)
|
||||
];
|
||||
|
||||
$invoice_number = $this->sale_lib->get_invoice_number();
|
||||
if ($invoice_number == null || $invoice_number == '') {
|
||||
$invoice_number = $this->token_lib->render($this->config['sales_invoice_format'], [], false);
|
||||
}
|
||||
|
||||
$data += [
|
||||
'invoice_number' => $invoice_number,
|
||||
'print_after_sale' => $this->sale_lib->is_print_after_sale(),
|
||||
'price_work_orders' => $this->sale_lib->is_price_work_orders(),
|
||||
'pos_mode' => $data['mode'] == 'sale' || $data['mode'] == 'return',
|
||||
'quote_number' => $this->sale_lib->get_quote_number(),
|
||||
'work_order_number' => $this->sale_lib->get_work_order_number(),
|
||||
'amount_due' => $data['cash_mode'] && ($data['selected_payment_type'] === lang('Sales.cash') || $data['payments_total'] > 0) ? $totals['cash_amount_due'] : $totals['amount_due']
|
||||
];
|
||||
$data['amount_change'] = $data['amount_due'] * -1;
|
||||
$data['payment_change_due'] = ((float) $data['amount_due'] < 0)
|
||||
? abs((float) $data['amount_due'])
|
||||
: max(((float) $data['payments_total']) - ((float) $data['amount_due']), 0);
|
||||
$data['paymentChangeDue'] = $data['payment_change_due'];
|
||||
|
||||
return view('sales/customer_display', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the sale edit modal. Used in app/Views/sales/register.php.
|
||||
*
|
||||
@@ -733,7 +887,7 @@ class Sales extends Secure_Controller
|
||||
$data["customer_comments"] = $customer_info->comments;
|
||||
$data['tax_id'] = $customer_info->tax_id;
|
||||
}
|
||||
$tax_details = $this->tax_lib->get_taxes($data['cart']);
|
||||
$tax_details = $this->tax_lib->get_taxes($data['cart']); // TODO: Duplicated code
|
||||
$data['taxes'] = $tax_details[0];
|
||||
$data['discount'] = $this->sale_lib->get_discount();
|
||||
$data['payments'] = $this->sale_lib->get_payments();
|
||||
@@ -745,7 +899,7 @@ class Sales extends Secure_Controller
|
||||
$data['payments_total'] = $totals['payment_total'];
|
||||
$data['payments_cover_total'] = $totals['payments_cover_total'];
|
||||
$data['cash_rounding'] = $this->session->get('cash_rounding');
|
||||
$data['cash_mode'] = $this->session->get('cash_mode');
|
||||
$data['cash_mode'] = $this->session->get('cash_mode'); // TODO: Duplicated code
|
||||
$data['prediscount_subtotal'] = $totals['prediscount_subtotal'];
|
||||
$data['cash_total'] = $totals['cash_total'];
|
||||
$data['non_cash_total'] = $totals['total'];
|
||||
@@ -816,6 +970,7 @@ class Sales extends Secure_Controller
|
||||
|
||||
// Resort and filter cart lines for printing
|
||||
$data['cart'] = $this->sale_lib->sort_and_filter_cart($data['cart']);
|
||||
$this->_append_secondary_currency($data);
|
||||
|
||||
if ($data['sale_id_num'] == NEW_ENTRY) {
|
||||
$data['error_message'] = lang('Sales.transaction_failed');
|
||||
@@ -855,6 +1010,7 @@ class Sales extends Secure_Controller
|
||||
$data['cart'] = $this->sale_lib->sort_and_filter_cart($data['cart']);
|
||||
|
||||
$data['barcode'] = null;
|
||||
$this->_append_secondary_currency($data);
|
||||
|
||||
$this->sale_lib->clear_all();
|
||||
return view('sales/work_order', $data);
|
||||
@@ -882,6 +1038,7 @@ class Sales extends Secure_Controller
|
||||
|
||||
$data['cart'] = $this->sale_lib->sort_and_filter_cart($data['cart']);
|
||||
$data['barcode'] = null;
|
||||
$this->_append_secondary_currency($data);
|
||||
|
||||
$this->sale_lib->clear_all();
|
||||
return view('sales/quote', $data);
|
||||
@@ -900,6 +1057,7 @@ class Sales extends Secure_Controller
|
||||
$data['sale_id'] = 'POS ' . $data['sale_id_num'];
|
||||
|
||||
$data['cart'] = $this->sale_lib->sort_and_filter_cart($data['cart']);
|
||||
$this->_append_secondary_currency($data);
|
||||
|
||||
if ($data['sale_id_num'] == NEW_ENTRY) {
|
||||
$data['error_message'] = lang('Sales.transaction_failed');
|
||||
@@ -939,7 +1097,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();
|
||||
@@ -976,13 +1137,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');
|
||||
@@ -1100,7 +1255,7 @@ class Sales extends Secure_Controller
|
||||
$data['subtotal'] = $totals['subtotal'];
|
||||
$data['payments_total'] = $totals['payment_total'];
|
||||
$data['payments_cover_total'] = $totals['payments_cover_total'];
|
||||
$data['cash_mode'] = $this->session->get('cash_mode');.
|
||||
$data['cash_mode'] = $this->session->get('cash_mode'); // TODO: Duplicated code.
|
||||
$data['prediscount_subtotal'] = $totals['prediscount_subtotal'];
|
||||
$data['cash_total'] = $totals['cash_total'];
|
||||
$data['non_cash_total'] = $totals['total'];
|
||||
@@ -1128,21 +1283,42 @@ class Sales extends Secure_Controller
|
||||
$data['quote_number'] = $sale_info['quote_number'];
|
||||
$data['sale_status'] = $sale_info['sale_status'];
|
||||
|
||||
$data['company_info'] = $this->buildCompanyInfo();
|
||||
$data['company_info'] = implode("\n", [$this->config['address'], $this->config['phone']]); // TODO: Duplicated code.
|
||||
|
||||
if ($this->config['account_number']) {
|
||||
$data['company_info'] .= "\n" . lang('Sales.account_number') . ": " . $this->config['account_number'];
|
||||
}
|
||||
if ($this->config['tax_id'] != '') {
|
||||
$data['company_info'] .= "\n" . lang('Sales.tax_id') . ": " . $this->config['tax_id'];
|
||||
}
|
||||
|
||||
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']);
|
||||
$data['print_after_sale'] = false;
|
||||
$data['price_work_orders'] = false;
|
||||
|
||||
$modeData = $this->getSaleModeLabel($this->sale_lib->get_mode());
|
||||
$data['mode_label'] = $modeData['mode_label'];
|
||||
$data['customer_required'] = $modeData['customer_required'];
|
||||
if ($this->sale_lib->get_mode() == 'sale_invoice') { // TODO: Duplicated code.
|
||||
$data['mode_label'] = lang('Sales.invoice');
|
||||
$data['customer_required'] = lang('Sales.customer_required');
|
||||
} elseif ($this->sale_lib->get_mode() == 'sale_quote') {
|
||||
$data['mode_label'] = lang('Sales.quote');
|
||||
$data['customer_required'] = lang('Sales.customer_required');
|
||||
} elseif ($this->sale_lib->get_mode() == 'sale_work_order') {
|
||||
$data['mode_label'] = lang('Sales.work_order');
|
||||
$data['customer_required'] = lang('Sales.customer_required');
|
||||
} elseif ($this->sale_lib->get_mode() == 'return') {
|
||||
$data['mode_label'] = lang('Sales.return');
|
||||
$data['customer_required'] = lang('Sales.customer_optional');
|
||||
} else {
|
||||
$data['mode_label'] = lang('Sales.receipt');
|
||||
$data['customer_required'] = lang('Sales.customer_optional');
|
||||
}
|
||||
|
||||
$invoice_type = $this->config['invoice_type'];
|
||||
if (!Sale_lib::isValidInvoiceType($invoice_type)) {
|
||||
$invoice_type = 'invoice';
|
||||
}
|
||||
$data['invoice_view'] = $invoice_type;
|
||||
$this->_append_secondary_currency($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
@@ -1174,7 +1350,7 @@ class Sales extends Secure_Controller
|
||||
$data['stock_locations'] = $this->stock_location->get_allowed_locations('sales');
|
||||
$data['stock_location'] = $this->sale_lib->get_sale_location();
|
||||
$data['tax_exclusive_subtotal'] = $this->sale_lib->get_subtotal(true, true);
|
||||
$tax_details = $this->tax_lib->get_taxes($data['cart']);.
|
||||
$tax_details = $this->tax_lib->get_taxes($data['cart']); // TODO: Duplicated code.
|
||||
$data['taxes'] = $tax_details[0];
|
||||
$data['discount'] = $this->sale_lib->get_discount();
|
||||
$data['payments'] = $this->sale_lib->get_payments();
|
||||
@@ -1192,7 +1368,7 @@ class Sales extends Secure_Controller
|
||||
// cash_mode indicates whether this sale is going to be processed using cash_rounding
|
||||
$cash_mode = $this->session->get('cash_mode');
|
||||
$data['cash_mode'] = $cash_mode;
|
||||
$data['prediscount_subtotal'] = $totals['prediscount_subtotal'];.
|
||||
$data['prediscount_subtotal'] = $totals['prediscount_subtotal']; // TODO: Duplicated code.
|
||||
$data['cash_total'] = $totals['cash_total'];
|
||||
$data['non_cash_total'] = $totals['total'];
|
||||
$data['cash_amount_due'] = $totals['cash_amount_due'];
|
||||
@@ -1209,6 +1385,7 @@ class Sales extends Secure_Controller
|
||||
}
|
||||
|
||||
$data['amount_change'] = $data['amount_due'] * -1;
|
||||
$this->_append_secondary_currency($data);
|
||||
|
||||
$data['comment'] = $this->sale_lib->get_comment();
|
||||
$data['email_receipt'] = $this->sale_lib->is_email_receipt();
|
||||
@@ -1239,9 +1416,23 @@ 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();
|
||||
|
||||
$modeData = $this->getSaleModeLabel($this->sale_lib->get_mode());
|
||||
$data['mode_label'] = $modeData['mode_label'];
|
||||
$data['customer_required'] = $modeData['customer_required'];
|
||||
// TODO: the if/else set below should be converted to a switch
|
||||
if ($this->sale_lib->get_mode() == 'sale_invoice') { // TODO: Duplicated code.
|
||||
$data['mode_label'] = lang('Sales.invoice');
|
||||
$data['customer_required'] = lang('Sales.customer_required');
|
||||
} elseif ($this->sale_lib->get_mode() == 'sale_quote') {
|
||||
$data['mode_label'] = lang('Sales.quote');
|
||||
$data['customer_required'] = lang('Sales.customer_required');
|
||||
} elseif ($this->sale_lib->get_mode() == 'sale_work_order') {
|
||||
$data['mode_label'] = lang('Sales.work_order');
|
||||
$data['customer_required'] = lang('Sales.customer_required');
|
||||
} elseif ($this->sale_lib->get_mode() == 'return') {
|
||||
$data['mode_label'] = lang('Sales.return');
|
||||
$data['customer_required'] = lang('Sales.customer_optional');
|
||||
} else {
|
||||
$data['mode_label'] = lang('Sales.receipt');
|
||||
$data['customer_required'] = lang('Sales.customer_optional');
|
||||
}
|
||||
|
||||
return view("sales/register", $data);
|
||||
}
|
||||
@@ -1734,3 +1925,5 @@ class Sales extends Secure_Controller
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -8,14 +8,12 @@ use App\Models\Tax;
|
||||
use App\Models\Tax_category;
|
||||
use App\Models\Tax_code;
|
||||
use App\Models\Tax_jurisdiction;
|
||||
use App\Traits\Controller\Shared;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Config\OSPOS;
|
||||
use Config\Services;
|
||||
|
||||
class Taxes extends Secure_Controller
|
||||
{
|
||||
use Shared;
|
||||
private array $config;
|
||||
private Tax_lib $tax_lib;
|
||||
private Tax $tax;
|
||||
@@ -142,26 +140,44 @@ class Taxes extends Secure_Controller
|
||||
{
|
||||
$tax_code_info = $this->tax->get_info($tax_code);
|
||||
|
||||
$default_tax_category_id = 1; // Tax category id is always the default tax category // TODO: This variable is not used anywhere in the code
|
||||
$default_tax_category = $this->tax->get_tax_category($default_tax_category_id); // TODO: This variable is not used anywhere in the code
|
||||
$default_tax_category_id = 1; // Tax category id is always the default tax category // TODO: Replace 1 with constant
|
||||
$default_tax_category = $this->tax->get_tax_category($default_tax_category_id); // TODO: this variable is never used in the code.
|
||||
|
||||
$tax_rate_info = $this->tax->get_rate_info($tax_code, $default_tax_category_id);
|
||||
|
||||
$data['rounding_options'] = Rounding_mode::get_rounding_options();
|
||||
$data['html_rounding_options'] = $this->get_html_rounding_options();
|
||||
|
||||
if ($this->config['tax_included']) {
|
||||
$data['default_tax_type'] = Tax_lib::TAX_TYPE_INCLUDED;
|
||||
} else {
|
||||
$data['default_tax_type'] = Tax_lib::TAX_TYPE_EXCLUDED;
|
||||
}
|
||||
|
||||
if ($tax_code == NEW_ENTRY) {
|
||||
$taxData = $this->initDefaultTaxCodeData();
|
||||
$data = array_merge($data, $taxData);
|
||||
$data['rounding_options'] = Rounding_mode::get_rounding_options();
|
||||
$data['html_rounding_options'] = $this->get_html_rounding_options();
|
||||
|
||||
if ($tax_code == NEW_ENTRY) { // TODO: Duplicated code
|
||||
$data['tax_code'] = '';
|
||||
$data['tax_code_name'] = '';
|
||||
$data['tax_code_type'] = '0';
|
||||
$data['city'] = '';
|
||||
$data['state'] = '';
|
||||
$data['tax_rate'] = '0.0000';
|
||||
$data['rate_tax_code'] = '';
|
||||
$data['rate_tax_category_id'] = 1;
|
||||
$data['tax_category'] = '';
|
||||
$data['add_tax_category'] = '';
|
||||
$data['rounding_code'] = '0';
|
||||
} else {
|
||||
$taxData = $this->buildTaxCodeData($tax_code_info, $tax_rate_info);
|
||||
$data = array_merge($data, $taxData);
|
||||
$data['tax_code'] = $tax_code;
|
||||
$data['tax_code_name'] = $tax_code_info->tax_code_name;
|
||||
$data['tax_code_type'] = $tax_code_info->tax_code_type;
|
||||
$data['city'] = $tax_code_info->city;
|
||||
$data['state'] = $tax_code_info->state;
|
||||
$data['rate_tax_code'] = $tax_code_info->rate_tax_code;
|
||||
$data['rate_tax_category_id'] = $tax_code_info->rate_tax_category_id;
|
||||
$data['tax_category'] = $tax_code_info->tax_category;
|
||||
$data['add_tax_category'] = '';
|
||||
$data['tax_rate'] = $tax_rate_info->tax_rate;
|
||||
$data['rounding_code'] = $tax_rate_info->rounding_code;
|
||||
}
|
||||
|
||||
$tax_rates = [];
|
||||
@@ -284,7 +300,7 @@ class Taxes extends Secure_Controller
|
||||
*/
|
||||
public function getView_tax_jurisdictions(int $tax_code = NEW_ENTRY): string // TODO: This appears to be called no where in the code.
|
||||
{
|
||||
$tax_code_info = $this->tax->get_info($tax_code);
|
||||
$tax_code_info = $this->tax->get_info($tax_code); // TODO: Duplicated code
|
||||
|
||||
$default_tax_category_id = 1; // Tax category id is always the default tax category
|
||||
$default_tax_category = $this->tax->get_tax_category($default_tax_category_id); // TODO: This variable is not used anywhere in the code
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,15 +4,16 @@ namespace App\Database\Migrations;
|
||||
|
||||
use App\Libraries\Tax_lib;
|
||||
use App\Models\Appconfig;
|
||||
use App\Traits\Database\SalesTaxMigration;
|
||||
use CodeIgniter\Database\Migration;
|
||||
use CodeIgniter\Database\ResultInterface;
|
||||
|
||||
/**
|
||||
* @property tax_lib tax_lib
|
||||
* @property appconfig appconfig
|
||||
*/
|
||||
class Migration_Sales_Tax_Data extends Migration
|
||||
{
|
||||
use SalesTaxMigration;
|
||||
|
||||
public const ROUND_UP = 5;
|
||||
public const ROUND_UP = 5; // TODO: These need to be moved to constants.php
|
||||
public const ROUND_DOWN = 6;
|
||||
public const HALF_FIVE = 7;
|
||||
public const YES = '1';
|
||||
@@ -326,18 +327,79 @@ class Migration_Sales_Tax_Data extends Migration
|
||||
}
|
||||
}
|
||||
|
||||
public function clean(string $string): string
|
||||
/**
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public function clean(string $string): string // TODO: $string is not a good name for this variable
|
||||
{
|
||||
return $this->cleanIdentifier($string);
|
||||
$string = str_replace(' ', '-', $string); // Replaces all spaces with hyphens.
|
||||
|
||||
return preg_replace('/[^A-Za-z0-9\-]/', '', $string); // Removes special chars.
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $sales_taxes
|
||||
* @return void
|
||||
*/
|
||||
public function apply_invoice_taxing(array &$sales_taxes): void
|
||||
{
|
||||
$this->applyInvoiceTaxing($sales_taxes);
|
||||
if (!empty($sales_taxes)) { // TODO: Duplicated code
|
||||
$sort = [];
|
||||
|
||||
foreach ($sales_taxes as $key => $value) {
|
||||
$sort['print_sequence'][$key] = $value['print_sequence'];
|
||||
}
|
||||
|
||||
array_multisort($sort['print_sequence'], SORT_ASC, $sales_taxes);
|
||||
}
|
||||
|
||||
$decimals = totals_decimals();
|
||||
|
||||
foreach ($sales_taxes as $row_number => $sales_tax) {
|
||||
$sales_taxes[$row_number]['sale_tax_amount'] = $this->get_sales_tax_for_amount($sales_tax['sale_tax_basis'], $sales_tax['tax_rate'], $sales_tax['rounding_code'], $decimals);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $sales_taxes
|
||||
* @return void
|
||||
*/
|
||||
public function round_sales_taxes(array &$sales_taxes): void
|
||||
{
|
||||
$this->roundSalesTaxes($sales_taxes, self::ROUND_UP, self::ROUND_DOWN, self::HALF_FIVE);
|
||||
if (!empty($sales_taxes)) {
|
||||
$sort = [];
|
||||
foreach ($sales_taxes as $k => $v) {
|
||||
$sort['print_sequence'][$k] = $v['print_sequence'];
|
||||
}
|
||||
array_multisort($sort['print_sequence'], SORT_ASC, $sales_taxes);
|
||||
}
|
||||
|
||||
$decimals = totals_decimals();
|
||||
|
||||
foreach ($sales_taxes as $row_number => $sales_tax) {
|
||||
$sale_tax_amount = (float)$sales_tax['sale_tax_amount'];
|
||||
$rounding_code = $sales_tax['rounding_code'];
|
||||
$rounded_sale_tax_amount = $sale_tax_amount;
|
||||
|
||||
if (
|
||||
$rounding_code == PHP_ROUND_HALF_UP
|
||||
|| $rounding_code == PHP_ROUND_HALF_DOWN
|
||||
|| $rounding_code == PHP_ROUND_HALF_EVEN
|
||||
|| $rounding_code == PHP_ROUND_HALF_ODD
|
||||
) {
|
||||
$rounded_sale_tax_amount = round($sale_tax_amount, $decimals, $rounding_code);
|
||||
} elseif ($rounding_code == Migration_Sales_Tax_Data::ROUND_UP) {
|
||||
$fig = (int) str_pad('1', $decimals, '0');
|
||||
$rounded_sale_tax_amount = (ceil($sale_tax_amount * $fig) / $fig);
|
||||
} elseif ($rounding_code == Migration_Sales_Tax_Data::ROUND_DOWN) {
|
||||
$fig = (int) str_pad('1', $decimals, '0');
|
||||
$rounded_sale_tax_amount = (floor($sale_tax_amount * $fig) / $fig);
|
||||
} elseif ($rounding_code == Migration_Sales_Tax_Data::HALF_FIVE) {
|
||||
$rounded_sale_tax_amount = round($sale_tax_amount / 5) * 5;
|
||||
}
|
||||
|
||||
$sales_taxes[$row_number]['sale_tax_amount'] = $rounded_sale_tax_amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,25 @@
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use App\Traits\Database\SalesTaxMigration;
|
||||
use CodeIgniter\Database\Migration;
|
||||
use App\Libraries\Tax_lib;
|
||||
use App\Models\Appconfig;
|
||||
use CodeIgniter\Database\ResultInterface;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @property appconfig appconfig
|
||||
* @property tax_lib tax_lib
|
||||
*/
|
||||
class Migration_TaxAmount extends Migration
|
||||
{
|
||||
use SalesTaxMigration;
|
||||
|
||||
public const ROUND_UP = 5;
|
||||
public const ROUND_DOWN = 6;
|
||||
public const HALF_FIVE = 7;
|
||||
public const YES = '1';
|
||||
public const VAT_TAX = '0';
|
||||
public const SALES_TAX = '1';
|
||||
public const SALES_TAX = '1'; // TODO: It appears that this constant is never used
|
||||
private Appconfig $appconfig;
|
||||
|
||||
public function __construct()
|
||||
@@ -302,18 +305,79 @@ class Migration_TaxAmount extends Migration
|
||||
}
|
||||
}
|
||||
|
||||
public function clean(string $string): string
|
||||
/**
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
public function clean(string $string): string // TODO: This can probably go into the migration helper as it's used it more than one migration. Also, $string needs to be refactored to a different name.
|
||||
{
|
||||
return $this->cleanIdentifier($string);
|
||||
$string = str_replace(' ', '-', $string); // Replaces all spaces with hyphens.
|
||||
|
||||
return preg_replace('/[^A-Za-z0-9\-]/', '', $string); // Removes special chars.
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $sales_taxes
|
||||
* @return void
|
||||
*/
|
||||
public function apply_invoice_taxing(array &$sales_taxes): void
|
||||
{
|
||||
$this->applyInvoiceTaxing($sales_taxes);
|
||||
if (!empty($sales_taxes)) { // TODO: Duplicated code
|
||||
$sort = [];
|
||||
foreach ($sales_taxes as $k => $v) {
|
||||
$sort['print_sequence'][$k] = $v['print_sequence'];
|
||||
}
|
||||
array_multisort($sort['print_sequence'], SORT_ASC, $sales_taxes);
|
||||
}
|
||||
|
||||
$decimals = totals_decimals();
|
||||
|
||||
foreach ($sales_taxes as $row_number => $sales_tax) {
|
||||
$sales_taxes[$row_number]['sale_tax_amount'] = $this->get_sales_tax_for_amount($sales_tax['sale_tax_basis'], $sales_tax['tax_rate'], $sales_tax['rounding_code'], $decimals);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $sales_taxes
|
||||
* @return void
|
||||
*/
|
||||
public function round_sales_taxes(array &$sales_taxes): void
|
||||
{
|
||||
$this->roundSalesTaxes($sales_taxes, self::ROUND_UP, self::ROUND_DOWN, self::HALF_FIVE);
|
||||
if (!empty($sales_taxes)) {
|
||||
$sort = [];
|
||||
|
||||
foreach ($sales_taxes as $k => $v) {
|
||||
$sort['print_sequence'][$k] = $v['print_sequence'];
|
||||
}
|
||||
|
||||
array_multisort($sort['print_sequence'], SORT_ASC, $sales_taxes);
|
||||
}
|
||||
|
||||
$decimals = totals_decimals();
|
||||
|
||||
foreach ($sales_taxes as $row_number => $sales_tax) {
|
||||
$sale_tax_amount = (float)$sales_tax['sale_tax_amount'];
|
||||
$rounding_code = $sales_tax['rounding_code'];
|
||||
$rounded_sale_tax_amount = $sale_tax_amount;
|
||||
|
||||
if (
|
||||
$rounding_code == PHP_ROUND_HALF_UP // TODO: This block of if/elseif statements can be converted to a switch.
|
||||
|| $rounding_code == PHP_ROUND_HALF_DOWN
|
||||
|| $rounding_code == PHP_ROUND_HALF_EVEN
|
||||
|| $rounding_code == PHP_ROUND_HALF_ODD
|
||||
) {
|
||||
$rounded_sale_tax_amount = round($sale_tax_amount, $decimals, $rounding_code);
|
||||
} elseif ($rounding_code == Migration_TaxAmount::ROUND_UP) {
|
||||
$fig = (int) str_pad('1', $decimals, '0');
|
||||
$rounded_sale_tax_amount = (ceil($sale_tax_amount * $fig) / $fig);
|
||||
} elseif ($rounding_code == Migration_TaxAmount::ROUND_DOWN) {
|
||||
$fig = (int) str_pad('1', $decimals, '0');
|
||||
$rounded_sale_tax_amount = (floor($sale_tax_amount * $fig) / $fig);
|
||||
} elseif ($rounding_code == Migration_TaxAmount::HALF_FIVE) {
|
||||
$rounded_sale_tax_amount = round($sale_tax_amount / 5) * 5;
|
||||
}
|
||||
|
||||
$sales_taxes[$row_number]['sale_tax_amount'] = $rounded_sale_tax_amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -365,6 +365,74 @@ function to_currency_no_money(?string $number): string
|
||||
return to_decimals($number, 'currency_decimals');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the secondary currency rendering context from app config values.
|
||||
*
|
||||
* @param array $config
|
||||
* @return array{show:bool,rate:float,symbol:string,code:string,decimals:int}
|
||||
*/
|
||||
function secondary_currency_context(array $config): array
|
||||
{
|
||||
$rate = (float) ($config['secondary_currency_rate'] ?? 0);
|
||||
$symbol = trim((string) ($config['secondary_currency_symbol'] ?? ''));
|
||||
$code = trim((string) ($config['secondary_currency_code'] ?? ''));
|
||||
$decimals = (int) ($config['secondary_currency_decimals'] ?? ($config['currency_decimals'] ?? DEFAULT_PRECISION));
|
||||
|
||||
return [
|
||||
'show' => (($config['secondary_currency_enabled'] ?? false) == 1) && $rate > 0,
|
||||
'rate' => $rate,
|
||||
'symbol' => $symbol,
|
||||
'code' => $code,
|
||||
'decimals' => $decimals,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a value in the secondary currency.
|
||||
*
|
||||
* @param float|int|string|null $number
|
||||
* @param array{show:bool,rate:float,symbol:string,code:string,decimals:int} $secondaryCurrency
|
||||
* @return string
|
||||
*/
|
||||
function to_secondary_currency(float|int|string|null $number, array $secondaryCurrency): string
|
||||
{
|
||||
if (!isset($number) || !$secondaryCurrency['show']) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$config = config(OSPOS::class)->settings;
|
||||
$amount = (float) $number * (float) $secondaryCurrency['rate'];
|
||||
$fmt = new NumberFormatter($config['number_locale'], NumberFormatter::CURRENCY);
|
||||
$fmt->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $secondaryCurrency['decimals']);
|
||||
$fmt->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $secondaryCurrency['decimals']);
|
||||
|
||||
if (empty($config['thousands_separator'])) {
|
||||
$fmt->setTextAttribute(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, '');
|
||||
}
|
||||
|
||||
$fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL, $secondaryCurrency['symbol'] !== '' ? $secondaryCurrency['symbol'] : ($secondaryCurrency['code'] !== '' ? $secondaryCurrency['code'] : ''));
|
||||
|
||||
return $fmt->format($amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the secondary and primary currency amounts together.
|
||||
*
|
||||
* @param float|int|string|null $number
|
||||
* @param array{show:bool,rate:float,symbol:string,code:string,decimals:int} $secondaryCurrency
|
||||
* @return string
|
||||
*/
|
||||
function to_secondary_currency_dual(float|int|string|null $number, array $secondaryCurrency): string
|
||||
{
|
||||
$secondary = to_secondary_currency($number, $secondaryCurrency);
|
||||
|
||||
if ($secondary === '') {
|
||||
return to_currency((string) $number);
|
||||
}
|
||||
|
||||
return $secondary . '<br>' . to_currency((string) $number);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $number
|
||||
* @return string
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -268,10 +268,19 @@ return [
|
||||
"receipt_show_company_name" => "Show Company Name",
|
||||
"receipt_show_description" => "Show Description",
|
||||
"receipt_show_serialnumber" => "Show Serial Number",
|
||||
"receipt_show_secondary_currency" => "Show Secondary Currency",
|
||||
"receipt_show_tax_ind" => "Show Tax Indicator",
|
||||
"receipt_show_taxes" => "Show Taxes",
|
||||
"receipt_show_total_discount" => "Show Total Discount",
|
||||
"receipt_template" => "Receipt Template",
|
||||
"secondary_currency" => "Secondary Currency",
|
||||
"secondary_currency_decimals" => "Secondary Currency Decimals",
|
||||
"secondary_currency_code" => "Secondary Currency Code",
|
||||
"secondary_currency_enable" => "Enable Secondary Currency",
|
||||
"secondary_currency_enable_tooltip" => "Show secondary currency fields and print/display values across the app.",
|
||||
"secondary_currency_rate" => "Secondary Currency Rate",
|
||||
"secondary_currency_settings" => "Secondary Currency Settings",
|
||||
"secondary_currency_symbol" => "Secondary Currency Symbol",
|
||||
"receiving_calculate_average_price" => "Calc avg. Price (Receiving)",
|
||||
"recv_invoice_format" => "Receivings Invoice Format",
|
||||
"register_mode_default" => "Default Register Mode",
|
||||
@@ -288,6 +297,7 @@ return [
|
||||
"security_issue" => "Security Vulnerability Warning",
|
||||
"server_notice" => "Please use the below info for issue reporting.",
|
||||
"service_charge" => "",
|
||||
"customer_display" => "Customer Display",
|
||||
"show_due_enable" => "",
|
||||
"show_office_group" => "Show office icon",
|
||||
"statistics" => "Send Statistics",
|
||||
@@ -330,3 +340,5 @@ return [
|
||||
"work_order_enable" => "Work Order Support",
|
||||
"work_order_format" => "Work Order Format",
|
||||
];
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ return [
|
||||
"account_number" => "Account #",
|
||||
"add_payment" => "Add Payment",
|
||||
"amount_due" => "Amount Due",
|
||||
"amount_due_lbp" => "Amount Due LBP",
|
||||
"amount_tendered" => "Amount Tendered",
|
||||
"authorized_signature" => "Authorized Signature",
|
||||
"cancel_sale" => "Cancel",
|
||||
@@ -19,6 +20,8 @@ return [
|
||||
"cash_deposit" => "Cash Deposit",
|
||||
"cash_filter" => "Cash",
|
||||
"change_due" => "Change Due",
|
||||
"change" => "Change",
|
||||
"currency_rate" => "Currency Rate",
|
||||
"change_price" => "Change Selling Price",
|
||||
"check" => "Check",
|
||||
"check_balance" => "Check remainder",
|
||||
@@ -40,6 +43,7 @@ return [
|
||||
"customer_address" => "Address",
|
||||
"customer_discount" => "Discount",
|
||||
"customer_email" => "Email",
|
||||
"customer_name" => "Customer Name",
|
||||
"customer_location" => "Location",
|
||||
"customer_mailchimp_status" => "MailChimp Status",
|
||||
"customer_optional" => "(Required for Due Payments)",
|
||||
@@ -73,12 +77,6 @@ return [
|
||||
"employee" => "Employee",
|
||||
"entry" => "Entry",
|
||||
"error_editing_item" => "Error editing item",
|
||||
"negative_price_invalid" => "Price cannot be negative.",
|
||||
"negative_quantity_invalid" => "Quantity cannot be negative.",
|
||||
"negative_discount_invalid" => "Discount cannot be negative.",
|
||||
"discount_percent_exceeds_100" => "Percentage discount cannot exceed 100%.",
|
||||
"discount_exceeds_item_total" => "Discount cannot exceed the item total.",
|
||||
"negative_total_invalid" => "Sale total cannot be negative. Check item discounts and quantities.",
|
||||
"find_or_scan_item" => "Find or Scan Item",
|
||||
"find_or_scan_item_or_receipt" => "Find or Scan Item or Receipt",
|
||||
"giftcard" => "Gift Card",
|
||||
@@ -109,6 +107,7 @@ return [
|
||||
"item_name" => "Item Name",
|
||||
"item_number" => "Item #",
|
||||
"item_out_of_stock" => "Item is out of stock.",
|
||||
"items" => "Items",
|
||||
"key_browser" => "Helpful Shortcuts",
|
||||
"key_cancel" => "Cancels Current Quote/Invoice/Sale",
|
||||
"key_customer_search" => "Customer Search",
|
||||
@@ -150,7 +149,9 @@ return [
|
||||
"payment_type" => "Type",
|
||||
"payments" => "",
|
||||
"payments_total" => "Payments Total",
|
||||
"loyalty_reward_points" => "Loyalty Reward Points",
|
||||
"price" => "Price",
|
||||
"price_with_currency" => "Price (%s)",
|
||||
"print_after_sale" => "Print after Sale",
|
||||
"quantity" => "Quantity",
|
||||
"quantity_less_than_reorder_level" => "Warning: Desired Quantity is below Reorder Level for that Item.",
|
||||
@@ -166,10 +167,13 @@ return [
|
||||
"receipt_number" => "Sale #",
|
||||
"receipt_sent" => "Receipt sent to",
|
||||
"receipt_unsent" => "Receipt failed to be sent to",
|
||||
"rate" => "Rate",
|
||||
"refund" => "Refund Type",
|
||||
"register" => "Sales Register",
|
||||
"remove_customer" => "Remove Customer",
|
||||
"remove_discount" => "",
|
||||
"customer_display" => "Customer Display",
|
||||
"summary" => "Summary",
|
||||
"return" => "Return",
|
||||
"rewards" => "Reward Points",
|
||||
"rewards_balance" => "Reward Points Balance",
|
||||
@@ -181,6 +185,7 @@ return [
|
||||
"sales_total" => "",
|
||||
"select_customer" => "Select Customer",
|
||||
"selected_customer" => "Selected Customer",
|
||||
"walk_in_customer" => "Walk-in Customer",
|
||||
"send_invoice" => "Send Invoice",
|
||||
"send_quote" => "Send Quote",
|
||||
"send_receipt" => "Send Receipt",
|
||||
@@ -211,6 +216,7 @@ return [
|
||||
"tax_percent" => "Tax %",
|
||||
"taxed_ind" => "T",
|
||||
"total" => "Total",
|
||||
"total_lbp" => "Total LBP",
|
||||
"total_tax_exclusive" => "Tax excluded",
|
||||
"transaction_failed" => "Sales Transaction failed.",
|
||||
"unable_to_add_item" => "Item add to Sale failed",
|
||||
@@ -230,3 +236,5 @@ return [
|
||||
"work_order_sent" => "Work Order sent to",
|
||||
"work_order_unsent" => "Work Order failed to be sent to",
|
||||
];
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -44,7 +43,9 @@ class MY_Migration extends MigrationRunner
|
||||
$result = $builder->get()->getRow();
|
||||
return $result ? $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
|
||||
*/
|
||||
|
||||
@@ -472,7 +472,7 @@ class Attribute extends Model
|
||||
}
|
||||
} elseif ($from_type === DROPDOWN) {
|
||||
if (in_array($to_type, [TEXT, CHECKBOX], true)) {
|
||||
if ($to_type === CHECKBOX) {
|
||||
if ($to_type === CHECKBOX) { // TODO: Duplicated code.
|
||||
$checkbox_attribute_values = $this->checkbox_attribute_values($definition_id);
|
||||
|
||||
$this->db->transStart();
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -423,7 +423,7 @@ class Customer extends Person
|
||||
$builder->orLike('phone_number', $search);
|
||||
$builder->orLike('account_number', $search);
|
||||
$builder->orLike('company_name', $search);
|
||||
$builder->orLike('CONCAT(first_name, " ", last_name)', $search);
|
||||
$builder->orLike('CONCAT(first_name, " ", last_name)', $search); // TODO: Duplicated code.
|
||||
$builder->groupEnd();
|
||||
$builder->where('deleted', 0);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -116,7 +116,7 @@ class Module extends Model
|
||||
public function get_allowed_office_modules(int $person_id): ResultInterface
|
||||
{
|
||||
$menus = ['office', 'both'];
|
||||
$builder = $this->db->table('modules');
|
||||
$builder = $this->db->table('modules'); // TODO: Duplicated code
|
||||
$builder->join('permissions', 'permissions.permission_id = modules.module_id');
|
||||
$builder->join('grants', 'permissions.permission_id = grants.permission_id');
|
||||
$builder->where('person_id', $person_id);
|
||||
|
||||
@@ -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')
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -3,21 +3,32 @@
|
||||
namespace App\Models\Reports;
|
||||
|
||||
use App\Models\Sale;
|
||||
use App\Traits\Models\Reports\SaleTypeFilter;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @property sale sale
|
||||
*
|
||||
*/
|
||||
class Detailed_sales extends Report
|
||||
{
|
||||
use SaleTypeFilter;
|
||||
|
||||
/**
|
||||
* @param array $inputs
|
||||
* @return void
|
||||
*/
|
||||
public function create(array $inputs): void
|
||||
{
|
||||
// Create our temp tables to work with the data in our report
|
||||
$sale = model(Sale::class);
|
||||
$sale->create_temp_table($inputs);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getDataColumns(): array
|
||||
{
|
||||
return [
|
||||
return [ // TODO: Duplicated code
|
||||
'summary' => [
|
||||
['id' => lang('Reports.sale_id')],
|
||||
['type_code' => lang('Reports.code_type')],
|
||||
@@ -108,11 +119,47 @@ class Detailed_sales extends Report
|
||||
MAX(payment_type) AS payment_type,
|
||||
MAX(comment) AS comment');
|
||||
|
||||
if ($inputs['location_id'] != 'all') {
|
||||
if ($inputs['location_id'] != 'all') { // TODO: Duplicated code
|
||||
$builder->where('item_location', $inputs['location_id']);
|
||||
}
|
||||
|
||||
$this->applySaleTypeFilter($builder, $inputs['sale_type'], false);
|
||||
switch ($inputs['sale_type']) {
|
||||
case 'complete':
|
||||
$builder->where('sale_status', COMPLETED);
|
||||
$builder->groupStart();
|
||||
$builder->where('sale_type', SALE_TYPE_POS);
|
||||
$builder->orWhere('sale_type', SALE_TYPE_INVOICE);
|
||||
$builder->orWhere('sale_type', SALE_TYPE_RETURN);
|
||||
$builder->groupEnd();
|
||||
break;
|
||||
|
||||
case 'sales':
|
||||
$builder->where('sale_status', COMPLETED);
|
||||
$builder->groupStart();
|
||||
$builder->where('sale_type', SALE_TYPE_POS);
|
||||
$builder->orWhere('sale_type', SALE_TYPE_INVOICE);
|
||||
$builder->groupEnd();
|
||||
break;
|
||||
|
||||
case 'quotes':
|
||||
$builder->where('sale_status', SUSPENDED);
|
||||
$builder->where('sale_type', SALE_TYPE_QUOTE);
|
||||
break;
|
||||
|
||||
case 'work_orders':
|
||||
$builder->where('sale_status', SUSPENDED);
|
||||
$builder->where('sale_type', SALE_TYPE_WORK_ORDER);
|
||||
break;
|
||||
|
||||
case 'canceled':
|
||||
$builder->where('sale_status', CANCELED);
|
||||
break;
|
||||
|
||||
case 'returns':
|
||||
$builder->where('sale_status', COMPLETED);
|
||||
$builder->where('sale_type', SALE_TYPE_RETURN);
|
||||
break;
|
||||
}
|
||||
|
||||
$builder->groupBy('sale_id');
|
||||
$builder->orderBy('MAX(sale_time)');
|
||||
@@ -162,16 +209,56 @@ class Detailed_sales extends Report
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $inputs
|
||||
* @return array
|
||||
*/
|
||||
public function getSummaryData(array $inputs): array
|
||||
{
|
||||
$builder = $this->db->table('sales_items_temp');
|
||||
$builder->select('SUM(subtotal) AS subtotal, SUM(tax) AS tax, SUM(total) AS total, SUM(cost) AS cost, SUM(profit) AS profit');
|
||||
|
||||
if ($inputs['location_id'] != 'all') {
|
||||
if ($inputs['location_id'] != 'all') { // TODO: Duplicated code
|
||||
$builder->where('item_location', $inputs['location_id']);
|
||||
}
|
||||
|
||||
$this->applySaleTypeFilter($builder, $inputs['sale_type'], false);
|
||||
switch ($inputs['sale_type']) {
|
||||
case 'complete':
|
||||
$builder->where('sale_status', COMPLETED);
|
||||
$builder->groupStart();
|
||||
$builder->where('sale_type', SALE_TYPE_POS);
|
||||
$builder->orWhere('sale_type', SALE_TYPE_INVOICE);
|
||||
$builder->orWhere('sale_type', SALE_TYPE_RETURN);
|
||||
$builder->groupEnd();
|
||||
break;
|
||||
|
||||
case 'sales':
|
||||
$builder->where('sale_status', COMPLETED);
|
||||
$builder->groupStart();
|
||||
$builder->where('sale_type', SALE_TYPE_POS);
|
||||
$builder->orWhere('sale_type', SALE_TYPE_INVOICE);
|
||||
$builder->groupEnd();
|
||||
break;
|
||||
|
||||
case 'quotes':
|
||||
$builder->where('sale_status', SUSPENDED);
|
||||
$builder->where('sale_type', SALE_TYPE_QUOTE);
|
||||
break;
|
||||
|
||||
case 'work_orders':
|
||||
$builder->where('sale_status', SUSPENDED);
|
||||
$builder->where('sale_type', SALE_TYPE_WORK_ORDER);
|
||||
break;
|
||||
|
||||
case 'canceled':
|
||||
$builder->where('sale_status', CANCELED);
|
||||
break;
|
||||
|
||||
case 'returns':
|
||||
$builder->where('sale_status', COMPLETED);
|
||||
$builder->where('sale_type', SALE_TYPE_RETURN);
|
||||
break;
|
||||
}
|
||||
|
||||
return $builder->get()->getRowArray();
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ class Specific_customer extends Report
|
||||
MAX(payment_type) AS payment_type,
|
||||
MAX(comment) AS comment');
|
||||
|
||||
$builder->where('customer_id', $inputs['customer_id']);
|
||||
$builder->where('customer_id', $inputs['customer_id']); // TODO: Duplicated code
|
||||
|
||||
if ($inputs['payment_type'] == 'invoices') {
|
||||
$builder->where('sale_type', SALE_TYPE_INVOICE);
|
||||
@@ -139,7 +139,7 @@ class Specific_customer extends Report
|
||||
break;
|
||||
}
|
||||
|
||||
$builder->groupBy('sale_id');
|
||||
$builder->groupBy('sale_id'); // TODO: Duplicated code
|
||||
$builder->orderBy('MAX(sale_time)');
|
||||
|
||||
$data = [];
|
||||
|
||||
@@ -27,7 +27,7 @@ class Specific_discount extends Report
|
||||
* @return array
|
||||
*/
|
||||
public function getDataColumns(): array
|
||||
{
|
||||
{ // TODO: Duplicated code
|
||||
return [
|
||||
'summary' => [
|
||||
['id' => lang('Reports.sale_id')],
|
||||
@@ -95,7 +95,7 @@ class Specific_discount extends Report
|
||||
MAX(payment_type) AS payment_type,
|
||||
MAX(comment) AS comment');
|
||||
|
||||
$builder->where('discount >=', $inputs['discount']);
|
||||
$builder->where('discount >=', $inputs['discount']); // TODO: Duplicated code
|
||||
$builder->where('discount_type', $inputs['discount_type']);
|
||||
|
||||
switch ($inputs['sale_type']) {
|
||||
@@ -136,7 +136,7 @@ class Specific_discount extends Report
|
||||
break;
|
||||
}
|
||||
|
||||
$builder->groupBy('sale_id');
|
||||
$builder->groupBy('sale_id'); // TODO: Duplicated code
|
||||
$builder->orderBy('MAX(sale_time)');
|
||||
|
||||
$data = [];
|
||||
@@ -168,7 +168,7 @@ class Specific_discount extends Report
|
||||
$builder = $this->db->table('sales_items_temp');
|
||||
$builder->select('SUM(subtotal) AS subtotal, SUM(tax) AS tax, SUM(total) AS total, SUM(cost) AS cost, SUM(profit) AS profit');
|
||||
|
||||
$builder->where('discount >=', $inputs['discount']);
|
||||
$builder->where('discount >=', $inputs['discount']); // TODO: Duplicated code
|
||||
$builder->where('discount_type', $inputs['discount_type']);
|
||||
|
||||
// TODO: this needs to be converted to a switch statement
|
||||
|
||||
@@ -93,7 +93,7 @@ class Specific_employee extends Report
|
||||
MAX(payment_type) AS payment_type,
|
||||
MAX(comment) AS comment');
|
||||
|
||||
$builder->where('employee_id', $inputs['employee_id']);
|
||||
$builder->where('employee_id', $inputs['employee_id']); // TODO: Duplicated code
|
||||
|
||||
switch ($inputs['sale_type']) {
|
||||
case 'complete':
|
||||
@@ -164,7 +164,7 @@ class Specific_employee extends Report
|
||||
{
|
||||
$builder = $this->db->table('sales_items_temp');
|
||||
$builder->select('SUM(subtotal) AS subtotal, SUM(tax) AS tax, SUM(total) AS total, SUM(cost) AS cost, SUM(profit) AS profit');
|
||||
$builder->where('employee_id', $inputs['employee_id']);
|
||||
$builder->where('employee_id', $inputs['employee_id']); // TODO: Duplicated code
|
||||
|
||||
// TODO: this needs to be converted to a switch statement
|
||||
if ($inputs['sale_type'] == 'complete') {
|
||||
|
||||
@@ -77,7 +77,7 @@ class Specific_supplier extends Report
|
||||
MAX(discount_type) AS discount_type,
|
||||
MAX(discount) AS discount');
|
||||
|
||||
$builder->where('supplier_id', $inputs['supplier_id']);
|
||||
$builder->where('supplier_id', $inputs['supplier_id']); // TODO: Duplicated code
|
||||
|
||||
switch ($inputs['sale_type']) {
|
||||
case 'complete':
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
namespace App\Models\Reports;
|
||||
|
||||
use App\Traits\Models\Reports\ReportDateFilter;
|
||||
use Config\OSPOS;
|
||||
|
||||
class Summary_expenses_categories extends Summary_report
|
||||
{
|
||||
use ReportDateFilter;
|
||||
|
||||
protected function _get_data_columns(): array
|
||||
/**
|
||||
* @return array[]
|
||||
*/
|
||||
protected function _get_data_columns(): array // TODO: Hungarian notation
|
||||
{
|
||||
return [
|
||||
['category_name' => lang('Reports.expenses_category')],
|
||||
@@ -19,47 +19,6 @@ class Summary_expenses_categories extends Summary_report
|
||||
];
|
||||
}
|
||||
|
||||
public function getData(array $inputs): array
|
||||
{
|
||||
$config = config(OSPOS::class)->settings;
|
||||
|
||||
$builder = $this->db->table('expenses AS expenses');
|
||||
$builder->select('expense_categories.category_name AS category_name, COUNT(expenses.expense_id) AS count, SUM(expenses.amount) AS total_amount, SUM(expenses.tax_amount) AS total_tax_amount');
|
||||
$builder->join('expense_categories AS expense_categories', 'expense_categories.expense_category_id = expenses.expense_category_id', 'LEFT');
|
||||
|
||||
if (empty($config['date_or_time_format'])) {
|
||||
$builder->where('DATE(expenses.date) BETWEEN ' . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date']));
|
||||
} else {
|
||||
$builder->where('expenses.date BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date'])));
|
||||
}
|
||||
|
||||
$builder->where('expenses.deleted', 0);
|
||||
|
||||
$builder->groupBy('expense_categories.category_name');
|
||||
$builder->orderBy('expense_categories.category_name');
|
||||
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
|
||||
public function getSummaryData(array $inputs): array
|
||||
{
|
||||
$config = config(OSPOS::class)->settings;
|
||||
|
||||
$builder = $this->db->table('expenses AS expenses');
|
||||
$builder->select('SUM(expenses.amount) AS expenses_total_amount, SUM(expenses.tax_amount) AS expenses_total_tax_amount');
|
||||
|
||||
if (empty($config['date_or_time_format'])) {
|
||||
$builder->where('DATE(expenses.date) BETWEEN ' . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date']));
|
||||
} else {
|
||||
$builder->where('expenses.date BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date']));
|
||||
}
|
||||
|
||||
$builder->where('expenses.deleted', 0);
|
||||
|
||||
return $builder->get()->getRowArray();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $inputs
|
||||
* @return array
|
||||
@@ -72,7 +31,8 @@ class Summary_expenses_categories extends Summary_report
|
||||
$builder->select('expense_categories.category_name AS category_name, COUNT(expenses.expense_id) AS count, SUM(expenses.amount) AS total_amount, SUM(expenses.tax_amount) AS total_tax_amount');
|
||||
$builder->join('expense_categories AS expense_categories', 'expense_categories.expense_category_id = expenses.expense_category_id', 'LEFT');
|
||||
|
||||
if (empty($config['date_or_time_format'])) {
|
||||
// TODO: convert this to ternary notation
|
||||
if (empty($config['date_or_time_format'])) { // TODO: Duplicated code
|
||||
$builder->where('DATE(expenses.date) BETWEEN ' . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date']));
|
||||
} else {
|
||||
$builder->where('expenses.date BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date'])));
|
||||
@@ -86,6 +46,10 @@ class Summary_expenses_categories extends Summary_report
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $inputs
|
||||
* @return array
|
||||
*/
|
||||
public function getSummaryData(array $inputs): array
|
||||
{
|
||||
$config = config(OSPOS::class)->settings;
|
||||
@@ -93,7 +57,7 @@ class Summary_expenses_categories extends Summary_report
|
||||
$builder = $this->db->table('expenses AS expenses');
|
||||
$builder->select('SUM(expenses.amount) AS expenses_total_amount, SUM(expenses.tax_amount) AS expenses_total_tax_amount');
|
||||
|
||||
if (empty($config['date_or_time_format'])) {
|
||||
if (empty($config['date_or_time_format'])) { // TODO: Duplicated code
|
||||
$builder->where('DATE(expenses.date) BETWEEN ' . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date']));
|
||||
} else {
|
||||
$builder->where('expenses.date BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date'])));
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
namespace App\Models\Reports;
|
||||
|
||||
use App\Traits\Models\Reports\ReportDateFilter;
|
||||
use Config\OSPOS;
|
||||
|
||||
class Summary_payments extends Summary_report
|
||||
{
|
||||
use ReportDateFilter;
|
||||
|
||||
protected function _get_data_columns(): array
|
||||
/**
|
||||
* @return array[]
|
||||
*/
|
||||
protected function _get_data_columns(): array // TODO: Hungarian notation
|
||||
{
|
||||
return [
|
||||
['trans_group' => lang('Reports.trans_group')],
|
||||
@@ -22,9 +22,13 @@ class Summary_payments extends Summary_report
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $inputs
|
||||
* @return array
|
||||
*/
|
||||
public function getData(array $inputs): array
|
||||
{
|
||||
$cash_payment = lang('Sales.cash');
|
||||
$cash_payment = lang('Sales.cash'); // TODO: This is never used. Should it be?
|
||||
$config = config(OSPOS::class)->settings;
|
||||
|
||||
$separator[] = [
|
||||
@@ -37,7 +41,14 @@ class Summary_payments extends Summary_report
|
||||
'trans_due' => ''
|
||||
];
|
||||
|
||||
$where = $this->buildDateWhereClause($inputs);
|
||||
$where = ''; // TODO: Duplicated code
|
||||
|
||||
// TODO: this needs to be converted to ternary notation
|
||||
if (empty($config['date_or_time_format'])) {
|
||||
$where .= 'DATE(sale_time) BETWEEN ' . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date']);
|
||||
} else {
|
||||
$where .= 'sale_time BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date']));
|
||||
}
|
||||
|
||||
$this->create_summary_payments_temp_tables($where);
|
||||
|
||||
|
||||
@@ -2,20 +2,25 @@
|
||||
|
||||
namespace App\Models\Reports;
|
||||
|
||||
use App\Traits\Models\Reports\ReportDateFilter;
|
||||
use App\Traits\Models\Reports\SaleTypeFilter;
|
||||
use CodeIgniter\Database\BaseBuilder;
|
||||
use Config\OSPOS;
|
||||
use CodeIgniter\Database\BaseBuilder;
|
||||
|
||||
abstract class Summary_report extends Report
|
||||
{
|
||||
use ReportDateFilter;
|
||||
use SaleTypeFilter;
|
||||
|
||||
private function __common_select(array $inputs, &$builder): void
|
||||
/**
|
||||
* Private interface implementing the core basic functionality for all reports
|
||||
*/
|
||||
private function __common_select(array $inputs, &$builder): void // TODO: Hungarian notation
|
||||
{
|
||||
$config = config(OSPOS::class)->settings;
|
||||
$where = $this->buildDateWhereClause($inputs);
|
||||
// TODO: convert to using QueryBuilder. Use App/Models/Reports/Summary_taxes.php getData() as a reference template
|
||||
$where = ''; // TODO: Duplicated code
|
||||
|
||||
if (empty($config['date_or_time_format'])) {
|
||||
$where .= 'DATE(sale_time) BETWEEN ' . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date']);
|
||||
} else {
|
||||
$where .= 'sale_time BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date']));
|
||||
}
|
||||
|
||||
$decimals = totals_decimals();
|
||||
|
||||
@@ -105,16 +110,48 @@ abstract class Summary_report extends Report
|
||||
{
|
||||
$config = config(OSPOS::class)->settings;
|
||||
|
||||
$this->applyDateFilter($builder, $inputs);
|
||||
// TODO: Probably going to need to rework these since you can't reference $builder without it's instantiation.
|
||||
if (empty($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']));
|
||||
} else {
|
||||
$builder->where('sales.sale_time BETWEEN ' . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date'])));
|
||||
}
|
||||
|
||||
if ($inputs['location_id'] != 'all') {
|
||||
$builder->where('sales_items.item_location', $inputs['location_id']);
|
||||
}
|
||||
|
||||
$this->applySaleTypeFilter($builder, $inputs['sale_type']);
|
||||
if ($inputs['sale_type'] == 'complete') {
|
||||
$builder->where('sales.sale_status', COMPLETED);
|
||||
$builder->groupStart();
|
||||
$builder->where('sales.sale_type', SALE_TYPE_POS);
|
||||
$builder->orWhere('sales.sale_type', SALE_TYPE_INVOICE);
|
||||
$builder->orWhere('sales.sale_type', SALE_TYPE_RETURN);
|
||||
$builder->groupEnd();
|
||||
} elseif ($inputs['sale_type'] == 'sales') {
|
||||
$builder->where('sales.sale_status', COMPLETED);
|
||||
$builder->groupStart();
|
||||
$builder->where('sales.sale_type', SALE_TYPE_POS);
|
||||
$builder->orWhere('sales.sale_type', SALE_TYPE_INVOICE);
|
||||
$builder->groupEnd();
|
||||
} elseif ($inputs['sale_type'] == 'quotes') {
|
||||
$builder->where('sales.sale_status', SUSPENDED);
|
||||
$builder->where('sales.sale_type', SALE_TYPE_QUOTE);
|
||||
} elseif ($inputs['sale_type'] == 'work_orders') {
|
||||
$builder->where('sales.sale_status', SUSPENDED);
|
||||
$builder->where('sales.sale_type', SALE_TYPE_WORK_ORDER);
|
||||
} elseif ($inputs['sale_type'] == 'canceled') {
|
||||
$builder->where('sales.sale_status', CANCELED);
|
||||
} elseif ($inputs['sale_type'] == 'returns') {
|
||||
$builder->where('sales.sale_status', COMPLETED);
|
||||
$builder->where('sales.sale_type', SALE_TYPE_RETURN);
|
||||
}
|
||||
}
|
||||
|
||||
abstract protected function _get_data_columns(): array;
|
||||
/**
|
||||
* Protected class interface implemented by derived classes where required
|
||||
*/
|
||||
abstract protected function _get_data_columns(): array; // TODO: hungarian notation
|
||||
|
||||
/**
|
||||
* @param array $inputs
|
||||
|
||||
@@ -2,13 +2,10 @@
|
||||
|
||||
namespace App\Models\Reports;
|
||||
|
||||
use App\Traits\Models\Reports\ReportDateFilter;
|
||||
use Config\OSPOS;
|
||||
|
||||
class Summary_sales_taxes extends Summary_report
|
||||
{
|
||||
use ReportDateFilter;
|
||||
|
||||
private array $config;
|
||||
|
||||
public function __construct()
|
||||
@@ -17,7 +14,10 @@ class Summary_sales_taxes extends Summary_report
|
||||
$this->config = config(OSPOS::class)->settings;
|
||||
}
|
||||
|
||||
protected function _get_data_columns(): array
|
||||
/**
|
||||
* @return array[]
|
||||
*/
|
||||
protected function _get_data_columns(): array // TODO: hungarian notation
|
||||
{
|
||||
return [
|
||||
['reporting_authority' => lang('Reports.authority')],
|
||||
@@ -28,16 +28,39 @@ class Summary_sales_taxes extends Summary_report
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $inputs
|
||||
* @param object $builder
|
||||
* @return void
|
||||
*/
|
||||
protected function _where(array $inputs, object &$builder): void
|
||||
{
|
||||
$builder->where('sales.sale_status', COMPLETED);
|
||||
$this->applyDateFilter($builder, $inputs, 'sales', 'sale_time');
|
||||
|
||||
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 >=', $inputs['start_date']);
|
||||
$builder->where('sales.sale_time <=', $inputs['end_date']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $inputs
|
||||
* @return array
|
||||
*/
|
||||
public function getData(array $inputs): array
|
||||
{
|
||||
$builder = $this->db->table('sales_taxes');
|
||||
$this->applyDateFilter($builder, $inputs, 'sales_taxes', 'sale_time');
|
||||
|
||||
if (empty($this->config['date_or_time_format'])) {
|
||||
$builder->where('DATE(sale_time) >=', $inputs['start_date']);
|
||||
$builder->where('DATE(sale_time) <=', $inputs['end_date']);
|
||||
} else {
|
||||
$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');
|
||||
$builder->join('sales', 'sales_taxes.sale_id = sales.sale_id', 'left');
|
||||
|
||||
@@ -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();
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits\Controller;
|
||||
|
||||
use Config\OSPOS;
|
||||
|
||||
/**
|
||||
* Shared trait for common controller functionality
|
||||
*/
|
||||
trait Shared
|
||||
{
|
||||
/**
|
||||
* Build supplier info array for views
|
||||
*
|
||||
* @param object $supplier_info Supplier info object
|
||||
* @param array $data Data array to populate
|
||||
* @return void
|
||||
*/
|
||||
protected function buildSupplierInfo(object $supplier_info, array &$data): void
|
||||
{
|
||||
$data['supplier'] = $supplier_info->company_name;
|
||||
$data['first_name'] = $supplier_info->first_name;
|
||||
$data['last_name'] = $supplier_info->last_name;
|
||||
$data['supplier_email'] = $supplier_info->email;
|
||||
$data['supplier_address'] = $supplier_info->address_1;
|
||||
if (!empty($supplier_info->zip) || !empty($supplier_info->city)) {
|
||||
$data['supplier_location'] = $supplier_info->zip . ' ' . $supplier_info->city;
|
||||
} else {
|
||||
$data['supplier_location'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get mode label and customer required text based on sale mode
|
||||
*
|
||||
* @param string $mode The sale mode
|
||||
* @return array{mode_label: string, customer_required: string}
|
||||
*/
|
||||
protected function getSaleModeLabel(string $mode): array
|
||||
{
|
||||
return match ($mode) {
|
||||
'sale_invoice' => [
|
||||
'mode_label' => lang('Sales.invoice'),
|
||||
'customer_required' => lang('Sales.customer_required')
|
||||
],
|
||||
'sale_quote' => [
|
||||
'mode_label' => lang('Sales.quote'),
|
||||
'customer_required' => lang('Sales.customer_required')
|
||||
],
|
||||
'sale_work_order' => [
|
||||
'mode_label' => lang('Sales.work_order'),
|
||||
'customer_required' => lang('Sales.customer_required')
|
||||
],
|
||||
'return' => [
|
||||
'mode_label' => lang('Sales.return'),
|
||||
'customer_required' => lang('Sales.customer_optional')
|
||||
],
|
||||
default => [
|
||||
'mode_label' => lang('Sales.receipt'),
|
||||
'customer_required' => lang('Sales.customer_optional')
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build company info string from config
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function buildCompanyInfo(): string
|
||||
{
|
||||
$config = config(OSPOS::class)->settings;
|
||||
$company_info = implode("\n", [$config['address'], $config['phone']]);
|
||||
|
||||
if (!empty($config['account_number'])) {
|
||||
$company_info .= "\n" . lang('Sales.account_number') . ": " . $config['account_number'];
|
||||
}
|
||||
if (!empty($config['tax_id'])) {
|
||||
$company_info .= "\n" . lang('Sales.tax_id') . ": " . $config['tax_id'];
|
||||
}
|
||||
|
||||
return $company_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize default tax code data for new entry
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function initDefaultTaxCodeData(): array
|
||||
{
|
||||
return [
|
||||
'tax_code' => '',
|
||||
'tax_code_name' => '',
|
||||
'tax_code_type' => '0',
|
||||
'city' => '',
|
||||
'state' => '',
|
||||
'tax_rate' => '0.0000',
|
||||
'rate_tax_code' => '',
|
||||
'rate_tax_category_id' => 1,
|
||||
'tax_category' => '',
|
||||
'add_tax_category' => '',
|
||||
'rounding_code' => '0'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate tax code data from existing tax code info
|
||||
*
|
||||
* @param object $tax_code_info Tax code info object
|
||||
* @param object $tax_rate_info Tax rate info object
|
||||
* @return array
|
||||
*/
|
||||
protected function buildTaxCodeData(object $tax_code_info, object $tax_rate_info): array
|
||||
{
|
||||
return [
|
||||
'tax_code' => $tax_code_info->tax_code,
|
||||
'tax_code_name' => $tax_code_info->tax_code_name,
|
||||
'tax_code_type' => $tax_code_info->tax_code_type,
|
||||
'city' => $tax_code_info->city,
|
||||
'state' => $tax_code_info->state,
|
||||
'rate_tax_code' => $tax_code_info->rate_tax_code,
|
||||
'rate_tax_category_id' => $tax_code_info->rate_tax_category_id,
|
||||
'tax_category' => $tax_code_info->tax_category,
|
||||
'add_tax_category' => '',
|
||||
'tax_rate' => $tax_rate_info->tax_rate,
|
||||
'rounding_code' => $tax_rate_info->rounding_code
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits\Database;
|
||||
|
||||
trait SalesTaxMigration
|
||||
{
|
||||
protected function cleanIdentifier(string $string): string
|
||||
{
|
||||
$string = str_replace(' ', '-', $string);
|
||||
return preg_replace('/[^A-Za-z0-9\-]/', '', $string);
|
||||
}
|
||||
|
||||
protected function applyInvoiceTaxing(array &$salesTaxes): void
|
||||
{
|
||||
if (!empty($salesTaxes)) {
|
||||
$sort = [];
|
||||
foreach ($salesTaxes as $k => $v) {
|
||||
$sort['print_sequence'][$k] = $v['print_sequence'];
|
||||
}
|
||||
array_multisort($sort['print_sequence'], SORT_ASC, $salesTaxes);
|
||||
}
|
||||
|
||||
$decimals = totals_decimals();
|
||||
|
||||
foreach ($salesTaxes as $rowNumber => $salesTax) {
|
||||
$salesTaxes[$rowNumber]['sale_tax_amount'] = $this->getSalesTaxForAmount(
|
||||
$salesTax['sale_tax_basis'],
|
||||
$salesTax['tax_rate'],
|
||||
$salesTax['rounding_code'],
|
||||
$decimals
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function roundSalesTaxes(array &$salesTaxes, int $halfUp = 1, int $roundUp = 5, int $roundDown = 6, int $halfFive = 7): void
|
||||
{
|
||||
if (!empty($salesTaxes)) {
|
||||
$sort = [];
|
||||
foreach ($salesTaxes as $k => $v) {
|
||||
$sort['print_sequence'][$k] = $v['print_sequence'];
|
||||
}
|
||||
array_multisort($sort['print_sequence'], SORT_ASC, $salesTaxes);
|
||||
}
|
||||
|
||||
$decimals = totals_decimals();
|
||||
|
||||
foreach ($salesTaxes as $rowNumber => $salesTax) {
|
||||
$saleTaxAmount = (float)$salesTax['sale_tax_amount'];
|
||||
$roundingCode = $salesTax['rounding_code'];
|
||||
$roundedSaleTaxAmount = $saleTaxAmount;
|
||||
|
||||
if (
|
||||
$roundingCode == PHP_ROUND_HALF_UP
|
||||
|| $roundingCode == PHP_ROUND_HALF_DOWN
|
||||
|| $roundingCode == PHP_ROUND_HALF_EVEN
|
||||
|| $roundingCode == PHP_ROUND_HALF_ODD
|
||||
) {
|
||||
$roundedSaleTaxAmount = round($saleTaxAmount, $decimals, $roundingCode);
|
||||
} elseif ($roundingCode == $roundUp) {
|
||||
$fig = (int) str_pad('1', $decimals, '0');
|
||||
$roundedSaleTaxAmount = (ceil($saleTaxAmount * $fig) / $fig);
|
||||
} elseif ($roundingCode == $roundDown) {
|
||||
$fig = (int) str_pad('1', $decimals, '0');
|
||||
$roundedSaleTaxAmount = (floor($saleTaxAmount * $fig) / $fig);
|
||||
} elseif ($roundingCode == $halfFive) {
|
||||
$roundedSaleTaxAmount = round($saleTaxAmount / 5) * 5;
|
||||
}
|
||||
|
||||
$salesTaxes[$rowNumber]['sale_tax_amount'] = $roundedSaleTaxAmount;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits\Models\Reports;
|
||||
|
||||
use CodeIgniter\Database\BaseBuilder;
|
||||
use Config\OSPOS;
|
||||
|
||||
trait ReportDateFilter
|
||||
{
|
||||
protected function buildDateWhereClause(array $inputs, string $dateColumn = 'sale_time'): string
|
||||
{
|
||||
$config = config(OSPOS::class)->settings;
|
||||
|
||||
if (empty($config['date_or_time_format'])) {
|
||||
return "DATE({$dateColumn}) BETWEEN " . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date']);
|
||||
}
|
||||
return "{$dateColumn} BETWEEN " . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date']));
|
||||
}
|
||||
|
||||
protected function applyDateFilter(BaseBuilder $builder, array $inputs, string $tablePrefix = 'sales', string $column = 'sale_time'): void
|
||||
{
|
||||
$config = config(OSPOS::class)->settings;
|
||||
|
||||
if (empty($config['date_or_time_format'])) {
|
||||
$builder->where("DATE({$tablePrefix}.{$column}) BETWEEN " . $this->db->escape($inputs['start_date']) . ' AND ' . $this->db->escape($inputs['end_date']));
|
||||
} else {
|
||||
$builder->where("{$tablePrefix}.{$column} BETWEEN " . $this->db->escape(rawurldecode($inputs['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($inputs['end_date'])));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits\Models\Reports;
|
||||
|
||||
use CodeIgniter\Database\BaseBuilder;
|
||||
|
||||
trait SaleTypeFilter
|
||||
{
|
||||
protected function applySaleTypeFilter(BaseBuilder $builder, string $saleType, bool $usePrefix = true): void
|
||||
{
|
||||
$prefix = $usePrefix ? 'sales.' : '';
|
||||
|
||||
if ($saleType === 'complete') {
|
||||
$builder->where("{$prefix}sale_status", COMPLETED);
|
||||
$builder->groupStart();
|
||||
$builder->where("{$prefix}sale_type", SALE_TYPE_POS);
|
||||
$builder->orWhere("{$prefix}sale_type", SALE_TYPE_INVOICE);
|
||||
$builder->orWhere("{$prefix}sale_type", SALE_TYPE_RETURN);
|
||||
$builder->groupEnd();
|
||||
} elseif ($saleType === 'sales') {
|
||||
$builder->where("{$prefix}sale_status", COMPLETED);
|
||||
$builder->groupStart();
|
||||
$builder->where("{$prefix}sale_type", SALE_TYPE_POS);
|
||||
$builder->orWhere("{$prefix}sale_type", SALE_TYPE_INVOICE);
|
||||
$builder->groupEnd();
|
||||
} elseif ($saleType === 'quotes') {
|
||||
$builder->where("{$prefix}sale_status", SUSPENDED);
|
||||
$builder->where("{$prefix}sale_type", SALE_TYPE_QUOTE);
|
||||
} elseif ($saleType === 'work_orders') {
|
||||
$builder->where("{$prefix}sale_status", SUSPENDED);
|
||||
$builder->where("{$prefix}sale_type", SALE_TYPE_WORK_ORDER);
|
||||
} elseif ($saleType === 'canceled') {
|
||||
$builder->where("{$prefix}sale_status", CANCELED);
|
||||
} elseif ($saleType === 'returns') {
|
||||
$builder->where("{$prefix}sale_status", COMPLETED);
|
||||
$builder->where("{$prefix}sale_type", SALE_TYPE_RETURN);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
) ?>
|
||||
</div>
|
||||
<div class="col-sm-7">
|
||||
<a href="<?= 'https://bootswatch.com/3/' . ('bootstrap' == ($config['theme']) ? 'default' : esc($config['theme'])) ?>" target="_blank" rel=”noopener”>
|
||||
<a href="<?= 'https://bootswatch.com/3/' . ('bootstrap' == ($config['theme']) ? 'default' : esc($config['theme'])) ?>" target="_blank" rel="noopener">
|
||||
<span><?= lang('Config.theme_preview') . ' ' . ucfirst(esc($config['theme'])) . ' ' ?></span>
|
||||
<span class="glyphicon glyphicon-new-window"></span>
|
||||
</a>
|
||||
@@ -130,14 +130,17 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group form-group-sm">
|
||||
<?= form_label(lang('Config.receiving_calculate_average_price'), 'receiving_calculate_average_price', ['class' => 'control-label col-xs-2']) ?>
|
||||
<div class="col-xs-1">
|
||||
<?= form_checkbox([
|
||||
'name' => 'receiving_calculate_average_price',
|
||||
'id' => 'receiving_calculate_average_price',
|
||||
'value' => 'receiving_calculate_average_price',
|
||||
'checked' => $config['receiving_calculate_average_price'] == 1
|
||||
]) ?>
|
||||
<?= form_label(lang('Config.receiving_cost_price_method'), 'receiving_cost_price_method', ['class' => 'control-label col-xs-2']) ?>
|
||||
<div class="col-xs-3">
|
||||
<?= form_dropdown(
|
||||
'receiving_cost_price_method',
|
||||
[
|
||||
'average' => lang('Config.receiving_cost_price_method_average'),
|
||||
'new' => lang('Config.receiving_cost_price_method_new'),
|
||||
],
|
||||
(($config['receiving_cost_price_method'] ?? (($config['receiving_calculate_average_price'] ?? 1) ? 'average' : 'new'))),
|
||||
['id' => 'receiving_cost_price_method', 'class' => 'form-control']
|
||||
) ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -278,7 +281,7 @@
|
||||
'checked' => $config['gcaptcha_enable'] == 1
|
||||
]) ?>
|
||||
<label class="control-label">
|
||||
<a href="https://www.google.com/recaptcha/admin" target="_blank">
|
||||
<a href="https://www.google.com/recaptcha/admin" target="_blank" rel="noopener noreferrer">
|
||||
<span class="glyphicon glyphicon-info-sign" data-toggle="tooltip" data-placement="right" title="<?= lang('Config.gcaptcha_tooltip') ?>"></span>
|
||||
</a>
|
||||
</label>
|
||||
@@ -405,6 +408,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group form-group-sm">
|
||||
<?= form_label(lang('Config.customer_display'), 'customer_display_enabled', ['class' => 'control-label col-xs-2']) ?>
|
||||
<div class="col-xs-1">
|
||||
<?= form_checkbox([
|
||||
'name' => 'customer_display_enabled',
|
||||
'id' => 'customer_display_enabled',
|
||||
'value' => 'customer_display_enabled',
|
||||
'checked' => ($config['customer_display_enabled'] ?? 1) == 1
|
||||
]) ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group form-group-sm">
|
||||
<?= form_label(lang('Config.show_office_group'), 'show_office_group', ['class' => 'control-label col-xs-2']) ?>
|
||||
<div class="col-xs-1">
|
||||
@@ -441,6 +456,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group form-group-sm">
|
||||
<?= form_label(lang('Config.category_dropdown'), 'category_dropdown', ['class' => 'control-label col-xs-2']) ?>
|
||||
<div class="col-xs-1">
|
||||
@@ -541,3 +557,6 @@
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
249
app/Views/partial/customer_display_header.php
Normal file
249
app/Views/partial/customer_display_header.php
Normal file
@@ -0,0 +1,249 @@
|
||||
<?php
|
||||
/**
|
||||
* @var array $config
|
||||
* @var string $companyName
|
||||
* @var string $companyDetails
|
||||
*/
|
||||
|
||||
helper('url');
|
||||
?>
|
||||
|
||||
<!doctype html>
|
||||
<html lang="<?= esc(service('request')->getLocale()) ?>">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><?= lang('Sales.customer_display') ?></title>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="<?= base_url('images/favicon.ico') ?>">
|
||||
<link rel="stylesheet" href="<?= base_url('resources/bootswatch/' . (empty($config['theme']) ? 'flatly' : esc($config['theme'])) . '/bootstrap.min.css') ?>">
|
||||
<link rel="stylesheet" href="<?= base_url('resources/opensourcepos-8e34d6a398.min.css') ?>">
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #f8f8f8;
|
||||
color: #333;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.customer-display-header {
|
||||
background: #1f3143;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
padding: 6px 12px;
|
||||
border-bottom: 1px solid #102131;
|
||||
}
|
||||
|
||||
.customer-display-shell {
|
||||
width: 100%;
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 12px 18px 18px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.customer-display-company {
|
||||
text-align: center;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.customer-display-company img {
|
||||
display: block;
|
||||
margin: 0 auto 6px;
|
||||
max-height: 84px;
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.customer-display-company .company-name {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.customer-display-company .company-details {
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.customer-display-company .company-phone {
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.customer-display-main-row {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: flex-start;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.customer-display-cart-column {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.customer-display-summary-column {
|
||||
flex: 0 0 320px;
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel,
|
||||
.customer-display-info-panel,
|
||||
.customer-display-items-panel {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel .panel-heading,
|
||||
.customer-display-info-panel .panel-heading,
|
||||
.customer-display-items-panel .panel-heading {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel .table,
|
||||
.customer-display-info-table {
|
||||
margin-bottom: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel .table > tbody > tr > th,
|
||||
.customer-display-info-table > tbody > tr > th {
|
||||
background: #f8fbfd;
|
||||
width: 56%;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel .table > tbody > tr > td,
|
||||
.customer-display-info-table > tbody > tr > td {
|
||||
width: 44%;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel .rate-row th,
|
||||
.customer-display-summary-panel .rate-row td {
|
||||
color: #c00000;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel .summary-section-row th {
|
||||
background: #eaf2f8;
|
||||
color: #1f3b5b;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel .summary-subtable {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel .summary-subtable > tbody > tr > th {
|
||||
background: #fdfefe;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel .summary-subtable > tbody > tr > td {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.register-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#register {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
table-layout: fixed;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#register th,
|
||||
#register td {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
padding: 6px 5px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#register thead th {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#register tbody td {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
#register tbody td.item-name-cell {
|
||||
font-size: 16px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#register tbody td.price-cell {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
#register tbody td.serial-cell {
|
||||
font-size: 12px;
|
||||
color: #2F4F4F;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel .table > tbody > tr > th,
|
||||
.customer-display-info-table > tbody > tr > th {
|
||||
border-top: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel .table > tbody > tr > td,
|
||||
.customer-display-info-table > tbody > tr > td {
|
||||
border-top: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel .panel-body,
|
||||
.customer-display-info-panel .panel-body,
|
||||
.customer-display-items-panel .panel-body {
|
||||
padding: 12px 15px;
|
||||
}
|
||||
|
||||
.customer-display-summary-column .panel-body {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.customer-display-summary-column .customer-name-value,
|
||||
.customer-display-summary-column .giftcard-value,
|
||||
.customer-display-summary-column .reward-value {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.customer-display-footer {
|
||||
margin-top: 14px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #777;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="customer-display-header">Open Source Point of Sale</div>
|
||||
<div class="customer-display-shell">
|
||||
<div class="customer-display-company">
|
||||
<?php if (!empty($config['company_logo'])) { ?>
|
||||
<img src="<?= base_url('uploads/' . esc($config['company_logo'], 'url')) ?>" alt="company_logo">
|
||||
<?php } ?>
|
||||
<div class="company-name"><?= esc($companyName) ?></div>
|
||||
<div class="company-phone">Phone: <?= esc((string)($config['phone'] ?? '')) ?></div>
|
||||
<?php if ($companyDetails !== '') { ?>
|
||||
<div class="company-details"><?= nl2br(esc($companyDetails)) ?></div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<div class="customer-display-main-row">
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
224
app/Views/sales/customer_display.php
Normal file
224
app/Views/sales/customer_display.php
Normal file
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
/**
|
||||
* @var array $cart
|
||||
* @var array $config
|
||||
* @var float $rate
|
||||
* @var float $total
|
||||
* @var float $subtotal
|
||||
* @var float $prediscount_subtotal
|
||||
* @var array $taxes
|
||||
* @var array $payments
|
||||
* @var float $amount_change
|
||||
*/
|
||||
|
||||
$priceWithCurrencyLabel = lang('Sales.price_with_currency');
|
||||
|
||||
?>
|
||||
|
||||
<?= view('partial/customer_display_header') ?>
|
||||
|
||||
<div class="customer-display-cart-column">
|
||||
<div class="register-wrap">
|
||||
<div class="panel panel-default customer-display-items-panel">
|
||||
<div class="panel-heading"><?= lang('Sales.items') ?></div>
|
||||
<div class="panel-body table-responsive">
|
||||
<table class="table table-striped table-condensed" id="register">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: <?= (int) $cartItemWidth ?>%;"><?= lang('Sales.item_name') ?></th>
|
||||
<?php if ($cartHasCustomerDisplay) { ?>
|
||||
<th style="width: <?= (int) $cartPriceWidth ?>%;"><?= sprintf($priceWithCurrencyLabel, esc($customerDisplayCurrencyLabel)) ?></th>
|
||||
<?php } ?>
|
||||
<th style="width: <?= (int) $cartOriginalWidth ?>%;"><?= sprintf($priceWithCurrencyLabel, esc($originalCurrencyLabel)) ?></th>
|
||||
<th style="width: <?= (int) $cartQuantityWidth ?>%;"><?= lang('Sales.quantity') ?></th>
|
||||
<th style="width: <?= (int) $cartDiscountWidth ?>%;"><?= lang('Sales.discount') ?></th>
|
||||
<th style="width: <?= (int) $cartTotalWidth ?>%;"><?= lang('Sales.total') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="cart_contents">
|
||||
<?php if (count($cart) == 0) { ?>
|
||||
<tr>
|
||||
<td colspan="<?= (int) $cartColspan ?>">
|
||||
<div class="alert alert-dismissible alert-info"><?= lang('Sales.no_items_in_cart') ?></div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php } else { ?>
|
||||
<?php foreach (array_reverse($cart, true) as $line => $item) { ?>
|
||||
<tr>
|
||||
<td class="item-name-cell">
|
||||
<?= esc($item['name']) ?><br>
|
||||
<?= !empty($item['attribute_values']) ? esc($item['attribute_values']) : '' ?>
|
||||
</td>
|
||||
<?php if ($cartHasCustomerDisplay) { ?>
|
||||
<td class="price-cell">
|
||||
<?= to_secondary_currency((float)$item['price'], $secondaryCurrency) ?>
|
||||
</td>
|
||||
<?php } ?>
|
||||
<td class="price-cell">
|
||||
<?= to_currency($item['price']) ?>
|
||||
</td>
|
||||
<td class="price-cell">
|
||||
<?= to_quantity_decimals($item['quantity']) ?>
|
||||
</td>
|
||||
<td class="price-cell">
|
||||
<?= to_decimals($item['discount'], 0) ?>
|
||||
</td>
|
||||
<td class="price-cell">
|
||||
<?= $item['item_type'] == ITEM_AMOUNT_ENTRY ? to_currency_no_money($item['discounted_total']) : to_currency($item['discounted_total']) ?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="<?= $cartHasCustomerDisplay ? 3 : 2 ?>"></td>
|
||||
<td class="serial-cell">
|
||||
<?= $item['is_serialized'] == 1 ? lang('Sales.serial') : '' ?>
|
||||
</td>
|
||||
<td colspan="2" class="serial-cell">
|
||||
<?php if ($item['is_serialized'] == 1) {
|
||||
echo esc($item['serialnumber']);
|
||||
} ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="customer-display-summary-column">
|
||||
<div class="panel panel-primary customer-display-summary-panel">
|
||||
<div class="panel-heading"><?= lang('Sales.summary') ?></div>
|
||||
<div class="panel-body">
|
||||
<table class="table table-condensed summary-subtable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th><?= lang('Sales.total') ?></th>
|
||||
<td><?= to_currency($total) ?></td>
|
||||
</tr>
|
||||
<?php if ($showCustomerDisplay): ?>
|
||||
<tr>
|
||||
<th><?= lang('Sales.total') ?> <?= esc($customerDisplayCurrencyLabel) ?></th>
|
||||
<td><?= to_secondary_currency((float)$total, $secondaryCurrency) ?></td>
|
||||
</tr>
|
||||
<tr class="rate-row">
|
||||
<th><?= lang('Sales.rate') ?></th>
|
||||
<td><?= number_format((float) $rate, 2) ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="table table-condensed summary-subtable" style="margin-top: 10px;">
|
||||
<tbody>
|
||||
<tr class="summary-section-row">
|
||||
<th colspan="2"><?= lang('Sales.customer') ?></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><?= lang('Sales.customer_name') ?></th>
|
||||
<td class="customer-name-value"><?= esc($customerName ?? lang('Sales.walk_in_customer')) ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><?= lang('Sales.giftcard_balance') ?></th>
|
||||
<td class="giftcard-value"><?= to_currency((float) ($giftcardRemainder ?? 0)) ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><?= lang('Sales.loyalty_reward_points') ?></th>
|
||||
<td class="reward-value"><?= esc((string)($customerRewardPoints ?? 0)) ?></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="table table-condensed summary-subtable" style="margin-top: 10px;">
|
||||
<tbody>
|
||||
<tr class="summary-section-row">
|
||||
<th colspan="2"><?= lang('Sales.change') ?></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><?= lang('Sales.payments_total') ?></th>
|
||||
<td><?= to_currency($payments_total) ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><?= lang('Sales.amount_due') ?></th>
|
||||
<td><?= to_currency($amount_due) ?></td>
|
||||
</tr>
|
||||
<?php if ($showCustomerDisplay): ?>
|
||||
<tr>
|
||||
<th><?= lang('Sales.amount_due') ?> <?= esc($customerDisplayCurrencyLabel) ?></th>
|
||||
<td><?= to_secondary_currency((float)$amount_due, $secondaryCurrency) ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<tr>
|
||||
<th><?= lang('Sales.change_due') ?></th>
|
||||
<td><?= to_currency($paymentChangeDue ?? 0) ?></td>
|
||||
</tr>
|
||||
<?php if ($showCustomerDisplay): ?>
|
||||
<tr>
|
||||
<th><?= lang('Sales.change_due') ?> <?= esc($customerDisplayCurrencyLabel) ?></th>
|
||||
<td><?= to_secondary_currency((float)($paymentChangeDue ?? 0), $secondaryCurrency) ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="customer-display-footer"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const customerDisplayId = new URLSearchParams(window.location.search).get('displayId') || '';
|
||||
const customerDisplayStorageSuffix = customerDisplayId !== '' ? '_' + customerDisplayId : '';
|
||||
const customerDisplayStorageKeys = {
|
||||
open: 'customerDisplayOpen' + customerDisplayStorageSuffix,
|
||||
dirtyAt: 'customerDisplayDirtyAt' + customerDisplayStorageSuffix
|
||||
};
|
||||
|
||||
localStorage.setItem(customerDisplayStorageKeys.open, '1');
|
||||
|
||||
let lastDirtyAt = localStorage.getItem(customerDisplayStorageKeys.dirtyAt) || '';
|
||||
let refreshTimer = null;
|
||||
|
||||
const scheduleRefresh = function(dirtyAt) {
|
||||
if (refreshTimer !== null) {
|
||||
clearTimeout(refreshTimer);
|
||||
}
|
||||
|
||||
refreshTimer = setTimeout(function() {
|
||||
if (localStorage.getItem(customerDisplayStorageKeys.open) !== '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (localStorage.getItem(customerDisplayStorageKeys.dirtyAt) === dirtyAt) {
|
||||
window.location.reload();
|
||||
}
|
||||
}, 700);
|
||||
};
|
||||
|
||||
const checkForRefresh = function() {
|
||||
const dirtyAt = localStorage.getItem(customerDisplayStorageKeys.dirtyAt) || '';
|
||||
if (dirtyAt !== '' && dirtyAt !== lastDirtyAt) {
|
||||
lastDirtyAt = dirtyAt;
|
||||
scheduleRefresh(dirtyAt);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('storage', function(event) {
|
||||
if (event.key === customerDisplayStorageKeys.dirtyAt) {
|
||||
checkForRefresh();
|
||||
}
|
||||
});
|
||||
|
||||
setInterval(checkForRefresh, 500);
|
||||
|
||||
window.addEventListener('beforeunload', function() {
|
||||
localStorage.removeItem(customerDisplayStorageKeys.open);
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -1,3 +1,24 @@
|
||||
<?php
|
||||
/**
|
||||
* @var array $keyboardShortcuts
|
||||
*/
|
||||
|
||||
$keyboardShortcuts ??= [];
|
||||
|
||||
$shortcut_labels = [
|
||||
'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')
|
||||
];
|
||||
?>
|
||||
|
||||
<div class="container-fluid">
|
||||
|
||||
<ul class="nav nav-tabs" id="SCTabs" data-toggle="tab">
|
||||
@@ -15,46 +36,13 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>ESC</code></td>
|
||||
<td><?= lang('Sales.key_cancel'); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ALT + 1</code></td>
|
||||
<td><?= lang('Sales.key_item_search'); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ALT + 2</code></td>
|
||||
<td><?= lang('Sales.key_customer_search'); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ALT + 3</code></td>
|
||||
<td><?= lang('Sales.key_suspend'); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ALT + 4</code></td>
|
||||
<td><?= lang('Sales.key_suspended'); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ALT + 5</code></td>
|
||||
<td><?= lang('Sales.key_tendered'); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ALT + 6</code></td>
|
||||
<td><?= lang('Sales.key_payment'); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ALT + 7</code></td>
|
||||
<td><?= lang('Sales.key_finish_sale'); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ALT + 8</code></td>
|
||||
<td><?= lang('Sales.key_finish_quote'); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ALT + 9</code></td>
|
||||
<td><?= lang('Sales.key_help_modal'); ?></td>
|
||||
</tr>
|
||||
<?php foreach ($shortcut_labels as $name => $label): ?>
|
||||
<?php $shortcut = $keyboardShortcuts[$name] ?? ['label' => '', 'code' => '']; ?>
|
||||
<tr>
|
||||
<td><code><?= esc($shortcut['label'] !== '' ? $shortcut['label'] : $shortcut['code']) ?></code></td>
|
||||
<td><?= esc($label) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
?>
|
||||
|
||||
<!doctype html>
|
||||
<html lang="<?= $this->request->getLocale() ?>">
|
||||
<html lang="<?= current_language_code() ?>">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
/**
|
||||
* @var int $sale_id_num
|
||||
* @var bool $print_after_sale
|
||||
* @var string $receipt_template_view
|
||||
* @var array $config
|
||||
*/
|
||||
|
||||
use App\Models\Employee;
|
||||
|
||||
$template = $receipt_template_view ?? 'receipt_default';
|
||||
|
||||
?>
|
||||
|
||||
<?= view('partial/header') ?>
|
||||
@@ -61,6 +64,6 @@ if (isset($error_message)) {
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?= view('sales/' . $config['receipt_template']) ?>
|
||||
<?= view('sales/' . $template) ?>
|
||||
|
||||
<?= view('partial/footer') ?>
|
||||
|
||||
@@ -61,6 +61,20 @@ if (isset($success)) {
|
||||
helper('url');
|
||||
?>
|
||||
|
||||
<?php if ($secondaryCurrency['show']): ?>
|
||||
<?php $secondaryCurrencyLabel = $secondaryCurrency['symbol'] ?: $secondaryCurrency['code']; ?>
|
||||
<table align="center" style="font-size: 22px; font-weight: 600; background-color: rgb(221, 221, 221); width: 25%; margin: 0 auto 0.5em; border: dashed 1px;">
|
||||
<tr>
|
||||
<td style="text-align: center; padding-right: 5%;"><?= lang(ucfirst($controller_name) . '.total') ?>:</td>
|
||||
<td style="text-align: center;"><?= to_currency($total) ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="text-align: center; padding-right: 5%;"><?= lang(ucfirst($controller_name) . '.total') ?> <?= esc($secondaryCurrencyLabel) ?>:</td>
|
||||
<td style="text-align: center;"><?= $secondaryTotalDisplay ?? to_secondary_currency((float) $total, $secondaryCurrency) ?></td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
|
||||
<div id="register_wrapper">
|
||||
|
||||
<!-- Top register controls -->
|
||||
@@ -90,6 +104,16 @@ helper('url');
|
||||
</li>
|
||||
<?php } ?>
|
||||
|
||||
<?php if (($config['customer_display_enabled'] ?? true) == 1) { ?>
|
||||
<li class="pull-right">
|
||||
<?= anchor(
|
||||
"$controller_name/customerDisplay",
|
||||
'<span class="glyphicon glyphicon-blackboard"> </span>' . lang(ucfirst($controller_name) . '.customer_display'),
|
||||
['class' => 'btn btn-success btn-sm', 'id' => 'show_customer_display', 'title' => lang(ucfirst($controller_name) . '.customer_display'), 'onclick' => 'return openCustomerDisplay(this.href);']
|
||||
) ?>
|
||||
</li>
|
||||
<?php } ?>
|
||||
|
||||
<li class="pull-right">
|
||||
<button class="btn btn-default btn-sm modal-dlg" id="show_suspended_sales_button" data-href="<?= esc("$controller_name/suspended") ?>"
|
||||
title="<?= lang(ucfirst($controller_name) . '.suspended_sales') ?>">
|
||||
@@ -191,7 +215,7 @@ helper('url');
|
||||
if ($items_module_allowed && $change_price) {
|
||||
echo form_input(['name' => 'price', 'class' => 'form-control input-sm', 'value' => to_currency_no_money($item['price']), 'tabindex' => ++$tabindex, 'onClick' => 'this.select();']);
|
||||
} else {
|
||||
echo to_currency($item['price']);
|
||||
echo $secondaryCurrency['show'] ? to_secondary_currency_dual((float) $item['price'], $secondaryCurrency) : to_currency($item['price']);
|
||||
echo form_hidden('price', to_currency_no_money($item['price']));
|
||||
}
|
||||
?>
|
||||
@@ -362,9 +386,6 @@ helper('url');
|
||||
<button class="btn btn-info btn-sm modal-dlg" data-btn-submit="<?= lang('Common.submit') ?>" data-href="<?= "customers/view" ?>" title="<?= lang(ucfirst($controller_name) . ".new_customer") ?>">
|
||||
<span class="glyphicon glyphicon-user"> </span><?= lang(ucfirst($controller_name) . ".new_customer") ?>
|
||||
</button>
|
||||
<button class="btn btn-default btn-sm modal-dlg" id="show_keyboard_help" data-href="<?= esc("$controller_name/salesKeyboardHelp") ?>" title="<?= lang(ucfirst($controller_name) . '.key_title') ?>">
|
||||
<span class="glyphicon glyphicon-share-alt"> </span><?= lang(ucfirst($controller_name) . '.key_help') ?>
|
||||
</button>
|
||||
</div>
|
||||
<?php } ?>
|
||||
<?= form_close() ?>
|
||||
@@ -380,7 +401,7 @@ helper('url');
|
||||
</tr>
|
||||
<?php foreach ($taxes as $tax_group_index => $tax) { ?>
|
||||
<tr>
|
||||
<th style="width: 55%;"><?= (float)$tax['tax_rate'] . '% ' . $tax['tax_group'] ?></th>
|
||||
<th style="width: 55%;"><?= (float)$tax['tax_rate'] . '% ' . esc($tax['tax_group']) ?></th>
|
||||
<th style="width: 45%; text-align: right;"><?= to_currency_tax($tax['sale_tax_amount']) ?></th>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
@@ -388,6 +409,12 @@ helper('url');
|
||||
<th style="width: 55%; font-size: 150%"><?= lang(ucfirst($controller_name) . '.total') ?></th>
|
||||
<th style="width: 45%; font-size: 150%; text-align: right;"><span id="sale_total"><?= to_currency($total) ?></span></th>
|
||||
</tr>
|
||||
<?php if ($secondaryCurrency['show']) { ?>
|
||||
<tr>
|
||||
<th style="width: 55%; font-size: 120%"><?= lang(ucfirst($controller_name) . '.total') ?> <?= esc($secondaryCurrencyLabel) ?></th>
|
||||
<th style="width: 45%; font-size: 120%; text-align: right;"><span id="sale_total_secondary_currency"><?= $secondaryTotalDisplay ?? to_secondary_currency((float) $total, $secondaryCurrency) ?></span></th>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
</table>
|
||||
|
||||
<?php if (count($cart) > 0) { // Only show this part if there are Items already in the register ?>
|
||||
@@ -396,11 +423,17 @@ helper('url');
|
||||
<th style="width: 55%;"><?= lang(ucfirst($controller_name) . '.payments_total') ?></th>
|
||||
<th style="width: 45%; text-align: right;"><?= to_currency($payments_total) ?></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th style="width: 55%; font-size: 120%"><?= lang(ucfirst($controller_name) . '.amount_due') ?></th>
|
||||
<th style="width: 45%; font-size: 120%; text-align: right;"><span id="sale_amount_due"><?= to_currency($amount_due) ?></span></th>
|
||||
</tr>
|
||||
<?php if ($secondaryCurrency['show']) { ?>
|
||||
<tr>
|
||||
<th style="width: 55%; font-size: 120%"><?= lang(ucfirst($controller_name) . '.amount_due') ?></th>
|
||||
<th style="width: 45%; font-size: 120%; text-align: right;"><span id="sale_amount_due"><?= to_currency($amount_due) ?></span></th>
|
||||
<th style="width: 55%; font-size: 120%"><?= lang(ucfirst($controller_name) . '.amount_due') ?> <?= esc($secondaryCurrencyLabel) ?></th>
|
||||
<th style="width: 45%; font-size: 120%; text-align: right;"><span id="sale_amount_due_secondary_currency"><?= $secondaryAmountDueDisplay ?? to_secondary_currency((float) $amount_due, $secondaryCurrency) ?></span></th>
|
||||
</tr>
|
||||
</table>
|
||||
<?php } ?>
|
||||
</table>
|
||||
|
||||
<div id="payment_details">
|
||||
<?php if ($payments_cover_total) { // Show Complete sale button instead of Add Payment if there is no amount due left ?>
|
||||
@@ -445,6 +478,7 @@ helper('url');
|
||||
?>
|
||||
<?php } else { ?>
|
||||
<?= form_open("$controller_name/addPayment", ['id' => 'add_payment_form', 'class' => 'form-horizontal']) ?>
|
||||
<input type="hidden" name="complete_after_payment" value="0">
|
||||
<table class="sales_table_100">
|
||||
<tr>
|
||||
<td><?= lang(ucfirst($controller_name) . '.payment') ?></td>
|
||||
@@ -565,8 +599,91 @@ helper('url');
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
const keyboardShortcuts = <?= json_encode($keyboardShortcuts ?? []) ?>;
|
||||
const paymentsCoverTotal = <?= json_encode((bool) $payments_cover_total) ?>;
|
||||
const shortcutCodes = {
|
||||
items: keyboardShortcuts?.items?.code ?? null,
|
||||
customers: keyboardShortcuts?.customers?.code ?? null,
|
||||
suspend: keyboardShortcuts?.suspend?.code ?? null,
|
||||
suspended: keyboardShortcuts?.suspended?.code ?? null,
|
||||
amount: keyboardShortcuts?.amount?.code ?? null,
|
||||
payment: keyboardShortcuts?.payment?.code ?? null,
|
||||
complete: keyboardShortcuts?.complete?.code ?? null,
|
||||
finish: keyboardShortcuts?.finish?.code ?? null,
|
||||
help: keyboardShortcuts?.help?.code ?? null,
|
||||
cancel: keyboardShortcuts?.cancel?.code ?? null
|
||||
};
|
||||
|
||||
window.customerDisplayWindow = window.customerDisplayWindow || null;
|
||||
window.customerDisplayDisplayId = window.customerDisplayDisplayId || sessionStorage.getItem('customerDisplayId') || localStorage.getItem('customerDisplayId') || '';
|
||||
|
||||
window.customerDisplayStorageSuffix = function() {
|
||||
return window.customerDisplayDisplayId ? '_' + window.customerDisplayDisplayId : '';
|
||||
};
|
||||
|
||||
window.customerDisplayStorageKeys = function() {
|
||||
const suffix = window.customerDisplayStorageSuffix();
|
||||
|
||||
return {
|
||||
open: 'customerDisplayOpen' + suffix,
|
||||
dirtyAt: 'customerDisplayDirtyAt' + suffix
|
||||
};
|
||||
};
|
||||
|
||||
window.openCustomerDisplay = function(url) {
|
||||
if (window.customerDisplayDisplayId === '') {
|
||||
window.customerDisplayDisplayId = String(Date.now()) + Math.random().toString(36).slice(2);
|
||||
}
|
||||
|
||||
const keys = window.customerDisplayStorageKeys();
|
||||
const displayUrl = new URL(url, window.location.href);
|
||||
displayUrl.searchParams.set('displayId', window.customerDisplayDisplayId);
|
||||
|
||||
sessionStorage.setItem('customerDisplayId', window.customerDisplayDisplayId);
|
||||
localStorage.setItem('customerDisplayId', window.customerDisplayDisplayId);
|
||||
localStorage.setItem(keys.open, '1');
|
||||
localStorage.setItem(keys.dirtyAt, String(Date.now()));
|
||||
window.customerDisplayWindow = window.open(displayUrl.toString(), 'customer_display_' + window.customerDisplayDisplayId, 'width=1280,height=720,resizable=yes,scrollbars=yes');
|
||||
if (window.customerDisplayWindow && !window.customerDisplayWindow.closed) {
|
||||
window.customerDisplayWindow.focus();
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
window.refreshCustomerDisplay = function() {
|
||||
const keys = window.customerDisplayStorageKeys();
|
||||
|
||||
if (localStorage.getItem(keys.open) !== '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem(keys.dirtyAt, String(Date.now()));
|
||||
if (window.customerDisplayWindow && !window.customerDisplayWindow.closed) {
|
||||
window.customerDisplayWindow.location.reload();
|
||||
window.customerDisplayWindow.focus();
|
||||
}
|
||||
};
|
||||
|
||||
window.notifyCustomerDisplay = function() {
|
||||
window.refreshCustomerDisplay();
|
||||
};
|
||||
|
||||
const secondaryAmounts = <?= json_encode([
|
||||
'total' => $secondaryTotalDisplay ?? null,
|
||||
'amountDue' => $secondaryAmountDueDisplay ?? null,
|
||||
'cashAmountDue' => $secondaryCashAmountDueDisplay ?? null,
|
||||
'nonCashTotal' => $secondaryNonCashTotalDisplay ?? null,
|
||||
'nonCashAmountDue' => $secondaryNonCashAmountDueDisplay ?? null
|
||||
], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>;
|
||||
|
||||
$(document).ready(function() {
|
||||
setTimeout(function() {
|
||||
window.notifyCustomerDisplay();
|
||||
}, 300);
|
||||
|
||||
const redirect = function() {
|
||||
window.notifyCustomerDisplay();
|
||||
window.location.href = "<?= site_url('sales'); ?>";
|
||||
};
|
||||
|
||||
@@ -594,7 +711,10 @@ helper('url');
|
||||
'item_id': item_id,
|
||||
'item_number': item_number,
|
||||
},
|
||||
dataType: 'json'
|
||||
dataType: 'json',
|
||||
success: function() {
|
||||
window.notifyCustomerDisplay();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -608,7 +728,10 @@ helper('url');
|
||||
'item_id': item_id,
|
||||
'item_name': item_name,
|
||||
},
|
||||
dataType: 'json'
|
||||
dataType: 'json',
|
||||
success: function() {
|
||||
window.notifyCustomerDisplay();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -622,7 +745,10 @@ helper('url');
|
||||
'item_id': item_id,
|
||||
'item_description': item_description,
|
||||
},
|
||||
dataType: 'json'
|
||||
dataType: 'json',
|
||||
success: function() {
|
||||
window.notifyCustomerDisplay();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -671,6 +797,7 @@ helper('url');
|
||||
delay: 10,
|
||||
select: function(a, ui) {
|
||||
$(this).val(ui.item.value);
|
||||
window.notifyCustomerDisplay();
|
||||
$('#select_customer_form').submit();
|
||||
return false;
|
||||
}
|
||||
@@ -689,6 +816,7 @@ helper('url');
|
||||
delay: 10,
|
||||
select: function(a, ui) {
|
||||
$(this).val(ui.item.value);
|
||||
window.notifyCustomerDisplay();
|
||||
$('#add_payment_form').submit();
|
||||
return false;
|
||||
}
|
||||
@@ -728,28 +856,34 @@ helper('url');
|
||||
});
|
||||
|
||||
$('#finish_sale_button').click(function() {
|
||||
window.notifyCustomerDisplay();
|
||||
$('#buttons_form').attr('action', "<?= "$controller_name/complete" ?>");
|
||||
$('#buttons_form').submit();
|
||||
});
|
||||
|
||||
$('#finish_invoice_quote_button').click(function() {
|
||||
window.notifyCustomerDisplay();
|
||||
$('#buttons_form').attr('action', "<?= "$controller_name/complete" ?>");
|
||||
$('#buttons_form').submit();
|
||||
});
|
||||
|
||||
$('#suspend_sale_button').click(function() {
|
||||
window.notifyCustomerDisplay();
|
||||
$('#buttons_form').attr('action', "<?= site_url("$controller_name/suspend") ?>");
|
||||
$('#buttons_form').submit();
|
||||
});
|
||||
|
||||
$('#cancel_sale_button').click(function() {
|
||||
if (confirm("<?= lang(ucfirst($controller_name) . '.confirm_cancel_sale') ?>")) {
|
||||
window.notifyCustomerDisplay();
|
||||
$('#buttons_form').attr('action', "<?= site_url("$controller_name/cancel") ?>");
|
||||
$('#buttons_form').submit();
|
||||
}
|
||||
});
|
||||
|
||||
$('#add_payment_button').click(function() {
|
||||
window.notifyCustomerDisplay();
|
||||
$('#add_payment_form').find('input[name="complete_after_payment"]').val('0');
|
||||
$('#add_payment_form').submit();
|
||||
});
|
||||
|
||||
@@ -785,11 +919,13 @@ helper('url');
|
||||
if (response.success) {
|
||||
if (resource.match(/customers$/)) {
|
||||
$('#customer').val(response.id);
|
||||
window.notifyCustomerDisplay();
|
||||
$('#select_customer_form').submit();
|
||||
} else {
|
||||
var $stock_location = $("select[name='stock_location']").val();
|
||||
$('#item_location').val($stock_location);
|
||||
$('#item').val(response.id);
|
||||
window.notifyCustomerDisplay();
|
||||
if (stay_open) {
|
||||
$('#add_item_form').ajaxSubmit();
|
||||
} else {
|
||||
@@ -812,10 +948,17 @@ helper('url');
|
||||
|
||||
function check_payment_type() {
|
||||
var cash_mode = <?= json_encode($cash_mode) ?>;
|
||||
const updateSecondaryRows = function(totalDisplay, amountDueDisplay) {
|
||||
if (totalDisplay !== null && amountDueDisplay !== null) {
|
||||
$("#sale_total_secondary_currency").html(totalDisplay);
|
||||
$("#sale_amount_due_secondary_currency").html(amountDueDisplay);
|
||||
}
|
||||
};
|
||||
|
||||
if ($("#payment_types").val() == "<?= lang(ucfirst($controller_name) . '.giftcard') ?>") {
|
||||
$("#sale_total").html("<?= to_currency($total) ?>");
|
||||
$("#sale_amount_due").html("<?= to_currency($amount_due) ?>");
|
||||
updateSecondaryRows(secondaryAmounts.total, secondaryAmounts.amountDue);
|
||||
$("#amount_tendered_label").html("<?= lang(ucfirst($controller_name) . '.giftcard_number') ?>");
|
||||
$("#amount_tendered:enabled").val('').focus();
|
||||
$(".giftcard-input").attr('disabled', false);
|
||||
@@ -824,6 +967,7 @@ helper('url');
|
||||
} else if (($("#payment_types").val() == "<?= lang(ucfirst($controller_name) . '.cash') ?>" && cash_mode == '1')) {
|
||||
$("#sale_total").html("<?= to_currency($non_cash_total) ?>");
|
||||
$("#sale_amount_due").html("<?= to_currency($cash_amount_due) ?>");
|
||||
updateSecondaryRows(secondaryAmounts.nonCashTotal, secondaryAmounts.cashAmountDue);
|
||||
$("#amount_tendered_label").html("<?= lang(ucfirst($controller_name) . '.amount_tendered') ?>");
|
||||
$("#amount_tendered:enabled").val("<?= to_currency_no_money($cash_amount_due) ?>");
|
||||
$(".giftcard-input").attr('disabled', true);
|
||||
@@ -831,6 +975,7 @@ helper('url');
|
||||
} else {
|
||||
$("#sale_total").html("<?= to_currency($non_cash_total) ?>");
|
||||
$("#sale_amount_due").html("<?= to_currency($amount_due) ?>");
|
||||
updateSecondaryRows(secondaryAmounts.nonCashTotal, secondaryAmounts.nonCashAmountDue);
|
||||
$("#amount_tendered_label").html("<?= lang(ucfirst($controller_name) . '.amount_tendered') ?>");
|
||||
$("#amount_tendered:enabled").val("<?= to_currency_no_money($amount_due) ?>");
|
||||
$(".giftcard-input").attr('disabled', true);
|
||||
@@ -839,43 +984,52 @@ helper('url');
|
||||
}
|
||||
|
||||
// Add Keyboard Shortcuts/Hotkeys to Sale Register
|
||||
document.body.onkeyup = function(e) {
|
||||
switch (event.altKey && event.keyCode) {
|
||||
case 49: // Alt + 1 Items Seach
|
||||
$("#item").focus();
|
||||
$("#item").select();
|
||||
break;
|
||||
case 50: // Alt + 2 Customers Search
|
||||
$("#customer").focus();
|
||||
$("#customer").select();
|
||||
break;
|
||||
case 51: // Alt + 3 Suspend Current Sale
|
||||
$("#suspend_sale_button").click();
|
||||
break;
|
||||
case 52: // Alt + 4 Check Suspended
|
||||
$("#show_suspended_sales_button").click();
|
||||
break;
|
||||
case 53: // Alt + 5 Edit Amount Tendered Value
|
||||
$("#amount_tendered").focus();
|
||||
$("#amount_tendered").select();
|
||||
break;
|
||||
case 54: // Alt + 6 Add Payment
|
||||
$("#add_payment_button").click();
|
||||
break;
|
||||
case 55: // Alt + 7 Add Payment and Complete Sales/Invoice
|
||||
$("#add_payment_button").click();
|
||||
window.location.href = "<?= 'sales/complete' ?>";
|
||||
break;
|
||||
case 56: // Alt + 8 Finish Quote/Invoice without payment
|
||||
$("#finish_invoice_quote_button").click();
|
||||
break;
|
||||
case 57: // Alt + 9 Open Shortcuts Help Modal
|
||||
$("#show_keyboard_help").click();
|
||||
break;
|
||||
document.body.onkeyup = function(event) {
|
||||
if ($(event.target).closest('.modal').length || $('.modal.in').length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.altKey) {
|
||||
switch (event.keyCode) {
|
||||
case shortcutCodes.items:
|
||||
$("#item").focus();
|
||||
$("#item").select();
|
||||
break;
|
||||
case shortcutCodes.customers:
|
||||
$("#customer").focus();
|
||||
$("#customer").select();
|
||||
break;
|
||||
case shortcutCodes.suspend:
|
||||
$("#suspend_sale_button").click();
|
||||
break;
|
||||
case shortcutCodes.suspended:
|
||||
$("#show_suspended_sales_button").click();
|
||||
break;
|
||||
case shortcutCodes.amount:
|
||||
$("#amount_tendered").focus();
|
||||
$("#amount_tendered").select();
|
||||
break;
|
||||
case shortcutCodes.payment:
|
||||
$("#add_payment_button").click();
|
||||
break;
|
||||
case shortcutCodes.complete:
|
||||
if (paymentsCoverTotal && $("#finish_sale_button").length) {
|
||||
$("#finish_sale_button").click();
|
||||
} else {
|
||||
$("#add_payment_button").click();
|
||||
}
|
||||
break;
|
||||
case shortcutCodes.finish:
|
||||
$("#finish_invoice_quote_button").click();
|
||||
break;
|
||||
case shortcutCodes.help:
|
||||
$("#show_keyboard_help").click();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (event.keyCode) {
|
||||
case 27: // ESC Cancel Current Sale
|
||||
case shortcutCodes.cancel:
|
||||
$("#cancel_sale_button").click();
|
||||
break;
|
||||
}
|
||||
@@ -883,3 +1037,6 @@ helper('url');
|
||||
</script>
|
||||
|
||||
<?= view('partial/footer') ?>
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
?>
|
||||
|
||||
<!doctype html>
|
||||
<html lang="<?= $this->request->getLocale() ?>">
|
||||
<html lang="<?= current_language_code() ?>">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
@@ -2,25 +2,19 @@
|
||||
"name": "opensourcepos/opensourcepos",
|
||||
"description": "Open Source Point of Sale is a web based POS system written in the PHP language. It uses MySQL as backend and has a simple user interface",
|
||||
"license": "MIT",
|
||||
"type": "project",
|
||||
"keywords": [
|
||||
"point-of-sale",
|
||||
"POS"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "jekkos"
|
||||
},
|
||||
{
|
||||
"name": "FrancescoUK"
|
||||
},
|
||||
{
|
||||
"name": "objecttothis"
|
||||
},
|
||||
{
|
||||
"name": "steveireland"
|
||||
}
|
||||
],
|
||||
"type": "project",
|
||||
"keywords": [
|
||||
"point-of-sale",
|
||||
"POS"
|
||||
],
|
||||
"homepage": "https://opensourcepos.org",
|
||||
"support": {
|
||||
"issues": "https://github.com/opensourcepos/opensourcepos/issues",
|
||||
@@ -31,8 +25,8 @@
|
||||
"matrix": "https://matrix.to/#/#opensourcepos_Lobby:gitter.im"
|
||||
},
|
||||
"require": {
|
||||
"ext-intl": "*",
|
||||
"php": "^8.2",
|
||||
"ext-intl": "*",
|
||||
"codeigniter4/framework": "4.7.2",
|
||||
"dompdf/dompdf": "^2.0.3",
|
||||
"ezyang/htmlpurifier": "^4.17",
|
||||
@@ -56,7 +50,7 @@
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"App\\": "app/",
|
||||
"CodeIgniter\\": "vendor/codeigniter4/framework/system/"
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
@@ -73,5 +67,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"test": "phpunit"
|
||||
},
|
||||
"scripts-descriptions": {
|
||||
"test": "Run unit tests"
|
||||
}
|
||||
}
|
||||
|
||||
8
composer.lock
generated
8
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "e95f6e5e86d323370ddb0df57c4d3fb3",
|
||||
"content-hash": "eabbc14aefdea4c933869069d6eadadb",
|
||||
"packages": [
|
||||
{
|
||||
"name": "codeigniter4/framework",
|
||||
@@ -5382,9 +5382,9 @@
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"ext-intl": "*",
|
||||
"php": "^8.1"
|
||||
"php": "^8.2",
|
||||
"ext-intl": "*"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ services:
|
||||
- .:/app
|
||||
environment:
|
||||
- CI_ENVIRONMENT=development
|
||||
- ALLOWED_HOSTNAMES=localhost
|
||||
- MYSQL_USERNAME=admin
|
||||
- MYSQL_PASSWORD=pointofsale
|
||||
- MYSQL_DB_NAME=ospos
|
||||
|
||||
@@ -16,6 +16,7 @@ services:
|
||||
- logs:/app/writable/logs
|
||||
environment:
|
||||
- CI_ENVIRONMENT=production
|
||||
- ALLOWED_HOSTNAMES=localhost
|
||||
- FORCE_HTTPS=false
|
||||
- PHP_TIMEZONE=UTC
|
||||
- MYSQL_USERNAME=admin
|
||||
|
||||
@@ -300,6 +300,7 @@ gulp.task('copy-menubar', function() {
|
||||
// Run all required tasks
|
||||
gulp.task('default',
|
||||
gulp.series('clean',
|
||||
'update-licenses',
|
||||
'copy-bootswatch',
|
||||
'copy-bootswatch5',
|
||||
'copy-bootstrap',
|
||||
|
||||
1712
package-lock.json
generated
1712
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
56
package.json
56
package.json
@@ -1,34 +1,33 @@
|
||||
{
|
||||
"name": "@opensourcepos/opensourcepos",
|
||||
"version": "3.4.2",
|
||||
"description": "pen Source Point of Sale is a web based point of sale system written in the PHP language. It uses MySQL as the data storage back-end and has a simple user interface.",
|
||||
"main": "index.php",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
"jekkos <jekkos - at - opensourcepos.org>",
|
||||
"FrancescoUK <francesco.lodolo.uk - at - gmail.com>",
|
||||
"objecttothis <objecttothis - at - gmail.com>",
|
||||
"SteveIreland <stevei - at - ruledomain.com>"
|
||||
],
|
||||
"files": [
|
||||
"dist/opensourcepos.$version.tgz"
|
||||
],
|
||||
"publishConfig": {
|
||||
"registry": "https://npm.pkg.github.com/"
|
||||
},
|
||||
"description": "Open Source Point of Sale is a web based point of sale system written in the PHP language. It uses MySQL as the data storage back-end and has a simple user interface.",
|
||||
"keywords": [
|
||||
"point-of-sale",
|
||||
"POS"
|
||||
],
|
||||
"homepage": "https://opensourcepos.org",
|
||||
"bugs": {
|
||||
"url": "https://github.com/opensourcepos/opensourcepos/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/opensourcepos/opensourcepos"
|
||||
"url": "git+https://github.com/opensourcepos/opensourcepos.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"contributors": [
|
||||
"jekkos <jekkos - at - opensourcepos.org>",
|
||||
"objecttothis <objecttothis - at - gmail.com>"
|
||||
],
|
||||
"files": [
|
||||
"dist/opensourcepos.$version.tgz"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "index.php",
|
||||
"scripts": {
|
||||
"build": "gulp default",
|
||||
"gulp": "gulp"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"bootstrap": "^3.4.1",
|
||||
"bootstrap-daterangepicker": "^2.1.27",
|
||||
@@ -39,9 +38,9 @@
|
||||
"bootstrap-tagsinput-2021": "^0.8.6",
|
||||
"bootstrap-toggle": "^2.2.2",
|
||||
"bootstrap3-dialog": "github:nakupanda/bootstrap3-dialog#master",
|
||||
"bootstrap5": "npm:bootstrap@^5.3.5",
|
||||
"bootstrap5": "npm:bootstrap@^5.3.8",
|
||||
"bootswatch": "^3.4.1",
|
||||
"bootswatch5": "npm:bootswatch@^5.3.5",
|
||||
"bootswatch5": "npm:bootswatch@^5.3.8",
|
||||
"chartist": "^0.11.4",
|
||||
"chartist-plugin-axistitle": "^0.0.7",
|
||||
"chartist-plugin-barlabels": "^0.0.5",
|
||||
@@ -49,7 +48,7 @@
|
||||
"chartist-plugin-tooltips": "^0.0.17",
|
||||
"clipboard": "^2.0.11",
|
||||
"coffeescript": "^2.7.0",
|
||||
"dompurify": "^3.3.2",
|
||||
"dompurify": "^3.4.0",
|
||||
"elegant-circles": "github:opensourcepos/elegant-circles#minified",
|
||||
"es6-promise": "^4.2.8",
|
||||
"file-saver": "^2.0.5",
|
||||
@@ -64,23 +63,26 @@
|
||||
"tableexport.jquery.plugin": "^1.30.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gulp": "^5.0.0",
|
||||
"gulp": "^5.0.1",
|
||||
"gulp-clean": "^0.4.0",
|
||||
"gulp-clean-css": "^4.3.0",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-debug": "^5.0.1",
|
||||
"gulp-gzip": "^1.4.2",
|
||||
"gulp-header": "^2.0.9",
|
||||
"gulp-header": "^2.0.12",
|
||||
"gulp-inject": "^5.0.5",
|
||||
"gulp-rename": "^2.0.0",
|
||||
"gulp-rev": "^10.0.0",
|
||||
"gulp-rename": "^2.1.0",
|
||||
"gulp-rev": "^12.0.0",
|
||||
"gulp-run": "^1.7.1",
|
||||
"gulp-tar": "^4.0.0",
|
||||
"gulp-tar": "^5.0.0",
|
||||
"gulp-uglify": "^3.0.2",
|
||||
"gulp-zip": "^6.1.0",
|
||||
"license-report": "^6.7.2",
|
||||
"npm-check-updates": "^17.1.14",
|
||||
"license-report": "^6.8.2",
|
||||
"npm-check-updates": "^22.1.1",
|
||||
"readable-stream": "^4.4.2",
|
||||
"stream-series": "^0.1.1"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://npm.pkg.github.com/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
<?php
|
||||
|
||||
use CodeIgniter\Boot;
|
||||
use Config\Paths;
|
||||
|
||||
/*
|
||||
*---------------------------------------------------------------
|
||||
* CHECK PHP VERSION
|
||||
*---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
$minPhpVersion = '8.1'; // If you update this, don't forget to update `spark`.
|
||||
$minPhpVersion = '8.2'; // If you update this, don't forget to update `spark`.
|
||||
if (version_compare(PHP_VERSION, $minPhpVersion, '<')) {
|
||||
$message = sprintf(
|
||||
'Your PHP version must be %s or higher to run CodeIgniter. Current version: %s',
|
||||
@@ -48,9 +51,9 @@ if (getcwd() . DIRECTORY_SEPARATOR !== FCPATH) {
|
||||
require FCPATH . '../app/Config/Paths.php';
|
||||
// ^^^ Change this line if you move your application folder
|
||||
|
||||
$paths = new Config\Paths();
|
||||
$paths = new Paths();
|
||||
|
||||
// LOAD THE FRAMEWORK BOOTSTRAP FILE
|
||||
require $paths->systemDirectory . '/Boot.php';
|
||||
|
||||
exit(CodeIgniter\Boot::bootWeb($paths));
|
||||
exit(Boot::bootWeb($paths));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user