mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-05-29 10:47:53 -04:00
Compare commits
149 Commits
feature/ap
...
issue-4459
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
196d1e4d3a | ||
|
|
a4c0d081a2 | ||
|
|
e7daa7a9db | ||
|
|
baf135dd42 | ||
|
|
165c3351eb | ||
|
|
905b58ca6e | ||
|
|
609b206375 | ||
|
|
6fec2464f8 | ||
|
|
332d8c8c69 | ||
|
|
577cf55b6a | ||
|
|
e70395bb85 | ||
|
|
7f9321eca0 | ||
|
|
71056d9b03 | ||
|
|
e17944d883 | ||
|
|
0ac427b2b1 | ||
|
|
3038f83a4a | ||
|
|
75f6ce3140 | ||
|
|
ce7a3ce341 | ||
|
|
d99d2855ec | ||
|
|
96b4b24d9b | ||
|
|
871231e406 | ||
|
|
e62477ed4e | ||
|
|
2a0997f267 | ||
|
|
1ca8effe08 | ||
|
|
ed2c975ad5 | ||
|
|
403feed3e5 | ||
|
|
7f6f36210c | ||
|
|
1121ced532 | ||
|
|
632a18212d | ||
|
|
3208f15244 | ||
|
|
079b809622 | ||
|
|
d685e09c29 | ||
|
|
149c27d60f | ||
|
|
57b7705cd4 | ||
|
|
e8951422c0 | ||
|
|
8afc57fcf4 | ||
|
|
7af64a9a21 | ||
|
|
46d5781498 | ||
|
|
66b61c0554 | ||
|
|
6b97131c48 | ||
|
|
a4c19a3c2c | ||
|
|
7ca8c9561a | ||
|
|
4fac5d9198 | ||
|
|
221995b6db | ||
|
|
91dbe5b869 | ||
|
|
afd908327b | ||
|
|
cfde66481d | ||
|
|
80f00c8552 | ||
|
|
dbdf4db4fb | ||
|
|
64004db271 | ||
|
|
7f20a5dd4c | ||
|
|
d7a276b488 | ||
|
|
57dbe43313 | ||
|
|
6f1c39d99e | ||
|
|
45902caa67 | ||
|
|
1fe865a100 | ||
|
|
90da63cb13 | ||
|
|
8da4aff262 | ||
|
|
0e9f4a998d | ||
|
|
85c7ce2da4 | ||
|
|
e723e2ddf4 | ||
|
|
71eb8de7fe | ||
|
|
9d5019e12e | ||
|
|
56670271d6 | ||
|
|
cef103445e | ||
|
|
68e9a56632 | ||
|
|
ba05536317 | ||
|
|
f74f286a51 | ||
|
|
7180ec33e8 | ||
|
|
496c8a8262 | ||
|
|
493d9cc9c1 | ||
|
|
f761e1464f | ||
|
|
a5bbb2bcc5 | ||
|
|
92ec321d08 | ||
|
|
e046e74c79 | ||
|
|
e0cd0f6129 | ||
|
|
3b102adf3f | ||
|
|
260358d611 | ||
|
|
e615200466 | ||
|
|
56cead478a | ||
|
|
7030f6bac3 | ||
|
|
299f62669a | ||
|
|
072865620a | ||
|
|
3bbd4c4c95 | ||
|
|
0253bf85b8 | ||
|
|
92c1be8bb1 | ||
|
|
23829eab35 | ||
|
|
c81c6506cb | ||
|
|
840d9ccc81 | ||
|
|
e763ee2acc | ||
|
|
8ef109efbc | ||
|
|
9a544096c2 | ||
|
|
3e4ac0b24d | ||
|
|
3c9c592ca3 | ||
|
|
a4d8bedbf3 | ||
|
|
c4304fd0a9 | ||
|
|
44fe2c087a | ||
|
|
985c1c55ce | ||
|
|
8029e5538f | ||
|
|
1a7683a8ac | ||
|
|
e4b92b58c3 | ||
|
|
dc1e448bc3 | ||
|
|
24b2825b31 | ||
|
|
38d672592b | ||
|
|
6f7e06e986 | ||
|
|
fda40d9340 | ||
|
|
b49186ec7c | ||
|
|
8b56f61b8a | ||
|
|
9820beb0e1 | ||
|
|
e01dad728f | ||
|
|
234f930079 | ||
|
|
3001dc0e17 | ||
|
|
3ba207e8b9 | ||
|
|
d684c49ebd | ||
|
|
071e641f95 | ||
|
|
48af67bd00 | ||
|
|
7cb1d95da7 | ||
|
|
bafe3ddf1b | ||
|
|
c482e75304 | ||
|
|
afc2f82dc6 | ||
|
|
ce411707b4 | ||
|
|
37c6e22fc4 | ||
|
|
3c7ece5c33 | ||
|
|
02fccaf43f | ||
|
|
ee4d44ed39 | ||
|
|
fa3f257e7b | ||
|
|
431a9951e9 | ||
|
|
f7e8d6e427 | ||
|
|
85889b6e65 | ||
|
|
6818f02ef9 | ||
|
|
436696b11b | ||
|
|
9a2b308647 | ||
|
|
1f55d96580 | ||
|
|
b2fadea44a | ||
|
|
0fdb3ba37b | ||
|
|
d7b2264ac1 | ||
|
|
a229bf6031 | ||
|
|
977fa5647b | ||
|
|
52b0a83190 | ||
|
|
f25a0f5b09 | ||
|
|
f0f288797a | ||
|
|
63083a0946 | ||
|
|
3a33098776 | ||
|
|
ca6a1b35af | ||
|
|
418580a52d | ||
|
|
31d25e06dc | ||
|
|
b1819b3b36 | ||
|
|
6705420373 | ||
|
|
d6b767c80a |
@@ -1,23 +1,56 @@
|
|||||||
node_modules
|
# Version control
|
||||||
tmp
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Sensitive config (user may mount their own)
|
||||||
app/Config/Email.php
|
app/Config/Email.php
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
tmp/
|
||||||
*.patch
|
*.patch
|
||||||
patches/
|
patches/
|
||||||
|
|
||||||
|
# IDE and editor files
|
||||||
.idea/
|
.idea/
|
||||||
git-svn-diff.py
|
.vscode/
|
||||||
*.bash
|
|
||||||
.swp
|
.swp
|
||||||
|
*.swp
|
||||||
.buildpath
|
.buildpath
|
||||||
.project
|
.project
|
||||||
.settings/*
|
.settings/
|
||||||
.git
|
|
||||||
dist/
|
# Development tools and configs
|
||||||
node_modules/
|
tests/
|
||||||
*.swp
|
phpunit.xml
|
||||||
|
.php-cs-fixer.*
|
||||||
|
phpstan.neon
|
||||||
|
*.bash
|
||||||
|
git-svn-diff.py
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
*.md
|
||||||
|
!LICENSE
|
||||||
|
branding/
|
||||||
|
|
||||||
|
# Build configs (not needed at runtime)
|
||||||
|
composer.json
|
||||||
|
composer.lock
|
||||||
|
package.json
|
||||||
|
package-lock.json
|
||||||
|
gulpfile.js
|
||||||
|
.env.example
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Temporary and backup files
|
||||||
*.rej
|
*.rej
|
||||||
*.orig
|
*.orig
|
||||||
*~
|
*~
|
||||||
*.~
|
*.~
|
||||||
*.log
|
*.log
|
||||||
app/writable/session/*
|
|
||||||
!app/writable/session/index.html
|
# CI
|
||||||
|
.github/
|
||||||
|
.github/workflows/
|
||||||
|
build/
|
||||||
|
|||||||
18
.env.example
18
.env.example
@@ -4,6 +4,24 @@
|
|||||||
|
|
||||||
CI_ENVIRONMENT = production
|
CI_ENVIRONMENT = production
|
||||||
|
|
||||||
|
#--------------------------------------------------------------------
|
||||||
|
# SECURITY: ALLOWED HOSTNAMES
|
||||||
|
#--------------------------------------------------------------------
|
||||||
|
# CRITICAL: Whitelist of allowed hostnames to prevent Host Header
|
||||||
|
# Injection attacks (GHSA-jchf-7hr6-h4f3).
|
||||||
|
#
|
||||||
|
# REQUIRED IN PRODUCTION: Application will fail to start if not configured.
|
||||||
|
# In development, falls back to 'localhost' with an error log.
|
||||||
|
#
|
||||||
|
# Configure with comma-separated list of domains/subdomains:
|
||||||
|
# app.allowedHostnames = 'yourdomain.com,www.yourdomain.com'
|
||||||
|
#
|
||||||
|
# For local development:
|
||||||
|
# app.allowedHostnames = 'localhost'
|
||||||
|
#
|
||||||
|
# Note: Do not include protocol (http/https) or port numbers.
|
||||||
|
app.allowedHostnames = ''
|
||||||
|
|
||||||
#--------------------------------------------------------------------
|
#--------------------------------------------------------------------
|
||||||
# DATABASE
|
# DATABASE
|
||||||
#--------------------------------------------------------------------
|
#--------------------------------------------------------------------
|
||||||
|
|||||||
308
.github/ISSUE_TEMPLATE/bug report.yml
vendored
308
.github/ISSUE_TEMPLATE/bug report.yml
vendored
@@ -1,121 +1,187 @@
|
|||||||
name: Bug Report
|
name: 🐛 Bug Report
|
||||||
description: File a bug report
|
description: File a bug report to help us improve
|
||||||
title: "[Bug]: "
|
title: "[Bug]: "
|
||||||
labels: ["bug", "triage"]
|
labels: ["bug", "triage"]
|
||||||
projects: ["ospos/3", "ospos/4"]
|
projects: ["ospos/3", "ospos/4"]
|
||||||
assignees:
|
assignees: []
|
||||||
- none
|
body:
|
||||||
body:
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
- type: markdown
|
# INTRODUCTION
|
||||||
attributes:
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
value: |
|
- type: markdown
|
||||||
Bug reports indicate that something is not working as intended.
|
attributes:
|
||||||
Please include as much detail as possible and submit a separate bug report for each problem.
|
value: |
|
||||||
Do not include personal identifying information such as email addresses or encryption keys.
|
## Thanks for taking the time to fill out this bug report! 🐜
|
||||||
- type: textarea
|
|
||||||
id: bug-description
|
Bug reports help us identify and fix issues. Please provide as much detail as possible.
|
||||||
attributes:
|
|
||||||
label: Bug Description?
|
> ⚠️ **Important:** Submit a separate bug report for each problem you encounter.
|
||||||
description: Describe the problem that you are seeing
|
>
|
||||||
placeholder: "Describe the problem that you are seeing"
|
> 🚫 Do not include personal identifying information such as email addresses or encryption keys.
|
||||||
validations:
|
|
||||||
required: true
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
- type: textarea
|
# PROBLEM DESCRIPTION
|
||||||
id: steps-reproduce
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
attributes:
|
- type: textarea
|
||||||
label: Steps to Reproduce?
|
id: bug-description
|
||||||
description: List the steps to reproduce this issue
|
attributes:
|
||||||
placeholder: "Steps to Reproduce"
|
label: 🐛 Bug Description
|
||||||
validations:
|
description: A clear and concise description of what the bug is.
|
||||||
required: true
|
placeholder: |
|
||||||
- type: textarea
|
Example: When I try to print a receipt, the application crashes
|
||||||
id: expected-behavior
|
with an error message saying "Unable to connect to printer".
|
||||||
attributes:
|
validations:
|
||||||
label: Expected Behavior?
|
required: true
|
||||||
description: Tell us what did you expect to happen?
|
|
||||||
placeholder: "Expected Behavior"
|
- type: textarea
|
||||||
validations:
|
id: steps-reproduce
|
||||||
required: true
|
attributes:
|
||||||
- type: dropdown
|
label: 📋 Steps to Reproduce
|
||||||
id: ospos-version
|
description: Detailed steps to reproduce the behavior.
|
||||||
attributes:
|
placeholder: |
|
||||||
label: OpensourcePOS Version
|
1. Go to '...'
|
||||||
description: What version of our software are you running?
|
2. Click on '...'
|
||||||
options:
|
3. Scroll down to '...'
|
||||||
- development (unreleased)
|
4. See error
|
||||||
- opensourcepos 3.4.1
|
validations:
|
||||||
- opensourcepos 3.4.0
|
required: true
|
||||||
- opensourcepos 3.3.9
|
|
||||||
- opensourcepos 3.3.8
|
- type: textarea
|
||||||
- opensourcepos 3.3.7
|
id: expected-behavior
|
||||||
default: 0
|
attributes:
|
||||||
validations:
|
label: ✅ Expected Behavior
|
||||||
required: true
|
description: A clear and concise description of what you expected to happen.
|
||||||
- type: dropdown
|
placeholder: |
|
||||||
id: php-version
|
Example: The receipt should print successfully without any errors.
|
||||||
attributes:
|
validations:
|
||||||
label: Php version
|
required: true
|
||||||
description: What version of Php?
|
|
||||||
options:
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
- Php 7.2
|
# ENVIRONMENT DETAILS
|
||||||
- Php 7.3
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
- Php 7.4
|
- type: dropdown
|
||||||
- Php 8.1
|
id: ospos-version
|
||||||
- Php 8.2
|
attributes:
|
||||||
- Php 8.3
|
label: 📦 OpenSourcePOS Version
|
||||||
- Php 8.4
|
description: What version of our software are you running?
|
||||||
default: 0
|
options:
|
||||||
validations:
|
- development (unreleased)
|
||||||
required: true
|
- OpenSourcePOS 3.4.2
|
||||||
- type: dropdown
|
- OpenSourcePOS 3.4.1
|
||||||
id: browsers
|
- OpenSourcePOS 3.4.0
|
||||||
attributes:
|
- OpenSourcePOS 3.3.9
|
||||||
label: What browsers are you seeing the problem on?
|
- OpenSourcePOS 3.3.8
|
||||||
multiple: true
|
default: 0
|
||||||
options:
|
validations:
|
||||||
- Firefox
|
required: true
|
||||||
- Chrome
|
|
||||||
- Safari
|
- type: dropdown
|
||||||
- Microsoft Edge
|
id: php-version
|
||||||
- Other
|
attributes:
|
||||||
- type: input
|
label: 🔧 PHP Version
|
||||||
id: server
|
description: What version of PHP are you running?
|
||||||
attributes:
|
options:
|
||||||
label: Server Operating System and version
|
- PHP 8.4
|
||||||
description: "Server Operating System "
|
- PHP 8.3
|
||||||
placeholder: "Server Operating System "
|
- PHP 8.2
|
||||||
validations:
|
- PHP 8.1
|
||||||
required: true
|
- PHP 7.4
|
||||||
- type: input
|
- Other
|
||||||
id: database
|
default: 0
|
||||||
attributes:
|
validations:
|
||||||
label: Database Management System and version
|
required: true
|
||||||
description: "Database Management System"
|
|
||||||
placeholder: "Database Management"
|
- type: dropdown
|
||||||
validations:
|
id: browsers
|
||||||
required: true
|
attributes:
|
||||||
- type: input
|
label: 🌐 Browser(s)
|
||||||
id: webserver
|
description: What browser(s) are you seeing the problem on?
|
||||||
attributes:
|
multiple: true
|
||||||
label: Web Server and version
|
options:
|
||||||
description: "Web Server and version "
|
- Firefox
|
||||||
placeholder: "Web Server and version "
|
- Chrome
|
||||||
validations:
|
- Safari
|
||||||
required: true
|
- Microsoft Edge
|
||||||
- type: textarea
|
- Other
|
||||||
id: servers
|
|
||||||
attributes:
|
- type: input
|
||||||
label: System Information Report (optional)
|
id: server
|
||||||
description: Copy and paste from OSPOS > Configuration > Setup & Conf > Setup & Conf?
|
attributes:
|
||||||
placeholder: System Information Report
|
label: 🖥️ Server Operating System
|
||||||
value: "System Information Report"
|
description: What server OS and version are you running?
|
||||||
validations:
|
placeholder: "e.g., Ubuntu 22.04, CentOS 7, Windows Server 2022"
|
||||||
required: true
|
validations:
|
||||||
- type: checkboxes
|
required: true
|
||||||
id: terms
|
|
||||||
attributes:
|
- type: input
|
||||||
label: Unmodified copy of OpensourcePOS
|
id: database
|
||||||
description: By submitting this issue you agree this copy has not been modified
|
attributes:
|
||||||
options:
|
label: 🗄️ Database
|
||||||
- label: I agree this copy has not been modified
|
description: What database management system and version are you using?
|
||||||
required: true
|
placeholder: "e.g., MySQL 8.0, MariaDB 10.11, Percona 8.0"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: webserver
|
||||||
|
attributes:
|
||||||
|
label: 🌍 Web Server
|
||||||
|
description: What web server and version are you using?
|
||||||
|
placeholder: "e.g., Apache 2.4, Nginx 1.24, Caddy 2.7"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# ADDITIONAL INFORMATION
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
- type: textarea
|
||||||
|
id: system-info
|
||||||
|
attributes:
|
||||||
|
label: 📊 System Information Report
|
||||||
|
description: |
|
||||||
|
Copy and paste the system information from OSPOS:
|
||||||
|
|
||||||
|
**Navigation:** Configuration → Setup & Conf → System Info
|
||||||
|
placeholder: |
|
||||||
|
Paste the System Information Report here...
|
||||||
|
render: text
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: 📜 Relevant Log Output
|
||||||
|
description: |
|
||||||
|
Please copy and paste any relevant log output.
|
||||||
|
|
||||||
|
**Log locations:**
|
||||||
|
- OSPOS logs: `writable/logs/`
|
||||||
|
- Web server logs: `/var/log/apache2/` or `/var/log/nginx/`
|
||||||
|
- PHP logs: Check your `php.ini` for `error_log` location
|
||||||
|
placeholder: |
|
||||||
|
Paste log output here...
|
||||||
|
render: shell
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: screenshots
|
||||||
|
attributes:
|
||||||
|
label: 📸 Screenshots
|
||||||
|
description: If applicable, add screenshots to help explain your problem.
|
||||||
|
placeholder: Drag and drop images here...
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# CONFIRMATION
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: ✓ Confirmation
|
||||||
|
description: Please confirm the following before submitting
|
||||||
|
options:
|
||||||
|
- label: I certify that this is an unmodified copy of OpenSourcePOS
|
||||||
|
required: true
|
||||||
|
- label: I have searched existing issues to ensure this bug has not already been reported
|
||||||
|
required: true
|
||||||
|
- label: I have provided all the information requested above
|
||||||
|
required: true
|
||||||
|
|||||||
199
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
199
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,63 +1,136 @@
|
|||||||
name: ✨ Feature Request
|
name: ✨ Feature Request
|
||||||
description: Suggest an idea for this project
|
description: Suggest an idea or enhancement for this project
|
||||||
title: "[Feature]: "
|
title: "[Feature]: "
|
||||||
labels: ["enhancement"]
|
labels: ["enhancement"]
|
||||||
assignees: ["none"]
|
assignees: []
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
attributes:
|
# INTRODUCTION
|
||||||
value: |
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
Thanks for taking the time to fill out this feature request! 🤗
|
- type: markdown
|
||||||
Please make sure this feature request hasn't been already submitted by someone by looking through other open/closed issues. 😃
|
attributes:
|
||||||
|
value: |
|
||||||
- type: dropdown
|
## Thanks for suggesting a new feature! 💡
|
||||||
attributes:
|
|
||||||
multiple: false
|
We appreciate you taking the time to help improve OpenSourcePOS.
|
||||||
label: Type of Feature
|
|
||||||
description: Select the type of feature request.
|
> 📋 **Before submitting:** Please search [existing feature requests](https://github.com/opensourcepos/opensourcepos/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement) to ensure your idea hasn't already been suggested.
|
||||||
options:
|
|
||||||
- "✨ New Feature"
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
- "📝 Documentation"
|
# FEATURE DETAILS
|
||||||
- "🎨 Style and UI"
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
- "🔨 Code Refactor"
|
- type: dropdown
|
||||||
- "⚡ Performance Improvements"
|
id: feature-type
|
||||||
- "✅ New Test"
|
attributes:
|
||||||
validations:
|
label: 🏷️ Feature Type
|
||||||
required: true
|
description: What type of feature are you requesting?
|
||||||
|
options:
|
||||||
- type: dropdown
|
- "✨ New Feature"
|
||||||
id: ospos-version
|
- "📝 Documentation Improvement"
|
||||||
attributes:
|
- "🎨 UI/UX Enhancement"
|
||||||
label: OpensourcePOS Version
|
- "🔨 Code Refactoring"
|
||||||
description: What version of our software are you running?
|
- "⚡ Performance Improvement"
|
||||||
options:
|
- "✅ New Test Coverage"
|
||||||
- opensourcepos 3.3.9
|
- "🔌 Plugin/Integration"
|
||||||
- opensourcepos 3.3.8
|
default: 0
|
||||||
- opensourcepos 3.3.7
|
validations:
|
||||||
default: 0
|
required: true
|
||||||
validations:
|
|
||||||
required: true
|
- type: dropdown
|
||||||
|
id: ospos-version
|
||||||
- type: textarea
|
attributes:
|
||||||
id: description
|
label: 📦 OpenSourcePOS Version
|
||||||
attributes:
|
description: What version are you currently running?
|
||||||
label: Description
|
options:
|
||||||
description: Give us a brief description of the feature or enhancement you would like
|
- development (unreleased)
|
||||||
validations:
|
- OpenSourcePOS 3.4.2
|
||||||
required: true
|
- OpenSourcePOS 3.4.1
|
||||||
|
- OpenSourcePOS 3.4.0
|
||||||
- type: textarea
|
- OpenSourcePOS 3.3.9
|
||||||
id: additional-information
|
- OpenSourcePOS 3.3.8
|
||||||
attributes:
|
default: 0
|
||||||
label: Additional Information
|
validations:
|
||||||
description: Give us some additional information on the feature request like proposed solutions, links, screenshots, etc.
|
required: true
|
||||||
|
|
||||||
- type: checkboxes
|
- type: textarea
|
||||||
id: terms
|
id: problem-statement
|
||||||
attributes:
|
attributes:
|
||||||
label: Verify you searched open requests in OpensourcePOS
|
label: 🎯 Problem Statement
|
||||||
description: By submitting this request you agree that you have searched Open Requests in the Tracker
|
description: |
|
||||||
options:
|
Is your feature request related to a problem? Please describe.
|
||||||
- label: I agree I have searched Open Requests
|
|
||||||
required: true
|
A clear description of what the problem is. Ex: I'm always frustrated when [...]
|
||||||
|
placeholder: |
|
||||||
|
Example: I always have to manually calculate taxes for different regions,
|
||||||
|
which is time-consuming and error-prone.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: proposed-solution
|
||||||
|
attributes:
|
||||||
|
label: 💡 Proposed Solution
|
||||||
|
description: A clear and concise description of what you want to happen.
|
||||||
|
placeholder: |
|
||||||
|
Example: Add an automatic tax calculation feature that:
|
||||||
|
- Detects the customer's region
|
||||||
|
- Applies the correct tax rate
|
||||||
|
- Generates a tax report automatically
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: 🔄 Alternatives Considered
|
||||||
|
description: A clear description of any alternative solutions or features you've considered.
|
||||||
|
placeholder: |
|
||||||
|
Example: I considered using an external tax service, but it would be
|
||||||
|
better to have this integrated directly into OpenSourcePOS.
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# ADDITIONAL INFORMATION
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
- type: textarea
|
||||||
|
id: additional-context
|
||||||
|
attributes:
|
||||||
|
label: 📎 Additional Context
|
||||||
|
description: |
|
||||||
|
Add any other context, screenshots, mockups, or references about the feature request here.
|
||||||
|
|
||||||
|
**Helpful additions:**
|
||||||
|
- Links to similar features in other software
|
||||||
|
- Mockups or diagrams
|
||||||
|
- Code examples
|
||||||
|
- Documentation references
|
||||||
|
placeholder: |
|
||||||
|
Any other relevant information, links, or screenshots...
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: acceptance-criteria
|
||||||
|
attributes:
|
||||||
|
label: ✅ Acceptance Criteria
|
||||||
|
description: |
|
||||||
|
(Optional) Define what "done" looks like for this feature.
|
||||||
|
|
||||||
|
Format: **Given** [context], **When** [action], **Then** [outcome]
|
||||||
|
placeholder: |
|
||||||
|
Given a customer is selected from region X
|
||||||
|
When the sale is completed
|
||||||
|
Then the tax rate for region X is automatically applied
|
||||||
|
And the tax amount is correctly calculated
|
||||||
|
And a tax entry is logged in the report
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# CONFIRMATION
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: ✓ Confirmation
|
||||||
|
description: Please confirm before submitting
|
||||||
|
options:
|
||||||
|
- label: I have searched existing feature requests to ensure this is not a duplicate
|
||||||
|
required: true
|
||||||
|
- label: I have provided a clear problem statement and proposed solution
|
||||||
|
required: true
|
||||||
61
.github/workflows/README.md
vendored
Normal file
61
.github/workflows/README.md
vendored
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# GitHub Actions
|
||||||
|
|
||||||
|
This document describes the CI/CD workflows for OSPOS.
|
||||||
|
|
||||||
|
## Build and Release Workflow (`.github/workflows/build-release.yml`)
|
||||||
|
|
||||||
|
### Build Process
|
||||||
|
- Setup PHP 8.2 with required extensions
|
||||||
|
- Setup Node.js 20
|
||||||
|
- Install composer dependencies
|
||||||
|
- Install npm dependencies
|
||||||
|
- Build frontend assets with Gulp
|
||||||
|
|
||||||
|
### Docker Images
|
||||||
|
- Build and push `opensourcepos` Docker image for multiple architectures (linux/amd64, linux/arm64)
|
||||||
|
- On master: tagged with version and `latest`
|
||||||
|
- On other branches: tagged with version only
|
||||||
|
- Pushed to Docker Hub
|
||||||
|
|
||||||
|
### Releases
|
||||||
|
- Create distribution archives (tar.gz, zip)
|
||||||
|
- Create/update GitHub "unstable" release on master branch only
|
||||||
|
|
||||||
|
## Required Secrets
|
||||||
|
|
||||||
|
To use this workflow, you need to add the following secrets to your repository:
|
||||||
|
|
||||||
|
1. **DOCKER_USERNAME** - Docker Hub username for pushing images
|
||||||
|
2. **DOCKER_PASSWORD** - Docker Hub password/token for pushing images
|
||||||
|
|
||||||
|
### How to add secrets
|
||||||
|
|
||||||
|
1. Go to your repository on GitHub
|
||||||
|
2. Click **Settings** → **Secrets and variables** → **Actions**
|
||||||
|
3. Click **New repository secret**
|
||||||
|
4. Add `DOCKER_USERNAME` and `DOCKER_PASSWORD`
|
||||||
|
|
||||||
|
The `GITHUB_TOKEN` is automatically provided by GitHub Actions.
|
||||||
|
|
||||||
|
## Workflow Triggers
|
||||||
|
|
||||||
|
- **Push to master** - Runs build, Docker push (with `latest` tag), and release
|
||||||
|
- **Push to other branches** - Runs build and Docker push (version tag only)
|
||||||
|
- **Push tags** - Runs build and Docker push (version tag only)
|
||||||
|
- **Pull requests** - Runs build only (PHPUnit tests run in parallel via phpunit.yml)
|
||||||
|
|
||||||
|
## Existing Workflows
|
||||||
|
|
||||||
|
This repository also has these workflows:
|
||||||
|
- `.github/workflows/main.yml` - PHP linting with PHP-CS-Fixer
|
||||||
|
- `.github/workflows/phpunit.yml` - PHPUnit tests (runs on all PHP versions 8.1-8.4)
|
||||||
|
- `.github/workflows/php-linter.yml` - PHP linting
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
PHPUnit tests are run separately via `.github/workflows/phpunit.yml` on every push and pull request, testing against PHP 8.1, 8.2, 8.3, and 8.4.
|
||||||
|
|
||||||
|
To test the build workflow:
|
||||||
|
1. Add the required secrets
|
||||||
|
2. Push to master or create a PR
|
||||||
|
3. Monitor the Actions tab in GitHub
|
||||||
214
.github/workflows/build-release.yml
vendored
Normal file
214
.github/workflows/build-release.yml
vendored
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
name: Build and Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.version.outputs.version }}
|
||||||
|
version-tag: ${{ steps.version.outputs.version-tag }}
|
||||||
|
short-sha: ${{ steps.version.outputs.short-sha }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.2'
|
||||||
|
extensions: intl, mbstring, mysqli, gd, bcmath, zip
|
||||||
|
coverage: none
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Get composer cache directory
|
||||||
|
run: echo "COMPOSER_CACHE_FILES_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Cache composer dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ env.COMPOSER_CACHE_FILES_DIR }}
|
||||||
|
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-composer-
|
||||||
|
|
||||||
|
- name: Get npm cache directory
|
||||||
|
run: echo "NPM_CACHE_DIR=$(npm config get cache)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Cache npm dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ env.NPM_CACHE_DIR }}
|
||||||
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-node-
|
||||||
|
|
||||||
|
- name: Install composer dependencies
|
||||||
|
run: composer install --no-dev --optimize-autoloader
|
||||||
|
|
||||||
|
- name: Install npm dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Install gulp globally
|
||||||
|
run: npm install -g gulp-cli
|
||||||
|
|
||||||
|
- name: Get version info
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION=$(grep "application_version" app/Config/App.php | sed "s/.*= '\(.*\)';/\1/g")
|
||||||
|
BRANCH=$(echo "${GITHUB_REF#refs/heads/}" | sed 's/feature\///' | tr '/' '_')
|
||||||
|
TAG=$(echo "${GITHUB_TAG:-$BRANCH}" | tr '/' '_')
|
||||||
|
SHORT_SHA=$(git rev-parse --short=6 HEAD)
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "version-tag=$VERSION-$BRANCH-$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||||
|
echo "short-sha=$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||||
|
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
|
||||||
|
env:
|
||||||
|
GITHUB_TAG: ${{ github.ref_name }}
|
||||||
|
|
||||||
|
- name: Create .env file
|
||||||
|
run: |
|
||||||
|
cp .env.example .env
|
||||||
|
sed -i 's/production/development/g' .env
|
||||||
|
|
||||||
|
- name: Update commit hash
|
||||||
|
run: |
|
||||||
|
SHORT_SHA="${{ steps.version.outputs.short-sha }}"
|
||||||
|
sed -i "s/commit_sha1 = 'dev'/commit_sha1 = '$SHORT_SHA'/g" app/Config/OSPOS.php
|
||||||
|
|
||||||
|
- name: Build frontend assets
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Create distribution archives
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
gulp compress
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
SHORT_SHA="${{ steps.version.outputs.short-sha }}"
|
||||||
|
mv dist/opensourcepos.tar "dist/opensourcepos.$VERSION.$SHORT_SHA.tar"
|
||||||
|
mv dist/opensourcepos.zip "dist/opensourcepos.$VERSION.$SHORT_SHA.zip"
|
||||||
|
|
||||||
|
- name: Upload build artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: dist-${{ steps.version.outputs.short-sha }}
|
||||||
|
path: dist/
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
- name: Upload build context for Docker
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build-context-${{ steps.version.outputs.short-sha }}
|
||||||
|
path: |
|
||||||
|
.
|
||||||
|
!.git
|
||||||
|
!node_modules
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
docker:
|
||||||
|
name: Build Docker Image
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
needs: build
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download build context
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build-context-${{ needs.build.outputs.short-sha }}
|
||||||
|
path: .
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Determine Docker tags
|
||||||
|
id: tags
|
||||||
|
run: |
|
||||||
|
BRANCH=$(echo "${GITHUB_REF#refs/heads/}" | tr '/' '_')
|
||||||
|
if [ "$BRANCH" = "master" ]; then
|
||||||
|
echo "tags=${{ secrets.DOCKER_USERNAME }}/opensourcepos:${{ needs.build.outputs.version-tag }},${{ secrets.DOCKER_USERNAME }}/opensourcepos:master" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "tags=${{ secrets.DOCKER_USERNAME }}/opensourcepos:${{ needs.build.outputs.version-tag }}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
env:
|
||||||
|
GITHUB_REF: ${{ github.ref }}
|
||||||
|
|
||||||
|
- name: Build and push Docker images
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
target: ospos
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.tags.outputs.tags }}
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Create Release
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download build artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: dist-${{ needs.build.outputs.short-sha }}
|
||||||
|
path: dist/
|
||||||
|
|
||||||
|
- name: Get version info
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION="${{ needs.build.outputs.version }}"
|
||||||
|
SHORT_SHA=$(git rev-parse --short=6 HEAD)
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "short-sha=$SHORT_SHA" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Create/Update unstable release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: unstable
|
||||||
|
name: Unstable OpenSourcePOS
|
||||||
|
body: |
|
||||||
|
This is a build of the latest master which might contain bugs. Use at your own risk.
|
||||||
|
|
||||||
|
Check the releases section for the latest official release.
|
||||||
|
files: |
|
||||||
|
dist/opensourcepos.${{ steps.version.outputs.version }}.${{ steps.version.outputs.short-sha }}.zip
|
||||||
|
prerelease: true
|
||||||
|
draft: false
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
71
.github/workflows/codeql-analysis.yml
vendored
71
.github/workflows/codeql-analysis.yml
vendored
@@ -1,71 +0,0 @@
|
|||||||
# For most projects, this workflow file will not need changing; you simply need
|
|
||||||
# to commit it to your repository.
|
|
||||||
#
|
|
||||||
# You may wish to alter this file to override the set of languages analyzed,
|
|
||||||
# or to provide custom queries or build logic.
|
|
||||||
#
|
|
||||||
# ******** NOTE ********
|
|
||||||
# We have attempted to detect the languages in your repository. Please check
|
|
||||||
# the `language` matrix defined below to confirm you have the correct set of
|
|
||||||
# supported CodeQL languages.
|
|
||||||
#
|
|
||||||
name: "CodeQL"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
pull_request:
|
|
||||||
# The branches below must be a subset of the branches above
|
|
||||||
branches: [ master ]
|
|
||||||
schedule:
|
|
||||||
- cron: '21 12 * * 3'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
analyze:
|
|
||||||
name: Analyze
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
security-events: write
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
language: [ 'javascript' ]
|
|
||||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
|
||||||
# Learn more:
|
|
||||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
|
||||||
- name: Initialize CodeQL
|
|
||||||
uses: github/codeql-action/init@v1
|
|
||||||
with:
|
|
||||||
languages: ${{ matrix.language }}
|
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
|
||||||
# By default, queries listed here will override any specified in a config file.
|
|
||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
|
||||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
|
||||||
- name: Autobuild
|
|
||||||
uses: github/codeql-action/autobuild@v1
|
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
|
||||||
# 📚 https://git.io/JvXDl
|
|
||||||
|
|
||||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
|
||||||
# and modify them (or add more) to build your code if your project
|
|
||||||
# uses a compiled language
|
|
||||||
|
|
||||||
#- run: |
|
|
||||||
# make bootstrap
|
|
||||||
# make release
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
|
||||||
uses: github/codeql-action/analyze@v1
|
|
||||||
1
.github/workflows/main.yml
vendored
1
.github/workflows/main.yml
vendored
@@ -28,7 +28,6 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
php-version:
|
php-version:
|
||||||
- '8.1'
|
|
||||||
- '8.2'
|
- '8.2'
|
||||||
- '8.3'
|
- '8.3'
|
||||||
- '8.4'
|
- '8.4'
|
||||||
|
|||||||
33
.github/workflows/opencode.yml
vendored
33
.github/workflows/opencode.yml
vendored
@@ -1,33 +0,0 @@
|
|||||||
name: opencode
|
|
||||||
|
|
||||||
on:
|
|
||||||
issue_comment:
|
|
||||||
types: [created]
|
|
||||||
pull_request_review_comment:
|
|
||||||
types: [created]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
opencode:
|
|
||||||
if: |
|
|
||||||
contains(github.event.comment.body, ' /oc') ||
|
|
||||||
startsWith(github.event.comment.body, '/oc') ||
|
|
||||||
contains(github.event.comment.body, ' /opencode') ||
|
|
||||||
startsWith(github.event.comment.body, '/opencode')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
contents: read
|
|
||||||
pull-requests: read
|
|
||||||
issues: read
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Run opencode
|
|
||||||
uses: anomalyco/opencode/github@latest
|
|
||||||
env:
|
|
||||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
||||||
with:
|
|
||||||
model: anthropic/claude-3-haiku-20240307
|
|
||||||
8
.github/workflows/php-linter.yml
vendored
8
.github/workflows/php-linter.yml
vendored
@@ -12,14 +12,6 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: PHP Lint 8.0
|
|
||||||
uses: dbfx/github-phplint/8.0@master
|
|
||||||
with:
|
|
||||||
folder-to-exclude: "! -path \"./vendor/*\" ! -path \"./folder/excluded/*\""
|
|
||||||
- name: PHP Lint 8.1
|
|
||||||
uses: dbfx/github-phplint/8.1@master
|
|
||||||
with:
|
|
||||||
folder-to-exclude: "! -path \"./vendor/*\" ! -path \"./folder/excluded/*\""
|
|
||||||
- name: PHP Lint 8.2
|
- name: PHP Lint 8.2
|
||||||
uses: dbfx/github-phplint/8.2@master
|
uses: dbfx/github-phplint/8.2@master
|
||||||
with:
|
with:
|
||||||
|
|||||||
17
.github/workflows/phpunit.yml
vendored
17
.github/workflows/phpunit.yml
vendored
@@ -34,7 +34,6 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
php-version:
|
php-version:
|
||||||
- '8.1'
|
|
||||||
- '8.2'
|
- '8.2'
|
||||||
- '8.3'
|
- '8.3'
|
||||||
- '8.4'
|
- '8.4'
|
||||||
@@ -69,9 +68,6 @@ jobs:
|
|||||||
- name: Install npm dependencies
|
- name: Install npm dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
- name: Build database.sql
|
|
||||||
run: npm run gulp build-database
|
|
||||||
|
|
||||||
- name: Start MariaDB
|
- name: Start MariaDB
|
||||||
run: |
|
run: |
|
||||||
docker run -d --name mysql \
|
docker run -d --name mysql \
|
||||||
@@ -79,7 +75,6 @@ jobs:
|
|||||||
-e MYSQL_DATABASE=ospos \
|
-e MYSQL_DATABASE=ospos \
|
||||||
-e MYSQL_USER=admin \
|
-e MYSQL_USER=admin \
|
||||||
-e MYSQL_PASSWORD=pointofsale \
|
-e MYSQL_PASSWORD=pointofsale \
|
||||||
-v $PWD/app/Database/database.sql:/docker-entrypoint-initdb.d/database.sql \
|
|
||||||
-p 3306:3306 \
|
-p 3306:3306 \
|
||||||
mariadb:10.5
|
mariadb:10.5
|
||||||
# Wait for MariaDB to be ready
|
# Wait for MariaDB to be ready
|
||||||
@@ -111,8 +106,16 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CI_ENVIRONMENT: testing
|
CI_ENVIRONMENT: testing
|
||||||
MYSQL_HOST_NAME: 127.0.0.1
|
MYSQL_HOST_NAME: 127.0.0.1
|
||||||
run: composer test
|
run: composer test -- --log-junit test-results/junit.xml
|
||||||
|
|
||||||
|
- name: Upload test results
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: test-results-php-${{ matrix.php-version }}
|
||||||
|
path: test-results/
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
- name: Stop MariaDB
|
- name: Stop MariaDB
|
||||||
if: always()
|
if: always()
|
||||||
run: docker stop mysql && docker rm mysql
|
run: docker stop mysql && docker rm mysql
|
||||||
|
|||||||
172
.github/workflows/release.yml
vendored
Normal file
172
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
name: Release Version Bump
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version_type:
|
||||||
|
description: 'Version bump type'
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- minor
|
||||||
|
- major
|
||||||
|
- patch
|
||||||
|
default: 'minor'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
prepare-release:
|
||||||
|
name: Prepare Release
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Get current version
|
||||||
|
id: current_version
|
||||||
|
run: |
|
||||||
|
CURRENT_VERSION=$(grep "application_version" app/Config/App.php | sed "s/.*= '\(.*\)';/\1/g")
|
||||||
|
echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Current version: $CURRENT_VERSION"
|
||||||
|
|
||||||
|
- name: Calculate new version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
CURRENT_VERSION="${{ steps.current_version.outputs.current_version }}"
|
||||||
|
VERSION_TYPE="${{ github.event.inputs.version_type }}"
|
||||||
|
|
||||||
|
# Parse current version
|
||||||
|
MAJOR=$(echo $CURRENT_VERSION | cut -d. -f1)
|
||||||
|
MINOR=$(echo $CURRENT_VERSION | cut -d. -f2)
|
||||||
|
PATCH=$(echo $CURRENT_VERSION | cut -d. -f3)
|
||||||
|
|
||||||
|
# Bump version based on type
|
||||||
|
case $VERSION_TYPE in
|
||||||
|
major)
|
||||||
|
MAJOR=$((MAJOR + 1))
|
||||||
|
MINOR=0
|
||||||
|
PATCH=0
|
||||||
|
;;
|
||||||
|
minor)
|
||||||
|
MINOR=$((MINOR + 1))
|
||||||
|
PATCH=0
|
||||||
|
;;
|
||||||
|
patch)
|
||||||
|
PATCH=$((PATCH + 1))
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
|
||||||
|
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "previous_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "New version: $NEW_VERSION (was: $CURRENT_VERSION, type: $VERSION_TYPE)"
|
||||||
|
|
||||||
|
- name: Update version in App.php
|
||||||
|
run: |
|
||||||
|
NEW_VERSION="${{ steps.version.outputs.new_version }}"
|
||||||
|
sed -i "s/public string \\\$application_version = '[^']*';/public string \\\$application_version = '$NEW_VERSION';/" app/Config/App.php
|
||||||
|
echo "Updated app/Config/App.php"
|
||||||
|
|
||||||
|
- name: Update version in package.json
|
||||||
|
run: |
|
||||||
|
NEW_VERSION="${{ steps.version.outputs.new_version }}"
|
||||||
|
sed -i "s/\"version\": \"[^\"]*\",/\"version\": \"$NEW_VERSION\",/" package.json
|
||||||
|
echo "Updated package.json"
|
||||||
|
|
||||||
|
- name: Update version in docker-compose.nginx.yml
|
||||||
|
run: |
|
||||||
|
NEW_VERSION="${{ steps.version.outputs.new_version }}"
|
||||||
|
sed -i "s/jekkos\/opensourcepos:[^ ]*/jekkos\/opensourcepos:$NEW_VERSION/" docker-compose.nginx.yml
|
||||||
|
echo "Updated docker-compose.nginx.yml"
|
||||||
|
|
||||||
|
- name: Update version in README.md
|
||||||
|
run: |
|
||||||
|
NEW_VERSION="${{ steps.version.outputs.new_version }}"
|
||||||
|
# Extract major.minor for the "latest X.Y version" text
|
||||||
|
MAJOR_MINOR=$(echo "$NEW_VERSION" | cut -d. -f1,2)
|
||||||
|
sed -i "s/The latest \`[0-9]*\.[0-9]*\` version/The latest \`${MAJOR_MINOR}\` version/" README.md
|
||||||
|
echo "Updated README.md with version ${MAJOR_MINOR}"
|
||||||
|
|
||||||
|
- name: Generate changelog
|
||||||
|
id: changelog
|
||||||
|
run: |
|
||||||
|
PREVIOUS_VERSION="${{ steps.version.outputs.previous_version }}"
|
||||||
|
NEW_VERSION="${{ steps.version.outputs.new_version }}"
|
||||||
|
|
||||||
|
# Get commits since last version
|
||||||
|
if git rev-parse "$PREVIOUS_VERSION" >/dev/null 2>&1; then
|
||||||
|
COMMITS=$(git log "$PREVIOUS_VERSION"..HEAD --pretty=format:"- %s" --no-merges)
|
||||||
|
else
|
||||||
|
COMMITS=$(git log --pretty=format:"- %s" --no-merges -50)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create changelog entry
|
||||||
|
CHANGELOG_FILE="CHANGELOG.md"
|
||||||
|
|
||||||
|
# Create the new version comparison link
|
||||||
|
NEW_LINK="[${NEW_VERSION}]: https://github.com/opensourcepos/opensourcepos/compare/${PREVIOUS_VERSION}...${NEW_VERSION}"
|
||||||
|
|
||||||
|
# Insert new link after [unreleased] line
|
||||||
|
sed -i "/^\[unreleased\]/a $NEW_LINK" "$CHANGELOG_FILE"
|
||||||
|
|
||||||
|
# Update [unreleased] link to start from new version
|
||||||
|
sed -i "s|^\[unreleased\]: .*|\[unreleased\]: https://github.com/opensourcepos/opensourcepos/compare/${NEW_VERSION}...HEAD|" "$CHANGELOG_FILE"
|
||||||
|
|
||||||
|
# Create version header and content using temp file to avoid sed issues with special characters
|
||||||
|
VERSION_DATE=$(date +%Y-%m-%d)
|
||||||
|
VERSION_HEADER="## [$NEW_VERSION] - $VERSION_DATE"
|
||||||
|
|
||||||
|
# Create temp file with changelog entry
|
||||||
|
TMP_FILE=$(mktemp)
|
||||||
|
{
|
||||||
|
echo ""
|
||||||
|
echo "$VERSION_HEADER"
|
||||||
|
echo ""
|
||||||
|
echo "$COMMITS"
|
||||||
|
} > "$TMP_FILE"
|
||||||
|
|
||||||
|
# Insert after Unreleased header
|
||||||
|
sed -i "/^## \[Unreleased\]/r $TMP_FILE" "$CHANGELOG_FILE"
|
||||||
|
rm "$TMP_FILE"
|
||||||
|
|
||||||
|
echo "Updated CHANGELOG.md"
|
||||||
|
echo "Changelog entries:"
|
||||||
|
echo "$COMMITS"
|
||||||
|
|
||||||
|
- name: Update version in issue templates
|
||||||
|
run: |
|
||||||
|
NEW_VERSION="${{ steps.version.outputs.new_version }}"
|
||||||
|
|
||||||
|
# Calculate version to remove (keep 5 versions)
|
||||||
|
PREVIOUS_VERSION="${{ steps.version.outputs.previous_version }}"
|
||||||
|
|
||||||
|
# Bug report template - insert new version after development (unreleased)
|
||||||
|
BUG_TEMPLATE=".github/ISSUE_TEMPLATE/bug report.yml"
|
||||||
|
sed -i "/- development (unreleased)/a\\ - OpenSourcePOS ${NEW_VERSION}" "$BUG_TEMPLATE"
|
||||||
|
# Remove the oldest version (5th version from the end)
|
||||||
|
sed -i "/OpenSourcePOS 3\\.3\\.7/d" "$BUG_TEMPLATE"
|
||||||
|
echo "Updated $BUG_TEMPLATE"
|
||||||
|
|
||||||
|
# Feature request template - insert new version after development (unreleased)
|
||||||
|
FEATURE_TEMPLATE=".github/ISSUE_TEMPLATE/feature_request.yml"
|
||||||
|
sed -i "/- development (unreleased)/a\\ - OpenSourcePOS ${NEW_VERSION}" "$FEATURE_TEMPLATE"
|
||||||
|
# Remove the oldest version (5th version from the end)
|
||||||
|
sed -i "/OpenSourcePOS 3\\.3\\.7/d" "$FEATURE_TEMPLATE"
|
||||||
|
echo "Updated $FEATURE_TEMPLATE"
|
||||||
|
|
||||||
|
- name: Commit version bump
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
NEW_VERSION="${{ steps.version.outputs.new_version }}"
|
||||||
|
|
||||||
|
git add app/Config/App.php package.json docker-compose.nginx.yml CHANGELOG.md README.md .github/ISSUE_TEMPLATE/
|
||||||
|
git commit -m "chore: release version $NEW_VERSION"
|
||||||
|
git push origin HEAD
|
||||||
54
.travis.yml
54
.travis.yml
@@ -1,54 +0,0 @@
|
|||||||
sudo: required
|
|
||||||
|
|
||||||
branches:
|
|
||||||
except:
|
|
||||||
- unstable
|
|
||||||
- weblate
|
|
||||||
services:
|
|
||||||
- docker
|
|
||||||
|
|
||||||
dist: jammy
|
|
||||||
language: node_js
|
|
||||||
node_js:
|
|
||||||
- 20
|
|
||||||
script:
|
|
||||||
- echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
|
||||||
- docker run --rm -u $(id -u) -v $(pwd):/app opensourcepos/composer:ci4 composer install
|
|
||||||
- version=$(grep application_version app/Config/App.php | sed "s/.*=\s'\(.*\)';/\1/g")
|
|
||||||
- cp .env.example .env && sed -i 's/production/development/g' .env
|
|
||||||
- sed -i "s/commit_sha1 = 'dev'/commit_sha1 = '$rev'/g" app/Config/OSPOS.php
|
|
||||||
- echo "$version-$branch-$rev"
|
|
||||||
- npm version "$version-$branch-$rev" --force || true
|
|
||||||
- sed -i 's/opensourcepos.tar.gz/opensourcepos.$version.tgz/g' package.json
|
|
||||||
- npm ci && npm install -g gulp && npm run build
|
|
||||||
- docker build . --target ospos -t ospos
|
|
||||||
- docker build . --target ospos_test -t ospos_test
|
|
||||||
- docker run --rm ospos_test /app/vendor/bin/phpunit --testdox
|
|
||||||
- docker build app/Database/ -t "jekkos/opensourcepos:sql-$TAG"
|
|
||||||
env:
|
|
||||||
global:
|
|
||||||
- BRANCH=$(echo ${TRAVIS_BRANCH} | sed s/feature\\///)
|
|
||||||
- TAG=$(echo "${TRAVIS_TAG:-$BRANCH}" | tr '/' '-')
|
|
||||||
- date=`date +%Y%m%d%H%M%S` && branch=${TRAVIS_BRANCH} && rev=`git rev-parse --short=6 HEAD`
|
|
||||||
after_success:
|
|
||||||
- docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" && docker tag "ospos:latest"
|
|
||||||
"jekkos/opensourcepos:$TAG" && docker push "jekkos/opensourcepos:$TAG" && docker push "jekkos/opensourcepos:sql-$TAG"
|
|
||||||
- gulp compress
|
|
||||||
- mv dist/opensourcepos.tar.gz "dist/opensourcepos.$version.$rev.tgz"
|
|
||||||
- mv dist/opensourcepos.zip "dist/opensourcepos.$version.$rev.zip"
|
|
||||||
deploy:
|
|
||||||
- provider: releases
|
|
||||||
edge: true
|
|
||||||
file: dist/opensourcepos.$version.$rev.zip
|
|
||||||
name: "Unstable OpensourcePos"
|
|
||||||
overwrite: true
|
|
||||||
release_notes: "This is a build of the latest master which might contain bugs. Use at your own risk. Check releases section for the latest official release"
|
|
||||||
prerelease: true
|
|
||||||
tag_name: unstable
|
|
||||||
user: jekkos
|
|
||||||
|
|
||||||
api_key:
|
|
||||||
secure: "KOukL8IFf/uL/BjMyCSKjf2vylydjcWqgEx0eMqFCg3nZ4ybMaOwPORRthIfyT72/FvGX/aoxxEn0uR/AEtb+hYQXHmNS+kZdX72JCe8LpGuZ7FJ5X+Eo9mhJcsmS+smd1sC95DySSc/GolKPo+0WtJYONY/xGCLxm+9Ay4HREg="
|
|
||||||
|
|
||||||
on:
|
|
||||||
branch: master
|
|
||||||
40
AGENTS.md
Normal file
40
AGENTS.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Agent Instructions
|
||||||
|
|
||||||
|
This document provides guidance for AI agents working on the Open Source Point of Sale (OSPOS) codebase.
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
- Follow PHP CodeIgniter 4 coding standards
|
||||||
|
- Run PHP-CS-Fixer before committing: `vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.no-header.php`
|
||||||
|
- Write PHP 8.1+ compatible code with proper type declarations
|
||||||
|
- Use PSR-12 naming conventions: `camelCase` for variables and functions, `PascalCase` for classes, `UPPER_CASE` for constants
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
- Create a new git worktree for each issue, based on the latest state of `origin/master`
|
||||||
|
- Commit fixes to the worktree and push to the remote
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Run PHPUnit tests: `composer test`
|
||||||
|
- Tests must pass before submitting changes
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
- Install dependencies: `composer install && npm install`
|
||||||
|
- Build assets: `npm run build` or `gulp`
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- Controllers go in `app/Controllers/`
|
||||||
|
- Models go in `app/Models/`
|
||||||
|
- Views go in `app/Views/`
|
||||||
|
- Database migrations in `app/Database/Migrations/`
|
||||||
|
- Use CodeIgniter 4 framework patterns and helpers
|
||||||
|
- Sanitize user input; escape output using `esc()` helper
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- Never commit secrets, credentials, or `.env` files
|
||||||
|
- Use parameterized queries to prevent SQL injection
|
||||||
|
- Validate and sanitize all user input
|
||||||
32
Dockerfile
32
Dockerfile
@@ -1,28 +1,22 @@
|
|||||||
FROM php:8.2-apache AS ospos
|
FROM php:8.2-apache AS ospos
|
||||||
LABEL maintainer="jekkos"
|
LABEL maintainer="jekkos"
|
||||||
|
|
||||||
RUN apt update && apt-get install -y libicu-dev libgd-dev
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
RUN a2enmod rewrite
|
libicu-dev \
|
||||||
RUN docker-php-ext-install mysqli bcmath intl gd
|
libgd-dev \
|
||||||
|
&& docker-php-ext-install mysqli bcmath intl gd \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& a2enmod rewrite
|
||||||
|
|
||||||
RUN echo "date.timezone = \"\${PHP_TIMEZONE}\"" > /usr/local/etc/php/conf.d/timezone.ini
|
RUN echo "date.timezone = \"\${PHP_TIMEZONE}\"" > /usr/local/etc/php/conf.d/timezone.ini
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . /app
|
COPY --chown=www-data:www-data . /app
|
||||||
RUN ln -s /app/*[^public] /var/www && rm -rf /var/www/html && ln -nsf /app/public /var/www/html
|
RUN chmod 770 /app/writable/uploads /app/writable/logs /app/writable/cache \
|
||||||
RUN chmod -R 770 /app/writable/uploads /app/writable/logs /app/writable/cache && chown -R www-data:www-data /app
|
&& ln -s /app/*[^public] /var/www \
|
||||||
|
&& rm -rf /var/www/html \
|
||||||
FROM ospos AS ospos_test
|
&& ln -nsf /app/public /var/www/html
|
||||||
|
|
||||||
COPY --from=composer /usr/bin/composer /usr/bin/composer
|
|
||||||
|
|
||||||
RUN apt-get install -y libzip-dev wget git
|
|
||||||
RUN wget https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh -O /bin/wait-for-it.sh && chmod +x /bin/wait-for-it.sh
|
|
||||||
RUN docker-php-ext-install zip
|
|
||||||
RUN composer install -d/app
|
|
||||||
#RUN sed -i 's/backupGlobals="true"/backupGlobals="false"/g' /app/tests/phpunit.xml
|
|
||||||
WORKDIR /app/tests
|
|
||||||
|
|
||||||
CMD ["/app/vendor/phpunit/phpunit/phpunit", "/app/test/helpers"]
|
|
||||||
|
|
||||||
FROM ospos AS ospos_dev
|
FROM ospos AS ospos_dev
|
||||||
|
|
||||||
|
|||||||
55
INSTALL.md
55
INSTALL.md
@@ -1,27 +1,68 @@
|
|||||||
## Server Requirements
|
## Server Requirements
|
||||||
|
|
||||||
- PHP version `8.1` to `8.4` are supported, PHP version `≤7.4` is NOT supported. Please note that PHP needs to have the extensions `php-json`, `php-gd`, `php-bcmath`, `php-intl`, `php-openssl`, `php-mbstring`, `php-curl` and `php-xml` installed and enabled. An unstable master build can be downloaded in the releases section.
|
- PHP version `8.2` to `8.4` are supported, PHP version `≤ 8.1` is NOT supported. Please note that PHP needs to have the extensions `php-json`, `php-gd`, `php-bcmath`, `php-intl`, `php-openssl`, `php-mbstring`, `php-curl` and `php-xml` installed and enabled. An unstable master build can be downloaded in the releases section.
|
||||||
- MySQL `5.7` is supported, also MariaDB replacement `10.x` is supported and might offer better performance.
|
- MySQL `5.7` is supported, also MariaDB replacement `10.x` is supported and might offer better performance.
|
||||||
- Apache `2.4` is supported. Nginx should work fine too, see [wiki page here](https://github.com/opensourcepos/opensourcepos/wiki/Local-Deployment-using-LEMP).
|
- Apache `2.4` is supported. Nginx should work fine too, see [wiki page here](https://github.com/opensourcepos/opensourcepos/wiki/Local-Deployment-using-LEMP).
|
||||||
- Raspberry PI based installations proved to work, see [wiki page here](<https://github.com/opensourcepos/opensourcepos/wiki/Installing-on-Raspberry-PI---Orange-PI-(Headless-OSPOS)>).
|
- Raspberry PI based installations proved to work, see [wiki page here](<https://github.com/opensourcepos/opensourcepos/wiki/Installing-on-Raspberry-PI---Orange-PI-(Headless-OSPOS)>).
|
||||||
- For Windows based installations please read [the wiki](https://github.com/opensourcepos/opensourcepos/wiki). There are closed issues about this subject, as this topic has been covered a lot.
|
- For Windows based installations please read [the wiki](https://github.com/opensourcepos/opensourcepos/wiki). There are closed issues about this subject, as this topic has been covered a lot.
|
||||||
|
|
||||||
|
## Security Configuration
|
||||||
|
|
||||||
|
### Allowed Hostnames (REQUIRED for Production)
|
||||||
|
|
||||||
|
⚠️ **CRITICAL**: OpenSourcePOS validates the Host header to prevent Host Header Injection attacks (GHSA-jchf-7hr6-h4f3). **You MUST configure `app.allowedHostnames` for production deployments. If not configured, the application will fail to start.**
|
||||||
|
|
||||||
|
**Add to your `.env` file:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Comma-separated list of allowed hostnames (no protocols or ports)
|
||||||
|
app.allowedHostnames = 'yourdomain.com,www.yourdomain.com'
|
||||||
|
```
|
||||||
|
|
||||||
|
**For local development:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
app.allowedHostnames = 'localhost'
|
||||||
|
```
|
||||||
|
|
||||||
|
**If you see this error at startup:**
|
||||||
|
|
||||||
|
```text
|
||||||
|
RuntimeException: Security: allowedHostnames is not configured.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Add `app.allowedHostnames` to your `.env` file with your domain(s).
|
||||||
|
|
||||||
|
**Why this matters:**
|
||||||
|
- Prevents Host Header Injection attacks (GHSA-jchf-7hr6-h4f3)
|
||||||
|
- Ensures URLs are generated with the correct domain
|
||||||
|
- Security advisory: https://github.com/opensourcepos/opensourcepos/security/advisories/GHSA-jchf-7hr6-h4f3
|
||||||
|
- Fixes issue #4480: .env configuration now works via comma-separated values
|
||||||
|
|
||||||
|
### HTTPS Behind Proxy
|
||||||
|
|
||||||
|
If your installation is behind a proxy with SSL offloading, set:
|
||||||
|
```
|
||||||
|
FORCE_HTTPS = true
|
||||||
|
```
|
||||||
|
|
||||||
## Local install
|
## Local install
|
||||||
|
|
||||||
First of all, if you're seeing the message `system folder missing` after launching your browser, or cannot find `database.sql`, that most likely means you have cloned the repository and have not built the project. To build the project from a source commit point instead of from an official release check out [Building OSPOS](BUILD.md). Otherwise, continue with the following steps.
|
First of all, if you're seeing the message `system folder missing` after launching your browser, that most likely means you have cloned the repository and have not built the project. To build the project from a source commit point instead of from an official release check out [Building OSPOS](BUILD.md). Otherwise, continue with the following steps.
|
||||||
|
|
||||||
1. Download the a [pre-release for a specific branch](https://github.com/opensourcepos/opensourcepos/releases) or the latest stable [from GitHub here](https://github.com/opensourcepos/opensourcepos/releases). A repository clone will not work unless know how to build the project.
|
1. Download the a [pre-release for a specific branch](https://github.com/opensourcepos/opensourcepos/releases) or the latest stable [from GitHub here](https://github.com/opensourcepos/opensourcepos/releases). A repository clone will not work unless know how to build the project.
|
||||||
2. Create/locate a new MySQL database to install Open Source Point of Sale into.
|
2. Create/locate a new MySQL database to install Open Source Point of Sale into.
|
||||||
3. Execute the file `app/Database/database.sql` to create the tables needed.
|
3. Unzip and upload Open Source Point of Sale files to the web-server.
|
||||||
4. Unzip and upload Open Source Point of Sale files to the web-server.
|
4. If `.env` does not exist, copy `.env.example` to `.env`.
|
||||||
5. Open `.env` file and modify credentials to connect to your database if needed. (First copy .env.example to .env and update)
|
5. Open `.env` and modify credentials to connect to your database if needed.
|
||||||
|
6. The database schema will be automatically created when you first access the application. Migrations run automatically on fresh installs.
|
||||||
7. Go to your install `public` dir via the browser.
|
7. Go to your install `public` dir via the browser.
|
||||||
8. Log in using
|
8. Log in using
|
||||||
- Username: admin
|
- Username: admin
|
||||||
- Password: pointofsale
|
- Password: pointofsale
|
||||||
9. If everything works, then set the `CI_ENVIRONMENT` variable to `production` in the .env file
|
9. If everything works, then set the `CI_ENVIRONMENT` variable to `production` in the .env file
|
||||||
9. Enjoy!
|
10. Enjoy!
|
||||||
10. Oops, an issue? Please make sure you read the FAQ, wiki page, and you checked open and closed issues on GitHub. PHP `display_errors` is disabled by default. Create an` app/Config/.env` file from the `.env.example` to enable it in a development environment.
|
11. Oops, an issue? Please make sure you read the FAQ, wiki page, and you checked open and closed issues on GitHub. PHP `display_errors` is disabled by default. Create an` app/Config/.env` file from the `.env.example` to enable it in a development environment.
|
||||||
|
|
||||||
## Local install using Docker
|
## Local install using Docker
|
||||||
|
|
||||||
|
|||||||
12
README.md
12
README.md
@@ -8,7 +8,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://app.travis-ci.com/opensourcepos/opensourcepos" target="_blank"><img src="https://api.travis-ci.com/opensourcepos/opensourcepos.svg?branch=master" alt="Build Status"></a>
|
<a href="https://github.com/opensourcepos/opensourcepos/actions/workflows/build-release.yml" target="_blank"><img src="https://github.com/opensourcepos/opensourcepos/actions/workflows/build-release.yml/badge.svg" alt="Build Status"></a>
|
||||||
<a href="https://app.gitter.im/#/room/#opensourcepos_Lobby:gitter.im?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge" target="_blank"><img src="https://badges.gitter.im/jekkos/opensourcepos.svg" alt="Join the chat at https://app.gitter.im"></a>
|
<a href="https://app.gitter.im/#/room/#opensourcepos_Lobby:gitter.im?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge" target="_blank"><img src="https://badges.gitter.im/jekkos/opensourcepos.svg" alt="Join the chat at https://app.gitter.im"></a>
|
||||||
<a href="https://badge.fury.io/gh/opensourcepos%2Fopensourcepos" target="_blank"><img src="https://badge.fury.io/gh/opensourcepos%2Fopensourcepos.svg" alt="Project Version"></a>
|
<a href="https://badge.fury.io/gh/opensourcepos%2Fopensourcepos" target="_blank"><img src="https://badge.fury.io/gh/opensourcepos%2Fopensourcepos.svg" alt="Project Version"></a>
|
||||||
<a href="https://translate.opensourcepos.org/engage/opensourcepos/?utm_source=widget" target="_blank"><img src="https://translate.opensourcepos.org/widgets/opensourcepos/-/svg-badge.svg" alt="Translation Status"></a>
|
<a href="https://translate.opensourcepos.org/engage/opensourcepos/?utm_source=widget" target="_blank"><img src="https://translate.opensourcepos.org/widgets/opensourcepos/-/svg-badge.svg" alt="Translation Status"></a>
|
||||||
@@ -102,11 +102,11 @@ NOTE: If you're running non-release code, please make sure you always run the la
|
|||||||
|
|
||||||
- If you have suhosin installed and face an issue with CSRF, please make sure you read [issue #1492](https://github.com/opensourcepos/opensourcepos/issues/1492).
|
- If you have suhosin installed and face an issue with CSRF, please make sure you read [issue #1492](https://github.com/opensourcepos/opensourcepos/issues/1492).
|
||||||
|
|
||||||
- PHP `≥ 8.1` is required to run this app.
|
- PHP `≥ 8.2` is required to run this app.
|
||||||
|
|
||||||
## 🏃 Keep the Machine Running
|
## 🏃 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)\
|
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=MUN6AEG7NY6H8)\
|
||||||
Or refer to the [FUNDING.yml](.github/FUNDING.yml) file.
|
Or refer to the [FUNDING.yml](.github/FUNDING.yml) file.
|
||||||
@@ -137,7 +137,7 @@ Any person or company found breaching the license agreement might find a bunch o
|
|||||||
|
|
||||||
## 🙏 Credits
|
## 🙏 Credits
|
||||||
|
|
||||||
| <div align="center">DigitalOcean</div> | <div align="center">JetBrains</div> | <div align="center">Travis CI</div> |
|
| <div align="center">DigitalOcean</div> | <div align="center">JetBrains</div> | <div align="center">GitHub</div> |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| <div align="center"><a href="https://www.digitalocean.com?utm_medium=opensource&utm_source=opensourcepos" target="_blank"><img src="https://github.com/user-attachments/assets/fbbf7433-ed35-407d-8946-fd03d236d350" alt="DigitalOcean Logo" height="50"></a></div> | <div align="center"><a href="https://www.jetbrains.com/idea/" target="_blank"><img src="https://github.com/opensourcepos/opensourcepos/assets/12870258/187f9bbe-4484-475c-9b58-5e5d5f931f09" alt="IntelliJ IDEA Logo" height="50"></a></div> | <div align="center"><a href="https://www.travis-ci.com/" target="_blank"><img src="https://github.com/opensourcepos/opensourcepos/assets/12870258/71cc2b44-83af-4510-a543-6358285f43c6" alt="Travis CI Logo" height="50"></a></div> |
|
| <div align="center"><a href="https://www.digitalocean.com?utm_medium=opensource&utm_source=opensourcepos" target="_blank"><img src="https://github.com/user-attachments/assets/fbbf7433-ed35-407d-8946-fd03d236d350" alt="DigitalOcean Logo" height="50"></a></div> | <div align="center"><a href="https://www.jetbrains.com/idea/" target="_blank"><img src="https://github.com/opensourcepos/opensourcepos/assets/12870258/187f9bbe-4484-475c-9b58-5e5d5f931f09" alt="IntelliJ IDEA Logo" height="50"></a></div> | <div align="center"><a href="https://github.com/features/actions" target="_blank"><img src="https://github.githubassets.com/images/modules/site/icons/eyebrow-panel/actions-icon.svg" alt="GitHub Actions Logo" height="50"></a></div> |
|
||||||
| Many thanks to [DigitalOcean](https://www.digitalocean.com) for providing the project with hosting credits. | Many thanks to [JetBrains](https://www.jetbrains.com/) for providing a free license of [IntelliJ IDEA](https://www.jetbrains.com/idea/) to kindly support the development of OSPOS. | Many thanks to [Travis CI](https://www.travis-ci.com/) for providing a free continuous integration service for open source projects. |
|
| Many thanks to [DigitalOcean](https://www.digitalocean.com) for providing the project with hosting credits. | Many thanks to [JetBrains](https://www.jetbrains.com/) for providing a free license of [IntelliJ IDEA](https://www.jetbrains.com/idea/) to kindly support the development of OSPOS. | Many thanks to [GitHub](https://github.com) for providing free continuous integration via GitHub Actions for open-source projects. |
|
||||||
|
|||||||
37
SECURITY.md
37
SECURITY.md
@@ -1,9 +1,9 @@
|
|||||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||||
|
|
||||||
|
|
||||||
- [Security Policy](#security-policy)
|
- [Security Policy](#security-policy)
|
||||||
- [Supported Versions](#supported-versions)
|
- [Supported Versions](#supported-versions)
|
||||||
|
- [Security Advisories](#security-advisories)
|
||||||
- [Reporting a Vulnerability](#reporting-a-vulnerability)
|
- [Reporting a Vulnerability](#reporting-a-vulnerability)
|
||||||
|
|
||||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||||
@@ -12,14 +12,35 @@
|
|||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
We release patches for security vulnerabilities. Which versions are eligible to receive such patches depend on the CVSS v3.0 Rating:
|
We release patches for security vulnerabilities.
|
||||||
|
|
||||||
| CVSS v3.0 | Supported Versions |
|
| Version | Supported |
|
||||||
| --------- | -------------------------------------------------- |
|
| --------- | ------------------ |
|
||||||
| 7.3 | 3.3.5 |
|
| >= 3.4.2 | :white_check_mark: |
|
||||||
| 9.8 | 3.3.6 |
|
| < 3.4.2 | :x: |
|
||||||
| 6.8 | 3.4.2 |
|
|
||||||
|
## 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).
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
Please report (suspected) security vulnerabilities to **[jeroen@steganos.dev](mailto:jeroen@steganos.dev)**. 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.
|
Please report (suspected) security vulnerabilities to **[jeroen@steganos.dev](mailto:jeroen@steganos.dev)**.
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -117,7 +117,7 @@ class App extends BaseConfig
|
|||||||
| DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!!
|
| DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!!
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
public string $permittedURIChars = 'a-z 0-9~%.:_\-=';
|
public string $permittedURIChars = 'a-z 0-9~%.:_\-';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* --------------------------------------------------------------------------
|
* --------------------------------------------------------------------------
|
||||||
@@ -278,14 +278,79 @@ class App extends BaseConfig
|
|||||||
* @see http://www.html5rocks.com/en/tutorials/security/content-security-policy/
|
* @see http://www.html5rocks.com/en/tutorials/security/content-security-policy/
|
||||||
* @see http://www.w3.org/TR/CSP/
|
* @see http://www.w3.org/TR/CSP/
|
||||||
*/
|
*/
|
||||||
public bool $CSPEnabled = false; // TODO: Currently CSP3 tags are not supported so enabling this causes problems with script-src-elem, style-src-attr and style-src-elem
|
public bool $CSPEnabled = false;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
|
||||||
|
// Solution for CodeIgniter 4 limitation: arrays cannot be set from .env
|
||||||
|
// See: https://github.com/codeigniter4/CodeIgniter4/issues/7311
|
||||||
|
$envAllowedHostnames = getenv('app.allowedHostnames');
|
||||||
|
if ($envAllowedHostnames !== false && trim($envAllowedHostnames) !== '') {
|
||||||
|
$this->allowedHostnames = array_values(array_filter(
|
||||||
|
array_map('trim', explode(',', $envAllowedHostnames)),
|
||||||
|
static fn (string $hostname): bool => $hostname !== ''
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
$this->https_on = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') || (isset($_ENV['FORCE_HTTPS']) && $_ENV['FORCE_HTTPS'] == 'true');
|
$this->https_on = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') || (isset($_ENV['FORCE_HTTPS']) && $_ENV['FORCE_HTTPS'] == 'true');
|
||||||
|
|
||||||
|
$host = $this->getValidHost();
|
||||||
$this->baseURL = $this->https_on ? 'https' : 'http';
|
$this->baseURL = $this->https_on ? 'https' : 'http';
|
||||||
$this->baseURL .= '://' . ((isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : 'localhost') . '/';
|
$this->baseURL .= '://' . $host . '/';
|
||||||
$this->baseURL .= str_replace(basename($_SERVER['SCRIPT_NAME']), '', $_SERVER['SCRIPT_NAME']);
|
$this->baseURL .= str_replace(basename($_SERVER['SCRIPT_NAME']), '', $_SERVER['SCRIPT_NAME']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates and returns a trusted hostname.
|
||||||
|
*
|
||||||
|
* Security: Prevents Host Header Injection attacks (GHSA-jchf-7hr6-h4f3)
|
||||||
|
* by validating the HTTP_HOST against a whitelist of allowed hostnames.
|
||||||
|
*
|
||||||
|
* In production: Fails fast if allowedHostnames is not configured.
|
||||||
|
* In development: Allows localhost fallback with an error log.
|
||||||
|
*
|
||||||
|
* @return string A validated hostname
|
||||||
|
* @throws \RuntimeException If allowedHostnames is not configured in production
|
||||||
|
*/
|
||||||
|
private function getValidHost(): string
|
||||||
|
{
|
||||||
|
$httpHost = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||||
|
|
||||||
|
// Determine environment
|
||||||
|
// CodeIgniter's test bootstrap sets $_SERVER['CI_ENVIRONMENT'] = 'testing'
|
||||||
|
// Check $_SERVER first, then $_ENV, then fall back to 'production'
|
||||||
|
$environment = $_SERVER['CI_ENVIRONMENT'] ?? $_ENV['CI_ENVIRONMENT'] ?? getenv('CI_ENVIRONMENT') ?: 'production';
|
||||||
|
|
||||||
|
if (empty($this->allowedHostnames)) {
|
||||||
|
$errorMessage =
|
||||||
|
'Security: allowedHostnames is not configured. ' .
|
||||||
|
'Host header injection protection is disabled. ' .
|
||||||
|
'Set app.allowedHostnames in your .env file. ' .
|
||||||
|
'Example: app.allowedHostnames = "example.com,www.example.com" ' .
|
||||||
|
'Received Host: ' . $httpHost;
|
||||||
|
|
||||||
|
// Production: Fail explicitly to prevent silent security vulnerabilities
|
||||||
|
// Testing and development: Allow localhost fallback
|
||||||
|
if ($environment === 'production') {
|
||||||
|
throw new \RuntimeException($errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
log_message('error', $errorMessage . ' Using localhost fallback (development only).');
|
||||||
|
return 'localhost';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($httpHost, $this->allowedHostnames, true)) {
|
||||||
|
return $httpHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host not in whitelist - use first configured hostname as fallback
|
||||||
|
log_message('warning',
|
||||||
|
'Security: Rejected HTTP_HOST "' . $httpHost . '" - not in allowedHostnames whitelist. ' .
|
||||||
|
'Using fallback: ' . $this->allowedHostnames[0]
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->allowedHostnames[0];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ use CodeIgniter\Config\AutoloadConfig;
|
|||||||
*
|
*
|
||||||
* NOTE: This class is required prior to Autoloader instantiation,
|
* NOTE: This class is required prior to Autoloader instantiation,
|
||||||
* and does not extend BaseConfig.
|
* and does not extend BaseConfig.
|
||||||
*
|
|
||||||
* @immutable
|
|
||||||
*/
|
*/
|
||||||
class Autoload extends AutoloadConfig
|
class Autoload extends AutoloadConfig
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,23 +1,38 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The environment testing is reserved for PHPUnit testing. It has special
|
||||||
|
* conditions built into the framework at various places to assist with that.
|
||||||
|
* You can’t use it for your development.
|
||||||
|
*/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| ERROR DISPLAY
|
| ERROR DISPLAY
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
| In development, we want to show as many errors as possible to help
|
||||||
|
| make sure they don't make it to production. And save us hours of
|
||||||
|
| painful debugging.
|
||||||
|
*/
|
||||||
error_reporting(E_ALL);
|
error_reporting(E_ALL);
|
||||||
ini_set('display_errors', '1');
|
ini_set('display_errors', '1');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| DEBUG BACKTRACES
|
| DEBUG BACKTRACES
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
| If true, this constant will tell the error screens to display debug
|
||||||
|
| backtraces along with the other error information. If you would
|
||||||
|
| prefer to not see this, set this value to false.
|
||||||
|
*/
|
||||||
defined('SHOW_DEBUG_BACKTRACE') || define('SHOW_DEBUG_BACKTRACE', true);
|
defined('SHOW_DEBUG_BACKTRACE') || define('SHOW_DEBUG_BACKTRACE', true);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| DEBUG MODE
|
| DEBUG MODE
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
*/
|
| Debug mode is an experimental flag that can allow changes throughout
|
||||||
defined('CI_DEBUG') || define('CI_DEBUG', true);
|
| the system. It's not widely used currently, and may not survive
|
||||||
|
| release of the framework.
|
||||||
|
*/
|
||||||
|
defined('CI_DEBUG') || define('CI_DEBUG', true);
|
||||||
|
|||||||
@@ -6,6 +6,22 @@ use CodeIgniter\Config\BaseConfig;
|
|||||||
|
|
||||||
class CURLRequest extends BaseConfig
|
class CURLRequest extends BaseConfig
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
|
* CURLRequest Share Connection Options
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* Share connection options between requests.
|
||||||
|
*
|
||||||
|
* @var list<int>
|
||||||
|
*
|
||||||
|
* @see https://www.php.net/manual/en/curl.constants.php#constant.curl-lock-data-connect
|
||||||
|
*/
|
||||||
|
public array $shareConnectionOptions = [
|
||||||
|
CURL_LOCK_DATA_CONNECT,
|
||||||
|
CURL_LOCK_DATA_DNS,
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* --------------------------------------------------------------------------
|
* --------------------------------------------------------------------------
|
||||||
* CURLRequest Share Options
|
* CURLRequest Share Options
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace Config;
|
namespace Config;
|
||||||
|
|
||||||
use CodeIgniter\Cache\CacheInterface;
|
use CodeIgniter\Cache\CacheInterface;
|
||||||
|
use CodeIgniter\Cache\Handlers\ApcuHandler;
|
||||||
use CodeIgniter\Cache\Handlers\DummyHandler;
|
use CodeIgniter\Cache\Handlers\DummyHandler;
|
||||||
use CodeIgniter\Cache\Handlers\FileHandler;
|
use CodeIgniter\Cache\Handlers\FileHandler;
|
||||||
use CodeIgniter\Cache\Handlers\MemcachedHandler;
|
use CodeIgniter\Cache\Handlers\MemcachedHandler;
|
||||||
@@ -78,7 +79,7 @@ class Cache extends BaseConfig
|
|||||||
* Your file storage preferences can be specified below, if you are using
|
* Your file storage preferences can be specified below, if you are using
|
||||||
* the File driver.
|
* the File driver.
|
||||||
*
|
*
|
||||||
* @var array<string, int|string|null>
|
* @var array{storePath?: string, mode?: int}
|
||||||
*/
|
*/
|
||||||
public array $file = [
|
public array $file = [
|
||||||
'storePath' => WRITEPATH . 'cache/',
|
'storePath' => WRITEPATH . 'cache/',
|
||||||
@@ -95,7 +96,7 @@ class Cache extends BaseConfig
|
|||||||
*
|
*
|
||||||
* @see https://codeigniter.com/user_guide/libraries/caching.html#memcached
|
* @see https://codeigniter.com/user_guide/libraries/caching.html#memcached
|
||||||
*
|
*
|
||||||
* @var array<string, bool|int|string>
|
* @var array{host?: string, port?: int, weight?: int, raw?: bool}
|
||||||
*/
|
*/
|
||||||
public array $memcached = [
|
public array $memcached = [
|
||||||
'host' => '127.0.0.1',
|
'host' => '127.0.0.1',
|
||||||
@@ -108,17 +109,28 @@ class Cache extends BaseConfig
|
|||||||
* -------------------------------------------------------------------------
|
* -------------------------------------------------------------------------
|
||||||
* Redis settings
|
* Redis settings
|
||||||
* -------------------------------------------------------------------------
|
* -------------------------------------------------------------------------
|
||||||
|
*
|
||||||
* Your Redis server can be specified below, if you are using
|
* Your Redis server can be specified below, if you are using
|
||||||
* the Redis or Predis drivers.
|
* the Redis or Predis drivers.
|
||||||
*
|
*
|
||||||
* @var array<string, int|string|null>
|
* @var array{
|
||||||
|
* host?: string,
|
||||||
|
* password?: string|null,
|
||||||
|
* port?: int,
|
||||||
|
* timeout?: int,
|
||||||
|
* async?: bool,
|
||||||
|
* persistent?: bool,
|
||||||
|
* database?: int
|
||||||
|
* }
|
||||||
*/
|
*/
|
||||||
public array $redis = [
|
public array $redis = [
|
||||||
'host' => '127.0.0.1',
|
'host' => '127.0.0.1',
|
||||||
'password' => null,
|
'password' => null,
|
||||||
'port' => 6379,
|
'port' => 6379,
|
||||||
'timeout' => 0,
|
'timeout' => 0,
|
||||||
'database' => 0,
|
'async' => false, // specific to Predis and ignored by the native Redis extension
|
||||||
|
'persistent' => false,
|
||||||
|
'database' => 0,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -132,6 +144,7 @@ class Cache extends BaseConfig
|
|||||||
* @var array<string, class-string<CacheInterface>>
|
* @var array<string, class-string<CacheInterface>>
|
||||||
*/
|
*/
|
||||||
public array $validHandlers = [
|
public array $validHandlers = [
|
||||||
|
'apcu' => ApcuHandler::class,
|
||||||
'dummy' => DummyHandler::class,
|
'dummy' => DummyHandler::class,
|
||||||
'file' => FileHandler::class,
|
'file' => FileHandler::class,
|
||||||
'memcached' => MemcachedHandler::class,
|
'memcached' => MemcachedHandler::class,
|
||||||
@@ -158,4 +171,28 @@ class Cache extends BaseConfig
|
|||||||
* @var bool|list<string>
|
* @var bool|list<string>
|
||||||
*/
|
*/
|
||||||
public $cacheQueryString = false;
|
public $cacheQueryString = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
|
* Web Page Caching: Cache Status Codes
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* HTTP status codes that are allowed to be cached. Only responses with
|
||||||
|
* these status codes will be cached by the PageCache filter.
|
||||||
|
*
|
||||||
|
* Default: [] - Cache all status codes (backward compatible)
|
||||||
|
*
|
||||||
|
* Recommended: [200] - Only cache successful responses
|
||||||
|
*
|
||||||
|
* You can also use status codes like:
|
||||||
|
* [200, 404, 410] - Cache successful responses and specific error codes
|
||||||
|
* [200, 201, 202, 203, 204] - All 2xx successful responses
|
||||||
|
*
|
||||||
|
* WARNING: Using [] may cache temporary error pages (404, 500, etc).
|
||||||
|
* Consider restricting to [200] for production applications to avoid
|
||||||
|
* caching errors that should be temporary.
|
||||||
|
*
|
||||||
|
* @var list<int>
|
||||||
|
*/
|
||||||
|
public array $cacheStatusCodes = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,3 +169,8 @@ const MAX_PRECISION = 1e14;
|
|||||||
const DEFAULT_PRECISION = 2;
|
const DEFAULT_PRECISION = 2;
|
||||||
const DEFAULT_LANGUAGE = 'english';
|
const DEFAULT_LANGUAGE = 'english';
|
||||||
const DEFAULT_LANGUAGE_CODE = 'en';
|
const DEFAULT_LANGUAGE_CODE = 'en';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin modules - list of modules required for admin privileges
|
||||||
|
*/
|
||||||
|
const ADMIN_MODULES = ['customers', 'employees', 'giftcards', 'items', 'item_kits', 'messages', 'receivings', 'reports', 'sales', 'config', 'suppliers'];
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ class ContentSecurityPolicy extends BaseConfig
|
|||||||
*/
|
*/
|
||||||
public ?string $reportURI = null;
|
public ?string $reportURI = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies a reporting endpoint to which violation reports ought to be sent.
|
||||||
|
*/
|
||||||
|
public ?string $reportTo = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instructs user agents to rewrite URL schemes, changing
|
* Instructs user agents to rewrite URL schemes, changing
|
||||||
* HTTP to HTTPS. This directive is for websites with
|
* HTTP to HTTPS. This directive is for websites with
|
||||||
@@ -38,12 +43,12 @@ class ContentSecurityPolicy extends BaseConfig
|
|||||||
public bool $upgradeInsecureRequests = false;
|
public bool $upgradeInsecureRequests = false;
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Sources allowed
|
// CSP DIRECTIVES SETTINGS
|
||||||
// NOTE: once you set a policy to 'none', it cannot be further restricted
|
// NOTE: once you set a policy to 'none', it cannot be further restricted
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will default to self if not overridden
|
* Will default to `'self'` if not overridden
|
||||||
*
|
*
|
||||||
* @var list<string>|string|null
|
* @var list<string>|string|null
|
||||||
*/
|
*/
|
||||||
@@ -64,6 +69,21 @@ class ContentSecurityPolicy extends BaseConfig
|
|||||||
'www.google.com www.gstatic.com'
|
'www.google.com www.gstatic.com'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies valid sources for JavaScript <script> elements.
|
||||||
|
*
|
||||||
|
* @var list<string>|string
|
||||||
|
*/
|
||||||
|
public array|string $scriptSrcElem = 'self';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies valid sources for JavaScript inline event
|
||||||
|
* handlers and JavaScript URLs.
|
||||||
|
*
|
||||||
|
* @var list<string>|string
|
||||||
|
*/
|
||||||
|
public array|string $scriptSrcAttr = 'self';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lists allowed stylesheets' URLs.
|
* Lists allowed stylesheets' URLs.
|
||||||
*
|
*
|
||||||
@@ -76,6 +96,21 @@ class ContentSecurityPolicy extends BaseConfig
|
|||||||
'https://fonts.googleapis.com',
|
'https://fonts.googleapis.com',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies valid sources for stylesheets <link> elements.
|
||||||
|
*
|
||||||
|
* @var list<string>|string
|
||||||
|
*/
|
||||||
|
public array|string $styleSrcElem = 'self';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies valid sources for stylesheets inline
|
||||||
|
* style attributes and `<style>` elements.
|
||||||
|
*
|
||||||
|
* @var list<string>|string
|
||||||
|
*/
|
||||||
|
public array|string $styleSrcAttr = 'self';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines the origins from which images can be loaded.
|
* Defines the origins from which images can be loaded.
|
||||||
*
|
*
|
||||||
@@ -169,6 +204,11 @@ class ContentSecurityPolicy extends BaseConfig
|
|||||||
*/
|
*/
|
||||||
public $manifestSrc;
|
public $manifestSrc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var list<string>|string
|
||||||
|
*/
|
||||||
|
public array|string $workerSrc = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Limits the kinds of plugins a page may invoke.
|
* Limits the kinds of plugins a page may invoke.
|
||||||
*
|
*
|
||||||
@@ -184,17 +224,17 @@ class ContentSecurityPolicy extends BaseConfig
|
|||||||
public $sandbox;
|
public $sandbox;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nonce tag for style
|
* Nonce placeholder for style tags.
|
||||||
*/
|
*/
|
||||||
public string $styleNonceTag = '{csp-style-nonce}';
|
public string $styleNonceTag = '{csp-style-nonce}';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nonce tag for script
|
* Nonce placeholder for script tags.
|
||||||
*/
|
*/
|
||||||
public string $scriptNonceTag = '{csp-script-nonce}';
|
public string $scriptNonceTag = '{csp-script-nonce}';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace nonce tag automatically
|
* Replace nonce tag automatically?
|
||||||
*/
|
*/
|
||||||
public bool $autoNonce = true;
|
public bool $autoNonce = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class Cookie extends BaseConfig
|
|||||||
* (empty string) means default SameSite attribute set by browsers (`Lax`)
|
* (empty string) means default SameSite attribute set by browsers (`Lax`)
|
||||||
* will be set on cookies. If set to `None`, `$secure` must also be set.
|
* will be set on cookies. If set to `None`, `$secure` must also be set.
|
||||||
*
|
*
|
||||||
* @phpstan-var 'None'|'Lax'|'Strict'|''
|
* @var ''|'Lax'|'None'|'Strict'
|
||||||
*/
|
*/
|
||||||
public string $samesite = 'Lax';
|
public string $samesite = 'Lax';
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ class Database extends Config
|
|||||||
'strictOn' => false,
|
'strictOn' => false,
|
||||||
'failover' => [],
|
'failover' => [],
|
||||||
'port' => 3306,
|
'port' => 3306,
|
||||||
|
'numberNative' => false,
|
||||||
|
'foundRows' => false,
|
||||||
'dateFormat' => [
|
'dateFormat' => [
|
||||||
'date' => 'Y-m-d',
|
'date' => 'Y-m-d',
|
||||||
'datetime' => 'Y-m-d H:i:s',
|
'datetime' => 'Y-m-d H:i:s',
|
||||||
@@ -55,26 +57,27 @@ class Database extends Config
|
|||||||
* @var array<string, mixed>
|
* @var array<string, mixed>
|
||||||
*/
|
*/
|
||||||
public array $tests = [
|
public array $tests = [
|
||||||
'DSN' => '',
|
'DSN' => '',
|
||||||
'hostname' => 'localhost',
|
'hostname' => 'localhost',
|
||||||
'username' => 'admin',
|
'username' => 'admin',
|
||||||
'password' => 'pointofsale',
|
'password' => 'pointofsale',
|
||||||
'database' => 'ospos',
|
'database' => 'ospos',
|
||||||
'DBDriver' => 'MySQLi',
|
'DBDriver' => 'MySQLi',
|
||||||
'DBPrefix' => 'ospos_',
|
'DBPrefix' => 'ospos_',
|
||||||
'pConnect' => false,
|
'pConnect' => false,
|
||||||
'DBDebug' => (ENVIRONMENT !== 'production'),
|
'DBDebug' => (ENVIRONMENT !== 'production'),
|
||||||
'charset' => 'utf8mb4',
|
'charset' => 'utf8mb4',
|
||||||
'DBCollat' => 'utf8mb4_general_ci',
|
'DBCollat' => 'utf8mb4_general_ci',
|
||||||
'swapPre' => '',
|
'swapPre' => '',
|
||||||
'encrypt' => false,
|
'encrypt' => false,
|
||||||
'compress' => false,
|
'compress' => false,
|
||||||
'strictOn' => false,
|
'strictOn' => false,
|
||||||
'failover' => [],
|
'failover' => [],
|
||||||
'port' => 3306,
|
'port' => 3306,
|
||||||
'foreignKeys' => true,
|
'foreignKeys' => true,
|
||||||
'busyTimeout' => 1000,
|
'busyTimeout' => 1000,
|
||||||
'dateFormat' => [
|
'synchronous' => null,
|
||||||
|
'dateFormat' => [
|
||||||
'date' => 'Y-m-d',
|
'date' => 'Y-m-d',
|
||||||
'datetime' => 'Y-m-d H:i:s',
|
'datetime' => 'Y-m-d H:i:s',
|
||||||
'time' => 'H:i:s',
|
'time' => 'H:i:s',
|
||||||
|
|||||||
@@ -2,9 +2,6 @@
|
|||||||
|
|
||||||
namespace Config;
|
namespace Config;
|
||||||
|
|
||||||
/**
|
|
||||||
* @immutable
|
|
||||||
*/
|
|
||||||
class DocTypes
|
class DocTypes
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ class Email extends BaseConfig
|
|||||||
*/
|
*/
|
||||||
public string $SMTPHost = 'mail.mxserver.com';
|
public string $SMTPHost = 'mail.mxserver.com';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Which SMTP authentication method to use: login, plain
|
||||||
|
*/
|
||||||
|
public string $SMTPAuthMethod = 'login';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SMTP Username
|
* SMTP Username
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -23,6 +23,23 @@ class Encryption extends BaseConfig
|
|||||||
*/
|
*/
|
||||||
public string $key = '';
|
public string $key = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
|
* Previous Encryption Keys
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* When rotating encryption keys, add old keys here to maintain ability
|
||||||
|
* to decrypt data encrypted with previous keys. Encryption always uses
|
||||||
|
* the current $key. Decryption tries current key first, then falls back
|
||||||
|
* to previous keys if decryption fails.
|
||||||
|
*
|
||||||
|
* In .env file, use comma-separated string:
|
||||||
|
* encryption.previousKeys = hex2bin:9be8c64fcea509867...,hex2bin:3f5a1d8e9c2b7a4f6...
|
||||||
|
*
|
||||||
|
* @var list<string>|string
|
||||||
|
*/
|
||||||
|
public array|string $previousKeys = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* --------------------------------------------------------------------------
|
* --------------------------------------------------------------------------
|
||||||
* Encryption Driver to Use
|
* Encryption Driver to Use
|
||||||
|
|||||||
@@ -65,12 +65,15 @@ class Filters extends BaseFilters
|
|||||||
* List of filter aliases that are always
|
* List of filter aliases that are always
|
||||||
* applied before and after every request.
|
* applied before and after every request.
|
||||||
*
|
*
|
||||||
* @var array<string, array<string, array<string, string>>>|array<string, list<string>>
|
* @var array{
|
||||||
|
* before: array<string, array{except: list<string>|string}>|list<string>,
|
||||||
|
* after: array<string, array{except: list<string>|string}>|list<string>
|
||||||
|
* }
|
||||||
*/
|
*/
|
||||||
public array $globals = [
|
public array $globals = [
|
||||||
'before' => [
|
'before' => [
|
||||||
'honeypot',
|
'honeypot',
|
||||||
'csrf' => ['except' => 'login'],
|
'csrf' => ['except' => 'login|migrate'],
|
||||||
'invalidchars',
|
'invalidchars',
|
||||||
],
|
],
|
||||||
'after' => [
|
'after' => [
|
||||||
@@ -100,7 +103,7 @@ class Filters extends BaseFilters
|
|||||||
* before or after URI patterns.
|
* before or after URI patterns.
|
||||||
*
|
*
|
||||||
* Example:
|
* Example:
|
||||||
* isLoggedIn' => ['before' => ['account/*', 'profiles/*']]
|
* 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']]
|
||||||
*
|
*
|
||||||
* @var array<string, array<string, list<string>>>
|
* @var array<string, array<string, list<string>>>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -61,4 +61,13 @@ class Format extends BaseConfig
|
|||||||
'application/xml' => 0,
|
'application/xml' => 0,
|
||||||
'text/xml' => 0,
|
'text/xml' => 0,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
|
* Maximum depth for JSON encoding.
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* This value determines how deep the JSON encoder will traverse nested structures.
|
||||||
|
*/
|
||||||
|
public int $jsonEncodeDepth = 512;
|
||||||
}
|
}
|
||||||
|
|||||||
40
app/Config/Hostnames.php
Normal file
40
app/Config/Hostnames.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Config;
|
||||||
|
|
||||||
|
class Hostnames
|
||||||
|
{
|
||||||
|
// List of known two-part TLDs for subdomain extraction
|
||||||
|
public const TWO_PART_TLDS = [
|
||||||
|
'co.uk', 'org.uk', 'gov.uk', 'ac.uk', 'sch.uk', 'ltd.uk', 'plc.uk',
|
||||||
|
'com.au', 'net.au', 'org.au', 'edu.au', 'gov.au', 'asn.au', 'id.au',
|
||||||
|
'co.jp', 'ac.jp', 'go.jp', 'or.jp', 'ne.jp', 'gr.jp',
|
||||||
|
'co.nz', 'org.nz', 'govt.nz', 'ac.nz', 'net.nz', 'geek.nz', 'maori.nz', 'school.nz',
|
||||||
|
'co.in', 'net.in', 'org.in', 'ind.in', 'ac.in', 'gov.in', 'res.in',
|
||||||
|
'com.cn', 'net.cn', 'org.cn', 'gov.cn', 'edu.cn',
|
||||||
|
'com.sg', 'net.sg', 'org.sg', 'gov.sg', 'edu.sg', 'per.sg',
|
||||||
|
'co.za', 'org.za', 'gov.za', 'ac.za', 'net.za',
|
||||||
|
'co.kr', 'or.kr', 'go.kr', 'ac.kr', 'ne.kr', 'pe.kr',
|
||||||
|
'co.th', 'or.th', 'go.th', 'ac.th', 'net.th', 'in.th',
|
||||||
|
'com.my', 'net.my', 'org.my', 'edu.my', 'gov.my', 'mil.my', 'name.my',
|
||||||
|
'com.mx', 'org.mx', 'net.mx', 'edu.mx', 'gob.mx',
|
||||||
|
'com.br', 'net.br', 'org.br', 'gov.br', 'edu.br', 'art.br', 'eng.br',
|
||||||
|
'co.il', 'org.il', 'ac.il', 'gov.il', 'net.il', 'muni.il',
|
||||||
|
'co.id', 'or.id', 'ac.id', 'go.id', 'net.id', 'web.id', 'my.id',
|
||||||
|
'com.hk', 'edu.hk', 'gov.hk', 'idv.hk', 'net.hk', 'org.hk',
|
||||||
|
'com.tw', 'net.tw', 'org.tw', 'edu.tw', 'gov.tw', 'idv.tw',
|
||||||
|
'com.sa', 'net.sa', 'org.sa', 'gov.sa', 'edu.sa', 'sch.sa', 'med.sa',
|
||||||
|
'co.ae', 'net.ae', 'org.ae', 'gov.ae', 'ac.ae', 'sch.ae',
|
||||||
|
'com.tr', 'net.tr', 'org.tr', 'gov.tr', 'edu.tr', 'av.tr', 'gen.tr',
|
||||||
|
'co.ke', 'or.ke', 'go.ke', 'ac.ke', 'sc.ke', 'me.ke', 'mobi.ke', 'info.ke',
|
||||||
|
'com.ng', 'org.ng', 'gov.ng', 'edu.ng', 'net.ng', 'sch.ng', 'name.ng',
|
||||||
|
'com.pk', 'net.pk', 'org.pk', 'gov.pk', 'edu.pk', 'fam.pk',
|
||||||
|
'com.eg', 'edu.eg', 'gov.eg', 'org.eg', 'net.eg',
|
||||||
|
'com.cy', 'net.cy', 'org.cy', 'gov.cy', 'ac.cy',
|
||||||
|
'com.lk', 'org.lk', 'edu.lk', 'gov.lk', 'net.lk', 'int.lk',
|
||||||
|
'com.bd', 'net.bd', 'org.bd', 'ac.bd', 'gov.bd', 'mil.bd',
|
||||||
|
'com.ar', 'net.ar', 'org.ar', 'gov.ar', 'edu.ar', 'mil.ar',
|
||||||
|
'gob.cl', 'com.pl', 'net.pl', 'org.pl', 'gov.pl', 'edu.pl',
|
||||||
|
'co.ir', 'ac.ir', 'org.ir', 'id.ir', 'gov.ir', 'sch.ir', 'net.ir',
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -16,6 +16,8 @@ class Images extends BaseConfig
|
|||||||
/**
|
/**
|
||||||
* The path to the image library.
|
* The path to the image library.
|
||||||
* Required for ImageMagick, GraphicsMagick, or NetPBM.
|
* Required for ImageMagick, GraphicsMagick, or NetPBM.
|
||||||
|
*
|
||||||
|
* @deprecated 4.7.0 No longer used.
|
||||||
*/
|
*/
|
||||||
public string $libraryPath = '/usr/local/bin/convert';
|
public string $libraryPath = '/usr/local/bin/convert';
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace Config;
|
|||||||
|
|
||||||
use CodeIgniter\Config\BaseConfig;
|
use CodeIgniter\Config\BaseConfig;
|
||||||
use CodeIgniter\Log\Handlers\FileHandler;
|
use CodeIgniter\Log\Handlers\FileHandler;
|
||||||
|
use CodeIgniter\Log\Handlers\HandlerInterface;
|
||||||
|
|
||||||
class Logger extends BaseConfig
|
class Logger extends BaseConfig
|
||||||
{
|
{
|
||||||
@@ -73,7 +74,7 @@ class Logger extends BaseConfig
|
|||||||
* Handlers are executed in the order defined in this array, starting with
|
* Handlers are executed in the order defined in this array, starting with
|
||||||
* the handler on top and continuing down.
|
* the handler on top and continuing down.
|
||||||
*
|
*
|
||||||
* @var array<class-string, array<string, int|list<string>|string>>
|
* @var array<class-string<HandlerInterface>, array<string, int|list<string>|string>>
|
||||||
*/
|
*/
|
||||||
public array $handlers = [
|
public array $handlers = [
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -47,4 +47,19 @@ class Migrations extends BaseConfig
|
|||||||
* - Y_m_d_His_
|
* - Y_m_d_His_
|
||||||
*/
|
*/
|
||||||
public string $timestampFormat = 'YmdHis_';
|
public string $timestampFormat = 'YmdHis_';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
|
* Enable/Disable Migration Lock
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* Locking is disabled by default.
|
||||||
|
*
|
||||||
|
* When enabled, it will prevent multiple migration processes
|
||||||
|
* from running at the same time by using a lock mechanism.
|
||||||
|
*
|
||||||
|
* This is useful in production environments to avoid conflicts
|
||||||
|
* or race conditions during concurrent deployments.
|
||||||
|
*/
|
||||||
|
public bool $lock = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
namespace Config;
|
namespace Config;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mimes
|
|
||||||
*
|
|
||||||
* This file contains an array of mime types. It is used by the
|
* This file contains an array of mime types. It is used by the
|
||||||
* Upload class to help identify allowed file types.
|
* Upload class to help identify allowed file types.
|
||||||
*
|
*
|
||||||
@@ -15,8 +13,6 @@ namespace Config;
|
|||||||
*
|
*
|
||||||
* When working with mime types, please make sure you have the ´fileinfo´
|
* When working with mime types, please make sure you have the ´fileinfo´
|
||||||
* extension enabled to reliably detect the media types.
|
* extension enabled to reliably detect the media types.
|
||||||
*
|
|
||||||
* @immutable
|
|
||||||
*/
|
*/
|
||||||
class Mimes
|
class Mimes
|
||||||
{
|
{
|
||||||
@@ -482,13 +478,16 @@ class Mimes
|
|||||||
'application/sla',
|
'application/sla',
|
||||||
'application/vnd.ms-pki.stl',
|
'application/vnd.ms-pki.stl',
|
||||||
'application/x-navistyle',
|
'application/x-navistyle',
|
||||||
|
'model/stl',
|
||||||
|
'application/octet-stream',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to determine the best mime type for the given file extension.
|
* Attempts to determine the best mime type for the given file extension.
|
||||||
*
|
*
|
||||||
* @return string|null The mime type found, or none if unable to determine.
|
* @param string $extension
|
||||||
|
* @return array|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): array|string|null
|
||||||
{
|
{
|
||||||
@@ -524,7 +523,7 @@ class Mimes
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reverse check the mime type list if no extension was proposed.
|
// 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) {
|
foreach (static::$mimes as $ext => $types) {
|
||||||
if (in_array($type, (array) $types, true)) {
|
if (in_array($type, (array) $types, true)) {
|
||||||
return $ext;
|
return $ext;
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ use CodeIgniter\Modules\Modules as BaseModules;
|
|||||||
*
|
*
|
||||||
* NOTE: This class is required prior to Autoloader instantiation,
|
* NOTE: This class is required prior to Autoloader instantiation,
|
||||||
* and does not extend BaseConfig.
|
* and does not extend BaseConfig.
|
||||||
*
|
|
||||||
* @immutable
|
|
||||||
*/
|
*/
|
||||||
class Modules extends BaseModules
|
class Modules extends BaseModules
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Config;
|
|||||||
use App\Models\Appconfig;
|
use App\Models\Appconfig;
|
||||||
use CodeIgniter\Cache\CacheInterface;
|
use CodeIgniter\Cache\CacheInterface;
|
||||||
use CodeIgniter\Config\BaseConfig;
|
use CodeIgniter\Config\BaseConfig;
|
||||||
|
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class holds the configuration options stored from the database so that on launch those settings can be cached
|
* This class holds the configuration options stored from the database so that on launch those settings can be cached
|
||||||
@@ -34,11 +35,21 @@ class OSPOS extends BaseConfig
|
|||||||
if ($cache) {
|
if ($cache) {
|
||||||
$this->settings = decode_array($cache);
|
$this->settings = decode_array($cache);
|
||||||
} else {
|
} else {
|
||||||
$appconfig = model(Appconfig::class);
|
try {
|
||||||
foreach ($appconfig->get_all()->getResult() as $app_config) {
|
$appconfig = model(Appconfig::class);
|
||||||
$this->settings[$app_config->key] = $app_config->value;
|
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'
|
||||||
|
];
|
||||||
}
|
}
|
||||||
$this->cache->save('settings', encode_array($this->settings));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,4 +61,4 @@ class OSPOS extends BaseConfig
|
|||||||
$this->cache->delete('settings');
|
$this->cache->delete('settings');
|
||||||
$this->set_settings();
|
$this->set_settings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ namespace Config;
|
|||||||
* NOTE: This class does not extend BaseConfig for performance reasons.
|
* NOTE: This class does not extend BaseConfig for performance reasons.
|
||||||
* So you cannot replace the property values with Environment Variables.
|
* So you cannot replace the property values with Environment Variables.
|
||||||
*
|
*
|
||||||
* @immutable
|
* WARNING: Do not use these options when running the app in the Worker Mode.
|
||||||
*/
|
*/
|
||||||
class Optimize
|
class Optimize
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ namespace Config;
|
|||||||
*
|
*
|
||||||
* NOTE: This class is required prior to Autoloader instantiation,
|
* NOTE: This class is required prior to Autoloader instantiation,
|
||||||
* and does not extend BaseConfig.
|
* and does not extend BaseConfig.
|
||||||
*
|
|
||||||
* @immutable
|
|
||||||
*/
|
*/
|
||||||
class Paths
|
class Paths
|
||||||
{
|
{
|
||||||
@@ -77,4 +75,16 @@ class Paths
|
|||||||
* is used when no value is provided to `Services::renderer()`.
|
* is used when no value is provided to `Services::renderer()`.
|
||||||
*/
|
*/
|
||||||
public string $viewDirectory = __DIR__ . '/../Views';
|
public string $viewDirectory = __DIR__ . '/../Views';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
* ENVIRONMENT DIRECTORY NAME
|
||||||
|
* ---------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* This variable must contain the name of the directory where
|
||||||
|
* the .env file is located.
|
||||||
|
* Please consider security implications when changing this
|
||||||
|
* value - the directory should not be publicly accessible.
|
||||||
|
*/
|
||||||
|
public string $envDirectory = __DIR__ . '/../../';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ $routes->setDefaultController('Login');
|
|||||||
$routes->get('/', 'Login::index');
|
$routes->get('/', 'Login::index');
|
||||||
$routes->get('login', 'Login::index');
|
$routes->get('login', 'Login::index');
|
||||||
$routes->post('login', 'Login::index');
|
$routes->post('login', 'Login::index');
|
||||||
|
$routes->post('migrate', 'Login::migrate');
|
||||||
|
|
||||||
$routes->add('no_access/index/(:segment)', 'No_access::index/$1');
|
$routes->add('no_access/index/(:segment)', 'No_access::index/$1');
|
||||||
$routes->add('no_access/index/(:segment)/(:segment)', 'No_access::index/$1/$2');
|
$routes->add('no_access/index/(:segment)/(:segment)', 'No_access::index/$1/$2');
|
||||||
|
|||||||
@@ -96,6 +96,15 @@ class Routing extends BaseRouting
|
|||||||
*/
|
*/
|
||||||
public bool $autoRoute = true;
|
public bool $autoRoute = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If TRUE, the system will look for attributes on controller
|
||||||
|
* class and methods that can run before and after the
|
||||||
|
* controller/method.
|
||||||
|
*
|
||||||
|
* If FALSE, will ignore any attributes.
|
||||||
|
*/
|
||||||
|
public bool $useControllerAttributes = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For Defined Routes.
|
* For Defined Routes.
|
||||||
* If TRUE, will enable the use of the 'prioritize' option
|
* If TRUE, will enable the use of the 'prioritize' option
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ class Security extends BaseConfig
|
|||||||
*
|
*
|
||||||
* Protection Method for Cross Site Request Forgery protection.
|
* Protection Method for Cross Site Request Forgery protection.
|
||||||
*
|
*
|
||||||
* @var string|false 'cookie', 'session', or false
|
* @var string 'cookie' or 'session'
|
||||||
*/
|
*/
|
||||||
public string|false $csrfProtection = 'session';
|
public string $csrfProtection = 'session';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* --------------------------------------------------------------------------
|
* --------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace Config;
|
namespace Config;
|
||||||
|
|
||||||
|
use App\Libraries\MY_Language;
|
||||||
use Locale;
|
use Locale;
|
||||||
use HTMLPurifier;
|
use HTMLPurifier;
|
||||||
use HTMLPurifier_Config;
|
use HTMLPurifier_Config;
|
||||||
@@ -38,9 +39,11 @@ class Services extends BaseService
|
|||||||
/**
|
/**
|
||||||
* Responsible for loading the language string translations.
|
* Responsible for loading the language string translations.
|
||||||
*
|
*
|
||||||
|
* @param string|null $locale
|
||||||
|
* @param bool $getShared
|
||||||
* @return MY_Language
|
* @return MY_Language
|
||||||
*/
|
*/
|
||||||
public static function language(?string $locale = null, bool $getShared = true)
|
public static function language(?string $locale = null, bool $getShared = true): MY_Language
|
||||||
{
|
{
|
||||||
if ($getShared) {
|
if ($getShared) {
|
||||||
return static::getSharedInstance('language', $locale)->setLocale($locale);
|
return static::getSharedInstance('language', $locale)->setLocale($locale);
|
||||||
@@ -55,12 +58,12 @@ class Services extends BaseService
|
|||||||
// Use '?:' for empty string check
|
// Use '?:' for empty string check
|
||||||
$locale = $locale ?: $requestLocale;
|
$locale = $locale ?: $requestLocale;
|
||||||
|
|
||||||
return new \App\Libraries\MY_Language($locale);
|
return new MY_Language($locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static $htmlPurifier;
|
private static HTMLPurifier $htmlPurifier;
|
||||||
|
|
||||||
public static function htmlPurifier($getShared = true)
|
public static function htmlPurifier($getShared = true): object
|
||||||
{
|
{
|
||||||
if ($getShared) {
|
if ($getShared) {
|
||||||
return static::getSharedInstance('htmlPurifier');
|
return static::getSharedInstance('htmlPurifier');
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
namespace Config;
|
namespace Config;
|
||||||
|
|
||||||
use CodeIgniter\Config\BaseConfig;
|
use CodeIgniter\Config\BaseConfig;
|
||||||
|
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||||
use CodeIgniter\Session\Handlers\BaseHandler;
|
use CodeIgniter\Session\Handlers\BaseHandler;
|
||||||
use CodeIgniter\Session\Handlers\DatabaseHandler;
|
use CodeIgniter\Session\Handlers\DatabaseHandler;
|
||||||
|
use CodeIgniter\Session\Handlers\FileHandler;
|
||||||
|
|
||||||
class Session extends BaseConfig
|
class Session extends BaseConfig
|
||||||
{
|
{
|
||||||
@@ -124,4 +126,23 @@ class Session extends BaseConfig
|
|||||||
* seconds.
|
* seconds.
|
||||||
*/
|
*/
|
||||||
public int $lockMaxRetries = 300;
|
public int $lockMaxRetries = 300;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
if ($this->driver === DatabaseHandler::class) {
|
||||||
|
try {
|
||||||
|
$db = Database::connect();
|
||||||
|
|
||||||
|
if (!$db->tableExists($this->savePath)) {
|
||||||
|
$this->driver = FileHandler::class;
|
||||||
|
$this->savePath = WRITEPATH . 'session';
|
||||||
|
}
|
||||||
|
} catch (DatabaseException $e) {
|
||||||
|
$this->driver = FileHandler::class;
|
||||||
|
$this->savePath = WRITEPATH . 'session';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,4 +119,29 @@ class Toolbar extends BaseConfig
|
|||||||
public array $watchedExtensions = [
|
public array $watchedExtensions = [
|
||||||
'php', 'css', 'js', 'html', 'svg', 'json', 'env',
|
'php', 'css', 'js', 'html', 'svg', 'json', 'env',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
|
* Ignored HTTP Headers
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* CodeIgniter Debug Toolbar normally injects HTML and JavaScript into every
|
||||||
|
* HTML response. This is correct for full page loads, but it breaks requests
|
||||||
|
* that expect only a clean HTML fragment.
|
||||||
|
*
|
||||||
|
* Libraries like HTMX, Unpoly, and Hotwire (Turbo) update parts of the page or
|
||||||
|
* manage navigation on the client side. Injecting the Debug Toolbar into their
|
||||||
|
* responses can cause invalid HTML, duplicated scripts, or JavaScript errors
|
||||||
|
* (such as infinite loops or "Maximum call stack size exceeded").
|
||||||
|
*
|
||||||
|
* Any request containing one of the following headers is treated as a
|
||||||
|
* client-managed or partial request, and the Debug Toolbar injection is skipped.
|
||||||
|
*
|
||||||
|
* @var array<string, string|null>
|
||||||
|
*/
|
||||||
|
public array $disableOnHeaders = [
|
||||||
|
'X-Requested-With' => 'xmlhttprequest', // AJAX requests
|
||||||
|
'HX-Request' => 'true', // HTMX requests
|
||||||
|
'X-Up-Version' => null, // Unpoly partial requests
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,9 +230,13 @@ class UserAgents extends BaseConfig
|
|||||||
*/
|
*/
|
||||||
public array $robots = [
|
public array $robots = [
|
||||||
'googlebot' => 'Googlebot',
|
'googlebot' => 'Googlebot',
|
||||||
|
'google-pagerenderer' => 'Google Page Renderer',
|
||||||
|
'google-read-aloud' => 'Google Read Aloud',
|
||||||
|
'google-safety' => 'Google Safety Bot',
|
||||||
'msnbot' => 'MSNBot',
|
'msnbot' => 'MSNBot',
|
||||||
'baiduspider' => 'Baiduspider',
|
'baiduspider' => 'Baiduspider',
|
||||||
'bingbot' => 'Bing',
|
'bingbot' => 'Bing',
|
||||||
|
'bingpreview' => 'BingPreview',
|
||||||
'slurp' => 'Inktomi Slurp',
|
'slurp' => 'Inktomi Slurp',
|
||||||
'yahoo' => 'Yahoo',
|
'yahoo' => 'Yahoo',
|
||||||
'ask jeeves' => 'Ask Jeeves',
|
'ask jeeves' => 'Ask Jeeves',
|
||||||
@@ -248,5 +252,11 @@ class UserAgents extends BaseConfig
|
|||||||
'ia_archiver' => 'Alexa Crawler',
|
'ia_archiver' => 'Alexa Crawler',
|
||||||
'MJ12bot' => 'Majestic-12',
|
'MJ12bot' => 'Majestic-12',
|
||||||
'Uptimebot' => 'Uptimebot',
|
'Uptimebot' => 'Uptimebot',
|
||||||
|
'duckduckbot' => 'DuckDuckBot',
|
||||||
|
'sogou' => 'Sogou Spider',
|
||||||
|
'exabot' => 'Exabot',
|
||||||
|
'bot' => 'Generic Bot',
|
||||||
|
'crawler' => 'Generic Crawler',
|
||||||
|
'spider' => 'Generic Spider',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,4 +135,19 @@ class OSPOSRules
|
|||||||
{
|
{
|
||||||
return parse_decimals($candidate) !== false;
|
return parse_decimals($candidate) !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that a locale-aware decimal value is non-negative (>= 0).
|
||||||
|
*
|
||||||
|
* @param string $candidate
|
||||||
|
* @param string|null $error
|
||||||
|
* @return bool
|
||||||
|
* @noinspection PhpUnused
|
||||||
|
*/
|
||||||
|
public function nonNegativeDecimal(string $candidate, ?string &$error = null): bool
|
||||||
|
{
|
||||||
|
$value = parse_decimals($candidate);
|
||||||
|
|
||||||
|
return $value !== false && $value >= 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,4 +59,21 @@ class View extends BaseView
|
|||||||
* @var list<class-string<ViewDecoratorInterface>>
|
* @var list<class-string<ViewDecoratorInterface>>
|
||||||
*/
|
*/
|
||||||
public array $decorators = [];
|
public array $decorators = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subdirectory within app/Views for namespaced view overrides.
|
||||||
|
*
|
||||||
|
* Namespaced views will be searched in:
|
||||||
|
*
|
||||||
|
* app/Views/{$appOverridesFolder}/{Namespace}/{view_path}.{php|html...}
|
||||||
|
*
|
||||||
|
* This allows application-level overrides for package or module views
|
||||||
|
* without modifying vendor source files.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* 'overrides' -> app/Views/overrides/Example/Blog/post/card.php
|
||||||
|
* 'vendor' -> app/Views/vendor/Example/Blog/post/card.php
|
||||||
|
* '' -> app/Views/Example/Blog/post/card.php (direct mapping)
|
||||||
|
*/
|
||||||
|
public string $appOverridesFolder = 'overrides';
|
||||||
}
|
}
|
||||||
|
|||||||
62
app/Config/WorkerMode.php
Normal file
62
app/Config/WorkerMode.php
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This configuration controls how CodeIgniter behaves when running
|
||||||
|
* in worker mode (with FrankenPHP).
|
||||||
|
*/
|
||||||
|
class WorkerMode
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Persistent Services
|
||||||
|
*
|
||||||
|
* List of service names that should persist across requests.
|
||||||
|
* These services will NOT be reset between requests.
|
||||||
|
*
|
||||||
|
* Services not in this list will be reset for each request to prevent
|
||||||
|
* state leakage.
|
||||||
|
*
|
||||||
|
* Recommended persistent services:
|
||||||
|
* - `autoloader`: PSR-4 autoloading configuration
|
||||||
|
* - `locator`: File locator
|
||||||
|
* - `exceptions`: Exception handler
|
||||||
|
* - `commands`: CLI commands registry
|
||||||
|
* - `codeigniter`: Main application instance
|
||||||
|
* - `superglobals`: Superglobals wrapper
|
||||||
|
* - `routes`: Router configuration
|
||||||
|
* - `cache`: Cache instance
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
public array $persistentServices = [
|
||||||
|
'autoloader',
|
||||||
|
'locator',
|
||||||
|
'exceptions',
|
||||||
|
'commands',
|
||||||
|
'codeigniter',
|
||||||
|
'superglobals',
|
||||||
|
'routes',
|
||||||
|
'cache',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset Event Listeners
|
||||||
|
*
|
||||||
|
* List of event names whose listeners should be removed between requests.
|
||||||
|
* Use this if you register event listeners inside other event callbacks
|
||||||
|
* (rather than at the top level of Config/Events.php), which would cause
|
||||||
|
* them to accumulate across requests in worker mode.
|
||||||
|
*
|
||||||
|
* @var list<string>
|
||||||
|
*/
|
||||||
|
public array $resetEventListeners = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force Garbage Collection
|
||||||
|
*
|
||||||
|
* Whether to force garbage collection after each request.
|
||||||
|
* Helps prevent memory leaks at a small performance cost.
|
||||||
|
*/
|
||||||
|
public bool $forceGarbageCollection = true;
|
||||||
|
}
|
||||||
@@ -106,12 +106,24 @@ class Attributes extends Secure_Controller
|
|||||||
$definition_flags |= $flag;
|
$definition_flags |= $flag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate definition_group (definition_fk) foreign key
|
||||||
|
$definition_group_input = $this->request->getPost('definition_group');
|
||||||
|
$definition_fk = $this->validateDefinitionGroup($definition_group_input);
|
||||||
|
|
||||||
|
if ($definition_fk === false) {
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'success' => false,
|
||||||
|
'message' => lang('Attributes.definition_invalid_group'),
|
||||||
|
'id' => NEW_ENTRY
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
// Save definition data
|
// Save definition data
|
||||||
$definition_data = [
|
$definition_data = [
|
||||||
'definition_name' => $this->request->getPost('definition_name'),
|
'definition_name' => $this->request->getPost('definition_name'),
|
||||||
'definition_unit' => $this->request->getPost('definition_unit') != '' ? $this->request->getPost('definition_unit') : null,
|
'definition_unit' => $this->request->getPost('definition_unit') != '' ? $this->request->getPost('definition_unit') : null,
|
||||||
'definition_flags' => $definition_flags,
|
'definition_flags' => $definition_flags,
|
||||||
'definition_fk' => $this->request->getPost('definition_group') != '' ? $this->request->getPost('definition_group') : null
|
'definition_fk' => $definition_fk
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($this->request->getPost('definition_type') != null) {
|
if ($this->request->getPost('definition_type') != null) {
|
||||||
@@ -120,7 +132,7 @@ class Attributes extends Secure_Controller
|
|||||||
|
|
||||||
$definition_name = $definition_data['definition_name'];
|
$definition_name = $definition_data['definition_name'];
|
||||||
|
|
||||||
if ($this->attribute->save_definition($definition_data, $definition_id)) {
|
if ($this->attribute->saveDefinition($definition_data, $definition_id)) {
|
||||||
// New definition
|
// New definition
|
||||||
if ($definition_id == NO_DEFINITION_ID) {
|
if ($definition_id == NO_DEFINITION_ID) {
|
||||||
$definition_values = json_decode(html_entity_decode($this->request->getPost('definition_values')));
|
$definition_values = json_decode(html_entity_decode($this->request->getPost('definition_values')));
|
||||||
@@ -150,6 +162,32 @@ class Attributes extends Secure_Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a definition_group foreign key.
|
||||||
|
* Returns the validated integer ID, null if empty, or false if invalid.
|
||||||
|
*
|
||||||
|
* @param mixed $definition_group_input
|
||||||
|
* @return int|null|false
|
||||||
|
*/
|
||||||
|
private function validateDefinitionGroup(mixed $definition_group_input): int|null|false
|
||||||
|
{
|
||||||
|
if ($definition_group_input === '' || $definition_group_input === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$definition_group_id = (int) $definition_group_input;
|
||||||
|
|
||||||
|
// Must be a positive integer, exist in attribute_definitions, and be of type GROUP
|
||||||
|
if ($definition_group_id <= 0
|
||||||
|
|| !$this->attribute->exists($definition_group_id)
|
||||||
|
|| $this->attribute->getAttributeInfo($definition_group_id)->definition_type !== GROUP
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $definition_group_id;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param int $definition_id
|
* @param int $definition_id
|
||||||
|
|||||||
@@ -3,56 +3,46 @@
|
|||||||
namespace App\Controllers;
|
namespace App\Controllers;
|
||||||
|
|
||||||
use CodeIgniter\Controller;
|
use CodeIgniter\Controller;
|
||||||
use CodeIgniter\HTTP\CLIRequest;
|
|
||||||
use CodeIgniter\HTTP\IncomingRequest;
|
|
||||||
use CodeIgniter\HTTP\RequestInterface;
|
use CodeIgniter\HTTP\RequestInterface;
|
||||||
use CodeIgniter\HTTP\ResponseInterface;
|
use CodeIgniter\HTTP\ResponseInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class BaseController
|
|
||||||
*
|
|
||||||
* BaseController provides a convenient place for loading components
|
* BaseController provides a convenient place for loading components
|
||||||
* and performing functions that are needed by all your controllers.
|
* and performing functions that are needed by all your controllers.
|
||||||
* Extend this class in any new controllers:
|
|
||||||
* class Home extends BaseController
|
|
||||||
*
|
*
|
||||||
* For security be sure to declare any new methods as protected or private.
|
* Extend this class in any new controllers:
|
||||||
|
* ```
|
||||||
|
* class Home extends BaseController
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* For security, be sure to declare any new methods as protected or private.
|
||||||
*/
|
*/
|
||||||
abstract class BaseController extends Controller
|
abstract class BaseController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Instance of the main Request object.
|
|
||||||
*
|
|
||||||
* @var CLIRequest|IncomingRequest
|
|
||||||
*/
|
|
||||||
protected $request;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An array of helpers to be loaded automatically upon
|
|
||||||
* class instantiation. These helpers will be available
|
|
||||||
* to all other controllers that extend BaseController.
|
|
||||||
*
|
|
||||||
* @var list<string>
|
|
||||||
*/
|
|
||||||
protected $helpers = [];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Be sure to declare properties for any property fetch you initialized.
|
* Be sure to declare properties for any property fetch you initialized.
|
||||||
* The creation of dynamic property is deprecated in PHP 8.2.
|
* The creation of dynamic property is deprecated in PHP 8.2.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// protected $session;
|
// protected $session;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param RequestInterface $request
|
||||||
|
* @param ResponseInterface $response
|
||||||
|
* @param LoggerInterface $logger
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
|
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger): void
|
||||||
{
|
{
|
||||||
// Do Not Edit This Line
|
// 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.
|
||||||
|
// $this->helpers = ['form', 'url'];
|
||||||
|
|
||||||
|
// Caution: Do not edit this line.
|
||||||
parent::initController($request, $response, $logger);
|
parent::initController($request, $response, $logger);
|
||||||
|
|
||||||
// Preload any models, libraries, etc, here.
|
// Preload any models, libraries, etc, here.
|
||||||
|
// $this->session = service('session');
|
||||||
// E.g.: $this->session = service('session');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ class Cashups extends Secure_Controller
|
|||||||
// filters that will be loaded in the multiselect dropdown
|
// filters that will be loaded in the multiselect dropdown
|
||||||
$data['filters'] = ['is_deleted' => lang('Cashups.is_deleted')];
|
$data['filters'] = ['is_deleted' => lang('Cashups.is_deleted')];
|
||||||
|
|
||||||
|
// Restore filters from URL
|
||||||
|
$data = array_merge($data, restoreTableFilters($this->request));
|
||||||
|
|
||||||
return view('cashups/manage', $data);
|
return view('cashups/manage', $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use App\Models\Appconfig;
|
|||||||
use App\Models\Attribute;
|
use App\Models\Attribute;
|
||||||
use App\Models\Customer_rewards;
|
use App\Models\Customer_rewards;
|
||||||
use App\Models\Dinner_table;
|
use App\Models\Dinner_table;
|
||||||
|
use App\Models\Item;
|
||||||
use App\Models\Module;
|
use App\Models\Module;
|
||||||
use App\Models\Enums\Rounding_mode;
|
use App\Models\Enums\Rounding_mode;
|
||||||
use App\Models\Stock_location;
|
use App\Models\Stock_location;
|
||||||
@@ -366,7 +367,7 @@ class Config extends Secure_Controller
|
|||||||
*/
|
*/
|
||||||
public function postSaveGeneral(): ResponseInterface
|
public function postSaveGeneral(): ResponseInterface
|
||||||
{
|
{
|
||||||
$batch_save_data = [
|
$batchSaveData = [
|
||||||
'theme' => $this->request->getPost('theme'),
|
'theme' => $this->request->getPost('theme'),
|
||||||
'login_form' => $this->request->getPost('login_form'),
|
'login_form' => $this->request->getPost('login_form'),
|
||||||
'default_sales_discount_type' => $this->request->getPost('default_sales_discount_type') != null,
|
'default_sales_discount_type' => $this->request->getPost('default_sales_discount_type') != null,
|
||||||
@@ -385,9 +386,9 @@ class Config extends Secure_Controller
|
|||||||
'gcaptcha_enable' => $this->request->getPost('gcaptcha_enable') != null,
|
'gcaptcha_enable' => $this->request->getPost('gcaptcha_enable') != null,
|
||||||
'gcaptcha_secret_key' => $this->request->getPost('gcaptcha_secret_key'),
|
'gcaptcha_secret_key' => $this->request->getPost('gcaptcha_secret_key'),
|
||||||
'gcaptcha_site_key' => $this->request->getPost('gcaptcha_site_key'),
|
'gcaptcha_site_key' => $this->request->getPost('gcaptcha_site_key'),
|
||||||
'suggestions_first_column' => $this->request->getPost('suggestions_first_column'),
|
'suggestions_first_column' => $this->validateSuggestionsColumn($this->request->getPost('suggestions_first_column'), 'first'),
|
||||||
'suggestions_second_column' => $this->request->getPost('suggestions_second_column'),
|
'suggestions_second_column' => $this->validateSuggestionsColumn($this->request->getPost('suggestions_second_column'), 'other'),
|
||||||
'suggestions_third_column' => $this->request->getPost('suggestions_third_column'),
|
'suggestions_third_column' => $this->validateSuggestionsColumn($this->request->getPost('suggestions_third_column'), 'other'),
|
||||||
'giftcard_number' => $this->request->getPost('giftcard_number'),
|
'giftcard_number' => $this->request->getPost('giftcard_number'),
|
||||||
'derive_sale_quantity' => $this->request->getPost('derive_sale_quantity') != null,
|
'derive_sale_quantity' => $this->request->getPost('derive_sale_quantity') != null,
|
||||||
'multi_pack_enabled' => $this->request->getPost('multi_pack_enabled') != null,
|
'multi_pack_enabled' => $this->request->getPost('multi_pack_enabled') != null,
|
||||||
@@ -397,19 +398,19 @@ class Config extends Secure_Controller
|
|||||||
|
|
||||||
$this->module->set_show_office_group($this->request->getPost('show_office_group') != null);
|
$this->module->set_show_office_group($this->request->getPost('show_office_group') != null);
|
||||||
|
|
||||||
if ($batch_save_data['category_dropdown'] == 1) {
|
if ($batchSaveData['category_dropdown']) {
|
||||||
$definition_data['definition_name'] = 'ospos_category';
|
$definitionData['definition_name'] = 'ospos_category';
|
||||||
$definition_data['definition_flags'] = 0;
|
$definitionData['definition_flags'] = 0;
|
||||||
$definition_data['definition_type'] = 'DROPDOWN';
|
$definitionData['definition_type'] = 'DROPDOWN';
|
||||||
$definition_data['definition_id'] = CATEGORY_DEFINITION_ID;
|
$definitionData['definition_id'] = CATEGORY_DEFINITION_ID;
|
||||||
$definition_data['deleted'] = 0;
|
$definitionData['deleted'] = 0;
|
||||||
|
|
||||||
$this->attribute->save_definition($definition_data, CATEGORY_DEFINITION_ID);
|
$this->attribute->saveDefinition($definitionData, CATEGORY_DEFINITION_ID);
|
||||||
} elseif ($batch_save_data['category_dropdown'] == NO_DEFINITION_ID) {
|
} elseif ($batchSaveData['category_dropdown'] == NO_DEFINITION_ID) {
|
||||||
$this->attribute->deleteDefinition(CATEGORY_DEFINITION_ID);
|
$this->attribute->deleteDefinition(CATEGORY_DEFINITION_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
$success = $this->appconfig->batch_save($batch_save_data);
|
$success = $this->appconfig->batch_save($batchSaveData);
|
||||||
|
|
||||||
return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]);
|
return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]);
|
||||||
}
|
}
|
||||||
@@ -461,8 +462,9 @@ class Config extends Secure_Controller
|
|||||||
public function postSaveLocale(): ResponseInterface
|
public function postSaveLocale(): ResponseInterface
|
||||||
{
|
{
|
||||||
$exploded = explode(":", $this->request->getPost('language'));
|
$exploded = explode(":", $this->request->getPost('language'));
|
||||||
|
$currency_symbol = $this->request->getPost('currency_symbol');
|
||||||
$batch_save_data = [
|
$batch_save_data = [
|
||||||
'currency_symbol' => $this->request->getPost('currency_symbol'),
|
'currency_symbol' => htmlspecialchars($currency_symbol ?? ''),
|
||||||
'currency_code' => $this->request->getPost('currency_code'),
|
'currency_code' => $this->request->getPost('currency_code'),
|
||||||
'language_code' => $exploded[0],
|
'language_code' => $exploded[0],
|
||||||
'language' => $exploded[1],
|
'language' => $exploded[1],
|
||||||
@@ -502,9 +504,24 @@ class Config extends Secure_Controller
|
|||||||
$password = $this->encrypter->encrypt($this->request->getPost('smtp_pass'));
|
$password = $this->encrypter->encrypt($this->request->getPost('smtp_pass'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$protocol = $this->request->getPost('protocol');
|
||||||
|
$mailpath = $this->request->getPost('mailpath');
|
||||||
|
|
||||||
|
// Validate mailpath: required for sendmail, optional for others but must be safe if provided
|
||||||
|
$isMailpathRequired = ($protocol === 'sendmail');
|
||||||
|
$isMailpathProvided = !empty($mailpath);
|
||||||
|
$isMailpathValid = $isMailpathProvided && preg_match('/^[a-zA-Z0-9_\-\/.]+$/', $mailpath);
|
||||||
|
|
||||||
|
if (($isMailpathRequired && !$isMailpathProvided) || ($isMailpathProvided && !$isMailpathValid)) {
|
||||||
|
return $this->response->setJSON([
|
||||||
|
'success' => false,
|
||||||
|
'message' => lang('Config.mailpath_invalid')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$batch_save_data = [
|
$batch_save_data = [
|
||||||
'protocol' => $this->request->getPost('protocol'),
|
'protocol' => $protocol,
|
||||||
'mailpath' => $this->request->getPost('mailpath'),
|
'mailpath' => $mailpath,
|
||||||
'smtp_host' => $this->request->getPost('smtp_host'),
|
'smtp_host' => $this->request->getPost('smtp_host'),
|
||||||
'smtp_user' => $this->request->getPost('smtp_user'),
|
'smtp_user' => $this->request->getPost('smtp_user'),
|
||||||
'smtp_pass' => $password,
|
'smtp_pass' => $password,
|
||||||
@@ -942,7 +959,9 @@ class Config extends Secure_Controller
|
|||||||
'work_order_enable' => $this->request->getPost('work_order_enable') != null,
|
'work_order_enable' => $this->request->getPost('work_order_enable') != null,
|
||||||
'work_order_format' => $this->request->getPost('work_order_format'),
|
'work_order_format' => $this->request->getPost('work_order_format'),
|
||||||
'last_used_work_order_number' => $this->request->getPost('last_used_work_order_number', FILTER_SANITIZE_NUMBER_INT),
|
'last_used_work_order_number' => $this->request->getPost('last_used_work_order_number', FILTER_SANITIZE_NUMBER_INT),
|
||||||
'invoice_type' => $this->request->getPost('invoice_type')
|
'invoice_type' => Sale_lib::isValidInvoiceType($this->request->getPost('invoice_type'))
|
||||||
|
? $this->request->getPost('invoice_type')
|
||||||
|
: 'invoice'
|
||||||
];
|
];
|
||||||
|
|
||||||
$success = $this->appconfig->batch_save($batch_save_data);
|
$success = $this->appconfig->batch_save($batch_save_data);
|
||||||
@@ -973,4 +992,26 @@ class Config extends Secure_Controller
|
|||||||
|
|
||||||
return $this->response->setJSON(['success' => $success]);
|
return $this->response->setJSON(['success' => $success]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates suggestions column configuration to prevent SQL injection.
|
||||||
|
*
|
||||||
|
* @param mixed $column The column value from POST
|
||||||
|
* @param string $fieldType Either 'first' or 'other' to determine default fallback
|
||||||
|
* @return string Validated column name
|
||||||
|
*/
|
||||||
|
private function validateSuggestionsColumn(mixed $column, string $fieldType): string
|
||||||
|
{
|
||||||
|
if (!is_string($column)) {
|
||||||
|
return $fieldType === 'first' ? 'name' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowed = $fieldType === 'first'
|
||||||
|
? Item::ALLOWED_SUGGESTIONS_COLUMNS
|
||||||
|
: Item::ALLOWED_SUGGESTIONS_COLUMNS_WITH_EMPTY;
|
||||||
|
|
||||||
|
$fallback = $fieldType === 'first' ? 'name' : '';
|
||||||
|
|
||||||
|
return in_array($column, $allowed, true) ? $column : $fallback;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class Employees extends Persons
|
|||||||
$person_info = $this->employee->get_info($employee_id);
|
$person_info = $this->employee->get_info($employee_id);
|
||||||
$current_user = $this->employee->get_logged_in_employee_info();
|
$current_user = $this->employee->get_logged_in_employee_info();
|
||||||
|
|
||||||
if ($employee_id != NEW_ENTRY && !$this->employee->can_modify_employee($person_info->person_id, $current_user->person_id)) {
|
if ($employee_id != NEW_ENTRY && !$this->employee->canModifyEmployee($person_info->person_id, $current_user->person_id)) {
|
||||||
header('Location: ' . base_url('no_access/employees/employees'));
|
header('Location: ' . base_url('no_access/employees/employees'));
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
@@ -120,7 +120,7 @@ class Employees extends Persons
|
|||||||
|
|
||||||
if ($employee_id != NEW_ENTRY) {
|
if ($employee_id != NEW_ENTRY) {
|
||||||
$target_employee = $this->employee->get_info($employee_id);
|
$target_employee = $this->employee->get_info($employee_id);
|
||||||
if (!$this->employee->can_modify_employee($target_employee->person_id, $current_user->person_id)) {
|
if (!$this->employee->canModifyEmployee($target_employee->person_id, $current_user->person_id)) {
|
||||||
return $this->response->setJSON([
|
return $this->response->setJSON([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => lang('Employees.error_updating_admin'),
|
'message' => lang('Employees.error_updating_admin'),
|
||||||
@@ -153,14 +153,14 @@ class Employees extends Persons
|
|||||||
];
|
];
|
||||||
|
|
||||||
$grants_array = [];
|
$grants_array = [];
|
||||||
$is_admin = $this->employee->is_admin($current_user->person_id);
|
$isAdmin = $this->employee->isAdmin($current_user->person_id);
|
||||||
|
|
||||||
foreach ($this->module->get_all_permissions()->getResult() as $permission) {
|
foreach ($this->module->get_all_permissions()->getResult() as $permission) {
|
||||||
$grants = [];
|
$grants = [];
|
||||||
$grant = $this->request->getPost('grant_' . $permission->permission_id) != null ? $this->request->getPost('grant_' . $permission->permission_id, FILTER_SANITIZE_FULL_SPECIAL_CHARS) : '';
|
$grant = $this->request->getPost('grant_' . $permission->permission_id) != null ? $this->request->getPost('grant_' . $permission->permission_id, FILTER_SANITIZE_FULL_SPECIAL_CHARS) : '';
|
||||||
|
|
||||||
if ($grant == $permission->permission_id) {
|
if ($grant == $permission->permission_id) {
|
||||||
if (!$is_admin && !$this->employee->has_grant($permission->permission_id, $current_user->person_id)) {
|
if (!$isAdmin && !$this->employee->has_grant($permission->permission_id, $current_user->person_id)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$grants['permission_id'] = $permission->permission_id;
|
$grants['permission_id'] = $permission->permission_id;
|
||||||
@@ -226,9 +226,9 @@ class Employees extends Persons
|
|||||||
$employees_to_delete = $this->request->getPost('ids', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
$employees_to_delete = $this->request->getPost('ids', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||||
$current_user = $this->employee->get_logged_in_employee_info();
|
$current_user = $this->employee->get_logged_in_employee_info();
|
||||||
|
|
||||||
if (!$this->employee->is_admin($current_user->person_id)) {
|
if (!$this->employee->isAdmin($current_user->person_id)) {
|
||||||
foreach ($employees_to_delete as $emp_id) {
|
foreach ($employees_to_delete as $emp_id) {
|
||||||
if ($this->employee->is_admin((int)$emp_id)) {
|
if ($this->employee->isAdmin((int)$emp_id)) {
|
||||||
return $this->response->setJSON(['success' => false, 'message' => lang('Employees.error_deleting_admin')]);
|
return $this->response->setJSON(['success' => false, 'message' => lang('Employees.error_deleting_admin')]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ class Expenses extends Secure_Controller
|
|||||||
'is_deleted' => lang('Expenses.is_deleted')
|
'is_deleted' => lang('Expenses.is_deleted')
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Restore filters from URL
|
||||||
|
$data = array_merge($data, restoreTableFilters($this->request));
|
||||||
|
|
||||||
return view('expenses/manage', $data);
|
return view('expenses/manage', $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,16 +93,23 @@ class Expenses extends Secure_Controller
|
|||||||
{
|
{
|
||||||
$data = []; // TODO: Duplicated code
|
$data = []; // TODO: Duplicated code
|
||||||
|
|
||||||
$data['employees'] = [];
|
|
||||||
foreach ($this->employee->get_all()->getResult() as $employee) {
|
|
||||||
foreach (get_object_vars($employee) as $property => $value) {
|
|
||||||
$employee->$property = $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
$data['employees'][$employee->person_id] = $employee->first_name . ' ' . $employee->last_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
$data['expenses_info'] = $this->expense->get_info($expense_id);
|
$data['expenses_info'] = $this->expense->get_info($expense_id);
|
||||||
|
$expense_id = $data['expenses_info']->expense_id;
|
||||||
|
|
||||||
|
$current_employee_id = $this->employee->get_logged_in_employee_info()->person_id;
|
||||||
|
$can_assign_employee = $this->employee->has_grant('employees', $current_employee_id);
|
||||||
|
|
||||||
|
$data['employees'] = [];
|
||||||
|
if ($can_assign_employee) {
|
||||||
|
foreach ($this->employee->get_all()->getResult() as $employee) {
|
||||||
|
$data['employees'][$employee->person_id] = $employee->first_name . ' ' . $employee->last_name;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$stored_employee_id = $expense_id == NEW_ENTRY ? $current_employee_id : $data['expenses_info']->employee_id;
|
||||||
|
$stored_employee = $this->employee->get_info($stored_employee_id);
|
||||||
|
$data['employees'][$stored_employee_id] = $stored_employee->first_name . ' ' . $stored_employee->last_name;
|
||||||
|
}
|
||||||
|
$data['can_assign_employee'] = $can_assign_employee;
|
||||||
|
|
||||||
$expense_categories = [];
|
$expense_categories = [];
|
||||||
foreach ($this->expense_category->get_all(0, 0, true)->getResultArray() as $row) {
|
foreach ($this->expense_category->get_all(0, 0, true)->getResultArray() as $row) {
|
||||||
@@ -107,11 +117,9 @@ class Expenses extends Secure_Controller
|
|||||||
}
|
}
|
||||||
$data['expense_categories'] = $expense_categories;
|
$data['expense_categories'] = $expense_categories;
|
||||||
|
|
||||||
$expense_id = $data['expenses_info']->expense_id;
|
|
||||||
|
|
||||||
if ($expense_id == NEW_ENTRY) {
|
if ($expense_id == NEW_ENTRY) {
|
||||||
$data['expenses_info']->date = date('Y-m-d H:i:s');
|
$data['expenses_info']->date = date('Y-m-d H:i:s');
|
||||||
$data['expenses_info']->employee_id = $this->employee->get_logged_in_employee_info()->person_id;
|
$data['expenses_info']->employee_id = $current_employee_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
$data['payments'] = [];
|
$data['payments'] = [];
|
||||||
@@ -152,6 +160,20 @@ class Expenses extends Secure_Controller
|
|||||||
|
|
||||||
$date_formatter = date_create_from_format($config['dateformat'] . ' ' . $config['timeformat'], $newdate);
|
$date_formatter = date_create_from_format($config['dateformat'] . ' ' . $config['timeformat'], $newdate);
|
||||||
|
|
||||||
|
$current_employee_id = $this->employee->get_logged_in_employee_info()->person_id;
|
||||||
|
$submitted_employee_id = $this->request->getPost('employee_id', FILTER_SANITIZE_NUMBER_INT);
|
||||||
|
|
||||||
|
if (!$this->employee->has_grant('employees', $current_employee_id)) {
|
||||||
|
if ($expense_id == NEW_ENTRY) {
|
||||||
|
$employee_id = $current_employee_id;
|
||||||
|
} else {
|
||||||
|
$existing_expense = $this->expense->get_info($expense_id);
|
||||||
|
$employee_id = $existing_expense->employee_id;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$employee_id = $submitted_employee_id;
|
||||||
|
}
|
||||||
|
|
||||||
$expense_data = [
|
$expense_data = [
|
||||||
'date' => $date_formatter->format('Y-m-d H:i:s'),
|
'date' => $date_formatter->format('Y-m-d H:i:s'),
|
||||||
'supplier_id' => $this->request->getPost('supplier_id') == '' ? null : $this->request->getPost('supplier_id', FILTER_SANITIZE_NUMBER_INT),
|
'supplier_id' => $this->request->getPost('supplier_id') == '' ? null : $this->request->getPost('supplier_id', FILTER_SANITIZE_NUMBER_INT),
|
||||||
@@ -161,7 +183,7 @@ class Expenses extends Secure_Controller
|
|||||||
'payment_type' => $this->request->getPost('payment_type', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
'payment_type' => $this->request->getPost('payment_type', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||||
'expense_category_id' => $this->request->getPost('expense_category_id', FILTER_SANITIZE_NUMBER_INT),
|
'expense_category_id' => $this->request->getPost('expense_category_id', FILTER_SANITIZE_NUMBER_INT),
|
||||||
'description' => $this->request->getPost('description', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
'description' => $this->request->getPost('description', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||||
'employee_id' => $this->request->getPost('employee_id', FILTER_SANITIZE_NUMBER_INT),
|
'employee_id' => $employee_id,
|
||||||
'deleted' => $this->request->getPost('deleted') != null
|
'deleted' => $this->request->getPost('deleted') != null
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Controllers;
|
namespace App\Controllers;
|
||||||
|
|
||||||
|
use App\Libraries\MY_Migration;
|
||||||
use CodeIgniter\HTTP\RedirectResponse;
|
use CodeIgniter\HTTP\RedirectResponse;
|
||||||
use CodeIgniter\HTTP\ResponseInterface;
|
use CodeIgniter\HTTP\ResponseInterface;
|
||||||
|
|
||||||
@@ -34,14 +35,23 @@ class Home extends Secure_Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load "change employee password" form
|
* Load the "change employee password" form
|
||||||
*
|
*
|
||||||
* @return string
|
* @param int $employeeId
|
||||||
* @noinspection PhpUnused
|
* @return ResponseInterface|string
|
||||||
*/
|
*/
|
||||||
public function getChangePassword(int $employee_id = -1): string // TODO: Replace -1 with a constant
|
public function getChangePassword(int $employeeId = NEW_ENTRY): ResponseInterface|string
|
||||||
{
|
{
|
||||||
$person_info = $this->employee->get_info($employee_id);
|
$loggedInEmployee = $this->employee->get_logged_in_employee_info();
|
||||||
|
$currentPersonId = $loggedInEmployee->person_id;
|
||||||
|
|
||||||
|
$employeeId = $employeeId === NEW_ENTRY ? $currentPersonId : $employeeId;
|
||||||
|
|
||||||
|
if (!$this->employee->isAdmin($currentPersonId) && $employeeId !== $currentPersonId) {
|
||||||
|
return $this->response->setStatusCode(403)->setBody(lang('Employees.unauthorized_modify'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$person_info = $this->employee->get_info($employeeId);
|
||||||
foreach (get_object_vars($person_info) as $property => $value) {
|
foreach (get_object_vars($person_info) as $property => $value) {
|
||||||
$person_info->$property = $value;
|
$person_info->$property = $value;
|
||||||
}
|
}
|
||||||
@@ -55,52 +65,63 @@ class Home extends Secure_Controller
|
|||||||
*
|
*
|
||||||
* @return ResponseInterface
|
* @return ResponseInterface
|
||||||
*/
|
*/
|
||||||
public function postSave(int $employee_id = -1): ResponseInterface // TODO: Replace -1 with a constant
|
public function postSave(int $employeeId = NEW_ENTRY): ResponseInterface
|
||||||
{
|
{
|
||||||
if (!empty($this->request->getPost('current_password')) && $employee_id != -1) {
|
$currentUser = $this->employee->get_logged_in_employee_info();
|
||||||
|
|
||||||
|
$employeeId = $employeeId === NEW_ENTRY ? $currentUser->person_id : $employeeId;
|
||||||
|
|
||||||
|
if (!$this->employee->isAdmin($currentUser->person_id) && $employeeId !== $currentUser->person_id) {
|
||||||
|
return $this->response->setStatusCode(403)->setJSON([
|
||||||
|
'success' => false,
|
||||||
|
'message' => lang('Employees.unauthorized_modify')
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($this->request->getPost('current_password')) && $employeeId != NEW_ENTRY) {
|
||||||
if ($this->employee->check_password($this->request->getPost('username', FILTER_SANITIZE_FULL_SPECIAL_CHARS), $this->request->getPost('current_password'))) {
|
if ($this->employee->check_password($this->request->getPost('username', FILTER_SANITIZE_FULL_SPECIAL_CHARS), $this->request->getPost('current_password'))) {
|
||||||
// Validate password length BEFORE hashing
|
// Validate password length BEFORE hashing
|
||||||
$new_password = $this->request->getPost('password');
|
$new_password = $this->request->getPost('password');
|
||||||
|
|
||||||
if (strlen($new_password) < 8) {
|
if (strlen($new_password) < 8) {
|
||||||
return $this->response->setJSON([
|
return $this->response->setJSON([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => lang('Employees.password_minlength'),
|
'message' => lang('Employees.password_minlength'),
|
||||||
'id' => -1
|
'id' => NEW_ENTRY
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$employee_data = [
|
$employee_data = [
|
||||||
'username' => $this->request->getPost('username', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
'username' => $this->request->getPost('username', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||||
'password' => password_hash($new_password, PASSWORD_DEFAULT),
|
'password' => password_hash($new_password, PASSWORD_DEFAULT),
|
||||||
'hash_version' => 2
|
'hash_version' => 2
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($this->employee->change_password($employee_data, $employee_id)) {
|
if ($this->employee->change_password($employee_data, $employeeId)) {
|
||||||
return $this->response->setJSON([
|
return $this->response->setJSON([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => lang('Employees.successful_change_password'),
|
'message' => lang('Employees.successful_change_password'),
|
||||||
'id' => $employee_id
|
'id' => $employeeId
|
||||||
]);
|
]);
|
||||||
} else { // Failure // TODO: Replace -1 with constant
|
} else {
|
||||||
return $this->response->setJSON([
|
return $this->response->setJSON([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => lang('Employees.unsuccessful_change_password'),
|
'message' => lang('Employees.unsuccessful_change_password'),
|
||||||
'id' => -1
|
'id' => NEW_ENTRY
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
} else { // TODO: Replace -1 with constant
|
} else {
|
||||||
return $this->response->setJSON([
|
return $this->response->setJSON([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => lang('Employees.current_password_invalid'),
|
'message' => lang('Employees.current_password_invalid'),
|
||||||
'id' => -1
|
'id' => NEW_ENTRY
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
} else { // TODO: Replace -1 with constant
|
} else {
|
||||||
return $this->response->setJSON([
|
return $this->response->setJSON([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => lang('Employees.current_password_invalid'),
|
'message' => lang('Employees.current_password_invalid'),
|
||||||
'id' => -1
|
'id' => NEW_ENTRY
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ namespace App\Controllers;
|
|||||||
|
|
||||||
use App\Libraries\Barcode_lib;
|
use App\Libraries\Barcode_lib;
|
||||||
use App\Libraries\Item_lib;
|
use App\Libraries\Item_lib;
|
||||||
|
|
||||||
use App\Models\Attribute;
|
use App\Models\Attribute;
|
||||||
use App\Models\Inventory;
|
use App\Models\Inventory;
|
||||||
use App\Models\Item;
|
use App\Models\Item;
|
||||||
@@ -14,7 +13,6 @@ use App\Models\Item_taxes;
|
|||||||
use App\Models\Stock_location;
|
use App\Models\Stock_location;
|
||||||
use App\Models\Supplier;
|
use App\Models\Supplier;
|
||||||
use App\Models\Tax_category;
|
use App\Models\Tax_category;
|
||||||
|
|
||||||
use CodeIgniter\HTTP\ResponseInterface;
|
use CodeIgniter\HTTP\ResponseInterface;
|
||||||
use CodeIgniter\Images\Handlers\BaseHandler;
|
use CodeIgniter\Images\Handlers\BaseHandler;
|
||||||
use CodeIgniter\HTTP\DownloadResponse;
|
use CodeIgniter\HTTP\DownloadResponse;
|
||||||
@@ -73,7 +71,12 @@ class Items extends Secure_Controller
|
|||||||
$this->session->set('allow_temp_items', 0);
|
$this->session->set('allow_temp_items', 0);
|
||||||
|
|
||||||
$data['table_headers'] = get_items_manage_table_headers();
|
$data['table_headers'] = get_items_manage_table_headers();
|
||||||
$data['stock_location'] = $this->item_lib->get_item_location();
|
|
||||||
|
// Restore stock_location from URL or session
|
||||||
|
$stockLocation = $this->request->getGet('stock_location', FILTER_SANITIZE_NUMBER_INT);
|
||||||
|
$data['stock_location'] = $stockLocation
|
||||||
|
? $stockLocation
|
||||||
|
: $this->item_lib->get_item_location();
|
||||||
$data['stock_locations'] = $this->stock_location->get_allowed_locations();
|
$data['stock_locations'] = $this->stock_location->get_allowed_locations();
|
||||||
|
|
||||||
// Filters that will be loaded in the multiselect dropdown
|
// Filters that will be loaded in the multiselect dropdown
|
||||||
@@ -87,6 +90,9 @@ class Items extends Secure_Controller
|
|||||||
'temporary' => lang('Items.temp')
|
'temporary' => lang('Items.temp')
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Restore filters from URL
|
||||||
|
$data = array_merge($data, restoreTableFilters($this->request));
|
||||||
|
|
||||||
return view('items/manage', $data);
|
return view('items/manage', $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +102,7 @@ class Items extends Secure_Controller
|
|||||||
**/
|
**/
|
||||||
public function getSearch(): ResponseInterface
|
public function getSearch(): ResponseInterface
|
||||||
{
|
{
|
||||||
$search = $this->request->getGet('search');
|
$search = $this->request->getGet('search', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||||
$limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT);
|
$limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT);
|
||||||
$offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT);
|
$offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT);
|
||||||
$sort = $this->sanitizeSortColumn(item_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'item_id');
|
$sort = $this->sanitizeSortColumn(item_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'item_id');
|
||||||
@@ -148,6 +154,7 @@ class Items extends Secure_Controller
|
|||||||
{
|
{
|
||||||
helper('file');
|
helper('file');
|
||||||
|
|
||||||
|
$pic_filename = rawurldecode($pic_filename);
|
||||||
$file_extension = pathinfo($pic_filename, PATHINFO_EXTENSION);
|
$file_extension = pathinfo($pic_filename, PATHINFO_EXTENSION);
|
||||||
$images = glob("./uploads/item_pics/$pic_filename");
|
$images = glob("./uploads/item_pics/$pic_filename");
|
||||||
$base_path = './uploads/item_pics/' . pathinfo($pic_filename, PATHINFO_FILENAME);
|
$base_path = './uploads/item_pics/' . pathinfo($pic_filename, PATHINFO_FILENAME);
|
||||||
@@ -377,7 +384,7 @@ class Items extends Secure_Controller
|
|||||||
} else {
|
} else {
|
||||||
$images = glob("./uploads/item_pics/$item_info->pic_filename");
|
$images = glob("./uploads/item_pics/$item_info->pic_filename");
|
||||||
}
|
}
|
||||||
$data['image_path'] = sizeof($images) > 0 ? base_url($images[0]) : '';
|
$data['image_path'] = sizeof($images) > 0 ? base_url(implode('/', array_map('rawurlencode', explode('/', ltrim($images[0], './'))))) : '';
|
||||||
} else {
|
} else {
|
||||||
$data['image_path'] = '';
|
$data['image_path'] = '';
|
||||||
}
|
}
|
||||||
@@ -475,9 +482,9 @@ class Items extends Secure_Controller
|
|||||||
foreach ($result as &$item) {
|
foreach ($result as &$item) {
|
||||||
if (isset($item['item_number']) && empty($item['item_number']) && $this->config['barcode_generate_if_empty']) {
|
if (isset($item['item_number']) && empty($item['item_number']) && $this->config['barcode_generate_if_empty']) {
|
||||||
if (isset($item['item_id'])) {
|
if (isset($item['item_id'])) {
|
||||||
$save_item = ['item_number' => $item['item_number']];
|
$save_item = ['item_number' => $item['item_number'], 'item_id' => $item['item_id']];
|
||||||
$this->item->save_value($save_item, $item['item_id']);
|
$this->item->saveValue($save_item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$data['items'] = $result;
|
$data['items'] = $result;
|
||||||
@@ -499,7 +506,7 @@ class Items extends Secure_Controller
|
|||||||
$data['definition_names'] = $this->attribute->get_definition_names();
|
$data['definition_names'] = $this->attribute->get_definition_names();
|
||||||
|
|
||||||
foreach ($data['definition_values'] as $definition_id => $definition_value) {
|
foreach ($data['definition_values'] as $definition_id => $definition_value) {
|
||||||
$attribute_value = $this->attribute->get_attribute_value($item_id, $definition_id);
|
$attribute_value = $this->attribute->getAttributeValue($item_id, $definition_id);
|
||||||
$attribute_id = (empty($attribute_value) || empty($attribute_value->attribute_id)) ? null : $attribute_value->attribute_id;
|
$attribute_id = (empty($attribute_value) || empty($attribute_value->attribute_id)) ? null : $attribute_value->attribute_id;
|
||||||
$values = &$data['definition_values'][$definition_id];
|
$values = &$data['definition_values'][$definition_id];
|
||||||
$values['attribute_id'] = $attribute_id;
|
$values['attribute_id'] = $attribute_id;
|
||||||
@@ -535,7 +542,7 @@ class Items extends Secure_Controller
|
|||||||
$data['definition_names'] = $this->attribute->get_definition_names();
|
$data['definition_names'] = $this->attribute->get_definition_names();
|
||||||
|
|
||||||
foreach ($data['definition_values'] as $definition_id => $definition_value) {
|
foreach ($data['definition_values'] as $definition_id => $definition_value) {
|
||||||
$attribute_value = $this->attribute->get_attribute_value($item_id, $definition_id);
|
$attribute_value = $this->attribute->getAttributeValue($item_id, $definition_id);
|
||||||
$attribute_id = (empty($attribute_value) || empty($attribute_value->attribute_id)) ? null : $attribute_value->attribute_id;
|
$attribute_id = (empty($attribute_value) || empty($attribute_value->attribute_id)) ? null : $attribute_value->attribute_id;
|
||||||
$values = &$data['definition_values'][$definition_id];
|
$values = &$data['definition_values'][$definition_id];
|
||||||
$values['attribute_id'] = $attribute_id;
|
$values['attribute_id'] = $attribute_id;
|
||||||
@@ -617,7 +624,7 @@ class Items extends Secure_Controller
|
|||||||
// Save item data
|
// Save item data
|
||||||
$item_data = [
|
$item_data = [
|
||||||
'name' => $this->request->getPost('name'),
|
'name' => $this->request->getPost('name'),
|
||||||
'description' => $this->request->getPost('description'),
|
'description' => $this->request->getPost('description', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||||
'category' => $this->request->getPost('category'),
|
'category' => $this->request->getPost('category'),
|
||||||
'item_type' => $item_type,
|
'item_type' => $item_type,
|
||||||
'stock_type' => $this->request->getPost('stock_type') === null ? HAS_STOCK : intval($this->request->getPost('stock_type')),
|
'stock_type' => $this->request->getPost('stock_type') === null ? HAS_STOCK : intval($this->request->getPost('stock_type')),
|
||||||
@@ -656,7 +663,12 @@ class Items extends Secure_Controller
|
|||||||
|
|
||||||
$employee_id = $this->employee->get_logged_in_employee_info()->person_id;
|
$employee_id = $this->employee->get_logged_in_employee_info()->person_id;
|
||||||
|
|
||||||
if ($this->item->save_value($item_data, $item_id)) {
|
// For updates, include item_id in data array
|
||||||
|
if ($item_id !== NEW_ENTRY) {
|
||||||
|
$item_data['item_id'] = $item_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->item->saveValue($item_data)) {
|
||||||
$success = true;
|
$success = true;
|
||||||
$new_item = false;
|
$new_item = false;
|
||||||
|
|
||||||
@@ -704,7 +716,7 @@ class Items extends Secure_Controller
|
|||||||
$item_quantity = $this->item_quantity->get_item_quantity($item_id, $location['location_id']);
|
$item_quantity = $this->item_quantity->get_item_quantity($item_id, $location['location_id']);
|
||||||
|
|
||||||
if ($item_quantity->quantity != $updated_quantity || $new_item) {
|
if ($item_quantity->quantity != $updated_quantity || $new_item) {
|
||||||
$success &= $this->item_quantity->save_value($location_detail, $item_id, $location['location_id']);
|
$success = $success && $this->item_quantity->save_value($location_detail, $item_id, $location['location_id']);
|
||||||
|
|
||||||
$inv_data = [
|
$inv_data = [
|
||||||
'trans_date' => date('Y-m-d H:i:s'),
|
'trans_date' => date('Y-m-d H:i:s'),
|
||||||
@@ -715,10 +727,10 @@ class Items extends Secure_Controller
|
|||||||
'trans_inventory' => $updated_quantity - $item_quantity->quantity
|
'trans_inventory' => $updated_quantity - $item_quantity->quantity
|
||||||
];
|
];
|
||||||
|
|
||||||
$success &= $this->inventory->insert($inv_data, false);
|
$success = $success && $this->inventory->insert($inv_data, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$this->saveItemAttributes($item_id);
|
$success = $success && $this->saveItemAttributes($item_id);
|
||||||
|
|
||||||
if ($success && $upload_success) {
|
if ($success && $upload_success) {
|
||||||
$message = lang('Items.successful_' . ($new_item ? 'adding' : 'updating')) . ' ' . $item_data['name'];
|
$message = lang('Items.successful_' . ($new_item ? 'adding' : 'updating')) . ' ' . $item_data['name'];
|
||||||
@@ -769,9 +781,12 @@ class Items extends Secure_Controller
|
|||||||
$filename = $file->getClientName();
|
$filename = $file->getClientName();
|
||||||
$info = pathinfo($filename);
|
$info = pathinfo($filename);
|
||||||
|
|
||||||
|
// Sanitize filename to remove problematic characters like spaces
|
||||||
|
$sanitized_name = preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $info['filename']);
|
||||||
|
|
||||||
$file_info = [
|
$file_info = [
|
||||||
'orig_name' => $filename,
|
'orig_name' => $filename,
|
||||||
'raw_name' => $info['filename'],
|
'raw_name' => $sanitized_name,
|
||||||
'file_ext' => $file->guessExtension()
|
'file_ext' => $file->guessExtension()
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -816,8 +831,8 @@ class Items extends Secure_Controller
|
|||||||
*/
|
*/
|
||||||
public function getRemoveLogo($item_id): ResponseInterface
|
public function getRemoveLogo($item_id): ResponseInterface
|
||||||
{
|
{
|
||||||
$item_data = ['pic_filename' => null];
|
$item_data = ['pic_filename' => null, 'item_id' => $item_id];
|
||||||
$result = $this->item->save_value($item_data, $item_id);
|
$result = $this->item->saveValue($item_data);
|
||||||
|
|
||||||
return $this->response->setJSON(['success' => $result]);
|
return $this->response->setJSON(['success' => $result]);
|
||||||
}
|
}
|
||||||
@@ -872,12 +887,12 @@ class Items extends Secure_Controller
|
|||||||
$items_to_update = $this->request->getPost('item_ids');
|
$items_to_update = $this->request->getPost('item_ids');
|
||||||
$item_data = [];
|
$item_data = [];
|
||||||
|
|
||||||
foreach ($_POST as $key => $value) {
|
foreach (Item::ALLOWED_BULK_EDIT_FIELDS as $field) {
|
||||||
// This field is nullable, so treat it differently
|
$value = $this->request->getPost($field);
|
||||||
if ($key === 'supplier_id' && $value !== '') {
|
if ($field === 'supplier_id' && $value !== '') {
|
||||||
$item_data[$key] = $value;
|
$item_data[$field] = $value;
|
||||||
} elseif ($value !== '' && !(in_array($key, ['item_ids', 'tax_names', 'tax_percents']))) {
|
} elseif ($value !== null && $value !== '') {
|
||||||
$item_data[$key] = $value;
|
$item_data[$field] = $value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -928,7 +943,7 @@ class Items extends Secure_Controller
|
|||||||
*/
|
*/
|
||||||
public function getGenerateCsvFile(): DownloadResponse
|
public function getGenerateCsvFile(): DownloadResponse
|
||||||
{
|
{
|
||||||
helper('importfile_helper');
|
helper('importfile');
|
||||||
$name = 'import_items.csv';
|
$name = 'import_items.csv';
|
||||||
$allowed_locations = $this->stock_location->get_allowed_locations();
|
$allowed_locations = $this->stock_location->get_allowed_locations();
|
||||||
$allowed_attributes = $this->attribute->get_definition_names();
|
$allowed_attributes = $this->attribute->get_definition_names();
|
||||||
@@ -947,14 +962,13 @@ class Items extends Secure_Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Imports items from CSV formatted file.
|
* Imports items from a CSV formatted file.
|
||||||
* @return ResponseInterface
|
* @return ResponseInterface
|
||||||
* @throws ReflectionException
|
|
||||||
* @noinspection PhpUnused
|
* @noinspection PhpUnused
|
||||||
*/
|
*/
|
||||||
public function postImportCsvFile(): ResponseInterface
|
public function postImportCsvFile(): ResponseInterface
|
||||||
{
|
{
|
||||||
helper('importfile_helper');
|
helper('importfile');
|
||||||
try {
|
try {
|
||||||
if ($_FILES['file_path']['error'] !== UPLOAD_ERR_OK) {
|
if ($_FILES['file_path']['error'] !== UPLOAD_ERR_OK) {
|
||||||
return $this->response->setJSON(['success' => false, 'message' => lang('Items.csv_import_failed')]);
|
return $this->response->setJSON(['success' => false, 'message' => lang('Items.csv_import_failed')]);
|
||||||
@@ -963,33 +977,33 @@ class Items extends Secure_Controller
|
|||||||
set_time_limit(240);
|
set_time_limit(240);
|
||||||
|
|
||||||
$failCodes = [];
|
$failCodes = [];
|
||||||
$csv_rows = get_csv_file($_FILES['file_path']['tmp_name']);
|
$csvRows = get_csv_file($_FILES['file_path']['tmp_name']);
|
||||||
$employee_id = $this->employee->get_logged_in_employee_info()->person_id;
|
$employeeId = $this->employee->get_logged_in_employee_info()->person_id;
|
||||||
$allowed_stock_locations = $this->stock_location->get_allowed_locations();
|
$allowedStockLocations = $this->stock_location->get_allowed_locations();
|
||||||
$attribute_definition_names = $this->attribute->get_definition_names();
|
$attributeDefinitionNames = $this->attribute->get_definition_names();
|
||||||
|
|
||||||
unset($attribute_definition_names[NEW_ENTRY]); // Removes the common_none_selected_text from the array
|
unset($attributeDefinitionNames[NEW_ENTRY]); // Removes the common_none_selected_text from the array
|
||||||
|
|
||||||
$attribute_data = [];
|
$attributeData = [];
|
||||||
|
|
||||||
foreach ($attribute_definition_names as $definition_name) {
|
foreach ($attributeDefinitionNames as $definitionName) {
|
||||||
$attribute_data[$definition_name] = $this->attribute->get_definition_by_name($definition_name)[0];
|
$attributeData[$definitionName] = $this->attribute->get_definition_by_name($definitionName)[0];
|
||||||
|
|
||||||
if ($attribute_data[$definition_name]['definition_type'] === DROPDOWN) {
|
if ($attributeData[$definitionName]['definition_type'] === DROPDOWN) {
|
||||||
$attribute_data[$definition_name]['dropdown_values'] = $this->attribute->get_definition_values($attribute_data[$definition_name]['definition_id']);
|
$attributeData[$definitionName]['dropdown_values'] = $this->attribute->get_definition_values($attributeData[$definitionName]['definition_id']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$db = db_connect();
|
$db = db_connect();
|
||||||
$db->transBegin(); // TODO: This section needs to be reworked so that the data array is being created then passed to the Item model because $db doesn't exist in the controller without being instantiated, but database operations should be restricted to the model
|
$db->transBegin(); // TODO: This section needs to be reworked so that the data array is being created then passed to the Item model because $db doesn't exist in the controller without being instantiated, but database operations should be restricted to the model
|
||||||
|
|
||||||
foreach ($csv_rows as $key => $row) {
|
foreach ($csvRows as $key => $row) {
|
||||||
$is_failed_row = false;
|
$isFailedRow = false;
|
||||||
$item_id = (int)$row['Id'];
|
$itemId = (int)$row['Id'];
|
||||||
$is_update = ($item_id > 0);
|
$isUpdate = ($itemId > 0);
|
||||||
$item_data = [
|
$itemData = [
|
||||||
'item_id' => $item_id,
|
'item_id' => $itemId,
|
||||||
'name' => $row['Item Name'],
|
'name' => $row['Item Name'],
|
||||||
'description' => $row['Description'],
|
'description' => filter_var($row['Description'], FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||||
'category' => $row['Category'],
|
'category' => $row['Category'],
|
||||||
'cost_price' => $row['Cost Price'],
|
'cost_price' => $row['Cost Price'],
|
||||||
'unit_price' => $row['Unit Price'],
|
'unit_price' => $row['Unit Price'],
|
||||||
@@ -999,50 +1013,62 @@ class Items extends Secure_Controller
|
|||||||
'pic_filename' => $row['Image']
|
'pic_filename' => $row['Image']
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!empty($row['supplier ID'])) {
|
if (!empty($row['Supplier ID'])) {
|
||||||
$item_data['supplier_id'] = $this->supplier->exists($row['Supplier ID']) ? $row['Supplier ID'] : null;
|
$itemData['supplier_id'] = $this->supplier->exists($row['Supplier ID']) ? $row['Supplier ID'] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($is_update) {
|
if ($isUpdate) {
|
||||||
$item_data['allow_alt_description'] = empty($row['Allow Alt Description']) ? null : $row['Allow Alt Description'];
|
$itemData['allow_alt_description'] = $row['Allow Alt Description'] === '' ? null : $row['Allow Alt Description'];
|
||||||
$item_data['is_serialized'] = empty($row['Item has Serial Number']) ? null : $row['Item has Serial Number'];
|
$itemData['is_serialized'] = $row['Item has Serial Number'] === '' ? null : $row['Item has Serial Number'];
|
||||||
} else {
|
} else {
|
||||||
$item_data['allow_alt_description'] = empty($row['Allow Alt Description']) ? '0' : '1';
|
$itemData['allow_alt_description'] = $row['Allow Alt Description'] === '' ? '0' : '1';
|
||||||
$item_data['is_serialized'] = empty($row['Item has Serial Number']) ? '0' : '1';
|
$itemData['is_serialized'] = $row['Item has Serial Number'] === '' ? '0' : '1';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($row['Barcode']) && !$is_update) {
|
if (!empty($row['Barcode'])) {
|
||||||
$item_data['item_number'] = $row['Barcode'];
|
$itemData['item_number'] = $row['Barcode'];
|
||||||
$is_failed_row = $this->item->item_number_exists($item_data['item_number']);
|
$isFailedRow = $this->item->item_number_exists($itemData['item_number'], $itemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$is_failed_row) {
|
if (!$isFailedRow) {
|
||||||
$is_failed_row = $this->data_error_check($row, $item_data, $allowed_stock_locations, $attribute_definition_names, $attribute_data);
|
$allowedStockLocations = $this->stock_location->get_allowed_locations();
|
||||||
|
$isFailedRow = $this->validateCSVData($row, $itemData, $allowedStockLocations, $attributeDefinitionNames, $attributeData);
|
||||||
|
if (!empty($invalidLocations)) {
|
||||||
|
$isFailedRow = true;
|
||||||
|
log_message('error', 'CSV import: Invalid stock location(s) found: ' . implode(', ', $invalidLocations));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove false, null, '' and empty strings but keep 0
|
// Remove false, null, '' and empty strings but keep 0
|
||||||
$item_data = array_filter($item_data, function ($value) {
|
$itemData = array_filter($itemData, function ($value) {
|
||||||
return $value !== null && strlen($value);
|
return $value !== null && strlen($value);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!$is_failed_row && $this->item->save_value($item_data, $item_id)) {
|
if (!$isFailedRow && $this->item->saveValue($itemData)) {
|
||||||
$this->save_tax_data($row, $item_data);
|
$this->save_tax_data($row, $itemData);
|
||||||
$this->save_inventory_quantities($row, $item_data, $allowed_stock_locations, $employee_id);
|
$this->save_inventory_quantities($row, $itemData, $allowedStockLocations, $employeeId);
|
||||||
$is_failed_row = $this->save_attribute_data($row, $item_data, $attribute_data); // TODO: $is_failed_row never gets used after this.
|
$csvAttributeValues = $this->extractAttributeData($row);
|
||||||
|
$isFailedRow = !$this->attribute->saveCSVRowAttributeData($csvAttributeValues, $itemData, $attributeData);
|
||||||
|
if ($isFailedRow) {
|
||||||
|
$failedRow = $key + 2;
|
||||||
|
$failCodes[] = $failedRow;
|
||||||
|
log_message('error', "CSV Item import failed on line $failedRow while saving attributes.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if ($is_update) {
|
if ($isUpdate) {
|
||||||
$item_data = array_merge($item_data, get_object_vars($this->item->get_info_by_id_or_number($item_id)));
|
$itemData = array_merge($itemData, get_object_vars($this->item->get_info_by_id_or_number($itemId)));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$failed_row = $key + 2;
|
$failedRow = $key + 2;
|
||||||
$failCodes[] = $failed_row;
|
$failCodes[] = $failedRow;
|
||||||
log_message('error', "CSV Item import failed on line $failed_row. This item was not imported.");
|
log_message('error', "CSV Item import failed on line $failedRow. This item was not imported.");
|
||||||
}
|
}
|
||||||
|
|
||||||
unset($csv_rows[$key]);
|
unset($csvRows[$key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$csv_rows = null;
|
$csvRows = null;
|
||||||
|
|
||||||
if (count($failCodes) > 0) {
|
if (count($failCodes) > 0) {
|
||||||
$message = lang('Items.csv_import_partially_failed', [count($failCodes), implode(', ', $failCodes)]);
|
$message = lang('Items.csv_import_partially_failed', [count($failCodes), implode(', ', $failCodes)]);
|
||||||
@@ -1050,6 +1076,7 @@ class Items extends Secure_Controller
|
|||||||
return $this->response->setJSON(['success' => false, 'message' => $message]);
|
return $this->response->setJSON(['success' => false, 'message' => $message]);
|
||||||
} else {
|
} else {
|
||||||
$db->transCommit();
|
$db->transCommit();
|
||||||
|
$this->attribute->deleteOrphanedValues();
|
||||||
|
|
||||||
return $this->response->setJSON(['success' => true, 'message' => lang('Items.csv_import_success')]);
|
return $this->response->setJSON(['success' => true, 'message' => lang('Items.csv_import_success')]);
|
||||||
}
|
}
|
||||||
@@ -1063,91 +1090,141 @@ class Items extends Secure_Controller
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function extractAttributeData(array $row): array
|
||||||
|
{
|
||||||
|
$attributeData = [];
|
||||||
|
|
||||||
|
foreach ($row as $key => $value) {
|
||||||
|
if (str_starts_with($key, 'attribute_')) {
|
||||||
|
$definitionName = substr($key, 10);
|
||||||
|
$attributeData[$definitionName] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $attributeData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that stock location columns in CSV row are valid locations
|
||||||
|
*
|
||||||
|
* @param array $row
|
||||||
|
* @param array $allowedLocations
|
||||||
|
* @return array Returns array of invalid location names, empty if all valid
|
||||||
|
*/
|
||||||
|
private function validateCSVStockLocations(array $row, array $allowedLocations): array
|
||||||
|
{
|
||||||
|
$invalidLocations = [];
|
||||||
|
$allowedLocationNames = array_values($allowedLocations);
|
||||||
|
|
||||||
|
foreach (array_keys($row) as $key) {
|
||||||
|
if (str_starts_with($key, 'location_')) {
|
||||||
|
$locationName = substr($key, 9);
|
||||||
|
if (!in_array($locationName, $allowedLocationNames)) {
|
||||||
|
$invalidLocations[] = $locationName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $invalidLocations;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks the entire line of data in an import file for errors
|
* Checks the entire line of data in an import file for errors
|
||||||
*
|
*
|
||||||
* @param array $row
|
* @param array $row
|
||||||
* @param array $item_data
|
* @param array $itemData
|
||||||
* @param array $allowed_locations
|
* @param array $allowedStockLocations
|
||||||
* @param array $definition_names
|
* @param array $definitionNames
|
||||||
* @param array $attribute_data
|
* @param array $attributeData
|
||||||
* @return bool Returns false if all data checks out and true when there is an error in the data
|
* @return bool Returns false if all data checks out and true when there is an error in the data
|
||||||
*/
|
*/
|
||||||
private function data_error_check(array $row, array $item_data, array $allowed_locations, array $definition_names, array $attribute_data): bool // TODO: Long function and large number of parameters in the declaration... perhaps refactoring is needed
|
private function validateCSVData(array $row, array $itemData, array $allowedStockLocations, array $definitionNames, array $attributeData): bool // TODO: Long function and large number of parameters in the declaration... perhaps refactoring is needed
|
||||||
{
|
{
|
||||||
$item_id = $row['Id'];
|
$itemId = $row['Id'];
|
||||||
$is_update = (bool)$item_id;
|
$isUpdate = (bool)$itemId;
|
||||||
|
|
||||||
// Check for empty required fields
|
// Check for empty required fields
|
||||||
$check_for_empty = [
|
$valuesToCheckForEmpty = [
|
||||||
'name' => $item_data['name'],
|
'name' => $itemData['name'],
|
||||||
'category' => $item_data['category'],
|
'category' => $itemData['category'],
|
||||||
'unit_price' => $item_data['unit_price']
|
'unit_price' => $itemData['unit_price']
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($check_for_empty as $key => $val) {
|
foreach ($valuesToCheckForEmpty as $key => $value) {
|
||||||
if (empty($val) && !$is_update) {
|
if (($value === null || $value === '') && !$isUpdate) {
|
||||||
log_message('error', "Empty required value in $key.");
|
log_message('error', "Empty required value in $key.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$is_update) {
|
if (!$isUpdate) {
|
||||||
$item_data['cost_price'] = empty($item_data['cost_price']) ? 0 : $item_data['cost_price']; // Allow for zero wholesale price
|
$itemData['cost_price'] = empty($itemData['cost_price']) ? 0 : $itemData['cost_price']; // Allow for zero wholesale price
|
||||||
} else {
|
} else {
|
||||||
if (!$this->item->exists($item_id)) {
|
if (!$this->item->exists($itemId)) {
|
||||||
log_message('error', "non-existent item_id: '$item_id' when either existing item_id or no item_id is required.");
|
log_message('error', "non-existent item_id: '$itemId' when either existing item_id or no item_id is required.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build array of fields to check for numerics
|
// Build array of fields to check for numerics
|
||||||
$check_for_numeric_values = [
|
$valuesToCheckForNumeric = [
|
||||||
'cost_price' => $item_data['cost_price'],
|
'cost_price' => $itemData['cost_price'],
|
||||||
'unit_price' => $item_data['unit_price'],
|
'unit_price' => $itemData['unit_price'],
|
||||||
'reorder_level' => $item_data['reorder_level'],
|
'reorder_level' => $itemData['reorder_level'],
|
||||||
'supplier_id' => $row['Supplier ID'],
|
'supplier_id' => $row['Supplier ID'],
|
||||||
'Tax 1 Percent' => $row['Tax 1 Percent'],
|
'Tax 1 Percent' => $row['Tax 1 Percent'],
|
||||||
'Tax 2 Percent' => $row['Tax 2 Percent']
|
'Tax 2 Percent' => $row['Tax 2 Percent']
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($allowed_locations as $location_name) {
|
foreach ($allowedStockLocations as $location_name) {
|
||||||
$check_for_numeric_values[] = $row["location_$location_name"];
|
$valuesToCheckForNumeric[] = $row["location_$location_name"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for non-numeric values which require numeric
|
// Check for non-numeric values which require numeric
|
||||||
foreach ($check_for_numeric_values as $key => $value) {
|
foreach ($valuesToCheckForNumeric as $key => $value) {
|
||||||
if (!is_numeric($value) && !empty($value)) {
|
if (!is_numeric($value) && !empty($value)) {
|
||||||
log_message('error', "non-numeric: '$value' for '$key' when numeric is required");
|
log_message('error', "non-numeric: '$value' for '$key' when numeric is required");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check stock locations
|
||||||
|
$invalidLocations = $this->validateCSVStockLocations($row, $allowedStockLocations);
|
||||||
|
if (!empty($invalidLocations)) {
|
||||||
|
log_message('error', 'CSV import: Invalid stock location(s) found: ' . implode(', ', $invalidLocations));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Check Attribute Data
|
// Check Attribute Data
|
||||||
foreach ($definition_names as $definition_name) {
|
foreach ($definitionNames as $definitionName) {
|
||||||
if (!empty($row["attribute_$definition_name"])) {
|
$attributeColumn = "attribute_$definitionName";
|
||||||
$definition_type = $attribute_data[$definition_name]['definition_type'];
|
if (array_key_exists($attributeColumn, $row) && $row[$attributeColumn] != '') {
|
||||||
$attribute_value = $row["attribute_$definition_name"];
|
$definitionType = $attributeData[$definitionName]['definition_type'];
|
||||||
|
$attributeValue = $row[$attributeColumn];
|
||||||
|
|
||||||
switch ($definition_type) {
|
if (strcasecmp($attributeValue, '_DELETE_') === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($definitionType) {
|
||||||
case DROPDOWN:
|
case DROPDOWN:
|
||||||
$dropdown_values = $attribute_data[$definition_name]['dropdown_values'];
|
$dropdownValues = $attributeData[$definitionName]['dropdown_values'];
|
||||||
$dropdown_values[] = '';
|
$dropdownValues[] = '';
|
||||||
|
|
||||||
if (!empty($attribute_value) && !in_array($attribute_value, $dropdown_values)) {
|
if (!empty($attributeValue) && !in_array($attributeValue, $dropdownValues)) {
|
||||||
log_message('error', "Value: '$attribute_value' is not an acceptable DROPDOWN value");
|
log_message('error', "Value: '$attributeValue' is not an acceptable DROPDOWN value");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case DECIMAL:
|
case DECIMAL:
|
||||||
if (!is_numeric($attribute_value) && !empty($attribute_value)) {
|
if (!is_numeric($attributeValue) && !empty($attributeValue)) {
|
||||||
log_message('error', "'$attribute_value' is not an acceptable DECIMAL value");
|
log_message('error', "'$attributeValue' is not an acceptable DECIMAL value");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case DATE:
|
case DATE:
|
||||||
if (!valid_date($attribute_value) && !empty($attribute_value)) {
|
if (!valid_date($attributeValue) && !empty($attributeValue)) {
|
||||||
log_message('error', "'$attribute_value' is not an acceptable DATE value. The value must match the set locale.");
|
log_message('error', "'$attributeValue' is not an acceptable DATE value. The value must match the set locale.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -1158,59 +1235,6 @@ class Items extends Secure_Controller
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves attribute data found in the CSV import.
|
|
||||||
*
|
|
||||||
* @param array $row
|
|
||||||
* @param array $item_data
|
|
||||||
* @param array $definitions
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
private function save_attribute_data(array $row, array $item_data, array $definitions): bool
|
|
||||||
{
|
|
||||||
foreach ($definitions as $definition) {
|
|
||||||
$attribute_name = $definition['definition_name'];
|
|
||||||
$attribute_value = $row["attribute_$attribute_name"];
|
|
||||||
|
|
||||||
// Create attribute value
|
|
||||||
if (!empty($attribute_value) || $attribute_value === '0') {
|
|
||||||
if ($definition['definition_type'] === CHECKBOX) {
|
|
||||||
$checkbox_is_unchecked = (strcasecmp($attribute_value, 'false') === 0 || $attribute_value === '0');
|
|
||||||
$attribute_value = $checkbox_is_unchecked ? '0' : '1';
|
|
||||||
|
|
||||||
$attribute_id = $this->store_attribute_value($attribute_value, $definition, $item_data['item_id']);
|
|
||||||
} elseif (!empty($attribute_value)) {
|
|
||||||
$attribute_id = $this->store_attribute_value($attribute_value, $definition, $item_data['item_id']);
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$attribute_id) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the attribute_value and attribute_link if necessary
|
|
||||||
*/
|
|
||||||
private function store_attribute_value(string $value, array $attribute_data, int $item_id)
|
|
||||||
{
|
|
||||||
$attribute_id = $this->attribute->attributeValueExists($value, $attribute_data['definition_type']);
|
|
||||||
|
|
||||||
$this->attribute->deleteAttributeLinks($item_id, $attribute_data['definition_id']);
|
|
||||||
|
|
||||||
if (!$attribute_id) {
|
|
||||||
$attribute_id = $this->attribute->saveAttributeValue($value, $attribute_data['definition_id'], $item_id, false, $attribute_data['definition_type']);
|
|
||||||
} elseif (!$this->attribute->saveAttributeLink($item_id, $attribute_data['definition_id'], $attribute_id)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $attribute_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves inventory quantities for the row in the appropriate stock locations.
|
* Saves inventory quantities for the row in the appropriate stock locations.
|
||||||
*
|
*
|
||||||
@@ -1293,8 +1317,8 @@ class Items extends Secure_Controller
|
|||||||
$images = glob(FCPATH . "uploads/item_pics/$item->pic_filename.*");
|
$images = glob(FCPATH . "uploads/item_pics/$item->pic_filename.*");
|
||||||
if (sizeof($images) > 0) {
|
if (sizeof($images) > 0) {
|
||||||
$new_pic_filename = pathinfo($images[0], PATHINFO_BASENAME);
|
$new_pic_filename = pathinfo($images[0], PATHINFO_BASENAME);
|
||||||
$item_data = ['pic_filename' => $new_pic_filename];
|
$item_data = ['pic_filename' => $new_pic_filename, 'item_id' => $item->item_id];
|
||||||
$this->item->save_value($item_data, $item->item_id);
|
$this->item->saveValue($item_data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1304,10 +1328,11 @@ class Items extends Secure_Controller
|
|||||||
* Saves item attributes for a given item.
|
* Saves item attributes for a given item.
|
||||||
*
|
*
|
||||||
* @param int $itemId The item for which attributes need to be saved to.
|
* @param int $itemId The item for which attributes need to be saved to.
|
||||||
* @return void
|
* @return bool Returns true when item attributes are successfully saved and false on error.
|
||||||
*/
|
*/
|
||||||
public function saveItemAttributes(int $itemId): void
|
public function saveItemAttributes(int $itemId): bool
|
||||||
{
|
{
|
||||||
|
$success = true;
|
||||||
$attributeLinks = $this->request->getPost('attribute_links') ?? [];
|
$attributeLinks = $this->request->getPost('attribute_links') ?? [];
|
||||||
$attributeIds = $this->request->getPost('attribute_ids');
|
$attributeIds = $this->request->getPost('attribute_ids');
|
||||||
|
|
||||||
@@ -1319,16 +1344,18 @@ class Items extends Secure_Controller
|
|||||||
switch ($definitionType) {
|
switch ($definitionType) {
|
||||||
case DROPDOWN:
|
case DROPDOWN:
|
||||||
$attributeId = $attributeValue;
|
$attributeId = $attributeValue;
|
||||||
|
$success = $success && $this->attribute->saveAttributeLink($itemId, $definitionId, $attributeId);
|
||||||
break;
|
break;
|
||||||
case DECIMAL:
|
case DECIMAL:
|
||||||
$attributeValue = parse_decimals($attributeValue);
|
$attributeValue = parse_decimals($attributeValue);
|
||||||
// Fall through to save the attribute value
|
// no break
|
||||||
default:
|
default:
|
||||||
$attributeId = $this->attribute->saveAttributeValue($attributeValue, $definitionId, $itemId, $attributeIds[$definitionId], $definitionType);
|
$attributeId = $this->attribute->saveAttributeValue($attributeValue, $definitionId, $itemId, $attributeIds[$definitionId], $definitionType);
|
||||||
|
$success = $success && ($attributeId > 0);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->attribute->saveAttributeLink($itemId, $definitionId, $attributeId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $success && $this->attribute->deleteOrphanedValues();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace App\Controllers;
|
|||||||
use App\Libraries\MY_Migration;
|
use App\Libraries\MY_Migration;
|
||||||
use App\Models\Employee;
|
use App\Models\Employee;
|
||||||
use CodeIgniter\HTTP\RedirectResponse;
|
use CodeIgniter\HTTP\RedirectResponse;
|
||||||
|
use CodeIgniter\HTTP\ResponseInterface;
|
||||||
use CodeIgniter\Model;
|
use CodeIgniter\Model;
|
||||||
use Config\OSPOS;
|
use Config\OSPOS;
|
||||||
use Config\Services;
|
use Config\Services;
|
||||||
@@ -36,6 +37,7 @@ class Login extends BaseController
|
|||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'has_errors' => false,
|
'has_errors' => false,
|
||||||
|
'is_new_install' => !(MY_Migration::get_current_version()),
|
||||||
'is_latest' => $migration->is_latest(),
|
'is_latest' => $migration->is_latest(),
|
||||||
'latest_version' => $migration->get_latest_migration(),
|
'latest_version' => $migration->get_latest_migration(),
|
||||||
'gcaptcha_enabled' => $gcaptcha_enabled,
|
'gcaptcha_enabled' => $gcaptcha_enabled,
|
||||||
@@ -71,4 +73,28 @@ class Login extends BaseController
|
|||||||
|
|
||||||
return redirect()->to('home');
|
return redirect()->to('home');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function migrate(): ResponseInterface
|
||||||
|
{
|
||||||
|
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()
|
||||||
|
])->setStatusCode(500);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,11 +190,11 @@ class Receivings extends Secure_Controller
|
|||||||
/**
|
/**
|
||||||
* Edit line item in current receiving. Used in app/Views/receivings/receiving.php
|
* Edit line item in current receiving. Used in app/Views/receivings/receiving.php
|
||||||
*
|
*
|
||||||
* @param string|int|null $item_id
|
* @param int|string|null $item_id
|
||||||
* @return string
|
* @return string
|
||||||
* @noinspection PhpUnused
|
* @noinspection PhpUnused
|
||||||
*/
|
*/
|
||||||
public function postEditItem($item_id): string
|
public function postEditItem(int|string|null $item_id): string
|
||||||
{
|
{
|
||||||
$data = [];
|
$data = [];
|
||||||
|
|
||||||
@@ -241,15 +241,26 @@ class Receivings extends Secure_Controller
|
|||||||
$data['suppliers'][$supplier->person_id] = $supplier->first_name . ' ' . $supplier->last_name;
|
$data['suppliers'][$supplier->person_id] = $supplier->first_name . ' ' . $supplier->last_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$receiving_info = $this->receiving->get_info($receiving_id)->getRowArray();
|
||||||
|
|
||||||
|
$current_employee_id = $this->employee->get_logged_in_employee_info()->person_id;
|
||||||
|
$can_assign_employee = $this->employee->has_grant('employees', $current_employee_id);
|
||||||
|
|
||||||
$data['employees'] = [];
|
$data['employees'] = [];
|
||||||
foreach ($this->employee->get_all()->getResult() as $employee) {
|
if ($can_assign_employee) {
|
||||||
$data['employees'][$employee->person_id] = $employee->first_name . ' ' . $employee->last_name;
|
foreach ($this->employee->get_all()->getResult() as $employee) {
|
||||||
|
$data['employees'][$employee->person_id] = $employee->first_name . ' ' . $employee->last_name;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$stored_employee_id = $receiving_info['employee_id'];
|
||||||
|
$stored_employee = $this->employee->get_info($stored_employee_id);
|
||||||
|
$data['employees'][$stored_employee_id] = $stored_employee->first_name . ' ' . $stored_employee->last_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
$receiving_info = $this->receiving->get_info($receiving_id)->getRowArray();
|
|
||||||
$data['selected_supplier_name'] = !empty($receiving_info['supplier_id']) ? $receiving_info['company_name'] : '';
|
$data['selected_supplier_name'] = !empty($receiving_info['supplier_id']) ? $receiving_info['company_name'] : '';
|
||||||
$data['selected_supplier_id'] = $receiving_info['supplier_id'];
|
$data['selected_supplier_id'] = $receiving_info['supplier_id'];
|
||||||
$data['receiving_info'] = $receiving_info;
|
$data['receiving_info'] = $receiving_info;
|
||||||
|
$data['can_assign_employee'] = $can_assign_employee;
|
||||||
|
|
||||||
return view('receivings/form', $data);
|
return view('receivings/form', $data);
|
||||||
}
|
}
|
||||||
@@ -269,8 +280,10 @@ class Receivings extends Secure_Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws ReflectionException
|
* @param int $receiving_id
|
||||||
|
* @param bool $update_inventory
|
||||||
* @return ResponseInterface
|
* @return ResponseInterface
|
||||||
|
* @throws ReflectionException
|
||||||
*/
|
*/
|
||||||
public function postDelete(int $receiving_id = -1, bool $update_inventory = true): ResponseInterface
|
public function postDelete(int $receiving_id = -1, bool $update_inventory = true): ResponseInterface
|
||||||
{
|
{
|
||||||
@@ -491,10 +504,20 @@ class Receivings extends Secure_Controller
|
|||||||
$date_formatter = date_create_from_format($this->config['dateformat'] . ' ' . $this->config['timeformat'], $newdate);
|
$date_formatter = date_create_from_format($this->config['dateformat'] . ' ' . $this->config['timeformat'], $newdate);
|
||||||
$receiving_time = $date_formatter->format('Y-m-d H:i:s');
|
$receiving_time = $date_formatter->format('Y-m-d H:i:s');
|
||||||
|
|
||||||
|
$current_employee_id = $this->employee->get_logged_in_employee_info()->person_id;
|
||||||
|
$submitted_employee_id = $this->request->getPost('employee_id', FILTER_SANITIZE_NUMBER_INT);
|
||||||
|
|
||||||
|
if (!$this->employee->has_grant('employees', $current_employee_id)) {
|
||||||
|
$existing_receiving = $this->receiving->get_info($receiving_id)->getRowArray();
|
||||||
|
$employee_id = $existing_receiving['employee_id'];
|
||||||
|
} else {
|
||||||
|
$employee_id = $submitted_employee_id;
|
||||||
|
}
|
||||||
|
|
||||||
$receiving_data = [
|
$receiving_data = [
|
||||||
'receiving_time' => $receiving_time,
|
'receiving_time' => $receiving_time,
|
||||||
'supplier_id' => $this->request->getPost('supplier_id') ? $this->request->getPost('supplier_id', FILTER_SANITIZE_NUMBER_INT) : null,
|
'supplier_id' => $this->request->getPost('supplier_id') ? $this->request->getPost('supplier_id', FILTER_SANITIZE_NUMBER_INT) : null,
|
||||||
'employee_id' => $this->request->getPost('employee_id', FILTER_SANITIZE_NUMBER_INT),
|
'employee_id' => $employee_id,
|
||||||
'comment' => $this->request->getPost('comment', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
'comment' => $this->request->getPost('comment', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||||
'reference' => $this->request->getPost('reference') != '' ? $this->request->getPost('reference', FILTER_SANITIZE_FULL_SPECIAL_CHARS) : null
|
'reference' => $this->request->getPost('reference') != '' ? $this->request->getPost('reference', FILTER_SANITIZE_FULL_SPECIAL_CHARS) : null
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1776,7 +1776,7 @@ class Reports extends Secure_Controller
|
|||||||
{
|
{
|
||||||
$this->clearCache();
|
$this->clearCache();
|
||||||
|
|
||||||
$definition_names = $this->attribute->get_definitions_by_flags(attribute::SHOW_IN_SALES);
|
$definition_names = $this->attribute->get_definitions_by_flags(attribute::SHOW_IN_SALES, true);
|
||||||
|
|
||||||
$inputs = [
|
$inputs = [
|
||||||
'start_date' => $start_date,
|
'start_date' => $start_date,
|
||||||
@@ -1789,7 +1789,12 @@ class Reports extends Secure_Controller
|
|||||||
$this->detailed_sales->create($inputs);
|
$this->detailed_sales->create($inputs);
|
||||||
|
|
||||||
$columns = $this->detailed_sales->getDataColumns();
|
$columns = $this->detailed_sales->getDataColumns();
|
||||||
$columns['details'] = array_merge($columns['details'], $definition_names);
|
// Extract just names for column headers
|
||||||
|
$definitionHeaders = [];
|
||||||
|
foreach ($definition_names as $definition_id => $definitionInfo) {
|
||||||
|
$definitionHeaders[$definition_id] = $definitionInfo['name'];
|
||||||
|
}
|
||||||
|
$columns['details'] = array_merge($columns['details'], $definitionHeaders);
|
||||||
|
|
||||||
$headers = $columns;
|
$headers = $columns;
|
||||||
|
|
||||||
@@ -1930,14 +1935,19 @@ class Reports extends Secure_Controller
|
|||||||
{
|
{
|
||||||
$this->clearCache();
|
$this->clearCache();
|
||||||
|
|
||||||
$definition_names = $this->attribute->get_definitions_by_flags(attribute::SHOW_IN_RECEIVINGS);
|
$definition_names = $this->attribute->get_definitions_by_flags(attribute::SHOW_IN_RECEIVINGS, true);
|
||||||
|
|
||||||
$inputs = ['start_date' => $start_date, 'end_date' => $end_date, 'receiving_type' => $receiving_type, 'location_id' => $location_id, 'definition_ids' => array_keys($definition_names)];
|
$inputs = ['start_date' => $start_date, 'end_date' => $end_date, 'receiving_type' => $receiving_type, 'location_id' => $location_id, 'definition_ids' => array_keys($definition_names)];
|
||||||
|
|
||||||
$this->detailed_receivings->create($inputs);
|
$this->detailed_receivings->create($inputs);
|
||||||
|
|
||||||
$columns = $this->detailed_receivings->getDataColumns();
|
$columns = $this->detailed_receivings->getDataColumns();
|
||||||
$columns['details'] = array_merge($columns['details'], $definition_names);
|
// Extract just names for column headers
|
||||||
|
$definitionHeaders = [];
|
||||||
|
foreach ($definition_names as $definition_id => $definitionInfo) {
|
||||||
|
$definitionHeaders[$definition_id] = $definitionInfo['name'];
|
||||||
|
}
|
||||||
|
$columns['details'] = array_merge($columns['details'], $definitionHeaders);
|
||||||
|
|
||||||
$headers = $columns;
|
$headers = $columns;
|
||||||
$report_data = $this->detailed_receivings->getData($inputs);
|
$report_data = $this->detailed_receivings->getData($inputs);
|
||||||
|
|||||||
@@ -75,15 +75,15 @@ class Sales extends Secure_Controller
|
|||||||
/**
|
/**
|
||||||
* Load the sale edit modal. Used in app/Views/sales/register.php.
|
* Load the sale edit modal. Used in app/Views/sales/register.php.
|
||||||
*
|
*
|
||||||
* @return string
|
* @return ResponseInterface|string
|
||||||
* @noinspection PhpUnused
|
* @noinspection PhpUnused
|
||||||
*/
|
*/
|
||||||
public function getManage(): string
|
public function getManage(): ResponseInterface|string
|
||||||
{
|
{
|
||||||
$person_id = $this->session->get('person_id');
|
$personId = $this->session->get('person_id');
|
||||||
|
|
||||||
if (!$this->employee->has_grant('reports_sales', $person_id)) {
|
if (!$this->employee->has_grant('reports_sales', $personId)) {
|
||||||
redirect('no_access/sales/reports_sales');
|
return redirect()->to('no_access/sales/reports_sales');
|
||||||
} else {
|
} else {
|
||||||
$data['table_headers'] = get_sales_manage_table_headers();
|
$data['table_headers'] = get_sales_manage_table_headers();
|
||||||
|
|
||||||
@@ -92,18 +92,31 @@ class Sales extends Secure_Controller
|
|||||||
'only_due' => lang('Sales.due_filter'),
|
'only_due' => lang('Sales.due_filter'),
|
||||||
'only_check' => lang('Sales.check_filter'),
|
'only_check' => lang('Sales.check_filter'),
|
||||||
'only_creditcard' => lang('Sales.credit_filter'),
|
'only_creditcard' => lang('Sales.credit_filter'),
|
||||||
|
'only_debit' => lang('Sales.debit'),
|
||||||
'only_invoices' => lang('Sales.invoice_filter'),
|
'only_invoices' => lang('Sales.invoice_filter'),
|
||||||
'selected_customer' => lang('Sales.selected_customer')
|
'selected_customer' => lang('Sales.selected_customer')
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($this->sale_lib->get_customer() != -1) {
|
if ($this->sale_lib->get_customer() != -1) {
|
||||||
$selected_filters = ['selected_customer'];
|
$selectedFilters = ['selected_customer'];
|
||||||
$data['customer_selected'] = true;
|
$data['customer_selected'] = true;
|
||||||
} else {
|
} else {
|
||||||
$data['customer_selected'] = false;
|
$data['customer_selected'] = false;
|
||||||
$selected_filters = [];
|
$selectedFilters = [];
|
||||||
}
|
}
|
||||||
$data['selected_filters'] = $selected_filters;
|
|
||||||
|
// Restore filters from URL query string
|
||||||
|
$filters = restoreTableFilters($this->request);
|
||||||
|
if (!empty($filters['selected_filters'])) {
|
||||||
|
$selectedFilters = array_merge($selectedFilters, $filters['selected_filters']);
|
||||||
|
}
|
||||||
|
if (isset($filters['start_date'])) {
|
||||||
|
$data['start_date'] = $filters['start_date'];
|
||||||
|
}
|
||||||
|
if (isset($filters['end_date'])) {
|
||||||
|
$data['end_date'] = $filters['end_date'];
|
||||||
|
}
|
||||||
|
$data['selected_filters'] = $selectedFilters;
|
||||||
|
|
||||||
return view('sales/manage', $data);
|
return view('sales/manage', $data);
|
||||||
}
|
}
|
||||||
@@ -142,6 +155,7 @@ class Sales extends Secure_Controller
|
|||||||
'only_check' => false,
|
'only_check' => false,
|
||||||
'selected_customer' => false,
|
'selected_customer' => false,
|
||||||
'only_creditcard' => false,
|
'only_creditcard' => false,
|
||||||
|
'only_debit' => false,
|
||||||
'only_invoices' => $this->config['invoice_enable'] && $this->request->getGet('only_invoices', FILTER_SANITIZE_NUMBER_INT),
|
'only_invoices' => $this->config['invoice_enable'] && $this->request->getGet('only_invoices', FILTER_SANITIZE_NUMBER_INT),
|
||||||
'is_valid_receipt' => $this->sale->is_valid_receipt($search)
|
'is_valid_receipt' => $this->sale->is_valid_receipt($search)
|
||||||
];
|
];
|
||||||
@@ -411,7 +425,7 @@ class Sales extends Secure_Controller
|
|||||||
$new_giftcard_value = $giftcard->get_giftcard_value($giftcard_num) - $this->sale_lib->get_amount_due();
|
$new_giftcard_value = $giftcard->get_giftcard_value($giftcard_num) - $this->sale_lib->get_amount_due();
|
||||||
$new_giftcard_value = max($new_giftcard_value, 0);
|
$new_giftcard_value = max($new_giftcard_value, 0);
|
||||||
$this->sale_lib->set_giftcard_remainder($new_giftcard_value);
|
$this->sale_lib->set_giftcard_remainder($new_giftcard_value);
|
||||||
$new_giftcard_value = str_replace('$', '\$', to_currency($new_giftcard_value));
|
$new_giftcard_value = to_currency($new_giftcard_value);
|
||||||
$data['warning'] = lang('Giftcards.remaining_balance', [$giftcard_num, $new_giftcard_value]);
|
$data['warning'] = lang('Giftcards.remaining_balance', [$giftcard_num, $new_giftcard_value]);
|
||||||
$amount_tendered = min($this->sale_lib->get_amount_due(), $giftcard->get_giftcard_value($giftcard_num));
|
$amount_tendered = min($this->sale_lib->get_amount_due(), $giftcard->get_giftcard_value($giftcard_num));
|
||||||
|
|
||||||
@@ -568,12 +582,21 @@ class Sales extends Secure_Controller
|
|||||||
$data = [];
|
$data = [];
|
||||||
|
|
||||||
$rules = [
|
$rules = [
|
||||||
'price' => 'trim|required|decimal_locale',
|
'price' => 'trim|required|decimal_locale|nonNegativeDecimal',
|
||||||
'quantity' => 'trim|required|decimal_locale',
|
'quantity' => 'trim|required|decimal_locale',
|
||||||
'discount' => 'trim|permit_empty|decimal_locale',
|
'discount' => 'trim|permit_empty|decimal_locale|nonNegativeDecimal',
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($this->validate($rules)) {
|
$messages = [
|
||||||
|
'price' => [
|
||||||
|
'nonNegativeDecimal' => lang('Sales.negative_price_invalid'),
|
||||||
|
],
|
||||||
|
'discount' => [
|
||||||
|
'nonNegativeDecimal' => lang('Sales.negative_discount_invalid'),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($this->validate($rules, $messages)) {
|
||||||
$description = $this->request->getPost('description', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
$description = $this->request->getPost('description', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||||
$serialnumber = $this->request->getPost('serialnumber', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
$serialnumber = $this->request->getPost('serialnumber', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||||
$price = parse_decimals($this->request->getPost('price'));
|
$price = parse_decimals($this->request->getPost('price'));
|
||||||
@@ -582,20 +605,38 @@ class Sales extends Secure_Controller
|
|||||||
$discount = $discount_type
|
$discount = $discount_type
|
||||||
? parse_quantity($this->request->getPost('discount'))
|
? parse_quantity($this->request->getPost('discount'))
|
||||||
: parse_decimals($this->request->getPost('discount'));
|
: parse_decimals($this->request->getPost('discount'));
|
||||||
|
$discount = $discount ?: 0;
|
||||||
|
|
||||||
|
// Return mode legitimately uses negative quantities for refunds
|
||||||
|
if ($this->sale_lib->get_mode() != 'return' && $quantity < 0) {
|
||||||
|
$data['error'] = lang('Sales.negative_quantity_invalid');
|
||||||
|
return $this->_reload($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Business logic: discount bounds depend on discount_type and item values
|
||||||
|
if ($discount_type == PERCENT && $discount > 100) {
|
||||||
|
$data['error'] = lang('Sales.discount_percent_exceeds_100');
|
||||||
|
return $this->_reload($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($discount_type == FIXED && bccomp((string)$discount, bcmul((string)abs($quantity), (string)$price, 2), 2) > 0) {
|
||||||
|
$data['error'] = lang('Sales.discount_exceeds_item_total');
|
||||||
|
return $this->_reload($data);
|
||||||
|
}
|
||||||
|
|
||||||
$item_location = $this->request->getPost('location', FILTER_SANITIZE_NUMBER_INT);
|
$item_location = $this->request->getPost('location', FILTER_SANITIZE_NUMBER_INT);
|
||||||
$discounted_total = $this->request->getPost('discounted_total') != ''
|
$discounted_total = $this->request->getPost('discounted_total') != ''
|
||||||
? parse_decimals($this->request->getPost('discounted_total') ?? '')
|
? parse_decimals($this->request->getPost('discounted_total') ?? '')
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
|
||||||
$this->sale_lib->edit_item($line, $description, $serialnumber, $quantity, $discount, $discount_type, $price, $discounted_total);
|
$this->sale_lib->edit_item($line, $description, $serialnumber, $quantity, $discount, $discount_type, $price, $discounted_total);
|
||||||
|
|
||||||
$this->sale_lib->empty_payments();
|
$this->sale_lib->empty_payments();
|
||||||
|
|
||||||
$data['warning'] = $this->sale_lib->out_of_stock($this->sale_lib->get_item_id($line), $item_location);
|
$data['warning'] = $this->sale_lib->out_of_stock($this->sale_lib->get_item_id($line), $item_location);
|
||||||
} else {
|
} else {
|
||||||
$data['error'] = lang('Sales.error_editing_item');
|
$errors = $this->validator->getErrors();
|
||||||
|
$data['error'] = $errors ? reset($errors) : lang('Sales.error_editing_item');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->_reload($data);
|
return $this->_reload($data);
|
||||||
@@ -709,6 +750,12 @@ class Sales extends Secure_Controller
|
|||||||
$data['cash_amount_due'] = $totals['cash_amount_due'];
|
$data['cash_amount_due'] = $totals['cash_amount_due'];
|
||||||
$data['non_cash_amount_due'] = $totals['amount_due'];
|
$data['non_cash_amount_due'] = $totals['amount_due'];
|
||||||
|
|
||||||
|
// Prevent negative total sales (fraud/theft vector) - returns can have negative totals for legitimate refunds
|
||||||
|
if ($this->sale_lib->get_mode() != 'return' && bccomp($totals['total'], '0') < 0) {
|
||||||
|
$data['error'] = lang('Sales.negative_total_invalid');
|
||||||
|
return $this->_reload($data);
|
||||||
|
}
|
||||||
|
|
||||||
if ($data['cash_mode']) { // TODO: Convert this to ternary notation
|
if ($data['cash_mode']) { // TODO: Convert this to ternary notation
|
||||||
$data['amount_due'] = $totals['cash_amount_due'];
|
$data['amount_due'] = $totals['cash_amount_due'];
|
||||||
} else {
|
} else {
|
||||||
@@ -749,14 +796,17 @@ class Sales extends Secure_Controller
|
|||||||
|
|
||||||
if ($sale_id == NEW_ENTRY && $this->sale->check_invoice_number_exists($invoice_number)) {
|
if ($sale_id == NEW_ENTRY && $this->sale->check_invoice_number_exists($invoice_number)) {
|
||||||
$data['error'] = lang('Sales.invoice_number_duplicate', [$invoice_number]);
|
$data['error'] = lang('Sales.invoice_number_duplicate', [$invoice_number]);
|
||||||
$this->_reload($data);
|
return $this->_reload($data);
|
||||||
} else {
|
} else {
|
||||||
$data['invoice_number'] = $invoice_number;
|
$data['invoice_number'] = $invoice_number;
|
||||||
$data['sale_status'] = COMPLETED;
|
$data['sale_status'] = COMPLETED;
|
||||||
$sale_type = SALE_TYPE_INVOICE;
|
$sale_type = SALE_TYPE_INVOICE;
|
||||||
|
|
||||||
// The PHP file name is the same as the invoice_type key
|
$invoice_type = $this->config['invoice_type'];
|
||||||
$invoice_view = $this->config['invoice_type'];
|
if (!Sale_lib::isValidInvoiceType($invoice_type)) {
|
||||||
|
$invoice_type = 'invoice';
|
||||||
|
}
|
||||||
|
$invoice_view = $invoice_type;
|
||||||
|
|
||||||
// Save the data to the sales table
|
// Save the data to the sales table
|
||||||
$data['sale_id_num'] = $this->sale->save_value($sale_id, $data['sale_status'], $data['cart'], $customer_id, $employee_id, $data['comments'], $invoice_number, $work_order_number, $quote_number, $sale_type, $data['payments'], $data['dinner_table'], $tax_details);
|
$data['sale_id_num'] = $this->sale->save_value($sale_id, $data['sale_status'], $data['cart'], $customer_id, $employee_id, $data['comments'], $invoice_number, $work_order_number, $quote_number, $sale_type, $data['payments'], $data['dinner_table'], $tax_details);
|
||||||
@@ -767,10 +817,11 @@ class Sales extends Secure_Controller
|
|||||||
|
|
||||||
if ($data['sale_id_num'] == NEW_ENTRY) {
|
if ($data['sale_id_num'] == NEW_ENTRY) {
|
||||||
$data['error_message'] = lang('Sales.transaction_failed');
|
$data['error_message'] = lang('Sales.transaction_failed');
|
||||||
|
return $this->_reload($data);
|
||||||
} else {
|
} else {
|
||||||
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']);
|
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']);
|
||||||
return view('sales/' . $invoice_view, $data);
|
|
||||||
$this->sale_lib->clear_all();
|
$this->sale_lib->clear_all();
|
||||||
|
return view('sales/' . $invoice_view, $data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} elseif ($this->sale_lib->is_work_order_mode()) {
|
} elseif ($this->sale_lib->is_work_order_mode()) {
|
||||||
@@ -790,7 +841,7 @@ class Sales extends Secure_Controller
|
|||||||
|
|
||||||
if ($sale_id == NEW_ENTRY && $this->sale->check_work_order_number_exists($work_order_number)) {
|
if ($sale_id == NEW_ENTRY && $this->sale->check_work_order_number_exists($work_order_number)) {
|
||||||
$data['error'] = lang('Sales.work_order_number_duplicate');
|
$data['error'] = lang('Sales.work_order_number_duplicate');
|
||||||
$this->_reload($data);
|
return $this->_reload($data);
|
||||||
} else {
|
} else {
|
||||||
$data['work_order_number'] = $work_order_number;
|
$data['work_order_number'] = $work_order_number;
|
||||||
$data['sale_status'] = SUSPENDED;
|
$data['sale_status'] = SUSPENDED;
|
||||||
@@ -803,9 +854,8 @@ class Sales extends Secure_Controller
|
|||||||
|
|
||||||
$data['barcode'] = null;
|
$data['barcode'] = null;
|
||||||
|
|
||||||
return view('sales/work_order', $data);
|
|
||||||
$this->sale_lib->clear_mode();
|
|
||||||
$this->sale_lib->clear_all();
|
$this->sale_lib->clear_all();
|
||||||
|
return view('sales/work_order', $data);
|
||||||
}
|
}
|
||||||
} elseif ($this->sale_lib->is_quote_mode()) {
|
} elseif ($this->sale_lib->is_quote_mode()) {
|
||||||
$data['sales_quote'] = lang('Sales.quote');
|
$data['sales_quote'] = lang('Sales.quote');
|
||||||
@@ -819,7 +869,7 @@ class Sales extends Secure_Controller
|
|||||||
|
|
||||||
if ($sale_id == NEW_ENTRY && $this->sale->check_quote_number_exists($quote_number)) {
|
if ($sale_id == NEW_ENTRY && $this->sale->check_quote_number_exists($quote_number)) {
|
||||||
$data['error'] = lang('Sales.quote_number_duplicate');
|
$data['error'] = lang('Sales.quote_number_duplicate');
|
||||||
$this->_reload($data);
|
return $this->_reload($data);
|
||||||
} else {
|
} else {
|
||||||
$data['quote_number'] = $quote_number;
|
$data['quote_number'] = $quote_number;
|
||||||
$data['sale_status'] = SUSPENDED;
|
$data['sale_status'] = SUSPENDED;
|
||||||
@@ -831,9 +881,8 @@ class Sales extends Secure_Controller
|
|||||||
$data['cart'] = $this->sale_lib->sort_and_filter_cart($data['cart']);
|
$data['cart'] = $this->sale_lib->sort_and_filter_cart($data['cart']);
|
||||||
$data['barcode'] = null;
|
$data['barcode'] = null;
|
||||||
|
|
||||||
return view('sales/quote', $data);
|
|
||||||
$this->sale_lib->clear_mode();
|
|
||||||
$this->sale_lib->clear_all();
|
$this->sale_lib->clear_all();
|
||||||
|
return view('sales/quote', $data);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Save the data to the sales table
|
// Save the data to the sales table
|
||||||
@@ -852,10 +901,11 @@ class Sales extends Secure_Controller
|
|||||||
|
|
||||||
if ($data['sale_id_num'] == NEW_ENTRY) {
|
if ($data['sale_id_num'] == NEW_ENTRY) {
|
||||||
$data['error_message'] = lang('Sales.transaction_failed');
|
$data['error_message'] = lang('Sales.transaction_failed');
|
||||||
|
return $this->_reload($data);
|
||||||
} else {
|
} else {
|
||||||
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']);
|
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']);
|
||||||
return view('sales/receipt', $data);
|
|
||||||
$this->sale_lib->clear_all();
|
$this->sale_lib->clear_all();
|
||||||
|
return view('sales/receipt', $data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1107,6 +1157,9 @@ class Sales extends Secure_Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$invoice_type = $this->config['invoice_type'];
|
$invoice_type = $this->config['invoice_type'];
|
||||||
|
if (!Sale_lib::isValidInvoiceType($invoice_type)) {
|
||||||
|
$invoice_type = 'invoice';
|
||||||
|
}
|
||||||
$data['invoice_view'] = $invoice_type;
|
$data['invoice_view'] = $invoice_type;
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
@@ -1642,10 +1695,11 @@ class Sales extends Secure_Controller
|
|||||||
$this->item->update_item_number($item_id, $item_number);
|
$this->item->update_item_number($item_id, $item_number);
|
||||||
$cart = $this->sale_lib->get_cart();
|
$cart = $this->sale_lib->get_cart();
|
||||||
$x = $this->search_cart_for_item_id($item_id, $cart);
|
$x = $this->search_cart_for_item_id($item_id, $cart);
|
||||||
if ($x != null) {
|
if ($x !== null) {
|
||||||
$cart[$x]['item_number'] = $item_number;
|
$cart[$x]['item_number'] = $item_number;
|
||||||
}
|
}
|
||||||
$this->sale_lib->set_cart($cart);
|
$this->sale_lib->set_cart($cart);
|
||||||
|
return $this->response->setJSON(['success' => true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1664,11 +1718,12 @@ class Sales extends Secure_Controller
|
|||||||
$cart = $this->sale_lib->get_cart();
|
$cart = $this->sale_lib->get_cart();
|
||||||
$x = $this->search_cart_for_item_id($item_id, $cart);
|
$x = $this->search_cart_for_item_id($item_id, $cart);
|
||||||
|
|
||||||
if ($x != null) {
|
if ($x !== null) {
|
||||||
$cart[$x]['name'] = $name;
|
$cart[$x]['name'] = $name;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->sale_lib->set_cart($cart);
|
$this->sale_lib->set_cart($cart);
|
||||||
|
return $this->response->setJSON(['success' => true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1687,11 +1742,12 @@ class Sales extends Secure_Controller
|
|||||||
$cart = $this->sale_lib->get_cart();
|
$cart = $this->sale_lib->get_cart();
|
||||||
$x = $this->search_cart_for_item_id($item_id, $cart);
|
$x = $this->search_cart_for_item_id($item_id, $cart);
|
||||||
|
|
||||||
if ($x != null) {
|
if ($x !== null) {
|
||||||
$cart[$x]['description'] = $description;
|
$cart[$x]['description'] = $description;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->sale_lib->set_cart($cart);
|
$this->sale_lib->set_cart($cart);
|
||||||
|
return $this->response->setJSON(['success' => true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class Tax_categories extends Secure_Controller
|
|||||||
$search = $this->request->getGet('search');
|
$search = $this->request->getGet('search');
|
||||||
$limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT);
|
$limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT);
|
||||||
$offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT);
|
$offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT);
|
||||||
$sort = $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
$sort = $this->sanitizeSortColumn(get_tax_categories_table_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'tax_category_id');
|
||||||
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||||
|
|
||||||
$tax_categories = $this->tax_category->search($search, $limit, $offset, $sort, $order);
|
$tax_categories = $this->tax_category->search($search, $limit, $offset, $sort, $order);
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class Tax_codes extends Secure_Controller
|
|||||||
$search = $this->request->getGet('search');
|
$search = $this->request->getGet('search');
|
||||||
$limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT);
|
$limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT);
|
||||||
$offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT);
|
$offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT);
|
||||||
$sort = $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
$sort = $this->sanitizeSortColumn(get_tax_code_table_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'tax_code');
|
||||||
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||||
|
|
||||||
$tax_codes = $this->tax_code->search($search, $limit, $offset, $sort, $order);
|
$tax_codes = $this->tax_code->search($search, $limit, $offset, $sort, $order);
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class Tax_jurisdictions extends Secure_Controller
|
|||||||
$search = $this->request->getGet('search');
|
$search = $this->request->getGet('search');
|
||||||
$limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT);
|
$limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT);
|
||||||
$offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT);
|
$offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT);
|
||||||
$sort = $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
$sort = $this->sanitizeSortColumn(get_tax_jurisdictions_table_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'jurisdiction_id');
|
||||||
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||||
|
|
||||||
$tax_jurisdictions = $this->tax_jurisdiction->search($search, $limit, $offset, $sort, $order);
|
$tax_jurisdictions = $this->tax_jurisdiction->search($search, $limit, $offset, $sort, $order);
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class Taxes extends Secure_Controller
|
|||||||
$search = $this->request->getGet('search');
|
$search = $this->request->getGet('search');
|
||||||
$limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT);
|
$limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT);
|
||||||
$offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT);
|
$offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT);
|
||||||
$sort = $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
$sort = $this->sanitizeSortColumn(get_tax_rates_manage_table_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'tax_rate_id');
|
||||||
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||||
|
|
||||||
$tax_rates = $this->tax->search($search, $limit, $offset, $sort, $order);
|
$tax_rates = $this->tax->search($search, $limit, $offset, $sort, $order);
|
||||||
|
|||||||
60
app/Database/Migrations/20170501000000_initial_schema.php
Normal file
60
app/Database/Migrations/20170501000000_initial_schema.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use CodeIgniter\Database\Migration;
|
||||||
|
|
||||||
|
class Migration_Initial_Schema extends Migration
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a migration step.
|
||||||
|
* Only runs on fresh installs - skips if database already has tables.
|
||||||
|
*
|
||||||
|
* For testing: CI4's DatabaseTestTrait with $refresh=true handles table
|
||||||
|
* cleanup/creation automatically. This migration only loads initial schema
|
||||||
|
* on fresh databases where no application tables exist.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
// Check if core application tables exist (existing install)
|
||||||
|
// Note: migrations table may exist even on fresh DB due to migration tracking
|
||||||
|
$tables = $this->db->listTables();
|
||||||
|
|
||||||
|
// Check for a core application table, not just migrations table
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
// Strip prefix if present for comparison
|
||||||
|
$tableName = str_replace($this->db->getPrefix(), '', $table);
|
||||||
|
if (in_array($tableName, ['app_config', 'items', 'employees', 'people'])) {
|
||||||
|
// Database already populated - skip initial schema
|
||||||
|
// This is an existing installation upgrading from older version
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fresh install - load initial schema
|
||||||
|
helper('migration');
|
||||||
|
execute_script(APPPATH . 'Database/Migrations/sqlscripts/initial_schema.sql');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revert a migration step.
|
||||||
|
* Cannot revert initial schema - would lose all data.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// Cannot safely revert initial schema
|
||||||
|
// Would require dropping all tables which would lose all data
|
||||||
|
$this->db->query('SET FOREIGN_KEY_CHECKS = 0');
|
||||||
|
|
||||||
|
foreach ($this->db->listTables() as $table) {
|
||||||
|
$this->db->query('DROP TABLE IF EXISTS `' . $table . '`');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->query('SET FOREIGN_KEY_CHECKS = 1');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -267,6 +267,8 @@ class Migration_Sales_Tax_Data extends Migration
|
|||||||
*/
|
*/
|
||||||
public function round_number(int $rounding_mode, string $amount, int $decimals): float
|
public function round_number(int $rounding_mode, string $amount, int $decimals): float
|
||||||
{
|
{
|
||||||
|
$amount = (float)$amount;
|
||||||
|
|
||||||
if ($rounding_mode == Migration_Sales_Tax_Data::ROUND_UP) {
|
if ($rounding_mode == Migration_Sales_Tax_Data::ROUND_UP) {
|
||||||
$fig = pow(10, $decimals);
|
$fig = pow(10, $decimals);
|
||||||
$rounded_total = (ceil($fig * $amount) + ceil($fig * $amount - ceil($fig * $amount))) / $fig;
|
$rounded_total = (ceil($fig * $amount) + ceil($fig * $amount - ceil($fig * $amount))) / $fig;
|
||||||
@@ -376,7 +378,7 @@ class Migration_Sales_Tax_Data extends Migration
|
|||||||
$decimals = totals_decimals();
|
$decimals = totals_decimals();
|
||||||
|
|
||||||
foreach ($sales_taxes as $row_number => $sales_tax) {
|
foreach ($sales_taxes as $row_number => $sales_tax) {
|
||||||
$sale_tax_amount = $sales_tax['sale_tax_amount'];
|
$sale_tax_amount = (float)$sales_tax['sale_tax_amount'];
|
||||||
$rounding_code = $sales_tax['rounding_code'];
|
$rounding_code = $sales_tax['rounding_code'];
|
||||||
$rounded_sale_tax_amount = $sale_tax_amount;
|
$rounded_sale_tax_amount = $sale_tax_amount;
|
||||||
|
|
||||||
|
|||||||
@@ -243,6 +243,8 @@ class Migration_TaxAmount extends Migration
|
|||||||
*/
|
*/
|
||||||
public function round_number(int $rounding_mode, string $amount, int $decimals): float // TODO: is this currency safe?
|
public function round_number(int $rounding_mode, string $amount, int $decimals): float // TODO: is this currency safe?
|
||||||
{ // TODO: This needs to be converted to a switch
|
{ // TODO: This needs to be converted to a switch
|
||||||
|
$amount = (float)$amount;
|
||||||
|
|
||||||
if ($rounding_mode == Migration_TaxAmount::ROUND_UP) { // TODO: === ?
|
if ($rounding_mode == Migration_TaxAmount::ROUND_UP) { // TODO: === ?
|
||||||
$fig = pow(10, $decimals);
|
$fig = pow(10, $decimals);
|
||||||
$rounded_total = (ceil($fig * $amount) + ceil($fig * $amount - ceil($fig * $amount))) / $fig;
|
$rounded_total = (ceil($fig * $amount) + ceil($fig * $amount - ceil($fig * $amount))) / $fig;
|
||||||
@@ -354,7 +356,7 @@ class Migration_TaxAmount extends Migration
|
|||||||
$decimals = totals_decimals();
|
$decimals = totals_decimals();
|
||||||
|
|
||||||
foreach ($sales_taxes as $row_number => $sales_tax) {
|
foreach ($sales_taxes as $row_number => $sales_tax) {
|
||||||
$sale_tax_amount = $sales_tax['sale_tax_amount'];
|
$sale_tax_amount = (float)$sales_tax['sale_tax_amount'];
|
||||||
$rounding_code = $sales_tax['rounding_code'];
|
$rounding_code = $sales_tax['rounding_code'];
|
||||||
$rounded_sale_tax_amount = $sale_tax_amount;
|
$rounded_sale_tax_amount = $sale_tax_amount;
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class Migration_database_optimizations extends Migration
|
|||||||
|
|
||||||
$attribute = model(Attribute::class);
|
$attribute = model(Attribute::class);
|
||||||
|
|
||||||
$attribute->delete_orphaned_values();
|
$attribute->deleteOrphanedValues();
|
||||||
|
|
||||||
$this->migrate_duplicate_attribute_values(DECIMAL);
|
$this->migrate_duplicate_attribute_values(DECIMAL);
|
||||||
$this->migrate_duplicate_attribute_values(DATE);
|
$this->migrate_duplicate_attribute_values(DATE);
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Database\Migrations;
|
||||||
|
|
||||||
|
use CodeIgniter\Database\Migration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration to sanitize existing image filenames by replacing spaces with underscores
|
||||||
|
* This fixes issue #4372 where thumbnails failed to load for images with spaces in filenames
|
||||||
|
*/
|
||||||
|
class FixImageFilenameSpaces extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Perform a migration.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
$builder = $db->table('ospos_items');
|
||||||
|
|
||||||
|
// Get all items with pic_filename containing spaces
|
||||||
|
$query = $builder->like('pic_filename', ' ', 'both')->get();
|
||||||
|
$items = $query->getResult();
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$old_filename = $item->pic_filename;
|
||||||
|
$ext = pathinfo($old_filename, PATHINFO_EXTENSION);
|
||||||
|
$base_name = pathinfo($old_filename, PATHINFO_FILENAME);
|
||||||
|
|
||||||
|
// Sanitize the filename by replacing spaces and special characters
|
||||||
|
$sanitized_name = preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $base_name);
|
||||||
|
$new_filename = $sanitized_name . '.' . $ext;
|
||||||
|
|
||||||
|
// Rename the file on the filesystem
|
||||||
|
$old_path = FCPATH . 'uploads/item_pics/' . $old_filename;
|
||||||
|
$new_path = FCPATH . 'uploads/item_pics/' . $new_filename;
|
||||||
|
|
||||||
|
if (file_exists($old_path)) {
|
||||||
|
// Rename the original file
|
||||||
|
if (rename($old_path, $new_path)) {
|
||||||
|
// Check if thumbnail exists and rename it too
|
||||||
|
$old_thumb = FCPATH . 'uploads/item_pics/' . $base_name . '_thumb.' . $ext;
|
||||||
|
$new_thumb = FCPATH . 'uploads/item_pics/' . $sanitized_name . '_thumb.' . $ext;
|
||||||
|
if (file_exists($old_thumb)) {
|
||||||
|
rename($old_thumb, $new_thumb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update database record
|
||||||
|
$builder->where('item_id', $item->item_id)
|
||||||
|
->update(['pic_filename' => $new_filename]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revert a migration.
|
||||||
|
* Note: This migration does not support rollback as the original filenames are lost
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// This migration cannot be safely reversed as the original filenames are lost
|
||||||
|
// after sanitization.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -730,3 +730,148 @@ CREATE TABLE `ospos_suppliers` (
|
|||||||
--
|
--
|
||||||
-- Dumping data for table `ospos_suppliers`
|
-- Dumping data for table `ospos_suppliers`
|
||||||
--
|
--
|
||||||
|
--
|
||||||
|
-- Constraints for dumped tables
|
||||||
|
--
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `ospos_customers`
|
||||||
|
--
|
||||||
|
ALTER TABLE `ospos_customers`
|
||||||
|
ADD CONSTRAINT `ospos_customers_ibfk_1` FOREIGN KEY (`person_id`) REFERENCES `ospos_people` (`person_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `ospos_employees`
|
||||||
|
--
|
||||||
|
ALTER TABLE `ospos_employees`
|
||||||
|
ADD CONSTRAINT `ospos_employees_ibfk_1` FOREIGN KEY (`person_id`) REFERENCES `ospos_people` (`person_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `ospos_inventory`
|
||||||
|
--
|
||||||
|
ALTER TABLE `ospos_inventory`
|
||||||
|
ADD CONSTRAINT `ospos_inventory_ibfk_1` FOREIGN KEY (`trans_items`) REFERENCES `ospos_items` (`item_id`),
|
||||||
|
ADD CONSTRAINT `ospos_inventory_ibfk_2` FOREIGN KEY (`trans_user`) REFERENCES `ospos_employees` (`person_id`),
|
||||||
|
ADD CONSTRAINT `ospos_inventory_ibfk_3` FOREIGN KEY (`trans_location`) REFERENCES `ospos_stock_locations` (`location_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `ospos_items`
|
||||||
|
--
|
||||||
|
ALTER TABLE `ospos_items`
|
||||||
|
ADD CONSTRAINT `ospos_items_ibfk_1` FOREIGN KEY (`supplier_id`) REFERENCES `ospos_suppliers` (`person_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `ospos_items_taxes`
|
||||||
|
--
|
||||||
|
ALTER TABLE `ospos_items_taxes`
|
||||||
|
ADD CONSTRAINT `ospos_items_taxes_ibfk_1` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `ospos_item_kit_items`
|
||||||
|
--
|
||||||
|
ALTER TABLE `ospos_item_kit_items`
|
||||||
|
ADD CONSTRAINT `ospos_item_kit_items_ibfk_1` FOREIGN KEY (`item_kit_id`) REFERENCES `ospos_item_kits` (`item_kit_id`) ON DELETE CASCADE,
|
||||||
|
ADD CONSTRAINT `ospos_item_kit_items_ibfk_2` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `ospos_permissions`
|
||||||
|
--
|
||||||
|
ALTER TABLE `ospos_permissions`
|
||||||
|
ADD CONSTRAINT `ospos_permissions_ibfk_1` FOREIGN KEY (`module_id`) REFERENCES `ospos_modules` (`module_id`) ON DELETE CASCADE,
|
||||||
|
ADD CONSTRAINT `ospos_permissions_ibfk_2` FOREIGN KEY (`location_id`) REFERENCES `ospos_stock_locations` (`location_id`) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `ospos_grants`
|
||||||
|
--
|
||||||
|
ALTER TABLE `ospos_grants`
|
||||||
|
ADD CONSTRAINT `ospos_grants_ibfk_1` foreign key (`permission_id`) references `ospos_permissions` (`permission_id`) ON DELETE CASCADE,
|
||||||
|
ADD CONSTRAINT `ospos_grants_ibfk_2` foreign key (`person_id`) references `ospos_employees` (`person_id`) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `ospos_receivings`
|
||||||
|
--
|
||||||
|
ALTER TABLE `ospos_receivings`
|
||||||
|
ADD CONSTRAINT `ospos_receivings_ibfk_1` FOREIGN KEY (`employee_id`) REFERENCES `ospos_employees` (`person_id`),
|
||||||
|
ADD CONSTRAINT `ospos_receivings_ibfk_2` FOREIGN KEY (`supplier_id`) REFERENCES `ospos_suppliers` (`person_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `ospos_receivings_items`
|
||||||
|
--
|
||||||
|
ALTER TABLE `ospos_receivings_items`
|
||||||
|
ADD CONSTRAINT `ospos_receivings_items_ibfk_1` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`),
|
||||||
|
ADD CONSTRAINT `ospos_receivings_items_ibfk_2` FOREIGN KEY (`receiving_id`) REFERENCES `ospos_receivings` (`receiving_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `ospos_sales`
|
||||||
|
--
|
||||||
|
ALTER TABLE `ospos_sales`
|
||||||
|
ADD CONSTRAINT `ospos_sales_ibfk_1` FOREIGN KEY (`employee_id`) REFERENCES `ospos_employees` (`person_id`),
|
||||||
|
ADD CONSTRAINT `ospos_sales_ibfk_2` FOREIGN KEY (`customer_id`) REFERENCES `ospos_customers` (`person_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `ospos_sales_items`
|
||||||
|
--
|
||||||
|
ALTER TABLE `ospos_sales_items`
|
||||||
|
ADD CONSTRAINT `ospos_sales_items_ibfk_1` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`),
|
||||||
|
ADD CONSTRAINT `ospos_sales_items_ibfk_2` FOREIGN KEY (`sale_id`) REFERENCES `ospos_sales` (`sale_id`),
|
||||||
|
ADD CONSTRAINT `ospos_sales_items_ibfk_3` FOREIGN KEY (`item_location`) REFERENCES `ospos_stock_locations` (`location_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `ospos_sales_items_taxes`
|
||||||
|
--
|
||||||
|
ALTER TABLE `ospos_sales_items_taxes`
|
||||||
|
ADD CONSTRAINT `ospos_sales_items_taxes_ibfk_1` FOREIGN KEY (`sale_id`,`item_id`,`line`) REFERENCES `ospos_sales_items` (`sale_id`,`item_id`,`line`),
|
||||||
|
ADD CONSTRAINT `ospos_sales_items_taxes_ibfk_2` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `ospos_sales_payments`
|
||||||
|
--
|
||||||
|
ALTER TABLE `ospos_sales_payments`
|
||||||
|
ADD CONSTRAINT `ospos_sales_payments_ibfk_1` FOREIGN KEY (`sale_id`) REFERENCES `ospos_sales` (`sale_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `ospos_sales_suspended`
|
||||||
|
--
|
||||||
|
ALTER TABLE `ospos_sales_suspended`
|
||||||
|
ADD CONSTRAINT `ospos_sales_suspended_ibfk_1` FOREIGN KEY (`employee_id`) REFERENCES `ospos_employees` (`person_id`),
|
||||||
|
ADD CONSTRAINT `ospos_sales_suspended_ibfk_2` FOREIGN KEY (`customer_id`) REFERENCES `ospos_customers` (`person_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `ospos_sales_suspended_items`
|
||||||
|
--
|
||||||
|
ALTER TABLE `ospos_sales_suspended_items`
|
||||||
|
ADD CONSTRAINT `ospos_sales_suspended_items_ibfk_1` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`),
|
||||||
|
ADD CONSTRAINT `ospos_sales_suspended_items_ibfk_2` FOREIGN KEY (`sale_id`) REFERENCES `ospos_sales_suspended` (`sale_id`),
|
||||||
|
ADD CONSTRAINT `ospos_sales_suspended_items_ibfk_3` FOREIGN KEY (`item_location`) REFERENCES `ospos_stock_locations` (`location_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `ospos_sales_suspended_items_taxes`
|
||||||
|
--
|
||||||
|
ALTER TABLE `ospos_sales_suspended_items_taxes`
|
||||||
|
ADD CONSTRAINT `ospos_sales_suspended_items_taxes_ibfk_1` FOREIGN KEY (`sale_id`,`item_id`,`line`) REFERENCES `ospos_sales_suspended_items` (`sale_id`,`item_id`,`line`),
|
||||||
|
ADD CONSTRAINT `ospos_sales_suspended_items_taxes_ibfk_2` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `ospos_sales_suspended_payments`
|
||||||
|
--
|
||||||
|
ALTER TABLE `ospos_sales_suspended_payments`
|
||||||
|
ADD CONSTRAINT `ospos_sales_suspended_payments_ibfk_1` FOREIGN KEY (`sale_id`) REFERENCES `ospos_sales_suspended` (`sale_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `ospos_item_quantities`
|
||||||
|
--
|
||||||
|
ALTER TABLE `ospos_item_quantities`
|
||||||
|
ADD CONSTRAINT `ospos_item_quantities_ibfk_1` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`),
|
||||||
|
ADD CONSTRAINT `ospos_item_quantities_ibfk_2` FOREIGN KEY (`location_id`) REFERENCES `ospos_stock_locations` (`location_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `ospos_suppliers`
|
||||||
|
--
|
||||||
|
ALTER TABLE `ospos_suppliers`
|
||||||
|
ADD CONSTRAINT `ospos_suppliers_ibfk_1` FOREIGN KEY (`person_id`) REFERENCES `ospos_people` (`person_id`);
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Constraints for table `ospos_giftcards`
|
||||||
|
--
|
||||||
|
ALTER TABLE `ospos_giftcards`
|
||||||
|
ADD CONSTRAINT `ospos_giftcards_ibfk_1` FOREIGN KEY (`person_id`) REFERENCES `ospos_people` (`person_id`);
|
||||||
37
app/Database/Seeds/TestDatabaseBootstrapSeeder.php
Normal file
37
app/Database/Seeds/TestDatabaseBootstrapSeeder.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Database\Seeds;
|
||||||
|
|
||||||
|
use CodeIgniter\Database\Seeder;
|
||||||
|
use Config\Database;
|
||||||
|
|
||||||
|
class TestDatabaseBootstrapSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
if (ENVIRONMENT !== 'testing') {
|
||||||
|
throw new \RuntimeException('TestDatabaseBootstrapSeeder can only run in the testing environment.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = config('Database');
|
||||||
|
$group = $config->tests;
|
||||||
|
$dbName = $group['database'];
|
||||||
|
|
||||||
|
if ($dbName === '' || !str_contains(strtolower($dbName), 'test')) {
|
||||||
|
throw new \RuntimeException("Refusing to reset non-test database: {$dbName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$serverConn = Database::connect([
|
||||||
|
'hostname' => $group['hostname'],
|
||||||
|
'username' => $group['username'],
|
||||||
|
'password' => $group['password'],
|
||||||
|
'DBDriver' => $group['DBDriver'],
|
||||||
|
'database' => null,
|
||||||
|
'charset' => $group['charset'] ?? 'utf8mb4',
|
||||||
|
'DBCollat' => $group['DBCollat'] ?? 'utf8mb4_general_ci',
|
||||||
|
], false);
|
||||||
|
|
||||||
|
$serverConn->query("DROP DATABASE IF EXISTS `{$dbName}`");
|
||||||
|
$serverConn->query("CREATE DATABASE IF NOT EXISTS `{$dbName}`");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
--
|
|
||||||
-- Constraints for dumped tables
|
|
||||||
--
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Constraints for table `ospos_customers`
|
|
||||||
--
|
|
||||||
ALTER TABLE `ospos_customers`
|
|
||||||
ADD CONSTRAINT `ospos_customers_ibfk_1` FOREIGN KEY (`person_id`) REFERENCES `ospos_people` (`person_id`);
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Constraints for table `ospos_employees`
|
|
||||||
--
|
|
||||||
ALTER TABLE `ospos_employees`
|
|
||||||
ADD CONSTRAINT `ospos_employees_ibfk_1` FOREIGN KEY (`person_id`) REFERENCES `ospos_people` (`person_id`);
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Constraints for table `ospos_inventory`
|
|
||||||
--
|
|
||||||
ALTER TABLE `ospos_inventory`
|
|
||||||
ADD CONSTRAINT `ospos_inventory_ibfk_1` FOREIGN KEY (`trans_items`) REFERENCES `ospos_items` (`item_id`),
|
|
||||||
ADD CONSTRAINT `ospos_inventory_ibfk_2` FOREIGN KEY (`trans_user`) REFERENCES `ospos_employees` (`person_id`),
|
|
||||||
ADD CONSTRAINT `ospos_inventory_ibfk_3` FOREIGN KEY (`trans_location`) REFERENCES `ospos_stock_locations` (`location_id`);
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Constraints for table `ospos_items`
|
|
||||||
--
|
|
||||||
ALTER TABLE `ospos_items`
|
|
||||||
ADD CONSTRAINT `ospos_items_ibfk_1` FOREIGN KEY (`supplier_id`) REFERENCES `ospos_suppliers` (`person_id`);
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Constraints for table `ospos_items_taxes`
|
|
||||||
--
|
|
||||||
ALTER TABLE `ospos_items_taxes`
|
|
||||||
ADD CONSTRAINT `ospos_items_taxes_ibfk_1` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`) ON DELETE CASCADE;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Constraints for table `ospos_item_kit_items`
|
|
||||||
--
|
|
||||||
ALTER TABLE `ospos_item_kit_items`
|
|
||||||
ADD CONSTRAINT `ospos_item_kit_items_ibfk_1` FOREIGN KEY (`item_kit_id`) REFERENCES `ospos_item_kits` (`item_kit_id`) ON DELETE CASCADE,
|
|
||||||
ADD CONSTRAINT `ospos_item_kit_items_ibfk_2` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`) ON DELETE CASCADE;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Constraints for table `ospos_permissions`
|
|
||||||
--
|
|
||||||
ALTER TABLE `ospos_permissions`
|
|
||||||
ADD CONSTRAINT `ospos_permissions_ibfk_1` FOREIGN KEY (`module_id`) REFERENCES `ospos_modules` (`module_id`) ON DELETE CASCADE,
|
|
||||||
ADD CONSTRAINT `ospos_permissions_ibfk_2` FOREIGN KEY (`location_id`) REFERENCES `ospos_stock_locations` (`location_id`) ON DELETE CASCADE;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Constraints for table `ospos_grants`
|
|
||||||
--
|
|
||||||
ALTER TABLE `ospos_grants`
|
|
||||||
ADD CONSTRAINT `ospos_grants_ibfk_1` foreign key (`permission_id`) references `ospos_permissions` (`permission_id`) ON DELETE CASCADE,
|
|
||||||
ADD CONSTRAINT `ospos_grants_ibfk_2` foreign key (`person_id`) references `ospos_employees` (`person_id`) ON DELETE CASCADE;
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Constraints for table `ospos_receivings`
|
|
||||||
--
|
|
||||||
ALTER TABLE `ospos_receivings`
|
|
||||||
ADD CONSTRAINT `ospos_receivings_ibfk_1` FOREIGN KEY (`employee_id`) REFERENCES `ospos_employees` (`person_id`),
|
|
||||||
ADD CONSTRAINT `ospos_receivings_ibfk_2` FOREIGN KEY (`supplier_id`) REFERENCES `ospos_suppliers` (`person_id`);
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Constraints for table `ospos_receivings_items`
|
|
||||||
--
|
|
||||||
ALTER TABLE `ospos_receivings_items`
|
|
||||||
ADD CONSTRAINT `ospos_receivings_items_ibfk_1` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`),
|
|
||||||
ADD CONSTRAINT `ospos_receivings_items_ibfk_2` FOREIGN KEY (`receiving_id`) REFERENCES `ospos_receivings` (`receiving_id`);
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Constraints for table `ospos_sales`
|
|
||||||
--
|
|
||||||
ALTER TABLE `ospos_sales`
|
|
||||||
ADD CONSTRAINT `ospos_sales_ibfk_1` FOREIGN KEY (`employee_id`) REFERENCES `ospos_employees` (`person_id`),
|
|
||||||
ADD CONSTRAINT `ospos_sales_ibfk_2` FOREIGN KEY (`customer_id`) REFERENCES `ospos_customers` (`person_id`);
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Constraints for table `ospos_sales_items`
|
|
||||||
--
|
|
||||||
ALTER TABLE `ospos_sales_items`
|
|
||||||
ADD CONSTRAINT `ospos_sales_items_ibfk_1` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`),
|
|
||||||
ADD CONSTRAINT `ospos_sales_items_ibfk_2` FOREIGN KEY (`sale_id`) REFERENCES `ospos_sales` (`sale_id`),
|
|
||||||
ADD CONSTRAINT `ospos_sales_items_ibfk_3` FOREIGN KEY (`item_location`) REFERENCES `ospos_stock_locations` (`location_id`);
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Constraints for table `ospos_sales_items_taxes`
|
|
||||||
--
|
|
||||||
ALTER TABLE `ospos_sales_items_taxes`
|
|
||||||
ADD CONSTRAINT `ospos_sales_items_taxes_ibfk_1` FOREIGN KEY (`sale_id`,`item_id`,`line`) REFERENCES `ospos_sales_items` (`sale_id`,`item_id`,`line`),
|
|
||||||
ADD CONSTRAINT `ospos_sales_items_taxes_ibfk_2` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`);
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Constraints for table `ospos_sales_payments`
|
|
||||||
--
|
|
||||||
ALTER TABLE `ospos_sales_payments`
|
|
||||||
ADD CONSTRAINT `ospos_sales_payments_ibfk_1` FOREIGN KEY (`sale_id`) REFERENCES `ospos_sales` (`sale_id`);
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Constraints for table `ospos_sales_suspended`
|
|
||||||
--
|
|
||||||
ALTER TABLE `ospos_sales_suspended`
|
|
||||||
ADD CONSTRAINT `ospos_sales_suspended_ibfk_1` FOREIGN KEY (`employee_id`) REFERENCES `ospos_employees` (`person_id`),
|
|
||||||
ADD CONSTRAINT `ospos_sales_suspended_ibfk_2` FOREIGN KEY (`customer_id`) REFERENCES `ospos_customers` (`person_id`);
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Constraints for table `ospos_sales_suspended_items`
|
|
||||||
--
|
|
||||||
ALTER TABLE `ospos_sales_suspended_items`
|
|
||||||
ADD CONSTRAINT `ospos_sales_suspended_items_ibfk_1` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`),
|
|
||||||
ADD CONSTRAINT `ospos_sales_suspended_items_ibfk_2` FOREIGN KEY (`sale_id`) REFERENCES `ospos_sales_suspended` (`sale_id`),
|
|
||||||
ADD CONSTRAINT `ospos_sales_suspended_items_ibfk_3` FOREIGN KEY (`item_location`) REFERENCES `ospos_stock_locations` (`location_id`);
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Constraints for table `ospos_sales_suspended_items_taxes`
|
|
||||||
--
|
|
||||||
ALTER TABLE `ospos_sales_suspended_items_taxes`
|
|
||||||
ADD CONSTRAINT `ospos_sales_suspended_items_taxes_ibfk_1` FOREIGN KEY (`sale_id`,`item_id`,`line`) REFERENCES `ospos_sales_suspended_items` (`sale_id`,`item_id`,`line`),
|
|
||||||
ADD CONSTRAINT `ospos_sales_suspended_items_taxes_ibfk_2` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`);
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Constraints for table `ospos_sales_suspended_payments`
|
|
||||||
--
|
|
||||||
ALTER TABLE `ospos_sales_suspended_payments`
|
|
||||||
ADD CONSTRAINT `ospos_sales_suspended_payments_ibfk_1` FOREIGN KEY (`sale_id`) REFERENCES `ospos_sales_suspended` (`sale_id`);
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Constraints for table `ospos_item_quantities`
|
|
||||||
--
|
|
||||||
ALTER TABLE `ospos_item_quantities`
|
|
||||||
ADD CONSTRAINT `ospos_item_quantities_ibfk_1` FOREIGN KEY (`item_id`) REFERENCES `ospos_items` (`item_id`),
|
|
||||||
ADD CONSTRAINT `ospos_item_quantities_ibfk_2` FOREIGN KEY (`location_id`) REFERENCES `ospos_stock_locations` (`location_id`);
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Constraints for table `ospos_suppliers`
|
|
||||||
--
|
|
||||||
ALTER TABLE `ospos_suppliers`
|
|
||||||
ADD CONSTRAINT `ospos_suppliers_ibfk_1` FOREIGN KEY (`person_id`) REFERENCES `ospos_people` (`person_id`);
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Constraints for table `ospos_giftcards`
|
|
||||||
--
|
|
||||||
ALTER TABLE `ospos_giftcards`
|
|
||||||
ADD CONSTRAINT `ospos_giftcards_ibfk_1` FOREIGN KEY (`person_id`) REFERENCES `ospos_people` (`person_id`);
|
|
||||||
@@ -36,21 +36,26 @@ class Db_log
|
|||||||
private function generate_message(): string
|
private function generate_message(): string
|
||||||
{
|
{
|
||||||
$db = Database::connect();
|
$db = Database::connect();
|
||||||
$last_query = $db->getLastQuery();
|
$lastQuery = $db->getLastQuery();
|
||||||
$affected_rows = $db->affectedRows();
|
|
||||||
$execution_time = $this->convert_time($last_query->getDuration());
|
if ($lastQuery === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$affectedRows = $db->affectedRows();
|
||||||
|
$executionTime = $this->convert_time($lastQuery->getDuration());
|
||||||
|
|
||||||
$message = '*** Query: ' . date('Y-m-d H:i:s T') . ' *******************'
|
$message = '*** Query: ' . date('Y-m-d H:i:s T') . ' *******************'
|
||||||
. "\n" . $last_query->getQuery()
|
. "\n" . $lastQuery->getQuery()
|
||||||
. "\n Affected rows: $affected_rows"
|
. "\n Affected rows: $affectedRows"
|
||||||
. "\n Execution Time: " . $execution_time['time'] . ' ' . $execution_time['unit'];
|
. "\n Execution Time: " . $executionTime['time'] . ' ' . $executionTime['unit'];
|
||||||
|
|
||||||
$long_query = ($execution_time['unit'] === 's') && ($execution_time['time'] > 0.5);
|
$longQuery = ($executionTime['unit'] === 's') && ($executionTime['time'] > 0.5);
|
||||||
if ($long_query) {
|
if ($longQuery) {
|
||||||
$message .= ' [LONG RUNNING QUERY]';
|
$message .= ' [LONG RUNNING QUERY]';
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->config->db_log_only_long && !$long_query ? '' : $message;
|
return $this->config->db_log_only_long && !$longQuery ? '' : $message;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ namespace App\Events;
|
|||||||
|
|
||||||
use App\Libraries\MY_Migration;
|
use App\Libraries\MY_Migration;
|
||||||
use App\Models\Appconfig;
|
use App\Models\Appconfig;
|
||||||
|
use CodeIgniter\Session\Handlers\DatabaseHandler;
|
||||||
|
use CodeIgniter\Session\Handlers\FileHandler;
|
||||||
use CodeIgniter\Session\Session;
|
use CodeIgniter\Session\Session;
|
||||||
use Config\OSPOS;
|
use Config\OSPOS;
|
||||||
use Config\Services;
|
use Config\Services;
|
||||||
@@ -19,38 +21,47 @@ class Load_config
|
|||||||
{
|
{
|
||||||
public Session $session;
|
public Session $session;
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads configuration from database into App CI config and then applies those settings
|
|
||||||
*/
|
|
||||||
public function load_config(): void
|
public function load_config(): void
|
||||||
{
|
{
|
||||||
// Migrations
|
|
||||||
$migration_config = config('Migrations');
|
$migration_config = config('Migrations');
|
||||||
$migration = new MY_Migration($migration_config);
|
$migration = new MY_Migration($migration_config);
|
||||||
|
|
||||||
$this->session = session();
|
$this->session = session();
|
||||||
|
|
||||||
// Database Configuration
|
|
||||||
$config = config(OSPOS::class);
|
$config = config(OSPOS::class);
|
||||||
|
|
||||||
if (!$migration->is_latest()) {
|
if (!$migration->is_latest()) {
|
||||||
$this->session->destroy();
|
$this->session->destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Language
|
$this->setDefaultLanguage($config);
|
||||||
$language_exists = file_exists('../app/Language/' . current_language_code());
|
|
||||||
|
|
||||||
if (current_language_code() == null || current_language() == null || !$language_exists) { // TODO: current_language() is undefined
|
|
||||||
$config->settings['language'] = 'english';
|
|
||||||
$config->settings['language_code'] = 'en';
|
|
||||||
}
|
|
||||||
|
|
||||||
$language = Services::language();
|
$language = Services::language();
|
||||||
$language->setLocale($config->settings['language_code']);
|
$language->setLocale(current_language_code());
|
||||||
|
|
||||||
// Time Zone
|
|
||||||
date_default_timezone_set($config->settings['timezone'] ?? ini_get('date.timezone'));
|
date_default_timezone_set($config->settings['timezone'] ?? ini_get('date.timezone'));
|
||||||
|
|
||||||
bcscale(max(2, totals_decimals() + tax_decimals()));
|
bcscale(max(2, totals_decimals() + tax_decimals()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function setDefaultLanguage(OSPOS $config): void
|
||||||
|
{
|
||||||
|
$languageCode = $config->settings['language_code'] ?? null;
|
||||||
|
|
||||||
|
if (empty($config->settings) || $languageCode === null) {
|
||||||
|
$config->settings['language'] = 'english';
|
||||||
|
$config->settings['language_code'] = 'en';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->languageExists($languageCode)) {
|
||||||
|
$config->settings['language'] = 'english';
|
||||||
|
$config->settings['language_code'] = 'en';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function languageExists(string $languageCode): bool
|
||||||
|
{
|
||||||
|
return file_exists(APPPATH . 'Language/' . $languageCode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
app/Helpers/attribute_helper.php
Normal file
35
app/Helpers/attribute_helper.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translates the attribute type to the corresponding database column name.
|
||||||
|
*
|
||||||
|
* Maps attribute type constants to their corresponding attribute_values table columns.
|
||||||
|
* Defaults to 'attribute_value' for TEXT, DROPDOWN and CHECKBOX attribute types.
|
||||||
|
*
|
||||||
|
* @param string $input The attribute type constant (DATE, DECIMAL, etc.)
|
||||||
|
* @return string The database column name for storing this attribute type
|
||||||
|
*/
|
||||||
|
function getAttributeDataType(string $input): string
|
||||||
|
{
|
||||||
|
$columnMap = [
|
||||||
|
DATE => 'attribute_date',
|
||||||
|
DECIMAL => 'attribute_decimal',
|
||||||
|
];
|
||||||
|
|
||||||
|
return $columnMap[$input] ?? 'attribute_value';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that the provided data type is an allowed attribute value type.
|
||||||
|
*
|
||||||
|
* @param string $dataType
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function validateAttributeValueType(string $dataType): void
|
||||||
|
{
|
||||||
|
$attributeValueTypes = ['attribute_value', 'attribute_decimal', 'attribute_date'];
|
||||||
|
|
||||||
|
if (!in_array($dataType, $attributeValueTypes, true)) {
|
||||||
|
throw new InvalidArgumentException('Invalid data type');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $stock_locations
|
* @param array $stock_locations
|
||||||
* @param array $attributes
|
* @param array $attributes
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function generate_import_items_csv(array $stock_locations, array $attributes): string
|
function generate_import_items_csv(array $stock_locations, array $attributes): string
|
||||||
{
|
{
|
||||||
$csv_headers = pack('CCC', 0xef, 0xbb, 0xbf); // Encode the Byte-Order Mark (BOM) so that UTF-8 File headers display properly in Microsoft Excel
|
$csv_headers = pack('CCC', 0xef, 0xbb, 0xbf); // Encode the Byte-Order Mark (BOM) so that UTF-8 File headers display properly in Microsoft Excel
|
||||||
|
|||||||
@@ -22,9 +22,7 @@ function current_language_code(bool $load_system_language = false): string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$language_code = $config['language_code'];
|
return $config->language_code ?? DEFAULT_LANGUAGE_CODE;
|
||||||
|
|
||||||
return empty($language_code) ? DEFAULT_LANGUAGE_CODE : $language_code;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,9 +43,7 @@ function current_language(bool $load_system_language = false): string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$language = $config['language'];
|
return $config->language ?? DEFAULT_LANGUAGE_CODE;
|
||||||
|
|
||||||
return empty($language) ? DEFAULT_LANGUAGE : $language;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,56 +11,54 @@ function check_encryption(): bool
|
|||||||
$old_key = config('Encryption')->key;
|
$old_key = config('Encryption')->key;
|
||||||
|
|
||||||
if ((empty($old_key)) || (strlen($old_key) < 64)) {
|
if ((empty($old_key)) || (strlen($old_key) < 64)) {
|
||||||
// Create Key
|
|
||||||
$encryption = new Encryption();
|
$encryption = new Encryption();
|
||||||
$key = bin2hex($encryption->createKey());
|
$key = bin2hex($encryption->createKey());
|
||||||
config('Encryption')->key = $key;
|
config('Encryption')->key = $key;
|
||||||
|
|
||||||
// Write to .env
|
|
||||||
$config_path = ROOTPATH . '.env';
|
$config_path = ROOTPATH . '.env';
|
||||||
$new_config_path = WRITEPATH . '/backup/.env';
|
|
||||||
$backup_path = WRITEPATH . '/backup/.env.bak';
|
$backup_path = WRITEPATH . '/backup/.env.bak';
|
||||||
|
|
||||||
$backup_folder = WRITEPATH . '/backup';
|
$backup_folder = WRITEPATH . '/backup';
|
||||||
|
|
||||||
if (!file_exists($backup_folder) && !mkdir($backup_folder)) {
|
if (!file_exists($backup_folder)) {
|
||||||
log_message('error', 'Could not create backup folder');
|
@mkdir($backup_folder, 0750, true);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!copy($config_path, $backup_path)) {
|
if (!file_exists($config_path)) {
|
||||||
log_message('error', "Unable to copy $config_path to $backup_path");
|
$example_path = ROOTPATH . '.env.example';
|
||||||
|
if (file_exists($example_path)) {
|
||||||
|
@copy($example_path, $config_path);
|
||||||
|
} else {
|
||||||
|
@file_put_contents($config_path, "# OSPOS Configuration\n\n");
|
||||||
|
}
|
||||||
|
@chmod($config_path, 0640);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy to backup
|
if (file_exists($config_path)) {
|
||||||
@chmod($config_path, 0660);
|
@copy($config_path, $backup_path);
|
||||||
@chmod($backup_path, 0660);
|
@chmod($backup_path, 0640);
|
||||||
|
@chmod($config_path, 0640);
|
||||||
|
|
||||||
$config_file = file_get_contents($config_path);
|
$config_file = file_get_contents($config_path);
|
||||||
$config_file = preg_replace("/(encryption\.key.*=.*)('.*')/", "$1'$key'", $config_file);
|
|
||||||
|
|
||||||
if (!empty($old_key)) {
|
if (strpos($config_file, 'encryption.key') !== false) {
|
||||||
$old_line = "# encryption.key = '$old_key' REMOVE IF UNNEEDED\r\n";
|
$config_file = preg_replace("/(encryption\.key.*=.*)('.*')/", "$1'$key'", $config_file);
|
||||||
$insertion_point = stripos($config_file, 'encryption.key');
|
} else {
|
||||||
$config_file = substr_replace($config_file, $old_line, $insertion_point, 0);
|
$config_file .= "\nencryption.key = '$key'\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($old_key)) {
|
||||||
|
$old_line = "# encryption.key = '$old_key' REMOVE IF UNNEEDED\r\n";
|
||||||
|
$insertion_point = stripos($config_file, 'encryption.key');
|
||||||
|
if ($insertion_point !== false) {
|
||||||
|
$config_file = substr_replace($config_file, $old_line, $insertion_point, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@file_put_contents($config_path, $config_file);
|
||||||
|
@chmod($config_path, 0640);
|
||||||
|
|
||||||
|
log_message('info', "Updated encryption key in $config_path");
|
||||||
}
|
}
|
||||||
|
|
||||||
$handle = @fopen($config_path, 'w+');
|
|
||||||
|
|
||||||
if (empty($handle)) {
|
|
||||||
log_message('error', "Unable to open $config_path for updating");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@chmod($config_path, 0660);
|
|
||||||
$write_failed = !fwrite($handle, $config_file);
|
|
||||||
fclose($handle);
|
|
||||||
|
|
||||||
if ($write_failed) {
|
|
||||||
log_message('error', "Unable to write to $config_path for updating.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
log_message('info', "File $config_path has been updated.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -74,23 +72,14 @@ function abort_encryption_conversion(): void
|
|||||||
$config_path = ROOTPATH . '.env';
|
$config_path = ROOTPATH . '.env';
|
||||||
$backup_path = WRITEPATH . '/backup/.env.bak';
|
$backup_path = WRITEPATH . '/backup/.env.bak';
|
||||||
|
|
||||||
$config_file = file_get_contents($backup_path);
|
if (!file_exists($backup_path)) {
|
||||||
|
return;
|
||||||
$handle = @fopen($config_path, 'w+');
|
|
||||||
|
|
||||||
if (empty($handle)) {
|
|
||||||
log_message('error', "Unable to open $config_path to undo encryption conversion");
|
|
||||||
} else {
|
|
||||||
@chmod($config_path, 0660);
|
|
||||||
$write_failed = !fwrite($handle, $config_file);
|
|
||||||
fclose($handle);
|
|
||||||
|
|
||||||
if ($write_failed) {
|
|
||||||
log_message('error', "Unable to write to $config_path to undo encryption conversion.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log_message('info', "File $config_path has been updated to undo encryption conversion");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@chmod($config_path, 0640);
|
||||||
|
$config_file = file_get_contents($backup_path);
|
||||||
|
@file_put_contents($config_path, $config_file);
|
||||||
|
log_message('info', "Restored $config_path from backup");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,13 +88,9 @@ function abort_encryption_conversion(): void
|
|||||||
function remove_backup(): void
|
function remove_backup(): void
|
||||||
{
|
{
|
||||||
$backup_path = WRITEPATH . '/backup/.env.bak';
|
$backup_path = WRITEPATH . '/backup/.env.bak';
|
||||||
if (! file_exists($backup_path)) {
|
if (!file_exists($backup_path)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!unlink($backup_path)) {
|
@unlink($backup_path);
|
||||||
log_message('error', "Unable to remove $backup_path.");
|
log_message('info', "Removed $backup_path");
|
||||||
return;
|
|
||||||
}
|
|
||||||
log_message('info', "File $backup_path has been removed");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use App\Models\Employee;
|
|||||||
use App\Models\Item_taxes;
|
use App\Models\Item_taxes;
|
||||||
use App\Models\Tax_category;
|
use App\Models\Tax_category;
|
||||||
use CodeIgniter\Database\ResultInterface;
|
use CodeIgniter\Database\ResultInterface;
|
||||||
|
use CodeIgniter\HTTP\IncomingRequest;
|
||||||
use CodeIgniter\Session\Session;
|
use CodeIgniter\Session\Session;
|
||||||
use Config\OSPOS;
|
use Config\OSPOS;
|
||||||
use Config\Services;
|
use Config\Services;
|
||||||
@@ -48,7 +49,7 @@ function transform_headers(array $headers, bool $readonly = false, bool $editabl
|
|||||||
'field' => key($element),
|
'field' => key($element),
|
||||||
'title' => current($element),
|
'title' => current($element),
|
||||||
'switchable' => $element['switchable'] ?? !preg_match('(^$| )', current($element)),
|
'switchable' => $element['switchable'] ?? !preg_match('(^$| )', current($element)),
|
||||||
'escape' => !preg_match("/(edit|email|messages|item_pic|customer_name|note)/", key($element)) && !(isset($element['escape']) && !$element['escape']),
|
'escape' => !preg_match("/(edit|email|messages|item_pic)/", key($element)) && !(isset($element['escape']) && !$element['escape']),
|
||||||
'sortable' => $element['sortable'] ?? current($element) != '',
|
'sortable' => $element['sortable'] ?? current($element) != '',
|
||||||
'checkbox' => $element['checkbox'] ?? false,
|
'checkbox' => $element['checkbox'] ?? false,
|
||||||
'class' => isset($element['checkbox']) || preg_match('(^$| )', current($element)) ? 'print_hide' : '',
|
'class' => isset($element['checkbox']) || preg_match('(^$| )', current($element)) ? 'print_hide' : '',
|
||||||
@@ -408,7 +409,7 @@ function get_items_manage_table_headers(): string
|
|||||||
{
|
{
|
||||||
$attribute = model(Attribute::class);
|
$attribute = model(Attribute::class);
|
||||||
$config = config(OSPOS::class)->settings;
|
$config = config(OSPOS::class)->settings;
|
||||||
$definition_names = $attribute->get_definitions_by_flags($attribute::SHOW_IN_ITEMS); // TODO: this should be made into a constant in constants.php
|
$definitionsWithTypes = $attribute->get_definitions_by_flags($attribute::SHOW_IN_ITEMS, true);
|
||||||
|
|
||||||
$headers = item_headers();
|
$headers = item_headers();
|
||||||
|
|
||||||
@@ -420,8 +421,8 @@ function get_items_manage_table_headers(): string
|
|||||||
|
|
||||||
$headers[] = ['item_pic' => lang('Items.image'), 'sortable' => false];
|
$headers[] = ['item_pic' => lang('Items.image'), 'sortable' => false];
|
||||||
|
|
||||||
foreach ($definition_names as $definition_id => $definition_name) {
|
foreach ($definitionsWithTypes as $definition_id => $definitionInfo) {
|
||||||
$headers[] = [$definition_id => $definition_name, 'sortable' => false];
|
$headers[] = [$definition_id => $definitionInfo['name'], 'sortable' => false];
|
||||||
}
|
}
|
||||||
|
|
||||||
$headers[] = ['inventory' => '', 'escape' => false];
|
$headers[] = ['inventory' => '', 'escape' => false];
|
||||||
@@ -470,7 +471,8 @@ function get_item_data_row(object $item): array
|
|||||||
: glob("./uploads/item_pics/$item->pic_filename");
|
: glob("./uploads/item_pics/$item->pic_filename");
|
||||||
|
|
||||||
if (sizeof($images) > 0) {
|
if (sizeof($images) > 0) {
|
||||||
$image .= '<a class="rollover" href="' . base_url($images[0]) . '"><img alt="Image thumbnail" src="' . site_url('items/PicThumb/' . pathinfo($images[0], PATHINFO_BASENAME)) . '"></a>';
|
$image_path = ltrim($images[0], './');
|
||||||
|
$image .= '<a class="rollover" href="' . base_url(implode('/', array_map('rawurlencode', explode('/', $image_path)))) . '"><img alt="Image thumbnail" src="' . site_url('items/PicThumb/' . rawurlencode(pathinfo($images[0], PATHINFO_BASENAME))) . '"></a>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,7 +480,7 @@ function get_item_data_row(object $item): array
|
|||||||
$item->name .= NAME_SEPARATOR . $item->pack_name;
|
$item->name .= NAME_SEPARATOR . $item->pack_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
$definition_names = $attribute->get_definitions_by_flags($attribute::SHOW_IN_ITEMS);
|
$definition_names = $attribute->get_definitions_by_flags($attribute::SHOW_IN_ITEMS, true);
|
||||||
|
|
||||||
$columns = [
|
$columns = [
|
||||||
'items.item_id' => $item->item_id,
|
'items.item_id' => $item->item_id,
|
||||||
@@ -576,8 +578,8 @@ function item_kit_headers(): array
|
|||||||
['item_kit_number' => lang('Item_kits.item_kit_number')],
|
['item_kit_number' => lang('Item_kits.item_kit_number')],
|
||||||
['name' => lang('Item_kits.name')],
|
['name' => lang('Item_kits.name')],
|
||||||
['description' => lang('Item_kits.description')],
|
['description' => lang('Item_kits.description')],
|
||||||
['total_cost_price' => lang('Items.cost_price'), 'sortable' => FALSE],
|
['total_cost_price' => lang('Items.cost_price'), 'sortable' => false],
|
||||||
['total_unit_price' => lang('Items.unit_price'), 'sortable' => FALSE]
|
['total_unit_price' => lang('Items.unit_price'), 'sortable' => false]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -633,7 +635,7 @@ function parse_attribute_values(array $columns, array $row): array
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $definition_names
|
* @param array $definition_names Array of definition_id => ['name' => name, 'type' => type] or definition_id => name
|
||||||
* @param array $row
|
* @param array $row
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
@@ -650,10 +652,16 @@ function expand_attribute_values(array $definition_names, array $row): array
|
|||||||
}
|
}
|
||||||
|
|
||||||
$attribute_values = [];
|
$attribute_values = [];
|
||||||
foreach ($definition_names as $definition_id => $definition_name) {
|
foreach ($definition_names as $definition_id => $definitionInfo) {
|
||||||
if (isset($indexed_values[$definition_id])) {
|
if (isset($indexed_values[$definition_id])) {
|
||||||
$attribute_value = $indexed_values[$definition_id];
|
$raw_value = $indexed_values[$definition_id];
|
||||||
$attribute_values["$definition_id"] = $attribute_value;
|
|
||||||
|
// Format DECIMAL attributes according to locale
|
||||||
|
if (is_array($definitionInfo) && isset($definitionInfo['type']) && $definitionInfo['type'] === DECIMAL) {
|
||||||
|
$attribute_values["$definition_id"] = to_decimals($raw_value);
|
||||||
|
} else {
|
||||||
|
$attribute_values["$definition_id"] = $raw_value;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$attribute_values["$definition_id"] = "";
|
$attribute_values["$definition_id"] = "";
|
||||||
}
|
}
|
||||||
@@ -735,7 +743,7 @@ function get_expense_category_manage_table_headers(): string
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the html data row for the expenses category
|
* Gets the html data row for the expense category
|
||||||
*/
|
*/
|
||||||
function get_expense_category_data_row(object $expense_category): array
|
function get_expense_category_data_row(object $expense_category): array
|
||||||
{
|
{
|
||||||
@@ -834,7 +842,7 @@ function get_expenses_data_last_row(object $expense): array
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the expenses payments summary
|
* Get the expense payments summary
|
||||||
*/
|
*/
|
||||||
function get_expenses_manage_payments_summary(array $payments, ResultInterface $expenses): string // TODO: $expenses is passed but never used.
|
function get_expenses_manage_payments_summary(array $payments, ResultInterface $expenses): string // TODO: $expenses is passed but never used.
|
||||||
{
|
{
|
||||||
@@ -924,3 +932,24 @@ function get_controller(): string
|
|||||||
$controller_name_parts = explode('\\', $controller_name);
|
$controller_name_parts = explode('\\', $controller_name);
|
||||||
return end($controller_name_parts);
|
return end($controller_name_parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores filter values from the URL query string.
|
||||||
|
*
|
||||||
|
* @param IncomingRequest $request The request object
|
||||||
|
* @return array Array with 'start_date', 'end_date', and 'selected_filters' keys
|
||||||
|
*/
|
||||||
|
function restoreTableFilters(IncomingRequest $request): array
|
||||||
|
{
|
||||||
|
$startDate = $request->getGet('start_date', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||||
|
$endDate = $request->getGet('end_date', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||||
|
$urlFilters = $request->getGet('filters', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||||
|
|
||||||
|
return array_filter([
|
||||||
|
'start_date' => $startDate ?: null,
|
||||||
|
'end_date' => $endDate ?: null,
|
||||||
|
'selected_filters' => $urlFilters ?? []
|
||||||
|
], function ($value) {
|
||||||
|
return $value !== null && $value !== [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -143,8 +143,7 @@ function get_tax_rates_manage_table_headers(): string
|
|||||||
*/
|
*/
|
||||||
function get_tax_rates_data_row($tax_rates_row): array
|
function get_tax_rates_data_row($tax_rates_row): array
|
||||||
{
|
{
|
||||||
$router = service('router');
|
$controller_name = 'taxes';
|
||||||
$controller_name = strtolower($router->controllerName());
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'tax_rate_id' => $tax_rates_row->tax_rate_id,
|
'tax_rate_id' => $tax_rates_row->tax_rate_id,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ if (!function_exists('base64url_encode')) {
|
|||||||
* @param string $data
|
* @param string $data
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
function base64url_encode($data)
|
function base64url_encode(string $data): string
|
||||||
{
|
{
|
||||||
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@ if (!function_exists('base64url_decode')) {
|
|||||||
* @param string $data
|
* @param string $data
|
||||||
* @return string|false
|
* @return string|false
|
||||||
*/
|
*/
|
||||||
function base64url_decode($data)
|
function base64url_decode(string $data): false|string
|
||||||
{
|
{
|
||||||
$remainder = strlen($data) % 4;
|
$remainder = strlen($data) % 4;
|
||||||
if ($remainder) {
|
if ($remainder) {
|
||||||
@@ -28,4 +28,4 @@ if (!function_exists('base64url_decode')) {
|
|||||||
}
|
}
|
||||||
return base64_decode(strtr($data, '-_', '+/'));
|
return base64_decode(strtr($data, '-_', '+/'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ return [
|
|||||||
"confirm_delete" => "هل أنت متأكد من أنك تريد حذف الميزات المحددة ؟",
|
"confirm_delete" => "هل أنت متأكد من أنك تريد حذف الميزات المحددة ؟",
|
||||||
"confirm_restore" => "هل أنت متأكد من أنك تريد استعادة السمة (السمات) المحددة؟",
|
"confirm_restore" => "هل أنت متأكد من أنك تريد استعادة السمة (السمات) المحددة؟",
|
||||||
"definition_cannot_be_deleted" => "لا يمكن حذف السمات المحددة",
|
"definition_cannot_be_deleted" => "لا يمكن حذف السمات المحددة",
|
||||||
|
"definition_invalid_group" => "المجموعة المحددة غير موجودة أو غير صالحة.",
|
||||||
"definition_error_adding_updating" => "لا يمكن إضافة السمة {0} أو تحديثها. يرجى التحقق من سجل الخطأ.",
|
"definition_error_adding_updating" => "لا يمكن إضافة السمة {0} أو تحديثها. يرجى التحقق من سجل الخطأ.",
|
||||||
"definition_flags" => "رؤية الميزات",
|
"definition_flags" => "رؤية الميزات",
|
||||||
"definition_group" => "المجموعة",
|
"definition_group" => "المجموعة",
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"all" => "الجميع",
|
'all' => "الجميع",
|
||||||
"columns" => "أعمدة",
|
'columns' => "أعمدة",
|
||||||
"hide_show_pagination" => "عرض/إخفاء أرقام الصفحات",
|
'hide_show_pagination' => "عرض/إخفاء أرقام الصفحات",
|
||||||
"loading" => "جارى التحميل، برجاء الإنتظار ...",
|
'loading' => "جارى التحميل، برجاء الإنتظار",
|
||||||
"page_from_to" => "عرض {0} إلى {1} من {2} صفوف",
|
'page_from_to' => "عرض {0} إلى {1} من {2} صفوف",
|
||||||
"refresh" => "إعادة تحميل",
|
'refresh' => "إعادة تحميل",
|
||||||
"rows_per_page" => "{0} صف بالصفحة",
|
'rows_per_page' => "{0} صف بالصفحة",
|
||||||
"toggle" => "تغيير",
|
'toggle' => "تغيير",
|
||||||
];
|
];
|
||||||
|
|||||||
49
app/Language/ar-EG/Calendar.php
Normal file
49
app/Language/ar-EG/Calendar.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
"su" => "أحد",
|
||||||
|
"mo" => "اثنين",
|
||||||
|
"tu" => "ثلاثاء",
|
||||||
|
"we" => "أربعاء",
|
||||||
|
"th" => "خميس",
|
||||||
|
"fr" => "جمعة",
|
||||||
|
"sa" => "سبت",
|
||||||
|
"sun" => "الأحد",
|
||||||
|
"mon" => "الاثنين",
|
||||||
|
"tue" => "الثلاثاء",
|
||||||
|
"wed" => "الأربعاء",
|
||||||
|
"thu" => "الخميس",
|
||||||
|
"fri" => "الجمعة",
|
||||||
|
"sat" => "السبت",
|
||||||
|
"sunday" => "الأحد",
|
||||||
|
"monday" => "الاثنين",
|
||||||
|
"tuesday" => "الثلاثاء",
|
||||||
|
"wednesday" => "الأربعاء",
|
||||||
|
"thursday" => "الخميس",
|
||||||
|
"friday" => "الجمعة",
|
||||||
|
"saturday" => "السبت",
|
||||||
|
"jan" => "يناير",
|
||||||
|
"feb" => "فبراير",
|
||||||
|
"mar" => "مارس",
|
||||||
|
"apr" => "أبريل",
|
||||||
|
"may" => "مايو",
|
||||||
|
"jun" => "يونيو",
|
||||||
|
"jul" => "يوليو",
|
||||||
|
"aug" => "أغسطس",
|
||||||
|
"sep" => "سبتمبر",
|
||||||
|
"oct" => "أكتوبر",
|
||||||
|
"nov" => "نوفمبر",
|
||||||
|
"dec" => "ديسمبر",
|
||||||
|
"january" => "يناير",
|
||||||
|
"february" => "فبراير",
|
||||||
|
"march" => "مارس",
|
||||||
|
"april" => "أبريل",
|
||||||
|
"mayl" => "مايو",
|
||||||
|
"june" => "يونيو",
|
||||||
|
"july" => "يوليو",
|
||||||
|
"august" => "أغسطس",
|
||||||
|
"september" => "سبتمبر",
|
||||||
|
"october" => "أكتوبر",
|
||||||
|
"november" => "نوفمبر",
|
||||||
|
"december" => "ديسمبر",
|
||||||
|
];
|
||||||
@@ -282,6 +282,7 @@ return [
|
|||||||
"right" => "يمين",
|
"right" => "يمين",
|
||||||
"sales_invoice_format" => "شكل فاتورة البيع",
|
"sales_invoice_format" => "شكل فاتورة البيع",
|
||||||
"sales_quote_format" => "شكل فاتورة عرض الاسعار",
|
"sales_quote_format" => "شكل فاتورة عرض الاسعار",
|
||||||
|
"mailpath_invalid" => "",
|
||||||
"saved_successfully" => "تم حفظ التهيئة بنجاح.",
|
"saved_successfully" => "تم حفظ التهيئة بنجاح.",
|
||||||
"saved_unsuccessfully" => "لم يتم حفظ التهيئة بنجاح.",
|
"saved_unsuccessfully" => "لم يتم حفظ التهيئة بنجاح.",
|
||||||
"security_issue" => "تحذير من ثغرة أمنية",
|
"security_issue" => "تحذير من ثغرة أمنية",
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ return [
|
|||||||
"current_password_invalid" => "كلمة المرور الحالية غير صحيحة.",
|
"current_password_invalid" => "كلمة المرور الحالية غير صحيحة.",
|
||||||
"employee" => "موظف",
|
"employee" => "موظف",
|
||||||
"error_adding_updating" => "خطاء فى إضافة/تعديل موظف.",
|
"error_adding_updating" => "خطاء فى إضافة/تعديل موظف.",
|
||||||
|
"error_deleting_admin" => "",
|
||||||
|
"error_updating_admin" => "",
|
||||||
"error_deleting_demo_admin" => "لايمكن حذف المستخدم admin الخاص بنسخة العرض.",
|
"error_deleting_demo_admin" => "لايمكن حذف المستخدم admin الخاص بنسخة العرض.",
|
||||||
"error_updating_demo_admin" => "لايمكن تغيير بيانات المستخدم admin الخاص بنسخة العرض.",
|
"error_updating_demo_admin" => "لايمكن تغيير بيانات المستخدم admin الخاص بنسخة العرض.",
|
||||||
"language" => "اللغة",
|
"language" => "اللغة",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ return [
|
|||||||
"cost_price_required" => "سعر التكلفة مطلوب.",
|
"cost_price_required" => "سعر التكلفة مطلوب.",
|
||||||
"count" => "تحديث المخزون",
|
"count" => "تحديث المخزون",
|
||||||
"csv_import_failed" => "فشل الإستيراد من اكسل",
|
"csv_import_failed" => "فشل الإستيراد من اكسل",
|
||||||
|
"csv_import_invalid_location" => "",
|
||||||
"csv_import_nodata_wrongformat" => "الملف الذى رفعته إما فارغ أو أنه مختلف البنية.",
|
"csv_import_nodata_wrongformat" => "الملف الذى رفعته إما فارغ أو أنه مختلف البنية.",
|
||||||
"csv_import_partially_failed" => "يوجد خطأ بنسبة {0} في استيراد الاصناف في السطر: {1}. لم يتم استيرادهم.",
|
"csv_import_partially_failed" => "يوجد خطأ بنسبة {0} في استيراد الاصناف في السطر: {1}. لم يتم استيرادهم.",
|
||||||
"csv_import_success" => "تم استيراد الأصناف بنجاح.",
|
"csv_import_success" => "تم استيراد الأصناف بنجاح.",
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ return [
|
|||||||
"login" => "دخول",
|
"login" => "دخول",
|
||||||
"logout" => "تسجيل خروج",
|
"logout" => "تسجيل خروج",
|
||||||
"migration_needed" => "سيبدأ ترحيل قاعدة البيانات إلى{0} بعد تسجيل الدخول.",
|
"migration_needed" => "سيبدأ ترحيل قاعدة البيانات إلى{0} بعد تسجيل الدخول.",
|
||||||
|
"migration_required" => "",
|
||||||
|
"migration_auth_message" => "",
|
||||||
|
"migration_initializing" => "",
|
||||||
|
"migration_running" => "",
|
||||||
|
"migration_complete" => "",
|
||||||
|
"migration_complete_login" => "",
|
||||||
|
"migration_failed" => "",
|
||||||
|
"migration_error_connection" => "",
|
||||||
|
"migration_complete_redirect" => "",
|
||||||
"password" => "كلمة السر",
|
"password" => "كلمة السر",
|
||||||
"required_username" => "",
|
"required_username" => "",
|
||||||
"username" => "اسم المستخدم",
|
"username" => "اسم المستخدم",
|
||||||
|
|||||||
@@ -73,6 +73,12 @@ return [
|
|||||||
"employee" => "الموظف",
|
"employee" => "الموظف",
|
||||||
"entry" => "ادخال",
|
"entry" => "ادخال",
|
||||||
"error_editing_item" => "خطاء فى تحرير الصنف",
|
"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" => "بحث/مسح باركود صنف",
|
||||||
"find_or_scan_item_or_receipt" => "بحث/مسح باركود صنف أو ايصال",
|
"find_or_scan_item_or_receipt" => "بحث/مسح باركود صنف أو ايصال",
|
||||||
"giftcard" => "بطاقة هدية",
|
"giftcard" => "بطاقة هدية",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ return [
|
|||||||
"confirm_delete" => "هل أنت متأكد من أنك تريد حذف الميزات المحددة ؟",
|
"confirm_delete" => "هل أنت متأكد من أنك تريد حذف الميزات المحددة ؟",
|
||||||
"confirm_restore" => "هل أنت متأكد من أنك تريد استعادة السمة (السمات) المحددة؟",
|
"confirm_restore" => "هل أنت متأكد من أنك تريد استعادة السمة (السمات) المحددة؟",
|
||||||
"definition_cannot_be_deleted" => "لا يمكن حذف السمات المحددة",
|
"definition_cannot_be_deleted" => "لا يمكن حذف السمات المحددة",
|
||||||
|
"definition_invalid_group" => "المجموعة المحددة غير موجودة أو غير صالحة.",
|
||||||
"definition_error_adding_updating" => "لا يمكن إضافة السمة {0} أو تحديثها. يرجى التحقق من سجل الخطأ.",
|
"definition_error_adding_updating" => "لا يمكن إضافة السمة {0} أو تحديثها. يرجى التحقق من سجل الخطأ.",
|
||||||
"definition_flags" => "رؤية الميزات",
|
"definition_flags" => "رؤية الميزات",
|
||||||
"definition_group" => "المجموعة",
|
"definition_group" => "المجموعة",
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"all" => "الكل",
|
'all' => "الكل",
|
||||||
"columns" => "أعمدة",
|
'columns' => "أعمدة",
|
||||||
"hide_show_pagination" => "عرض/إخفاء أرقام الصفحات",
|
'hide_show_pagination' => "عرض/إخفاء أرقام الصفحات",
|
||||||
"loading" => "جارى التحميل، برجاء الإنتظار ...",
|
'loading' => "جارى التحميل، برجاء الإنتظار",
|
||||||
"page_from_to" => "عرض {0} إلى {1} من {2} صفوف",
|
'page_from_to' => "عرض {0} إلى {1} من {2} صفوف",
|
||||||
"refresh" => "إعادة تحميل",
|
'refresh' => "إعادة تحميل",
|
||||||
"rows_per_page" => "{0} صف بالصفحة",
|
'rows_per_page' => "{0} صف بالصفحة",
|
||||||
"toggle" => "تغيير",
|
'toggle' => "تغيير",
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -282,6 +282,7 @@ return [
|
|||||||
"right" => "يمين",
|
"right" => "يمين",
|
||||||
"sales_invoice_format" => "شكل فاتورة البيع",
|
"sales_invoice_format" => "شكل فاتورة البيع",
|
||||||
"sales_quote_format" => "شكل فاتورة عرض الاسعار",
|
"sales_quote_format" => "شكل فاتورة عرض الاسعار",
|
||||||
|
"mailpath_invalid" => "",
|
||||||
"saved_successfully" => "تم حفظ التهيئة بنجاح.",
|
"saved_successfully" => "تم حفظ التهيئة بنجاح.",
|
||||||
"saved_unsuccessfully" => "لم يتم حفظ التهيئة بنجاح.",
|
"saved_unsuccessfully" => "لم يتم حفظ التهيئة بنجاح.",
|
||||||
"security_issue" => "تحذير من ثغرة أمنية",
|
"security_issue" => "تحذير من ثغرة أمنية",
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ return [
|
|||||||
"current_password_invalid" => "كلمة المرور الحالية غير صحيحة.",
|
"current_password_invalid" => "كلمة المرور الحالية غير صحيحة.",
|
||||||
"employee" => "موظف",
|
"employee" => "موظف",
|
||||||
"error_adding_updating" => "خطاء فى إضافة/تعديل موظف.",
|
"error_adding_updating" => "خطاء فى إضافة/تعديل موظف.",
|
||||||
|
"error_deleting_admin" => "",
|
||||||
|
"error_updating_admin" => "",
|
||||||
"error_deleting_demo_admin" => "لايمكن حذف المستخدم admin الخاص بنسخة العرض.",
|
"error_deleting_demo_admin" => "لايمكن حذف المستخدم admin الخاص بنسخة العرض.",
|
||||||
"error_updating_demo_admin" => "لايمكن تغيير بيانات المستخدم admin الخاص بنسخة العرض.",
|
"error_updating_demo_admin" => "لايمكن تغيير بيانات المستخدم admin الخاص بنسخة العرض.",
|
||||||
"language" => "اللغة",
|
"language" => "اللغة",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ return [
|
|||||||
"cost_price_required" => "سعر التكلفة مطلوب.",
|
"cost_price_required" => "سعر التكلفة مطلوب.",
|
||||||
"count" => "تحديث المخزون",
|
"count" => "تحديث المخزون",
|
||||||
"csv_import_failed" => "فشل الإستيراد من اكسل",
|
"csv_import_failed" => "فشل الإستيراد من اكسل",
|
||||||
|
"csv_import_invalid_location" => "",
|
||||||
"csv_import_nodata_wrongformat" => "الملف الذى رفعته إما فارغ أو أنه مختلف البنية.",
|
"csv_import_nodata_wrongformat" => "الملف الذى رفعته إما فارغ أو أنه مختلف البنية.",
|
||||||
"csv_import_partially_failed" => "يوجد خطأ بنسبة {0} في استيراد الاصناف في السطر: {1}. لم يتم استيرادهم.",
|
"csv_import_partially_failed" => "يوجد خطأ بنسبة {0} في استيراد الاصناف في السطر: {1}. لم يتم استيرادهم.",
|
||||||
"csv_import_success" => "تم استيراد الأصناف بنجاح.",
|
"csv_import_success" => "تم استيراد الأصناف بنجاح.",
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ return [
|
|||||||
"login" => "دخول",
|
"login" => "دخول",
|
||||||
"logout" => "تسجيل خروج",
|
"logout" => "تسجيل خروج",
|
||||||
"migration_needed" => "سيبدأ ترحيل قاعدة البيانات إلى{0} بعد تسجيل الدخول.",
|
"migration_needed" => "سيبدأ ترحيل قاعدة البيانات إلى{0} بعد تسجيل الدخول.",
|
||||||
|
"migration_required" => "",
|
||||||
|
"migration_auth_message" => "",
|
||||||
|
"migration_initializing" => "",
|
||||||
|
"migration_running" => "",
|
||||||
|
"migration_complete" => "",
|
||||||
|
"migration_complete_login" => "",
|
||||||
|
"migration_failed" => "",
|
||||||
|
"migration_error_connection" => "",
|
||||||
|
"migration_complete_redirect" => "",
|
||||||
"password" => "كلمة السر",
|
"password" => "كلمة السر",
|
||||||
"required_username" => "خانة أسم المستخدم مطلوبة.",
|
"required_username" => "خانة أسم المستخدم مطلوبة.",
|
||||||
"username" => "اسم المستخدم",
|
"username" => "اسم المستخدم",
|
||||||
|
|||||||
@@ -73,6 +73,12 @@ return [
|
|||||||
"employee" => "الموظف",
|
"employee" => "الموظف",
|
||||||
"entry" => "ادخال",
|
"entry" => "ادخال",
|
||||||
"error_editing_item" => "خطاء فى تعديل المادة",
|
"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" => "بحث/مسح باركود المادة",
|
||||||
"find_or_scan_item_or_receipt" => "بحث/مسح باركود المادة أو الايصال",
|
"find_or_scan_item_or_receipt" => "بحث/مسح باركود المادة أو الايصال",
|
||||||
"giftcard" => "بطاقة هدية",
|
"giftcard" => "بطاقة هدية",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ return [
|
|||||||
"confirm_delete" => "Seçilmiş Atributları silmək istədiyinizdən əminsinizmi?",
|
"confirm_delete" => "Seçilmiş Atributları silmək istədiyinizdən əminsinizmi?",
|
||||||
"confirm_restore" => "Seçilmiş atributları bərpa etmək istədiyinizə əminsinizmi?",
|
"confirm_restore" => "Seçilmiş atributları bərpa etmək istədiyinizə əminsinizmi?",
|
||||||
"definition_cannot_be_deleted" => "Seçilmiş xüsusiyyətləri silmək olmadı",
|
"definition_cannot_be_deleted" => "Seçilmiş xüsusiyyətləri silmək olmadı",
|
||||||
|
"definition_invalid_group" => "",
|
||||||
"definition_error_adding_updating" => "{0} -in atributları əlavə oluna və yenilənə bilmədi. Lütfən XƏTA loq faylını yoxlayın.",
|
"definition_error_adding_updating" => "{0} -in atributları əlavə oluna və yenilənə bilmədi. Lütfən XƏTA loq faylını yoxlayın.",
|
||||||
"definition_flags" => "Atribut görünüşü",
|
"definition_flags" => "Atribut görünüşü",
|
||||||
"definition_group" => "Qrup",
|
"definition_group" => "Qrup",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user