mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-05-25 16:54:58 -04:00
Compare commits
4 Commits
person_att
...
feature/in
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
302a76b84a | ||
|
|
2725c6e872 | ||
|
|
7cd2d3e61f | ||
|
|
3c5f4c1465 |
@@ -1,56 +1,23 @@
|
||||
# Version control
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Sensitive config (user may mount their own)
|
||||
node_modules
|
||||
tmp
|
||||
app/Config/Email.php
|
||||
|
||||
# Build artifacts
|
||||
node_modules/
|
||||
dist/
|
||||
tmp/
|
||||
*.patch
|
||||
patches/
|
||||
|
||||
# IDE and editor files
|
||||
.idea/
|
||||
.vscode/
|
||||
git-svn-diff.py
|
||||
*.bash
|
||||
.swp
|
||||
*.swp
|
||||
.buildpath
|
||||
.project
|
||||
.settings/
|
||||
|
||||
# Development tools and configs
|
||||
tests/
|
||||
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
|
||||
.settings/*
|
||||
.git
|
||||
dist/
|
||||
node_modules/
|
||||
*.swp
|
||||
*.rej
|
||||
*.orig
|
||||
*~
|
||||
*.~
|
||||
*.log
|
||||
|
||||
# CI
|
||||
.github/
|
||||
.github/workflows/
|
||||
build/
|
||||
app/writable/session/*
|
||||
!app/writable/session/index.html
|
||||
|
||||
18
.env.example
18
.env.example
@@ -4,24 +4,6 @@
|
||||
|
||||
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
|
||||
#--------------------------------------------------------------------
|
||||
|
||||
308
.github/ISSUE_TEMPLATE/bug report.yml
vendored
308
.github/ISSUE_TEMPLATE/bug report.yml
vendored
@@ -1,187 +1,121 @@
|
||||
name: 🐛 Bug Report
|
||||
description: File a bug report to help us improve
|
||||
title: "[Bug]: "
|
||||
labels: ["bug", "triage"]
|
||||
projects: ["ospos/3", "ospos/4"]
|
||||
assignees: []
|
||||
body:
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# INTRODUCTION
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Thanks for taking the time to fill out this bug report! 🐜
|
||||
|
||||
Bug reports help us identify and fix issues. Please provide as much detail as possible.
|
||||
|
||||
> ⚠️ **Important:** Submit a separate bug report for each problem you encounter.
|
||||
>
|
||||
> 🚫 Do not include personal identifying information such as email addresses or encryption keys.
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# PROBLEM DESCRIPTION
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
- type: textarea
|
||||
id: bug-description
|
||||
attributes:
|
||||
label: 🐛 Bug Description
|
||||
description: A clear and concise description of what the bug is.
|
||||
placeholder: |
|
||||
Example: When I try to print a receipt, the application crashes
|
||||
with an error message saying "Unable to connect to printer".
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: steps-reproduce
|
||||
attributes:
|
||||
label: 📋 Steps to Reproduce
|
||||
description: Detailed steps to reproduce the behavior.
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
3. Scroll down to '...'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: ✅ Expected Behavior
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
placeholder: |
|
||||
Example: The receipt should print successfully without any errors.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# ENVIRONMENT DETAILS
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
- type: dropdown
|
||||
id: ospos-version
|
||||
attributes:
|
||||
label: 📦 OpenSourcePOS Version
|
||||
description: What version of our software are you running?
|
||||
options:
|
||||
- development (unreleased)
|
||||
- OpenSourcePOS 3.4.2
|
||||
- OpenSourcePOS 3.4.1
|
||||
- OpenSourcePOS 3.4.0
|
||||
- OpenSourcePOS 3.3.9
|
||||
- OpenSourcePOS 3.3.8
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: php-version
|
||||
attributes:
|
||||
label: 🔧 PHP Version
|
||||
description: What version of PHP are you running?
|
||||
options:
|
||||
- PHP 8.4
|
||||
- PHP 8.3
|
||||
- PHP 8.2
|
||||
- PHP 8.1
|
||||
- PHP 7.4
|
||||
- Other
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: 🌐 Browser(s)
|
||||
description: What browser(s) are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Firefox
|
||||
- Chrome
|
||||
- Safari
|
||||
- Microsoft Edge
|
||||
- Other
|
||||
|
||||
- type: input
|
||||
id: server
|
||||
attributes:
|
||||
label: 🖥️ Server Operating System
|
||||
description: What server OS and version are you running?
|
||||
placeholder: "e.g., Ubuntu 22.04, CentOS 7, Windows Server 2022"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: database
|
||||
attributes:
|
||||
label: 🗄️ Database
|
||||
description: What database management system and version are you using?
|
||||
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
|
||||
name: Bug Report
|
||||
description: File a bug report
|
||||
title: "[Bug]: "
|
||||
labels: ["bug", "triage"]
|
||||
projects: ["ospos/3", "ospos/4"]
|
||||
assignees:
|
||||
- none
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Bug reports indicate that something is not working as intended.
|
||||
Please include as much detail as possible and submit a separate bug report for each problem.
|
||||
Do not include personal identifying information such as email addresses or encryption keys.
|
||||
- type: textarea
|
||||
id: bug-description
|
||||
attributes:
|
||||
label: Bug Description?
|
||||
description: Describe the problem that you are seeing
|
||||
placeholder: "Describe the problem that you are seeing"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps-reproduce
|
||||
attributes:
|
||||
label: Steps to Reproduce?
|
||||
description: List the steps to reproduce this issue
|
||||
placeholder: "Steps to Reproduce"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected Behavior?
|
||||
description: Tell us what did you expect to happen?
|
||||
placeholder: "Expected Behavior"
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: ospos-version
|
||||
attributes:
|
||||
label: OpensourcePOS Version
|
||||
description: What version of our software are you running?
|
||||
options:
|
||||
- development (unreleased)
|
||||
- opensourcepos 3.4.1
|
||||
- opensourcepos 3.4.0
|
||||
- opensourcepos 3.3.9
|
||||
- opensourcepos 3.3.8
|
||||
- opensourcepos 3.3.7
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: php-version
|
||||
attributes:
|
||||
label: Php version
|
||||
description: What version of Php?
|
||||
options:
|
||||
- Php 7.2
|
||||
- Php 7.3
|
||||
- Php 7.4
|
||||
- Php 8.1
|
||||
- Php 8.2
|
||||
- Php 8.3
|
||||
- Php 8.4
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browsers
|
||||
attributes:
|
||||
label: What browsers are you seeing the problem on?
|
||||
multiple: true
|
||||
options:
|
||||
- Firefox
|
||||
- Chrome
|
||||
- Safari
|
||||
- Microsoft Edge
|
||||
- Other
|
||||
- type: input
|
||||
id: server
|
||||
attributes:
|
||||
label: Server Operating System and version
|
||||
description: "Server Operating System "
|
||||
placeholder: "Server Operating System "
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: database
|
||||
attributes:
|
||||
label: Database Management System and version
|
||||
description: "Database Management System"
|
||||
placeholder: "Database Management"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: webserver
|
||||
attributes:
|
||||
label: Web Server and version
|
||||
description: "Web Server and version "
|
||||
placeholder: "Web Server and version "
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: servers
|
||||
attributes:
|
||||
label: System Information Report (optional)
|
||||
description: Copy and paste from OSPOS > Configuration > Setup & Conf > Setup & Conf?
|
||||
placeholder: System Information Report
|
||||
value: "System Information Report"
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Unmodified copy of OpensourcePOS
|
||||
description: By submitting this issue you agree this copy has not been modified
|
||||
options:
|
||||
- label: I agree this copy has not been modified
|
||||
required: true
|
||||
|
||||
199
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
199
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,136 +1,63 @@
|
||||
name: ✨ Feature Request
|
||||
description: Suggest an idea or enhancement for this project
|
||||
title: "[Feature]: "
|
||||
labels: ["enhancement"]
|
||||
assignees: []
|
||||
body:
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# INTRODUCTION
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Thanks for suggesting a new feature! 💡
|
||||
|
||||
We appreciate you taking the time to help improve OpenSourcePOS.
|
||||
|
||||
> 📋 **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.
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# FEATURE DETAILS
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
- type: dropdown
|
||||
id: feature-type
|
||||
attributes:
|
||||
label: 🏷️ Feature Type
|
||||
description: What type of feature are you requesting?
|
||||
options:
|
||||
- "✨ New Feature"
|
||||
- "📝 Documentation Improvement"
|
||||
- "🎨 UI/UX Enhancement"
|
||||
- "🔨 Code Refactoring"
|
||||
- "⚡ Performance Improvement"
|
||||
- "✅ New Test Coverage"
|
||||
- "🔌 Plugin/Integration"
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: ospos-version
|
||||
attributes:
|
||||
label: 📦 OpenSourcePOS Version
|
||||
description: What version are you currently running?
|
||||
options:
|
||||
- development (unreleased)
|
||||
- OpenSourcePOS 3.4.2
|
||||
- OpenSourcePOS 3.4.1
|
||||
- OpenSourcePOS 3.4.0
|
||||
- OpenSourcePOS 3.3.9
|
||||
- OpenSourcePOS 3.3.8
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem-statement
|
||||
attributes:
|
||||
label: 🎯 Problem Statement
|
||||
description: |
|
||||
Is your feature request related to a problem? Please describe.
|
||||
|
||||
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
|
||||
name: ✨ Feature Request
|
||||
description: Suggest an idea for this project
|
||||
title: "[Feature]: "
|
||||
labels: ["enhancement"]
|
||||
assignees: ["none"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this feature request! 🤗
|
||||
Please make sure this feature request hasn't been already submitted by someone by looking through other open/closed issues. 😃
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
multiple: false
|
||||
label: Type of Feature
|
||||
description: Select the type of feature request.
|
||||
options:
|
||||
- "✨ New Feature"
|
||||
- "📝 Documentation"
|
||||
- "🎨 Style and UI"
|
||||
- "🔨 Code Refactor"
|
||||
- "⚡ Performance Improvements"
|
||||
- "✅ New Test"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: ospos-version
|
||||
attributes:
|
||||
label: OpensourcePOS Version
|
||||
description: What version of our software are you running?
|
||||
options:
|
||||
- opensourcepos 3.3.9
|
||||
- opensourcepos 3.3.8
|
||||
- opensourcepos 3.3.7
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Give us a brief description of the feature or enhancement you would like
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional-information
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: Give us some additional information on the feature request like proposed solutions, links, screenshots, etc.
|
||||
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: Verify you searched open requests in OpensourcePOS
|
||||
description: By submitting this request you agree that you have searched Open Requests in the Tracker
|
||||
options:
|
||||
- label: I agree I have searched Open Requests
|
||||
required: true
|
||||
|
||||
|
||||
61
.github/workflows/README.md
vendored
61
.github/workflows/README.md
vendored
@@ -1,61 +0,0 @@
|
||||
# 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
214
.github/workflows/build-release.yml
vendored
@@ -1,214 +0,0 @@
|
||||
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
Normal file
71
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
# 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
|
||||
144
.github/workflows/integration-tests.yml
vendored
Normal file
144
.github/workflows/integration-tests.yml
vendored
Normal file
@@ -0,0 +1,144 @@
|
||||
name: Integration Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, main ]
|
||||
paths:
|
||||
- 'app/**'
|
||||
- 'public/**'
|
||||
- 'docker/**'
|
||||
- 'docker-compose*.yml'
|
||||
- 'tests/**'
|
||||
- 'integration-tests/**'
|
||||
- '.github/workflows/integration-tests.yml'
|
||||
pull_request:
|
||||
branches: [ master, main ]
|
||||
paths:
|
||||
- 'app/**'
|
||||
- 'public/**'
|
||||
- 'docker/**'
|
||||
- 'docker-compose*.yml'
|
||||
- 'tests/**'
|
||||
- 'integration-tests/**'
|
||||
- '.github/workflows/integration-tests.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
integration:
|
||||
name: Docker Integration Tests
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run Basic Integration Tests
|
||||
run: chmod +x integration-tests/run-integration-tests.sh && cd integration-tests && ./run-integration-tests.sh
|
||||
|
||||
- name: View Logs on Failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== Application Logs ==="
|
||||
docker logs opensourcepos-integration-tests-ospos-1
|
||||
echo ""
|
||||
echo "=== Database Logs ==="
|
||||
docker logs mysql || docker logs opensourcepos-mysql || echo "No database logs found"
|
||||
|
||||
- name: Stop Docker Stack
|
||||
if: always()
|
||||
run: docker compose down -v || true
|
||||
|
||||
playwright:
|
||||
name: Playwright E2E Tests
|
||||
runs-on: ubuntu-22.04
|
||||
needs: integration
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps chromium firefox
|
||||
|
||||
- name: Start Docker Stack
|
||||
run: docker compose up -d
|
||||
|
||||
- name: Wait for Application
|
||||
run: |
|
||||
echo "Waiting for application to be ready..."
|
||||
timeout 90 bash -c 'until curl -s -f http://localhost/ > /dev/null; do sleep 2; done'
|
||||
echo "Application is ready!"
|
||||
|
||||
- name: Run Playwright Tests
|
||||
run: npm run test
|
||||
env:
|
||||
BASE_URL: http://localhost
|
||||
|
||||
- name: Upload Playwright Report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report
|
||||
path: integration-tests/playwright-report/
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload Test Results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-results
|
||||
path: integration-tests/test-results/
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload Screenshots on Failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-screenshots
|
||||
path: integration-tests/test-results/**/*.png
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload Trace Files on Failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-traces
|
||||
path: integration-tests/test-results/**/*.zip
|
||||
retention-days: 7
|
||||
|
||||
- name: View Logs on Failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== Application Logs ==="
|
||||
docker logs opensourcepos-integration-tests-ospos-1
|
||||
echo ""
|
||||
echo "=== Database Logs ==="
|
||||
docker logs mysql || docker logs opensourcepos-mysql || echo "No database logs found"
|
||||
|
||||
- name: Stop Docker Stack
|
||||
if: always()
|
||||
run: docker compose down -v || true
|
||||
1
.github/workflows/main.yml
vendored
1
.github/workflows/main.yml
vendored
@@ -28,6 +28,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php-version:
|
||||
- '8.1'
|
||||
- '8.2'
|
||||
- '8.3'
|
||||
- '8.4'
|
||||
|
||||
33
.github/workflows/opencode.yml
vendored
Normal file
33
.github/workflows/opencode.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
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,6 +12,14 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
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
|
||||
uses: dbfx/github-phplint/8.2@master
|
||||
with:
|
||||
|
||||
7
.github/workflows/phpunit.yml
vendored
7
.github/workflows/phpunit.yml
vendored
@@ -34,6 +34,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
php-version:
|
||||
- '8.1'
|
||||
- '8.2'
|
||||
- '8.3'
|
||||
- '8.4'
|
||||
@@ -68,6 +69,9 @@ jobs:
|
||||
- name: Install npm dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Build database.sql
|
||||
run: npm run gulp build-database
|
||||
|
||||
- name: Start MariaDB
|
||||
run: |
|
||||
docker run -d --name mysql \
|
||||
@@ -75,6 +79,7 @@ jobs:
|
||||
-e MYSQL_DATABASE=ospos \
|
||||
-e MYSQL_USER=admin \
|
||||
-e MYSQL_PASSWORD=pointofsale \
|
||||
-v $PWD/app/Database/database.sql:/docker-entrypoint-initdb.d/database.sql \
|
||||
-p 3306:3306 \
|
||||
mariadb:10.5
|
||||
# Wait for MariaDB to be ready
|
||||
@@ -118,4 +123,4 @@ jobs:
|
||||
|
||||
- name: Stop MariaDB
|
||||
if: always()
|
||||
run: docker stop mysql && docker rm mysql
|
||||
run: docker stop mysql && docker rm mysql
|
||||
172
.github/workflows/release.yml
vendored
172
.github/workflows/release.yml
vendored
@@ -1,172 +0,0 @@
|
||||
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
Normal file
54
.travis.yml
Normal file
@@ -0,0 +1,54 @@
|
||||
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
40
AGENTS.md
@@ -1,40 +0,0 @@
|
||||
# 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,22 +1,28 @@
|
||||
FROM php:8.2-apache AS ospos
|
||||
LABEL maintainer="jekkos"
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libicu-dev \
|
||||
libgd-dev \
|
||||
&& docker-php-ext-install mysqli bcmath intl gd \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& a2enmod rewrite
|
||||
|
||||
RUN apt update && apt-get install -y libicu-dev libgd-dev
|
||||
RUN a2enmod rewrite
|
||||
RUN docker-php-ext-install mysqli bcmath intl gd
|
||||
RUN echo "date.timezone = \"\${PHP_TIMEZONE}\"" > /usr/local/etc/php/conf.d/timezone.ini
|
||||
|
||||
WORKDIR /app
|
||||
COPY --chown=www-data:www-data . /app
|
||||
RUN chmod 770 /app/writable/uploads /app/writable/logs /app/writable/cache \
|
||||
&& ln -s /app/*[^public] /var/www \
|
||||
&& rm -rf /var/www/html \
|
||||
&& ln -nsf /app/public /var/www/html
|
||||
COPY . /app
|
||||
RUN ln -s /app/*[^public] /var/www && rm -rf /var/www/html && ln -nsf /app/public /var/www/html
|
||||
RUN chmod -R 770 /app/writable/uploads /app/writable/logs /app/writable/cache && chown -R www-data:www-data /app
|
||||
|
||||
FROM ospos AS ospos_test
|
||||
|
||||
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
|
||||
|
||||
|
||||
55
INSTALL.md
55
INSTALL.md
@@ -1,68 +1,27 @@
|
||||
## Server Requirements
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- 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).
|
||||
- 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.
|
||||
|
||||
## 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
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
3. 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` 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.
|
||||
3. Execute the file `app/Database/database.sql` to create the tables needed.
|
||||
4. Unzip and upload Open Source Point of Sale files to the web-server.
|
||||
5. Open `.env` file and modify credentials to connect to your database if needed. (First copy .env.example to .env and update)
|
||||
7. Go to your install `public` dir via the browser.
|
||||
8. Log in using
|
||||
- Username: admin
|
||||
- Password: pointofsale
|
||||
9. If everything works, then set the `CI_ENVIRONMENT` variable to `production` in the .env file
|
||||
10. Enjoy!
|
||||
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.
|
||||
9. 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.
|
||||
|
||||
## Local install using Docker
|
||||
|
||||
|
||||
12
README.md
12
README.md
@@ -8,7 +8,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<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.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://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://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).
|
||||
|
||||
- PHP `≥ 8.2` is required to run this app.
|
||||
- PHP `≥ 8.1` is required to run this app.
|
||||
|
||||
## 🏃 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. Please star the project if you like it!
|
||||
If you like our project, please consider buying us a coffee through the button below so we can keep adding features.
|
||||
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=MUN6AEG7NY6H8)\
|
||||
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
|
||||
|
||||
| <div align="center">DigitalOcean</div> | <div align="center">JetBrains</div> | <div align="center">GitHub</div> |
|
||||
| <div align="center">DigitalOcean</div> | <div align="center">JetBrains</div> | <div align="center">Travis CI</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 [GitHub](https://github.com) for providing free continuous integration via GitHub Actions for open-source projects. |
|
||||
| <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> |
|
||||
| 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. |
|
||||
|
||||
37
SECURITY.md
37
SECURITY.md
@@ -1,9 +1,9 @@
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
|
||||
|
||||
- [Security Policy](#security-policy)
|
||||
- [Supported Versions](#supported-versions)
|
||||
- [Security Advisories](#security-advisories)
|
||||
- [Reporting a Vulnerability](#reporting-a-vulnerability)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
@@ -12,35 +12,14 @@
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We release patches for security vulnerabilities.
|
||||
We release patches for security vulnerabilities. Which versions are eligible to receive such patches depend on the CVSS v3.0 Rating:
|
||||
|
||||
| Version | Supported |
|
||||
| --------- | ------------------ |
|
||||
| >= 3.4.2 | :white_check_mark: |
|
||||
| < 3.4.2 | :x: |
|
||||
|
||||
## 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).
|
||||
| CVSS v3.0 | Supported Versions |
|
||||
| --------- | -------------------------------------------------- |
|
||||
| 7.3 | 3.3.5 |
|
||||
| 9.8 | 3.3.6 |
|
||||
| 6.8 | 3.4.2 |
|
||||
|
||||
## 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!!
|
||||
|
|
||||
*/
|
||||
public string $permittedURIChars = 'a-z 0-9~%.:_\-';
|
||||
public string $permittedURIChars = 'a-z 0-9~%.:_\-=';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
@@ -278,79 +278,14 @@ class App extends BaseConfig
|
||||
* @see http://www.html5rocks.com/en/tutorials/security/content-security-policy/
|
||||
* @see http://www.w3.org/TR/CSP/
|
||||
*/
|
||||
public bool $CSPEnabled = false;
|
||||
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 function __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');
|
||||
|
||||
$host = $this->getValidHost();
|
||||
$this->baseURL = $this->https_on ? 'https' : 'http';
|
||||
$this->baseURL .= '://' . $host . '/';
|
||||
$this->baseURL .= '://' . ((isset($_SERVER['HTTP_HOST'])) ? $_SERVER['HTTP_HOST'] : 'localhost') . '/';
|
||||
$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,6 +17,8 @@ use CodeIgniter\Config\AutoloadConfig;
|
||||
*
|
||||
* NOTE: This class is required prior to Autoloader instantiation,
|
||||
* and does not extend BaseConfig.
|
||||
*
|
||||
* @immutable
|
||||
*/
|
||||
class Autoload extends AutoloadConfig
|
||||
{
|
||||
|
||||
@@ -1,38 +1,23 @@
|
||||
<?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);
|
||||
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);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| DEBUG MODE
|
||||
| DEBUG MODE
|
||||
|--------------------------------------------------------------------------
|
||||
| Debug mode is an experimental flag that can allow changes throughout
|
||||
| the system. It's not widely used currently, and may not survive
|
||||
| release of the framework.
|
||||
*/
|
||||
defined('CI_DEBUG') || define('CI_DEBUG', true);
|
||||
*/
|
||||
defined('CI_DEBUG') || define('CI_DEBUG', true);
|
||||
@@ -6,22 +6,6 @@ use CodeIgniter\Config\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
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace Config;
|
||||
|
||||
use CodeIgniter\Cache\CacheInterface;
|
||||
use CodeIgniter\Cache\Handlers\ApcuHandler;
|
||||
use CodeIgniter\Cache\Handlers\DummyHandler;
|
||||
use CodeIgniter\Cache\Handlers\FileHandler;
|
||||
use CodeIgniter\Cache\Handlers\MemcachedHandler;
|
||||
@@ -79,7 +78,7 @@ class Cache extends BaseConfig
|
||||
* Your file storage preferences can be specified below, if you are using
|
||||
* the File driver.
|
||||
*
|
||||
* @var array{storePath?: string, mode?: int}
|
||||
* @var array<string, int|string|null>
|
||||
*/
|
||||
public array $file = [
|
||||
'storePath' => WRITEPATH . 'cache/',
|
||||
@@ -96,7 +95,7 @@ class Cache extends BaseConfig
|
||||
*
|
||||
* @see https://codeigniter.com/user_guide/libraries/caching.html#memcached
|
||||
*
|
||||
* @var array{host?: string, port?: int, weight?: int, raw?: bool}
|
||||
* @var array<string, bool|int|string>
|
||||
*/
|
||||
public array $memcached = [
|
||||
'host' => '127.0.0.1',
|
||||
@@ -109,28 +108,17 @@ class Cache extends BaseConfig
|
||||
* -------------------------------------------------------------------------
|
||||
* Redis settings
|
||||
* -------------------------------------------------------------------------
|
||||
*
|
||||
* Your Redis server can be specified below, if you are using
|
||||
* the Redis or Predis drivers.
|
||||
*
|
||||
* @var array{
|
||||
* host?: string,
|
||||
* password?: string|null,
|
||||
* port?: int,
|
||||
* timeout?: int,
|
||||
* async?: bool,
|
||||
* persistent?: bool,
|
||||
* database?: int
|
||||
* }
|
||||
* @var array<string, int|string|null>
|
||||
*/
|
||||
public array $redis = [
|
||||
'host' => '127.0.0.1',
|
||||
'password' => null,
|
||||
'port' => 6379,
|
||||
'timeout' => 0,
|
||||
'async' => false, // specific to Predis and ignored by the native Redis extension
|
||||
'persistent' => false,
|
||||
'database' => 0,
|
||||
'host' => '127.0.0.1',
|
||||
'password' => null,
|
||||
'port' => 6379,
|
||||
'timeout' => 0,
|
||||
'database' => 0,
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -144,7 +132,6 @@ class Cache extends BaseConfig
|
||||
* @var array<string, class-string<CacheInterface>>
|
||||
*/
|
||||
public array $validHandlers = [
|
||||
'apcu' => ApcuHandler::class,
|
||||
'dummy' => DummyHandler::class,
|
||||
'file' => FileHandler::class,
|
||||
'memcached' => MemcachedHandler::class,
|
||||
@@ -171,28 +158,4 @@ class Cache extends BaseConfig
|
||||
* @var bool|list<string>
|
||||
*/
|
||||
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 = [];
|
||||
}
|
||||
|
||||
@@ -30,11 +30,6 @@ class ContentSecurityPolicy extends BaseConfig
|
||||
*/
|
||||
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
|
||||
* HTTP to HTTPS. This directive is for websites with
|
||||
@@ -43,12 +38,12 @@ class ContentSecurityPolicy extends BaseConfig
|
||||
public bool $upgradeInsecureRequests = false;
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// CSP DIRECTIVES SETTINGS
|
||||
// Sources allowed
|
||||
// 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
|
||||
*/
|
||||
@@ -69,21 +64,6 @@ class ContentSecurityPolicy extends BaseConfig
|
||||
'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.
|
||||
*
|
||||
@@ -96,21 +76,6 @@ class ContentSecurityPolicy extends BaseConfig
|
||||
'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.
|
||||
*
|
||||
@@ -204,11 +169,6 @@ class ContentSecurityPolicy extends BaseConfig
|
||||
*/
|
||||
public $manifestSrc;
|
||||
|
||||
/**
|
||||
* @var list<string>|string
|
||||
*/
|
||||
public array|string $workerSrc = [];
|
||||
|
||||
/**
|
||||
* Limits the kinds of plugins a page may invoke.
|
||||
*
|
||||
@@ -224,17 +184,17 @@ class ContentSecurityPolicy extends BaseConfig
|
||||
public $sandbox;
|
||||
|
||||
/**
|
||||
* Nonce placeholder for style tags.
|
||||
* Nonce tag for style
|
||||
*/
|
||||
public string $styleNonceTag = '{csp-style-nonce}';
|
||||
|
||||
/**
|
||||
* Nonce placeholder for script tags.
|
||||
* Nonce tag for script
|
||||
*/
|
||||
public string $scriptNonceTag = '{csp-script-nonce}';
|
||||
|
||||
/**
|
||||
* Replace nonce tag automatically?
|
||||
* Replace nonce tag automatically
|
||||
*/
|
||||
public bool $autoNonce = true;
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ class Cookie extends BaseConfig
|
||||
* (empty string) means default SameSite attribute set by browsers (`Lax`)
|
||||
* will be set on cookies. If set to `None`, `$secure` must also be set.
|
||||
*
|
||||
* @var ''|'Lax'|'None'|'Strict'
|
||||
* @phpstan-var 'None'|'Lax'|'Strict'|''
|
||||
*/
|
||||
public string $samesite = 'Lax';
|
||||
|
||||
|
||||
@@ -42,8 +42,6 @@ class Database extends Config
|
||||
'strictOn' => false,
|
||||
'failover' => [],
|
||||
'port' => 3306,
|
||||
'numberNative' => false,
|
||||
'foundRows' => false,
|
||||
'dateFormat' => [
|
||||
'date' => 'Y-m-d',
|
||||
'datetime' => 'Y-m-d H:i:s',
|
||||
@@ -57,27 +55,26 @@ class Database extends Config
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
public array $tests = [
|
||||
'DSN' => '',
|
||||
'hostname' => 'localhost',
|
||||
'username' => 'admin',
|
||||
'password' => 'pointofsale',
|
||||
'database' => 'ospos',
|
||||
'DBDriver' => 'MySQLi',
|
||||
'DBPrefix' => 'ospos_',
|
||||
'pConnect' => false,
|
||||
'DBDebug' => (ENVIRONMENT !== 'production'),
|
||||
'charset' => 'utf8mb4',
|
||||
'DBCollat' => 'utf8mb4_general_ci',
|
||||
'swapPre' => '',
|
||||
'encrypt' => false,
|
||||
'compress' => false,
|
||||
'strictOn' => false,
|
||||
'failover' => [],
|
||||
'port' => 3306,
|
||||
'foreignKeys' => true,
|
||||
'busyTimeout' => 1000,
|
||||
'synchronous' => null,
|
||||
'dateFormat' => [
|
||||
'DSN' => '',
|
||||
'hostname' => 'localhost',
|
||||
'username' => 'admin',
|
||||
'password' => 'pointofsale',
|
||||
'database' => 'ospos',
|
||||
'DBDriver' => 'MySQLi',
|
||||
'DBPrefix' => 'ospos_',
|
||||
'pConnect' => false,
|
||||
'DBDebug' => (ENVIRONMENT !== 'production'),
|
||||
'charset' => 'utf8mb4',
|
||||
'DBCollat' => 'utf8mb4_general_ci',
|
||||
'swapPre' => '',
|
||||
'encrypt' => false,
|
||||
'compress' => false,
|
||||
'strictOn' => false,
|
||||
'failover' => [],
|
||||
'port' => 3306,
|
||||
'foreignKeys' => true,
|
||||
'busyTimeout' => 1000,
|
||||
'dateFormat' => [
|
||||
'date' => 'Y-m-d',
|
||||
'datetime' => 'Y-m-d H:i:s',
|
||||
'time' => 'H:i:s',
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
namespace Config;
|
||||
|
||||
/**
|
||||
* @immutable
|
||||
*/
|
||||
class DocTypes
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -30,11 +30,6 @@ class Email extends BaseConfig
|
||||
*/
|
||||
public string $SMTPHost = 'mail.mxserver.com';
|
||||
|
||||
/**
|
||||
* Which SMTP authentication method to use: login, plain
|
||||
*/
|
||||
public string $SMTPAuthMethod = 'login';
|
||||
|
||||
/**
|
||||
* SMTP Username
|
||||
*/
|
||||
|
||||
@@ -23,23 +23,6 @@ class Encryption extends BaseConfig
|
||||
*/
|
||||
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
|
||||
|
||||
@@ -65,15 +65,12 @@ class Filters extends BaseFilters
|
||||
* List of filter aliases that are always
|
||||
* applied before and after every request.
|
||||
*
|
||||
* @var array{
|
||||
* before: array<string, array{except: list<string>|string}>|list<string>,
|
||||
* after: array<string, array{except: list<string>|string}>|list<string>
|
||||
* }
|
||||
* @var array<string, array<string, array<string, string>>>|array<string, list<string>>
|
||||
*/
|
||||
public array $globals = [
|
||||
'before' => [
|
||||
'honeypot',
|
||||
'csrf' => ['except' => 'login|migrate'],
|
||||
'csrf' => ['except' => 'login'],
|
||||
'invalidchars',
|
||||
],
|
||||
'after' => [
|
||||
@@ -103,7 +100,7 @@ class Filters extends BaseFilters
|
||||
* before or after URI patterns.
|
||||
*
|
||||
* Example:
|
||||
* 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']]
|
||||
* isLoggedIn' => ['before' => ['account/*', 'profiles/*']]
|
||||
*
|
||||
* @var array<string, array<string, list<string>>>
|
||||
*/
|
||||
|
||||
@@ -61,13 +61,4 @@ class Format extends BaseConfig
|
||||
'application/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;
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
<?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,8 +16,6 @@ class Images extends BaseConfig
|
||||
/**
|
||||
* The path to the image library.
|
||||
* Required for ImageMagick, GraphicsMagick, or NetPBM.
|
||||
*
|
||||
* @deprecated 4.7.0 No longer used.
|
||||
*/
|
||||
public string $libraryPath = '/usr/local/bin/convert';
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace Config;
|
||||
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
use CodeIgniter\Log\Handlers\FileHandler;
|
||||
use CodeIgniter\Log\Handlers\HandlerInterface;
|
||||
|
||||
class Logger extends BaseConfig
|
||||
{
|
||||
@@ -74,7 +73,7 @@ class Logger extends BaseConfig
|
||||
* Handlers are executed in the order defined in this array, starting with
|
||||
* the handler on top and continuing down.
|
||||
*
|
||||
* @var array<class-string<HandlerInterface>, array<string, int|list<string>|string>>
|
||||
* @var array<class-string, array<string, int|list<string>|string>>
|
||||
*/
|
||||
public array $handlers = [
|
||||
/*
|
||||
|
||||
@@ -47,19 +47,4 @@ class Migrations extends BaseConfig
|
||||
* - Y_m_d_His_
|
||||
*/
|
||||
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,6 +3,8 @@
|
||||
namespace Config;
|
||||
|
||||
/**
|
||||
* Mimes
|
||||
*
|
||||
* This file contains an array of mime types. It is used by the
|
||||
* Upload class to help identify allowed file types.
|
||||
*
|
||||
@@ -13,6 +15,8 @@ namespace Config;
|
||||
*
|
||||
* When working with mime types, please make sure you have the ´fileinfo´
|
||||
* extension enabled to reliably detect the media types.
|
||||
*
|
||||
* @immutable
|
||||
*/
|
||||
class Mimes
|
||||
{
|
||||
@@ -478,16 +482,13 @@ class Mimes
|
||||
'application/sla',
|
||||
'application/vnd.ms-pki.stl',
|
||||
'application/x-navistyle',
|
||||
'model/stl',
|
||||
'application/octet-stream',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Attempts to determine the best mime type for the given file extension.
|
||||
*
|
||||
* @param string $extension
|
||||
* @return array|string|null The mime type found, or none if unable to determine.
|
||||
* @return string|null The mime type found, or none if unable to determine.
|
||||
*/
|
||||
public static function guessTypeFromExtension(string $extension): array|string|null
|
||||
{
|
||||
@@ -523,7 +524,7 @@ class Mimes
|
||||
}
|
||||
|
||||
// Reverse check the mime type list if no extension was proposed.
|
||||
// This search is order-sensitive!
|
||||
// This search is order sensitive!
|
||||
foreach (static::$mimes as $ext => $types) {
|
||||
if (in_array($type, (array) $types, true)) {
|
||||
return $ext;
|
||||
|
||||
@@ -9,6 +9,8 @@ use CodeIgniter\Modules\Modules as BaseModules;
|
||||
*
|
||||
* NOTE: This class is required prior to Autoloader instantiation,
|
||||
* and does not extend BaseConfig.
|
||||
*
|
||||
* @immutable
|
||||
*/
|
||||
class Modules extends BaseModules
|
||||
{
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace Config;
|
||||
use App\Models\Appconfig;
|
||||
use CodeIgniter\Cache\CacheInterface;
|
||||
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
|
||||
@@ -35,21 +34,11 @@ class OSPOS extends BaseConfig
|
||||
if ($cache) {
|
||||
$this->settings = decode_array($cache);
|
||||
} else {
|
||||
try {
|
||||
$appconfig = model(Appconfig::class);
|
||||
foreach ($appconfig->get_all()->getResult() as $app_config) {
|
||||
$this->settings[$app_config->key] = $app_config->value;
|
||||
}
|
||||
$this->cache->save('settings', encode_array($this->settings));
|
||||
} catch (DatabaseException $e) {
|
||||
// Database table doesn't exist yet (migrations haven't run)
|
||||
// Return empty settings to allow migration page to display
|
||||
$this->settings = [
|
||||
'language' => 'english',
|
||||
'language_code' => 'en',
|
||||
'company' => 'Home'
|
||||
];
|
||||
$appconfig = model(Appconfig::class);
|
||||
foreach ($appconfig->get_all()->getResult() as $app_config) {
|
||||
$this->settings[$app_config->key] = $app_config->value;
|
||||
}
|
||||
$this->cache->save('settings', encode_array($this->settings));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,4 +50,4 @@ class OSPOS extends BaseConfig
|
||||
$this->cache->delete('settings');
|
||||
$this->set_settings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Config;
|
||||
* NOTE: This class does not extend BaseConfig for performance reasons.
|
||||
* So you cannot replace the property values with Environment Variables.
|
||||
*
|
||||
* WARNING: Do not use these options when running the app in the Worker Mode.
|
||||
* @immutable
|
||||
*/
|
||||
class Optimize
|
||||
{
|
||||
|
||||
@@ -15,6 +15,8 @@ namespace Config;
|
||||
*
|
||||
* NOTE: This class is required prior to Autoloader instantiation,
|
||||
* and does not extend BaseConfig.
|
||||
*
|
||||
* @immutable
|
||||
*/
|
||||
class Paths
|
||||
{
|
||||
@@ -75,16 +77,4 @@ class Paths
|
||||
* is used when no value is provided to `Services::renderer()`.
|
||||
*/
|
||||
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,7 +10,6 @@ $routes->setDefaultController('Login');
|
||||
$routes->get('/', 'Login::index');
|
||||
$routes->get('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)/(:segment)', 'No_access::index/$1/$2');
|
||||
|
||||
@@ -96,15 +96,6 @@ class Routing extends BaseRouting
|
||||
*/
|
||||
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.
|
||||
* 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.
|
||||
*
|
||||
* @var string 'cookie' or 'session'
|
||||
* @var string|false 'cookie', 'session', or false
|
||||
*/
|
||||
public string $csrfProtection = 'session';
|
||||
public string|false $csrfProtection = 'session';
|
||||
|
||||
/**
|
||||
* --------------------------------------------------------------------------
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace Config;
|
||||
|
||||
use App\Libraries\MY_Language;
|
||||
use Locale;
|
||||
use HTMLPurifier;
|
||||
use HTMLPurifier_Config;
|
||||
@@ -39,11 +38,9 @@ class Services extends BaseService
|
||||
/**
|
||||
* Responsible for loading the language string translations.
|
||||
*
|
||||
* @param string|null $locale
|
||||
* @param bool $getShared
|
||||
* @return MY_Language
|
||||
*/
|
||||
public static function language(?string $locale = null, bool $getShared = true): MY_Language
|
||||
public static function language(?string $locale = null, bool $getShared = true)
|
||||
{
|
||||
if ($getShared) {
|
||||
return static::getSharedInstance('language', $locale)->setLocale($locale);
|
||||
@@ -58,12 +55,12 @@ class Services extends BaseService
|
||||
// Use '?:' for empty string check
|
||||
$locale = $locale ?: $requestLocale;
|
||||
|
||||
return new MY_Language($locale);
|
||||
return new \App\Libraries\MY_Language($locale);
|
||||
}
|
||||
|
||||
private static HTMLPurifier $htmlPurifier;
|
||||
private static $htmlPurifier;
|
||||
|
||||
public static function htmlPurifier($getShared = true): object
|
||||
public static function htmlPurifier($getShared = true)
|
||||
{
|
||||
if ($getShared) {
|
||||
return static::getSharedInstance('htmlPurifier');
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
namespace Config;
|
||||
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Session\Handlers\BaseHandler;
|
||||
use CodeIgniter\Session\Handlers\DatabaseHandler;
|
||||
use CodeIgniter\Session\Handlers\FileHandler;
|
||||
|
||||
class Session extends BaseConfig
|
||||
{
|
||||
@@ -126,23 +124,4 @@ class Session extends BaseConfig
|
||||
* seconds.
|
||||
*/
|
||||
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,29 +119,4 @@ class Toolbar extends BaseConfig
|
||||
public array $watchedExtensions = [
|
||||
'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,13 +230,9 @@ class UserAgents extends BaseConfig
|
||||
*/
|
||||
public array $robots = [
|
||||
'googlebot' => 'Googlebot',
|
||||
'google-pagerenderer' => 'Google Page Renderer',
|
||||
'google-read-aloud' => 'Google Read Aloud',
|
||||
'google-safety' => 'Google Safety Bot',
|
||||
'msnbot' => 'MSNBot',
|
||||
'baiduspider' => 'Baiduspider',
|
||||
'bingbot' => 'Bing',
|
||||
'bingpreview' => 'BingPreview',
|
||||
'slurp' => 'Inktomi Slurp',
|
||||
'yahoo' => 'Yahoo',
|
||||
'ask jeeves' => 'Ask Jeeves',
|
||||
@@ -252,11 +248,5 @@ class UserAgents extends BaseConfig
|
||||
'ia_archiver' => 'Alexa Crawler',
|
||||
'MJ12bot' => 'Majestic-12',
|
||||
'Uptimebot' => 'Uptimebot',
|
||||
'duckduckbot' => 'DuckDuckBot',
|
||||
'sogou' => 'Sogou Spider',
|
||||
'exabot' => 'Exabot',
|
||||
'bot' => 'Generic Bot',
|
||||
'crawler' => 'Generic Crawler',
|
||||
'spider' => 'Generic Spider',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -135,19 +135,4 @@ class OSPOSRules
|
||||
{
|
||||
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,21 +59,4 @@ class View extends BaseView
|
||||
* @var list<class-string<ViewDecoratorInterface>>
|
||||
*/
|
||||
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';
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
<?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,24 +106,12 @@ class Attributes extends Secure_Controller
|
||||
$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
|
||||
$definition_data = [
|
||||
'definition_name' => $this->request->getPost('definition_name'),
|
||||
'definition_unit' => $this->request->getPost('definition_unit') != '' ? $this->request->getPost('definition_unit') : null,
|
||||
'definition_flags' => $definition_flags,
|
||||
'definition_fk' => $definition_fk
|
||||
'definition_fk' => $this->request->getPost('definition_group') != '' ? $this->request->getPost('definition_group') : null
|
||||
];
|
||||
|
||||
if ($this->request->getPost('definition_type') != null) {
|
||||
@@ -132,7 +120,7 @@ class Attributes extends Secure_Controller
|
||||
|
||||
$definition_name = $definition_data['definition_name'];
|
||||
|
||||
if ($this->attribute->saveDefinition($definition_data, $definition_id)) {
|
||||
if ($this->attribute->save_definition($definition_data, $definition_id)) {
|
||||
// New definition
|
||||
if ($definition_id == NO_DEFINITION_ID) {
|
||||
$definition_values = json_decode(html_entity_decode($this->request->getPost('definition_values')));
|
||||
@@ -162,32 +150,6 @@ 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
|
||||
@@ -246,7 +208,7 @@ class Attributes extends Secure_Controller
|
||||
$data['definition_group'][''] = lang('Common.none_selected_text');
|
||||
$data['definition_info'] = $info;
|
||||
|
||||
$show_all = Attribute::SHOW_IN_ITEMS | Attribute::SHOW_IN_RECEIVINGS | Attribute::SHOW_IN_SALES | Attribute::SHOW_IN_SEARCH | Attribute::SHOW_IN_CUSTOMERS | Attribute::SHOW_IN_EMPLOYEES | Attribute::SHOW_IN_SUPPLIERS;
|
||||
$show_all = Attribute::SHOW_IN_ITEMS | Attribute::SHOW_IN_RECEIVINGS | Attribute::SHOW_IN_SALES;
|
||||
$data['definition_flags'] = $this->get_attributes($show_all);
|
||||
$selected_flags = $info->definition_flags === '' ? $show_all : $info->definition_flags;
|
||||
$data['selected_definition_flags'] = $this->get_attributes($selected_flags);
|
||||
|
||||
@@ -3,46 +3,56 @@
|
||||
namespace App\Controllers;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\HTTP\CLIRequest;
|
||||
use CodeIgniter\HTTP\IncomingRequest;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* Class BaseController
|
||||
*
|
||||
* BaseController provides a convenient place for loading components
|
||||
* 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.
|
||||
* For security be sure to declare any new methods as protected or private.
|
||||
*/
|
||||
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.
|
||||
* The creation of dynamic property is deprecated in PHP 8.2.
|
||||
*/
|
||||
|
||||
// protected $session;
|
||||
|
||||
/**
|
||||
* @param RequestInterface $request
|
||||
* @param ResponseInterface $response
|
||||
* @param LoggerInterface $logger
|
||||
* @return void
|
||||
*/
|
||||
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger): void
|
||||
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
|
||||
{
|
||||
// Load here all helpers you want to be available in your controllers that extend BaseController.
|
||||
// Caution: Do not put the this below the parent::initController() call below.
|
||||
// $this->helpers = ['form', 'url'];
|
||||
|
||||
// Caution: Do not edit this line.
|
||||
// Do Not Edit This Line
|
||||
parent::initController($request, $response, $logger);
|
||||
|
||||
// Preload any models, libraries, etc, here.
|
||||
// $this->session = service('session');
|
||||
|
||||
// E.g.: $this->session = service('session');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,9 +36,6 @@ class Cashups extends Secure_Controller
|
||||
// filters that will be loaded in the multiselect dropdown
|
||||
$data['filters'] = ['is_deleted' => lang('Cashups.is_deleted')];
|
||||
|
||||
// Restore filters from URL
|
||||
$data = array_merge($data, restoreTableFilters($this->request));
|
||||
|
||||
return view('cashups/manage', $data);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ use App\Models\Appconfig;
|
||||
use App\Models\Attribute;
|
||||
use App\Models\Customer_rewards;
|
||||
use App\Models\Dinner_table;
|
||||
use App\Models\Item;
|
||||
use App\Models\Module;
|
||||
use App\Models\Enums\Rounding_mode;
|
||||
use App\Models\Stock_location;
|
||||
@@ -367,7 +366,7 @@ class Config extends Secure_Controller
|
||||
*/
|
||||
public function postSaveGeneral(): ResponseInterface
|
||||
{
|
||||
$batchSaveData = [
|
||||
$batch_save_data = [
|
||||
'theme' => $this->request->getPost('theme'),
|
||||
'login_form' => $this->request->getPost('login_form'),
|
||||
'default_sales_discount_type' => $this->request->getPost('default_sales_discount_type') != null,
|
||||
@@ -386,9 +385,9 @@ class Config extends Secure_Controller
|
||||
'gcaptcha_enable' => $this->request->getPost('gcaptcha_enable') != null,
|
||||
'gcaptcha_secret_key' => $this->request->getPost('gcaptcha_secret_key'),
|
||||
'gcaptcha_site_key' => $this->request->getPost('gcaptcha_site_key'),
|
||||
'suggestions_first_column' => $this->validateSuggestionsColumn($this->request->getPost('suggestions_first_column'), 'first'),
|
||||
'suggestions_second_column' => $this->validateSuggestionsColumn($this->request->getPost('suggestions_second_column'), 'other'),
|
||||
'suggestions_third_column' => $this->validateSuggestionsColumn($this->request->getPost('suggestions_third_column'), 'other'),
|
||||
'suggestions_first_column' => $this->request->getPost('suggestions_first_column'),
|
||||
'suggestions_second_column' => $this->request->getPost('suggestions_second_column'),
|
||||
'suggestions_third_column' => $this->request->getPost('suggestions_third_column'),
|
||||
'giftcard_number' => $this->request->getPost('giftcard_number'),
|
||||
'derive_sale_quantity' => $this->request->getPost('derive_sale_quantity') != null,
|
||||
'multi_pack_enabled' => $this->request->getPost('multi_pack_enabled') != null,
|
||||
@@ -398,26 +397,19 @@ class Config extends Secure_Controller
|
||||
|
||||
$this->module->set_show_office_group($this->request->getPost('show_office_group') != null);
|
||||
|
||||
$this->db->transStart();
|
||||
if ($batch_save_data['category_dropdown'] == 1) {
|
||||
$definition_data['definition_name'] = 'ospos_category';
|
||||
$definition_data['definition_flags'] = 0;
|
||||
$definition_data['definition_type'] = 'DROPDOWN';
|
||||
$definition_data['definition_id'] = CATEGORY_DEFINITION_ID;
|
||||
$definition_data['deleted'] = 0;
|
||||
|
||||
$attributeSuccess = true;
|
||||
if ($batchSaveData['category_dropdown']) {
|
||||
$definitionData['definition_name'] = 'ospos_category';
|
||||
$definitionData['definition_flags'] = 0;
|
||||
$definitionData['definition_type'] = 'DROPDOWN';
|
||||
$definitionData['definition_id'] = CATEGORY_DEFINITION_ID;
|
||||
$definitionData['deleted'] = 0;
|
||||
|
||||
$attributeSuccess = $this->attribute->saveDefinition($definitionData, CATEGORY_DEFINITION_ID);
|
||||
} elseif ($batchSaveData['category_dropdown'] == NO_DEFINITION_ID) {
|
||||
$attributeSuccess = $this->attribute->deleteDefinition(CATEGORY_DEFINITION_ID);
|
||||
$this->attribute->save_definition($definition_data, CATEGORY_DEFINITION_ID);
|
||||
} elseif ($batch_save_data['category_dropdown'] == NO_DEFINITION_ID) {
|
||||
$this->attribute->deleteDefinition(CATEGORY_DEFINITION_ID);
|
||||
}
|
||||
|
||||
$success = $attributeSuccess && $this->appconfig->batch_save($batchSaveData);
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
$success = $success && $this->db->transStatus();
|
||||
$success = $this->appconfig->batch_save($batch_save_data);
|
||||
|
||||
return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]);
|
||||
}
|
||||
@@ -430,35 +422,32 @@ class Config extends Secure_Controller
|
||||
*/
|
||||
public function postCheckNumberLocale(): ResponseInterface
|
||||
{
|
||||
$numberLocale = $this->request->getPost('number_locale');
|
||||
$saveNumberLocale = $this->request->getPost('save_number_locale');
|
||||
$postedCurrencySymbol = $this->request->getPost('currency_symbol');
|
||||
$postedCurrencyCode = $this->request->getPost('currency_code');
|
||||
$number_locale = $this->request->getPost('number_locale');
|
||||
$save_number_locale = $this->request->getPost('save_number_locale');
|
||||
|
||||
$fmt = new NumberFormatter($numberLocale, NumberFormatter::CURRENCY);
|
||||
|
||||
// Use posted values if provided, otherwise fall back to locale defaults
|
||||
$currencySymbol = $postedCurrencySymbol !== '' ? $postedCurrencySymbol : $fmt->getSymbol(NumberFormatter::CURRENCY_SYMBOL);
|
||||
$currencyCode = $postedCurrencyCode !== '' ? $postedCurrencyCode : $fmt->getTextAttribute(NumberFormatter::CURRENCY_CODE);
|
||||
|
||||
// Update saved locale if it changed
|
||||
if ($numberLocale !== $saveNumberLocale) {
|
||||
$saveNumberLocale = $numberLocale;
|
||||
$fmt = new NumberFormatter($number_locale, NumberFormatter::CURRENCY);
|
||||
if ($number_locale != $save_number_locale) {
|
||||
$currency_symbol = $fmt->getSymbol(NumberFormatter::CURRENCY_SYMBOL);
|
||||
$currency_code = $fmt->getTextAttribute(NumberFormatter::CURRENCY_CODE);
|
||||
$save_number_locale = $number_locale;
|
||||
} else {
|
||||
$currency_symbol = empty($this->request->getPost('currency_symbol')) ? $fmt->getSymbol(NumberFormatter::CURRENCY_SYMBOL) : $this->request->getPost('currency_symbol');
|
||||
$currency_code = empty($this->request->getPost('currency_code')) ? $fmt->getTextAttribute(NumberFormatter::CURRENCY_CODE) : $this->request->getPost('currency_code');
|
||||
}
|
||||
|
||||
if ($this->request->getPost('thousands_separator') == 'false') {
|
||||
$fmt->setTextAttribute(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, '');
|
||||
}
|
||||
|
||||
$fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL, $currencySymbol);
|
||||
$numberLocaleExample = $fmt->format(1234567890.12300);
|
||||
$fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL, $currency_symbol);
|
||||
$number_local_example = $fmt->format(1234567890.12300);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'success' => $numberLocaleExample != false,
|
||||
'save_number_locale' => $saveNumberLocale,
|
||||
'number_locale_example' => $numberLocaleExample,
|
||||
'currency_symbol' => $currencySymbol,
|
||||
'currency_code' => $currencyCode,
|
||||
'success' => $number_local_example != false,
|
||||
'save_number_locale' => $save_number_locale,
|
||||
'number_locale_example' => $number_local_example,
|
||||
'currency_symbol' => $currency_symbol,
|
||||
'currency_code' => $currency_code,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -514,24 +503,9 @@ class Config extends Secure_Controller
|
||||
$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 = [
|
||||
'protocol' => $protocol,
|
||||
'mailpath' => $mailpath,
|
||||
'protocol' => $this->request->getPost('protocol'),
|
||||
'mailpath' => $this->request->getPost('mailpath'),
|
||||
'smtp_host' => $this->request->getPost('smtp_host'),
|
||||
'smtp_user' => $this->request->getPost('smtp_user'),
|
||||
'smtp_pass' => $password,
|
||||
@@ -1002,26 +976,4 @@ class Config extends Secure_Controller
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Libraries\Mailchimp_lib;
|
||||
use App\Models\Attribute;
|
||||
|
||||
use App\Models\Customer;
|
||||
use App\Models\Customer_rewards;
|
||||
use App\Models\Tax_code;
|
||||
@@ -15,31 +15,34 @@ use stdClass;
|
||||
|
||||
class Customers extends Persons
|
||||
{
|
||||
private string $listId;
|
||||
private Mailchimp_lib $mailchimpLib;
|
||||
private Customer_rewards $customerRewards;
|
||||
private string $_list_id;
|
||||
private Mailchimp_lib $mailchimp_lib;
|
||||
private Customer_rewards $customer_rewards;
|
||||
private Customer $customer;
|
||||
private Tax_code $taxCode;
|
||||
private array $appConfig;
|
||||
private Tax_code $tax_code;
|
||||
private array $config;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('customers');
|
||||
$this->mailchimpLib = new Mailchimp_lib();
|
||||
$this->customerRewards = model(Customer_rewards::class);
|
||||
$this->mailchimp_lib = new Mailchimp_lib();
|
||||
$this->customer_rewards = model(Customer_rewards::class);
|
||||
$this->customer = model(Customer::class);
|
||||
$this->taxCode = model(Tax_code::class);
|
||||
$this->appConfig = config(OSPOS::class)->settings;
|
||||
$this->tax_code = model(Tax_code::class);
|
||||
$this->config = config(OSPOS::class)->settings;
|
||||
|
||||
$encrypter = Services::encrypter();
|
||||
|
||||
if (!empty($this->appConfig['mailchimp_list_id'])) {
|
||||
$this->listId = $encrypter->decrypt($this->appConfig['mailchimp_list_id']);
|
||||
if (!empty($this->config['mailchimp_list_id'])) {
|
||||
$this->_list_id = $encrypter->decrypt($this->config['mailchimp_list_id']);
|
||||
} else {
|
||||
$this->listId = '';
|
||||
$this->_list_id = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getIndex(): string
|
||||
{
|
||||
$data['table_headers'] = get_customer_manage_table_headers();
|
||||
@@ -47,13 +50,19 @@ class Customers extends Persons
|
||||
return view('people/manage', $data);
|
||||
}
|
||||
|
||||
public function getRow(int $rowId): ResponseInterface
|
||||
/**
|
||||
* Gets one row for a customer manage table. This is called using AJAX to update one row.
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getRow(int $row_id): ResponseInterface
|
||||
{
|
||||
$person = $this->customer->get_info($rowId);
|
||||
$person = $this->customer->get_info($row_id);
|
||||
|
||||
$stats = $this->customer->get_stats($person->person_id);
|
||||
// Retrieve the total amount the customer spent so far together with min, max and average values
|
||||
$stats = $this->customer->get_stats($person->person_id); // TODO: This and the next 11 lines are duplicated in search(). Extract a method.
|
||||
|
||||
if (empty($stats)) {
|
||||
// Create object with empty properties.
|
||||
$stats = new stdClass();
|
||||
$stats->total = 0;
|
||||
$stats->min = 0;
|
||||
@@ -63,11 +72,17 @@ class Customers extends Persons
|
||||
$stats->quantity = 0;
|
||||
}
|
||||
|
||||
$dataRow = get_customer_data_row($person, $stats);
|
||||
$data_row = get_customer_data_row($person, $stats);
|
||||
|
||||
return $this->response->setJSON($dataRow);
|
||||
return $this->response->setJSON($data_row);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns customer table data rows. This will be called with AJAX.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getSearch(): ResponseInterface
|
||||
{
|
||||
$search = $this->request->getGet('search');
|
||||
@@ -77,13 +92,15 @@ class Customers extends Persons
|
||||
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
|
||||
$customers = $this->customer->search($search, $limit, $offset, $sort, $order);
|
||||
$totalRows = $this->customer->get_found_rows($search);
|
||||
$total_rows = $this->customer->get_found_rows($search);
|
||||
|
||||
$dataRows = [];
|
||||
$data_rows = [];
|
||||
|
||||
foreach ($customers->getResult() as $person) {
|
||||
$stats = $this->customer->get_stats($person->person_id);
|
||||
// Retrieve the total amount the customer spent so far together with min, max and average values
|
||||
$stats = $this->customer->get_stats($person->person_id); // TODO: duplicated... see above
|
||||
if (empty($stats)) {
|
||||
// Create object with empty properties.
|
||||
$stats = new stdClass();
|
||||
$stats->total = 0;
|
||||
$stats->min = 0;
|
||||
@@ -93,12 +110,16 @@ class Customers extends Persons
|
||||
$stats->quantity = 0;
|
||||
}
|
||||
|
||||
$dataRows[] = get_customer_data_row($person, $stats);
|
||||
$data_rows[] = get_customer_data_row($person, $stats);
|
||||
}
|
||||
|
||||
return $this->response->setJSON(['total' => $totalRows, 'rows' => $dataRows]);
|
||||
return $this->response->setJSON(['total' => $total_rows, 'rows' => $data_rows]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives search suggestions based on what is being searched for
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getSuggest(): ResponseInterface
|
||||
{
|
||||
$search = $this->request->getGet('term');
|
||||
@@ -107,7 +128,10 @@ class Customers extends Persons
|
||||
return $this->response->setJSON($suggestions);
|
||||
}
|
||||
|
||||
public function suggestSearch(): ResponseInterface
|
||||
/**
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function suggest_search(): ResponseInterface
|
||||
{
|
||||
$search = $this->request->getGet('term');
|
||||
$suggestions = $this->customer->get_search_suggestions($search, 25, false);
|
||||
@@ -115,11 +139,16 @@ class Customers extends Persons
|
||||
return $this->response->setJSON($suggestions);
|
||||
}
|
||||
|
||||
public function getView(int $customerId = NEW_ENTRY): string
|
||||
/**
|
||||
* Loads the customer edit form
|
||||
* @return string
|
||||
*/
|
||||
public function getView(int $customer_id = NEW_ENTRY): string
|
||||
{
|
||||
if ($customerId == null) $customerId = NEW_ENTRY;
|
||||
// Set default values
|
||||
if ($customer_id == null) $customer_id = NEW_ENTRY;
|
||||
|
||||
$info = $this->customer->get_info($customerId);
|
||||
$info = $this->customer->get_info($customer_id);
|
||||
foreach (get_object_vars($info) as $property => $value) {
|
||||
$info->$property = $value;
|
||||
}
|
||||
@@ -130,27 +159,28 @@ class Customers extends Persons
|
||||
$data['person_info']->employee_id = $this->employee->get_logged_in_employee_info()->person_id;
|
||||
}
|
||||
|
||||
$employeeInfo = $this->employee->get_info($info->employee_id);
|
||||
$data['employee'] = $employeeInfo->first_name . ' ' . $employeeInfo->last_name;
|
||||
$employee_info = $this->employee->get_info($info->employee_id);
|
||||
$data['employee'] = $employee_info->first_name . ' ' . $employee_info->last_name;
|
||||
|
||||
$taxCodeInfo = $this->taxCode->get_info($info->sales_tax_code_id);
|
||||
$tax_code_info = $this->tax_code->get_info($info->sales_tax_code_id);
|
||||
|
||||
if ($taxCodeInfo->tax_code != null) {
|
||||
$data['sales_tax_code_label'] = $taxCodeInfo->tax_code . ' ' . $taxCodeInfo->tax_code_name;
|
||||
if ($tax_code_info->tax_code != null) {
|
||||
$data['sales_tax_code_label'] = $tax_code_info->tax_code . ' ' . $tax_code_info->tax_code_name;
|
||||
} else {
|
||||
$data['sales_tax_code_label'] = '';
|
||||
}
|
||||
|
||||
$packages = ['' => lang('Items.none')];
|
||||
foreach ($this->customerRewards->get_all()->getResultArray() as $row) {
|
||||
foreach ($this->customer_rewards->get_all()->getResultArray() as $row) {
|
||||
$packages[$row['package_id']] = $row['package_name'];
|
||||
}
|
||||
$data['packages'] = $packages;
|
||||
$data['selected_package'] = $info->package_id;
|
||||
|
||||
$data['use_destination_based_tax'] = $this->appConfig['use_destination_based_tax'];
|
||||
$data['use_destination_based_tax'] = $this->config['use_destination_based_tax'];
|
||||
|
||||
$stats = $this->customer->get_stats($customerId);
|
||||
// Retrieve the total amount the customer spent so far together with min, max and average values
|
||||
$stats = $this->customer->get_stats($customer_id);
|
||||
if (!empty($stats)) {
|
||||
foreach (get_object_vars($stats) as $property => $value) {
|
||||
$info->$property = $value;
|
||||
@@ -158,11 +188,14 @@ class Customers extends Persons
|
||||
$data['stats'] = $stats;
|
||||
}
|
||||
|
||||
// Retrieve the info from Mailchimp only if there is an email address assigned
|
||||
if (!empty($info->email)) {
|
||||
if (($mailchimpInfo = $this->mailchimpLib->getMemberInfo($this->listId, $info->email)) !== false) {
|
||||
$data['mailchimp_info'] = $mailchimpInfo;
|
||||
// Collect Mailchimp customer info
|
||||
if (($mailchimp_info = $this->mailchimp_lib->getMemberInfo($this->_list_id, $info->email)) !== false) {
|
||||
$data['mailchimp_info'] = $mailchimp_info;
|
||||
|
||||
if (($activities = $this->mailchimpLib->getMemberActivity($this->listId, $info->email)) !== false) {
|
||||
// Collect customer Mailchimp emails activities (stats)
|
||||
if (($activities = $this->mailchimp_lib->getMemberActivity($this->_list_id, $info->email)) !== false) {
|
||||
if (array_key_exists('activity', $activities)) {
|
||||
$open = 0;
|
||||
$unopen = 0;
|
||||
@@ -202,25 +235,22 @@ class Customers extends Persons
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets person attributes for a customer (AJAX)
|
||||
* Inserts/updates a customer
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getAttributes(int $customerId = NEW_ENTRY): string
|
||||
public function postSave(int $customer_id = NEW_ENTRY): ResponseInterface
|
||||
{
|
||||
return $this->getPersonAttributes($customerId, Attribute::SHOW_IN_CUSTOMERS);
|
||||
}
|
||||
|
||||
public function postSave(int $customerId = NEW_ENTRY): ResponseInterface
|
||||
{
|
||||
$firstName = $this->request->getPost('first_name');
|
||||
$lastName = $this->request->getPost('last_name');
|
||||
$first_name = $this->request->getPost('first_name');
|
||||
$last_name = $this->request->getPost('last_name');
|
||||
$email = strtolower($this->request->getPost('email', FILTER_SANITIZE_EMAIL));
|
||||
|
||||
$firstName = $this->nameize($firstName);
|
||||
$lastName = $this->nameize($lastName);
|
||||
// Format first and last name properly
|
||||
$first_name = $this->nameize($first_name);
|
||||
$last_name = $this->nameize($last_name);
|
||||
|
||||
$personData = [
|
||||
'first_name' => $firstName,
|
||||
'last_name' => $lastName,
|
||||
$person_data = [
|
||||
'first_name' => $first_name,
|
||||
'last_name' => $last_name,
|
||||
'gender' => $this->request->getPost('gender', FILTER_SANITIZE_NUMBER_INT),
|
||||
'email' => $email,
|
||||
'phone_number' => $this->request->getPost('phone_number'),
|
||||
@@ -233,9 +263,9 @@ class Customers extends Persons
|
||||
'comments' => $this->request->getPost('comments')
|
||||
];
|
||||
|
||||
$dateFormatter = date_create_from_format($this->appConfig['dateformat'] . ' ' . $this->appConfig['timeformat'], $this->request->getPost('date'));
|
||||
$date_formatter = date_create_from_format($this->config['dateformat'] . ' ' . $this->config['timeformat'], $this->request->getPost('date'));
|
||||
|
||||
$customerData = [
|
||||
$customer_data = [
|
||||
'consent' => $this->request->getPost('consent') != null,
|
||||
'account_number' => $this->request->getPost('account_number') == '' ? null : $this->request->getPost('account_number'),
|
||||
'tax_id' => $this->request->getPost('tax_id'),
|
||||
@@ -244,57 +274,68 @@ class Customers extends Persons
|
||||
'discount_type' => $this->request->getPost('discount_type') == null ? PERCENT : $this->request->getPost('discount_type', FILTER_SANITIZE_NUMBER_INT),
|
||||
'package_id' => $this->request->getPost('package_id') == '' ? null : $this->request->getPost('package_id'),
|
||||
'taxable' => $this->request->getPost('taxable') != null,
|
||||
'date' => $dateFormatter->format('Y-m-d H:i:s'),
|
||||
'date' => $date_formatter->format('Y-m-d H:i:s'),
|
||||
'employee_id' => $this->request->getPost('employee_id', FILTER_SANITIZE_NUMBER_INT),
|
||||
'sales_tax_code_id' => $this->request->getPost('sales_tax_code_id') == '' ? null : $this->request->getPost('sales_tax_code_id', FILTER_SANITIZE_NUMBER_INT)
|
||||
];
|
||||
|
||||
if ($this->customer->save_customer($personData, $customerData, $customerId)) {
|
||||
$personId = $customerId == NEW_ENTRY ? $customerData['person_id'] : $customerId;
|
||||
$this->savePersonAttributes($personId, Attribute::SHOW_IN_CUSTOMERS);
|
||||
|
||||
$mailchimpStatus = $this->request->getPost('mailchimp_status');
|
||||
$this->mailchimpLib->addOrUpdateMember(
|
||||
$this->listId,
|
||||
if ($this->customer->save_customer($person_data, $customer_data, $customer_id)) {
|
||||
// Save customer to Mailchimp selected list // TODO: addOrUpdateMember should be refactored. Potentially pass an array or object instead of 6 parameters.
|
||||
$mailchimp_status = $this->request->getPost('mailchimp_status');
|
||||
$this->mailchimp_lib->addOrUpdateMember(
|
||||
$this->_list_id,
|
||||
$email,
|
||||
$firstName,
|
||||
$lastName,
|
||||
$mailchimpStatus == null ? "" : $mailchimpStatus,
|
||||
$first_name,
|
||||
$last_name,
|
||||
$mailchimp_status == null ? "" : $mailchimp_status,
|
||||
['vip' => $this->request->getPost('mailchimp_vip') != null]
|
||||
);
|
||||
|
||||
if ($customerId == NEW_ENTRY) {
|
||||
// New customer
|
||||
if ($customer_id == NEW_ENTRY) {
|
||||
return $this->response->setJSON([
|
||||
'success' => true,
|
||||
'message' => lang('Customers.successful_adding') . ' ' . $firstName . ' ' . $lastName,
|
||||
'id' => $customerData['person_id']
|
||||
'message' => lang('Customers.successful_adding') . ' ' . $first_name . ' ' . $last_name,
|
||||
'id' => $customer_data['person_id']
|
||||
]);
|
||||
} else {
|
||||
} else { // Existing customer
|
||||
return $this->response->setJSON([
|
||||
'success' => true,
|
||||
'message' => lang('Customers.successful_updating') . ' ' . $firstName . ' ' . $lastName,
|
||||
'id' => $customerId
|
||||
'message' => lang('Customers.successful_updating') . ' ' . $first_name . ' ' . $last_name,
|
||||
'id' => $customer_id
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
} else { // Failure
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'message' => lang('Customers.error_adding_updating') . ' ' . $firstName . ' ' . $lastName,
|
||||
'message' => lang('Customers.error_adding_updating') . ' ' . $first_name . ' ' . $last_name,
|
||||
'id' => NEW_ENTRY
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if an email address already exists. Used in app/Views/customers/form.php
|
||||
*
|
||||
* @return ResponseInterface
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function postCheckEmail(): ResponseInterface
|
||||
{
|
||||
$email = strtolower($this->request->getPost('email', FILTER_SANITIZE_EMAIL));
|
||||
$personId = $this->request->getPost('person_id', FILTER_SANITIZE_NUMBER_INT);
|
||||
$person_id = $this->request->getPost('person_id', FILTER_SANITIZE_NUMBER_INT);
|
||||
|
||||
$exists = $this->customer->check_email_exists($email, $personId);
|
||||
$exists = $this->customer->check_email_exists($email, $person_id);
|
||||
|
||||
return $this->response->setJSON(!$exists ? 'true' : 'false');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if an account number already exists. Used in app/Views/customers/form.php
|
||||
*
|
||||
* @return ResponseInterface
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function postCheckAccountNumber(): ResponseInterface
|
||||
{
|
||||
$exists = $this->customer->check_account_number_exists($this->request->getPost('account_number'), $this->request->getPost('person_id', FILTER_SANITIZE_NUMBER_INT));
|
||||
@@ -302,22 +343,27 @@ class Customers extends Persons
|
||||
return $this->response->setJSON(!$exists ? 'true' : 'false');
|
||||
}
|
||||
|
||||
/**
|
||||
* This deletes customers from the customers table
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function postDelete(): ResponseInterface
|
||||
{
|
||||
$customersToDelete = $this->request->getPost('ids');
|
||||
$customersInfo = $this->customer->get_multiple_info($customersToDelete);
|
||||
$customers_to_delete = $this->request->getPost('ids');
|
||||
$customers_info = $this->customer->get_multiple_info($customers_to_delete);
|
||||
|
||||
$count = 0;
|
||||
|
||||
foreach ($customersInfo->getResult() as $info) {
|
||||
foreach ($customers_info->getResult() as $info) {
|
||||
if ($this->customer->delete($info->person_id)) {
|
||||
$this->mailchimpLib->removeMember($this->listId, $info->email);
|
||||
// remove customer from Mailchimp selected list
|
||||
$this->mailchimp_lib->removeMember($this->_list_id, $info->email);
|
||||
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($count == count($customersToDelete)) {
|
||||
if ($count == count($customers_to_delete)) {
|
||||
return $this->response->setJSON([
|
||||
'success' => true,
|
||||
'message' => lang('Customers.successful_deleted') . ' ' . $count . ' ' . lang('Customers.one_or_multiple')
|
||||
@@ -327,6 +373,12 @@ class Customers extends Persons
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Customers import from csv spreadsheet
|
||||
*
|
||||
* @return DownloadResponse The template for Customer CSV imports is returned and download forced.
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function getCsv(): DownloadResponse
|
||||
{
|
||||
$name = 'importCustomers.csv';
|
||||
@@ -334,17 +386,30 @@ class Customers extends Persons
|
||||
return $this->response->download($name, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the customer CSV import modal. Used in app/Views/people/manage.php
|
||||
*
|
||||
* @return string
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function getCsvImport(): string
|
||||
{
|
||||
return view('customers/form_csv_import');
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a CSV file containing customers. Used in app/Views/customers/form_csv_import.php
|
||||
*
|
||||
* @return ResponseInterface
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function postImportCsvFile(): ResponseInterface
|
||||
{
|
||||
if ($_FILES['file_path']['error'] != UPLOAD_ERR_OK) {
|
||||
return $this->response->setJSON(['success' => false, 'message' => lang('Customers.csv_import_failed')]);
|
||||
} else {
|
||||
if (($handle = fopen($_FILES['file_path']['tmp_name'], 'r')) !== false) {
|
||||
// Skip the first row as it's the table description
|
||||
fgetcsv($handle);
|
||||
$i = 1;
|
||||
|
||||
@@ -355,7 +420,7 @@ class Customers extends Persons
|
||||
|
||||
if (sizeof($data) >= 16 && $consent) {
|
||||
$email = strtolower($data[4]);
|
||||
$personData = [
|
||||
$person_data = [
|
||||
'first_name' => $data[0],
|
||||
'last_name' => $data[1],
|
||||
'gender' => $data[2],
|
||||
@@ -370,7 +435,7 @@ class Customers extends Persons
|
||||
'comments' => $data[12]
|
||||
];
|
||||
|
||||
$customerData = [
|
||||
$customer_data = [
|
||||
'consent' => $consent,
|
||||
'company_name' => $data[13],
|
||||
'discount' => $data[15],
|
||||
@@ -379,13 +444,14 @@ class Customers extends Persons
|
||||
'date' => date('Y-m-d H:i:s'),
|
||||
'employee_id' => $this->employee->get_logged_in_employee_info()->person_id
|
||||
];
|
||||
$accountNumber = $data[14];
|
||||
$account_number = $data[14];
|
||||
|
||||
// Don't duplicate people with same email
|
||||
$invalidated = $this->customer->check_email_exists($email);
|
||||
|
||||
if ($accountNumber != '') {
|
||||
$customerData['account_number'] = $accountNumber;
|
||||
$invalidated &= $this->customer->check_account_number_exists($accountNumber);
|
||||
if ($account_number != '') {
|
||||
$customer_data['account_number'] = $account_number;
|
||||
$invalidated &= $this->customer->check_account_number_exists($account_number);
|
||||
}
|
||||
} else {
|
||||
$invalidated = true;
|
||||
@@ -394,8 +460,9 @@ class Customers extends Persons
|
||||
if ($invalidated) {
|
||||
$failCodes[] = $i;
|
||||
log_message('error', "Row $i was not imported: Either email or account number already exist or data was invalid.");
|
||||
} elseif ($this->customer->save_customer($personData, $customerData)) {
|
||||
$this->mailchimpLib->addOrUpdateMember($this->listId, $personData['email'], $personData['first_name'], '', $personData['last_name']);
|
||||
} elseif ($this->customer->save_customer($person_data, $customer_data)) {
|
||||
// Save customer to Mailchimp selected list
|
||||
$this->mailchimp_lib->addOrUpdateMember($this->_list_id, $person_data['email'], $person_data['first_name'], '', $person_data['last_name']);
|
||||
} else {
|
||||
$failCodes[] = $i;
|
||||
}
|
||||
@@ -415,4 +482,4 @@ class Customers extends Persons
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,18 @@
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\Attribute;
|
||||
use App\Models\Module;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Config\Services;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @property module module
|
||||
*
|
||||
*/
|
||||
class Employees extends Persons
|
||||
{
|
||||
protected Module $module;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('employees');
|
||||
@@ -18,25 +21,35 @@ class Employees extends Persons
|
||||
$this->module = model('Module');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns employee table data rows. This will be called with AJAX.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function getSearch(): ResponseInterface
|
||||
{
|
||||
$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);
|
||||
$sort = $this->sanitizeSortColumn(person_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'people.person_id');
|
||||
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
$sort = $this->sanitizeSortColumn(person_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'people.person_id');
|
||||
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
|
||||
$employees = $this->employee->search($search, $limit, $offset, $sort, $order);
|
||||
$totalRows = $this->employee->get_found_rows($search);
|
||||
$total_rows = $this->employee->get_found_rows($search);
|
||||
|
||||
$dataRows = [];
|
||||
$data_rows = [];
|
||||
foreach ($employees->getResult() as $person) {
|
||||
$dataRows[] = get_person_data_row($person);
|
||||
$data_rows[] = get_person_data_row($person);
|
||||
}
|
||||
|
||||
return $this->response->setJSON(['total' => $totalRows, 'rows' => $dataRows]);
|
||||
return $this->response->setJSON(['total' => $total_rows, 'rows' => $data_rows]);
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX called function gives search suggestions based on what is being searched for.
|
||||
*
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getSuggest(): ResponseInterface
|
||||
{
|
||||
$search = $this->request->getGet('term');
|
||||
@@ -45,7 +58,10 @@ class Employees extends Persons
|
||||
return $this->response->setJSON($suggestions);
|
||||
}
|
||||
|
||||
public function suggestSearch(): ResponseInterface
|
||||
/**
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function suggest_search(): ResponseInterface
|
||||
{
|
||||
$search = $this->request->getPost('term');
|
||||
$suggestions = $this->employee->get_search_suggestions($search);
|
||||
@@ -53,35 +69,39 @@ class Employees extends Persons
|
||||
return $this->response->setJSON($suggestions);
|
||||
}
|
||||
|
||||
public function getView(int $employeeId = NEW_ENTRY): string
|
||||
/**
|
||||
* Loads the employee edit form
|
||||
* @return string
|
||||
*/
|
||||
public function getView(int $employee_id = NEW_ENTRY): string
|
||||
{
|
||||
$personInfo = $this->employee->get_info($employeeId);
|
||||
$currentUser = $this->employee->get_logged_in_employee_info();
|
||||
$person_info = $this->employee->get_info($employee_id);
|
||||
$current_user = $this->employee->get_logged_in_employee_info();
|
||||
|
||||
if ($employeeId != NEW_ENTRY && !$this->employee->canModifyEmployee($personInfo->person_id, $currentUser->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'));
|
||||
exit();
|
||||
}
|
||||
|
||||
foreach (get_object_vars($personInfo) as $property => $value) {
|
||||
$personInfo->$property = $value;
|
||||
foreach (get_object_vars($person_info) as $property => $value) {
|
||||
$person_info->$property = $value;
|
||||
}
|
||||
$data['person_info'] = $personInfo;
|
||||
$data['employee_id'] = $employeeId;
|
||||
$data['person_info'] = $person_info;
|
||||
$data['employee_id'] = $employee_id;
|
||||
|
||||
$modules = [];
|
||||
foreach ($this->module->get_all_modules()->getResult() as $module) {
|
||||
$module->grant = $this->employee->has_grant($module->module_id, $personInfo->person_id);
|
||||
$module->menu_group = $this->employee->get_menu_group($module->module_id, $personInfo->person_id);
|
||||
$module->grant = $this->employee->has_grant($module->module_id, $person_info->person_id);
|
||||
$module->menu_group = $this->employee->get_menu_group($module->module_id, $person_info->person_id);
|
||||
|
||||
$modules[] = $module;
|
||||
}
|
||||
$data['all_modules'] = $modules;
|
||||
|
||||
$permissions = [];
|
||||
foreach ($this->module->get_all_subpermissions()->getResult() as $permission) {
|
||||
foreach ($this->module->get_all_subpermissions()->getResult() as $permission) { // TODO: subpermissions does not follow naming standards.
|
||||
$permission->permission_id = str_replace(' ', '_', $permission->permission_id);
|
||||
$permission->grant = $this->employee->has_grant($permission->permission_id, $personInfo->person_id);
|
||||
$permission->grant = $this->employee->has_grant($permission->permission_id, $person_info->person_id);
|
||||
|
||||
$permissions[] = $permission;
|
||||
}
|
||||
@@ -90,18 +110,17 @@ class Employees extends Persons
|
||||
return view('employees/form', $data);
|
||||
}
|
||||
|
||||
public function getAttributes(int $employeeId = NEW_ENTRY): string
|
||||
/**
|
||||
* Inserts/updates an employee
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function postSave(int $employee_id = NEW_ENTRY): ResponseInterface
|
||||
{
|
||||
return $this->getPersonAttributes($employeeId, Attribute::SHOW_IN_EMPLOYEES);
|
||||
}
|
||||
$current_user = $this->employee->get_logged_in_employee_info();
|
||||
|
||||
public function postSave(int $employeeId = NEW_ENTRY): ResponseInterface
|
||||
{
|
||||
$currentUser = $this->employee->get_logged_in_employee_info();
|
||||
|
||||
if ($employeeId != NEW_ENTRY) {
|
||||
$targetEmployee = $this->employee->get_info($employeeId);
|
||||
if (!$this->employee->canModifyEmployee($targetEmployee->person_id, $currentUser->person_id)) {
|
||||
if ($employee_id != NEW_ENTRY) {
|
||||
$target_employee = $this->employee->get_info($employee_id);
|
||||
if (!$this->employee->canModifyEmployee($target_employee->person_id, $current_user->person_id)) {
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'message' => lang('Employees.error_updating_admin'),
|
||||
@@ -110,16 +129,17 @@ class Employees extends Persons
|
||||
}
|
||||
}
|
||||
|
||||
$firstName = $this->request->getPost('first_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
$lastName = $this->request->getPost('last_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
$first_name = $this->request->getPost('first_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS); // TODO: duplicated code
|
||||
$last_name = $this->request->getPost('last_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
$email = strtolower($this->request->getPost('email', FILTER_SANITIZE_EMAIL));
|
||||
|
||||
$firstName = $this->nameize($firstName);
|
||||
$lastName = $this->nameize($lastName);
|
||||
// format first and last name properly
|
||||
$first_name = $this->nameize($first_name);
|
||||
$last_name = $this->nameize($last_name);
|
||||
|
||||
$personData = [
|
||||
'first_name' => $firstName,
|
||||
'last_name' => $lastName,
|
||||
$person_data = [
|
||||
'first_name' => $first_name,
|
||||
'last_name' => $last_name,
|
||||
'gender' => $this->request->getPost('gender', FILTER_SANITIZE_NUMBER_INT),
|
||||
'email' => $email,
|
||||
'phone_number' => $this->request->getPost('phone_number', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||
@@ -132,98 +152,108 @@ class Employees extends Persons
|
||||
'comments' => $this->request->getPost('comments', FILTER_SANITIZE_FULL_SPECIAL_CHARS)
|
||||
];
|
||||
|
||||
$grantsArray = [];
|
||||
$isAdmin = $this->employee->isAdmin($currentUser->person_id);
|
||||
$grants_array = [];
|
||||
$isAdmin = $this->employee->isAdmin($current_user->person_id);
|
||||
|
||||
foreach ($this->module->get_all_permissions()->getResult() as $permission) {
|
||||
$grants = [];
|
||||
$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 (!$isAdmin && !$this->employee->has_grant($permission->permission_id, $currentUser->person_id)) {
|
||||
if (!$isAdmin && !$this->employee->has_grant($permission->permission_id, $current_user->person_id)) {
|
||||
continue;
|
||||
}
|
||||
$grants['permission_id'] = $permission->permission_id;
|
||||
$grants['menu_group'] = $this->request->getPost('menu_group_' . $permission->permission_id) != null ? $this->request->getPost('menu_group_' . $permission->permission_id, FILTER_SANITIZE_FULL_SPECIAL_CHARS) : '--';
|
||||
$grantsArray[] = $grants;
|
||||
$grants_array[] = $grants;
|
||||
}
|
||||
}
|
||||
|
||||
// Password has been changed OR first time password set
|
||||
if (!empty($this->request->getPost('password')) && ENVIRONMENT != 'testing') {
|
||||
$exploded = explode(":", $this->request->getPost('language', FILTER_SANITIZE_FULL_SPECIAL_CHARS));
|
||||
$employeeData = [
|
||||
$employee_data = [
|
||||
'username' => $this->request->getPost('username', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||
'password' => password_hash($this->request->getPost('password'), PASSWORD_DEFAULT),
|
||||
'hash_version' => 2,
|
||||
'language_code' => $exploded[0],
|
||||
'language' => $exploded[1]
|
||||
];
|
||||
} else {
|
||||
} else { // Password not changed
|
||||
$exploded = explode(":", $this->request->getPost('language', FILTER_SANITIZE_FULL_SPECIAL_CHARS));
|
||||
$employeeData = [
|
||||
$employee_data = [
|
||||
'username' => $this->request->getPost('username', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||
'language_code' => $exploded[0],
|
||||
'language' => $exploded[1]
|
||||
];
|
||||
}
|
||||
|
||||
if ($this->employee->save_employee($personData, $employeeData, $grantsArray, $employeeId)) {
|
||||
$personId = $employeeId == NEW_ENTRY ? $employeeData['person_id'] : $employeeId;
|
||||
$this->savePersonAttributes($personId, Attribute::SHOW_IN_EMPLOYEES);
|
||||
|
||||
if ($employeeId == NEW_ENTRY) {
|
||||
if ($this->employee->save_employee($person_data, $employee_data, $grants_array, $employee_id)) {
|
||||
// New employee
|
||||
if ($employee_id == NEW_ENTRY) {
|
||||
return $this->response->setJSON([
|
||||
'success' => true,
|
||||
'message' => lang('Employees.successful_adding') . ' ' . $firstName . ' ' . $lastName,
|
||||
'id' => $employeeData['person_id']
|
||||
'message' => lang('Employees.successful_adding') . ' ' . $first_name . ' ' . $last_name,
|
||||
'id' => $employee_data['person_id']
|
||||
]);
|
||||
} else {
|
||||
$loggedInEmployeeId = session()->get('person_id');
|
||||
if ($employeeId == $loggedInEmployeeId) {
|
||||
session()->set('language_code', $employeeData['language_code']);
|
||||
session()->set('language', $employeeData['language']);
|
||||
} else { // Existing employee
|
||||
$logged_in_employee_id = session()->get('person_id');
|
||||
if ($employee_id == $logged_in_employee_id) {
|
||||
session()->set('language_code', $employee_data['language_code']);
|
||||
session()->set('language', $employee_data['language']);
|
||||
}
|
||||
return $this->response->setJSON([
|
||||
'success' => true,
|
||||
'message' => lang('Employees.successful_updating') . ' ' . $firstName . ' ' . $lastName,
|
||||
'id' => $employeeId
|
||||
'message' => lang('Employees.successful_updating') . ' ' . $first_name . ' ' . $last_name,
|
||||
'id' => $employee_id
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
} else { // Failure
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'message' => lang('Employees.error_adding_updating') . ' ' . $firstName . ' ' . $lastName,
|
||||
'message' => lang('Employees.error_adding_updating') . ' ' . $first_name . ' ' . $last_name,
|
||||
'id' => NEW_ENTRY
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This deletes employees from the employees table
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function postDelete(): ResponseInterface
|
||||
{
|
||||
$employeesToDelete = $this->request->getPost('ids', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
$currentUser = $this->employee->get_logged_in_employee_info();
|
||||
$employees_to_delete = $this->request->getPost('ids', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
$current_user = $this->employee->get_logged_in_employee_info();
|
||||
|
||||
if (!$this->employee->isAdmin($currentUser->person_id)) {
|
||||
foreach ($employeesToDelete as $empId) {
|
||||
if ($this->employee->isAdmin((int)$empId)) {
|
||||
if (!$this->employee->isAdmin($current_user->person_id)) {
|
||||
foreach ($employees_to_delete as $emp_id) {
|
||||
if ($this->employee->isAdmin((int)$emp_id)) {
|
||||
return $this->response->setJSON(['success' => false, 'message' => lang('Employees.error_deleting_admin')]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->employee->delete_list($employeesToDelete)) {
|
||||
if ($this->employee->delete_list($employees_to_delete)) { // TODO: this is passing a string, but delete_list expects an array
|
||||
return $this->response->setJSON([
|
||||
'success' => true,
|
||||
'message' => lang('Employees.successful_deleted') . ' ' . count($employeesToDelete) . ' ' . lang('Employees.one_or_multiple')
|
||||
'message' => lang('Employees.successful_deleted') . ' ' . count($employees_to_delete) . ' ' . lang('Employees.one_or_multiple')
|
||||
]);
|
||||
} else {
|
||||
return $this->response->setJSON(['success' => false, 'message' => lang('Employees.cannot_be_deleted')]);
|
||||
}
|
||||
}
|
||||
|
||||
public function getCheckUsername($employeeId): ResponseInterface
|
||||
/**
|
||||
* Checks an employee username against the database. Used in app\Views\employees\form.php
|
||||
*
|
||||
* @param $employee_id
|
||||
* @return ResponseInterface
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function getCheckUsername($employee_id): ResponseInterface
|
||||
{
|
||||
$exists = $this->employee->username_exists($employeeId, $this->request->getGet('username'));
|
||||
$exists = $this->employee->username_exists($employee_id, $this->request->getGet('username'));
|
||||
return $this->response->setJSON(!$exists ? 'true' : 'false');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,9 +38,6 @@ class Expenses extends Secure_Controller
|
||||
'is_deleted' => lang('Expenses.is_deleted')
|
||||
];
|
||||
|
||||
// Restore filters from URL
|
||||
$data = array_merge($data, restoreTableFilters($this->request));
|
||||
|
||||
return view('expenses/manage', $data);
|
||||
}
|
||||
|
||||
@@ -93,23 +90,16 @@ class Expenses extends Secure_Controller
|
||||
{
|
||||
$data = []; // TODO: Duplicated code
|
||||
|
||||
$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;
|
||||
foreach ($this->employee->get_all()->getResult() as $employee) {
|
||||
foreach (get_object_vars($employee) as $property => $value) {
|
||||
$employee->$property = $value;
|
||||
}
|
||||
} 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['employees'][$employee->person_id] = $employee->first_name . ' ' . $employee->last_name;
|
||||
}
|
||||
$data['can_assign_employee'] = $can_assign_employee;
|
||||
|
||||
$data['expenses_info'] = $this->expense->get_info($expense_id);
|
||||
|
||||
$expense_categories = [];
|
||||
foreach ($this->expense_category->get_all(0, 0, true)->getResultArray() as $row) {
|
||||
@@ -117,9 +107,11 @@ class Expenses extends Secure_Controller
|
||||
}
|
||||
$data['expense_categories'] = $expense_categories;
|
||||
|
||||
$expense_id = $data['expenses_info']->expense_id;
|
||||
|
||||
if ($expense_id == NEW_ENTRY) {
|
||||
$data['expenses_info']->date = date('Y-m-d H:i:s');
|
||||
$data['expenses_info']->employee_id = $current_employee_id;
|
||||
$data['expenses_info']->employee_id = $this->employee->get_logged_in_employee_info()->person_id;
|
||||
}
|
||||
|
||||
$data['payments'] = [];
|
||||
@@ -160,20 +152,6 @@ class Expenses extends Secure_Controller
|
||||
|
||||
$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 = [
|
||||
'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),
|
||||
@@ -183,7 +161,7 @@ class Expenses extends Secure_Controller
|
||||
'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),
|
||||
'description' => $this->request->getPost('description', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||
'employee_id' => $employee_id,
|
||||
'employee_id' => $this->request->getPost('employee_id', FILTER_SANITIZE_NUMBER_INT),
|
||||
'deleted' => $this->request->getPost('deleted') != null
|
||||
];
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Libraries\MY_Migration;
|
||||
use CodeIgniter\HTTP\RedirectResponse;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
@@ -35,20 +34,21 @@ class Home extends Secure_Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the "change employee password" form
|
||||
* Load "change employee password" form
|
||||
*
|
||||
* @param int $employeeId
|
||||
* @return ResponseInterface|string
|
||||
* @return string
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function getChangePassword(int $employeeId = NEW_ENTRY): ResponseInterface|string
|
||||
public function getChangePassword(int $employeeId = NEW_ENTRY): string
|
||||
{
|
||||
$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'));
|
||||
if (!$this->employee->can_modify_employee($employeeId, $currentPersonId)) {
|
||||
header('Location: ' . base_url('no_access/home/home'));
|
||||
exit();
|
||||
}
|
||||
|
||||
$person_info = $this->employee->get_info($employeeId);
|
||||
@@ -71,7 +71,7 @@ class Home extends Secure_Controller
|
||||
|
||||
$employeeId = $employeeId === NEW_ENTRY ? $currentUser->person_id : $employeeId;
|
||||
|
||||
if (!$this->employee->isAdmin($currentUser->person_id) && $employeeId !== $currentUser->person_id) {
|
||||
if (!$this->employee->can_modify_employee($employeeId, $currentUser->person_id)) {
|
||||
return $this->response->setStatusCode(403)->setJSON([
|
||||
'success' => false,
|
||||
'message' => lang('Employees.unauthorized_modify')
|
||||
@@ -82,7 +82,7 @@ class Home extends Secure_Controller
|
||||
if ($this->employee->check_password($this->request->getPost('username', FILTER_SANITIZE_FULL_SPECIAL_CHARS), $this->request->getPost('current_password'))) {
|
||||
// Validate password length BEFORE hashing
|
||||
$new_password = $this->request->getPost('password');
|
||||
|
||||
|
||||
if (strlen($new_password) < 8) {
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
@@ -90,7 +90,7 @@ class Home extends Secure_Controller
|
||||
'id' => NEW_ENTRY
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
$employee_data = [
|
||||
'username' => $this->request->getPost('username', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||
'password' => password_hash($new_password, PASSWORD_DEFAULT),
|
||||
@@ -125,4 +125,4 @@ class Home extends Secure_Controller
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Controllers;
|
||||
|
||||
use App\Libraries\Barcode_lib;
|
||||
use App\Libraries\Item_lib;
|
||||
|
||||
use App\Models\Attribute;
|
||||
use App\Models\Inventory;
|
||||
use App\Models\Item;
|
||||
@@ -13,6 +14,7 @@ use App\Models\Item_taxes;
|
||||
use App\Models\Stock_location;
|
||||
use App\Models\Supplier;
|
||||
use App\Models\Tax_category;
|
||||
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use CodeIgniter\Images\Handlers\BaseHandler;
|
||||
use CodeIgniter\HTTP\DownloadResponse;
|
||||
@@ -71,12 +73,7 @@ class Items extends Secure_Controller
|
||||
$this->session->set('allow_temp_items', 0);
|
||||
|
||||
$data['table_headers'] = get_items_manage_table_headers();
|
||||
|
||||
// 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_location'] = $this->item_lib->get_item_location();
|
||||
$data['stock_locations'] = $this->stock_location->get_allowed_locations();
|
||||
|
||||
// Filters that will be loaded in the multiselect dropdown
|
||||
@@ -90,9 +87,6 @@ class Items extends Secure_Controller
|
||||
'temporary' => lang('Items.temp')
|
||||
];
|
||||
|
||||
// Restore filters from URL
|
||||
$data = array_merge($data, restoreTableFilters($this->request));
|
||||
|
||||
return view('items/manage', $data);
|
||||
}
|
||||
|
||||
@@ -506,7 +500,7 @@ class Items extends Secure_Controller
|
||||
$data['definition_names'] = $this->attribute->get_definition_names();
|
||||
|
||||
foreach ($data['definition_values'] as $definition_id => $definition_value) {
|
||||
$attribute_value = $this->attribute->getAttributeValue($item_id, $definition_id);
|
||||
$attribute_value = $this->attribute->get_attribute_value($item_id, $definition_id);
|
||||
$attribute_id = (empty($attribute_value) || empty($attribute_value->attribute_id)) ? null : $attribute_value->attribute_id;
|
||||
$values = &$data['definition_values'][$definition_id];
|
||||
$values['attribute_id'] = $attribute_id;
|
||||
@@ -542,7 +536,7 @@ class Items extends Secure_Controller
|
||||
$data['definition_names'] = $this->attribute->get_definition_names();
|
||||
|
||||
foreach ($data['definition_values'] as $definition_id => $definition_value) {
|
||||
$attribute_value = $this->attribute->getAttributeValue($item_id, $definition_id);
|
||||
$attribute_value = $this->attribute->get_attribute_value($item_id, $definition_id);
|
||||
$attribute_id = (empty($attribute_value) || empty($attribute_value->attribute_id)) ? null : $attribute_value->attribute_id;
|
||||
$values = &$data['definition_values'][$definition_id];
|
||||
$values['attribute_id'] = $attribute_id;
|
||||
@@ -711,7 +705,7 @@ class Items extends Secure_Controller
|
||||
$item_quantity = $this->item_quantity->get_item_quantity($item_id, $location['location_id']);
|
||||
|
||||
if ($item_quantity->quantity != $updated_quantity || $new_item) {
|
||||
$success = $success && $this->item_quantity->save_value($location_detail, $item_id, $location['location_id']);
|
||||
$success &= $this->item_quantity->save_value($location_detail, $item_id, $location['location_id']);
|
||||
|
||||
$inv_data = [
|
||||
'trans_date' => date('Y-m-d H:i:s'),
|
||||
@@ -722,10 +716,10 @@ class Items extends Secure_Controller
|
||||
'trans_inventory' => $updated_quantity - $item_quantity->quantity
|
||||
];
|
||||
|
||||
$success = $success && $this->inventory->insert($inv_data, false);
|
||||
$success &= $this->inventory->insert($inv_data, false);
|
||||
}
|
||||
}
|
||||
$success = $success && $this->saveItemAttributes($item_id);
|
||||
$this->saveItemAttributes($item_id);
|
||||
|
||||
if ($success && $upload_success) {
|
||||
$message = lang('Items.successful_' . ($new_item ? 'adding' : 'updating')) . ' ' . $item_data['name'];
|
||||
@@ -775,7 +769,7 @@ class Items extends Secure_Controller
|
||||
|
||||
$filename = $file->getClientName();
|
||||
$info = pathinfo($filename);
|
||||
|
||||
|
||||
// Sanitize filename to remove problematic characters like spaces
|
||||
$sanitized_name = preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $info['filename']);
|
||||
|
||||
@@ -882,12 +876,12 @@ class Items extends Secure_Controller
|
||||
$items_to_update = $this->request->getPost('item_ids');
|
||||
$item_data = [];
|
||||
|
||||
foreach (Item::ALLOWED_BULK_EDIT_FIELDS as $field) {
|
||||
$value = $this->request->getPost($field);
|
||||
if ($field === 'supplier_id' && $value !== '') {
|
||||
$item_data[$field] = $value;
|
||||
} elseif ($value !== null && $value !== '') {
|
||||
$item_data[$field] = $value;
|
||||
foreach ($_POST as $key => $value) {
|
||||
// This field is nullable, so treat it differently
|
||||
if ($key === 'supplier_id' && $value !== '') {
|
||||
$item_data[$key] = $value;
|
||||
} elseif ($value !== '' && !(in_array($key, ['item_ids', 'tax_names', 'tax_percents']))) {
|
||||
$item_data[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -938,7 +932,7 @@ class Items extends Secure_Controller
|
||||
*/
|
||||
public function getGenerateCsvFile(): DownloadResponse
|
||||
{
|
||||
helper('importfile');
|
||||
helper('importfile_helper');
|
||||
$name = 'import_items.csv';
|
||||
$allowed_locations = $this->stock_location->get_allowed_locations();
|
||||
$allowed_attributes = $this->attribute->get_definition_names();
|
||||
@@ -957,13 +951,14 @@ class Items extends Secure_Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports items from a CSV formatted file.
|
||||
* Imports items from CSV formatted file.
|
||||
* @return ResponseInterface
|
||||
* @throws ReflectionException
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function postImportCsvFile(): ResponseInterface
|
||||
{
|
||||
helper('importfile');
|
||||
helper('importfile_helper');
|
||||
try {
|
||||
if ($_FILES['file_path']['error'] !== UPLOAD_ERR_OK) {
|
||||
return $this->response->setJSON(['success' => false, 'message' => lang('Items.csv_import_failed')]);
|
||||
@@ -972,33 +967,33 @@ class Items extends Secure_Controller
|
||||
set_time_limit(240);
|
||||
|
||||
$failCodes = [];
|
||||
$csvRows = get_csv_file($_FILES['file_path']['tmp_name']);
|
||||
$employeeId = $this->employee->get_logged_in_employee_info()->person_id;
|
||||
$allowedStockLocations = $this->stock_location->get_allowed_locations();
|
||||
$attributeDefinitionNames = $this->attribute->get_definition_names();
|
||||
$csv_rows = get_csv_file($_FILES['file_path']['tmp_name']);
|
||||
$employee_id = $this->employee->get_logged_in_employee_info()->person_id;
|
||||
$allowed_stock_locations = $this->stock_location->get_allowed_locations();
|
||||
$attribute_definition_names = $this->attribute->get_definition_names();
|
||||
|
||||
unset($attributeDefinitionNames[NEW_ENTRY]); // Removes the common_none_selected_text from the array
|
||||
unset($attribute_definition_names[NEW_ENTRY]); // Removes the common_none_selected_text from the array
|
||||
|
||||
$attributeData = [];
|
||||
$attribute_data = [];
|
||||
|
||||
foreach ($attributeDefinitionNames as $definitionName) {
|
||||
$attributeData[$definitionName] = $this->attribute->get_definition_by_name($definitionName)[0];
|
||||
foreach ($attribute_definition_names as $definition_name) {
|
||||
$attribute_data[$definition_name] = $this->attribute->get_definition_by_name($definition_name)[0];
|
||||
|
||||
if ($attributeData[$definitionName]['definition_type'] === DROPDOWN) {
|
||||
$attributeData[$definitionName]['dropdown_values'] = $this->attribute->get_definition_values($attributeData[$definitionName]['definition_id']);
|
||||
if ($attribute_data[$definition_name]['definition_type'] === DROPDOWN) {
|
||||
$attribute_data[$definition_name]['dropdown_values'] = $this->attribute->get_definition_values($attribute_data[$definition_name]['definition_id']);
|
||||
}
|
||||
}
|
||||
$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
|
||||
|
||||
foreach ($csvRows as $key => $row) {
|
||||
$isFailedRow = false;
|
||||
$itemId = (int)$row['Id'];
|
||||
$isUpdate = ($itemId > 0);
|
||||
$itemData = [
|
||||
'item_id' => $itemId,
|
||||
foreach ($csv_rows as $key => $row) {
|
||||
$is_failed_row = false;
|
||||
$item_id = (int)$row['Id'];
|
||||
$is_update = ($item_id > 0);
|
||||
$item_data = [
|
||||
'item_id' => $item_id,
|
||||
'name' => $row['Item Name'],
|
||||
'description' => filter_var($row['Description'], FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||
'description' => $row['Description'],
|
||||
'category' => $row['Category'],
|
||||
'cost_price' => $row['Cost Price'],
|
||||
'unit_price' => $row['Unit Price'],
|
||||
@@ -1008,26 +1003,25 @@ class Items extends Secure_Controller
|
||||
'pic_filename' => $row['Image']
|
||||
];
|
||||
|
||||
if (!empty($row['Supplier ID'])) {
|
||||
$itemData['supplier_id'] = $this->supplier->exists($row['Supplier ID']) ? $row['Supplier ID'] : null;
|
||||
if (!empty($row['supplier ID'])) {
|
||||
$item_data['supplier_id'] = $this->supplier->exists($row['Supplier ID']) ? $row['Supplier ID'] : null;
|
||||
}
|
||||
|
||||
if ($isUpdate) {
|
||||
$itemData['allow_alt_description'] = $row['Allow Alt Description'] === '' ? null : $row['Allow Alt Description'];
|
||||
$itemData['is_serialized'] = $row['Item has Serial Number'] === '' ? null : $row['Item has Serial Number'];
|
||||
if ($is_update) {
|
||||
$item_data['allow_alt_description'] = empty($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'];
|
||||
} else {
|
||||
$itemData['allow_alt_description'] = $row['Allow Alt Description'] === '' ? '0' : '1';
|
||||
$itemData['is_serialized'] = $row['Item has Serial Number'] === '' ? '0' : '1';
|
||||
$item_data['allow_alt_description'] = empty($row['Allow Alt Description']) ? '0' : '1';
|
||||
$item_data['is_serialized'] = empty($row['Item has Serial Number']) ? '0' : '1';
|
||||
}
|
||||
|
||||
if (!empty($row['Barcode'])) {
|
||||
$itemData['item_number'] = $row['Barcode'];
|
||||
$isFailedRow = $this->item->item_number_exists($itemData['item_number'], $itemId);
|
||||
if (!empty($row['Barcode']) && !$is_update) {
|
||||
$item_data['item_number'] = $row['Barcode'];
|
||||
$is_failed_row = $this->item->item_number_exists($item_data['item_number']);
|
||||
}
|
||||
|
||||
if (!$isFailedRow) {
|
||||
$allowedStockLocations = $this->stock_location->get_allowed_locations();
|
||||
$isFailedRow = $this->validateCSVData($row, $itemData, $allowedStockLocations, $attributeDefinitionNames, $attributeData);
|
||||
if (!$is_failed_row) {
|
||||
$invalidLocations = $this->validateCSVStockLocations($row, $allowedStockLocations);
|
||||
if (!empty($invalidLocations)) {
|
||||
$isFailedRow = true;
|
||||
log_message('error', 'CSV import: Invalid stock location(s) found: ' . implode(', ', $invalidLocations));
|
||||
@@ -1035,35 +1029,28 @@ class Items extends Secure_Controller
|
||||
}
|
||||
|
||||
// Remove false, null, '' and empty strings but keep 0
|
||||
$itemData = array_filter($itemData, function ($value) {
|
||||
$item_data = array_filter($item_data, function ($value) {
|
||||
return $value !== null && strlen($value);
|
||||
});
|
||||
|
||||
if (!$isFailedRow && $this->item->save_value($itemData, $itemId)) {
|
||||
$this->save_tax_data($row, $itemData);
|
||||
$this->save_inventory_quantities($row, $itemData, $allowedStockLocations, $employeeId);
|
||||
$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_failed_row && $this->item->save_value($item_data, $item_id)) {
|
||||
$this->save_tax_data($row, $item_data);
|
||||
$this->save_inventory_quantities($row, $item_data, $allowed_stock_locations, $employee_id);
|
||||
$is_failed_row = $this->save_attribute_data($row, $item_data, $attribute_data); // TODO: $is_failed_row never gets used after this.
|
||||
|
||||
if ($isUpdate) {
|
||||
$itemData = array_merge($itemData, get_object_vars($this->item->get_info_by_id_or_number($itemId)));
|
||||
if ($is_update) {
|
||||
$item_data = array_merge($item_data, get_object_vars($this->item->get_info_by_id_or_number($item_id)));
|
||||
}
|
||||
} else {
|
||||
$failedRow = $key + 2;
|
||||
$failCodes[] = $failedRow;
|
||||
log_message('error', "CSV Item import failed on line $failedRow. This item was not imported.");
|
||||
$failed_row = $key + 2;
|
||||
$failCodes[] = $failed_row;
|
||||
log_message('error', "CSV Item import failed on line $failed_row. This item was not imported.");
|
||||
}
|
||||
|
||||
unset($csvRows[$key]);
|
||||
unset($csv_rows[$key]);
|
||||
}
|
||||
|
||||
$csvRows = null;
|
||||
$csv_rows = null;
|
||||
|
||||
if (count($failCodes) > 0) {
|
||||
$message = lang('Items.csv_import_partially_failed', [count($failCodes), implode(', ', $failCodes)]);
|
||||
@@ -1071,7 +1058,6 @@ class Items extends Secure_Controller
|
||||
return $this->response->setJSON(['success' => false, 'message' => $message]);
|
||||
} else {
|
||||
$db->transCommit();
|
||||
$this->attribute->deleteOrphanedValues();
|
||||
|
||||
return $this->response->setJSON(['success' => true, 'message' => lang('Items.csv_import_success')]);
|
||||
}
|
||||
@@ -1085,20 +1071,6 @@ 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
|
||||
*
|
||||
@@ -1127,99 +1099,87 @@ class Items extends Secure_Controller
|
||||
* Checks the entire line of data in an import file for errors
|
||||
*
|
||||
* @param array $row
|
||||
* @param array $itemData
|
||||
* @param array $allowedStockLocations
|
||||
* @param array $definitionNames
|
||||
* @param array $attributeData
|
||||
* @param array $item_data
|
||||
* @param array $allowed_locations
|
||||
* @param array $definition_names
|
||||
* @param array $attribute_data
|
||||
* @return bool Returns false if all data checks out and true when there is an error in the data
|
||||
*/
|
||||
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
|
||||
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
|
||||
{
|
||||
$itemId = $row['Id'];
|
||||
$isUpdate = (bool)$itemId;
|
||||
$item_id = $row['Id'];
|
||||
$is_update = (bool)$item_id;
|
||||
|
||||
// Check for empty required fields
|
||||
$valuesToCheckForEmpty = [
|
||||
'name' => $itemData['name'],
|
||||
'category' => $itemData['category'],
|
||||
'unit_price' => $itemData['unit_price']
|
||||
$check_for_empty = [
|
||||
'name' => $item_data['name'],
|
||||
'category' => $item_data['category'],
|
||||
'unit_price' => $item_data['unit_price']
|
||||
];
|
||||
|
||||
foreach ($valuesToCheckForEmpty as $key => $value) {
|
||||
if (($value === null || $value === '') && !$isUpdate) {
|
||||
foreach ($check_for_empty as $key => $val) {
|
||||
if (empty($val) && !$is_update) {
|
||||
log_message('error', "Empty required value in $key.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isUpdate) {
|
||||
$itemData['cost_price'] = empty($itemData['cost_price']) ? 0 : $itemData['cost_price']; // Allow for zero wholesale price
|
||||
if (!$is_update) {
|
||||
$item_data['cost_price'] = empty($item_data['cost_price']) ? 0 : $item_data['cost_price']; // Allow for zero wholesale price
|
||||
} else {
|
||||
if (!$this->item->exists($itemId)) {
|
||||
log_message('error', "non-existent item_id: '$itemId' when either existing item_id or no item_id is required.");
|
||||
if (!$this->item->exists($item_id)) {
|
||||
log_message('error', "non-existent item_id: '$item_id' when either existing item_id or no item_id is required.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Build array of fields to check for numerics
|
||||
$valuesToCheckForNumeric = [
|
||||
'cost_price' => $itemData['cost_price'],
|
||||
'unit_price' => $itemData['unit_price'],
|
||||
'reorder_level' => $itemData['reorder_level'],
|
||||
$check_for_numeric_values = [
|
||||
'cost_price' => $item_data['cost_price'],
|
||||
'unit_price' => $item_data['unit_price'],
|
||||
'reorder_level' => $item_data['reorder_level'],
|
||||
'supplier_id' => $row['Supplier ID'],
|
||||
'Tax 1 Percent' => $row['Tax 1 Percent'],
|
||||
'Tax 2 Percent' => $row['Tax 2 Percent']
|
||||
];
|
||||
|
||||
foreach ($allowedStockLocations as $location_name) {
|
||||
$valuesToCheckForNumeric[] = $row["location_$location_name"];
|
||||
foreach ($allowed_locations as $location_name) {
|
||||
$check_for_numeric_values[] = $row["location_$location_name"];
|
||||
}
|
||||
|
||||
// Check for non-numeric values which require numeric
|
||||
foreach ($valuesToCheckForNumeric as $key => $value) {
|
||||
foreach ($check_for_numeric_values as $key => $value) {
|
||||
if (!is_numeric($value) && !empty($value)) {
|
||||
log_message('error', "non-numeric: '$value' for '$key' when numeric is required");
|
||||
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
|
||||
foreach ($definitionNames as $definitionName) {
|
||||
$attributeColumn = "attribute_$definitionName";
|
||||
if (array_key_exists($attributeColumn, $row) && $row[$attributeColumn] != '') {
|
||||
$definitionType = $attributeData[$definitionName]['definition_type'];
|
||||
$attributeValue = $row[$attributeColumn];
|
||||
foreach ($definition_names as $definition_name) {
|
||||
if (!empty($row["attribute_$definition_name"])) {
|
||||
$definition_type = $attribute_data[$definition_name]['definition_type'];
|
||||
$attribute_value = $row["attribute_$definition_name"];
|
||||
|
||||
if (strcasecmp($attributeValue, '_DELETE_') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch ($definitionType) {
|
||||
switch ($definition_type) {
|
||||
case DROPDOWN:
|
||||
$dropdownValues = $attributeData[$definitionName]['dropdown_values'];
|
||||
$dropdownValues[] = '';
|
||||
$dropdown_values = $attribute_data[$definition_name]['dropdown_values'];
|
||||
$dropdown_values[] = '';
|
||||
|
||||
if (!empty($attributeValue) && !in_array($attributeValue, $dropdownValues)) {
|
||||
log_message('error', "Value: '$attributeValue' is not an acceptable DROPDOWN value");
|
||||
if (!empty($attribute_value) && !in_array($attribute_value, $dropdown_values)) {
|
||||
log_message('error', "Value: '$attribute_value' is not an acceptable DROPDOWN value");
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case DECIMAL:
|
||||
if (!is_numeric($attributeValue) && !empty($attributeValue)) {
|
||||
log_message('error', "'$attributeValue' is not an acceptable DECIMAL value");
|
||||
if (!is_numeric($attribute_value) && !empty($attribute_value)) {
|
||||
log_message('error', "'$attribute_value' is not an acceptable DECIMAL value");
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case DATE:
|
||||
if (!valid_date($attributeValue) && !empty($attributeValue)) {
|
||||
log_message('error', "'$attributeValue' is not an acceptable DATE value. The value must match the set locale.");
|
||||
if (!valid_date($attribute_value) && !empty($attribute_value)) {
|
||||
log_message('error', "'$attribute_value' is not an acceptable DATE value. The value must match the set locale.");
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
@@ -1230,6 +1190,59 @@ class Items extends Secure_Controller
|
||||
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.
|
||||
*
|
||||
@@ -1323,11 +1336,10 @@ class Items extends Secure_Controller
|
||||
* Saves item attributes for a given item.
|
||||
*
|
||||
* @param int $itemId The item for which attributes need to be saved to.
|
||||
* @return bool Returns true when item attributes are successfully saved and false on error.
|
||||
* @return void
|
||||
*/
|
||||
public function saveItemAttributes(int $itemId): bool
|
||||
public function saveItemAttributes(int $itemId): void
|
||||
{
|
||||
$success = true;
|
||||
$attributeLinks = $this->request->getPost('attribute_links') ?? [];
|
||||
$attributeIds = $this->request->getPost('attribute_ids');
|
||||
|
||||
@@ -1339,18 +1351,16 @@ class Items extends Secure_Controller
|
||||
switch ($definitionType) {
|
||||
case DROPDOWN:
|
||||
$attributeId = $attributeValue;
|
||||
$success = $success && $this->attribute->saveAttributeLink($itemId, $definitionId, $attributeId);
|
||||
break;
|
||||
case DECIMAL:
|
||||
$attributeValue = parse_decimals($attributeValue);
|
||||
// no break
|
||||
// Fall through to save the attribute value
|
||||
default:
|
||||
$attributeId = $this->attribute->saveAttributeValue($attributeValue, $definitionId, $itemId, $attributeIds[$definitionId], $definitionType);
|
||||
$success = $success && ($attributeId > 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $success && $this->attribute->deleteOrphanedValues();
|
||||
$this->attribute->saveAttributeLink($itemId, $definitionId, $attributeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace App\Controllers;
|
||||
use App\Libraries\MY_Migration;
|
||||
use App\Models\Employee;
|
||||
use CodeIgniter\HTTP\RedirectResponse;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use CodeIgniter\Model;
|
||||
use Config\OSPOS;
|
||||
use Config\Services;
|
||||
@@ -37,7 +36,6 @@ class Login extends BaseController
|
||||
|
||||
$data = [
|
||||
'has_errors' => false,
|
||||
'is_new_install' => !(MY_Migration::get_current_version()),
|
||||
'is_latest' => $migration->is_latest(),
|
||||
'latest_version' => $migration->get_latest_migration(),
|
||||
'gcaptcha_enabled' => $gcaptcha_enabled,
|
||||
@@ -73,28 +71,4 @@ class Login extends BaseController
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,28 +2,28 @@
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\Attribute;
|
||||
use App\Models\Person;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Config\OSPOS;
|
||||
use Config\Services;
|
||||
use function Tamtamchik\NameCase\str_name_case;
|
||||
|
||||
abstract class Persons extends Secure_Controller
|
||||
{
|
||||
protected Person $person;
|
||||
protected Attribute $attribute;
|
||||
protected array $appConfig;
|
||||
|
||||
public function __construct(?string $moduleId = null)
|
||||
/**
|
||||
* @param string|null $module_id
|
||||
*/
|
||||
public function __construct(?string $module_id = null)
|
||||
{
|
||||
parent::__construct($moduleId);
|
||||
parent::__construct($module_id);
|
||||
|
||||
$this->person = model(Person::class);
|
||||
$this->attribute = model(Attribute::class);
|
||||
$this->appConfig = config(OSPOS::class)->settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getIndex(): string
|
||||
{
|
||||
$data['table_headers'] = get_people_manage_table_headers();
|
||||
@@ -31,6 +31,10 @@ abstract class Persons extends Secure_Controller
|
||||
return view('people/manage', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives search suggestions based on what is being searched for
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getSuggest(): ResponseInterface
|
||||
{
|
||||
$search = $this->request->getGet('term');
|
||||
@@ -39,88 +43,34 @@ abstract class Persons extends Secure_Controller
|
||||
return $this->response->setJSON($suggestions);
|
||||
}
|
||||
|
||||
public function getRow(int $rowId): ResponseInterface
|
||||
/**
|
||||
* Gets one row for a person manage table. This is called using AJAX to update one row.
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getRow(int $row_id): ResponseInterface
|
||||
{
|
||||
$dataRow = get_person_data_row($this->person->get_info($rowId));
|
||||
$data_row = get_person_data_row($this->person->get_info($row_id));
|
||||
|
||||
return $this->response->setJSON($dataRow);
|
||||
}
|
||||
|
||||
protected function getPersonAttributes(int $personId, int $definitionFlags): string
|
||||
{
|
||||
$data['person_id'] = $personId;
|
||||
$data['config'] = $this->appConfig;
|
||||
$definitionIds = json_decode($this->request->getGet('definition_ids') ?? '', true);
|
||||
$data['definition_values'] = $this->attribute->getAttributesByPerson($personId) + $this->attribute->get_values_by_definitions($definitionIds);
|
||||
$data['definition_names'] = $this->attribute->getDefinitionsByType(true, $definitionFlags);
|
||||
|
||||
foreach ($data['definition_values'] as $definitionId => $definitionValue) {
|
||||
$attributeValue = $this->attribute->getPersonAttributeValue($personId, $definitionId);
|
||||
$attributeId = (empty($attributeValue) || empty($attributeValue->attribute_id)) ? null : $attributeValue->attribute_id;
|
||||
$values = &$data['definition_values'][$definitionId];
|
||||
$values['attribute_id'] = $attributeId;
|
||||
$values['attribute_value'] = $attributeValue;
|
||||
$values['selected_value'] = '';
|
||||
|
||||
if ($definitionValue['definition_type'] === DROPDOWN) {
|
||||
$values['values'] = $this->attribute->get_definition_values($definitionId);
|
||||
$linkValue = $this->getPersonLinkValue($personId, $definitionId);
|
||||
$values['selected_value'] = (empty($linkValue)) ? '' : $linkValue->attribute_id;
|
||||
}
|
||||
|
||||
if (!empty($definitionIds[$definitionId])) {
|
||||
$values['selected_value'] = $definitionIds[$definitionId];
|
||||
}
|
||||
|
||||
unset($data['definition_names'][$definitionId]);
|
||||
}
|
||||
|
||||
return view('attributes/person', $data);
|
||||
}
|
||||
|
||||
private function getPersonLinkValue(int $personId, int $definitionId): ?object
|
||||
{
|
||||
$builder = $this->db->table('attribute_links');
|
||||
$builder->where('person_id', $personId);
|
||||
$builder->where('item_id', null);
|
||||
$builder->where('sale_id', null);
|
||||
$builder->where('receiving_id', null);
|
||||
$builder->where('definition_id', $definitionId);
|
||||
|
||||
return $builder->get()->getRowObject();
|
||||
}
|
||||
|
||||
protected function savePersonAttributes(int $personId, int $definitionFlags): void
|
||||
{
|
||||
$attributeLinks = $this->request->getPost('attribute_links') ?? [];
|
||||
$attributeIds = $this->request->getPost('attribute_ids') ?? [];
|
||||
|
||||
$this->attribute->deletePersonAttributeLinks($personId);
|
||||
|
||||
foreach ($attributeLinks as $definitionId => $attributeId) {
|
||||
$definitionInfo = $this->attribute->getAttributeInfo((int)$definitionId);
|
||||
$definitionType = $definitionInfo->definition_type;
|
||||
|
||||
if ($definitionType !== DROPDOWN) {
|
||||
$attributeId = $this->attribute->savePersonAttributeValue(
|
||||
$attributeId,
|
||||
(int)$definitionId,
|
||||
$personId,
|
||||
$attributeIds[$definitionId] ?? false,
|
||||
$definitionType
|
||||
);
|
||||
}
|
||||
|
||||
$this->attribute->savePersonAttributeLink($personId, (int)$definitionId, (int)$attributeId);
|
||||
}
|
||||
return $this->response->setJSON($data_row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Capitalize segments of a name, and put the rest into lower case.
|
||||
* You can pass the characters you want to use as delimiters as exceptions.
|
||||
* The function supports UTF-8 strings
|
||||
*
|
||||
* Example:
|
||||
* i.e. <?php echo nameize("john o'grady-smith"); ?>
|
||||
*
|
||||
* returns John O'Grady-Smith
|
||||
*/
|
||||
protected function nameize(string $input): string
|
||||
{
|
||||
$adjustedName = str_name_case($input);
|
||||
$adjusted_name = str_name_case($input);
|
||||
|
||||
// TODO: Use preg_replace to match HTML entities and convert them to lowercase. This is a workaround for https://github.com/tamtamchik/namecase/issues/20
|
||||
return preg_replace_callback('/&[a-zA-Z0-9#]+;/', function ($matches) {
|
||||
return strtolower($matches[0]);
|
||||
}, $adjustedName);
|
||||
}, $adjusted_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,11 +190,11 @@ class Receivings extends Secure_Controller
|
||||
/**
|
||||
* Edit line item in current receiving. Used in app/Views/receivings/receiving.php
|
||||
*
|
||||
* @param int|string|null $item_id
|
||||
* @param string|int|null $item_id
|
||||
* @return string
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function postEditItem(int|string|null $item_id): string
|
||||
public function postEditItem($item_id): string
|
||||
{
|
||||
$data = [];
|
||||
|
||||
@@ -241,26 +241,15 @@ class Receivings extends Secure_Controller
|
||||
$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'] = [];
|
||||
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 = $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;
|
||||
foreach ($this->employee->get_all()->getResult() as $employee) {
|
||||
$data['employees'][$employee->person_id] = $employee->first_name . ' ' . $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_id'] = $receiving_info['supplier_id'];
|
||||
$data['receiving_info'] = $receiving_info;
|
||||
$data['can_assign_employee'] = $can_assign_employee;
|
||||
|
||||
return view('receivings/form', $data);
|
||||
}
|
||||
@@ -280,10 +269,8 @@ class Receivings extends Secure_Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $receiving_id
|
||||
* @param bool $update_inventory
|
||||
* @return ResponseInterface
|
||||
* @throws ReflectionException
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function postDelete(int $receiving_id = -1, bool $update_inventory = true): ResponseInterface
|
||||
{
|
||||
@@ -504,20 +491,10 @@ class Receivings extends Secure_Controller
|
||||
$date_formatter = date_create_from_format($this->config['dateformat'] . ' ' . $this->config['timeformat'], $newdate);
|
||||
$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_time' => $receiving_time,
|
||||
'supplier_id' => $this->request->getPost('supplier_id') ? $this->request->getPost('supplier_id', FILTER_SANITIZE_NUMBER_INT) : null,
|
||||
'employee_id' => $employee_id,
|
||||
'employee_id' => $this->request->getPost('employee_id', FILTER_SANITIZE_NUMBER_INT),
|
||||
'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
|
||||
];
|
||||
|
||||
@@ -1776,7 +1776,7 @@ class Reports extends Secure_Controller
|
||||
{
|
||||
$this->clearCache();
|
||||
|
||||
$definition_names = $this->attribute->get_definitions_by_flags(attribute::SHOW_IN_SALES, true);
|
||||
$definition_names = $this->attribute->get_definitions_by_flags(attribute::SHOW_IN_SALES);
|
||||
|
||||
$inputs = [
|
||||
'start_date' => $start_date,
|
||||
@@ -1789,12 +1789,7 @@ class Reports extends Secure_Controller
|
||||
$this->detailed_sales->create($inputs);
|
||||
|
||||
$columns = $this->detailed_sales->getDataColumns();
|
||||
// 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);
|
||||
$columns['details'] = array_merge($columns['details'], $definition_names);
|
||||
|
||||
$headers = $columns;
|
||||
|
||||
@@ -1935,19 +1930,14 @@ class Reports extends Secure_Controller
|
||||
{
|
||||
$this->clearCache();
|
||||
|
||||
$definition_names = $this->attribute->get_definitions_by_flags(attribute::SHOW_IN_RECEIVINGS, true);
|
||||
$definition_names = $this->attribute->get_definitions_by_flags(attribute::SHOW_IN_RECEIVINGS);
|
||||
|
||||
$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);
|
||||
|
||||
$columns = $this->detailed_receivings->getDataColumns();
|
||||
// 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);
|
||||
$columns['details'] = array_merge($columns['details'], $definition_names);
|
||||
|
||||
$headers = $columns;
|
||||
$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.
|
||||
*
|
||||
* @return ResponseInterface|string
|
||||
* @return string
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function getManage(): ResponseInterface|string
|
||||
public function getManage(): string
|
||||
{
|
||||
$personId = $this->session->get('person_id');
|
||||
$person_id = $this->session->get('person_id');
|
||||
|
||||
if (!$this->employee->has_grant('reports_sales', $personId)) {
|
||||
return redirect()->to('no_access/sales/reports_sales');
|
||||
if (!$this->employee->has_grant('reports_sales', $person_id)) {
|
||||
redirect('no_access/sales/reports_sales');
|
||||
} else {
|
||||
$data['table_headers'] = get_sales_manage_table_headers();
|
||||
|
||||
@@ -92,31 +92,18 @@ class Sales extends Secure_Controller
|
||||
'only_due' => lang('Sales.due_filter'),
|
||||
'only_check' => lang('Sales.check_filter'),
|
||||
'only_creditcard' => lang('Sales.credit_filter'),
|
||||
'only_debit' => lang('Sales.debit'),
|
||||
'only_invoices' => lang('Sales.invoice_filter'),
|
||||
'selected_customer' => lang('Sales.selected_customer')
|
||||
];
|
||||
|
||||
if ($this->sale_lib->get_customer() != -1) {
|
||||
$selectedFilters = ['selected_customer'];
|
||||
$selected_filters = ['selected_customer'];
|
||||
$data['customer_selected'] = true;
|
||||
} else {
|
||||
$data['customer_selected'] = false;
|
||||
$selectedFilters = [];
|
||||
$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;
|
||||
$data['selected_filters'] = $selected_filters;
|
||||
|
||||
return view('sales/manage', $data);
|
||||
}
|
||||
@@ -155,7 +142,6 @@ class Sales extends Secure_Controller
|
||||
'only_check' => false,
|
||||
'selected_customer' => false,
|
||||
'only_creditcard' => false,
|
||||
'only_debit' => false,
|
||||
'only_invoices' => $this->config['invoice_enable'] && $this->request->getGet('only_invoices', FILTER_SANITIZE_NUMBER_INT),
|
||||
'is_valid_receipt' => $this->sale->is_valid_receipt($search)
|
||||
];
|
||||
@@ -425,7 +411,7 @@ class Sales extends Secure_Controller
|
||||
$new_giftcard_value = $giftcard->get_giftcard_value($giftcard_num) - $this->sale_lib->get_amount_due();
|
||||
$new_giftcard_value = max($new_giftcard_value, 0);
|
||||
$this->sale_lib->set_giftcard_remainder($new_giftcard_value);
|
||||
$new_giftcard_value = to_currency($new_giftcard_value);
|
||||
$new_giftcard_value = str_replace('$', '\$', to_currency($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));
|
||||
|
||||
@@ -582,21 +568,12 @@ class Sales extends Secure_Controller
|
||||
$data = [];
|
||||
|
||||
$rules = [
|
||||
'price' => 'trim|required|decimal_locale|nonNegativeDecimal',
|
||||
'price' => 'trim|required|decimal_locale',
|
||||
'quantity' => 'trim|required|decimal_locale',
|
||||
'discount' => 'trim|permit_empty|decimal_locale|nonNegativeDecimal',
|
||||
'discount' => 'trim|permit_empty|decimal_locale',
|
||||
];
|
||||
|
||||
$messages = [
|
||||
'price' => [
|
||||
'nonNegativeDecimal' => lang('Sales.negative_price_invalid'),
|
||||
],
|
||||
'discount' => [
|
||||
'nonNegativeDecimal' => lang('Sales.negative_discount_invalid'),
|
||||
],
|
||||
];
|
||||
|
||||
if ($this->validate($rules, $messages)) {
|
||||
if ($this->validate($rules)) {
|
||||
$description = $this->request->getPost('description', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
$serialnumber = $this->request->getPost('serialnumber', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
$price = parse_decimals($this->request->getPost('price'));
|
||||
@@ -605,38 +582,20 @@ class Sales extends Secure_Controller
|
||||
$discount = $discount_type
|
||||
? parse_quantity($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);
|
||||
$discounted_total = $this->request->getPost('discounted_total') != ''
|
||||
? parse_decimals($this->request->getPost('discounted_total') ?? '')
|
||||
: null;
|
||||
|
||||
|
||||
$this->sale_lib->edit_item($line, $description, $serialnumber, $quantity, $discount, $discount_type, $price, $discounted_total);
|
||||
|
||||
$this->sale_lib->empty_payments();
|
||||
|
||||
$data['warning'] = $this->sale_lib->out_of_stock($this->sale_lib->get_item_id($line), $item_location);
|
||||
} else {
|
||||
$errors = $this->validator->getErrors();
|
||||
$data['error'] = $errors ? reset($errors) : lang('Sales.error_editing_item');
|
||||
$data['error'] = lang('Sales.error_editing_item');
|
||||
}
|
||||
|
||||
return $this->_reload($data);
|
||||
@@ -750,12 +709,6 @@ class Sales extends Secure_Controller
|
||||
$data['cash_amount_due'] = $totals['cash_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
|
||||
$data['amount_due'] = $totals['cash_amount_due'];
|
||||
} else {
|
||||
@@ -796,7 +749,7 @@ class Sales extends Secure_Controller
|
||||
|
||||
if ($sale_id == NEW_ENTRY && $this->sale->check_invoice_number_exists($invoice_number)) {
|
||||
$data['error'] = lang('Sales.invoice_number_duplicate', [$invoice_number]);
|
||||
return $this->_reload($data);
|
||||
$this->_reload($data);
|
||||
} else {
|
||||
$data['invoice_number'] = $invoice_number;
|
||||
$data['sale_status'] = COMPLETED;
|
||||
@@ -817,11 +770,10 @@ class Sales extends Secure_Controller
|
||||
|
||||
if ($data['sale_id_num'] == NEW_ENTRY) {
|
||||
$data['error_message'] = lang('Sales.transaction_failed');
|
||||
return $this->_reload($data);
|
||||
} else {
|
||||
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']);
|
||||
$this->sale_lib->clear_all();
|
||||
return view('sales/' . $invoice_view, $data);
|
||||
$this->sale_lib->clear_all();
|
||||
}
|
||||
}
|
||||
} elseif ($this->sale_lib->is_work_order_mode()) {
|
||||
@@ -841,7 +793,7 @@ class Sales extends Secure_Controller
|
||||
|
||||
if ($sale_id == NEW_ENTRY && $this->sale->check_work_order_number_exists($work_order_number)) {
|
||||
$data['error'] = lang('Sales.work_order_number_duplicate');
|
||||
return $this->_reload($data);
|
||||
$this->_reload($data);
|
||||
} else {
|
||||
$data['work_order_number'] = $work_order_number;
|
||||
$data['sale_status'] = SUSPENDED;
|
||||
@@ -854,8 +806,9 @@ class Sales extends Secure_Controller
|
||||
|
||||
$data['barcode'] = null;
|
||||
|
||||
$this->sale_lib->clear_all();
|
||||
return view('sales/work_order', $data);
|
||||
$this->sale_lib->clear_mode();
|
||||
$this->sale_lib->clear_all();
|
||||
}
|
||||
} elseif ($this->sale_lib->is_quote_mode()) {
|
||||
$data['sales_quote'] = lang('Sales.quote');
|
||||
@@ -869,7 +822,7 @@ class Sales extends Secure_Controller
|
||||
|
||||
if ($sale_id == NEW_ENTRY && $this->sale->check_quote_number_exists($quote_number)) {
|
||||
$data['error'] = lang('Sales.quote_number_duplicate');
|
||||
return $this->_reload($data);
|
||||
$this->_reload($data);
|
||||
} else {
|
||||
$data['quote_number'] = $quote_number;
|
||||
$data['sale_status'] = SUSPENDED;
|
||||
@@ -881,8 +834,9 @@ class Sales extends Secure_Controller
|
||||
$data['cart'] = $this->sale_lib->sort_and_filter_cart($data['cart']);
|
||||
$data['barcode'] = null;
|
||||
|
||||
$this->sale_lib->clear_all();
|
||||
return view('sales/quote', $data);
|
||||
$this->sale_lib->clear_mode();
|
||||
$this->sale_lib->clear_all();
|
||||
}
|
||||
} else {
|
||||
// Save the data to the sales table
|
||||
@@ -901,11 +855,10 @@ class Sales extends Secure_Controller
|
||||
|
||||
if ($data['sale_id_num'] == NEW_ENTRY) {
|
||||
$data['error_message'] = lang('Sales.transaction_failed');
|
||||
return $this->_reload($data);
|
||||
} else {
|
||||
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']);
|
||||
$this->sale_lib->clear_all();
|
||||
return view('sales/receipt', $data);
|
||||
$this->sale_lib->clear_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -937,10 +890,7 @@ class Sales extends Secure_Controller
|
||||
new Token_customer((array)$sale_data)
|
||||
];
|
||||
$text = $this->token_lib->render($text, $tokens);
|
||||
$sale_data['mimetype'] = $this->email_lib->getLogoMimeType();
|
||||
|
||||
// Build img_tag for email views that need it (receipt_email.php)
|
||||
$sale_data['img_tag'] = $this->email_lib->buildLogoImgTag();
|
||||
$sale_data['mimetype'] = mime_content_type(FCPATH . 'uploads/' . $this->config['company_logo']);
|
||||
|
||||
// Generate email attachment: invoice in PDF format
|
||||
$view = Services::renderer();
|
||||
@@ -977,7 +927,13 @@ class Sales extends Secure_Controller
|
||||
|
||||
if (!empty($sale_data['customer_email'])) {
|
||||
$sale_data['barcode'] = $this->barcode_lib->generate_receipt_barcode($sale_data['sale_id']);
|
||||
$sale_data['img_tag'] = $this->email_lib->buildLogoImgTag();
|
||||
$sale_data['img_tag'] = '';
|
||||
|
||||
$logo_path = FCPATH . 'uploads/' . $this->config['company_logo'];
|
||||
if (!empty($this->config['company_logo']) && file_exists($logo_path)) {
|
||||
$logo_data = base64_encode(file_get_contents($logo_path));
|
||||
$sale_data['img_tag'] = '<img id="image" src="data:image/png;base64,' . $logo_data . '" alt="company_logo">';
|
||||
}
|
||||
|
||||
$to = $sale_data['customer_email'];
|
||||
$subject = lang('Sales.receipt');
|
||||
@@ -1692,11 +1648,10 @@ class Sales extends Secure_Controller
|
||||
$this->item->update_item_number($item_id, $item_number);
|
||||
$cart = $this->sale_lib->get_cart();
|
||||
$x = $this->search_cart_for_item_id($item_id, $cart);
|
||||
if ($x !== null) {
|
||||
if ($x != null) {
|
||||
$cart[$x]['item_number'] = $item_number;
|
||||
}
|
||||
$this->sale_lib->set_cart($cart);
|
||||
return $this->response->setJSON(['success' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1715,12 +1670,11 @@ class Sales extends Secure_Controller
|
||||
$cart = $this->sale_lib->get_cart();
|
||||
$x = $this->search_cart_for_item_id($item_id, $cart);
|
||||
|
||||
if ($x !== null) {
|
||||
if ($x != null) {
|
||||
$cart[$x]['name'] = $name;
|
||||
}
|
||||
|
||||
$this->sale_lib->set_cart($cart);
|
||||
return $this->response->setJSON(['success' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1739,12 +1693,11 @@ class Sales extends Secure_Controller
|
||||
$cart = $this->sale_lib->get_cart();
|
||||
$x = $this->search_cart_for_item_id($item_id, $cart);
|
||||
|
||||
if ($x !== null) {
|
||||
if ($x != null) {
|
||||
$cart[$x]['description'] = $description;
|
||||
}
|
||||
|
||||
$this->sale_lib->set_cart($cart);
|
||||
return $this->response->setJSON(['success' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\Attribute;
|
||||
use App\Models\Supplier;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Config\Services;
|
||||
@@ -18,6 +17,9 @@ class Suppliers extends Persons
|
||||
$this->supplier = model(Supplier::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getIndex(): string
|
||||
{
|
||||
$data['table_headers'] = get_suppliers_manage_table_headers();
|
||||
@@ -25,14 +27,23 @@ class Suppliers extends Persons
|
||||
return view('people/manage', $data);
|
||||
}
|
||||
|
||||
public function getRow($rowId): ResponseInterface
|
||||
/**
|
||||
* Gets one row for a supplier manage table. This is called using AJAX to update one row.
|
||||
* @param $row_id
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getRow($row_id): ResponseInterface
|
||||
{
|
||||
$dataRow = get_supplier_data_row($this->supplier->get_info($rowId));
|
||||
$dataRow['category'] = $this->supplier->get_category_name($dataRow['category']);
|
||||
$data_row = get_supplier_data_row($this->supplier->get_info($row_id));
|
||||
$data_row['category'] = $this->supplier->get_category_name($data_row['category']);
|
||||
|
||||
return $this->response->setJSON($dataRow);
|
||||
return $this->response->setJSON($data_row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Supplier table data rows. This will be called with AJAX.
|
||||
* @return void
|
||||
**/
|
||||
public function getSearch(): ResponseInterface
|
||||
{
|
||||
$search = $this->request->getGet('search');
|
||||
@@ -42,19 +53,23 @@ class Suppliers extends Persons
|
||||
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
|
||||
$suppliers = $this->supplier->search($search, $limit, $offset, $sort, $order);
|
||||
$totalRows = $this->supplier->get_found_rows($search);
|
||||
$total_rows = $this->supplier->get_found_rows($search);
|
||||
|
||||
$dataRows = [];
|
||||
$data_rows = [];
|
||||
|
||||
foreach ($suppliers->getResult() as $supplier) {
|
||||
$row = get_supplier_data_row($supplier);
|
||||
$row['category'] = $this->supplier->get_category_name($row['category']);
|
||||
$dataRows[] = $row;
|
||||
$data_rows[] = $row;
|
||||
}
|
||||
|
||||
return $this->response->setJSON(['total' => $totalRows, 'rows' => $dataRows]);
|
||||
return $this->response->setJSON(['total' => $total_rows, 'rows' => $data_rows]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives search suggestions based on what is being searched for
|
||||
* @return ResponseInterface
|
||||
**/
|
||||
public function getSuggest(): ResponseInterface
|
||||
{
|
||||
$search = $this->request->getGet('term');
|
||||
@@ -63,7 +78,10 @@ class Suppliers extends Persons
|
||||
return $this->response->setJSON($suggestions);
|
||||
}
|
||||
|
||||
public function suggestSearch(): ResponseInterface
|
||||
/**
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function suggest_search(): ResponseInterface
|
||||
{
|
||||
$search = $this->request->getPost('term');
|
||||
$suggestions = $this->supplier->get_search_suggestions($search, false);
|
||||
@@ -71,9 +89,15 @@ class Suppliers extends Persons
|
||||
return $this->response->setJSON($suggestions);
|
||||
}
|
||||
|
||||
public function getView(int $supplierId = NEW_ENTRY): string
|
||||
/**
|
||||
* Loads the supplier edit form
|
||||
*
|
||||
* @param int $supplier_id
|
||||
* @return string
|
||||
*/
|
||||
public function getView(int $supplier_id = NEW_ENTRY): string
|
||||
{
|
||||
$info = $this->supplier->get_info($supplierId);
|
||||
$info = $this->supplier->get_info($supplier_id);
|
||||
foreach (get_object_vars($info) as $property => $value) {
|
||||
$info->$property = $value;
|
||||
}
|
||||
@@ -83,23 +107,25 @@ class Suppliers extends Persons
|
||||
return view("suppliers/form", $data);
|
||||
}
|
||||
|
||||
public function getAttributes(int $supplierId = NEW_ENTRY): string
|
||||
/**
|
||||
* Inserts/updates a supplier
|
||||
*
|
||||
* @param int $supplier_id
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function postSave(int $supplier_id = NEW_ENTRY): ResponseInterface
|
||||
{
|
||||
return $this->getPersonAttributes($supplierId, Attribute::SHOW_IN_SUPPLIERS);
|
||||
}
|
||||
|
||||
public function postSave(int $supplierId = NEW_ENTRY): ResponseInterface
|
||||
{
|
||||
$firstName = $this->request->getPost('first_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
$lastName = $this->request->getPost('last_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
$first_name = $this->request->getPost('first_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS); // TODO: Duplicate code
|
||||
$last_name = $this->request->getPost('last_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
$email = strtolower($this->request->getPost('email', FILTER_SANITIZE_EMAIL));
|
||||
|
||||
$firstName = $this->nameize($firstName);
|
||||
$lastName = $this->nameize($lastName);
|
||||
// Format first and last name properly
|
||||
$first_name = $this->nameize($first_name);
|
||||
$last_name = $this->nameize($last_name);
|
||||
|
||||
$personData = [
|
||||
'first_name' => $firstName,
|
||||
'last_name' => $lastName,
|
||||
$person_data = [
|
||||
'first_name' => $first_name,
|
||||
'last_name' => $last_name,
|
||||
'gender' => $this->request->getPost('gender'),
|
||||
'email' => $email,
|
||||
'phone_number' => $this->request->getPost('phone_number', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||
@@ -112,7 +138,7 @@ class Suppliers extends Persons
|
||||
'comments' => $this->request->getPost('comments', FILTER_SANITIZE_FULL_SPECIAL_CHARS)
|
||||
];
|
||||
|
||||
$supplierData = [
|
||||
$supplier_data = [
|
||||
'company_name' => $this->request->getPost('company_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||
'agency_name' => $this->request->getPost('agency_name', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||
'category' => $this->request->getPost('category', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||
@@ -120,43 +146,47 @@ class Suppliers extends Persons
|
||||
'tax_id' => $this->request->getPost('tax_id', FILTER_SANITIZE_NUMBER_INT)
|
||||
];
|
||||
|
||||
if ($this->supplier->save_supplier($personData, $supplierData, $supplierId)) {
|
||||
$personId = $supplierId == NEW_ENTRY ? $supplierData['person_id'] : $supplierId;
|
||||
$this->savePersonAttributes($personId, Attribute::SHOW_IN_SUPPLIERS);
|
||||
|
||||
if ($supplierId == NEW_ENTRY) {
|
||||
if ($this->supplier->save_supplier($person_data, $supplier_data, $supplier_id)) {
|
||||
// New supplier
|
||||
if ($supplier_id == NEW_ENTRY) {
|
||||
return $this->response->setJSON([
|
||||
'success' => true,
|
||||
'message' => lang('Suppliers.successful_adding') . ' ' . $supplierData['company_name'],
|
||||
'id' => $supplierData['person_id']
|
||||
'message' => lang('Suppliers.successful_adding') . ' ' . $supplier_data['company_name'],
|
||||
'id' => $supplier_data['person_id']
|
||||
]);
|
||||
} else {
|
||||
} else { // Existing supplier
|
||||
|
||||
return $this->response->setJSON([
|
||||
'success' => true,
|
||||
'message' => lang('Suppliers.successful_updating') . ' ' . $supplierData['company_name'],
|
||||
'id' => $supplierId
|
||||
'message' => lang('Suppliers.successful_updating') . ' ' . $supplier_data['company_name'],
|
||||
'id' => $supplier_id
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
} else { // Failure
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'message' => lang('Suppliers.error_adding_updating') . ' ' . $supplierData['company_name'],
|
||||
'message' => lang('Suppliers.error_adding_updating') . ' ' . $supplier_data['company_name'],
|
||||
'id' => NEW_ENTRY
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This deletes suppliers from the suppliers table
|
||||
*
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function postDelete(): ResponseInterface
|
||||
{
|
||||
$suppliersToDelete = $this->request->getPost('ids', FILTER_SANITIZE_NUMBER_INT);
|
||||
$suppliers_to_delete = $this->request->getPost('ids', FILTER_SANITIZE_NUMBER_INT);
|
||||
|
||||
if ($this->supplier->delete_list($suppliersToDelete)) {
|
||||
if ($this->supplier->delete_list($suppliers_to_delete)) {
|
||||
return $this->response->setJSON([
|
||||
'success' => true,
|
||||
'message' => lang('Suppliers.successful_deleted') . ' ' . count($suppliersToDelete) . ' ' . lang('Suppliers.one_or_multiple')
|
||||
'message' => lang('Suppliers.successful_deleted') . ' ' . count($suppliers_to_delete) . ' ' . lang('Suppliers.one_or_multiple')
|
||||
]);
|
||||
} else {
|
||||
return $this->response->setJSON(['success' => false, 'message' => lang('Suppliers.cannot_be_deleted')]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class Tax_categories extends Secure_Controller
|
||||
$search = $this->request->getGet('search');
|
||||
$limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT);
|
||||
$offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT);
|
||||
$sort = $this->sanitizeSortColumn(get_tax_categories_table_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'tax_category_id');
|
||||
$sort = $this->request->getGet('sort', 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);
|
||||
|
||||
@@ -50,7 +50,7 @@ class Tax_codes extends Secure_Controller
|
||||
$search = $this->request->getGet('search');
|
||||
$limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT);
|
||||
$offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT);
|
||||
$sort = $this->sanitizeSortColumn(get_tax_code_table_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'tax_code');
|
||||
$sort = $this->request->getGet('sort', 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);
|
||||
|
||||
@@ -43,7 +43,7 @@ class Tax_jurisdictions extends Secure_Controller
|
||||
$search = $this->request->getGet('search');
|
||||
$limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT);
|
||||
$offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT);
|
||||
$sort = $this->sanitizeSortColumn(get_tax_jurisdictions_table_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'jurisdiction_id');
|
||||
$sort = $this->request->getGet('sort', 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);
|
||||
|
||||
@@ -81,7 +81,7 @@ class Taxes extends Secure_Controller
|
||||
$search = $this->request->getGet('search');
|
||||
$limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT);
|
||||
$offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT);
|
||||
$sort = $this->sanitizeSortColumn(get_tax_rates_manage_table_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'tax_rate_id');
|
||||
$sort = $this->request->getGet('sort', 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);
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
<?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');
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ class Migration_database_optimizations extends Migration
|
||||
|
||||
$attribute = model(Attribute::class);
|
||||
|
||||
$attribute->deleteOrphanedValues();
|
||||
$attribute->delete_orphaned_values();
|
||||
|
||||
$this->migrate_duplicate_attribute_values(DECIMAL);
|
||||
$this->migrate_duplicate_attribute_values(DATE);
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
use Config\Database;
|
||||
|
||||
class AddPersonToAttributeLinks extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
helper('migration');
|
||||
|
||||
// First, modify the generated unique column to include person_id
|
||||
// Drop the existing unique constraint
|
||||
$this->db->query('ALTER TABLE `ospos_attribute_links` DROP INDEX `attribute_links_uq3`');
|
||||
$this->db->query('ALTER TABLE `ospos_attribute_links` DROP COLUMN `generated_unique_column`');
|
||||
|
||||
// Add person_id column
|
||||
$this->db->query('ALTER TABLE `ospos_attribute_links` ADD COLUMN `person_id` INT(10) NULL AFTER `receiving_id`');
|
||||
|
||||
// Add index for person_id
|
||||
$this->db->query('ALTER TABLE `ospos_attribute_links` ADD KEY `person_id` (`person_id`)');
|
||||
|
||||
// Add foreign key constraint for person_id
|
||||
$this->db->query('ALTER TABLE `ospos_attribute_links` ADD CONSTRAINT `ospos_attribute_links_ibfk_6` FOREIGN KEY (`person_id`) REFERENCES `ospos_people` (`person_id`) ON DELETE CASCADE');
|
||||
|
||||
// Recreate the generated unique column with person_id support
|
||||
// This ensures uniqueness for both item attributes and person attributes
|
||||
$this->db->query("ALTER TABLE `ospos_attribute_links`
|
||||
ADD COLUMN `generated_unique_column` VARCHAR(255) GENERATED ALWAYS AS (
|
||||
CASE
|
||||
WHEN `sale_id` IS NULL AND `receiving_id` IS NULL AND `item_id` IS NOT NULL THEN CONCAT('item-', `definition_id`, '-', `item_id`)
|
||||
WHEN `sale_id` IS NULL AND `receiving_id` IS NULL AND `item_id` IS NULL AND `person_id` IS NOT NULL THEN CONCAT('person-', `definition_id`, '-', `person_id`)
|
||||
ELSE NULL
|
||||
END
|
||||
) STORED");
|
||||
|
||||
// Re-add unique constraint
|
||||
$this->db->query('ALTER TABLE `ospos_attribute_links` ADD UNIQUE INDEX `attribute_links_uq3` (`generated_unique_column`)');
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Drop person_id related constraints and column
|
||||
$this->db->query('ALTER TABLE `ospos_attribute_links` DROP INDEX `attribute_links_uq3`');
|
||||
$this->db->query('ALTER TABLE `ospos_attribute_links` DROP COLUMN `generated_unique_column`');
|
||||
$this->db->query('ALTER TABLE `ospos_attribute_links` DROP FOREIGN KEY `ospos_attribute_links_ibfk_6`');
|
||||
$this->db->query('ALTER TABLE `ospos_attribute_links` DROP COLUMN `person_id`');
|
||||
|
||||
// Restore original generated column
|
||||
$this->db->query("ALTER TABLE `ospos_attribute_links`
|
||||
ADD COLUMN `generated_unique_column` VARCHAR(255) GENERATED ALWAYS AS (
|
||||
CASE
|
||||
WHEN `sale_id` IS NULL AND `receiving_id` IS NULL AND `item_id` IS NOT NULL THEN CONCAT(`definition_id`, '-', `item_id`)
|
||||
ELSE NULL
|
||||
END
|
||||
) STORED");
|
||||
$this->db->query('ALTER TABLE `ospos_attribute_links` ADD UNIQUE INDEX `attribute_links_uq3` (`generated_unique_column`)');
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddPersonAttributeFlag extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$this->db->query('ALTER TABLE `ospos_attribute_definitions` ADD COLUMN `person_attribute` TINYINT(1) DEFAULT 0 AFTER `definition_flags`');
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->db->query('ALTER TABLE `ospos_attribute_definitions` DROP COLUMN `person_attribute`');
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
<?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}`");
|
||||
}
|
||||
}
|
||||
145
app/Database/constraints.sql
Normal file
145
app/Database/constraints.sql
Normal file
@@ -0,0 +1,145 @@
|
||||
--
|
||||
-- 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`);
|
||||
@@ -730,148 +730,3 @@ CREATE 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`);
|
||||
@@ -36,26 +36,21 @@ class Db_log
|
||||
private function generate_message(): string
|
||||
{
|
||||
$db = Database::connect();
|
||||
$lastQuery = $db->getLastQuery();
|
||||
|
||||
if ($lastQuery === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$affectedRows = $db->affectedRows();
|
||||
$executionTime = $this->convert_time($lastQuery->getDuration());
|
||||
$last_query = $db->getLastQuery();
|
||||
$affected_rows = $db->affectedRows();
|
||||
$execution_time = $this->convert_time($last_query->getDuration());
|
||||
|
||||
$message = '*** Query: ' . date('Y-m-d H:i:s T') . ' *******************'
|
||||
. "\n" . $lastQuery->getQuery()
|
||||
. "\n Affected rows: $affectedRows"
|
||||
. "\n Execution Time: " . $executionTime['time'] . ' ' . $executionTime['unit'];
|
||||
. "\n" . $last_query->getQuery()
|
||||
. "\n Affected rows: $affected_rows"
|
||||
. "\n Execution Time: " . $execution_time['time'] . ' ' . $execution_time['unit'];
|
||||
|
||||
$longQuery = ($executionTime['unit'] === 's') && ($executionTime['time'] > 0.5);
|
||||
if ($longQuery) {
|
||||
$long_query = ($execution_time['unit'] === 's') && ($execution_time['time'] > 0.5);
|
||||
if ($long_query) {
|
||||
$message .= ' [LONG RUNNING QUERY]';
|
||||
}
|
||||
|
||||
return $this->config->db_log_only_long && !$longQuery ? '' : $message;
|
||||
return $this->config->db_log_only_long && !$long_query ? '' : $message;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,8 +4,6 @@ namespace App\Events;
|
||||
|
||||
use App\Libraries\MY_Migration;
|
||||
use App\Models\Appconfig;
|
||||
use CodeIgniter\Session\Handlers\DatabaseHandler;
|
||||
use CodeIgniter\Session\Handlers\FileHandler;
|
||||
use CodeIgniter\Session\Session;
|
||||
use Config\OSPOS;
|
||||
use Config\Services;
|
||||
@@ -21,47 +19,38 @@ class Load_config
|
||||
{
|
||||
public Session $session;
|
||||
|
||||
/**
|
||||
* Loads configuration from database into App CI config and then applies those settings
|
||||
*/
|
||||
public function load_config(): void
|
||||
{
|
||||
// Migrations
|
||||
$migration_config = config('Migrations');
|
||||
$migration = new MY_Migration($migration_config);
|
||||
|
||||
$this->session = session();
|
||||
|
||||
// Database Configuration
|
||||
$config = config(OSPOS::class);
|
||||
|
||||
if (!$migration->is_latest()) {
|
||||
$this->session->destroy();
|
||||
}
|
||||
|
||||
$this->setDefaultLanguage($config);
|
||||
// Language
|
||||
$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->setLocale(current_language_code());
|
||||
$language->setLocale($config->settings['language_code']);
|
||||
|
||||
// Time Zone
|
||||
date_default_timezone_set($config->settings['timezone'] ?? ini_get('date.timezone'));
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<?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
|
||||
|
||||
/**
|
||||
* @param array $stock_locations
|
||||
* @param array $attributes
|
||||
* @return 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
|
||||
|
||||
@@ -22,7 +22,9 @@ function current_language_code(bool $load_system_language = false): string
|
||||
}
|
||||
}
|
||||
|
||||
return $config['language_code'] ?? DEFAULT_LANGUAGE_CODE;
|
||||
$language_code = $config['language_code'];
|
||||
|
||||
return empty($language_code) ? DEFAULT_LANGUAGE_CODE : $language_code;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,7 +45,9 @@ function current_language(bool $load_system_language = false): string
|
||||
}
|
||||
}
|
||||
|
||||
return $config['language'] ?? DEFAULT_LANGUAGE;
|
||||
$language = $config['language'];
|
||||
|
||||
return empty($language) ? DEFAULT_LANGUAGE : $language;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,54 +11,56 @@ function check_encryption(): bool
|
||||
$old_key = config('Encryption')->key;
|
||||
|
||||
if ((empty($old_key)) || (strlen($old_key) < 64)) {
|
||||
// Create Key
|
||||
$encryption = new Encryption();
|
||||
$key = bin2hex($encryption->createKey());
|
||||
config('Encryption')->key = $key;
|
||||
|
||||
// Write to .env
|
||||
$config_path = ROOTPATH . '.env';
|
||||
$new_config_path = WRITEPATH . '/backup/.env';
|
||||
$backup_path = WRITEPATH . '/backup/.env.bak';
|
||||
|
||||
$backup_folder = WRITEPATH . '/backup';
|
||||
|
||||
if (!file_exists($backup_folder)) {
|
||||
@mkdir($backup_folder, 0750, true);
|
||||
if (!file_exists($backup_folder) && !mkdir($backup_folder)) {
|
||||
log_message('error', 'Could not create backup folder');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!file_exists($config_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);
|
||||
if (!copy($config_path, $backup_path)) {
|
||||
log_message('error', "Unable to copy $config_path to $backup_path");
|
||||
}
|
||||
|
||||
if (file_exists($config_path)) {
|
||||
@copy($config_path, $backup_path);
|
||||
@chmod($backup_path, 0640);
|
||||
@chmod($config_path, 0640);
|
||||
// Copy to backup
|
||||
@chmod($config_path, 0660);
|
||||
@chmod($backup_path, 0660);
|
||||
|
||||
$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 (strpos($config_file, 'encryption.key') !== false) {
|
||||
$config_file = preg_replace("/(encryption\.key.*=.*)('.*')/", "$1'$key'", $config_file);
|
||||
} else {
|
||||
$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");
|
||||
if (!empty($old_key)) {
|
||||
$old_line = "# encryption.key = '$old_key' REMOVE IF UNNEEDED\r\n";
|
||||
$insertion_point = stripos($config_file, 'encryption.key');
|
||||
$config_file = substr_replace($config_file, $old_line, $insertion_point, 0);
|
||||
}
|
||||
|
||||
$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;
|
||||
@@ -72,14 +74,23 @@ function abort_encryption_conversion(): void
|
||||
$config_path = ROOTPATH . '.env';
|
||||
$backup_path = WRITEPATH . '/backup/.env.bak';
|
||||
|
||||
if (!file_exists($backup_path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@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");
|
||||
|
||||
$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");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,9 +99,13 @@ function abort_encryption_conversion(): void
|
||||
function remove_backup(): void
|
||||
{
|
||||
$backup_path = WRITEPATH . '/backup/.env.bak';
|
||||
if (!file_exists($backup_path)) {
|
||||
if (! file_exists($backup_path)) {
|
||||
return;
|
||||
}
|
||||
@unlink($backup_path);
|
||||
log_message('info', "Removed $backup_path");
|
||||
if (!unlink($backup_path)) {
|
||||
log_message('error', "Unable to remove $backup_path.");
|
||||
return;
|
||||
}
|
||||
log_message('info', "File $backup_path has been removed");
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ use App\Models\Employee;
|
||||
use App\Models\Item_taxes;
|
||||
use App\Models\Tax_category;
|
||||
use CodeIgniter\Database\ResultInterface;
|
||||
use CodeIgniter\HTTP\IncomingRequest;
|
||||
use CodeIgniter\Session\Session;
|
||||
use Config\OSPOS;
|
||||
use Config\Services;
|
||||
@@ -409,7 +408,7 @@ function get_items_manage_table_headers(): string
|
||||
{
|
||||
$attribute = model(Attribute::class);
|
||||
$config = config(OSPOS::class)->settings;
|
||||
$definitionsWithTypes = $attribute->get_definitions_by_flags($attribute::SHOW_IN_ITEMS, true);
|
||||
$definition_names = $attribute->get_definitions_by_flags($attribute::SHOW_IN_ITEMS); // TODO: this should be made into a constant in constants.php
|
||||
|
||||
$headers = item_headers();
|
||||
|
||||
@@ -421,8 +420,8 @@ function get_items_manage_table_headers(): string
|
||||
|
||||
$headers[] = ['item_pic' => lang('Items.image'), 'sortable' => false];
|
||||
|
||||
foreach ($definitionsWithTypes as $definition_id => $definitionInfo) {
|
||||
$headers[] = [$definition_id => $definitionInfo['name'], 'sortable' => false];
|
||||
foreach ($definition_names as $definition_id => $definition_name) {
|
||||
$headers[] = [$definition_id => $definition_name, 'sortable' => false];
|
||||
}
|
||||
|
||||
$headers[] = ['inventory' => '', 'escape' => false];
|
||||
@@ -480,7 +479,7 @@ function get_item_data_row(object $item): array
|
||||
$item->name .= NAME_SEPARATOR . $item->pack_name;
|
||||
}
|
||||
|
||||
$definition_names = $attribute->get_definitions_by_flags($attribute::SHOW_IN_ITEMS, true);
|
||||
$definition_names = $attribute->get_definitions_by_flags($attribute::SHOW_IN_ITEMS);
|
||||
|
||||
$columns = [
|
||||
'items.item_id' => $item->item_id,
|
||||
@@ -578,8 +577,8 @@ function item_kit_headers(): array
|
||||
['item_kit_number' => lang('Item_kits.item_kit_number')],
|
||||
['name' => lang('Item_kits.name')],
|
||||
['description' => lang('Item_kits.description')],
|
||||
['total_cost_price' => lang('Items.cost_price'), 'sortable' => false],
|
||||
['total_unit_price' => lang('Items.unit_price'), 'sortable' => false]
|
||||
['total_cost_price' => lang('Items.cost_price'), 'sortable' => FALSE],
|
||||
['total_unit_price' => lang('Items.unit_price'), 'sortable' => FALSE]
|
||||
];
|
||||
}
|
||||
|
||||
@@ -635,7 +634,7 @@ function parse_attribute_values(array $columns, array $row): array
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $definition_names Array of definition_id => ['name' => name, 'type' => type] or definition_id => name
|
||||
* @param array $definition_names
|
||||
* @param array $row
|
||||
* @return array
|
||||
*/
|
||||
@@ -652,16 +651,10 @@ function expand_attribute_values(array $definition_names, array $row): array
|
||||
}
|
||||
|
||||
$attribute_values = [];
|
||||
foreach ($definition_names as $definition_id => $definitionInfo) {
|
||||
foreach ($definition_names as $definition_id => $definition_name) {
|
||||
if (isset($indexed_values[$definition_id])) {
|
||||
$raw_value = $indexed_values[$definition_id];
|
||||
|
||||
// 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;
|
||||
}
|
||||
$attribute_value = $indexed_values[$definition_id];
|
||||
$attribute_values["$definition_id"] = $attribute_value;
|
||||
} else {
|
||||
$attribute_values["$definition_id"] = "";
|
||||
}
|
||||
@@ -743,7 +736,7 @@ function get_expense_category_manage_table_headers(): string
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the html data row for the expense category
|
||||
* Gets the html data row for the expenses category
|
||||
*/
|
||||
function get_expense_category_data_row(object $expense_category): array
|
||||
{
|
||||
@@ -842,7 +835,7 @@ function get_expenses_data_last_row(object $expense): array
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the expense payments summary
|
||||
* Get the expenses payments summary
|
||||
*/
|
||||
function get_expenses_manage_payments_summary(array $payments, ResultInterface $expenses): string // TODO: $expenses is passed but never used.
|
||||
{
|
||||
@@ -932,24 +925,3 @@ function get_controller(): string
|
||||
$controller_name_parts = explode('\\', $controller_name);
|
||||
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,7 +143,8 @@ function get_tax_rates_manage_table_headers(): string
|
||||
*/
|
||||
function get_tax_rates_data_row($tax_rates_row): array
|
||||
{
|
||||
$controller_name = 'taxes';
|
||||
$router = service('router');
|
||||
$controller_name = strtolower($router->controllerName());
|
||||
|
||||
return [
|
||||
'tax_rate_id' => $tax_rates_row->tax_rate_id,
|
||||
|
||||
@@ -7,7 +7,7 @@ if (!function_exists('base64url_encode')) {
|
||||
* @param string $data
|
||||
* @return string
|
||||
*/
|
||||
function base64url_encode(string $data): string
|
||||
function base64url_encode($data)
|
||||
{
|
||||
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
||||
}
|
||||
@@ -20,7 +20,7 @@ if (!function_exists('base64url_decode')) {
|
||||
* @param string $data
|
||||
* @return string|false
|
||||
*/
|
||||
function base64url_decode(string $data): false|string
|
||||
function base64url_decode($data)
|
||||
{
|
||||
$remainder = strlen($data) % 4;
|
||||
if ($remainder) {
|
||||
@@ -28,4 +28,4 @@ if (!function_exists('base64url_decode')) {
|
||||
}
|
||||
return base64_decode(strtr($data, '-_', '+/'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "هل أنت متأكد من أنك تريد حذف الميزات المحددة ؟",
|
||||
"confirm_restore" => "هل أنت متأكد من أنك تريد استعادة السمة (السمات) المحددة؟",
|
||||
"definition_cannot_be_deleted" => "لا يمكن حذف السمات المحددة",
|
||||
"definition_invalid_group" => "المجموعة المحددة غير موجودة أو غير صالحة.",
|
||||
"definition_error_adding_updating" => "لا يمكن إضافة السمة {0} أو تحديثها. يرجى التحقق من سجل الخطأ.",
|
||||
"definition_flags" => "رؤية الميزات",
|
||||
"definition_group" => "المجموعة",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'all' => "الجميع",
|
||||
'columns' => "أعمدة",
|
||||
'hide_show_pagination' => "عرض/إخفاء أرقام الصفحات",
|
||||
'loading' => "جارى التحميل، برجاء الإنتظار",
|
||||
'page_from_to' => "عرض {0} إلى {1} من {2} صفوف",
|
||||
'refresh' => "إعادة تحميل",
|
||||
'rows_per_page' => "{0} صف بالصفحة",
|
||||
'toggle' => "تغيير",
|
||||
"all" => "الجميع",
|
||||
"columns" => "أعمدة",
|
||||
"hide_show_pagination" => "عرض/إخفاء أرقام الصفحات",
|
||||
"loading" => "جارى التحميل، برجاء الإنتظار ...",
|
||||
"page_from_to" => "عرض {0} إلى {1} من {2} صفوف",
|
||||
"refresh" => "إعادة تحميل",
|
||||
"rows_per_page" => "{0} صف بالصفحة",
|
||||
"toggle" => "تغيير",
|
||||
];
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<?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,7 +282,6 @@ return [
|
||||
"right" => "يمين",
|
||||
"sales_invoice_format" => "شكل فاتورة البيع",
|
||||
"sales_quote_format" => "شكل فاتورة عرض الاسعار",
|
||||
"mailpath_invalid" => "",
|
||||
"saved_successfully" => "تم حفظ التهيئة بنجاح.",
|
||||
"saved_unsuccessfully" => "لم يتم حفظ التهيئة بنجاح.",
|
||||
"security_issue" => "تحذير من ثغرة أمنية",
|
||||
|
||||
@@ -14,8 +14,6 @@ return [
|
||||
"current_password_invalid" => "كلمة المرور الحالية غير صحيحة.",
|
||||
"employee" => "موظف",
|
||||
"error_adding_updating" => "خطاء فى إضافة/تعديل موظف.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "لايمكن حذف المستخدم admin الخاص بنسخة العرض.",
|
||||
"error_updating_demo_admin" => "لايمكن تغيير بيانات المستخدم admin الخاص بنسخة العرض.",
|
||||
"language" => "اللغة",
|
||||
|
||||
@@ -26,7 +26,6 @@ return [
|
||||
"cost_price_required" => "سعر التكلفة مطلوب.",
|
||||
"count" => "تحديث المخزون",
|
||||
"csv_import_failed" => "فشل الإستيراد من اكسل",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "الملف الذى رفعته إما فارغ أو أنه مختلف البنية.",
|
||||
"csv_import_partially_failed" => "يوجد خطأ بنسبة {0} في استيراد الاصناف في السطر: {1}. لم يتم استيرادهم.",
|
||||
"csv_import_success" => "تم استيراد الأصناف بنجاح.",
|
||||
|
||||
@@ -9,15 +9,6 @@ return [
|
||||
"login" => "دخول",
|
||||
"logout" => "تسجيل خروج",
|
||||
"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" => "كلمة السر",
|
||||
"required_username" => "",
|
||||
"username" => "اسم المستخدم",
|
||||
|
||||
@@ -73,12 +73,6 @@ return [
|
||||
"employee" => "الموظف",
|
||||
"entry" => "ادخال",
|
||||
"error_editing_item" => "خطاء فى تحرير الصنف",
|
||||
"negative_price_invalid" => "",
|
||||
"negative_quantity_invalid" => "",
|
||||
"negative_discount_invalid" => "",
|
||||
"discount_percent_exceeds_100" => "",
|
||||
"discount_exceeds_item_total" => "",
|
||||
"negative_total_invalid" => "",
|
||||
"find_or_scan_item" => "بحث/مسح باركود صنف",
|
||||
"find_or_scan_item_or_receipt" => "بحث/مسح باركود صنف أو ايصال",
|
||||
"giftcard" => "بطاقة هدية",
|
||||
|
||||
@@ -5,7 +5,6 @@ return [
|
||||
"confirm_delete" => "هل أنت متأكد من أنك تريد حذف الميزات المحددة ؟",
|
||||
"confirm_restore" => "هل أنت متأكد من أنك تريد استعادة السمة (السمات) المحددة؟",
|
||||
"definition_cannot_be_deleted" => "لا يمكن حذف السمات المحددة",
|
||||
"definition_invalid_group" => "المجموعة المحددة غير موجودة أو غير صالحة.",
|
||||
"definition_error_adding_updating" => "لا يمكن إضافة السمة {0} أو تحديثها. يرجى التحقق من سجل الخطأ.",
|
||||
"definition_flags" => "رؤية الميزات",
|
||||
"definition_group" => "المجموعة",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'all' => "الكل",
|
||||
'columns' => "أعمدة",
|
||||
'hide_show_pagination' => "عرض/إخفاء أرقام الصفحات",
|
||||
'loading' => "جارى التحميل، برجاء الإنتظار",
|
||||
'page_from_to' => "عرض {0} إلى {1} من {2} صفوف",
|
||||
'refresh' => "إعادة تحميل",
|
||||
'rows_per_page' => "{0} صف بالصفحة",
|
||||
'toggle' => "تغيير",
|
||||
"all" => "الكل",
|
||||
"columns" => "أعمدة",
|
||||
"hide_show_pagination" => "عرض/إخفاء أرقام الصفحات",
|
||||
"loading" => "جارى التحميل، برجاء الإنتظار ...",
|
||||
"page_from_to" => "عرض {0} إلى {1} من {2} صفوف",
|
||||
"refresh" => "إعادة تحميل",
|
||||
"rows_per_page" => "{0} صف بالصفحة",
|
||||
"toggle" => "تغيير",
|
||||
];
|
||||
|
||||
@@ -282,7 +282,6 @@ return [
|
||||
"right" => "يمين",
|
||||
"sales_invoice_format" => "شكل فاتورة البيع",
|
||||
"sales_quote_format" => "شكل فاتورة عرض الاسعار",
|
||||
"mailpath_invalid" => "",
|
||||
"saved_successfully" => "تم حفظ التهيئة بنجاح.",
|
||||
"saved_unsuccessfully" => "لم يتم حفظ التهيئة بنجاح.",
|
||||
"security_issue" => "تحذير من ثغرة أمنية",
|
||||
|
||||
@@ -14,8 +14,6 @@ return [
|
||||
"current_password_invalid" => "كلمة المرور الحالية غير صحيحة.",
|
||||
"employee" => "موظف",
|
||||
"error_adding_updating" => "خطاء فى إضافة/تعديل موظف.",
|
||||
"error_deleting_admin" => "",
|
||||
"error_updating_admin" => "",
|
||||
"error_deleting_demo_admin" => "لايمكن حذف المستخدم admin الخاص بنسخة العرض.",
|
||||
"error_updating_demo_admin" => "لايمكن تغيير بيانات المستخدم admin الخاص بنسخة العرض.",
|
||||
"language" => "اللغة",
|
||||
|
||||
@@ -26,7 +26,6 @@ return [
|
||||
"cost_price_required" => "سعر التكلفة مطلوب.",
|
||||
"count" => "تحديث المخزون",
|
||||
"csv_import_failed" => "فشل الإستيراد من اكسل",
|
||||
"csv_import_invalid_location" => "",
|
||||
"csv_import_nodata_wrongformat" => "الملف الذى رفعته إما فارغ أو أنه مختلف البنية.",
|
||||
"csv_import_partially_failed" => "يوجد خطأ بنسبة {0} في استيراد الاصناف في السطر: {1}. لم يتم استيرادهم.",
|
||||
"csv_import_success" => "تم استيراد الأصناف بنجاح.",
|
||||
|
||||
@@ -9,15 +9,6 @@ return [
|
||||
"login" => "دخول",
|
||||
"logout" => "تسجيل خروج",
|
||||
"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" => "كلمة السر",
|
||||
"required_username" => "خانة أسم المستخدم مطلوبة.",
|
||||
"username" => "اسم المستخدم",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user