mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-10-08 02:22:17 -04:00
Merge branch 'master' into feat-export-toggle-all
This commit is contained in:
239 files changed
+6385
-2374
No files matched your search
+2
-1
@@ -40,7 +40,8 @@ composer.lock
|
||||
package.json
|
||||
package-lock.json
|
||||
gulpfile.js
|
||||
.env.example
|
||||
.env
|
||||
.env.*
|
||||
.dockerignore
|
||||
|
||||
# Temporary and backup files
|
||||
|
||||
@@ -60,6 +60,13 @@ database.tests.DBPrefix='ospos_'
|
||||
# ENCRYPTION_KEY is read as a fallback when encryption.key is empty, so no
|
||||
# shared key needs to be committed or baked into the shipped image.
|
||||
encryption.key=''
|
||||
# Persistent secret for HMAC-hashing login-throttle cache keys. Left blank and
|
||||
# provisioned on startup (php spark env:provision); independent of encryption.key.
|
||||
# For Docker/Compose, pass it via the THROTTLE_KEY env var instead:
|
||||
# docker run -e THROTTLE_KEY="$(openssl rand -hex 32)" opensourcepos
|
||||
# THROTTLE_KEY is read as a fallback when throttle.key is empty, so no
|
||||
# shared secret needs to be committed or baked into the shipped image.
|
||||
throttle.key=''
|
||||
|
||||
#--------------------------------------------------------------------
|
||||
# LOGGER
|
||||
|
||||
@@ -13,8 +13,10 @@ This document describes the CI/CD workflows for OSPOS.
|
||||
|
||||
### 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
|
||||
- On `master`: tagged `master` and `<sha>`
|
||||
- On other branches: tagged `<branch>-<sha>`
|
||||
- On a semver tag (e.g. `3.4.2`): tagged `<version>` and `latest`
|
||||
- The version number is never stamped onto `master`/branch builds — it only appears on tag releases
|
||||
- Pushed to Docker Hub
|
||||
|
||||
### Releases
|
||||
@@ -39,10 +41,10 @@ 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)
|
||||
- **Push to master** - Runs build, Docker push (`master` + `<sha>` tags), and creates/updates the `unstable` release
|
||||
- **Push to other branches** - Runs build and Docker push (`<branch>-<sha>` tag)
|
||||
- **Push a semver tag** (e.g. `3.4.2`) - Runs build and Docker push (`<version>` + `latest` tags)
|
||||
- **Pull requests** - Runs build only (PHPUnit tests run in parallel via phpunit.yml); no Docker push
|
||||
|
||||
## Existing Workflows
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@ name: Build and Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
tags:
|
||||
- '[0-9]+.[0-9]+.[0-9]+'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
@@ -86,6 +90,18 @@ jobs:
|
||||
env:
|
||||
GITHUB_TAG: ${{ github.ref_name }}
|
||||
|
||||
- name: Validate release tag
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
# The ZIP below is named from App.php, while the draft-release job
|
||||
# globs by the tag name. A manually-pushed tag that does not match
|
||||
# App.php would silently produce a draft release with no archive, so
|
||||
# fail fast instead.
|
||||
if [ "${GITHUB_REF_NAME}" != "${{ steps.version.outputs.version }}" ]; then
|
||||
echo "::error::tag ${GITHUB_REF_NAME} does not match App.php version ${{ steps.version.outputs.version }}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create .env file
|
||||
run: |
|
||||
cp .env.example .env
|
||||
@@ -154,11 +170,19 @@ jobs:
|
||||
- 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
|
||||
REGISTRY="${{ secrets.DOCKER_USERNAME }}/opensourcepos"
|
||||
SHA="${{ needs.build.outputs.short-sha }}"
|
||||
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
|
||||
VERSION="${GITHUB_REF#refs/tags/}"
|
||||
echo "tags=${REGISTRY}:${VERSION},${REGISTRY}:latest" >> "$GITHUB_OUTPUT"
|
||||
elif [[ "$GITHUB_REF" == refs/heads/master ]]; then
|
||||
echo "tags=${REGISTRY}:master,${REGISTRY}:${SHA}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "tags=${{ secrets.DOCKER_USERNAME }}/opensourcepos:${{ needs.build.outputs.version-tag }}" >> $GITHUB_OUTPUT
|
||||
BRANCH="${GITHUB_REF#refs/heads/}"
|
||||
BRANCH="$(printf '%s' "$BRANCH" | LC_ALL=C tr -c 'A-Za-z0-9_.-' '_')"
|
||||
BRANCH="${BRANCH:0:$((128 - ${#SHA} - 1))}"
|
||||
[[ "$BRANCH" == [-.]* ]] && BRANCH="_${BRANCH:1}"
|
||||
echo "tags=${REGISTRY}:${BRANCH}-${SHA}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
@@ -172,8 +196,8 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ steps.tags.outputs.tags }}
|
||||
|
||||
release:
|
||||
name: Create Release
|
||||
unstable-release:
|
||||
name: Create Unstable Release
|
||||
needs: build
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
@@ -213,3 +237,77 @@ jobs:
|
||||
draft: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
official-release:
|
||||
name: Create Official Release (Draft)
|
||||
needs: [build, docker]
|
||||
runs-on: ubuntu-22.04
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist-${{ needs.build.outputs.short-sha }}
|
||||
path: dist/
|
||||
|
||||
- name: Build release notes
|
||||
run: |
|
||||
VERSION="${GITHUB_REF_NAME}"
|
||||
# Extract this version's changelog section (header + entries) from CHANGELOG.md.
|
||||
SECTION=$(awk -v v="$VERSION" '
|
||||
$0 ~ "^## \\[" v "\\] - " {insec=1; print; next}
|
||||
insec && $0 ~ "^## \\[" {insec=0; next}
|
||||
insec {print}
|
||||
' CHANGELOG.md)
|
||||
if [ -z "$SECTION" ]; then
|
||||
echo "WARNING: no changelog section found for $VERSION"
|
||||
SECTION="Changelog section for ${VERSION} is not present in CHANGELOG.md."
|
||||
fi
|
||||
{
|
||||
echo "## Upgrade instructions"
|
||||
echo ""
|
||||
echo "**Docker:**"
|
||||
echo ""
|
||||
echo '```bash'
|
||||
echo "docker pull ${{ secrets.DOCKER_USERNAME }}/opensourcepos:${VERSION}"
|
||||
echo "docker compose -f docker-compose.nginx.yml up -d"
|
||||
echo '```'
|
||||
echo ""
|
||||
echo "**Existing installation:** download one of the archives below, extract it over your current install, and run any new database migrations."
|
||||
echo ""
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "$SECTION"
|
||||
} > /tmp/release_notes.md
|
||||
echo "=== release notes (first 12 lines) ==="
|
||||
head -12 /tmp/release_notes.md
|
||||
env:
|
||||
GITHUB_REF_NAME: ${{ github.ref_name }}
|
||||
|
||||
- name: Create draft release
|
||||
run: |
|
||||
VERSION="${GITHUB_REF_NAME}"
|
||||
ZIP=$(ls dist/opensourcepos."${VERSION}".*.zip 2>/dev/null | head -1)
|
||||
if [ -n "$ZIP" ]; then
|
||||
gh release create "$VERSION" \
|
||||
--draft \
|
||||
--title "OpenSourcePOS ${VERSION}" \
|
||||
--notes-file /tmp/release_notes.md \
|
||||
"$ZIP"
|
||||
else
|
||||
gh release create "$VERSION" \
|
||||
--draft \
|
||||
--title "OpenSourcePOS ${VERSION}" \
|
||||
--notes-file /tmp/release_notes.md
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_REF_NAME: ${{ github.ref_name }}
|
||||
@@ -8,15 +8,25 @@ on:
|
||||
jobs:
|
||||
delete_unstable_release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Delete last unstable release"
|
||||
uses: sgpublic/delete-release-action@v1.2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.TOKEN }}
|
||||
with:
|
||||
release-drop: false
|
||||
release-drop-tag: false
|
||||
pre-release-drop: true
|
||||
pre-release-keep-count: -1
|
||||
pre-release-drop-tag: true
|
||||
|
||||
steps:
|
||||
- name: "Delete the unstable release (matched by name)"
|
||||
env:
|
||||
TOKEN: ${{ secrets.TOKEN }}
|
||||
RELEASE_NAME: "Unstable OpenSourcePOS"
|
||||
TAG_NAME: "unstable"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
api="https://api.github.com/repos/${GITHUB_REPOSITORY}"
|
||||
# Resolve the release id from its exact name so only this one release
|
||||
# is ever touched; drafts and every other release are left alone.
|
||||
id=$(curl -fsSL -H "Authorization: Bearer ${TOKEN}" "${api}/releases?per_page=100" \
|
||||
| jq -r --arg n "${RELEASE_NAME}" '.[] | select(.name==$n) | .id // empty')
|
||||
if [ -z "${id}" ]; then
|
||||
echo "No release named '${RELEASE_NAME}' found; nothing to do."
|
||||
exit 0
|
||||
fi
|
||||
curl -fsSL -X DELETE -H "Authorization: Bearer ${TOKEN}" "${api}/releases/${id}" >/dev/null
|
||||
echo "Deleted release '${RELEASE_NAME}' (id=${id})"
|
||||
curl -fsSL -X DELETE -H "Authorization: Bearer ${TOKEN}" "${api}/git/refs/tags/${TAG_NAME}" >/dev/null \
|
||||
|| echo "Tag '${TAG_NAME}' already gone; skipping."
|
||||
echo "Deleted tag '${TAG_NAME}'"
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
comment:
|
||||
name: Comment deployment status
|
||||
needs: [prepare, deploy]
|
||||
if: always()
|
||||
if: always() && needs.prepare.result == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
@@ -118,6 +118,22 @@ jobs:
|
||||
KEY=$(openssl rand -hex 32)
|
||||
printf 'ENCRYPTION_KEY=%s\n' "$KEY" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Provision per-run throttle key
|
||||
# checkThrottleEncryption() is a read-only guard that throws unless a
|
||||
# throttle.key is present (app/Helpers/security_helper.php). It is
|
||||
# normally minted at container startup via `php spark env:provision`,
|
||||
# so the test environment must provide one.
|
||||
#
|
||||
# We write it into the .env file (replacing the empty placeholder
|
||||
# copied from .env.example) rather than exporting an OS env var:
|
||||
# CodeIgniter's env() resolves $_ENV first, and DotEnv populates
|
||||
# $_ENV['throttle.key']='' from .env, which would shadow any OS var
|
||||
# before getenv() is ever consulted.
|
||||
run: |
|
||||
KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/^throttle\.key=''/throttle.key='$KEY'/" .env
|
||||
grep -Eq "^throttle\.key=.+" .env
|
||||
|
||||
- name: Run PHPUnit tests
|
||||
env:
|
||||
CI_ENVIRONMENT: testing
|
||||
|
||||
+132
-139
@@ -1,24 +1,40 @@
|
||||
name: Release Version Bump
|
||||
name: Release
|
||||
|
||||
# Cuts the current release (changelog + tag + draft release) and, optionally,
|
||||
# bumps App.php to the next dev version. One workflow, full release cycle.
|
||||
#
|
||||
# Flow:
|
||||
# 1. Release current version:
|
||||
# - generate git-cliff changelog section (base..HEAD)
|
||||
# - insert it into CHANGELOG.md + GFM compare refs
|
||||
# - commit + push the changelog
|
||||
# - tag the current version and push the tag with the repo PAT
|
||||
# (a PAT push triggers build-release.yml, whose official-release job
|
||||
# creates the DRAFT GitHub Release with the changelog + assets)
|
||||
# 2. (optional) bump to the next dev version:
|
||||
# - update App.php / package.json / docker-compose / issue templates
|
||||
# - commit + push
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_type:
|
||||
description: 'Version bump type'
|
||||
next_bump:
|
||||
description: 'After cutting this release, bump App.php to the next dev version'
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
- patch
|
||||
default: 'minor'
|
||||
- skip
|
||||
default: 'patch'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
prepare-release:
|
||||
name: Prepare Release
|
||||
release:
|
||||
name: Cut release and bump next version
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
@@ -26,147 +42,124 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Use the repo PAT (an admin token) so the changelog + bump pushes
|
||||
# below can update the protected master branch (enforce_admins is off).
|
||||
# GITHUB_TOKEN cannot, because master requires a PR review.
|
||||
token: ${{ secrets.TOKEN }}
|
||||
|
||||
- name: Get current version
|
||||
id: current_version
|
||||
- name: Install git-cliff
|
||||
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"
|
||||
V=2.14.2
|
||||
curl -sLSfL "https://github.com/orhun/git-cliff/releases/download/v${V}/git-cliff-${V}-x86_64-unknown-linux-musl.tar.gz" -o /tmp/cliff.tar.gz
|
||||
tar xzf /tmp/cliff.tar.gz -C /tmp
|
||||
mv "/tmp/git-cliff-${V}/git-cliff" /usr/local/bin/git-cliff
|
||||
chmod +x /usr/local/bin/git-cliff
|
||||
git cliff --version
|
||||
|
||||
- name: Calculate new version
|
||||
- name: Determine 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)
|
||||
APP_VERSION=$(grep "application_version" app/Config/App.php | sed "s/.*= '\(.*\)';/\1/g")
|
||||
TARGET="$APP_VERSION"
|
||||
if ! [[ "$TARGET" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "ERROR: target '$TARGET' is not in X.Y.Z form"; exit 1
|
||||
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"
|
||||
BASE=$(git tag --list | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1)
|
||||
if [ -z "$BASE" ]; then
|
||||
echo "ERROR: no previous release tag found"; exit 1
|
||||
fi
|
||||
if [ "$BASE" = "$TARGET" ]; then
|
||||
echo "ERROR: target $TARGET equals base $BASE; nothing to release"; exit 1
|
||||
fi
|
||||
if ! printf '%s\n%s\n' "$BASE" "$TARGET" | sort -VC 2>/dev/null; then
|
||||
echo "ERROR: target $TARGET is not greater than base $BASE"; exit 1
|
||||
fi
|
||||
echo "target=$TARGET" >> "$GITHUB_OUTPUT"
|
||||
echo "base=$BASE" >> "$GITHUB_OUTPUT"
|
||||
echo "Releasing $TARGET (previous release: $BASE)"
|
||||
|
||||
- name: Update version in issue templates
|
||||
- name: Generate changelog section
|
||||
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"
|
||||
git cliff --config cliff.toml \
|
||||
--tag "${{ steps.version.outputs.target }}" \
|
||||
--output /tmp/section.md \
|
||||
"${{ steps.version.outputs.base }}..HEAD"
|
||||
sed -i '/./,$!d' /tmp/section.md
|
||||
echo "Generated $(grep -c '^- ' /tmp/section.md) entries"
|
||||
echo "=== first 5 ==="
|
||||
head -5 /tmp/section.md
|
||||
|
||||
- name: Commit version bump
|
||||
- name: Insert into CHANGELOG.md
|
||||
run: |
|
||||
TARGET="${{ steps.version.outputs.target }}"
|
||||
BASE="${{ steps.version.outputs.base }}"
|
||||
sed -i "/^\[unreleased\]:/a [${TARGET}]: https://github.com/opensourcepos/opensourcepos/compare/${BASE}...${TARGET}" CHANGELOG.md
|
||||
sed -i "s|^\[unreleased\]:.*|\[unreleased\]: https://github.com/opensourcepos/opensourcepos/compare/${TARGET}...HEAD|" CHANGELOG.md
|
||||
LINE=$(grep -nE '^## \[[0-9]+\.' CHANGELOG.md | head -1 | cut -d: -f1)
|
||||
printf '%s\n\n' "$(cat /tmp/section.md)" > /tmp/section_final.md
|
||||
sed -i "$((LINE-1))r /tmp/section_final.md" CHANGELOG.md
|
||||
echo "Inserted $TARGET section before line $LINE"
|
||||
|
||||
- name: Commit changelog
|
||||
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
|
||||
git add CHANGELOG.md
|
||||
git commit -m "docs: add ${{ steps.version.outputs.target }} changelog"
|
||||
git push origin HEAD
|
||||
|
||||
- name: Tag and push (PAT triggers build-release.yml)
|
||||
run: |
|
||||
TAG="${{ steps.version.outputs.target }}"
|
||||
git tag "$TAG"
|
||||
# Push with the repo PAT (not GITHUB_TOKEN) so the tag-push event
|
||||
# triggers build-release.yml, whose official-release job creates the
|
||||
# draft release with the changelog and assets.
|
||||
git push "https://x-access-token:${RELEASE_PAT}@github.com/opensourcepos/opensourcepos.git" "refs/tags/$TAG"
|
||||
echo "Released $TAG — draft release will be created by build-release.yml"
|
||||
env:
|
||||
RELEASE_PAT: ${{ secrets.TOKEN }}
|
||||
|
||||
- name: Bump to next dev version (optional)
|
||||
if: ${{ inputs.next_bump != 'skip' }}
|
||||
run: |
|
||||
CURRENT="${{ steps.version.outputs.target }}"
|
||||
MAJOR=$(echo "$CURRENT" | cut -d. -f1)
|
||||
MINOR=$(echo "$CURRENT" | cut -d. -f2)
|
||||
PATCH=$(echo "$CURRENT" | cut -d. -f3)
|
||||
case "${{ inputs.next_bump }}" in
|
||||
major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
|
||||
minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
|
||||
patch) PATCH=$((PATCH + 1)) ;;
|
||||
*) echo "ERROR: unknown bump type ${{ inputs.next_bump }}"; exit 1 ;;
|
||||
esac
|
||||
NEXT="${MAJOR}.${MINOR}.${PATCH}"
|
||||
echo "Bumping to next dev version $NEXT"
|
||||
|
||||
sed -i "s/public string \\\$application_version = '[^']*';/public string \\\$application_version = '$NEXT';/" app/Config/App.php
|
||||
sed -i "s/\"version\": \"[^\"]*\",/\"version\": \"$NEXT\",/" package.json
|
||||
# Keep package-lock.json in sync: bump only the @opensourcepos/opensourcepos
|
||||
# package version (top-level + packages.""), leave dependency versions alone.
|
||||
awk -v v="$NEXT" '
|
||||
/"name": "@opensourcepos\/opensourcepos"/ { expect=1 }
|
||||
expect && /"version": / { sub(/"version": "[^"]*"/, "\"version\": \"" v "\""); expect=0 }
|
||||
{ print }
|
||||
' package-lock.json > .package-lock.tmp && mv .package-lock.tmp package-lock.json
|
||||
# Update the "latest X.Y version" README line only when major.minor changes
|
||||
NEW_MM=$(echo "$NEXT" | cut -d. -f1,2)
|
||||
OLD_MM=$(echo "$CURRENT" | cut -d. -f1,2)
|
||||
if [ "$NEW_MM" != "$OLD_MM" ]; then
|
||||
sed -i "s/The latest \`[0-9]*\.[0-9]*\` version/The latest \`${NEW_MM}\` version/" README.md
|
||||
fi
|
||||
|
||||
echo "=== version refs after bump ==="
|
||||
grep "application_version" app/Config/App.php
|
||||
grep '"version"' package.json | head -1
|
||||
grep -A1 '"name": "@opensourcepos/opensourcepos"' package-lock.json
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add app/Config/App.php package.json package-lock.json
|
||||
[ "$NEW_MM" != "$OLD_MM" ] && git add README.md
|
||||
git commit -m "chore: bump version to $NEXT"
|
||||
git push origin HEAD
|
||||
@@ -12,7 +12,7 @@ This document provides guidance for AI agents working on the Open Source Point o
|
||||
- PHP 8.2+ features acceptable (named arguments, enums, readonly properties)
|
||||
- Write PHP 8.2+ compatible code with proper type declarations
|
||||
- Always import classes, functions, and constants with a `use` statement at the top of the file instead of referencing them inline via fully-qualified name (e.g. `use Config\Database;` then `Database::connect()`, not `\Config\Database::connect()`)
|
||||
- Do not add comments or docblocks that merely restate what the code already makes clear — only comment on non-obvious rationale, constraints, or behavior
|
||||
- No useless comments or docblocks: if the code speaks for itself, explanatory comments are unnecessary. Do not add comments or docblocks that merely restate what the code already makes clear (e.g. `@param array $items` next to `array $items`, or a comment repeating a method's name) — only comment on non-obvious rationale, constraints, or behavior
|
||||
- Views in `app/Views/errors/html/` are excluded from the fixer
|
||||
- Run fixer before committing: `vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.no-header.php`
|
||||
- **JavaScript**: use `const` for variables that are never reassigned, `let` for variables that are. Never use `var`.
|
||||
@@ -27,6 +27,7 @@ This document provides guidance for AI agents working on the Open Source Point o
|
||||
- Run PHPUnit tests: `composer test`
|
||||
- Tests must pass before submitting changes
|
||||
- **One test file per class under test.** A controller, model, library, or helper gets exactly one test file covering all of its behavior — `Item_kits.php` → `tests/Controllers/Item_kitsTest.php` (or `Item_kitsControllerTest.php`, matching this codebase's existing `*ControllerTest.php` suffix for controllers), `Sale_lib.php` → `tests/Libraries/Sale_libTest.php`, etc. Do not create feature- or endpoint-scoped test files alongside a class's main test file (e.g. no `Item_kitsBarcodeTest.php` next to `Item_kitsControllerTest.php`) — add the new test methods to the existing file for that class instead. If no test file exists yet for the class, create the one canonical file rather than a narrowly-scoped one.
|
||||
- **Name test methods as `test{MethodUnderTest}_{Behavior}`.** `PascalCase` the method under test, then an underscore, then a short behavior phrase — `testPostSave_RejectsNegativeTaxPercent`, `testSaveLocale_AcceptsValidReferenceCodeMinMax`. Start the behavior with a verb: `Accepts`/`Requires` for valid or boundary cases, `Rejects` for invalid ones. The name should read as a self-documenting sentence and needs no docblock.
|
||||
|
||||
## Build
|
||||
|
||||
@@ -50,6 +51,7 @@ This document provides guidance for AI agents working on the Open Source Point o
|
||||
- **When explicitly asked to translate a phrase for a non-English language file, always provide the actual translation** — never leave the value as an empty string, and never leave source English text in a non-English language file
|
||||
- Never copy English text from a neighboring key as a value for a non-English language file, even if that neighboring key is already untranslated — evaluate each key independently
|
||||
- Only `app/Language/en/` and `app/Language/en-GB/` should contain English strings
|
||||
- When translating a string containing placeholders (e.g. `{filePath}`, `{reason}`) or literal filenames/keys (e.g. `throttle.key`, `.env`), keep that placeholder or filename text unchanged and untranslated — move it to whatever position is grammatically correct in the translated sentence
|
||||
- Plugin language files (`app/Plugins/*/Language/`) follow the same localization rules as `app/Language/`
|
||||
- Use `'` to encapsulate key and string values. If the value contains `'` then it should be escaped as `\'`
|
||||
- Align the `=>` of a newly inserted key with the `=>` column already used by the rest of the file, if that file pads keys to a fixed column (not all do — some files have no padding at all). If the new key is longer than the widest existing key and would push the alignment column further right, reformat the whole file to the new wider column rather than leaving only the new line at a different width
|
||||
@@ -59,3 +61,4 @@ This document provides guidance for AI agents working on the Open Source Point o
|
||||
- Never commit secrets, credentials, or `.env` files
|
||||
- Use parameterized queries to prevent SQL injection
|
||||
- Validate and sanitize all user input
|
||||
- Never reference security advisory IDs (CVE, GHSA, etc.) in code, comments, commit messages, docblocks, documentation, or URLs — treat them the same as secrets. They act as a roadmap for attackers researching the exact exploit a fix addresses.
|
||||
@@ -14,6 +14,7 @@ The build process uses the build tools "npm" and "gulp" to piece everything toge
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Install Node.js 20 or later (the build fails on Node 18 and earlier - one of the license reporting dependencies requires newer JavaScript regex features)
|
||||
- Install the latest version of NPM (tested using version 9.4.2)
|
||||
- Install the latest version of Composer (tested using composer 2.5.1)
|
||||
|
||||
|
||||
+239
-1
@@ -1,4 +1,5 @@
|
||||
[unreleased]: https://github.com/opensourcepos/opensourcepos/compare/3.4.1...HEAD
|
||||
[unreleased]: https://github.com/opensourcepos/opensourcepos/compare/3.4.2...HEAD
|
||||
[3.4.2]: https://github.com/opensourcepos/opensourcepos/compare/3.4.1...3.4.2
|
||||
[3.4.1]: https://github.com/opensourcepos/opensourcepos/compare/3.4.0...3.4.1
|
||||
[3.4.0]: https://github.com/opensourcepos/opensourcepos/compare/3.3.9...3.4.0
|
||||
[3.3.9]: https://github.com/opensourcepos/opensourcepos/compare/3.3.8...3.3.9
|
||||
@@ -33,6 +34,243 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [3.4.2] - 2026-09-30
|
||||
- Fix writable folder permission check (#4270) (#4273) by @jekkos
|
||||
- Extended payment delete fix (#4274) by @jekkos
|
||||
- Upgrade github workflow (#3708) (#4280) by @jekkos
|
||||
- Fix typo in writeable (#4270) by @jekkos
|
||||
- Fix migration 20250522000000 (#4284) by @jekkos
|
||||
- Upgrade to ci 4.6.2 (#4296) (#4298) by @jekkos
|
||||
- Fix barcode generation in items (#4270) by @jekkos
|
||||
- Allow empty tax category id (#4285) (#4288) by @jekkos
|
||||
- Fix security incident email address (#4298) by @jekkos
|
||||
- Fix item kits update (#4294) by @jekkos
|
||||
- Revert toast message sanitization (#4302) by @jekkos
|
||||
- Fix for suspended sales (#4283) (#4303) by @jekkos
|
||||
- Fix reference to uploads folder (#4270) (#4286) by @jekkos
|
||||
- Add generic try/catch in import (#4302) by @jekkos
|
||||
- Bump jspdf from 3.0.1 to 3.0.2 (#4309) by @dependabot[bot]
|
||||
- Fix mount path for uploads (#4308) by @jekkos
|
||||
- Add transactions to missing config keys migration. (#4318) by @Joe Williams
|
||||
- [Feature] Add logging to migrations (#4327) by @Joe Williams
|
||||
- Clean up docker compose setup (#4308) by @jekkos
|
||||
- Fix tax configuration pages (#4331) by @jekkos
|
||||
- Update SECURITY.md contact (#4335) by @jekkos
|
||||
- Add server side validation for password (#4335) by @jekkos
|
||||
- Add env variable to disallow pwd change (#4325) by @jekkos
|
||||
- Add recent releases to issue template (#4317) by @jekkos
|
||||
- Add DOMpurify + fix XSS (#4341) by @jekkos
|
||||
- Fix attachment cid (#4314) by @jekkos
|
||||
- Add DOMPurify to JS includes (#4341) by @jekkos
|
||||
- Allow anonymous giftcard creation (#4278) by @jekkos
|
||||
- Fix toast notifications in config (#4341) (#4343) by @jekkos
|
||||
- Fix creation of date attribute value (#4310) (#4344) by @jekkos
|
||||
- Fix wrong migration script location (#4285) by @jekkos
|
||||
- Escape return_policy in receipt + invoice (#4349) by @jekkos
|
||||
- Fix for detailed suppliers report (#4351) by @jekkos
|
||||
- Add show/hide cost price & profit feature - in reports #4130 (#4350) by @BhojKamal
|
||||
- Fix travis build after merge (#4130) by @jekkos
|
||||
- Add equals as permitted URI character (#4329) by @Chathura Dilushanka
|
||||
- Fix multiple XSS vulnerabilities (#3965) (#4356) by @jekkos
|
||||
- Bump lodash from 4.17.21 to 4.17.23 (#4369) by @dependabot[bot]
|
||||
- Bump jspdf and jspdf-autotable (#4373) by @dependabot[bot]
|
||||
- Fix XSS vulnerabilities in invoices + receipts (#3965) (#4363) by @jekkos
|
||||
- Fix XSS vulnerability in attributes (#3965) by @jekkos
|
||||
- Fix XSS vulnerability in register (#3965) by @jekkos
|
||||
- Fix XSS vulnerability in register (#3965) by @jekkos
|
||||
- Fix XSS vulnerabilities in invoice_email.php view by @jekkos
|
||||
- Fix permission bypass in Reports submodule access control (#4389) by @jekkos
|
||||
- Use Content-Type application/json for AJAX responses (#4357) by @jekkos
|
||||
- Language Array Key Typo Fix (#4371) by @Lucas Lyimo
|
||||
- Fix: Refresh session language for employee after update. (#4245) by @jekkos
|
||||
- Fix Docker image upload by replacing slashes in TAG by @jekkos
|
||||
- Fix broken object-level authorization in Employees controller (CVE-worthy) (#4391) by @jekkos
|
||||
- Bump dompurify from 3.3.1 to 3.3.2 (#4402) by @dependabot[bot]
|
||||
- Fix incorrect argument types in migration round_number() methods (#4403) by @jekkos
|
||||
- dd validation for invalid stock locations in CSV import (#4399) by @jekkos
|
||||
- fix(security): whitelist and validate invoice template types (#4393) by @jekkos
|
||||
- Fix second-order SQL injection in currency_symbol config (#4390) by @jekkos
|
||||
- Add row-level authorization to password change endpoints (#4401) by @jekkos
|
||||
- Fix: Handle image filenames with spaces in thumbnails by @jekkos
|
||||
- Fix: Sanitize image filenames to prevent thumbnail display issues (#4372) by @jekkos
|
||||
- Add migration to fix existing image filenames with spaces (#4372) by @jekkos
|
||||
- Refactor: Move ADMIN_MODULES to constants, rename methods to camelCase by @jekkos
|
||||
- Fix SQL injection in custom attribute search by @Ollama
|
||||
- Fix stored XSS vulnerability in item descriptions by @Ollama
|
||||
- Fix stored XSS vulnerabilities in employee permissions and customer data by @Ollama
|
||||
- Fix: Preserve CHECKBOX attribute state when adding attributes (#4385) by @jekkos
|
||||
- Fix payment type becoming null when editing sales by @Ollama
|
||||
- Fix broken SQL injection fix - use havingLike() instead of having() with named params by @Ollama
|
||||
- Fix mass assignment vulnerability in bulk edit (GHSA-49mq-h2g4-grr9) by @Ollama
|
||||
- Sync language files (#3468) by @Ollama
|
||||
- Add workflow to auto-update issue templates with releases by @Ollama
|
||||
- Update SECURITY.md with published security advisories by @Ollama
|
||||
- Bump jspdf from 4.1.0 to 4.2.0 (#4383) by @dependabot[bot]
|
||||
- Add filter persistence for table views via URL query string (#4400) by @jekkos
|
||||
- Fix filter persistence javascript issues (#4400) by @jekkos
|
||||
- Fix PHPUnit test configuration for database connectivity (#4430) by @jekkos
|
||||
- Fix IDOR vulnerability in password change (GHSA-mcc2-8rp2-q6ch) (#4427) by @jekkos
|
||||
- Fix XSS vulnerability in tax invoice view (#4432) by @jekkos
|
||||
- Fix permission bypass in Sales.getManage() access control (#4428) by @jekkos
|
||||
- Update SECURITY.md with published security advisories (#4431) by @jekkos
|
||||
- Fix SQL injection in suggestions column configuration (#4421) by @jekkos
|
||||
- Fix PHPUnit environment variables not being set (#4434) by @jekkos
|
||||
- Fix DECIMAL attribute not respecting locale format (#4422) by @jekkos
|
||||
- Fix stored XSS vulnerability in Attribute Definitions (GHSA-rvfg-ww4r-rwqf) (#4429) by @jekkos
|
||||
- Fix: Host Header Injection vulnerability (GHSA-jchf-7hr6-h4f3) by @Ollama
|
||||
- Fix stored XSS in gcaptcha_site_key on login page by @Ollama
|
||||
- Fix stored XSS via stock location name by @Ollama
|
||||
- Fix Token_lib::render() for PHP 8.4 compatibility by @Ollama
|
||||
- Use CIUnitTestCase for consistency with other tests by @Ollama
|
||||
- Fix: Pass parameter to generate() and add composite format tests by @Ollama
|
||||
- Fix strftime directives handling and tighten test assertions by @Ollama
|
||||
- Add AGENTS.md with coding guidelines for AI agents by @Ollama
|
||||
- Fix: Add Debit Card filter to Daily Sales and Takings by @Ollama
|
||||
- Fix Taxes Summary Report totals not matching row values by @Ollama
|
||||
- Add unit tests for Taxes Summary Report calculations by @Ollama
|
||||
- Fix rounding consistency and update tests per review feedback by @Ollama
|
||||
- Rewrite tests to use database integration testing by @Ollama
|
||||
- Add seed data to tests for proper integration testing by @Ollama
|
||||
- Fix: Restrict employee selection in expenses and receivings forms by @Ollama
|
||||
- Fix review comments: remove redundant loop and add XSS escaping by @Ollama
|
||||
- Bump jspdf from 4.2.0 to 4.2.1 by @dependabot[bot]
|
||||
- Bump picomatch from 2.3.1 to 2.3.2 (#4451) by @dependabot[bot]
|
||||
- fix: Clear sale session after completing sale by @Ollama
|
||||
- fix: Remove redundant clear_mode() calls by @Ollama
|
||||
- Translate missing strings in multiple languages by @Ollama
|
||||
- Fix translation issues from code review by @Ollama
|
||||
- Remove English fallbacks from non-English translations by @Ollama
|
||||
- Add Calendar.php translations for missing languages by @Ollama
|
||||
- feat: migrate CI from Travis to GitHub Actions with enhancements by @Ollama
|
||||
- refactor: remove tables.sql and constraints.sql (#4447) by @Ollama
|
||||
- refactor: remove build-database gulp task (#4447) by @Ollama
|
||||
- refactor: optimize Docker image size by @Ollama
|
||||
- fix: remove duplicate phpunit.xml that prevented tests from running by @Ollama
|
||||
- fix: Use file-based session until database is migrated by @Ollama
|
||||
- feat: Improve migration UX on login page by @Ollama
|
||||
- Disable opencode workflow + run docker build by @jekkos
|
||||
- Fix negative price/quantity/discount validation (GHSA-wv3j-pp8r-7q43) (#4450) by @Nozomu Sasaki (Paul)
|
||||
- fix(ci): replace / with _ in branch names for Docker tags by @Ollama
|
||||
- fix(security): prevent command injection in sendmail path configuration by @Ollama
|
||||
- fix(security): prevent SQL injection in tax controller sort columns by @Ollama
|
||||
- feat: add release workflow with automated version bumping by @Ollama
|
||||
- refactor: simplify release workflow to version bump only by @Ollama
|
||||
- fix: address review comments by @Ollama
|
||||
- fix: address all review comments and restore issue template version update by @Ollama
|
||||
- fix: Tax Rate form not loading due to router service failure (#4479) by @jekkos
|
||||
- fix: Handle empty database on fresh install (#4467) by @jekkos
|
||||
- Fix: Improve allowedHostnames .env configuration and fail-fast in production (#4482) by @jekkos
|
||||
- [Feature]: Case-sensitive attribute updates and CSV Import attribute deletion capability (#4384) by @objecttothis
|
||||
- fix: change docker image tag to master by @jekkos
|
||||
- Update to CodeIgniter 4.7.2 (#4485) by @objecttothis
|
||||
- Bump lodash from 4.17.23 to 4.18.1 (#4462) by @dependabot[bot]
|
||||
- [Fix]: Add missing return statements to Sales Controller functions by @Ollama
|
||||
- Encourage users to star the project by @objecttothis
|
||||
- Bump dompurify from 3.3.2 to 3.4.0 (#4512) by @dependabot[bot]
|
||||
- fix: propagate attribute definition failures in postSaveGeneral() (#4509) by @jekkos
|
||||
- fix: Escape dynamic output and fix CSS property in barcode_sheet.php (#4501) by @jekkos
|
||||
- Fix CRC currency reverting to EUR/LAK in locale config (#4511) by @jekkos
|
||||
- fix: Add missing $img_tag variable in Sales::getSendPdf() (#4515) by @jekkos
|
||||
- fix: Language dropdown not displaying saved language correctly (#4518) by @jekkos
|
||||
- fix: Scope orWhere clauses in Item::exists() and Item::get_item_id() (#4520) by @jekkos
|
||||
- fix: Update calendar translations (#4498) by @jekkos
|
||||
- fix: Catch mysqli_sql_exception in DB fallback handlers for fresh Docker installs (#4525) by @jekkos
|
||||
- fix(home): improve internal data type handling for user identification in auth process by @enricodelarosa
|
||||
- Assignable Keyboard Shortcuts Updates (#4532) by @WShells
|
||||
- chore: miscellaneous updates and improvements (#4530) by @BudsieBuds
|
||||
- chore(deps): bump minimatch from 3.1.2 to 3.1.5 (#4536) by @dependabot[bot]
|
||||
- chore: sync project files to match upstream templates (#4537) by @BudsieBuds
|
||||
- fix(ci): include hidden files in Docker build context (#4543) by @jekkos
|
||||
- feat: add ALLOWED_HOSTNAMES environment variable support for Docker/Compose (#4544) by @jekkos
|
||||
- fix(docker): correct permissions and fix migration barcode_type error (#4546) by @jekkos
|
||||
- docs: Update SECURITY.md with disclosure process (#4549) by @jekkos
|
||||
- feat: Bank transfer and wallet payment option added #4540 (#4547) by @BhojKamal
|
||||
- fix(security): Path traversal vulnerability in getPicThumb (#4545) by @jekkos
|
||||
- fix(security): SQL injection and path traversal vulnerabilities (#4539) by @jekkos
|
||||
- fix: Capture CSV import failures in save_tax_data and save_inventory_quantities (#4507) by @jekkos
|
||||
- fix: validate attributeId > 0 in saveAttributeLink() (#4508) by @jekkos
|
||||
- feat: Add deployment workflow with approval gates (#4522) by @jekkos
|
||||
- Bugfixes to get Migration working on MySQL and MariaDB (#4551) by @objecttothis
|
||||
- Bugfix: Sale search in register not handling trailing space properly (#4557) by @objecttothis
|
||||
- fix: cast string returns to int in MY_Migration (#4560) by @jekkos
|
||||
- Add fallback for allowedHostnames environment variable (#4565) by @objecttothis
|
||||
- fix: Allow searching by Sale ID in Takings/Daily Sales view (#4569) by @jekkos
|
||||
- Add Guards to Database Migration (#4571) by @objecttothis
|
||||
- fix: tax rate inputs blank with comma-decimal locales (#4555) by @jekkos
|
||||
- fix(security): Fix DOMPDF RCE and customer email sanitization (#4568) by @jekkos
|
||||
- Fix overly lenient date validation (#4574) by @objecttothis
|
||||
- fix(security): Escape attribute value in register by @jekkos
|
||||
- chore(deps): bump dompurify from 3.4.0 to 3.4.11 (#4578) by @dependabot[bot]
|
||||
- Bugfix: Fix problems with migration UI in login (#4589) by @objecttothis
|
||||
- Forgotten commit from login migration branch (#4592) by @objecttothis
|
||||
- Feature: Payment reference code (#4587) by @objecttothis
|
||||
- fix(giftcard): correct return type and rename getGiftcardId method (#4600) by @objecttothis
|
||||
- chore(deps): bump dompurify from 3.4.11 to 3.4.12 (#4602) by @dependabot[bot]
|
||||
- bugfix(reports): crash on detailed sales report when sale has multiple payments with reference codes (#4599) by @objecttothis
|
||||
- style(models): normalize quote style in SQL GROUP_CONCAT expression (#4608) by @objecttothis
|
||||
- chore(deps): upgrade dompdf from v2.0.8 to v3.1.6 (#4610) by @objecttothis
|
||||
- chore(deps): bump brace-expansion (#4614) by @dependabot[bot]
|
||||
- chore(deps): add xlsx via SheetJS CDN and upgrade tableexport plugin (#4615) by @objecttothis
|
||||
- chore(deps): bump lodash.template from 4.5.0 to 4.18.1 (#4616) by @objecttothis
|
||||
- fix(login): skip auth validation on new install to allow migration (#4609) by @objecttothis
|
||||
- fix: Wrap postSave() in single transaction for atomicity (#4506) by @jekkos
|
||||
- refactor: Replace var with let/const in JavaScript files (#4503) by @jekkos
|
||||
- fix: get_definition_by_name() returns single row instead of multi-dimensional array (#4452) (#4464) by @Jonathan Chang
|
||||
- fix(config): validate theme param to prevent XSS via invalid theme (#4620) by @objecttothis
|
||||
- fix(sales): enforce server-side authorization for price changes (#4631) by @objecttothis
|
||||
- fix(sales): escape quote number in email template to prevent XSS (#4625) by @objecttothis
|
||||
- refactor(migrations): rename execute_script to executeScript across all migrations (#4611) by @objecttothis
|
||||
- fix(auth): validate gcaptcha before password to prevent bypass (#4618) by @objecttothis
|
||||
- refactor: apply PSR-12 naming to Attribute definition methods (#4624) by @Rayan Abdul Cader
|
||||
- fix(security): sanitize filenames and escape logo path in config (#4630) by @objecttothis
|
||||
- fix: use db_connect() for item save transactions (#4636) by @richardmilles
|
||||
- fix(xss): remove redundant escaping that double-encoded item attribute values (#4628) by @objecttothis
|
||||
- chore(deps): bump codeigniter4/framework from 4.7.2 to 4.7.4 (#4638) by @dependabot[bot]
|
||||
- chore(deps): bump dompurify from 3.4.12 to 3.4.13 (#4639) by @dependabot[bot]
|
||||
- Reject item CSV imports whose header row is missing required columns (#4597) by @Sai Asish Y
|
||||
- fix(items): validate item_number and skip receiving quantity default for temp items (#4621) by @objecttothis
|
||||
- Feature: CodeIgniter Throttler (#4619) by @objecttothis
|
||||
- Bugfix: Resolve Race Condition in Rewards and Gift Card Spending (#4640) by @objecttothis
|
||||
- hotfix(auth): hash throttler keys to improve security (#4646) by @objecttothis
|
||||
- fix(items): add explicit sentinel value for clearing supplier in bulk edit (#4617) by @objecttothis
|
||||
- Codeigniter changes between 4.7.2 and 4.7.4 (#4650) by @objecttothis
|
||||
- Hotfix: Fix CI3 database migration caused by regression (#4649) by @objecttothis
|
||||
- fix(sales): gate per-record endpoints behind reports_sales grant (#4627) by @objecttothis
|
||||
- fix(email): update method call to camelCase for PSR-12 compliance (#4659) by @objecttothis
|
||||
- Feature admin account safeguards (#4657) by @objecttothis
|
||||
- Ensure payload data is escaped to prevent XSS (#4664) by @objecttothis
|
||||
- fix(sales): enforce reports_sales grant on search endpoint (#4673) by @objecttothis
|
||||
- fix(reports, home): resolve double-URL-decoding bypass for method grants (#4660) (#4666) by @objecttothis
|
||||
- Bugfix tax names (#4677) by @objecttothis
|
||||
- feat(validation, tests): add `valid_path_strict` rule and integrate into mailpath validation (#4684) by @objecttothis
|
||||
- fix(sales): harden payment validation and gift card handling by @objecttothis
|
||||
- fix: prevent duplicate items when editing imported rows (#4634) by @richardmilles
|
||||
- fix(barcode): resolve string interpolation issue in barcode display html (#4692) by @Vighnesh Nilajakar
|
||||
- fix(sales): gate getSearch behind reports_sales grant by @jekkos
|
||||
- bugfix(sales): reject non-negative gift-card amount_tendered (#4674) by @jekkos
|
||||
- fix(sales): harden unsuspend with auth, status gating, and null safety by @objecttothis
|
||||
- fix(tests): resolve all phpunit failures — clean-DB suite green (#4626) (#4691) by @jekkos
|
||||
- fix(licenses): guard malformed data, parallelize gulp tasks, require Node 20 by @objecttothis
|
||||
- fix(validation): broaden sendmail path regex, expand i18n, strip advisory IDs by @objecttothis
|
||||
- fix(security): handle special characters in `.env` key values and improve insertion logic (#4656) by @objecttothis
|
||||
- fix(locale): validate language_code against known locales to block path traversal (#4704) by @jekkos
|
||||
- Fix GHSA-frx7-c5vv-m3mr: recompute cashup total server-side and force owner identity (#4706) by @jekkos
|
||||
- feat(security): add THROTTLE_KEY env-var fallback for throttle.key (#4707) by @jekkos
|
||||
- fix(i18n): translate remaining English labels in Swiss German Items.php (#4701) by @Rayan Abdul Cader
|
||||
- fix(ci): stop stamping app version onto master and branch Docker tags (#4709) by @jekkos
|
||||
- chore(deps): bump fflate from 0.8.2 to 0.8.3 (#4690) by @dependabot[bot]
|
||||
- fix(i18n): swap print_delay_autoreturn number/required messages in 5 locales (#4699) by @Rayan Abdul Cader
|
||||
- chore(release): unified git-cliff release workflow (changelog + tag + optional bump) (#4711) by @jekkos
|
||||
- fix(release): push changelog/bump to master via admin PAT (GITHUB_TOKEN blocked by branch protection) by @jekkos
|
||||
- docs: add 3.4.2 changelog by @github-actions[bot]
|
||||
- chore: bump version to 3.4.3 by @github-actions[bot]
|
||||
- fix(security): HTML-escape attribute dropdown option labels in items attributes view (#4715) by @jekkos
|
||||
- fix(release): keep package-lock.json version in sync on bump (#4718) by @jekkos
|
||||
- fix(security): strip all HTML tags from $.notify alert messages (#4716) by @jekkos
|
||||
- chore: strip advisory IDs from code comments and changelog (#4720) by @jekkos
|
||||
- fix(security): report unwritable .env.lock, make throttle limits configurable (#4714) by @jekkos
|
||||
- chore: reset 3.4.2 (undo premature 3.4.3 bump + stale changelog) for re-cut by @jekkos
|
||||
|
||||
## [3.4.1] - 2025-06-05
|
||||
- Feature: PSR-12 Compliant Indentation by @objecttothis in ([#4196](https://github.com/opensourcepos/opensourcepos/pull/4196))
|
||||
- Add .env to dist zip by @jekkos in ([#4199](https://github.com/opensourcepos/opensourcepos/pull/4199))
|
||||
|
||||
+11
-4
@@ -10,7 +10,7 @@
|
||||
|
||||
### 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.**
|
||||
⚠️ **CRITICAL**: OpenSourcePOS validates the Host header to prevent Host Header Injection attacks. **You MUST configure `app.allowedHostnames` for production deployments. If not configured, the application will fail to start.**
|
||||
|
||||
**Add to your `.env` file:**
|
||||
|
||||
@@ -34,9 +34,8 @@ 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)
|
||||
- Prevents Host Header Injection attacks
|
||||
- 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
|
||||
@@ -72,7 +71,15 @@ Docker runs natively on Mac and Linux. Windows requires WSL2 to be installed. Pl
|
||||
|
||||
**Be aware that this setup is not suited for production usage! Change the default passwords in the compose file before exposing the containers publicly.**
|
||||
|
||||
Start the containers using the following command
|
||||
First create a **regular `.env` file** in the project root (a missing one is not
|
||||
auto-created as a file — see the compose `create_host_path: false` setting). Copy
|
||||
the shipped example:
|
||||
|
||||
```
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Then start the containers:
|
||||
|
||||
```
|
||||
docker-compose up
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace App\Commands;
|
||||
|
||||
use App\Libraries\CI3SecretConverter;
|
||||
use CodeIgniter\CLI\BaseCommand;
|
||||
use CodeIgniter\CLI\CLI;
|
||||
use Exception;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Idempotent startup provisioning of the encryption + throttle keys.
|
||||
*
|
||||
* In bare-metal deployments the web runtime can auto-provision keys inline
|
||||
* (see checkEncryption()/checkThrottleEncryption(), gated by .env
|
||||
* writability). In Docker/Compose deployments .env is typically mounted
|
||||
* read-only, so keys are minted here at container start via
|
||||
* `php spark env:provision`. For legacy CI3 deployments this command
|
||||
* also re-encrypts stored CI3 secrets to the CI4 cipher (shared
|
||||
* implementation with the interactive ConvertToCI4 migration).
|
||||
*/
|
||||
class EnvProvision extends BaseCommand
|
||||
{
|
||||
/**
|
||||
* The command's group.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $group = 'Environment';
|
||||
|
||||
protected $name = 'env:provision';
|
||||
|
||||
protected $usage = 'env:provision';
|
||||
|
||||
protected $description = 'Ensures the encryption and throttle keys are provisioned and converts any legacy CI3-encrypted secrets.';
|
||||
|
||||
public function run(array $params): void
|
||||
{
|
||||
helper('security');
|
||||
|
||||
$throttleKey = provisionThrottleKey();
|
||||
CLI::write('throttle.key : ' . ($throttleKey !== '' ? 'present' : 'MISSING'), 'green');
|
||||
|
||||
$encryptionConfig = config('Encryption');
|
||||
$key = (string) ($encryptionConfig->key ?? '');
|
||||
|
||||
if ($key !== '' && strlen($key) >= 64) {
|
||||
CLI::write('encryption.key : CI4 key already present', 'green');
|
||||
CLI::newLine();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$converter = new CI3SecretConverter();
|
||||
|
||||
if ($key !== '' && strlen($key) < 64) {
|
||||
// DB read, safe outside the .env lock.
|
||||
$plain = $converter->decryptAll($key);
|
||||
$hasData = $this->anyNonEmpty($plain);
|
||||
|
||||
// Backup -> rotate -> re-encrypt -> verify -> persist under a single .env lock.
|
||||
rotateEncryptionKeyTransaction($key, static function () use ($plain, $hasData, $converter): void {
|
||||
$encrypted = $converter->encryptAll($plain);
|
||||
|
||||
if (array_diff_assoc($plain, $converter->verifyAll($encrypted)) !== []) {
|
||||
throw new RuntimeException('Failed to verify converted encryption data.');
|
||||
}
|
||||
|
||||
if ($hasData) {
|
||||
$converter->saveAll($encrypted);
|
||||
}
|
||||
});
|
||||
|
||||
CLI::write('encryption.key : rotated CI3 -> CI4 key', 'green');
|
||||
if ($hasData) {
|
||||
CLI::write('legacy secrets : converted and verified to CI4 cipher', 'green');
|
||||
}
|
||||
} else {
|
||||
// Fresh key (no old key to decrypt): a single atomic write suffices;
|
||||
// the transaction wrapper still serialises it against other workers.
|
||||
rotateEncryptionKeyTransaction(null, static function (): void {});
|
||||
|
||||
CLI::write('encryption.key : new CI4 key generated', 'green');
|
||||
|
||||
if ($this->legacySecretsPresent()) {
|
||||
CLI::write('legacy secrets : WARNING - stored CI3 secrets found but no CI3 key to decrypt them; they could not be recovered', 'yellow');
|
||||
}
|
||||
}
|
||||
|
||||
CLI::newLine();
|
||||
CLI::write('env:provision complete.', 'green');
|
||||
CLI::newLine();
|
||||
}
|
||||
|
||||
private function anyNonEmpty(array $plain): bool
|
||||
{
|
||||
foreach ($plain as $value) {
|
||||
if ((string) $value !== '') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function legacySecretsPresent(): bool
|
||||
{
|
||||
try {
|
||||
$appConfig = model('Appconfig');
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (CI3SecretConverter::LEGACY_KEYS as $col) {
|
||||
try {
|
||||
if ($appConfig->get_value($col) !== '') {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -12,7 +12,7 @@ class App extends BaseConfig
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $application_version = '3.4.2';
|
||||
public string $application_version = '3.4.3';
|
||||
|
||||
/**
|
||||
* This is the commit hash for the version you are currently using.
|
||||
@@ -307,7 +307,7 @@ class App extends BaseConfig
|
||||
/**
|
||||
* Validates and returns a trusted hostname.
|
||||
*
|
||||
* Security: Prevents Host Header Injection attacks (GHSA-jchf-7hr6-h4f3)
|
||||
* Security: Prevents Host Header Injection attacks
|
||||
* by validating the HTTP_HOST against a whitelist of allowed hostnames.
|
||||
*
|
||||
* In production: Fails fast if allowedHostnames is not configured.
|
||||
|
||||
@@ -112,8 +112,47 @@ class Encryption extends BaseConfig
|
||||
parent::__construct();
|
||||
|
||||
if ($this->key === '') {
|
||||
$envKey = getenv('ENCRYPTION_KEY');
|
||||
$this->key = $envKey === false ? '' : $envKey;
|
||||
// Fallback sources (notably the ENCRYPTION_KEY Docker var, which the
|
||||
// parent never reads) were not decode-parsed, so run them through
|
||||
// the same parser to keep hex2bin:/base64: keys consistent.
|
||||
$this->key = self::parseKey(self::resolveKey(
|
||||
(string) ($_SERVER['encryption.key'] ?? ''),
|
||||
(string) ($_ENV['encryption.key'] ?? ''),
|
||||
(string) getenv('encryption.key'),
|
||||
(string) getenv('ENCRYPTION_KEY'),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a key's `hex2bin:`/`base64:` prefix, mirroring
|
||||
* BaseConfig::parseEncryptionKey(); kept static so it is unit-testable.
|
||||
*/
|
||||
public static function parseKey(string $key): string
|
||||
{
|
||||
if (str_starts_with($key, 'hex2bin:')) {
|
||||
return (string) hex2bin(substr($key, 8));
|
||||
}
|
||||
|
||||
if (str_starts_with($key, 'base64:')) {
|
||||
return (string) base64_decode(substr($key, 7), true);
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the first non-empty source (highest precedence first). Cascading
|
||||
* past empty strings (vs `??`) avoids a blank value shadowing the real key.
|
||||
*/
|
||||
public static function resolveKey(string ...$sources): string
|
||||
{
|
||||
foreach ($sources as $source) {
|
||||
if ($source !== '') {
|
||||
return $source;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -28,9 +28,6 @@ use App\Events\Method;
|
||||
|
||||
Events::on('pre_system', static function (): void {
|
||||
if (ENVIRONMENT !== 'testing') {
|
||||
helper('security');
|
||||
checkThrottleEncryption();
|
||||
|
||||
$value = ini_get('zlib.output_compression');
|
||||
if (filter_var($value, FILTER_VALIDATE_BOOLEAN) || (int) $value > 0) {
|
||||
throw FrameworkException::forEnabledZlibOutputCompression();
|
||||
|
||||
@@ -77,9 +77,9 @@ class Filters extends BaseFilters
|
||||
public array $globals = [
|
||||
'before' => [
|
||||
'honeypot',
|
||||
'isLoggedIn' => ['except' => 'login|migrate'],
|
||||
'csrf' => ['except' => 'login|migrate'],
|
||||
'invalidchars',
|
||||
'isLoggedIn' => ['except' => 'login|migrate'],
|
||||
],
|
||||
'after' => [
|
||||
'toolbar',
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Config;
|
||||
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
|
||||
/**
|
||||
* Resolves the on-disk locations the security_helper functions read/write for
|
||||
* the runtime-generated .env secrets (encryption.key + throttle.key).
|
||||
*
|
||||
* Centralising these paths in a config object (mirroring Config\Encryption)
|
||||
* lets the helper and the test suite agree on the same source of truth, and
|
||||
* lets tests redirect all three to a per-run sandbox by mutating the shared
|
||||
* instance — without changing any helper signature.
|
||||
*
|
||||
* The defaults are the production locations; tests override them in setUp().
|
||||
*/
|
||||
class SecurityEnv extends BaseConfig
|
||||
{
|
||||
/**
|
||||
* Path to the .env file that holds the two runtime-generated secrets.
|
||||
*/
|
||||
public string $envPath = ROOTPATH . '.env';
|
||||
|
||||
/**
|
||||
* Backup location used around encryption-key rotation so a failing
|
||||
* conversion can be rolled back from the previous .env contents.
|
||||
*/
|
||||
public string $backupPath = WRITEPATH . '/backup/.env.bak';
|
||||
|
||||
/**
|
||||
* Dedicated mutex file for coordinating concurrent .env writes. Kept
|
||||
* separate from .env itself because on Windows a file cannot be
|
||||
* renamed/deleted while a handle to it is open.
|
||||
*/
|
||||
public string $lockPath = ROOTPATH . '.env.lock';
|
||||
}
|
||||
@@ -254,10 +254,10 @@ class OSPOSRules
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the candidate is a plain filesystem path: only letters, digits,
|
||||
* underscore, dash, dot and forward slash. Uses \A...\z (not ^...$) because PCRE's $
|
||||
* also matches immediately before a single trailing newline, which would let a
|
||||
* value like "/usr/bin/php\n" slip through — the bug behind GHSA-jc56-j8m6-q627.
|
||||
* Validates a plain filesystem path, allowing space/colon/backslash for Windows paths and
|
||||
* trailing sendmail-style args. Excludes shell metacharacters since this value is concatenated
|
||||
* unescaped into a popen() call. Uses \A...\z, not ^...$, since $ also matches before a
|
||||
* trailing newline.
|
||||
*
|
||||
* @param string $candidate
|
||||
* @param string|null $error
|
||||
@@ -270,6 +270,6 @@ class OSPOSRules
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool) preg_match('/\A[a-zA-Z0-9_\-\/.]+\z/', $candidate);
|
||||
return (bool) preg_match('/\A[a-zA-Z0-9_\-\/.: \\\\]+\z/', $candidate);
|
||||
}
|
||||
}
|
||||
@@ -257,26 +257,26 @@ class Attributes extends Secure_Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $definition_id
|
||||
* @param int $definitionId
|
||||
* @return string
|
||||
*/
|
||||
public function getView(int $definition_id = NO_DEFINITION_ID): string
|
||||
public function getView(int $definitionId = NO_DEFINITION_ID): string
|
||||
{
|
||||
$info = $this->attribute->getAttributeInfo($definition_id);
|
||||
$info = $this->attribute->getAttributeInfo($definitionId);
|
||||
foreach (get_object_vars($info) as $property => $value) {
|
||||
$info->$property = $value;
|
||||
}
|
||||
|
||||
$data['definition_id'] = $definition_id;
|
||||
$data['definition_values'] = $this->attribute->getDefinitionValues($definition_id);
|
||||
$data['definition_group'] = $this->attribute->getDefinitionsByType(GROUP, $definition_id);
|
||||
$data['definition_id'] = $definitionId;
|
||||
$data['definition_values'] = $this->attribute->getDefinitionValues($definitionId);
|
||||
$data['definition_group'] = $this->attribute->getDefinitionsByType(GROUP, $definitionId);
|
||||
$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;
|
||||
$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);
|
||||
$showAll = Attribute::SHOW_IN_ITEMS | Attribute::SHOW_IN_RECEIVINGS | Attribute::SHOW_IN_SALES | Attribute::SHOW_IN_SEARCH;
|
||||
$data['definition_flags'] = $this->get_attributes($showAll);
|
||||
$selectedFlags = $info->definition_flags === '' ? $showAll : $info->definition_flags;
|
||||
$data['selected_definition_flags'] = $this->get_attributes($selectedFlags);
|
||||
|
||||
return view('attributes/form', $data);
|
||||
}
|
||||
|
||||
@@ -211,20 +211,29 @@ class Cashups extends Secure_Controller
|
||||
$close_date = $this->request->getPost('close_date');
|
||||
$close_date_formatter = date_create_from_format($this->config['dateformat'] . ' ' . $this->config['timeformat'], $close_date);
|
||||
|
||||
$open_amount_cash = parse_decimals($this->request->getPost('open_amount_cash'));
|
||||
$transfer_amount_cash = parse_decimals($this->request->getPost('transfer_amount_cash'));
|
||||
$closed_amount_cash = parse_decimals($this->request->getPost('closed_amount_cash'));
|
||||
$closed_amount_due = parse_decimals($this->request->getPost('closed_amount_due'));
|
||||
$closed_amount_card = parse_decimals($this->request->getPost('closed_amount_card'));
|
||||
$closed_amount_check = parse_decimals($this->request->getPost('closed_amount_check'));
|
||||
|
||||
$logged_in_employee = $this->employee->get_logged_in_employee_info();
|
||||
|
||||
$cash_up_data = [
|
||||
'open_date' => $open_date_formatter->format('Y-m-d H:i:s'),
|
||||
'close_date' => $close_date_formatter->format('Y-m-d H:i:s'),
|
||||
'open_amount_cash' => parse_decimals($this->request->getPost('open_amount_cash')),
|
||||
'transfer_amount_cash' => parse_decimals($this->request->getPost('transfer_amount_cash')),
|
||||
'closed_amount_cash' => parse_decimals($this->request->getPost('closed_amount_cash')),
|
||||
'closed_amount_due' => parse_decimals($this->request->getPost('closed_amount_due')),
|
||||
'closed_amount_card' => parse_decimals($this->request->getPost('closed_amount_card')),
|
||||
'closed_amount_check' => parse_decimals($this->request->getPost('closed_amount_check')),
|
||||
'closed_amount_total' => parse_decimals($this->request->getPost('closed_amount_total')),
|
||||
'open_amount_cash' => $open_amount_cash,
|
||||
'transfer_amount_cash' => $transfer_amount_cash,
|
||||
'closed_amount_cash' => $closed_amount_cash,
|
||||
'closed_amount_due' => $closed_amount_due,
|
||||
'closed_amount_card' => $closed_amount_card,
|
||||
'closed_amount_check' => $closed_amount_check,
|
||||
'closed_amount_total' => $this->_calculate_total($open_amount_cash, $transfer_amount_cash, $closed_amount_due, $closed_amount_cash, $closed_amount_card, $closed_amount_check),
|
||||
'note' => $this->request->getPost('note') != null,
|
||||
'description' => $this->request->getPost('description', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
|
||||
'open_employee_id' => $this->request->getPost('open_employee_id', FILTER_SANITIZE_NUMBER_INT),
|
||||
'close_employee_id' => $this->request->getPost('close_employee_id', FILTER_SANITIZE_NUMBER_INT),
|
||||
'open_employee_id' => $logged_in_employee->person_id,
|
||||
'close_employee_id' => $logged_in_employee->person_id,
|
||||
'deleted' => $this->request->getPost('deleted') != null
|
||||
];
|
||||
|
||||
|
||||
+66
-37
@@ -158,14 +158,20 @@ class Config extends Secure_Controller
|
||||
$file = file_get_contents('license/npm-prod.LICENSES');
|
||||
$array = json_decode($file, true);
|
||||
|
||||
foreach ($array as $dependency) {
|
||||
$license[$i]['text'] .= "library: {$dependency['name']}\n";
|
||||
$license[$i]['text'] .= "authors: {$dependency['author']}\n";
|
||||
$license[$i]['text'] .= "website: {$dependency['homepage']}\n";
|
||||
$license[$i]['text'] .= "version: {$dependency['installedVersion']}\n";
|
||||
$license[$i]['text'] .= "license: {$dependency['licenseType']}\n";
|
||||
if (is_array($array)) {
|
||||
foreach ($array as $dependency) {
|
||||
if (!is_array($dependency) || count(array_intersect(['name', 'author', 'homepage', 'installedVersion', 'licenseType'], array_keys($dependency))) !== 5) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$license[$i]['text'] .= "\n";
|
||||
$license[$i]['text'] .= "library: {$dependency['name']}\n";
|
||||
$license[$i]['text'] .= "authors: {$dependency['author']}\n";
|
||||
$license[$i]['text'] .= "website: {$dependency['homepage']}\n";
|
||||
$license[$i]['text'] .= "version: {$dependency['installedVersion']}\n";
|
||||
$license[$i]['text'] .= "license: {$dependency['licenseType']}\n";
|
||||
|
||||
$license[$i]['text'] .= "\n";
|
||||
}
|
||||
}
|
||||
$license[$i]['text'] = rtrim($license[$i]['text'], "\n");
|
||||
}
|
||||
@@ -178,14 +184,20 @@ class Config extends Secure_Controller
|
||||
$file = file_get_contents('license/npm-dev.LICENSES');
|
||||
$array = json_decode($file, true);
|
||||
|
||||
foreach ($array as $dependency) {
|
||||
$license[$i]['text'] .= "library: {$dependency['name']}\n";
|
||||
$license[$i]['text'] .= "authors: {$dependency['author']}\n";
|
||||
$license[$i]['text'] .= "website: {$dependency['homepage']}\n";
|
||||
$license[$i]['text'] .= "version: {$dependency['installedVersion']}\n";
|
||||
$license[$i]['text'] .= "license: {$dependency['licenseType']}\n";
|
||||
if (is_array($array)) {
|
||||
foreach ($array as $dependency) {
|
||||
if (!is_array($dependency) || count(array_intersect(['name', 'author', 'homepage', 'installedVersion', 'licenseType'], array_keys($dependency))) !== 5) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$license[$i]['text'] .= "\n";
|
||||
$license[$i]['text'] .= "library: {$dependency['name']}\n";
|
||||
$license[$i]['text'] .= "authors: {$dependency['author']}\n";
|
||||
$license[$i]['text'] .= "website: {$dependency['homepage']}\n";
|
||||
$license[$i]['text'] .= "version: {$dependency['installedVersion']}\n";
|
||||
$license[$i]['text'] .= "license: {$dependency['licenseType']}\n";
|
||||
|
||||
$license[$i]['text'] .= "\n";
|
||||
}
|
||||
}
|
||||
$license[$i]['text'] = rtrim($license[$i]['text'], "\n");
|
||||
}
|
||||
@@ -254,7 +266,9 @@ class Config extends Secure_Controller
|
||||
$data['selected_image_allowed_types'] = explode(',', $this->config['image_allowed_types']);
|
||||
|
||||
// Integrations Related fields
|
||||
$data['mailchimp'] = [];
|
||||
$data['mailchimp'] = [];
|
||||
$data['smtp_pass_set'] = !empty($this->config['smtp_pass']);
|
||||
$data['msg_pwd_set'] = !empty($this->config['msg_pwd']);
|
||||
|
||||
if (checkEncryption()) { // TODO: Hungarian notation
|
||||
if (!isset($this->encrypter)) {
|
||||
@@ -262,9 +276,8 @@ class Config extends Secure_Controller
|
||||
$this->encrypter = Services::encrypter();
|
||||
}
|
||||
|
||||
$data['mailchimp']['api_key'] = (isset($this->config['mailchimp_api_key']) && !empty($this->config['mailchimp_api_key']))
|
||||
? $this->encrypter->decrypt($this->config['mailchimp_api_key'])
|
||||
: '';
|
||||
$data['mailchimp']['api_key'] = '';
|
||||
$data['mailchimp']['api_key_set'] = !empty($this->config['mailchimp_api_key']);
|
||||
|
||||
$data['mailchimp']['list_id'] = (isset($this->config['mailchimp_list_id']) && !empty($this->config['mailchimp_list_id']))
|
||||
? $this->encrypter->decrypt($this->config['mailchimp_list_id'])
|
||||
@@ -273,8 +286,9 @@ class Config extends Secure_Controller
|
||||
// Remove any backup of .env created by check_encryption()
|
||||
removeBackup();
|
||||
} else {
|
||||
$data['mailchimp']['api_key'] = '';
|
||||
$data['mailchimp']['list_id'] = '';
|
||||
$data['mailchimp']['api_key'] = '';
|
||||
$data['mailchimp']['api_key_set'] = false;
|
||||
$data['mailchimp']['list_id'] = '';
|
||||
}
|
||||
|
||||
$data['mailchimp']['lists'] = $this->_mailchimp();
|
||||
@@ -492,7 +506,12 @@ class Config extends Secure_Controller
|
||||
return $this->response->setJSON(['success' => false, 'message' => reset($errors)]);
|
||||
}
|
||||
|
||||
$exploded = explode(":", $this->request->getPost('language'));
|
||||
$language = $this->request->getPost('language');
|
||||
if (!in_array($language, array_keys(get_languages()), true)) {
|
||||
return $this->response->setJSON(['success' => false, 'message' => 'Invalid language']);
|
||||
}
|
||||
|
||||
$exploded = explode(":", $language);
|
||||
$currency_symbol = $this->request->getPost('currency_symbol');
|
||||
$batch_save_data = [
|
||||
'currency_symbol' => htmlspecialchars($currency_symbol ?? ''),
|
||||
@@ -531,10 +550,12 @@ class Config extends Secure_Controller
|
||||
*/
|
||||
public function postSaveEmail(): ResponseInterface
|
||||
{
|
||||
$password = '';
|
||||
$postedPass = (string) $this->request->getPost('smtp_pass');
|
||||
|
||||
if (checkEncryption() && !empty($this->request->getPost('smtp_pass'))) {
|
||||
$password = $this->encrypter->encrypt($this->request->getPost('smtp_pass'));
|
||||
if (checkEncryption() && $postedPass !== '') {
|
||||
$password = $this->encrypter->encrypt($postedPass);
|
||||
} else {
|
||||
$password = (string) ($this->config['smtp_pass'] ?? '');
|
||||
}
|
||||
|
||||
$protocol = $this->request->getPost('protocol');
|
||||
@@ -582,10 +603,12 @@ class Config extends Secure_Controller
|
||||
*/
|
||||
public function postSaveMessage(): ResponseInterface
|
||||
{
|
||||
$password = '';
|
||||
$postedPwd = (string) $this->request->getPost('msg_pwd');
|
||||
|
||||
if (checkEncryption() && !empty($this->request->getPost('msg_pwd'))) {
|
||||
$password = $this->encrypter->encrypt($this->request->getPost('msg_pwd'));
|
||||
if (checkEncryption() && $postedPwd !== '') {
|
||||
$password = $this->encrypter->encrypt($postedPwd);
|
||||
} else {
|
||||
$password = (string) ($this->config['msg_pwd'] ?? '');
|
||||
}
|
||||
|
||||
$batch_save_data = [
|
||||
@@ -648,19 +671,25 @@ class Config extends Secure_Controller
|
||||
*/
|
||||
public function postSaveMailchimp(): ResponseInterface
|
||||
{
|
||||
$api_key = '';
|
||||
$list_id = '';
|
||||
$postedKey = (string) $this->request->getPost('mailchimp_api_key');
|
||||
$postedList = (string) $this->request->getPost('mailchimp_list_id');
|
||||
|
||||
if (checkEncryption()) {
|
||||
$api_key_unencrypted = $this->request->getPost('mailchimp_api_key');
|
||||
if (!empty($api_key_unencrypted)) {
|
||||
$api_key = $this->encrypter->encrypt($api_key_unencrypted);
|
||||
}
|
||||
$api_key = $postedKey !== ''
|
||||
? $this->encrypter->encrypt($postedKey)
|
||||
: (string) ($this->config['mailchimp_api_key'] ?? '');
|
||||
|
||||
$list_id_unencrypted = $this->request->getPost('mailchimp_list_id');
|
||||
if (!empty($list_id_unencrypted)) {
|
||||
$list_id = $this->encrypter->encrypt($list_id_unencrypted);
|
||||
}
|
||||
$list_id = $postedList !== ''
|
||||
? $this->encrypter->encrypt($postedList)
|
||||
: (string) ($this->config['mailchimp_list_id'] ?? '');
|
||||
} else {
|
||||
$api_key = $postedKey !== ''
|
||||
? (string) $postedKey
|
||||
: (string) ($this->config['mailchimp_api_key'] ?? '');
|
||||
|
||||
$list_id = $postedList !== ''
|
||||
? (string) $postedList
|
||||
: (string) ($this->config['mailchimp_list_id'] ?? '');
|
||||
}
|
||||
|
||||
$batch_save_data = ['mailchimp_api_key' => $api_key, 'mailchimp_list_id' => $list_id];
|
||||
|
||||
+77
-17
@@ -76,7 +76,7 @@ class Items extends Secure_Controller
|
||||
{
|
||||
$this->session->set('allow_temp_items', 0);
|
||||
|
||||
$data['table_headers'] = get_items_manage_table_headers();
|
||||
$data['table_headers'] = getItemsManageTableHeaders();
|
||||
|
||||
// Restore stock_location from URL or session
|
||||
$stockLocation = $this->request->getGet('stock_location', FILTER_SANITIZE_NUMBER_INT);
|
||||
@@ -111,12 +111,14 @@ class Items extends Secure_Controller
|
||||
$search = $this->request->getGet('search', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
$limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT);
|
||||
$offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT);
|
||||
$sort = $this->sanitizeSortColumn(item_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'item_id');
|
||||
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
|
||||
$this->item_lib->set_item_location($this->request->getGet('stock_location'));
|
||||
|
||||
$definitionNames = $this->attribute->getDefinitionsByFlags(Attribute::SHOW_IN_ITEMS);
|
||||
$definitionNamesWithTypes = $this->attribute->getDefinitionsByFlags(Attribute::SHOW_IN_ITEMS, true);
|
||||
$definitionIds = array_keys($definitionNamesWithTypes);
|
||||
|
||||
$sort = $this->sanitizeSortColumn(itemSortColumns($definitionIds), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'items.item_id');
|
||||
|
||||
$filters = [
|
||||
'start_date' => $this->request->getGet('start_date'),
|
||||
@@ -129,25 +131,79 @@ class Items extends Secure_Controller
|
||||
'search_custom' => false,
|
||||
'is_deleted' => false,
|
||||
'temporary' => false,
|
||||
'definition_ids' => array_keys($definitionNames)
|
||||
'definition_ids' => $definitionIds
|
||||
];
|
||||
|
||||
// Check if any filter is set in the multiselect dropdown
|
||||
$request_filters = array_fill_keys($this->request->getGet('filters', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?? [], true);
|
||||
$filters = array_merge($filters, $request_filters);
|
||||
$items = $this->item->search($search, $filters, $limit, $offset, $sort, $order);
|
||||
$total_rows = $this->item->get_found_rows($search, $filters);
|
||||
$data_rows = [];
|
||||
$requestFilters = array_fill_keys($this->request->getGet('filters', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?? [], true);
|
||||
$filters = array_merge($filters, $requestFilters);
|
||||
|
||||
foreach ($items->getResult() as $item) {
|
||||
$data_rows[] = get_item_data_row($item);
|
||||
// When search_custom is enabled, include attributes that are searchable but may not be visible in table
|
||||
if (!empty($filters['search_custom'])) {
|
||||
$searchableDefinitions = $this->attribute->getDefinitionsByFlags(Attribute::SHOW_IN_ITEMS | Attribute::SHOW_IN_SEARCH);
|
||||
$filters['definition_ids'] = array_keys($searchableDefinitions);
|
||||
}
|
||||
|
||||
$items = $this->item->search($search, $filters, $limit, $offset, $sort, $order);
|
||||
$itemResults = $items->getResult();
|
||||
$totalRows = $this->item->get_found_rows($search, $filters);
|
||||
$taxPercentsByItemId = $this->buildTaxPercentsByItem($itemResults);
|
||||
|
||||
$dataRows = [];
|
||||
|
||||
foreach ($itemResults as $item) {
|
||||
$dataRows[] = getItemDataRow($item, $definitionNamesWithTypes, $taxPercentsByItemId);
|
||||
|
||||
if ($item->pic_filename !== null) {
|
||||
$this->update_pic_filename($item);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->response->setJSON(['total' => $total_rows, 'rows' => $data_rows]);
|
||||
return $this->response->setJSON(['total' => $totalRows, 'rows' => $dataRows]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array item_id => formatted tax percents string
|
||||
*/
|
||||
private function buildTaxPercentsByItem(array $items): array
|
||||
{
|
||||
$config = config(OSPOS::class)->settings;
|
||||
$taxPercentsByItemId = [];
|
||||
|
||||
if ($config['use_destination_based_tax']) {
|
||||
$taxCategoryIds = array_unique(array_filter(array_map(fn ($item) => $item->tax_category_id, $items)));
|
||||
|
||||
if (empty($taxCategoryIds)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$taxCategoryNames = [];
|
||||
foreach ($this->tax_category->get_multiple_info($taxCategoryIds)->getResult() as $taxCategoryInfo) {
|
||||
$taxCategoryNames[$taxCategoryInfo->tax_category_id] = $taxCategoryInfo->tax_category;
|
||||
}
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ($item->tax_category_id !== null && isset($taxCategoryNames[$item->tax_category_id])) {
|
||||
$taxPercentsByItemId[$item->item_id] = $taxCategoryNames[$item->tax_category_id];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$itemIds = array_map(fn ($item) => $item->item_id, $items);
|
||||
$itemTaxesByItemId = $this->item_taxes->getInfoMultiple($itemIds);
|
||||
|
||||
foreach ($itemIds as $itemId) {
|
||||
$taxPercents = '';
|
||||
foreach ($itemTaxesByItemId[$itemId] ?? [] as $taxInfo) {
|
||||
$taxPercents .= to_tax_decimals($taxInfo['percent']) . '%, ';
|
||||
}
|
||||
|
||||
// Remove ', ' from last item
|
||||
$taxPercents = substr($taxPercents, 0, -2);
|
||||
$taxPercentsByItemId[$itemId] = !$taxPercents ? '-' : $taxPercents;
|
||||
}
|
||||
}
|
||||
|
||||
return $taxPercentsByItemId;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -276,17 +332,21 @@ class Items extends Secure_Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $item_ids
|
||||
* @param string $itemIds
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getRow(string $item_ids): ResponseInterface // TODO: An array would be better for parameter.
|
||||
public function getRow(string $itemIds): ResponseInterface // TODO: An array would be better for parameter.
|
||||
{
|
||||
$item_infos = $this->item->get_multiple_info(explode(':', $item_ids), $this->item_lib->get_item_location());
|
||||
$itemInfos = $this->item->get_multiple_info(explode(':', $itemIds), $this->item_lib->get_item_location());
|
||||
$itemResults = $itemInfos->getResult();
|
||||
|
||||
$definitionNames = $this->attribute->getDefinitionsByFlags(Attribute::SHOW_IN_ITEMS, true);
|
||||
$taxPercentsByItemId = $this->buildTaxPercentsByItem($itemResults);
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($item_infos->getResult() as $item_info) {
|
||||
$result[$item_info->item_id] = get_item_data_row($item_info);
|
||||
foreach ($itemResults as $itemInfo) {
|
||||
$result[$itemInfo->item_id] = getItemDataRow($itemInfo, $definitionNames, $taxPercentsByItemId);
|
||||
}
|
||||
|
||||
return $this->response->setJSON($result);
|
||||
|
||||
@@ -75,7 +75,17 @@ class Secure_Controller extends BaseController
|
||||
|
||||
public function sanitizeSortColumn($headers, $field, $default): string
|
||||
{
|
||||
return $field != null && in_array($field, array_keys(array_merge(...$headers))) ? $field : $default;
|
||||
if ($field === null) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
// Flatten keys directly as array_merge() renumbers numeric string keys
|
||||
$validColumns = [];
|
||||
foreach ($headers as $header) {
|
||||
$validColumns[] = (string) array_key_first($header);
|
||||
}
|
||||
|
||||
return in_array((string) $field, $validColumns, true) ? $field : $default;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,14 +2,12 @@
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use App\Models\Appconfig;
|
||||
use App\Libraries\CI3SecretConverter;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Database\Forge;
|
||||
use CodeIgniter\Database\Migration;
|
||||
use CodeIgniter\HTTP\Exceptions\RedirectException;
|
||||
use Config\Encryption;
|
||||
use Config\Services;
|
||||
use ReflectionException;
|
||||
use RuntimeException;
|
||||
|
||||
class ConvertToCI4 extends Migration
|
||||
{
|
||||
@@ -33,18 +31,34 @@ class ConvertToCI4 extends Migration
|
||||
throw new DatabaseException('Migration script 3.4.0_CI4Conversion.sql failed. Check logs for details.');
|
||||
}
|
||||
|
||||
$existingKey = config('Encryption')->key;
|
||||
$existingKey = (string) config('Encryption')->key;
|
||||
|
||||
if (!empty($existingKey) && strlen($existingKey) < 64) {
|
||||
$this->convertCI3EncryptedData();
|
||||
} else {
|
||||
if (!checkEncryption()) {
|
||||
abortEncryptionConversion();
|
||||
throw new DatabaseException('Failed to persist encryption key. Check logs for details.');
|
||||
}
|
||||
// A valid CI4 key requires no write — just confirm it is usable.
|
||||
if ($existingKey !== '' && strlen($existingKey) >= 64) {
|
||||
checkEncryption();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
removeBackup();
|
||||
// Every branch below writes to .env. If the runtime user cannot write
|
||||
// it (e.g. Docker/Compose with a read-only .env mount), fail with an
|
||||
// actionable message instead of a raw fopen() error deep in the writer.
|
||||
if (!envFileIsWritable()) {
|
||||
log_message('critical', 'Encryption key not provisioned and .env is not writable. Run `php spark env:provision` to generate one.');
|
||||
|
||||
throw new RuntimeException(lang('Error.encryption_key_not_provisioned'));
|
||||
}
|
||||
|
||||
if ($existingKey !== '' && strlen($existingKey) < 64) {
|
||||
// Old CI3-era key: decrypt, rotate, re-encrypt, persist — all under
|
||||
// a single .env lock (see convertCI3EncryptedData).
|
||||
$this->convertCI3EncryptedData($existingKey);
|
||||
} else {
|
||||
// No key at all: provision a fresh one (single atomic write), then
|
||||
// drop the incidental pre-write backup left behind by the rotation.
|
||||
rotateEncryptionKey(null);
|
||||
removeBackup();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,95 +67,35 @@ class ConvertToCI4 extends Migration
|
||||
public function down(): void {}
|
||||
|
||||
/**
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
private function convertCI3EncryptedData(): void
|
||||
{
|
||||
$appConfig = model(Appconfig::class);
|
||||
|
||||
$ci3EncryptedData = [
|
||||
'clcdesq_api_key' => '',
|
||||
'clcdesq_api_url' => '',
|
||||
'mailchimp_api_key' => '',
|
||||
'mailchimp_list_id' => '',
|
||||
'smtp_pass' => ''
|
||||
];
|
||||
|
||||
foreach ($ci3EncryptedData as $key => $value) {
|
||||
$ci3EncryptedData[$key] = $appConfig->get_value($key);
|
||||
}
|
||||
|
||||
$decryptedData = $this->decryptCI3Data($ci3EncryptedData);
|
||||
|
||||
if (!checkEncryption()) {
|
||||
abortEncryptionConversion();
|
||||
throw new DatabaseException('Failed to persist encryption key. Check logs for details.');
|
||||
}
|
||||
|
||||
$ci4EncryptedData = $this->encryptData($decryptedData);
|
||||
|
||||
$success = empty(array_diff_assoc($decryptedData, $this->decryptData($ci4EncryptedData)));
|
||||
if (!$success) {
|
||||
abortEncryptionConversion();
|
||||
throw new RedirectException('login'); // TODO: Need to figure out how to pass the error to the Login controller so that it gets displayed.
|
||||
}
|
||||
|
||||
if (!$appConfig->batch_save($ci4EncryptedData)) {
|
||||
abortEncryptionConversion();
|
||||
throw new DatabaseException('Failed to save converted encryption data. Check logs for details.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts CI3 encrypted data and returns the plaintext values.
|
||||
* Decrypts legacy CI3-encrypted secrets with the old key, rotates to a
|
||||
* fresh CI4 key, re-encrypts under the new key, verifies the round trip,
|
||||
* and persists the result.
|
||||
*
|
||||
* @param array $encryptedData Data encrypted using CI3 methodology.
|
||||
* @return array Plaintext, unencrypted data.
|
||||
*/
|
||||
private function decryptCI3Data(array $encryptedData): array
|
||||
{
|
||||
$config = new Encryption();
|
||||
$config->driver = 'OpenSSL';
|
||||
$config->key = config('Encryption')->key;
|
||||
$config->cipher = 'AES-128-CBC';
|
||||
$config->rawData = false;
|
||||
$config->encryptKeyInfo = 'encryption';
|
||||
$config->authKeyInfo = 'authentication';
|
||||
|
||||
$encrypter = Services::encrypter($config);
|
||||
|
||||
return array_map(function ($value) use ($encrypter) {
|
||||
return !empty($value) ? $encrypter->decrypt($value) : '';
|
||||
}, $encryptedData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts data using CI4 algorithms.
|
||||
* The actual decryption/encryption is delegated to CI3SecretConverter so
|
||||
* the docker startup command (env:provision) shares the same code path.
|
||||
*
|
||||
* @param array $plainData Data to be encrypted.
|
||||
* @return array Encrypted data.
|
||||
* @param string $oldKey the CI3-era key currently in .env
|
||||
* @throws RedirectException
|
||||
*/
|
||||
private function encryptData(array $plainData): array
|
||||
private function convertCI3EncryptedData(string $oldKey): void
|
||||
{
|
||||
$encrypter = Services::encrypter();
|
||||
$converter = new CI3SecretConverter();
|
||||
|
||||
return array_map(function ($value) use ($encrypter) {
|
||||
return $value !== '' ? $encrypter->encrypt($value) : '';
|
||||
}, $plainData);
|
||||
}
|
||||
// DB read, safe outside the .env lock.
|
||||
$plain = $converter->decryptAll($oldKey);
|
||||
|
||||
/**
|
||||
* Decrypts data using CI4 algorithms.
|
||||
*
|
||||
* @param array $encryptedData Data to be decrypted.
|
||||
* @return array Decrypted data.
|
||||
*/
|
||||
private function decryptData(array $encryptedData): array
|
||||
{
|
||||
$encrypter = Services::encrypter();
|
||||
// Backup -> rotate -> re-encrypt -> verify -> persist under one .env
|
||||
// lock. On any failure the transaction restores the pre-rotation .env
|
||||
// (still holding the lock); on success it drops the backup.
|
||||
rotateEncryptionKeyTransaction($oldKey, static function () use ($plain, $converter): void {
|
||||
$encrypted = $converter->encryptAll($plain);
|
||||
|
||||
return array_map(function ($value) use ($encrypter) {
|
||||
return !empty($value) ? $encrypter->decrypt($value) : '';
|
||||
}, $encryptedData);
|
||||
// Verify the round trip before committing so we never lose data.
|
||||
if (array_diff_assoc($plain, $converter->verifyAll($encrypted)) !== []) {
|
||||
throw new RedirectException('login'); // TODO: Need to figure out how to pass the error to the Login controller so that it gets displayed.
|
||||
}
|
||||
|
||||
$converter->saveAll($encrypted);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,10 @@ class Load_config
|
||||
|
||||
private function languageExists(string $languageCode): bool
|
||||
{
|
||||
if (strpbrk($languageCode, '/\\') !== false || str_contains($languageCode, '..') || str_contains($languageCode, "\0")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return file_exists(APPPATH . 'Language/' . $languageCode);
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,19 @@ class IsLoggedIn implements FilterInterface
|
||||
$employee = model(Employee::class);
|
||||
|
||||
if (!$employee->is_logged_in()) {
|
||||
if ($request->isAJAX()) {
|
||||
return service('response')
|
||||
->setStatusCode(401)
|
||||
->setJSON([
|
||||
'success' => false,
|
||||
'message' => 'Session expired. Please log in again.',
|
||||
]);
|
||||
}
|
||||
|
||||
throw new RedirectException('login');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
|
||||
|
||||
@@ -8,22 +8,37 @@ use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Config\Services;
|
||||
|
||||
/**
|
||||
* Rate limits login/migrate POST attempts, keyed by IP and by submitted
|
||||
* username, to mitigate brute-force and credential-stuffing attacks
|
||||
* (GHSA-hm9c-xchj-xgcp). Backed by CodeIgniter's cache-based Throttler,
|
||||
* so limits are per-server (not shared across nodes on file cache).
|
||||
* Rate limits login/migrate POST attempts by IP and submitted username to
|
||||
* mitigate brute-force/credential-stuffing. Backed by CodeIgniter's
|
||||
* cache-based Throttler, so limits are per-server (not shared on file cache).
|
||||
*
|
||||
* Tunable via `throttle.capacity` (default 5; 0 disables) and
|
||||
* `throttle.seconds` (window, default 60) in .env.
|
||||
*/
|
||||
class Throttle implements FilterInterface
|
||||
{
|
||||
private const CAPACITY = 5;
|
||||
private const SECONDS = 60;
|
||||
|
||||
public function before(RequestInterface $request, $arguments = null)
|
||||
{
|
||||
if ($request->getMethod() !== 'POST') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Non-positive integer = explicit disable; missing/non-numeric falls
|
||||
// back to the default so a typo (e.g. "five") cannot bypass lockout.
|
||||
$capacity = filter_var(env('throttle.capacity'), FILTER_VALIDATE_INT);
|
||||
if ($capacity === false) {
|
||||
$capacity = 5;
|
||||
}
|
||||
if ($capacity <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$seconds = filter_var(env('throttle.seconds'), FILTER_VALIDATE_INT);
|
||||
if ($seconds === false) {
|
||||
$seconds = 60;
|
||||
}
|
||||
$seconds = max(1, $seconds);
|
||||
|
||||
helper('security');
|
||||
|
||||
$throttler = Services::throttler();
|
||||
@@ -34,8 +49,8 @@ class Throttle implements FilterInterface
|
||||
$username = is_scalar($rawUsername) ? strtolower((string) $rawUsername) : '';
|
||||
$usernameKey = $username !== '' ? 'login-user-' . hash_hmac('sha256', $username, $secret) : null;
|
||||
|
||||
$ipOk = $throttler->check($ipKey, self::CAPACITY, self::SECONDS);
|
||||
$usernameOk = $usernameKey === null || $throttler->check($usernameKey, self::CAPACITY, self::SECONDS);
|
||||
$ipOk = $throttler->check($ipKey, $capacity, $seconds);
|
||||
$usernameOk = $usernameKey === null || $throttler->check($usernameKey, $capacity, $seconds);
|
||||
|
||||
if (!$ipOk || !$usernameOk) {
|
||||
log_message('warning', 'Login throttled for IP {ip} (username: {username})', [
|
||||
|
||||
+420
-107
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
|
||||
use App\Libraries\CI3SecretConverter;
|
||||
use CodeIgniter\Encryption\Encryption;
|
||||
use Config\Services;
|
||||
use Random\RandomException;
|
||||
@@ -14,17 +15,23 @@ use Random\RandomException;
|
||||
*/
|
||||
function lockEnvFile()
|
||||
{
|
||||
$lockPath = ROOTPATH . '.env.lock';
|
||||
$lockPath = config('SecurityEnv')->lockPath;
|
||||
|
||||
$handle = @fopen($lockPath, 'c+');
|
||||
if ($handle === false) {
|
||||
throw new RuntimeException("Unable to open $lockPath");
|
||||
$reason = error_get_last()['message'] ?? 'unknown error';
|
||||
log_message('critical', "Unable to open $lockPath: $reason");
|
||||
|
||||
throw new RuntimeException(lang('Error.unable_to_open_lock_file', ['filePath' => $lockPath, 'reason' => $reason]));
|
||||
}
|
||||
|
||||
if (!flock($handle, LOCK_EX)) {
|
||||
fclose($handle);
|
||||
|
||||
throw new RuntimeException("Unable to lock $lockPath");
|
||||
$reason = error_get_last()['message'] ?? 'unknown error';
|
||||
log_message('critical', "Unable to lock $lockPath: $reason");
|
||||
|
||||
throw new RuntimeException(lang('Error.unable_to_lock_file', ['filePath' => $lockPath, 'reason' => $reason]));
|
||||
}
|
||||
|
||||
return $handle;
|
||||
@@ -41,16 +48,13 @@ function unlockEnvFile($handle): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces or inserts a single `key='value'` line in .env
|
||||
* Copies .env.example to .env (or creates a stub) if .env does not exist yet.
|
||||
*
|
||||
* @param string $envKey
|
||||
* @param string $value
|
||||
* @return bool true on success, false if the write could not be completed
|
||||
* @param string $configPath
|
||||
* @return bool true if $configPath exists (already did, or was just created)
|
||||
*/
|
||||
function writeEnvKey(string $envKey, string $value): bool
|
||||
function initializeEnvFile(string $configPath): bool
|
||||
{
|
||||
$configPath = ROOTPATH . '.env';
|
||||
|
||||
if (!file_exists($configPath)) {
|
||||
$examplePath = ROOTPATH . '.env.example';
|
||||
if (file_exists($examplePath)) {
|
||||
@@ -61,56 +65,70 @@ function writeEnvKey(string $envKey, string $value): bool
|
||||
@chmod($configPath, 0640);
|
||||
}
|
||||
|
||||
if (!file_exists($configPath)) {
|
||||
return file_exists($configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces or inserts a single `key='value'` line in .env
|
||||
*
|
||||
* @param string $envKey
|
||||
* @param string $value
|
||||
* @return bool true on success, false if the write could not be completed
|
||||
*/
|
||||
function writeEnvKey(string $envKey, string $value): bool
|
||||
{
|
||||
$configPath = config('SecurityEnv')->envPath;
|
||||
|
||||
if (!initializeEnvFile($configPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$lock = lockEnvFile();
|
||||
|
||||
try {
|
||||
$configFile = file_get_contents($configPath);
|
||||
$configFile = @file_get_contents($configPath);
|
||||
if ($configFile === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$configFile = applyEnvKeyReplacement($configFile, $envKey, $value);
|
||||
$updated = applyEnvKeyReplacement($configFile, $envKey, $value);
|
||||
if ($updated === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return atomicWriteFile($configPath, $configFile);
|
||||
return atomicWriteFile($configPath, $updated);
|
||||
} finally {
|
||||
unlockEnvFile($lock);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $configFile
|
||||
* @param string $envKey
|
||||
* @param string $value
|
||||
* @return string
|
||||
*/
|
||||
function applyEnvKeyReplacement(string $configFile, string $envKey, string $value): string
|
||||
function applyEnvKeyReplacement(string $configFile, string $envKey, string $value): ?string
|
||||
{
|
||||
$pattern = '/^\s*' . preg_quote($envKey, '/') . '\s*=.*/m';
|
||||
$escapedValue = str_replace(['\\', '$'], ['\\\\', '\\$'], $value);
|
||||
|
||||
if (preg_match($pattern, $configFile)) {
|
||||
return preg_replace($pattern, "$envKey='$value'", $configFile, 1);
|
||||
return preg_replace_callback($pattern, static fn () => "$envKey='$escapedValue'", $configFile, 1);
|
||||
}
|
||||
|
||||
// New keys insert right after encryption.key so it stays first for easy backup/rotation.
|
||||
if (preg_match('/^encryption\.key\s*=.*$/m', $configFile, $matches, PREG_OFFSET_CAPTURE)) {
|
||||
$insertAt = $matches[0][1] + strlen($matches[0][0]);
|
||||
$insertAt = (int) $matches[0][1] + strlen($matches[0][0]);
|
||||
|
||||
return substr_replace($configFile, "\n$envKey='$value'", $insertAt, 0);
|
||||
return substr_replace($configFile, "\n$envKey='$escapedValue'", $insertAt, 0);
|
||||
}
|
||||
|
||||
return $configFile . "\n$envKey='$value'\n";
|
||||
return $configFile . "\n$envKey='$escapedValue'\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes $contents to a temp file in the same directory as $path, then
|
||||
* renames it onto $path so readers never observe a partially-written file.
|
||||
* renames it onto $path so readers never observe a partially written file.
|
||||
*
|
||||
* @param string $path
|
||||
* @param string $contents
|
||||
* @return bool
|
||||
* @throws RandomException
|
||||
*/
|
||||
function atomicWriteFile(string $path, string $contents): bool
|
||||
{
|
||||
@@ -145,10 +163,7 @@ function atomicWriteFile(string $path, string $contents): bool
|
||||
|
||||
fclose($handle);
|
||||
|
||||
// rename() overwrites an existing destination on POSIX. On Windows it
|
||||
// does not, so fall back to unlink()+rename() there. Callers must not
|
||||
// hold any open handle on $path — Windows can't unlink/rename a path
|
||||
// that's still open, even by the same process.
|
||||
// On Windows rename() does not overwrite an existing destination. Fall back to unlink()+rename().
|
||||
if (!@rename($tmpPath, $path)) {
|
||||
if (PHP_OS_FAMILY !== 'Windows' || !@unlink($path) || !@rename($tmpPath, $path)) {
|
||||
@unlink($tmpPath);
|
||||
@@ -163,105 +178,390 @@ function atomicWriteFile(string $path, string $contents): bool
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
* Copies $configPath to $backupPath, creating the backup folder if needed.
|
||||
*
|
||||
* @param string $configPath
|
||||
* @param string $backupPath
|
||||
* @return bool true when the backup exists and is readable, false otherwise
|
||||
*/
|
||||
function checkEncryption(): bool
|
||||
function backupEnvFile(string $configPath, string $backupPath): bool
|
||||
{
|
||||
$oldKey = config('Encryption')->key;
|
||||
$backupFolder = dirname($backupPath);
|
||||
|
||||
if ((empty($oldKey)) || (strlen($oldKey) < 64)) {
|
||||
$encryption = new Encryption();
|
||||
$key = bin2hex($encryption->createKey());
|
||||
config('Encryption')->key = $key;
|
||||
if (!file_exists($backupFolder) && !@mkdir($backupFolder, 0750, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$configPath = ROOTPATH . '.env';
|
||||
$backupPath = WRITEPATH . '/backup/.env.bak';
|
||||
$backupFolder = WRITEPATH . '/backup';
|
||||
if (!@copy($configPath, $backupPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!file_exists($backupFolder)) {
|
||||
@mkdir($backupFolder, 0750, true);
|
||||
}
|
||||
if (!is_readable($backupPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!file_exists($configPath)) {
|
||||
$examplePath = ROOTPATH . '.env.example';
|
||||
if (file_exists($examplePath)) {
|
||||
@copy($examplePath, $configPath);
|
||||
} else {
|
||||
@file_put_contents($configPath, "# OSPOS Configuration\n\n");
|
||||
}
|
||||
@chmod($configPath, 0640);
|
||||
}
|
||||
|
||||
if (file_exists($configPath)) {
|
||||
@copy($configPath, $backupPath);
|
||||
@chmod($backupPath, 0640);
|
||||
@chmod($configPath, 0640);
|
||||
|
||||
if (!writeEnvKey('encryption.key', $key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!empty($oldKey)) {
|
||||
$configFile = file_get_contents($configPath);
|
||||
$oldLine = "# encryption.key='$oldKey' REMOVE IF UNNEEDED\r\n";
|
||||
if (preg_match('/^encryption\.key\s*=/m', $configFile, $matches, PREG_OFFSET_CAPTURE)) {
|
||||
$configFile = substr_replace($configFile, $oldLine, $matches[0][1], 0);
|
||||
@file_put_contents($configPath, $configFile);
|
||||
@chmod($configPath, 0640);
|
||||
}
|
||||
}
|
||||
|
||||
log_message('info', "Updated encryption key in $configPath");
|
||||
}
|
||||
if (@chmod($backupPath, 0640) !== true || @chmod($configPath, 0640) !== true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a persistent secret for HMAC-hashing login-throttle cache keys.
|
||||
* Applies the new encryption.key to $configFile, preserving $oldKey as a
|
||||
* commented-out backup line immediately before it.
|
||||
*
|
||||
* Deliberately independent of checkEncryption()/encryption.key: the throttle
|
||||
* filter runs before the login-triggered CI3->CI4 migration, so provisioning
|
||||
* this secret must never touch or rotate the encryption key.
|
||||
* @param string $configFile
|
||||
* @param string $key
|
||||
* @param string $oldKey
|
||||
* @return string|null updated file contents, or null if the replacement failed
|
||||
*/
|
||||
function writeNewEncryptionKey(string $configFile, string $key, string $oldKey): ?string
|
||||
{
|
||||
$updated = applyEnvKeyReplacement($configFile, 'encryption.key', $key);
|
||||
if ($updated === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!empty($oldKey)) {
|
||||
$oldLine = "# encryption.key='$oldKey' REMOVE IF UNNEEDED\r\n";
|
||||
if (preg_match('/^encryption\.key\s*=/m', $updated, $matches, PREG_OFFSET_CAPTURE)) {
|
||||
$updated = substr_replace($updated, $oldLine, $matches[0][1], 0);
|
||||
}
|
||||
}
|
||||
|
||||
return $updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the current process can write to (or create) .env.
|
||||
*
|
||||
* @return string
|
||||
* @throws RandomException
|
||||
* @throws RuntimeException if the key cannot be durably persisted
|
||||
* The write path (temp file + lock, then rename) needs write permission on
|
||||
* the DIRECTORY, not on .env itself — a bind-mounted .env can be writable
|
||||
* while the dir (or a root-owned .env.lock) is not, so .env's own mode is not
|
||||
* a reliable signal.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function envFileIsWritable(): bool
|
||||
{
|
||||
$configPath = config('SecurityEnv')->envPath;
|
||||
$lockPath = config('SecurityEnv')->lockPath;
|
||||
$dir = dirname($configPath);
|
||||
|
||||
if (!is_writable($dir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Windows-only: rename() can't replace a read-only destination, so an
|
||||
// existing .env must itself be writable (POSIX rename() can, if the dir is).
|
||||
if (PHP_OS_FAMILY === 'Windows'
|
||||
&& file_exists($configPath)
|
||||
&& !is_writable($configPath)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// An existing mutex file (e.g. left by a prior root env:provision) must be writable.
|
||||
if (file_exists($lockPath) && !is_writable($lockPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a usable CI4 encryption key is available.
|
||||
*
|
||||
* Behaviour:
|
||||
* - Key present and >= 64 characters: returns true immediately (no I/O).
|
||||
* - Key missing or CI3-era (< 64 chars) and .env IS writable:
|
||||
* - Short key: decrypts legacy CI3 secrets, rotates to a fresh CI4 key,
|
||||
* re-encrypts under the new key, verifies the round-trip, persists result.
|
||||
* - Missing key: generates a fresh key and persists it.
|
||||
* - Key missing or CI3-era (< 64 chars) and .env NOT writable:
|
||||
* throws — the key was presumably already provisioned externally
|
||||
* (e.g. `php spark env:provision` at container startup); the in-memory
|
||||
* config simply hasn't been reloaded yet.
|
||||
*
|
||||
* @param CI3SecretConverter|null $converter injectable converter used to
|
||||
* decrypt/re-encrypt legacy CI3
|
||||
* secrets in the short-key branch;
|
||||
* defaults to the real converter
|
||||
* (tests may pass a fake to avoid
|
||||
* hitting the database)
|
||||
* @return bool true when a valid key is available
|
||||
* @throws Throwable when .env is not writable or the CI3 -> CI4 conversion fails
|
||||
*/
|
||||
function checkEncryption(?CI3SecretConverter $converter = null): bool
|
||||
{
|
||||
$key = (string) config('Encryption')->key;
|
||||
|
||||
if ($key !== '' && strlen($key) >= 64) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!envFileIsWritable()) {
|
||||
log_message('critical', 'Encryption key not provisioned and .env is not writable. Run `php spark env:provision` to generate one.');
|
||||
|
||||
throw new RuntimeException(lang('Error.encryption_key_not_provisioned'));
|
||||
}
|
||||
|
||||
if ($key !== '') {
|
||||
$converter = $converter ?? new CI3SecretConverter();
|
||||
|
||||
// Legacy plaintext is a DB read; safe outside the .env lock (the lock
|
||||
// only guards the .env file write, which the rotation performs).
|
||||
$plain = $converter->decryptAll($key);
|
||||
|
||||
$nonEmpty = false;
|
||||
foreach ($plain as $value) {
|
||||
if ((string) $value !== '') {
|
||||
$nonEmpty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Run the whole backup -> rotate -> re-encrypt -> verify -> persist unit
|
||||
// under a single .env lock. On any failure, the pre-rotation backup is
|
||||
// restored (also in-lock); on success the backup is removed (in-lock).
|
||||
rotateEncryptionKeyTransaction($key, static function () use ($plain, $converter, $nonEmpty): void {
|
||||
$encrypted = $converter->encryptAll($plain);
|
||||
|
||||
if (array_diff_assoc($plain, $converter->verifyAll($encrypted)) !== []) {
|
||||
throw new RuntimeException(lang('Error.unable_to_persist_encryption_key', ['filePath' => config('SecurityEnv')->envPath]));
|
||||
}
|
||||
|
||||
if ($nonEmpty) {
|
||||
$converter->saveAll($encrypted);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Fresh key (no old key to decrypt): a single atomic write suffices.
|
||||
rotateEncryptionKey(null);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the persistent HMAC secret used to hash login-throttle cache keys.
|
||||
*
|
||||
* Behaviour:
|
||||
* - Key already present (throttle.key or THROTTLE_KEY): returned immediately (no I/O).
|
||||
* - Key missing and .env IS writable: generates a fresh key and persists it.
|
||||
* - Key missing and .env NOT writable:
|
||||
* throws — the key was presumably already provisioned externally
|
||||
* (e.g. `php spark env:provision` at container startup).
|
||||
*
|
||||
* THROTTLE_KEY is a real environment-variable fallback (mirroring ENCRYPTION_KEY):
|
||||
* it is consulted only when throttle.key is empty, so it can be supplied via
|
||||
* Docker/Compose without writing a shared secret into .env.
|
||||
*
|
||||
* @return string the throttle key
|
||||
* @throws RuntimeException if the key cannot be provisioned
|
||||
*/
|
||||
function checkThrottleEncryption(): string
|
||||
{
|
||||
$key = (string) env('throttle.key', '');
|
||||
|
||||
if (!empty($key)) {
|
||||
if ($key === '') {
|
||||
$envKey = getenv('THROTTLE_KEY');
|
||||
$key = $envKey === false ? '' : $envKey;
|
||||
}
|
||||
|
||||
if ($key !== '') {
|
||||
return $key;
|
||||
}
|
||||
|
||||
$configPath = ROOTPATH . '.env';
|
||||
if (!envFileIsWritable()) {
|
||||
log_message('critical', 'Throttle key not provisioned and .env is not writable. Run `php spark env:provision` to generate one.');
|
||||
|
||||
if (!file_exists($configPath)) {
|
||||
$examplePath = ROOTPATH . '.env.example';
|
||||
if (file_exists($examplePath)) {
|
||||
@copy($examplePath, $configPath);
|
||||
} else {
|
||||
@file_put_contents($configPath, "# OSPOS Configuration\n\n");
|
||||
}
|
||||
@chmod($configPath, 0640);
|
||||
throw new RuntimeException(lang('Error.throttle_key_not_provisioned'));
|
||||
}
|
||||
|
||||
if (!file_exists($configPath)) {
|
||||
throw new RuntimeException("Unable to create $configPath to provision throttle.key");
|
||||
return provisionThrottleKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a strong encryption key, persists it to `.env` (rotating in place
|
||||
* so callers can re-encrypt CI3 data that was sealed with the old key), and
|
||||
* applies it to the running config instance.
|
||||
*
|
||||
* Standalone key-write path: acquires and holds the `.env` mutex for the
|
||||
* duration of the write, so concurrent callers see atomic key writes. Callers
|
||||
* that also run a longer multi-step conversion (re-encrypt, verify, persist)
|
||||
* must use rotateEncryptionKeyTransaction() instead, so a single lock is held
|
||||
* for the whole backup -> rotate -> re-encrypt -> persist unit and the
|
||||
* key-write does not take a nested lock on the same mutex (which would
|
||||
* deadlock).
|
||||
*
|
||||
* @param string|null $oldKey current key to rotate from; when non-empty it is
|
||||
* preserved as a commented backup line for the
|
||||
* CI3->CI4 re-encryption pass
|
||||
* @return string the newly generated key
|
||||
* @throws RuntimeException if the key could not be generated or persisted
|
||||
* @throws RandomException
|
||||
*/
|
||||
function rotateEncryptionKey(?string $oldKey = null): string
|
||||
{
|
||||
$lock = lockEnvFile();
|
||||
|
||||
try {
|
||||
return rotateEncryptionKeyUnlock($oldKey);
|
||||
} finally {
|
||||
unlockEnvFile($lock);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock-free core of rotateEncryptionKey(): generates a key, backs up .env,
|
||||
* writes the new key (atomic), and applies it to the running config.
|
||||
*
|
||||
* Do not call directly from context that does not already hold the lock —
|
||||
* that leaves a window in which another worker can read or write .env before
|
||||
* the atomic write lands. The two callers, rotateEncryptionKey() and
|
||||
* rotateEncryptionKeyTransaction(), each acquire the lock first and pass the
|
||||
* lock-free core through.
|
||||
*
|
||||
* @param string|null $oldKey current key to rotate from
|
||||
* @return string the newly generated key
|
||||
* @throws RuntimeException if the key could not be generated or persisted
|
||||
* @throws RandomException
|
||||
*/
|
||||
function rotateEncryptionKeyUnlock(?string $oldKey): string
|
||||
{
|
||||
$encryption = new Encryption();
|
||||
$key = bin2hex($encryption->createKey());
|
||||
|
||||
$configPath = config('SecurityEnv')->envPath;
|
||||
$backupPath = config('SecurityEnv')->backupPath;
|
||||
|
||||
if (!initializeEnvFile($configPath)) {
|
||||
throw new RuntimeException(lang('Error.unable_to_create_env_file', ['filePath' => $configPath]));
|
||||
}
|
||||
|
||||
if (file_exists($configPath)) {
|
||||
if (!backupEnvFile($configPath, $backupPath)) {
|
||||
log_message('critical', "Unable to back up $configPath to $backupPath before rotation; aborting.");
|
||||
|
||||
throw new RuntimeException(lang('Error.unable_to_persist_encryption_key', ['filePath' => $configPath]));
|
||||
}
|
||||
}
|
||||
|
||||
$configFile = @file_get_contents($configPath);
|
||||
if ($configFile === false) {
|
||||
log_message('critical', "Unable to read $configPath before rotation; aborting.");
|
||||
|
||||
throw new RuntimeException(lang('Error.unable_to_read_env_file', ['filePath' => $configPath]));
|
||||
}
|
||||
|
||||
$updated = writeNewEncryptionKey($configFile, $key, (string) $oldKey);
|
||||
if ($updated === null) {
|
||||
throw new RuntimeException(lang('Error.unable_to_persist_encryption_key', ['filePath' => $configPath]));
|
||||
}
|
||||
|
||||
if (!atomicWriteFile($configPath, $updated)) {
|
||||
throw new RuntimeException(lang('Error.unable_to_persist_encryption_key', ['filePath' => $configPath]));
|
||||
}
|
||||
|
||||
config('Encryption')->key = $key;
|
||||
|
||||
log_message('info', "Rotated encryption key in $configPath");
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the CI3 -> CI4 key conversion as a single transaction on the `.env`
|
||||
* mutex.
|
||||
*
|
||||
* The lock is acquired BEFORE the .env backup and released only AFTER
|
||||
* $conversion has run, so the entire unit (backup -> rotate -> re-encrypt ->
|
||||
* verify -> persist) is atomic with respect to any other worker that also
|
||||
* takes the lock. The $conversion callback must throw on failure; the caller
|
||||
* is expected to catch the exception and roll back via
|
||||
* abortEncryptionConversion().
|
||||
*
|
||||
* Calls the lock-free rotateEncryptionKeyUnlock() internally so the
|
||||
* transaction lock is not re-acquired on the same mutex (which would deadlock)
|
||||
* — the mutex is held across the backup, the key write, the re-encryption,
|
||||
* verification, and persistence.
|
||||
*
|
||||
* @param string|null $oldKey current key to rotate from
|
||||
* @param callable $conversion callback(): void, called after the new key
|
||||
* has been persisted and applied to the running
|
||||
* config; expected to perform re-encryption,
|
||||
* verification, and persistence. Must throw on
|
||||
* failure; the pre-rotation backup is restored
|
||||
* before the exception propagates.
|
||||
* @return string the newly generated key
|
||||
* @throws Throwable from $conversion (the lock is released in `finally`)
|
||||
* @throws RuntimeException
|
||||
* @throws RandomException
|
||||
*/
|
||||
function rotateEncryptionKeyTransaction(?string $oldKey, callable $conversion): string
|
||||
{
|
||||
// Hold one lock for the entire transaction (backup -> rotate -> re-encrypt
|
||||
// -> verify -> persist -> cleanup). If we cannot acquire it, we fail
|
||||
// rather than silently run without synchronisation — the invariant (no
|
||||
// interleaved key + ciphertext writes) requires it.
|
||||
$lock = lockEnvFile();
|
||||
|
||||
try {
|
||||
// Lock-free inner write: the outer lock IS the transaction lock, so no
|
||||
// nested re-acquire on the same mutex (which would deadlock).
|
||||
$newKey = rotateEncryptionKeyUnlock($oldKey);
|
||||
|
||||
// Multi-step conversion (re-encrypt -> verify -> persist) runs while
|
||||
// still holding the lock, so a concurrent worker cannot interleave its
|
||||
// key write between the rotation and this ciphertext save.
|
||||
$conversion();
|
||||
|
||||
// Success: drop the pre-rotation backup, in-lock.
|
||||
removeBackup();
|
||||
|
||||
return $newKey;
|
||||
} catch (Throwable $e) {
|
||||
// Failure (CI4 EncryptionException / ReflectionException from saveAll,
|
||||
// or a failed round-trip verify, or the rotation itself failed):
|
||||
// restore the pre-rotation .env while still holding the lock, then
|
||||
// rethrow. catch runs before finally, so the restore is atomic with
|
||||
// the held lock.
|
||||
abortEncryptionConversion();
|
||||
|
||||
throw $e;
|
||||
} finally {
|
||||
unlockEnvFile($lock);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a persistent throttle secret exists, generating and persisting one
|
||||
* to `.env` if it is missing. Idempotent: safe to call on every startup.
|
||||
*
|
||||
* @return string the throttle key
|
||||
* @throws RuntimeException if the key could not be created or persisted
|
||||
*/
|
||||
function provisionThrottleKey(): string
|
||||
{
|
||||
$configPath = config('SecurityEnv')->envPath;
|
||||
|
||||
if (!initializeEnvFile($configPath)) {
|
||||
throw new RuntimeException(lang('Error.unable_to_create_env_file', ['filePath' => $configPath]));
|
||||
}
|
||||
|
||||
$lock = lockEnvFile();
|
||||
|
||||
try {
|
||||
$configFile = file_get_contents($configPath);
|
||||
$configFile = @file_get_contents($configPath);
|
||||
if ($configFile === false) {
|
||||
throw new RuntimeException("Unable to read $configPath to provision throttle.key");
|
||||
log_message('critical', "Unable to read $configPath while provisioning throttle key; aborting.");
|
||||
|
||||
throw new RuntimeException(lang('Error.unable_to_read_env_file', ['filePath' => $configPath]));
|
||||
}
|
||||
|
||||
// Another process may have provisioned the key while we waited for the lock.
|
||||
$key = '';
|
||||
if (preg_match('/^\s*throttle\.key\s*=\s*[\'"]?([^\'"\r\n]*)/m', $configFile, $matches)) {
|
||||
$existing = trim($matches[1]);
|
||||
if ($existing !== '') {
|
||||
@@ -269,13 +569,13 @@ function checkThrottleEncryption(): string
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($key)) {
|
||||
if ($key === '') {
|
||||
$key = bin2hex(random_bytes(32));
|
||||
$configFile = applyEnvKeyReplacement($configFile, 'throttle.key', $key);
|
||||
}
|
||||
|
||||
if (!atomicWriteFile($configPath, $configFile)) {
|
||||
throw new RuntimeException("Unable to persist throttle.key to $configPath");
|
||||
}
|
||||
$updated = applyEnvKeyReplacement($configFile, 'throttle.key', $key);
|
||||
if ($updated === null || !atomicWriteFile($configPath, $updated)) {
|
||||
throw new RuntimeException(lang('Error.unable_to_persist_throttle_key', ['filePath' => $configPath]));
|
||||
}
|
||||
} finally {
|
||||
unlockEnvFile($lock);
|
||||
@@ -285,7 +585,7 @@ function checkThrottleEncryption(): string
|
||||
$_ENV['throttle.key'] = $key;
|
||||
$_SERVER['throttle.key'] = $key;
|
||||
|
||||
log_message('info', 'Provisioned throttle key in ' . ROOTPATH . '.env');
|
||||
log_message('info', 'Provisioned throttle key in ' . config('SecurityEnv')->envPath);
|
||||
|
||||
return $key;
|
||||
}
|
||||
@@ -295,16 +595,29 @@ function checkThrottleEncryption(): string
|
||||
*/
|
||||
function abortEncryptionConversion(): void
|
||||
{
|
||||
$configPath = ROOTPATH . '.env';
|
||||
$backupPath = WRITEPATH . '/backup/.env.bak';
|
||||
$configPath = config('SecurityEnv')->envPath;
|
||||
$backupPath = config('SecurityEnv')->backupPath;
|
||||
|
||||
if (!file_exists($backupPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@chmod($configPath, 0640);
|
||||
// A backup exists, so the restore must succeed or fail loudly; a silent
|
||||
// failure would leave .env holding the new key while the DB still holds the
|
||||
// old ciphertext, making the data undecryptable after the next restart.
|
||||
if (!is_file($backupPath) || !is_readable($backupPath)) {
|
||||
throw new RuntimeException(lang('Error.unable_to_read_env_file', ['filePath' => $backupPath]));
|
||||
}
|
||||
|
||||
$configFile = file_get_contents($backupPath);
|
||||
@file_put_contents($configPath, $configFile);
|
||||
if ($configFile === false) {
|
||||
throw new RuntimeException(lang('Error.unable_to_read_env_file', ['filePath' => $backupPath]));
|
||||
}
|
||||
|
||||
if (!atomicWriteFile($configPath, $configFile)) {
|
||||
throw new RuntimeException(lang('Error.unable_to_persist_encryption_key', ['filePath' => $configPath]));
|
||||
}
|
||||
|
||||
log_message('info', "Restored $configPath from backup");
|
||||
}
|
||||
|
||||
@@ -313,7 +626,7 @@ function abortEncryptionConversion(): void
|
||||
*/
|
||||
function removeBackup(): void
|
||||
{
|
||||
$backupPath = WRITEPATH . '/backup/.env.bak';
|
||||
$backupPath = config('SecurityEnv')->backupPath;
|
||||
if (!file_exists($backupPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
use App\Models\Attribute;
|
||||
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;
|
||||
@@ -402,10 +400,33 @@ function item_headers(): array
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all sortable column keys for items table, including dynamic attribute columns.
|
||||
*
|
||||
* @param array|null $definitionIds Attribute definition IDs to append as sortable columns.
|
||||
* If null, resolved via a query against SHOW_IN_ITEMS.
|
||||
* @return array Array of column headers in the format sanitizeSortColumn() expects
|
||||
*/
|
||||
function itemSortColumns(?array $definitionIds = null): array
|
||||
{
|
||||
if ($definitionIds === null) {
|
||||
$attribute = model(Attribute::class);
|
||||
$definitionIds = array_keys($attribute->getDefinitionsByFlags($attribute::SHOW_IN_ITEMS));
|
||||
}
|
||||
|
||||
$headers = item_headers();
|
||||
|
||||
foreach ($definitionIds as $definitionId) {
|
||||
$headers[] = [(string) $definitionId => ''];
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the header for the items tabular view
|
||||
*/
|
||||
function get_items_manage_table_headers(): string
|
||||
function getItemsManageTableHeaders(): string
|
||||
{
|
||||
$attribute = model(Attribute::class);
|
||||
$config = config(OSPOS::class)->settings;
|
||||
@@ -421,8 +442,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 ($definitionsWithTypes as $definitionId => $definitionInfo) {
|
||||
$headers[] = [$definitionId => $definitionInfo['name'], 'sortable' => true];
|
||||
}
|
||||
|
||||
$headers[] = ['inventory' => '', 'escape' => false];
|
||||
@@ -433,32 +454,17 @@ function get_items_manage_table_headers(): string
|
||||
|
||||
/**
|
||||
* Get the html data row for the item
|
||||
*
|
||||
* @param object $item
|
||||
* @param array $definitionNames Attribute definitions with types, keyed by definition_id (see Attribute::getDefinitionsByFlags(..., true))
|
||||
* @param array $taxPercentsByItemId Pre-computed tax percent strings, keyed by item_id (see Items::buildTaxPercentsByItem())
|
||||
* @return array
|
||||
*/
|
||||
function get_item_data_row(object $item): array
|
||||
function getItemDataRow(object $item, array $definitionNames, array $taxPercentsByItemId): array
|
||||
{
|
||||
$attribute = model(Attribute::class);
|
||||
$item_taxes = model(Item_taxes::class);
|
||||
$tax_category = model(Tax_category::class);
|
||||
$config = config(OSPOS::class)->settings;
|
||||
|
||||
if ($config['use_destination_based_tax']) {
|
||||
if ($item->tax_category_id == null) { // TODO: === ?
|
||||
$tax_percents = '-';
|
||||
} else {
|
||||
$tax_category_info = $tax_category->get_info($item->tax_category_id);
|
||||
$tax_percents = $tax_category_info->tax_category;
|
||||
}
|
||||
} else {
|
||||
$item_tax_info = $item_taxes->get_info($item->item_id);
|
||||
$tax_percents = '';
|
||||
foreach ($item_tax_info as $tax_info) {
|
||||
$tax_percents .= to_tax_decimals($tax_info['percent']) . '%, ';
|
||||
}
|
||||
|
||||
// Remove ', ' from last item
|
||||
$tax_percents = substr($tax_percents, 0, -2);
|
||||
$tax_percents = !$tax_percents ? '-' : $tax_percents;
|
||||
}
|
||||
$taxPercents = $taxPercentsByItemId[$item->item_id] ?? '-';
|
||||
|
||||
$controller = get_controller();
|
||||
|
||||
@@ -471,8 +477,8 @@ function get_item_data_row(object $item): array
|
||||
: glob("./uploads/item_pics/$item->pic_filename");
|
||||
|
||||
if (sizeof($images) > 0) {
|
||||
$image_path = ltrim($images[0], './');
|
||||
$image .= '<a class="rollover" href="' . base_url(implode('/', array_map('rawurlencode', explode('/', $image_path)))) . '"><img alt="Image thumbnail" src="' . site_url('items/PicThumb/' . rawurlencode(pathinfo($images[0], PATHINFO_BASENAME))) . '"></a>';
|
||||
$imagePath = ltrim($images[0], './');
|
||||
$image .= '<a class="rollover" href="' . base_url(implode('/', array_map('rawurlencode', explode('/', $imagePath)))) . '"><img alt="Image thumbnail" src="' . site_url('items/PicThumb/' . rawurlencode(pathinfo($images[0], PATHINFO_BASENAME))) . '"></a>';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,8 +486,6 @@ function get_item_data_row(object $item): array
|
||||
$item->name .= NAME_SEPARATOR . $item->pack_name;
|
||||
}
|
||||
|
||||
$definition_names = $attribute->getDefinitionsByFlags($attribute::SHOW_IN_ITEMS, true);
|
||||
|
||||
$columns = [
|
||||
'items.item_id' => $item->item_id,
|
||||
'item_number' => $item->item_number,
|
||||
@@ -491,7 +495,7 @@ function get_item_data_row(object $item): array
|
||||
'cost_price' => to_currency($item->cost_price),
|
||||
'unit_price' => to_currency($item->unit_price),
|
||||
'quantity' => to_quantity_decimals($item->quantity),
|
||||
'tax_percents' => !$tax_percents ? '-' : $tax_percents,
|
||||
'tax_percents' => !$taxPercents ? '-' : $taxPercents,
|
||||
'item_pic' => $image
|
||||
];
|
||||
|
||||
@@ -524,7 +528,7 @@ function get_item_data_row(object $item): array
|
||||
)
|
||||
];
|
||||
|
||||
return $columns + expand_attribute_values($definition_names, (array) $item) + $icons;
|
||||
return $columns + expand_attribute_values($definitionNames, (array) $item) + $icons;
|
||||
}
|
||||
|
||||
function giftcard_headers(): array
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"attribute_value_invalid_chars" => "الميزات لا يمكن أن تحتوي على ':' أو'|'",
|
||||
"confirm_delete" => "هل أنت متأكد من أنك تريد حذف الميزات المحددة ؟",
|
||||
"confirm_restore" => "هل أنت متأكد من أنك تريد استعادة السمة (السمات) المحددة؟",
|
||||
"definition_cannot_be_deleted" => "لا يمكن حذف السمات المحددة",
|
||||
"definition_invalid_group" => "المجموعة المحددة غير موجودة أو غير صالحة.",
|
||||
"definition_error_adding_updating" => "لا يمكن إضافة السمة {0} أو تحديثها. يرجى التحقق من سجل الخطأ.",
|
||||
"definition_flags" => "رؤية الميزات",
|
||||
"definition_group" => "المجموعة",
|
||||
"definition_id" => "كود",
|
||||
"definition_name" => "إضافة ميزة",
|
||||
"definition_name_required" => "اسم الميزة هي خانة اجبارية",
|
||||
"definition_one_or_multiple" => "ميزة/ميزات",
|
||||
"definition_successful_adding" => "لقد تم إضافة صنف بنجاح",
|
||||
"definition_successful_deleted" => "لقد تم حذف ميزة بنجاح",
|
||||
"definition_successful_updating" => "تم تعديل الميزة بنجاح",
|
||||
"definition_type" => "نوع الميزة",
|
||||
"definition_type_required" => "نوع الميزة هي خانة إجبارية",
|
||||
"definition_unit" => "وحدة قياس",
|
||||
"definition_values" => "قيمة الميزة",
|
||||
"new" => "اضافة ميزة جديده",
|
||||
"no_attributes_to_display" => "لا يوجد اصناف للعرض",
|
||||
"receipt_visibility" => "وصل",
|
||||
"show_in_items" => "اظهار في الصنف",
|
||||
"show_in_items_visibility" => "الصنف",
|
||||
"show_in_receipt" => "اظهار على الوصل",
|
||||
"show_in_receivings" => "اظهار في استلام البضائع",
|
||||
"show_in_receivings_visibility" => "استلام البضائع",
|
||||
"show_in_sales" => "اظهار خلال البيع",
|
||||
"show_in_sales_visibility" => "البيع",
|
||||
"update" => "تحديث الميزات",
|
||||
'attribute_value_invalid_chars' => 'الميزات لا يمكن أن تحتوي على \':\' أو\'|\'',
|
||||
'confirm_delete' => 'هل أنت متأكد من أنك تريد حذف الميزات المحددة ؟',
|
||||
'confirm_restore' => 'هل أنت متأكد من أنك تريد استعادة السمة (السمات) المحددة؟',
|
||||
'definition_cannot_be_deleted' => 'لا يمكن حذف السمات المحددة',
|
||||
'definition_invalid_group' => 'المجموعة المحددة غير موجودة أو غير صالحة.',
|
||||
'definition_error_adding_updating' => 'لا يمكن إضافة السمة {0} أو تحديثها. يرجى التحقق من سجل الخطأ.',
|
||||
'definition_flags' => 'رؤية الميزات',
|
||||
'definition_group' => 'المجموعة',
|
||||
'definition_id' => 'كود',
|
||||
'definition_name' => 'إضافة ميزة',
|
||||
'definition_name_required' => 'اسم الميزة هي خانة اجبارية',
|
||||
'definition_one_or_multiple' => 'ميزة/ميزات',
|
||||
'definition_successful_adding' => 'لقد تم إضافة صنف بنجاح',
|
||||
'definition_successful_deleted' => 'لقد تم حذف ميزة بنجاح',
|
||||
'definition_successful_updating' => 'تم تعديل الميزة بنجاح',
|
||||
'definition_type' => 'نوع الميزة',
|
||||
'definition_type_required' => 'نوع الميزة هي خانة إجبارية',
|
||||
'definition_unit' => 'وحدة قياس',
|
||||
'definition_values' => 'قيمة الميزة',
|
||||
'new' => 'اضافة ميزة جديده',
|
||||
'no_attributes_to_display' => 'لا يوجد اصناف للعرض',
|
||||
'receipt_visibility' => 'وصل',
|
||||
'show_in_items' => 'اظهار في الصنف',
|
||||
'show_in_items_visibility' => 'الصنف',
|
||||
'show_in_receipt' => 'اظهار على الوصل',
|
||||
'show_in_receivings' => 'اظهار في استلام البضائع',
|
||||
'show_in_receivings_visibility' => 'استلام البضائع',
|
||||
'show_in_sales' => 'اظهار خلال البيع',
|
||||
'show_in_sales_visibility' => 'البيع',
|
||||
'show_in_search' => 'اظهار في البحث',
|
||||
'show_in_search_visibility' => 'بحث',
|
||||
'update' => 'تحديث الميزات',
|
||||
];
|
||||
@@ -210,7 +210,6 @@ return [
|
||||
'msg_msg' => 'الرسائل النصية المحفوظة',
|
||||
'msg_msg_placeholder' => 'إذا أردت إستخدام قالب للرسائل القصيرة احفظه هنا. عدا ذلك أترك هذا الحقل فارغ.',
|
||||
'msg_pwd' => 'SMS-API كلمة السر لـ',
|
||||
'msg_pwd_required' => 'مطلوب SMS-API كلمة السر لـ',
|
||||
'msg_src' => 'SMS-API كود المرسل لـ',
|
||||
'msg_src_required' => 'مطلوب SMS-API كود المرسل لـ',
|
||||
'msg_uid' => 'SMS-API اسم المستخدم لـ',
|
||||
@@ -285,7 +284,7 @@ return [
|
||||
'right' => 'يمين',
|
||||
'sales_invoice_format' => 'شكل فاتورة البيع',
|
||||
'sales_quote_format' => 'شكل فاتورة عرض الاسعار',
|
||||
'mailpath_invalid' => '',
|
||||
'mailpath_invalid' => 'مسار sendmail غير صالح. يُسمح فقط بالحروف والأرقام والشرطات والشرطات السفلية والشرطات المائلة والشرطات المائلة العكسية والنقطتين الرأسيتين والمسافات والنقاط.',
|
||||
'saved_successfully' => 'تم حفظ التهيئة بنجاح.',
|
||||
'saved_unsuccessfully' => 'لم يتم حفظ التهيئة بنجاح.',
|
||||
'security_issue' => 'تحذير من ثغرة أمنية',
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"no_permission_module" => "ليس لديك صلاحيات للوصول لهذا القسم",
|
||||
"unknown" => "غير معروف",
|
||||
'no_permission_module' => 'ليس لديك صلاحيات للوصول لهذا القسم',
|
||||
'unable_to_create_env_file' => 'تعذر إنشاء {filePath} لتوفير throttle.key',
|
||||
'unable_to_lock_file' => 'تعذر قفل {filePath}: {reason}',
|
||||
'unable_to_open_lock_file' => 'تعذر فتح {filePath}: {reason}',
|
||||
'unable_to_persist_throttle_key' => 'تعذر حفظ throttle.key في {filePath}',
|
||||
'unable_to_persist_encryption_key' => '',
|
||||
'encryption_key_not_provisioned' => '',
|
||||
'throttle_key_not_provisioned' => '',
|
||||
'unable_to_read_env_file' => 'تعذر قراءة {filePath} لتوفير throttle.key',
|
||||
'unknown' => 'غير معروف',
|
||||
];
|
||||
@@ -128,6 +128,7 @@ return [
|
||||
"tax_rate" => "معدل الضريبة",
|
||||
"taxes" => "الضرائب",
|
||||
"taxes_summary_report" => "تقرير ملخص الضرائب",
|
||||
"toggle_cost_and_profit" => "",
|
||||
"total" => "الإجمالى",
|
||||
"total_inventory_value" => "إجمالى قيمة المخزن",
|
||||
"total_low_sell_quantity" => "مجموع الكميات حسب الصنف الاولي",
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"attribute_value_invalid_chars" => "الميزات لا يمكن أن تحتوي على ':' أو'|'",
|
||||
"confirm_delete" => "هل أنت متأكد من أنك تريد حذف الميزات المحددة ؟",
|
||||
"confirm_restore" => "هل أنت متأكد من أنك تريد استعادة السمة (السمات) المحددة؟",
|
||||
"definition_cannot_be_deleted" => "لا يمكن حذف السمات المحددة",
|
||||
"definition_invalid_group" => "المجموعة المحددة غير موجودة أو غير صالحة.",
|
||||
"definition_error_adding_updating" => "لا يمكن إضافة السمة {0} أو تحديثها. يرجى التحقق من سجل الخطأ.",
|
||||
"definition_flags" => "رؤية الميزات",
|
||||
"definition_group" => "المجموعة",
|
||||
"definition_id" => "كود",
|
||||
"definition_name" => "إضافة ميزة",
|
||||
"definition_name_required" => "اسم الميزة هي خانة اجبارية",
|
||||
"definition_one_or_multiple" => "ميزة/ميزات",
|
||||
"definition_successful_adding" => "لقد تم إضافة صنف بنجاح",
|
||||
"definition_successful_deleted" => "لقد تم حذف ميزة بنجاح",
|
||||
"definition_successful_updating" => "تم تعديل الميزة بنجاح",
|
||||
"definition_type" => "نوع الميزة",
|
||||
"definition_type_required" => "نوع الميزة هي خانة إجبارية",
|
||||
"definition_unit" => "وحدة قياس",
|
||||
"definition_values" => "قيمة الميزة",
|
||||
"new" => "اضافة ميزة جديده",
|
||||
"no_attributes_to_display" => "لا يوجد اصناف للعرض",
|
||||
"receipt_visibility" => "وصل",
|
||||
"show_in_items" => "اظهار في الصنف",
|
||||
"show_in_items_visibility" => "الصنف",
|
||||
"show_in_receipt" => "اظهار على الوصل",
|
||||
"show_in_receivings" => "اظهار في استلام البضائع",
|
||||
"show_in_receivings_visibility" => "استلام البضائع",
|
||||
"show_in_sales" => "اظهار خلال البيع",
|
||||
"show_in_sales_visibility" => "البيع",
|
||||
"update" => "تحديث الميزات",
|
||||
'attribute_value_invalid_chars' => 'الميزات لا يمكن أن تحتوي على \':\' أو\'|\'',
|
||||
'confirm_delete' => 'هل أنت متأكد من أنك تريد حذف الميزات المحددة ؟',
|
||||
'confirm_restore' => 'هل أنت متأكد من أنك تريد استعادة السمة (السمات) المحددة؟',
|
||||
'definition_cannot_be_deleted' => 'لا يمكن حذف السمات المحددة',
|
||||
'definition_invalid_group' => 'المجموعة المحددة غير موجودة أو غير صالحة.',
|
||||
'definition_error_adding_updating' => 'لا يمكن إضافة السمة {0} أو تحديثها. يرجى التحقق من سجل الخطأ.',
|
||||
'definition_flags' => 'رؤية الميزات',
|
||||
'definition_group' => 'المجموعة',
|
||||
'definition_id' => 'كود',
|
||||
'definition_name' => 'إضافة ميزة',
|
||||
'definition_name_required' => 'اسم الميزة هي خانة اجبارية',
|
||||
'definition_one_or_multiple' => 'ميزة/ميزات',
|
||||
'definition_successful_adding' => 'لقد تم إضافة صنف بنجاح',
|
||||
'definition_successful_deleted' => 'لقد تم حذف ميزة بنجاح',
|
||||
'definition_successful_updating' => 'تم تعديل الميزة بنجاح',
|
||||
'definition_type' => 'نوع الميزة',
|
||||
'definition_type_required' => 'نوع الميزة هي خانة إجبارية',
|
||||
'definition_unit' => 'وحدة قياس',
|
||||
'definition_values' => 'قيمة الميزة',
|
||||
'new' => 'اضافة ميزة جديده',
|
||||
'no_attributes_to_display' => 'لا يوجد اصناف للعرض',
|
||||
'receipt_visibility' => 'وصل',
|
||||
'show_in_items' => 'اظهار في الصنف',
|
||||
'show_in_items_visibility' => 'الصنف',
|
||||
'show_in_receipt' => 'اظهار على الوصل',
|
||||
'show_in_receivings' => 'اظهار في استلام البضائع',
|
||||
'show_in_receivings_visibility' => 'استلام البضائع',
|
||||
'show_in_sales' => 'اظهار خلال البيع',
|
||||
'show_in_sales_visibility' => 'البيع',
|
||||
'show_in_search' => 'اظهار في البحث',
|
||||
'show_in_search_visibility' => 'بحث',
|
||||
'update' => 'تحديث الميزات',
|
||||
];
|
||||
@@ -210,7 +210,6 @@ return [
|
||||
'msg_msg' => 'الرسائل النصية المحفوظة',
|
||||
'msg_msg_placeholder' => 'إذا أردت إستخدام قالب للرسائل القصيرة احفظه هنا. عدا ذلك أترك هذا الحقل فارغ.',
|
||||
'msg_pwd' => 'SMS-API كلمة السر لـ',
|
||||
'msg_pwd_required' => 'مطلوب SMS-API كلمة السر لـ',
|
||||
'msg_src' => 'SMS-API كود المرسل لـ',
|
||||
'msg_src_required' => 'مطلوب SMS-API كود المرسل لـ',
|
||||
'msg_uid' => 'SMS-API اسم المستخدم لـ',
|
||||
@@ -285,7 +284,7 @@ return [
|
||||
'right' => 'يمين',
|
||||
'sales_invoice_format' => 'شكل فاتورة البيع',
|
||||
'sales_quote_format' => 'شكل فاتورة عرض الاسعار',
|
||||
'mailpath_invalid' => '',
|
||||
'mailpath_invalid' => 'مسار sendmail غير صالح. يُسمح فقط بالحروف والأرقام والشرطات والشرطات السفلية والشرطات المائلة والشرطات المائلة العكسية والنقطتين الرأسيتين والمسافات والنقاط.',
|
||||
'saved_successfully' => 'تم حفظ التهيئة بنجاح.',
|
||||
'saved_unsuccessfully' => 'لم يتم حفظ التهيئة بنجاح.',
|
||||
'security_issue' => 'تحذير من ثغرة أمنية',
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"no_permission_module" => "ليس لديك صلاحيات للوصول لهذا القسم",
|
||||
"unknown" => "غير معروف",
|
||||
'no_permission_module' => 'ليس لديك صلاحيات للوصول لهذا القسم',
|
||||
'unable_to_create_env_file' => 'تعذر إنشاء {filePath} لتوفير throttle.key',
|
||||
'unable_to_lock_file' => 'تعذر قفل {filePath}: {reason}',
|
||||
'unable_to_open_lock_file' => 'تعذر فتح {filePath}: {reason}',
|
||||
'unable_to_persist_throttle_key' => 'تعذر حفظ throttle.key في {filePath}',
|
||||
'unable_to_persist_encryption_key' => '',
|
||||
'encryption_key_not_provisioned' => '',
|
||||
'throttle_key_not_provisioned' => '',
|
||||
'unable_to_read_env_file' => 'تعذر قراءة {filePath} لتوفير throttle.key',
|
||||
'unknown' => 'غير معروف',
|
||||
];
|
||||
@@ -128,6 +128,7 @@ return [
|
||||
"tax_rate" => "معدل الضريبة",
|
||||
"taxes" => "الضرائب",
|
||||
"taxes_summary_report" => "تقرير ملخص الضرائب",
|
||||
"toggle_cost_and_profit" => "",
|
||||
"total" => "الإجمالى",
|
||||
"total_inventory_value" => "إجمالى قيمة المخزن",
|
||||
"total_low_sell_quantity" => "مجموع الكميات حسب الصنف الاولي",
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"attribute_value_invalid_chars" => "Atribut dəyəri bu ':' və ya '|' olmaz",
|
||||
"confirm_delete" => "Seçilmiş Atributları silmək istədiyinizdən əminsinizmi?",
|
||||
"confirm_restore" => "Seçilmiş atributları bərpa etmək istədiyinizə əminsinizmi?",
|
||||
"definition_cannot_be_deleted" => "Seçilmiş xüsusiyyətləri silmək olmadı",
|
||||
"definition_invalid_group" => "",
|
||||
"definition_error_adding_updating" => "{0} -in atributları əlavə oluna və yenilənə bilmədi. Lütfən XƏTA loq faylını yoxlayın.",
|
||||
"definition_flags" => "Atribut görünüşü",
|
||||
"definition_group" => "Qrup",
|
||||
"definition_id" => "Id",
|
||||
"definition_name" => "Atribut əlavə et",
|
||||
"definition_name_required" => "Atribut adı zəruri bir sahədir",
|
||||
"definition_one_or_multiple" => "atribut (lar)",
|
||||
"definition_successful_adding" => "Malı uğurla əlavə etdiniz",
|
||||
"definition_successful_deleted" => "Siz uğurla sildiniz",
|
||||
"definition_successful_updating" => "Atributunuzu müvəffəqiyyətlə yenilədiniz",
|
||||
"definition_type" => "Atribut Tipi",
|
||||
"definition_type_required" => "Atribut növü tələb olunan sahədir",
|
||||
"definition_unit" => "Ölçü vahidi",
|
||||
"definition_values" => "Atribut dəyərləri",
|
||||
"new" => "Yeni atributlar",
|
||||
"no_attributes_to_display" => "Göstərmək üçün heç bir element yoxdur",
|
||||
"receipt_visibility" => "Qəbz",
|
||||
"show_in_items" => "Malları göstər",
|
||||
"show_in_items_visibility" => "Mallar",
|
||||
"show_in_receipt" => "Qəbzi göstərin",
|
||||
"show_in_receivings" => "Alacaqlarda göstərin",
|
||||
"show_in_receivings_visibility" => "Alınanlar",
|
||||
"show_in_sales" => "Satışda göstərin",
|
||||
"show_in_sales_visibility" => "Satışlar",
|
||||
"update" => "Atributları yenilə",
|
||||
'attribute_value_invalid_chars' => 'Atribut dəyəri bu \':\' və ya \'|\' olmaz',
|
||||
'confirm_delete' => 'Seçilmiş Atributları silmək istədiyinizdən əminsinizmi?',
|
||||
'confirm_restore' => 'Seçilmiş atributları bərpa etmək istədiyinizə əminsinizmi?',
|
||||
'definition_cannot_be_deleted' => 'Seçilmiş xüsusiyyətləri silmək olmadı',
|
||||
'definition_invalid_group' => '',
|
||||
'definition_error_adding_updating' => '{0} -in atributları əlavə oluna və yenilənə bilmədi. Lütfən XƏTA loq faylını yoxlayın.',
|
||||
'definition_flags' => 'Atribut görünüşü',
|
||||
'definition_group' => 'Qrup',
|
||||
'definition_id' => 'Id',
|
||||
'definition_name' => 'Atribut əlavə et',
|
||||
'definition_name_required' => 'Atribut adı zəruri bir sahədir',
|
||||
'definition_one_or_multiple' => 'atribut (lar)',
|
||||
'definition_successful_adding' => 'Malı uğurla əlavə etdiniz',
|
||||
'definition_successful_deleted' => 'Siz uğurla sildiniz',
|
||||
'definition_successful_updating' => 'Atributunuzu müvəffəqiyyətlə yenilədiniz',
|
||||
'definition_type' => 'Atribut Tipi',
|
||||
'definition_type_required' => 'Atribut növü tələb olunan sahədir',
|
||||
'definition_unit' => 'Ölçü vahidi',
|
||||
'definition_values' => 'Atribut dəyərləri',
|
||||
'new' => 'Yeni atributlar',
|
||||
'no_attributes_to_display' => 'Göstərmək üçün heç bir element yoxdur',
|
||||
'receipt_visibility' => 'Qəbz',
|
||||
'show_in_items' => 'Malları göstər',
|
||||
'show_in_items_visibility' => 'Mallar',
|
||||
'show_in_receipt' => 'Qəbzi göstərin',
|
||||
'show_in_receivings' => 'Alacaqlarda göstərin',
|
||||
'show_in_receivings_visibility' => 'Alınanlar',
|
||||
'show_in_sales' => 'Satışda göstərin',
|
||||
'show_in_sales_visibility' => 'Satışlar',
|
||||
'show_in_search' => 'Axtarışda göstər',
|
||||
'show_in_search_visibility' => 'Axtarış',
|
||||
'update' => 'Atributları yenilə',
|
||||
];
|
||||
@@ -210,7 +210,6 @@ return [
|
||||
'msg_msg' => 'Saxlanılan Mətn Mesajı',
|
||||
'msg_msg_placeholder' => 'SMS şablonunu istifadə etmək istəyirsinizsə, mesajınızı buraya qeyd edin, əks halda qutunu boş buraxın.',
|
||||
'msg_pwd' => 'SMS-API Şifrəsi',
|
||||
'msg_pwd_required' => 'SMS-API şifrəsi tələb olunan bir sahədir',
|
||||
'msg_src' => 'SMS-API Göndərici ID',
|
||||
'msg_src_required' => 'SMS-API Göndərici ID tələb olunan sahədir',
|
||||
'msg_uid' => 'SMS-API İstifadəçi adı',
|
||||
@@ -285,7 +284,7 @@ return [
|
||||
'right' => 'Sağ',
|
||||
'sales_invoice_format' => 'Satış Fatura Formatı',
|
||||
'sales_quote_format' => 'Satış Sitat Formati',
|
||||
'mailpath_invalid' => '',
|
||||
'mailpath_invalid' => 'Yanlış sendmail yolu. Yalnız hərflərə, rəqəmlərə, tirelərə, alt xətlərə, əyri xətlərə, tərs əyri xətlərə, iki nöqtəyə, boşluqlara və nöqtələrə icazə verilir.',
|
||||
'saved_successfully' => 'Konfiqurasiya uğurla saxlanıldı.',
|
||||
'saved_unsuccessfully' => 'Konfiqurasiyanı saxlamq mümkün olmadı.',
|
||||
'security_issue' => 'Təhlükəsizlik açığı xəbərdarlığı',
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"no_permission_module" => "sizin icazəniz yoxdur",
|
||||
"unknown" => "naməlum",
|
||||
'no_permission_module' => 'sizin icazəniz yoxdur',
|
||||
'unable_to_create_env_file' => 'throttle.key təmin etmək üçün {filePath} yaradıla bilmədi',
|
||||
'unable_to_lock_file' => '{filePath} kilidlənə bilmədi: {reason}',
|
||||
'unable_to_open_lock_file' => '{filePath} açıla bilmədi: {reason}',
|
||||
'unable_to_persist_throttle_key' => 'throttle.key {filePath} faylına yazıla bilmədi',
|
||||
'unable_to_persist_encryption_key' => '',
|
||||
'encryption_key_not_provisioned' => '',
|
||||
'throttle_key_not_provisioned' => '',
|
||||
'unable_to_read_env_file' => 'throttle.key təmin etmək üçün {filePath} oxuna bilmədi',
|
||||
'unknown' => 'naməlum',
|
||||
];
|
||||
@@ -1,34 +1,36 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"attribute_value_invalid_chars" => "",
|
||||
"confirm_delete" => "",
|
||||
"confirm_restore" => "",
|
||||
"definition_cannot_be_deleted" => "",
|
||||
"definition_invalid_group" => "",
|
||||
"definition_error_adding_updating" => "",
|
||||
"definition_flags" => "",
|
||||
"definition_group" => "",
|
||||
"definition_id" => "",
|
||||
"definition_name" => "",
|
||||
"definition_name_required" => "",
|
||||
"definition_one_or_multiple" => "",
|
||||
"definition_successful_adding" => "",
|
||||
"definition_successful_deleted" => "",
|
||||
"definition_successful_updating" => "",
|
||||
"definition_type" => "",
|
||||
"definition_type_required" => "",
|
||||
"definition_unit" => "",
|
||||
"definition_values" => "",
|
||||
"new" => "",
|
||||
"no_attributes_to_display" => "",
|
||||
"receipt_visibility" => "",
|
||||
"show_in_items" => "",
|
||||
"show_in_items_visibility" => "",
|
||||
"show_in_receipt" => "",
|
||||
"show_in_receivings" => "",
|
||||
"show_in_receivings_visibility" => "",
|
||||
"show_in_sales" => "",
|
||||
"show_in_sales_visibility" => "",
|
||||
"update" => "",
|
||||
'attribute_value_invalid_chars' => '',
|
||||
'confirm_delete' => '',
|
||||
'confirm_restore' => '',
|
||||
'definition_cannot_be_deleted' => '',
|
||||
'definition_invalid_group' => '',
|
||||
'definition_error_adding_updating' => '',
|
||||
'definition_flags' => '',
|
||||
'definition_group' => '',
|
||||
'definition_id' => '',
|
||||
'definition_name' => '',
|
||||
'definition_name_required' => '',
|
||||
'definition_one_or_multiple' => '',
|
||||
'definition_successful_adding' => '',
|
||||
'definition_successful_deleted' => '',
|
||||
'definition_successful_updating' => '',
|
||||
'definition_type' => '',
|
||||
'definition_type_required' => '',
|
||||
'definition_unit' => '',
|
||||
'definition_values' => '',
|
||||
'new' => '',
|
||||
'no_attributes_to_display' => '',
|
||||
'receipt_visibility' => '',
|
||||
'show_in_items' => '',
|
||||
'show_in_items_visibility' => '',
|
||||
'show_in_receipt' => '',
|
||||
'show_in_receivings' => '',
|
||||
'show_in_receivings_visibility' => '',
|
||||
'show_in_sales' => '',
|
||||
'show_in_sales_visibility' => '',
|
||||
'show_in_search' => 'Показване в търсенето',
|
||||
'show_in_search_visibility' => 'Търсене',
|
||||
'update' => '',
|
||||
];
|
||||
@@ -210,7 +210,6 @@ return [
|
||||
'msg_msg' => 'Saved Text Message',
|
||||
'msg_msg_placeholder' => 'If you wish to use a SMS template save your message here, otherwise leave the box blank.',
|
||||
'msg_pwd' => 'SMS-API Password',
|
||||
'msg_pwd_required' => 'SMS-API Password is a required field',
|
||||
'msg_src' => 'SMS-API Sender ID',
|
||||
'msg_src_required' => 'SMS-API Sender ID is a required field',
|
||||
'msg_uid' => 'SMS-API Username',
|
||||
@@ -285,7 +284,7 @@ return [
|
||||
'right' => 'Right',
|
||||
'sales_invoice_format' => 'Sales Invoice Format',
|
||||
'sales_quote_format' => 'Sales Quote Format',
|
||||
'mailpath_invalid' => '',
|
||||
'mailpath_invalid' => 'Невалиден път до sendmail. Разрешени са само букви, цифри, тирета, долни черти, наклонени черти, обратни наклонени черти, двоеточия, интервали и точки.',
|
||||
'saved_successfully' => 'Configuration save successful.',
|
||||
'saved_unsuccessfully' => 'Configuration save failed.',
|
||||
'security_issue' => 'Security Vulnerability Warning',
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"no_permission_module" => "Нямате разрешение за достъп до модула с име",
|
||||
"unknown" => "Неизвестна грешка",
|
||||
'no_permission_module' => 'Нямате разрешение за достъп до модула с име',
|
||||
'unable_to_create_env_file' => 'Неуспешно създаване на {filePath} за осигуряване на throttle.key',
|
||||
'unable_to_lock_file' => 'Неуспешно заключване на {filePath}: {reason}',
|
||||
'unable_to_open_lock_file' => 'Неуспешно отваряне на {filePath}: {reason}',
|
||||
'unable_to_persist_throttle_key' => 'Неуспешно записване на throttle.key в {filePath}',
|
||||
'unable_to_persist_encryption_key' => '',
|
||||
'encryption_key_not_provisioned' => '',
|
||||
'throttle_key_not_provisioned' => '',
|
||||
'unable_to_read_env_file' => 'Неуспешно четене на {filePath} за осигуряване на throttle.key',
|
||||
'unknown' => 'Неизвестна грешка',
|
||||
];
|
||||
@@ -128,6 +128,7 @@ return [
|
||||
"tax_rate" => "",
|
||||
"taxes" => "Taxes",
|
||||
"taxes_summary_report" => "Taxes Summary Report",
|
||||
"toggle_cost_and_profit" => "",
|
||||
"total" => "Total",
|
||||
"total_inventory_value" => "Total Inventory Value",
|
||||
"total_low_sell_quantity" => "",
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"attribute_value_invalid_chars" => "Vrijednost atributa ne može sadržavati ':' ili '|'",
|
||||
"confirm_delete" => "Da li ste sigurni da želite da izbrišete izabrani atribut?",
|
||||
"confirm_restore" => "Da li ste sigurni da želite vratiti izabrane atribute?",
|
||||
"definition_cannot_be_deleted" => "Nije moguće izbrisati izabrane atribut",
|
||||
"definition_invalid_group" => "",
|
||||
"definition_error_adding_updating" => "Atribut {0} nije moguće dodati ili ažurirati. Molimo provjerite dnevnik grešaka.",
|
||||
"definition_flags" => "Vidljivost atributa",
|
||||
"definition_group" => "Grupa",
|
||||
"definition_id" => "Id",
|
||||
"definition_name" => "Dodaj Atribut",
|
||||
"definition_name_required" => "Ime atributa je obavezno polje",
|
||||
"definition_one_or_multiple" => "atributi",
|
||||
"definition_successful_adding" => "Uspješno ste dodali stavku",
|
||||
"definition_successful_deleted" => "Uspješno ste izbrisali",
|
||||
"definition_successful_updating" => "Uspješno ste ažurirali atribute",
|
||||
"definition_type" => "Tip atributa",
|
||||
"definition_type_required" => "Tip atributa je obavezno polje",
|
||||
"definition_unit" => "Jedinice mjere",
|
||||
"definition_values" => "Vrijednosti atributa",
|
||||
"new" => "Novi atribut",
|
||||
"no_attributes_to_display" => "Nema stavki za prikaz",
|
||||
"receipt_visibility" => "Račun",
|
||||
"show_in_items" => "Prikaži u stavkama",
|
||||
"show_in_items_visibility" => "Stavka",
|
||||
"show_in_receipt" => "Prikaži u računu",
|
||||
"show_in_receivings" => "Prikaži na ulazima",
|
||||
"show_in_receivings_visibility" => "Ulazi",
|
||||
"show_in_sales" => "Prikaži u prodaji",
|
||||
"show_in_sales_visibility" => "Prodaja",
|
||||
"update" => "Ažuriraj atribut",
|
||||
'attribute_value_invalid_chars' => 'Vrijednost atributa ne može sadržavati \':\' ili \'|\'',
|
||||
'confirm_delete' => 'Da li ste sigurni da želite da izbrišete izabrani atribut?',
|
||||
'confirm_restore' => 'Da li ste sigurni da želite vratiti izabrane atribute?',
|
||||
'definition_cannot_be_deleted' => 'Nije moguće izbrisati izabrane atribut',
|
||||
'definition_invalid_group' => '',
|
||||
'definition_error_adding_updating' => 'Atribut {0} nije moguće dodati ili ažurirati. Molimo provjerite dnevnik grešaka.',
|
||||
'definition_flags' => 'Vidljivost atributa',
|
||||
'definition_group' => 'Grupa',
|
||||
'definition_id' => 'Id',
|
||||
'definition_name' => 'Dodaj Atribut',
|
||||
'definition_name_required' => 'Ime atributa je obavezno polje',
|
||||
'definition_one_or_multiple' => 'atributi',
|
||||
'definition_successful_adding' => 'Uspješno ste dodali stavku',
|
||||
'definition_successful_deleted' => 'Uspješno ste izbrisali',
|
||||
'definition_successful_updating' => 'Uspješno ste ažurirali atribute',
|
||||
'definition_type' => 'Tip atributa',
|
||||
'definition_type_required' => 'Tip atributa je obavezno polje',
|
||||
'definition_unit' => 'Jedinice mjere',
|
||||
'definition_values' => 'Vrijednosti atributa',
|
||||
'new' => 'Novi atribut',
|
||||
'no_attributes_to_display' => 'Nema stavki za prikaz',
|
||||
'receipt_visibility' => 'Račun',
|
||||
'show_in_items' => 'Prikaži u stavkama',
|
||||
'show_in_items_visibility' => 'Stavka',
|
||||
'show_in_receipt' => 'Prikaži u računu',
|
||||
'show_in_receivings' => 'Prikaži na ulazima',
|
||||
'show_in_receivings_visibility' => 'Ulazi',
|
||||
'show_in_sales' => 'Prikaži u prodaji',
|
||||
'show_in_sales_visibility' => 'Prodaja',
|
||||
'show_in_search' => 'Prikaži u pretrazi',
|
||||
'show_in_search_visibility' => 'Pretraga',
|
||||
'update' => 'Ažuriraj atribut',
|
||||
];
|
||||
@@ -210,7 +210,6 @@ return [
|
||||
'msg_msg' => 'Snimljena tekst poruka',
|
||||
'msg_msg_placeholder' => 'Ako želite koristiti SMS šablon, snimite poruku ovdje. U suprotnom ostavite prazno polje.',
|
||||
'msg_pwd' => 'SMS-API lozinke',
|
||||
'msg_pwd_required' => 'SMS-API lozinke je obavezno polje',
|
||||
'msg_src' => 'SMS-API ID pošiljaoca',
|
||||
'msg_src_required' => 'SMS-API Id pošiljaoca je obavezno polje',
|
||||
'msg_uid' => 'SMS-API korisnika',
|
||||
@@ -285,7 +284,7 @@ return [
|
||||
'right' => 'Desno',
|
||||
'sales_invoice_format' => 'Format fakture',
|
||||
'sales_quote_format' => 'Format navedene prodaje',
|
||||
'mailpath_invalid' => '',
|
||||
'mailpath_invalid' => 'Nevažeća sendmail putanja. Dozvoljena su samo slova, brojevi, crtice, donje crte, kose crte, obrnute kose crte, dvotačke, razmaci i tačke.',
|
||||
'saved_successfully' => 'Konfiguracija je uspješno snimljena.',
|
||||
'saved_unsuccessfully' => 'Konfiguracija nije uspješno snimljena.',
|
||||
'security_issue' => 'Upozorenje o sigurnosnoj ranjivosti',
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"no_permission_module" => "Nemate dozvolu za pristup modulu",
|
||||
"unknown" => "Neočekivana greška",
|
||||
'no_permission_module' => 'Nemate dozvolu za pristup modulu',
|
||||
'unable_to_create_env_file' => 'Nije moguće kreirati {filePath} za obezbjeđivanje throttle.key',
|
||||
'unable_to_lock_file' => 'Nije moguće zaključati {filePath}: {reason}',
|
||||
'unable_to_open_lock_file' => 'Nije moguće otvoriti {filePath}: {reason}',
|
||||
'unable_to_persist_throttle_key' => 'Nije moguće trajno sačuvati throttle.key u {filePath}',
|
||||
'unable_to_persist_encryption_key' => '',
|
||||
'encryption_key_not_provisioned' => '',
|
||||
'throttle_key_not_provisioned' => '',
|
||||
'unable_to_read_env_file' => 'Nije moguće pročitati {filePath} za obezbjeđivanje throttle.key',
|
||||
'unknown' => 'Neočekivana greška',
|
||||
];
|
||||
@@ -128,6 +128,7 @@ return [
|
||||
"tax_rate" => "Poreska stopa",
|
||||
"taxes" => "Porezi",
|
||||
"taxes_summary_report" => "Zbirni izvještaj poreza",
|
||||
"toggle_cost_and_profit" => "",
|
||||
"total" => "Ukupno",
|
||||
"total_inventory_value" => "Ukupan iznos zalihe",
|
||||
"total_low_sell_quantity" => "Ukupno količina niskih prodaja",
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"attribute_value_invalid_chars" => "بەهای تایبەتمەندیی ناتوانێت '_' یان ' لەخۆبگرێت|'",
|
||||
"confirm_delete" => "ئایا دڵنیای کە دەتەوێت تایبەتمەندییە هەڵبژێردراوەکە(کان) بسڕیتەوە؟",
|
||||
"confirm_restore" => "ئایا دڵنیای کە دەتەوێت تایبەتمەندییە هەڵبژێردراوەکە(کان) بگەڕێنیتەوە؟",
|
||||
"definition_cannot_be_deleted" => "نەتوانرا تایبەتمەندی هەڵبژێردراو بسڕدرێتەوە",
|
||||
"definition_invalid_group" => "",
|
||||
"definition_error_adding_updating" => "تایبەتمەندی {0} نەتوانرا زیاد بکرێت یان نوێ بکرێتەوە. تکایە لیستی هەڵەکان بپشکنە.",
|
||||
"definition_flags" => "توانای بینراویی تایبەتمەندی",
|
||||
"definition_group" => "گروپ",
|
||||
"definition_id" => "ناسنامە",
|
||||
"definition_name" => "تایبەتمەندی زیاد بکە",
|
||||
"definition_name_required" => "ناوی تایبەتمەندی خانەیەکی پێویستە",
|
||||
"definition_one_or_multiple" => "تایبەتمەندی(ەکان)",
|
||||
"definition_successful_adding" => "بەسەرکەوتوویی ئایتمەکەت زیادکرد",
|
||||
"definition_successful_deleted" => "بەسەرکەوتوویی سڕیوتەتەوە",
|
||||
"definition_successful_updating" => "بەسەرکەوتوویی تایبەتمەندیت نوێ کردووەتەوە",
|
||||
"definition_type" => "جۆری تایبەتمەندی",
|
||||
"definition_type_required" => "جۆری تایبەتمەندی خانەیەکی پێویستە",
|
||||
"definition_unit" => "یەکەی پێوانەکردن",
|
||||
"definition_values" => "بەهاکانی تایبەتمەندی",
|
||||
"new" => "تایبەتمەندی نوێ",
|
||||
"no_attributes_to_display" => "هیچ تایبەتمەندییەک نییە بۆ پیشاندان",
|
||||
"receipt_visibility" => "ڕەچەتە",
|
||||
"show_in_items" => "لەناو ئایتمەکان نیشانی بدە",
|
||||
"show_in_items_visibility" => "ئایتمەکان",
|
||||
"show_in_receipt" => "لە ڕەچەتەکەدا نیشانی بدە",
|
||||
"show_in_receivings" => "لە بەدەستگەیشتووەکاندا نیشانی بدە",
|
||||
"show_in_receivings_visibility" => "بەدەستگەیشتووکان",
|
||||
"show_in_sales" => "لە فرۆشتندا نیشانی بدە",
|
||||
"show_in_sales_visibility" => "فرۆشتن",
|
||||
"update" => "تایبەتمەندی نوێ بکەرەوە",
|
||||
'attribute_value_invalid_chars' => 'بەهای تایبەتمەندیی ناتوانێت \'_\' یان \' لەخۆبگرێت|\'',
|
||||
'confirm_delete' => 'ئایا دڵنیای کە دەتەوێت تایبەتمەندییە هەڵبژێردراوەکە(کان) بسڕیتەوە؟',
|
||||
'confirm_restore' => 'ئایا دڵنیای کە دەتەوێت تایبەتمەندییە هەڵبژێردراوەکە(کان) بگەڕێنیتەوە؟',
|
||||
'definition_cannot_be_deleted' => 'نەتوانرا تایبەتمەندی هەڵبژێردراو بسڕدرێتەوە',
|
||||
'definition_invalid_group' => '',
|
||||
'definition_error_adding_updating' => 'تایبەتمەندی {0} نەتوانرا زیاد بکرێت یان نوێ بکرێتەوە. تکایە لیستی هەڵەکان بپشکنە.',
|
||||
'definition_flags' => 'توانای بینراویی تایبەتمەندی',
|
||||
'definition_group' => 'گروپ',
|
||||
'definition_id' => 'ناسنامە',
|
||||
'definition_name' => 'تایبەتمەندی زیاد بکە',
|
||||
'definition_name_required' => 'ناوی تایبەتمەندی خانەیەکی پێویستە',
|
||||
'definition_one_or_multiple' => 'تایبەتمەندی(ەکان)',
|
||||
'definition_successful_adding' => 'بەسەرکەوتوویی ئایتمەکەت زیادکرد',
|
||||
'definition_successful_deleted' => 'بەسەرکەوتوویی سڕیوتەتەوە',
|
||||
'definition_successful_updating' => 'بەسەرکەوتوویی تایبەتمەندیت نوێ کردووەتەوە',
|
||||
'definition_type' => 'جۆری تایبەتمەندی',
|
||||
'definition_type_required' => 'جۆری تایبەتمەندی خانەیەکی پێویستە',
|
||||
'definition_unit' => 'یەکەی پێوانەکردن',
|
||||
'definition_values' => 'بەهاکانی تایبەتمەندی',
|
||||
'new' => 'تایبەتمەندی نوێ',
|
||||
'no_attributes_to_display' => 'هیچ تایبەتمەندییەک نییە بۆ پیشاندان',
|
||||
'receipt_visibility' => 'ڕەچەتە',
|
||||
'show_in_items' => 'لەناو ئایتمەکان نیشانی بدە',
|
||||
'show_in_items_visibility' => 'ئایتمەکان',
|
||||
'show_in_receipt' => 'لە ڕەچەتەکەدا نیشانی بدە',
|
||||
'show_in_receivings' => 'لە بەدەستگەیشتووەکاندا نیشانی بدە',
|
||||
'show_in_receivings_visibility' => 'بەدەستگەیشتووکان',
|
||||
'show_in_sales' => 'لە فرۆشتندا نیشانی بدە',
|
||||
'show_in_sales_visibility' => 'فرۆشتن',
|
||||
'show_in_search' => 'لە گەڕاندا نیشانی بدە',
|
||||
'show_in_search_visibility' => 'گەڕان',
|
||||
'update' => 'تایبەتمەندی نوێ بکەرەوە',
|
||||
];
|
||||
@@ -210,7 +210,6 @@ return [
|
||||
'msg_msg' => 'دەقی نامەی پاشەکەوتکراو',
|
||||
'msg_msg_placeholder' => 'ئەگەر دەتەوێت تێمپڵەیتی کورتەنامە بەکاربهێنیت ئەوا نامەکەت لێرەدا پاشەکەوت بکە، ئەگەرنا خانەکە بە بەتاڵی بهێڵەرەوە.',
|
||||
'msg_pwd' => 'وشەی نهێنی کورتەنامە-ئەی پی ئای',
|
||||
'msg_pwd_required' => 'خانەی وشەی نهێنی کورتەنامە-ئەی پی ئای پێویستە',
|
||||
'msg_src' => 'ناسنامەی نێردەری کورتەنامە-ئەی پی ئای',
|
||||
'msg_src_required' => 'خانەی ناسنامەی کورتەنامە-ئەی پی ئای پێویستە',
|
||||
'msg_uid' => 'ناوی بەکارهێنەری کورتەنامە-ئەی پی ئای',
|
||||
@@ -285,6 +284,7 @@ return [
|
||||
'right' => 'ڕاست',
|
||||
'sales_invoice_format' => 'فۆڕماتی فاکتورەی فرۆشتن',
|
||||
'sales_quote_format' => 'فۆڕماتی دەرخستەی نرخەکانی فرۆشتن',
|
||||
'mailpath_invalid' => 'ڕێچکەی sendmail نادروستە. تەنها پیت، ژمارە، هێڵی بەستەرەوە، هێڵی ژێرەوە، سلاشی ڕاست، سلاشی چەپ، دوو خاڵ، بۆشایی و خاڵ ڕێگەپێدراون.',
|
||||
'saved_successfully' => 'پاشەکەوتکردنی ڕێکخستن سەرکەوتوو بوو.',
|
||||
'saved_unsuccessfully' => 'پاشەکەوتکردنی ڕێکخستن سەرکەوتوو نەبوو.',
|
||||
'security_issue' => 'ئاگادارکردنەوەی لاوازی ئاسایش',
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"no_permission_module" => "تۆ ڕێگەپێدانت نییە بۆ دەستگەیشتن بەو مۆدیولەی کە ناوی لێنراوە",
|
||||
"unknown" => "هەڵەیەکی چاوەڕواننەکراو",
|
||||
'no_permission_module' => 'تۆ ڕێگەپێدانت نییە بۆ دەستگەیشتن بەو مۆدیولەی کە ناوی لێنراوە',
|
||||
'unable_to_create_env_file' => 'نەتوانرا {filePath} دروست بکرێت بۆ دابینکردنی throttle.key',
|
||||
'unable_to_lock_file' => 'نەتوانرا {filePath} قوفڵ بکرێت: {reason}',
|
||||
'unable_to_open_lock_file' => 'نەتوانرا {filePath} کرایەوە: {reason}',
|
||||
'unable_to_persist_throttle_key' => 'نەتوانرا throttle.key لە {filePath} پاشەکەوت بکرێت',
|
||||
'unable_to_persist_encryption_key' => '',
|
||||
'encryption_key_not_provisioned' => '',
|
||||
'throttle_key_not_provisioned' => '',
|
||||
'unable_to_read_env_file' => 'نەتوانرا {filePath} بخوێنرێتەوە بۆ دابینکردنی throttle.key',
|
||||
'unknown' => 'هەڵەیەکی چاوەڕواننەکراو',
|
||||
];
|
||||
@@ -128,6 +128,7 @@ return [
|
||||
'tax_rate' => "ڕێژەی باج",
|
||||
'taxes' => "باجەکان",
|
||||
'taxes_summary_report' => "ڕاپۆرتی پوختەی باجەکان",
|
||||
'toggle_cost_and_profit' => "",
|
||||
'total' => "گشتی",
|
||||
'total_inventory_value' => "کۆی بەهای جەرد",
|
||||
'total_low_sell_quantity' => "کۆی گشتی بڕی فرۆشتنی کەم",
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"attribute_value_invalid_chars" => "",
|
||||
"confirm_delete" => "",
|
||||
"confirm_restore" => "",
|
||||
"definition_cannot_be_deleted" => "",
|
||||
"definition_invalid_group" => "",
|
||||
"definition_error_adding_updating" => "",
|
||||
"definition_flags" => "",
|
||||
"definition_group" => "",
|
||||
"definition_id" => "",
|
||||
"definition_name" => "",
|
||||
"definition_name_required" => "",
|
||||
"definition_one_or_multiple" => "",
|
||||
"definition_successful_adding" => "",
|
||||
"definition_successful_deleted" => "",
|
||||
"definition_successful_updating" => "",
|
||||
"definition_type" => "",
|
||||
"definition_type_required" => "",
|
||||
"definition_unit" => "",
|
||||
"definition_values" => "",
|
||||
"new" => "",
|
||||
"no_attributes_to_display" => "",
|
||||
"receipt_visibility" => "",
|
||||
"show_in_items" => "",
|
||||
"show_in_items_visibility" => "",
|
||||
"show_in_receipt" => "",
|
||||
"show_in_receivings" => "",
|
||||
"show_in_receivings_visibility" => "",
|
||||
"show_in_sales" => "",
|
||||
"show_in_sales_visibility" => "",
|
||||
"update" => "",
|
||||
'attribute_value_invalid_chars' => '',
|
||||
'confirm_delete' => '',
|
||||
'confirm_restore' => '',
|
||||
'definition_cannot_be_deleted' => '',
|
||||
'definition_invalid_group' => '',
|
||||
'definition_error_adding_updating' => '',
|
||||
'definition_flags' => '',
|
||||
'definition_group' => '',
|
||||
'definition_id' => '',
|
||||
'definition_name' => '',
|
||||
'definition_name_required' => '',
|
||||
'definition_one_or_multiple' => '',
|
||||
'definition_successful_adding' => '',
|
||||
'definition_successful_deleted' => '',
|
||||
'definition_successful_updating' => '',
|
||||
'definition_type' => '',
|
||||
'definition_type_required' => '',
|
||||
'definition_unit' => '',
|
||||
'definition_values' => '',
|
||||
'new' => '',
|
||||
'no_attributes_to_display' => '',
|
||||
'receipt_visibility' => '',
|
||||
'show_in_items' => '',
|
||||
'show_in_items_visibility' => '',
|
||||
'show_in_receipt' => '',
|
||||
'show_in_receivings' => '',
|
||||
'show_in_receivings_visibility' => '',
|
||||
'show_in_sales' => '',
|
||||
'show_in_sales_visibility' => '',
|
||||
'show_in_search' => 'Zobrazit ve vyhledávání',
|
||||
'show_in_search_visibility' => 'Vyhledávání',
|
||||
'update' => '',
|
||||
];
|
||||
@@ -210,7 +210,6 @@ return [
|
||||
'msg_msg' => '',
|
||||
'msg_msg_placeholder' => '',
|
||||
'msg_pwd' => '',
|
||||
'msg_pwd_required' => '',
|
||||
'msg_src' => '',
|
||||
'msg_src_required' => '',
|
||||
'msg_uid' => '',
|
||||
@@ -285,7 +284,7 @@ return [
|
||||
'right' => '',
|
||||
'sales_invoice_format' => '',
|
||||
'sales_quote_format' => '',
|
||||
'mailpath_invalid' => '',
|
||||
'mailpath_invalid' => 'Neplatná cesta k sendmailu. Povolena jsou pouze písmena, číslice, pomlčky, podtržítka, lomítka, zpětná lomítka, dvojtečky, mezery a tečky.',
|
||||
'saved_successfully' => '',
|
||||
'saved_unsuccessfully' => '',
|
||||
'security_issue' => 'Security Vulnerability Warning',
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"no_permission_module" => "",
|
||||
"unknown" => "",
|
||||
'no_permission_module' => 'Nemáte oprávnění přistupovat k modulu s názvem',
|
||||
'unable_to_create_env_file' => 'Nelze vytvořit {filePath} pro zajištění throttle.key',
|
||||
'unable_to_lock_file' => 'Nelze uzamknout {filePath}: {reason}',
|
||||
'unable_to_open_lock_file' => 'Nelze otevřít {filePath}: {reason}',
|
||||
'unable_to_persist_throttle_key' => 'Nelze trvale uložit throttle.key do {filePath}',
|
||||
'unable_to_persist_encryption_key' => '',
|
||||
'encryption_key_not_provisioned' => '',
|
||||
'throttle_key_not_provisioned' => '',
|
||||
'unable_to_read_env_file' => 'Nelze přečíst {filePath} pro zajištění throttle.key',
|
||||
'unknown' => 'Neočekávaná chyba',
|
||||
];
|
||||
@@ -128,6 +128,7 @@ return [
|
||||
"tax_rate" => "",
|
||||
"taxes" => "Daně",
|
||||
"taxes_summary_report" => "Přehled podle DPH",
|
||||
"toggle_cost_and_profit" => "",
|
||||
"total" => "Celkem",
|
||||
"total_inventory_value" => "Celková cena skladu",
|
||||
"total_low_sell_quantity" => "",
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"attribute_value_invalid_chars" => "Egenskabens værdi kan ikke indeholde ':' or '|'",
|
||||
"confirm_delete" => "Er du sikker på, at du vil slette de valgte egenskaber?",
|
||||
"confirm_restore" => "Er du sikker på, at du vil gendanne de valgte egenskaber?",
|
||||
"definition_cannot_be_deleted" => "De valgte egenskaber kunne ikke slettes",
|
||||
"definition_invalid_group" => "Den valgte gruppe findes ikke eller er ugyldig.",
|
||||
"definition_error_adding_updating" => "Egenskab {0} Kunne ikke tilføjes eller opdateres. Tjek venligst fejlprotokollen.",
|
||||
"definition_flags" => "Egenskabens Synlighed",
|
||||
"definition_group" => "Gruppe",
|
||||
"definition_id" => "Id",
|
||||
"definition_name" => "Tilføj egenskab",
|
||||
"definition_name_required" => "Egenskabens navn er et obligatorisk felt",
|
||||
"definition_one_or_multiple" => "Egenskab(er)",
|
||||
"definition_successful_adding" => "Du har tilføjet en genstand",
|
||||
"definition_successful_deleted" => "Du har slettet en genstand",
|
||||
"definition_successful_updating" => "Du har opdateret en egenskab",
|
||||
"definition_type" => "Egenskabs-type",
|
||||
"definition_type_required" => "Egenskabs-type er et obligatorisk felt",
|
||||
"definition_unit" => "Måleenhed",
|
||||
"definition_values" => "Egenskabens værdier",
|
||||
"new" => "Ny egenskab",
|
||||
"no_attributes_to_display" => "Ingen genstande at vise",
|
||||
"receipt_visibility" => "Kvittering",
|
||||
"show_in_items" => "Vis i genstande",
|
||||
"show_in_items_visibility" => "Genstande",
|
||||
"show_in_receipt" => "Vis i kvittering",
|
||||
"show_in_receivings" => "Vis i modtagelser",
|
||||
"show_in_receivings_visibility" => "Modtagelser",
|
||||
"show_in_sales" => "Vis i salg",
|
||||
"show_in_sales_visibility" => "Salg",
|
||||
"update" => "Opdater egenskab",
|
||||
'attribute_value_invalid_chars' => 'Egenskabens værdi kan ikke indeholde \':\' or \'|\'',
|
||||
'confirm_delete' => 'Er du sikker på, at du vil slette de valgte egenskaber?',
|
||||
'confirm_restore' => 'Er du sikker på, at du vil gendanne de valgte egenskaber?',
|
||||
'definition_cannot_be_deleted' => 'De valgte egenskaber kunne ikke slettes',
|
||||
'definition_invalid_group' => 'Den valgte gruppe findes ikke eller er ugyldig.',
|
||||
'definition_error_adding_updating' => 'Egenskab {0} Kunne ikke tilføjes eller opdateres. Tjek venligst fejlprotokollen.',
|
||||
'definition_flags' => 'Egenskabens Synlighed',
|
||||
'definition_group' => 'Gruppe',
|
||||
'definition_id' => 'Id',
|
||||
'definition_name' => 'Tilføj egenskab',
|
||||
'definition_name_required' => 'Egenskabens navn er et obligatorisk felt',
|
||||
'definition_one_or_multiple' => 'Egenskab(er)',
|
||||
'definition_successful_adding' => 'Du har tilføjet en genstand',
|
||||
'definition_successful_deleted' => 'Du har slettet en genstand',
|
||||
'definition_successful_updating' => 'Du har opdateret en egenskab',
|
||||
'definition_type' => 'Egenskabs-type',
|
||||
'definition_type_required' => 'Egenskabs-type er et obligatorisk felt',
|
||||
'definition_unit' => 'Måleenhed',
|
||||
'definition_values' => 'Egenskabens værdier',
|
||||
'new' => 'Ny egenskab',
|
||||
'no_attributes_to_display' => 'Ingen genstande at vise',
|
||||
'receipt_visibility' => 'Kvittering',
|
||||
'show_in_items' => 'Vis i genstande',
|
||||
'show_in_items_visibility' => 'Genstande',
|
||||
'show_in_receipt' => 'Vis i kvittering',
|
||||
'show_in_receivings' => 'Vis i modtagelser',
|
||||
'show_in_receivings_visibility' => 'Modtagelser',
|
||||
'show_in_sales' => 'Vis i salg',
|
||||
'show_in_sales_visibility' => 'Salg',
|
||||
'show_in_search' => 'Vis i søgning',
|
||||
'show_in_search_visibility' => 'Søgning',
|
||||
'update' => 'Opdater egenskab',
|
||||
];
|
||||
@@ -210,7 +210,6 @@ return [
|
||||
'msg_msg' => 'Saved Text Message',
|
||||
'msg_msg_placeholder' => 'If you wish to use a SMS template save your message here, otherwise leave the box blank.',
|
||||
'msg_pwd' => 'SMS-API Password',
|
||||
'msg_pwd_required' => 'SMS-API Password is a required field',
|
||||
'msg_src' => 'SMS-API Sender ID',
|
||||
'msg_src_required' => 'SMS-API Sender ID is a required field',
|
||||
'msg_uid' => 'SMS-API Username',
|
||||
@@ -237,8 +236,8 @@ return [
|
||||
'print_bottom_margin_number' => 'Margin Bottom must be a number.',
|
||||
'print_bottom_margin_required' => 'Margin Bottom is a required field.',
|
||||
'print_delay_autoreturn' => 'Autoreturn to Sale delay',
|
||||
'print_delay_autoreturn_number' => 'Autoreturn to Sale delay is a required field.',
|
||||
'print_delay_autoreturn_required' => 'Autoreturn to Sale delay must be a number.',
|
||||
'print_delay_autoreturn_number' => '',
|
||||
'print_delay_autoreturn_required' => '',
|
||||
'print_footer' => 'Print Browser Footer',
|
||||
'print_header' => 'Print Browser Header',
|
||||
'print_left_margin' => 'Margin Left',
|
||||
@@ -285,7 +284,7 @@ return [
|
||||
'right' => 'Right',
|
||||
'sales_invoice_format' => 'Sales Invoice Format',
|
||||
'sales_quote_format' => 'Sales Quote Format',
|
||||
'mailpath_invalid' => '',
|
||||
'mailpath_invalid' => 'Ugyldig sendmail-sti. Kun bogstaver, tal, bindestreger, understregninger, skråstreger, omvendte skråstreger, kolon, mellemrum og punktummer er tilladt.',
|
||||
'saved_successfully' => 'Configuration save successful.',
|
||||
'saved_unsuccessfully' => 'Configuration save failed.',
|
||||
'security_issue' => 'Security Vulnerability Warning',
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"no_permission_module" => "You do not have permission to access the module named",
|
||||
"unknown" => "Unexpected error",
|
||||
'no_permission_module' => 'Du har ikke tilladelse til at tilgå det navngivne modul',
|
||||
'unable_to_create_env_file' => 'Kunne ikke oprette {filePath} for at klargøre throttle.key',
|
||||
'unable_to_lock_file' => 'Kunne ikke låse {filePath}: {reason}',
|
||||
'unable_to_open_lock_file' => 'Kunne ikke åbne {filePath}: {reason}',
|
||||
'unable_to_persist_throttle_key' => 'Kunne ikke gemme throttle.key i {filePath}',
|
||||
'unable_to_persist_encryption_key' => '',
|
||||
'encryption_key_not_provisioned' => '',
|
||||
'throttle_key_not_provisioned' => '',
|
||||
'unable_to_read_env_file' => 'Kunne ikke læse {filePath} for at klargøre throttle.key',
|
||||
'unknown' => 'Uventet fejl',
|
||||
];
|
||||
@@ -128,6 +128,7 @@ return [
|
||||
"tax_rate" => "",
|
||||
"taxes" => "",
|
||||
"taxes_summary_report" => "",
|
||||
"toggle_cost_and_profit" => "",
|
||||
"total" => "",
|
||||
"total_inventory_value" => "",
|
||||
"total_low_sell_quantity" => "",
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"attribute_value_invalid_chars" => "",
|
||||
"confirm_delete" => "",
|
||||
"confirm_restore" => "",
|
||||
"definition_cannot_be_deleted" => "",
|
||||
"definition_invalid_group" => "Die ausgewählte Gruppe existiert nicht oder ist ungültig.",
|
||||
"definition_error_adding_updating" => "",
|
||||
"definition_flags" => "",
|
||||
"definition_group" => "",
|
||||
"definition_id" => "",
|
||||
"definition_name" => "",
|
||||
"definition_name_required" => "",
|
||||
"definition_one_or_multiple" => "",
|
||||
"definition_successful_adding" => "",
|
||||
"definition_successful_deleted" => "",
|
||||
"definition_successful_updating" => "",
|
||||
"definition_type" => "",
|
||||
"definition_type_required" => "",
|
||||
"definition_unit" => "",
|
||||
"definition_values" => "",
|
||||
"new" => "",
|
||||
"no_attributes_to_display" => "",
|
||||
"receipt_visibility" => "",
|
||||
"show_in_items" => "",
|
||||
"show_in_items_visibility" => "",
|
||||
"show_in_receipt" => "",
|
||||
"show_in_receivings" => "",
|
||||
"show_in_receivings_visibility" => "",
|
||||
"show_in_sales" => "",
|
||||
"show_in_sales_visibility" => "",
|
||||
"update" => "",
|
||||
'attribute_value_invalid_chars' => '',
|
||||
'confirm_delete' => '',
|
||||
'confirm_restore' => '',
|
||||
'definition_cannot_be_deleted' => '',
|
||||
'definition_invalid_group' => 'Die ausgewählte Gruppe existiert nicht oder ist ungültig.',
|
||||
'definition_error_adding_updating' => '',
|
||||
'definition_flags' => '',
|
||||
'definition_group' => '',
|
||||
'definition_id' => '',
|
||||
'definition_name' => '',
|
||||
'definition_name_required' => '',
|
||||
'definition_one_or_multiple' => '',
|
||||
'definition_successful_adding' => '',
|
||||
'definition_successful_deleted' => '',
|
||||
'definition_successful_updating' => '',
|
||||
'definition_type' => '',
|
||||
'definition_type_required' => '',
|
||||
'definition_unit' => '',
|
||||
'definition_values' => '',
|
||||
'new' => '',
|
||||
'no_attributes_to_display' => '',
|
||||
'receipt_visibility' => '',
|
||||
'show_in_items' => '',
|
||||
'show_in_items_visibility' => '',
|
||||
'show_in_receipt' => '',
|
||||
'show_in_receivings' => '',
|
||||
'show_in_receivings_visibility' => '',
|
||||
'show_in_sales' => '',
|
||||
'show_in_sales_visibility' => '',
|
||||
'show_in_search' => 'In Suche anzeigen',
|
||||
'show_in_search_visibility' => 'Suche',
|
||||
'update' => '',
|
||||
];
|
||||
@@ -210,7 +210,6 @@ return [
|
||||
'msg_msg' => 'Saved Text Message',
|
||||
'msg_msg_placeholder' => 'If you wish to use a SMS template save your message here. Otherwise leave the box blank.',
|
||||
'msg_pwd' => 'SMS-API Password',
|
||||
'msg_pwd_required' => 'SMS-API Password is a required field',
|
||||
'msg_src' => 'SMS-API Sender ID',
|
||||
'msg_src_required' => 'SMS-API Sender ID is a required field',
|
||||
'msg_uid' => 'SMS-API Username',
|
||||
@@ -285,7 +284,7 @@ return [
|
||||
'right' => 'Right',
|
||||
'sales_invoice_format' => 'Format Verkaufsrechnung',
|
||||
'sales_quote_format' => '',
|
||||
'mailpath_invalid' => 'Ungültiger Sendmail-Pfad. Nur Buchstaben, Zahlen, Bindestriche, Unterstriche, Schrägstriche und Punkte sind erlaubt.',
|
||||
'mailpath_invalid' => 'Ungültiger Sendmail-Pfad. Nur Buchstaben, Zahlen, Bindestriche, Unterstriche, Schrägstriche, Rückwärtsschrägstriche, Doppelpunkte, Leerzeichen und Punkte sind erlaubt.',
|
||||
'saved_successfully' => 'Einstellungen erfolgreich gesichert',
|
||||
'saved_unsuccessfully' => 'Einstellungen konnten nicht gesichert werden',
|
||||
'security_issue' => 'Security Vulnerability Warning',
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"no_permission_module" => "Sie haben nicht die Zugangsrechte für das gewählte Modul",
|
||||
"unknown" => "Unbekannter Fehler",
|
||||
'no_permission_module' => 'Sie haben nicht die Zugangsrechte für das gewählte Modul',
|
||||
'unable_to_create_env_file' => '{filePath} konnte nicht erstellt werden, um throttle.key bereitzustellen',
|
||||
'unable_to_lock_file' => '{filePath} konnte nicht gesperrt werden: {reason}',
|
||||
'unable_to_open_lock_file' => '{filePath} konnte nicht geöffnet werden: {reason}',
|
||||
'unable_to_persist_throttle_key' => 'throttle.key konnte nicht in {filePath} gespeichert werden',
|
||||
'unable_to_persist_encryption_key' => '',
|
||||
'encryption_key_not_provisioned' => '',
|
||||
'throttle_key_not_provisioned' => '',
|
||||
'unable_to_read_env_file' => '{filePath} konnte nicht gelesen werden, um throttle.key bereitzustellen',
|
||||
'unknown' => 'Unbekannter Fehler',
|
||||
];
|
||||
@@ -16,9 +16,9 @@ return [
|
||||
'change_all_to_not_allow_allow_desc' => 'Ändere alle zu NICHT Erlaube Alt. Bez.',
|
||||
'change_all_to_serialized' => 'Ändere alle zu serialisiert',
|
||||
'change_all_to_unserialized' => 'Ändere alle zu nicht serialisiert',
|
||||
'change_image' => 'Change Image',
|
||||
'change_image' => 'Bild ändern',
|
||||
'confirm_bulk_edit' => 'Wollen Sie alle gewählten Artikel ändern?',
|
||||
'confirm_bulk_edit_wipe_taxes' => 'All item tax information will be replaced!',
|
||||
'confirm_bulk_edit_wipe_taxes' => 'Alle Artikel-Steuerinformationen werden ersetzt.',
|
||||
'confirm_delete' => 'Wollen Sie alle gewählten Artikel löschen?',
|
||||
'confirm_restore' => '',
|
||||
'cost_price' => 'Einstandspreis',
|
||||
@@ -27,9 +27,9 @@ return [
|
||||
'count' => 'Ändere Bestand',
|
||||
'csv_import_failed' => 'CSV Import fehlerhaft',
|
||||
'csv_import_invalid_location' => '',
|
||||
'csv_import_nodata_wrongformat' => 'Your uploaded file has no data or wrong format',
|
||||
'csv_import_partially_failed' => 'Most Items imported. But some were not, here is the list',
|
||||
'csv_import_success' => 'Import of Items successful',
|
||||
'csv_import_nodata_wrongformat' => 'Die hochgeladene Datei enthält keine Daten oder ist falsch formatiert.',
|
||||
'csv_import_partially_failed' => 'Bei {0} Artikelimporten sind in den Zeilen {1} Fehler aufgetreten. Es wurden keine Zeilen importiert.',
|
||||
'csv_import_success' => 'Artikelimport erfolgreich.',
|
||||
'current_quantity' => 'Aktuelle Menge',
|
||||
'default_pack_name' => '',
|
||||
'description' => 'Bezeichnung',
|
||||
@@ -45,7 +45,7 @@ return [
|
||||
'hsn_code' => '',
|
||||
'image' => 'Bild',
|
||||
'import_items_csv' => 'Importiere Artikel mit CSV Datei',
|
||||
'info_provided_by' => 'Info provided by',
|
||||
'info_provided_by' => 'Informationen bereitgestellt von',
|
||||
'inventory' => 'Lagerbestand',
|
||||
'inventory_CSV_import_quantity' => '',
|
||||
'inventory_comments' => 'Bemerkungen',
|
||||
@@ -85,7 +85,7 @@ return [
|
||||
'quantity_number' => 'Menge muss eine Zahl sein',
|
||||
'quantity_required' => 'Menge ist erforderlich',
|
||||
'receiving_quantity' => 'Eingangsmenge',
|
||||
'remove_image' => 'Remove Image',
|
||||
'remove_image' => 'Bild löschen',
|
||||
'reorder_level' => 'Mindestbestand',
|
||||
'reorder_level_number' => 'Mindestbestand muss eine Zahl sein',
|
||||
'reorder_level_required' => 'Mindestbestand ist erforderlich',
|
||||
@@ -93,7 +93,7 @@ return [
|
||||
'sales_tax_1' => 'Umsatzsteuer 1',
|
||||
'sales_tax_2' => 'Umsatzsteuer 2',
|
||||
'search_attributes' => 'Suche in Zusatzfeldern',
|
||||
'select_image' => 'Select Image',
|
||||
'select_image' => 'Bild auswählen',
|
||||
'serialized_items' => 'Serialisierte Artikel',
|
||||
'standard' => '',
|
||||
'stock' => '',
|
||||
|
||||
@@ -128,6 +128,7 @@ return [
|
||||
"tax_rate" => "",
|
||||
"taxes" => "Steuern",
|
||||
"taxes_summary_report" => "Bericht: Steuern (summarisch)",
|
||||
"toggle_cost_and_profit" => "Kosten & Gewinn umschalten",
|
||||
"total" => "Total",
|
||||
"total_inventory_value" => "Total Inventarwert",
|
||||
"total_low_sell_quantity" => "",
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"attribute_value_invalid_chars" => "Attributwert darf nicht ':' oder '|' enthalten",
|
||||
"confirm_delete" => "Sind Sie sicher, dass Sie die ausgewählten Attribute löschen möchten?",
|
||||
"confirm_restore" => "Sind Sie sicher, dass Sie die ausgewählten Attribute wiederherstellen möchten?",
|
||||
"definition_cannot_be_deleted" => "Ausgewählte Attribute konnten nicht gelöscht werden",
|
||||
"definition_invalid_group" => "Die ausgewählte Gruppe existiert nicht oder ist ungültig.",
|
||||
"definition_error_adding_updating" => "Das Attribut {0} konnte nicht hinzugefügt oder aktualisiert werden. Bitte überprüfen Sie den Error-Log.",
|
||||
"definition_flags" => "Attribut Sichtbarkeit",
|
||||
"definition_group" => "Gruppe",
|
||||
"definition_id" => "ID",
|
||||
"definition_name" => "Attribut hinzufügen",
|
||||
"definition_name_required" => "Attributname ist ein Pflichtfeld",
|
||||
"definition_one_or_multiple" => "Attribut(e)",
|
||||
"definition_successful_adding" => "Artikel erfolgreich hinzugefügt",
|
||||
"definition_successful_deleted" => "Löschung erfolgreich",
|
||||
"definition_successful_updating" => "Sie haben das Attributerfolgreich aktualisiert",
|
||||
"definition_type" => "Attribut Typ",
|
||||
"definition_type_required" => "Attribut ist ein Pflichtfeld",
|
||||
"definition_unit" => "Maßeinheit",
|
||||
"definition_values" => "Attribut Werte",
|
||||
"new" => "Neues Attribut",
|
||||
"no_attributes_to_display" => "Keine Elemente zum Anzeigen",
|
||||
"receipt_visibility" => "Quittung",
|
||||
"show_in_items" => "In Artikeln anzeigen",
|
||||
"show_in_items_visibility" => "Artikel",
|
||||
"show_in_receipt" => "In Quittung anzeigen",
|
||||
"show_in_receivings" => "In Eingängen anzeigen",
|
||||
"show_in_receivings_visibility" => "Eingänge",
|
||||
"show_in_sales" => "In Verkäufen anzeigen",
|
||||
"show_in_sales_visibility" => "Verkauf",
|
||||
"update" => "Attribut aktualisieren",
|
||||
'attribute_value_invalid_chars' => 'Attributwert darf nicht \':\' oder \'|\' enthalten',
|
||||
'confirm_delete' => 'Sind Sie sicher, dass Sie die ausgewählten Attribute löschen möchten?',
|
||||
'confirm_restore' => 'Sind Sie sicher, dass Sie die ausgewählten Attribute wiederherstellen möchten?',
|
||||
'definition_cannot_be_deleted' => 'Ausgewählte Attribute konnten nicht gelöscht werden',
|
||||
'definition_invalid_group' => 'Die ausgewählte Gruppe existiert nicht oder ist ungültig.',
|
||||
'definition_error_adding_updating' => 'Das Attribut {0} konnte nicht hinzugefügt oder aktualisiert werden. Bitte überprüfen Sie den Error-Log.',
|
||||
'definition_flags' => 'Attribut Sichtbarkeit',
|
||||
'definition_group' => 'Gruppe',
|
||||
'definition_id' => 'ID',
|
||||
'definition_name' => 'Attribut hinzufügen',
|
||||
'definition_name_required' => 'Attributname ist ein Pflichtfeld',
|
||||
'definition_one_or_multiple' => 'Attribut(e)',
|
||||
'definition_successful_adding' => 'Artikel erfolgreich hinzugefügt',
|
||||
'definition_successful_deleted' => 'Löschung erfolgreich',
|
||||
'definition_successful_updating' => 'Sie haben das Attributerfolgreich aktualisiert',
|
||||
'definition_type' => 'Attribut Typ',
|
||||
'definition_type_required' => 'Attribut ist ein Pflichtfeld',
|
||||
'definition_unit' => 'Maßeinheit',
|
||||
'definition_values' => 'Attribut Werte',
|
||||
'new' => 'Neues Attribut',
|
||||
'no_attributes_to_display' => 'Keine Elemente zum Anzeigen',
|
||||
'receipt_visibility' => 'Quittung',
|
||||
'show_in_items' => 'In Artikeln anzeigen',
|
||||
'show_in_items_visibility' => 'Artikel',
|
||||
'show_in_receipt' => 'In Quittung anzeigen',
|
||||
'show_in_receivings' => 'In Eingängen anzeigen',
|
||||
'show_in_receivings_visibility' => 'Eingänge',
|
||||
'show_in_sales' => 'In Verkäufen anzeigen',
|
||||
'show_in_sales_visibility' => 'Verkauf',
|
||||
'show_in_search' => 'In Suche anzeigen',
|
||||
'show_in_search_visibility' => 'Suche',
|
||||
'update' => 'Attribut aktualisieren',
|
||||
];
|
||||
@@ -210,7 +210,6 @@ return [
|
||||
'msg_msg' => 'Gespeicherte Nachricht',
|
||||
'msg_msg_placeholder' => 'Wenn Sie eine SMS Vorlage benutzen wollen, geben Sie diese hier ein, ansonsten lassen Sie dieses Feld frei.',
|
||||
'msg_pwd' => 'SMS-API Passwort',
|
||||
'msg_pwd_required' => 'SMS-API Passwort ist ein Pflichtfeld',
|
||||
'msg_src' => 'SMS-API Sender ID',
|
||||
'msg_src_required' => 'SMS-API Sender ID ist ein Pflichtfeld',
|
||||
'msg_uid' => 'SMS-API Benutzername',
|
||||
@@ -285,7 +284,7 @@ return [
|
||||
'right' => 'Rechts',
|
||||
'sales_invoice_format' => 'Format Verkaufsrechnung',
|
||||
'sales_quote_format' => 'Angebotsformat',
|
||||
'mailpath_invalid' => 'Ungültiger Sendmail-Pfad. Nur Buchstaben, Zahlen, Bindestriche, Unterstriche, Schrägstriche und Punkte sind erlaubt.',
|
||||
'mailpath_invalid' => 'Ungültiger Sendmail-Pfad. Nur Buchstaben, Zahlen, Bindestriche, Unterstriche, Schrägstriche, Rückwärtsschrägstriche, Doppelpunkte, Leerzeichen und Punkte sind erlaubt.',
|
||||
'saved_successfully' => 'Einstellungen erfolgreich gesichert.',
|
||||
'saved_unsuccessfully' => 'Einstellungen konnten nicht gesichert werden.',
|
||||
'security_issue' => 'Security Vulnerability Warning',
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"no_permission_module" => "Sie haben nicht die Zugangsrechte für das gewählte Modul",
|
||||
"unknown" => "Unbekannter Fehler",
|
||||
'no_permission_module' => 'Sie haben nicht die Zugangsrechte für das gewählte Modul',
|
||||
'unable_to_create_env_file' => '{filePath} konnte nicht erstellt werden, um throttle.key bereitzustellen',
|
||||
'unable_to_lock_file' => '{filePath} konnte nicht gesperrt werden: {reason}',
|
||||
'unable_to_open_lock_file' => '{filePath} konnte nicht geöffnet werden: {reason}',
|
||||
'unable_to_persist_throttle_key' => 'throttle.key konnte nicht in {filePath} gespeichert werden',
|
||||
'unable_to_persist_encryption_key' => '',
|
||||
'encryption_key_not_provisioned' => '',
|
||||
'throttle_key_not_provisioned' => '',
|
||||
'unable_to_read_env_file' => '{filePath} konnte nicht gelesen werden, um throttle.key bereitzustellen',
|
||||
'unknown' => 'Unbekannter Fehler',
|
||||
];
|
||||
@@ -1,34 +1,36 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"attribute_value_invalid_chars" => "Η τιμή του χαρακτηριστικού δεν μπορεί να περιέχει ':' ή '|'",
|
||||
"confirm_delete" => "Είστε βέβαιοι ότι θέλετε να διαγράψετε τα επιλεγμένα χαρακτηριστικά;",
|
||||
"confirm_restore" => "Είστε βέβαιοι ότι θέλετε να επαναφέρετε τα επιλεγμένα χαρακτηριστικά;",
|
||||
"definition_cannot_be_deleted" => "Δεν ήταν δυνατή η διαγραφή των επιλεγμένων χαρακτηριστικών",
|
||||
"definition_invalid_group" => "Η επιλεγμένη ομάδα δεν υπάρχει ή δεν είναι έγκυρη.",
|
||||
"definition_error_adding_updating" => "Το χαρακτηριστικό {0} δεν ήταν δυνατό να προστεθεί ή να ενημερωθεί. Ελέγξτε το αρχείο καταγραφής σφαλμάτων.",
|
||||
"definition_flags" => "Ορατότητα χαρακτηριστικών",
|
||||
"definition_group" => "Ομάδα",
|
||||
"definition_id" => "Id",
|
||||
"definition_name" => "Προσθήκη χαρακτηριστικού",
|
||||
"definition_name_required" => "Το όνομα του χαρακτηριστικού είναι υποχρεωτικό πεδίο",
|
||||
"definition_one_or_multiple" => "Χαρακτηριστικό(ά)",
|
||||
"definition_successful_adding" => "Έχετε προσθέσει με επιτυχία στοιχείο",
|
||||
"definition_successful_deleted" => "",
|
||||
"definition_successful_updating" => "Έχετε ενημερώσει με επιτυχία το χαρακτηριστικό",
|
||||
"definition_type" => "Τύπος Χαρακτηριστικού",
|
||||
"definition_type_required" => "Ο τύπος ιδιότητας είναι υποχρεωτικό πεδίο",
|
||||
"definition_unit" => "Μονάδα μέτρησης",
|
||||
"definition_values" => "Τιμές Χαρακτηριστικών",
|
||||
"new" => "Νέο χαρακτηριστικό",
|
||||
"no_attributes_to_display" => "Δεν υπάρχουν στοιχεία για εμφάνιση",
|
||||
"receipt_visibility" => "",
|
||||
"show_in_items" => "",
|
||||
"show_in_items_visibility" => "",
|
||||
"show_in_receipt" => "",
|
||||
"show_in_receivings" => "",
|
||||
"show_in_receivings_visibility" => "",
|
||||
"show_in_sales" => "",
|
||||
"show_in_sales_visibility" => "",
|
||||
"update" => "",
|
||||
'attribute_value_invalid_chars' => 'Η τιμή του χαρακτηριστικού δεν μπορεί να περιέχει \':\' ή \'|\'',
|
||||
'confirm_delete' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε τα επιλεγμένα χαρακτηριστικά;',
|
||||
'confirm_restore' => 'Είστε βέβαιοι ότι θέλετε να επαναφέρετε τα επιλεγμένα χαρακτηριστικά;',
|
||||
'definition_cannot_be_deleted' => 'Δεν ήταν δυνατή η διαγραφή των επιλεγμένων χαρακτηριστικών',
|
||||
'definition_invalid_group' => 'Η επιλεγμένη ομάδα δεν υπάρχει ή δεν είναι έγκυρη.',
|
||||
'definition_error_adding_updating' => 'Το χαρακτηριστικό {0} δεν ήταν δυνατό να προστεθεί ή να ενημερωθεί. Ελέγξτε το αρχείο καταγραφής σφαλμάτων.',
|
||||
'definition_flags' => 'Ορατότητα χαρακτηριστικών',
|
||||
'definition_group' => 'Ομάδα',
|
||||
'definition_id' => 'Id',
|
||||
'definition_name' => 'Προσθήκη χαρακτηριστικού',
|
||||
'definition_name_required' => 'Το όνομα του χαρακτηριστικού είναι υποχρεωτικό πεδίο',
|
||||
'definition_one_or_multiple' => 'Χαρακτηριστικό(ά)',
|
||||
'definition_successful_adding' => 'Έχετε προσθέσει με επιτυχία στοιχείο',
|
||||
'definition_successful_deleted' => '',
|
||||
'definition_successful_updating' => 'Έχετε ενημερώσει με επιτυχία το χαρακτηριστικό',
|
||||
'definition_type' => 'Τύπος Χαρακτηριστικού',
|
||||
'definition_type_required' => 'Ο τύπος ιδιότητας είναι υποχρεωτικό πεδίο',
|
||||
'definition_unit' => 'Μονάδα μέτρησης',
|
||||
'definition_values' => 'Τιμές Χαρακτηριστικών',
|
||||
'new' => 'Νέο χαρακτηριστικό',
|
||||
'no_attributes_to_display' => 'Δεν υπάρχουν στοιχεία για εμφάνιση',
|
||||
'receipt_visibility' => '',
|
||||
'show_in_items' => '',
|
||||
'show_in_items_visibility' => '',
|
||||
'show_in_receipt' => '',
|
||||
'show_in_receivings' => '',
|
||||
'show_in_receivings_visibility' => '',
|
||||
'show_in_sales' => '',
|
||||
'show_in_sales_visibility' => '',
|
||||
'show_in_search' => 'Εμφάνιση στην αναζήτηση',
|
||||
'show_in_search_visibility' => 'Αναζήτηση',
|
||||
'update' => '',
|
||||
];
|
||||
@@ -210,7 +210,6 @@ return [
|
||||
'msg_msg' => '',
|
||||
'msg_msg_placeholder' => '',
|
||||
'msg_pwd' => '',
|
||||
'msg_pwd_required' => '',
|
||||
'msg_src' => '',
|
||||
'msg_src_required' => '',
|
||||
'msg_uid' => '',
|
||||
@@ -285,7 +284,7 @@ return [
|
||||
'right' => '',
|
||||
'sales_invoice_format' => '',
|
||||
'sales_quote_format' => '',
|
||||
'mailpath_invalid' => '',
|
||||
'mailpath_invalid' => 'Μη έγκυρη διαδρομή sendmail. Επιτρέπονται μόνο γράμματα, αριθμοί, παύλες, κάτω παύλες, κάθετοι, ανάστροφες κάθετοι, άνω-κάτω τελείες, κενά και τελείες.',
|
||||
'saved_successfully' => '',
|
||||
'saved_unsuccessfully' => '',
|
||||
'security_issue' => 'Security Vulnerability Warning',
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"no_permission_module" => "",
|
||||
"unknown" => "",
|
||||
'no_permission_module' => 'Δεν έχετε δικαίωμα πρόσβασης στη μονάδα με το όνομα',
|
||||
'unable_to_create_env_file' => 'Δεν ήταν δυνατή η δημιουργία του {filePath} για την παροχή του throttle.key',
|
||||
'unable_to_lock_file' => 'Δεν ήταν δυνατό το κλείδωμα του {filePath}: {reason}',
|
||||
'unable_to_open_lock_file' => 'Δεν ήταν δυνατό το άνοιγμα του {filePath}: {reason}',
|
||||
'unable_to_persist_throttle_key' => 'Δεν ήταν δυνατή η αποθήκευση του throttle.key στο {filePath}',
|
||||
'unable_to_persist_encryption_key' => '',
|
||||
'encryption_key_not_provisioned' => '',
|
||||
'throttle_key_not_provisioned' => '',
|
||||
'unable_to_read_env_file' => 'Δεν ήταν δυνατή η ανάγνωση του {filePath} για την παροχή του throttle.key',
|
||||
'unknown' => 'Μη αναμενόμενο σφάλμα',
|
||||
];
|
||||
@@ -128,6 +128,7 @@ return [
|
||||
"tax_rate" => "",
|
||||
"taxes" => "",
|
||||
"taxes_summary_report" => "",
|
||||
"toggle_cost_and_profit" => "",
|
||||
"total" => "",
|
||||
"total_inventory_value" => "",
|
||||
"total_low_sell_quantity" => "",
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"attribute_value_invalid_chars" => "Attribute value cannot contain '_' or '|'",
|
||||
"confirm_delete" => "Are you sure you want to delete the selected attribute(s)?",
|
||||
"confirm_restore" => "Are you sure you want to restore the selected attribute(s)?",
|
||||
"definition_cannot_be_deleted" => "Could not delete selected attribute(s)",
|
||||
"definition_error_adding_updating" => "Attribute {0} could not be added or updated. Please check the error log.",
|
||||
"definition_invalid_group" => "The selected group does not exist or is invalid.",
|
||||
"definition_flags" => "Attribute Visibility",
|
||||
"definition_group" => "Group",
|
||||
"definition_id" => "Id",
|
||||
"definition_name" => "Add Attribute",
|
||||
"definition_name_required" => "Attribute name is a required field",
|
||||
"definition_one_or_multiple" => "attribute(s)",
|
||||
"definition_successful_adding" => "You have successfully added item",
|
||||
"definition_successful_deleted" => "You have successfully deleted",
|
||||
"definition_successful_updating" => "You have successfully updated attribute",
|
||||
"definition_type" => "Attribute Type",
|
||||
"definition_type_required" => "Attribute type is a required field",
|
||||
"definition_unit" => "Measurement Unit",
|
||||
"definition_values" => "Attribute Values",
|
||||
"new" => "New Attribute",
|
||||
"no_attributes_to_display" => "No Items to display",
|
||||
"receipt_visibility" => "Receipt",
|
||||
"show_in_items" => "Show in items",
|
||||
"show_in_items_visibility" => "Items",
|
||||
"show_in_receipt" => "Show in receipt",
|
||||
"show_in_receivings" => "Show in receivings",
|
||||
"show_in_receivings_visibility" => "Receivings",
|
||||
"show_in_sales" => "Show in sales",
|
||||
"show_in_sales_visibility" => "Sales",
|
||||
"update" => "Update Attribute",
|
||||
'attribute_value_invalid_chars' => 'Attribute value cannot contain \'_\' or \'|\'',
|
||||
'confirm_delete' => 'Are you sure you want to delete the selected attribute(s)?',
|
||||
'confirm_restore' => 'Are you sure you want to restore the selected attribute(s)?',
|
||||
'definition_cannot_be_deleted' => 'Could not delete selected attribute(s)',
|
||||
'definition_error_adding_updating' => 'Attribute {0} could not be added or updated. Please check the error log.',
|
||||
'definition_invalid_group' => 'The selected group does not exist or is invalid.',
|
||||
'definition_flags' => 'Attribute Visibility',
|
||||
'definition_group' => 'Group',
|
||||
'definition_id' => 'Id',
|
||||
'definition_name' => 'Add Attribute',
|
||||
'definition_name_required' => 'Attribute name is a required field',
|
||||
'definition_one_or_multiple' => 'attribute(s)',
|
||||
'definition_successful_adding' => 'You have successfully added item',
|
||||
'definition_successful_deleted' => 'You have successfully deleted',
|
||||
'definition_successful_updating' => 'You have successfully updated attribute',
|
||||
'definition_type' => 'Attribute Type',
|
||||
'definition_type_required' => 'Attribute type is a required field',
|
||||
'definition_unit' => 'Measurement Unit',
|
||||
'definition_values' => 'Attribute Values',
|
||||
'new' => 'New Attribute',
|
||||
'no_attributes_to_display' => 'No Attributes to display',
|
||||
'receipt_visibility' => 'Receipt',
|
||||
'show_in_items' => 'Show in items',
|
||||
'show_in_items_visibility' => 'Items',
|
||||
'show_in_receipt' => 'Show in receipt',
|
||||
'show_in_receivings' => 'Show in receivings',
|
||||
'show_in_receivings_visibility' => 'Receivings',
|
||||
'show_in_sales' => 'Show in sales',
|
||||
'show_in_sales_visibility' => 'Sales',
|
||||
'show_in_search' => 'Show in search',
|
||||
'show_in_search_visibility' => 'Search',
|
||||
'update' => 'Update Attribute',
|
||||
];
|
||||
@@ -210,7 +210,6 @@ return [
|
||||
'msg_msg' => 'Saved Text Message',
|
||||
'msg_msg_placeholder' => 'If you wish to use a SMS template save your message here. Otherwise leave the box blank.',
|
||||
'msg_pwd' => 'SMS-API Password',
|
||||
'msg_pwd_required' => 'SMS-API Password is a required field',
|
||||
'msg_src' => 'SMS-API Sender ID',
|
||||
'msg_src_required' => 'SMS-API Sender ID is a required field',
|
||||
'msg_uid' => 'SMS-API Username',
|
||||
@@ -237,8 +236,8 @@ return [
|
||||
'print_bottom_margin_number' => 'Bottom Margin must be a number.',
|
||||
'print_bottom_margin_required' => 'Bottom Margin is a required field.',
|
||||
'print_delay_autoreturn' => 'Autoreturn to Sale delay',
|
||||
'print_delay_autoreturn_number' => 'Autoreturn to Sale delay is a required field.',
|
||||
'print_delay_autoreturn_required' => 'Autoreturn to Sale delay must be a number.',
|
||||
'print_delay_autoreturn_number' => 'Autoreturn to Sale delay must be a number.',
|
||||
'print_delay_autoreturn_required' => 'Autoreturn to Sale delay is a required field.',
|
||||
'print_footer' => 'Print Browser Footer',
|
||||
'print_header' => 'Print Browser Header',
|
||||
'print_left_margin' => 'Margin Left',
|
||||
@@ -285,7 +284,7 @@ return [
|
||||
'right' => 'Right',
|
||||
'sales_invoice_format' => 'Sales Invoice Format',
|
||||
'sales_quote_format' => 'Sales Quote Format',
|
||||
'mailpath_invalid' => 'Invalid sendmail path. Only letters, numbers, dashes, underscores, slashes and dots are allowed.',
|
||||
'mailpath_invalid' => 'Invalid sendmail path. Only letters, numbers, dashes, underscores, slashes, backslashes, colons, spaces and dots are allowed.',
|
||||
'saved_successfully' => 'Configuration saved successfully.',
|
||||
'saved_unsuccessfully' => 'Configuration save failed.',
|
||||
'security_issue' => 'Security Vulnerability Warning',
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"no_permission_module" => "You do not have the permission to access the module named",
|
||||
"unknown" => "Unexpected error",
|
||||
'no_permission_module' => 'You do not have the permission to access the module named',
|
||||
'unable_to_create_env_file' => 'Unable to create {filePath} to provision an environment key',
|
||||
'unable_to_lock_file' => 'Unable to lock {filePath}: {reason}',
|
||||
'unable_to_open_lock_file' => 'Unable to open {filePath}: {reason}',
|
||||
'unable_to_persist_throttle_key' => 'Unable to persist throttle.key to {filePath}',
|
||||
'unable_to_persist_encryption_key' => '',
|
||||
'encryption_key_not_provisioned' => '',
|
||||
'throttle_key_not_provisioned' => '',
|
||||
'unable_to_read_env_file' => 'Unable to read {filePath} to provision an environment key',
|
||||
'unknown' => 'Unexpected error',
|
||||
];
|
||||
@@ -1,34 +1,36 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"attribute_value_invalid_chars" => "Attribute value cannot contain '_' or '|'",
|
||||
"confirm_delete" => "Are you sure you want to delete the selected attribute(s)?",
|
||||
"confirm_restore" => "Are you sure you want to restore the selected attribute(s)?",
|
||||
"definition_cannot_be_deleted" => "Could not delete selected attribute(s)",
|
||||
"definition_error_adding_updating" => "Attribute {0} could not be added or updated. Please check the error log.",
|
||||
"definition_invalid_group" => "The selected group does not exist or is invalid.",
|
||||
"definition_flags" => "Attribute Visibility",
|
||||
"definition_group" => "Group",
|
||||
"definition_id" => "Id",
|
||||
"definition_name" => "Add Attribute",
|
||||
"definition_name_required" => "Attribute name is a required field",
|
||||
"definition_one_or_multiple" => "attribute(s)",
|
||||
"definition_successful_adding" => "You have successfully added item",
|
||||
"definition_successful_deleted" => "You have successfully deleted",
|
||||
"definition_successful_updating" => "You have successfully updated attribute",
|
||||
"definition_type" => "Attribute Type",
|
||||
"definition_type_required" => "Attribute type is a required field",
|
||||
"definition_unit" => "Measurement Unit",
|
||||
"definition_values" => "Attribute Values",
|
||||
"new" => "New Attribute",
|
||||
"no_attributes_to_display" => "No Attributes to display",
|
||||
"receipt_visibility" => "Receipt",
|
||||
"show_in_items" => "Show in items",
|
||||
"show_in_items_visibility" => "Items",
|
||||
"show_in_receipt" => "Show in receipt",
|
||||
"show_in_receivings" => "Show in receivings",
|
||||
"show_in_receivings_visibility" => "Receivings",
|
||||
"show_in_sales" => "Show in sales",
|
||||
"show_in_sales_visibility" => "Sales",
|
||||
"update" => "Update Attribute",
|
||||
'attribute_value_invalid_chars' => 'Attribute value cannot contain \'_\' or \'|\'',
|
||||
'confirm_delete' => 'Are you sure you want to delete the selected attribute(s)?',
|
||||
'confirm_restore' => 'Are you sure you want to restore the selected attribute(s)?',
|
||||
'definition_cannot_be_deleted' => 'Could not delete selected attribute(s)',
|
||||
'definition_error_adding_updating' => 'Attribute {0} could not be added or updated. Please check the error log.',
|
||||
'definition_invalid_group' => 'The selected group does not exist or is invalid.',
|
||||
'definition_flags' => 'Attribute Visibility',
|
||||
'definition_group' => 'Group',
|
||||
'definition_id' => 'Id',
|
||||
'definition_name' => 'Add Attribute',
|
||||
'definition_name_required' => 'Attribute name is a required field',
|
||||
'definition_one_or_multiple' => 'attribute(s)',
|
||||
'definition_successful_adding' => 'You have successfully added item',
|
||||
'definition_successful_deleted' => 'You have successfully deleted',
|
||||
'definition_successful_updating' => 'You have successfully updated attribute',
|
||||
'definition_type' => 'Attribute Type',
|
||||
'definition_type_required' => 'Attribute type is a required field',
|
||||
'definition_unit' => 'Measurement Unit',
|
||||
'definition_values' => 'Attribute Values',
|
||||
'new' => 'New Attribute',
|
||||
'no_attributes_to_display' => 'No Attributes to display',
|
||||
'receipt_visibility' => 'Receipt',
|
||||
'show_in_items' => 'Show in items',
|
||||
'show_in_items_visibility' => 'Items',
|
||||
'show_in_receipt' => 'Show in receipt',
|
||||
'show_in_receivings' => 'Show in receivings',
|
||||
'show_in_receivings_visibility' => 'Receivings',
|
||||
'show_in_sales' => 'Show in sales',
|
||||
'show_in_sales_visibility' => 'Sales',
|
||||
'show_in_search' => 'Show in search',
|
||||
'show_in_search_visibility' => 'Search',
|
||||
'update' => 'Update Attribute',
|
||||
];
|
||||
@@ -119,6 +119,7 @@ return [
|
||||
'email_smtp_crypto' => 'SMTP Encryption',
|
||||
'email_smtp_host' => 'SMTP Server',
|
||||
'email_smtp_pass' => 'SMTP Password',
|
||||
'email_smtp_pass_set' => 'Already set — leave blank to keep',
|
||||
'email_smtp_port' => 'SMTP Port',
|
||||
'email_smtp_timeout' => 'SMTP Timeout (s)',
|
||||
'email_smtp_user' => 'SMTP Username',
|
||||
@@ -200,6 +201,7 @@ return [
|
||||
'logout' => 'Do you want to make a backup before logging out? Click [OK] to backup or [Cancel] to logout.',
|
||||
'mailchimp' => 'MailChimp',
|
||||
'mailchimp_api_key' => 'MailChimp API Key',
|
||||
'mailchimp_api_key_set' => 'Already set — leave blank to keep',
|
||||
'mailchimp_configuration' => 'MailChimp Configuration',
|
||||
'mailchimp_key_successfully' => 'API Key is valid.',
|
||||
'mailchimp_key_unsuccessfully' => 'API Key is invalid.',
|
||||
@@ -210,7 +212,7 @@ return [
|
||||
'msg_msg' => 'Saved Text Message',
|
||||
'msg_msg_placeholder' => 'If you wish to use a SMS template save your message here, otherwise leave the box blank.',
|
||||
'msg_pwd' => 'SMS-API Password',
|
||||
'msg_pwd_required' => 'SMS-API Password is a required field',
|
||||
'msg_pwd_set' => 'Already set — leave blank to keep',
|
||||
'msg_src' => 'SMS-API Sender ID',
|
||||
'msg_src_required' => 'SMS-API Sender ID is a required field',
|
||||
'msg_uid' => 'SMS-API Username',
|
||||
@@ -285,7 +287,7 @@ return [
|
||||
'right' => 'Right',
|
||||
'sales_invoice_format' => 'Sales Invoice Format',
|
||||
'sales_quote_format' => 'Sales Quote Format',
|
||||
'mailpath_invalid' => 'Invalid sendmail path. Only letters, numbers, dashes, underscores, slashes and dots are allowed.',
|
||||
'mailpath_invalid' => 'Invalid sendmail path. Only letters, numbers, dashes, underscores, slashes, backslashes, colons, spaces and dots are allowed.',
|
||||
'saved_successfully' => 'Configuration save successful.',
|
||||
'saved_unsuccessfully' => 'Configuration save failed.',
|
||||
'security_issue' => 'Security Vulnerability Warning',
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"no_permission_module" => "You do not have permission to access the module named",
|
||||
"unknown" => "Unexpected error",
|
||||
'no_permission_module' => 'You do not have permission to access the module named',
|
||||
'unable_to_create_env_file' => 'Unable to create {filePath} to provision an environment key',
|
||||
'unable_to_lock_file' => 'Unable to lock {filePath}: {reason}',
|
||||
'unable_to_open_lock_file' => 'Unable to open {filePath}: {reason}',
|
||||
'unable_to_persist_throttle_key' => 'Unable to persist throttle.key to {filePath}',
|
||||
'unable_to_persist_encryption_key' => 'Unable to persist the encryption key to {filePath}',
|
||||
'encryption_key_not_provisioned' => 'No encryption key is provisioned. Run `php spark env:provision` before continuing.',
|
||||
'throttle_key_not_provisioned' => 'No throttle key is provisioned. Run `php spark env:provision` before continuing.',
|
||||
'unable_to_read_env_file' => 'Unable to read {filePath} to provision an environment key',
|
||||
'unknown' => 'Unexpected error',
|
||||
];
|
||||
@@ -128,6 +128,7 @@ return [
|
||||
"tax_rate" => "Tax Rate",
|
||||
"taxes" => "Taxes",
|
||||
"taxes_summary_report" => "Taxes Summary Report",
|
||||
"toggle_cost_and_profit" => "Toggle Cost & Profit",
|
||||
"total" => "Total",
|
||||
"total_inventory_value" => "Total Inventory Value",
|
||||
"total_low_sell_quantity" => "Total Low Sell Quantity",
|
||||
@@ -146,5 +147,4 @@ return [
|
||||
"used" => "Points Used",
|
||||
"work_orders" => "Work Orders",
|
||||
"zero_and_less" => "Zero and less",
|
||||
"toggle_cost_and_profit" => "Toggle Cost & Profit",
|
||||
];
|
||||
@@ -1,34 +1,36 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"attribute_value_invalid_chars" => "El valor del atributo no puede contener ':' o '|'",
|
||||
"confirm_delete" => "¿Está seguro de que desea borrar los atributos seleccionados?",
|
||||
"confirm_restore" => "¿Está seguro de que desea restaurar los atributos seleccionados?",
|
||||
"definition_cannot_be_deleted" => "No se han podido borrar los atributos seleccionados",
|
||||
"definition_invalid_group" => "El grupo seleccionado no existe o no es válido.",
|
||||
"definition_error_adding_updating" => "El atributo {0} no pudo ser agregado o actulizado. Por favor compruebe el registro de errores.",
|
||||
"definition_flags" => "Visibilidad del atributo",
|
||||
"definition_group" => "Grupo",
|
||||
"definition_id" => "Id",
|
||||
"definition_name" => "Agregar Atributo",
|
||||
"definition_name_required" => "El nombre del Atributo es un campo obligatorio",
|
||||
"definition_one_or_multiple" => "atributo(s)",
|
||||
"definition_successful_adding" => "Ha agregado con éxito el atributo",
|
||||
"definition_successful_deleted" => "Ha eliminado con éxito",
|
||||
"definition_successful_updating" => "Ha actualizado con éxito el atributo",
|
||||
"definition_type" => "Tipo de Atributo",
|
||||
"definition_type_required" => "Tipo de Atributo es un campo obligatorio",
|
||||
"definition_unit" => "Unidad de medida",
|
||||
"definition_values" => "Valores del Atributo",
|
||||
"new" => "Nuevo Atributo",
|
||||
"no_attributes_to_display" => "No hay Atributos para mostrar",
|
||||
"receipt_visibility" => "Recibo",
|
||||
"show_in_items" => "Mostrar en ítems",
|
||||
"show_in_items_visibility" => "Ítems",
|
||||
"show_in_receipt" => "Mostrar en recibo",
|
||||
"show_in_receivings" => "Mostrar en recibos",
|
||||
"show_in_receivings_visibility" => "Recibos",
|
||||
"show_in_sales" => "Mostrar en ventas",
|
||||
"show_in_sales_visibility" => "Ventas",
|
||||
"update" => "Actualizar Atributo",
|
||||
'attribute_value_invalid_chars' => 'El valor del atributo no puede contener \':\' o \'|\'',
|
||||
'confirm_delete' => '¿Está seguro de que desea borrar los atributos seleccionados?',
|
||||
'confirm_restore' => '¿Está seguro de que desea restaurar los atributos seleccionados?',
|
||||
'definition_cannot_be_deleted' => 'No se han podido borrar los atributos seleccionados',
|
||||
'definition_invalid_group' => 'El grupo seleccionado no existe o no es válido.',
|
||||
'definition_error_adding_updating' => 'El atributo {0} no pudo ser agregado o actulizado. Por favor compruebe el registro de errores.',
|
||||
'definition_flags' => 'Visibilidad del atributo',
|
||||
'definition_group' => 'Grupo',
|
||||
'definition_id' => 'Id',
|
||||
'definition_name' => 'Agregar Atributo',
|
||||
'definition_name_required' => 'El nombre del Atributo es un campo obligatorio',
|
||||
'definition_one_or_multiple' => 'atributo(s)',
|
||||
'definition_successful_adding' => 'Ha agregado con éxito el atributo',
|
||||
'definition_successful_deleted' => 'Ha eliminado con éxito',
|
||||
'definition_successful_updating' => 'Ha actualizado con éxito el atributo',
|
||||
'definition_type' => 'Tipo de Atributo',
|
||||
'definition_type_required' => 'Tipo de Atributo es un campo obligatorio',
|
||||
'definition_unit' => 'Unidad de medida',
|
||||
'definition_values' => 'Valores del Atributo',
|
||||
'new' => 'Nuevo Atributo',
|
||||
'no_attributes_to_display' => 'No hay Atributos para mostrar',
|
||||
'receipt_visibility' => 'Recibo',
|
||||
'show_in_items' => 'Mostrar en ítems',
|
||||
'show_in_items_visibility' => 'Ítems',
|
||||
'show_in_receipt' => 'Mostrar en recibo',
|
||||
'show_in_receivings' => 'Mostrar en recibos',
|
||||
'show_in_receivings_visibility' => 'Recibos',
|
||||
'show_in_sales' => 'Mostrar en ventas',
|
||||
'show_in_sales_visibility' => 'Ventas',
|
||||
'show_in_search' => 'Mostrar en búsqueda',
|
||||
'show_in_search_visibility' => 'Búsqueda',
|
||||
'update' => 'Actualizar Atributo',
|
||||
];
|
||||
@@ -210,7 +210,6 @@ return [
|
||||
'msg_msg' => 'Texto del mensaje guardado',
|
||||
'msg_msg_placeholder' => 'Si desea usar un formato de SMS guarde su mensaje aquí, en caso contrario deje en blanco.',
|
||||
'msg_pwd' => 'SMS-API Password',
|
||||
'msg_pwd_required' => 'SMS-API Password es un campo requerido',
|
||||
'msg_src' => 'SMS-API ID remitente',
|
||||
'msg_src_required' => 'SMS-API ID remitente es un campo requerido',
|
||||
'msg_uid' => 'SMS-API Usuario',
|
||||
@@ -285,7 +284,7 @@ return [
|
||||
'right' => 'Derecha',
|
||||
'sales_invoice_format' => 'Formato de Facturas de Venta',
|
||||
'sales_quote_format' => 'Formato de presupuesto de las ventas',
|
||||
'mailpath_invalid' => 'Ruta de sendmail inválida. Solo se permiten letras, números, guiones, guiones bajos, barras y puntos.',
|
||||
'mailpath_invalid' => 'Ruta de sendmail no válida. Solo se permiten letras, números, guiones, guiones bajos, barras, barras invertidas, dos puntos, espacios y puntos.',
|
||||
'saved_successfully' => 'Configuración guardada satisfactoriamente.',
|
||||
'saved_unsuccessfully' => 'Configuración no guardada.',
|
||||
'security_issue' => 'Advertencia de vulnerabilidad de seguridad',
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"no_permission_module" => "No tienes permiso para accesar el módulo llamado",
|
||||
"unknown" => "desconocido",
|
||||
'no_permission_module' => 'No tienes permiso para accesar el módulo llamado',
|
||||
'unable_to_create_env_file' => 'No se pudo crear {filePath} para aprovisionar throttle.key',
|
||||
'unable_to_lock_file' => 'No se pudo bloquear {filePath}: {reason}',
|
||||
'unable_to_open_lock_file' => 'No se pudo abrir {filePath}: {reason}',
|
||||
'unable_to_persist_throttle_key' => 'No se pudo guardar throttle.key en {filePath}',
|
||||
'unable_to_persist_encryption_key' => '',
|
||||
'encryption_key_not_provisioned' => '',
|
||||
'throttle_key_not_provisioned' => '',
|
||||
'unable_to_read_env_file' => 'No se pudo leer {filePath} para aprovisionar throttle.key',
|
||||
'unknown' => 'desconocido',
|
||||
];
|
||||
@@ -1,34 +1,36 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"attribute_value_invalid_chars" => "El valor del atributo no puede contener ':' or '|'",
|
||||
"confirm_delete" => "¿Está seguro de eliminar el/los atributo(s) seleccionado(s)?",
|
||||
"confirm_restore" => "¿Está seguro que quiere restaurar los atributos seleccionados?",
|
||||
"definition_cannot_be_deleted" => "No ha sido posible eliminar el/los atributo(s) seleccionado(s)",
|
||||
"definition_invalid_group" => "El grupo seleccionado no existe o no es válido.",
|
||||
"definition_error_adding_updating" => "El atributo {0} no pudo ser agregado o actualizado. Favor de revisar el registro de errorres.",
|
||||
"definition_flags" => "Visibilidad del atributo",
|
||||
"definition_group" => "Grupo",
|
||||
"definition_id" => "Id",
|
||||
"definition_name" => "Agregar Atributo",
|
||||
"definition_name_required" => "El nombre del atributo es un campo requerido",
|
||||
"definition_one_or_multiple" => "atributo(s)",
|
||||
"definition_successful_adding" => "Has agregado un atributo correctamente",
|
||||
"definition_successful_deleted" => "El atributo se ha eliminado correctamente",
|
||||
"definition_successful_updating" => "El atributo se ha actualizado correctamente",
|
||||
"definition_type" => "Tipo de atributo",
|
||||
"definition_type_required" => "El tipo de atributo es un campo requerido",
|
||||
"definition_unit" => "Unidad de Medida",
|
||||
"definition_values" => "Valores del atributo",
|
||||
"new" => "Nuevo atributo",
|
||||
"no_attributes_to_display" => "Sin artículos para mostrar",
|
||||
"receipt_visibility" => "Recibo",
|
||||
"show_in_items" => "Mostrar en artículos",
|
||||
"show_in_items_visibility" => "Artículos",
|
||||
"show_in_receipt" => "Mostrar en recibo",
|
||||
"show_in_receivings" => "Mostrar en recepciones",
|
||||
"show_in_receivings_visibility" => "Recepciones",
|
||||
"show_in_sales" => "Mostrar en Ventas",
|
||||
"show_in_sales_visibility" => "Ventas",
|
||||
"update" => "Actualizar atributo",
|
||||
'attribute_value_invalid_chars' => 'El valor del atributo no puede contener \':\' or \'|\'',
|
||||
'confirm_delete' => '¿Está seguro de eliminar el/los atributo(s) seleccionado(s)?',
|
||||
'confirm_restore' => '¿Está seguro que quiere restaurar los atributos seleccionados?',
|
||||
'definition_cannot_be_deleted' => 'No ha sido posible eliminar el/los atributo(s) seleccionado(s)',
|
||||
'definition_invalid_group' => 'El grupo seleccionado no existe o no es válido.',
|
||||
'definition_error_adding_updating' => 'El atributo {0} no pudo ser agregado o actualizado. Favor de revisar el registro de errorres.',
|
||||
'definition_flags' => 'Visibilidad del atributo',
|
||||
'definition_group' => 'Grupo',
|
||||
'definition_id' => 'Id',
|
||||
'definition_name' => 'Agregar Atributo',
|
||||
'definition_name_required' => 'El nombre del atributo es un campo requerido',
|
||||
'definition_one_or_multiple' => 'atributo(s)',
|
||||
'definition_successful_adding' => 'Has agregado un atributo correctamente',
|
||||
'definition_successful_deleted' => 'El atributo se ha eliminado correctamente',
|
||||
'definition_successful_updating' => 'El atributo se ha actualizado correctamente',
|
||||
'definition_type' => 'Tipo de atributo',
|
||||
'definition_type_required' => 'El tipo de atributo es un campo requerido',
|
||||
'definition_unit' => 'Unidad de Medida',
|
||||
'definition_values' => 'Valores del atributo',
|
||||
'new' => 'Nuevo atributo',
|
||||
'no_attributes_to_display' => 'Sin artículos para mostrar',
|
||||
'receipt_visibility' => 'Recibo',
|
||||
'show_in_items' => 'Mostrar en artículos',
|
||||
'show_in_items_visibility' => 'Artículos',
|
||||
'show_in_receipt' => 'Mostrar en recibo',
|
||||
'show_in_receivings' => 'Mostrar en recepciones',
|
||||
'show_in_receivings_visibility' => 'Recepciones',
|
||||
'show_in_sales' => 'Mostrar en Ventas',
|
||||
'show_in_sales_visibility' => 'Ventas',
|
||||
'show_in_search' => 'Mostrar en búsqueda',
|
||||
'show_in_search_visibility' => 'Búsqueda',
|
||||
'update' => 'Actualizar atributo',
|
||||
];
|
||||
@@ -210,7 +210,6 @@ return [
|
||||
'msg_msg' => 'Saved Text Message',
|
||||
'msg_msg_placeholder' => 'If you wish to use a SMS template save your message here, otherwise leave the box blank.',
|
||||
'msg_pwd' => 'SMS-API Password',
|
||||
'msg_pwd_required' => 'SMS-API Password is a required field',
|
||||
'msg_src' => 'SMS-API Sender ID',
|
||||
'msg_src_required' => 'SMS-API Sender ID is a required field',
|
||||
'msg_uid' => 'SMS-API Username',
|
||||
@@ -237,8 +236,8 @@ return [
|
||||
'print_bottom_margin_number' => 'Margin Bottom must be a number.',
|
||||
'print_bottom_margin_required' => 'Margin Bottom is a required field.',
|
||||
'print_delay_autoreturn' => 'Autoreturn to Sale delay',
|
||||
'print_delay_autoreturn_number' => 'Autoreturn to Sale delay is a required field.',
|
||||
'print_delay_autoreturn_required' => 'Autoreturn to Sale delay must be a number.',
|
||||
'print_delay_autoreturn_number' => '',
|
||||
'print_delay_autoreturn_required' => '',
|
||||
'print_footer' => 'Print Browser Footer',
|
||||
'print_header' => 'Print Browser Header',
|
||||
'print_left_margin' => 'Margin Left',
|
||||
@@ -285,7 +284,7 @@ return [
|
||||
'right' => 'Right',
|
||||
'sales_invoice_format' => 'Sales Invoice Format',
|
||||
'sales_quote_format' => 'Sales Quote Format',
|
||||
'mailpath_invalid' => 'Ruta de sendmail inválida. Solo se permiten letras, números, guiones, guiones bajos, barras y puntos.',
|
||||
'mailpath_invalid' => 'Ruta de sendmail inválida. Solo se permiten letras, números, guiones, guiones bajos, diagonales, diagonales invertidas, dos puntos, espacios y puntos.',
|
||||
'saved_successfully' => 'Configuration save successful.',
|
||||
'saved_unsuccessfully' => 'Configuration save failed.',
|
||||
'security_issue' => 'Security Vulnerability Warning',
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"no_permission_module" => "No tienes permiso para acceder el módulo llamado",
|
||||
"unknown" => "Error inesperado",
|
||||
'no_permission_module' => 'No tienes permiso para acceder el módulo llamado',
|
||||
'unable_to_create_env_file' => 'No se pudo crear {filePath} para aprovisionar throttle.key',
|
||||
'unable_to_lock_file' => 'No se pudo bloquear {filePath}: {reason}',
|
||||
'unable_to_open_lock_file' => 'No se pudo abrir {filePath}: {reason}',
|
||||
'unable_to_persist_throttle_key' => 'No se pudo guardar throttle.key en {filePath}',
|
||||
'unable_to_persist_encryption_key' => '',
|
||||
'encryption_key_not_provisioned' => '',
|
||||
'throttle_key_not_provisioned' => '',
|
||||
'unable_to_read_env_file' => 'No se pudo leer {filePath} para aprovisionar throttle.key',
|
||||
'unknown' => 'Error inesperado',
|
||||
];
|
||||
@@ -128,6 +128,7 @@ return [
|
||||
"tax_rate" => "Tarifa de Impuesto",
|
||||
"taxes" => "Impuestos",
|
||||
"taxes_summary_report" => "Reporte Resumido de Impuestos",
|
||||
"toggle_cost_and_profit" => "Alternar Costo y Ganancia",
|
||||
"total" => "Total",
|
||||
"total_inventory_value" => "Valor Total del Inventario",
|
||||
"total_low_sell_quantity" => "Cantidad Total de Venta Baja",
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"attribute_value_invalid_chars" => "مقدار ویژگی نمی تواند حاوی | یا ـ باشد",
|
||||
"confirm_delete" => "آیا مطمئن هستید که می خواهید ویژگی (های) انتخاب شده را حذف کنید؟",
|
||||
"confirm_restore" => "آیا مطمئن هستید که می خواهید ویژگی (های) انتخاب شده را بازیابی کنید؟",
|
||||
"definition_cannot_be_deleted" => "نمی توان ویژگی (های) انتخابی را حذف کرد",
|
||||
"definition_invalid_group" => "گروه انتخاب شده وجود ندارد یا نامعتبر است.",
|
||||
"definition_error_adding_updating" => "ویژگی{0} اضافه نشد یا به روز نمی شود. لطفا گزارش خطا را بررسی کنید.",
|
||||
"definition_flags" => "قابلیت مشاهده ویژگی",
|
||||
"definition_group" => "گروه",
|
||||
"definition_id" => "شناسه",
|
||||
"definition_name" => "افزودن ویژگی",
|
||||
"definition_name_required" => "نام ویژگی یک فیلد ضروری است",
|
||||
"definition_one_or_multiple" => "(ویژگی(ها",
|
||||
"definition_successful_adding" => "شما مورد را با موفقیت اضافه کردید",
|
||||
"definition_successful_deleted" => "شما با موفقیت حذف شده اید",
|
||||
"definition_successful_updating" => "شما ویژگی را با موفقیت به روز کردید",
|
||||
"definition_type" => "نوع ویژگی",
|
||||
"definition_type_required" => "نوع ویژگی یک فیلد ضروری است",
|
||||
"definition_unit" => "واحد اندازه گیری",
|
||||
"definition_values" => "مقادیر مشخصه",
|
||||
"new" => "ویژگی جدید",
|
||||
"no_attributes_to_display" => "هیچ موردی برای نمایش",
|
||||
"receipt_visibility" => "اعلام وصول",
|
||||
"show_in_items" => "نمایش در موارد",
|
||||
"show_in_items_visibility" => "موارد",
|
||||
"show_in_receipt" => "نمایش در رسید",
|
||||
"show_in_receivings" => "نمایش در دریافت ها",
|
||||
"show_in_receivings_visibility" => "دریافت",
|
||||
"show_in_sales" => "نمایش در فروش",
|
||||
"show_in_sales_visibility" => "حراجی",
|
||||
"update" => "به روز کردن ویژگی",
|
||||
'attribute_value_invalid_chars' => 'مقدار ویژگی نمی تواند حاوی | یا ـ باشد',
|
||||
'confirm_delete' => 'آیا مطمئن هستید که می خواهید ویژگی (های) انتخاب شده را حذف کنید؟',
|
||||
'confirm_restore' => 'آیا مطمئن هستید که می خواهید ویژگی (های) انتخاب شده را بازیابی کنید؟',
|
||||
'definition_cannot_be_deleted' => 'نمی توان ویژگی (های) انتخابی را حذف کرد',
|
||||
'definition_invalid_group' => 'گروه انتخاب شده وجود ندارد یا نامعتبر است.',
|
||||
'definition_error_adding_updating' => 'ویژگی{0} اضافه نشد یا به روز نمی شود. لطفا گزارش خطا را بررسی کنید.',
|
||||
'definition_flags' => 'قابلیت مشاهده ویژگی',
|
||||
'definition_group' => 'گروه',
|
||||
'definition_id' => 'شناسه',
|
||||
'definition_name' => 'افزودن ویژگی',
|
||||
'definition_name_required' => 'نام ویژگی یک فیلد ضروری است',
|
||||
'definition_one_or_multiple' => '(ویژگی(ها',
|
||||
'definition_successful_adding' => 'شما مورد را با موفقیت اضافه کردید',
|
||||
'definition_successful_deleted' => 'شما با موفقیت حذف شده اید',
|
||||
'definition_successful_updating' => 'شما ویژگی را با موفقیت به روز کردید',
|
||||
'definition_type' => 'نوع ویژگی',
|
||||
'definition_type_required' => 'نوع ویژگی یک فیلد ضروری است',
|
||||
'definition_unit' => 'واحد اندازه گیری',
|
||||
'definition_values' => 'مقادیر مشخصه',
|
||||
'new' => 'ویژگی جدید',
|
||||
'no_attributes_to_display' => 'هیچ موردی برای نمایش',
|
||||
'receipt_visibility' => 'اعلام وصول',
|
||||
'show_in_items' => 'نمایش در موارد',
|
||||
'show_in_items_visibility' => 'موارد',
|
||||
'show_in_receipt' => 'نمایش در رسید',
|
||||
'show_in_receivings' => 'نمایش در دریافت ها',
|
||||
'show_in_receivings_visibility' => 'دریافت',
|
||||
'show_in_sales' => 'نمایش در فروش',
|
||||
'show_in_sales_visibility' => 'حراجی',
|
||||
'show_in_search' => 'نمایش در جستجو',
|
||||
'show_in_search_visibility' => 'جستجو',
|
||||
'update' => 'به روز کردن ویژگی',
|
||||
];
|
||||
@@ -210,7 +210,6 @@ return [
|
||||
'msg_msg' => 'پیام متنی ذخیره شده',
|
||||
'msg_msg_placeholder' => 'اگر می خواهید از یک پیام کوتاه استفاده کنید ، پیام خود را در اینجا ذخیره کنید ، در غیر این صورت جعبه را خالی بگذارید.',
|
||||
'msg_pwd' => 'گذرواژه SMS-API',
|
||||
'msg_pwd_required' => 'گذرواژه ایپیآی اساماس یک فیلد ضروری است',
|
||||
'msg_src' => 'شناسه فرستنده ایپیآی اساماس ',
|
||||
'msg_src_required' => 'شناسه فرستنده ایپیآی اساماس یک زمینه ضروری است',
|
||||
'msg_uid' => 'نام کاربری ایپیآی اساماس ',
|
||||
@@ -285,7 +284,7 @@ return [
|
||||
'right' => 'درست',
|
||||
'sales_invoice_format' => 'قالب فاکتور فروش',
|
||||
'sales_quote_format' => 'قالب فروش قیمت',
|
||||
'mailpath_invalid' => '',
|
||||
'mailpath_invalid' => 'مسیر sendmail نامعتبر است. فقط حروف، اعداد، خطتیره، زیرخط، اسلش، بکاسلش، دونقطه، فاصله و نقطه مجاز هستند.',
|
||||
'saved_successfully' => 'پیکربندی ذخیره موفقیت آمیز است.',
|
||||
'saved_unsuccessfully' => 'ذخیره پیکربندی انجام نشد.',
|
||||
'security_issue' => 'هشدار آسیب پذیری امنیتی',
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"no_permission_module" => "شما اجازه دسترسی به ماژول به نام خود را ندارید",
|
||||
"unknown" => "خطای غیر منتظره",
|
||||
'no_permission_module' => 'شما اجازه دسترسی به ماژول به نام خود را ندارید',
|
||||
'unable_to_create_env_file' => 'ایجاد {filePath} برای آمادهسازی throttle.key ممکن نشد',
|
||||
'unable_to_lock_file' => 'قفل کردن {filePath} ممکن نشد: {reason}',
|
||||
'unable_to_open_lock_file' => 'باز کردن {filePath} ممکن نشد: {reason}',
|
||||
'unable_to_persist_throttle_key' => 'ذخیره throttle.key در {filePath} ممکن نشد',
|
||||
'unable_to_persist_encryption_key' => '',
|
||||
'encryption_key_not_provisioned' => '',
|
||||
'throttle_key_not_provisioned' => '',
|
||||
'unable_to_read_env_file' => 'خواندن {filePath} برای آمادهسازی throttle.key ممکن نشد',
|
||||
'unknown' => 'خطای غیر منتظره',
|
||||
];
|
||||
@@ -128,6 +128,7 @@ return [
|
||||
"tax_rate" => "نرخ مالیات",
|
||||
"taxes" => "مالیات",
|
||||
"taxes_summary_report" => "گزارش خلاصه مالیات",
|
||||
"toggle_cost_and_profit" => "",
|
||||
"total" => "جمع",
|
||||
"total_inventory_value" => "ارزش کل موجودی",
|
||||
"total_low_sell_quantity" => "تعداد کم فروش کم",
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"attribute_value_invalid_chars" => "La valeur de l'attribut ne doit pas contenir '_' ou '|'",
|
||||
"confirm_delete" => "Êtes-vous certain de vouloir supprimer le(s) attribut(s) sélectionné(s) ?",
|
||||
"confirm_restore" => "Êtes-vous certain de vouloir restaurer le(s) attribut(s) sélectionné(s) ?",
|
||||
"definition_cannot_be_deleted" => "Le(s) attribut(s) sélectionné(s) n'ont pas pu être supprimé(s)",
|
||||
"definition_invalid_group" => "Le groupe sélectionné n'existe pas ou est invalide.",
|
||||
"definition_error_adding_updating" => "L'attribut {0} n'a pas pu être ajouté ou mis à jour. Veuillez vérifier le journal d'erreurs.",
|
||||
"definition_flags" => "Visibilité de l'attribut",
|
||||
"definition_group" => "Groupe",
|
||||
"definition_id" => "ID",
|
||||
"definition_name" => "Ajouter un attribut",
|
||||
"definition_name_required" => "Le nom de l'attribut est requis",
|
||||
"definition_one_or_multiple" => "attribut(s)",
|
||||
"definition_successful_adding" => "Vous avez ajouté l'article avec succès",
|
||||
"definition_successful_deleted" => "Vous avez supprimé avec succès",
|
||||
"definition_successful_updating" => "Vous avez mis-à-jour l'attribut avec succès",
|
||||
"definition_type" => "Type d'attribut",
|
||||
"definition_type_required" => "Le type d'attribut est requis",
|
||||
"definition_unit" => "Unité de mesure",
|
||||
"definition_values" => "Valeurs de l'attribut",
|
||||
"new" => "Nouvel attribut",
|
||||
"no_attributes_to_display" => "Aucun attribut à afficher",
|
||||
"receipt_visibility" => "Reçu",
|
||||
"show_in_items" => "Afficher dans les articles",
|
||||
"show_in_items_visibility" => "Articles",
|
||||
"show_in_receipt" => "Afficher sur le reçu",
|
||||
"show_in_receivings" => "Afficher dans les réceptions",
|
||||
"show_in_receivings_visibility" => "Réceptions",
|
||||
"show_in_sales" => "Afficher dans les ventes",
|
||||
"show_in_sales_visibility" => "Ventes",
|
||||
"update" => "Mettre à jour l'attribut",
|
||||
'attribute_value_invalid_chars' => 'La valeur de l\'attribut ne doit pas contenir \'_\' ou \'|\'',
|
||||
'confirm_delete' => 'Êtes-vous certain de vouloir supprimer le(s) attribut(s) sélectionné(s) ?',
|
||||
'confirm_restore' => 'Êtes-vous certain de vouloir restaurer le(s) attribut(s) sélectionné(s) ?',
|
||||
'definition_cannot_be_deleted' => 'Le(s) attribut(s) sélectionné(s) n\'ont pas pu être supprimé(s)',
|
||||
'definition_invalid_group' => 'Le groupe sélectionné n\'existe pas ou est invalide.',
|
||||
'definition_error_adding_updating' => 'L\'attribut {0} n\'a pas pu être ajouté ou mis à jour. Veuillez vérifier le journal d\'erreurs.',
|
||||
'definition_flags' => 'Visibilité de l\'attribut',
|
||||
'definition_group' => 'Groupe',
|
||||
'definition_id' => 'ID',
|
||||
'definition_name' => 'Ajouter un attribut',
|
||||
'definition_name_required' => 'Le nom de l\'attribut est requis',
|
||||
'definition_one_or_multiple' => 'attribut(s)',
|
||||
'definition_successful_adding' => 'Vous avez ajouté l\'article avec succès',
|
||||
'definition_successful_deleted' => 'Vous avez supprimé avec succès',
|
||||
'definition_successful_updating' => 'Vous avez mis-à-jour l\'attribut avec succès',
|
||||
'definition_type' => 'Type d\'attribut',
|
||||
'definition_type_required' => 'Le type d\'attribut est requis',
|
||||
'definition_unit' => 'Unité de mesure',
|
||||
'definition_values' => 'Valeurs de l\'attribut',
|
||||
'new' => 'Nouvel attribut',
|
||||
'no_attributes_to_display' => 'Aucun attribut à afficher',
|
||||
'receipt_visibility' => 'Reçu',
|
||||
'show_in_items' => 'Afficher dans les articles',
|
||||
'show_in_items_visibility' => 'Articles',
|
||||
'show_in_receipt' => 'Afficher sur le reçu',
|
||||
'show_in_receivings' => 'Afficher dans les réceptions',
|
||||
'show_in_receivings_visibility' => 'Réceptions',
|
||||
'show_in_sales' => 'Afficher dans les ventes',
|
||||
'show_in_sales_visibility' => 'Ventes',
|
||||
'show_in_search' => 'Afficher dans la recherche',
|
||||
'show_in_search_visibility' => 'Recherche',
|
||||
'update' => 'Mettre à jour l\'attribut',
|
||||
];
|
||||
@@ -210,7 +210,6 @@ return [
|
||||
'msg_msg' => 'Message texte enregistré',
|
||||
'msg_msg_placeholder' => 'Si vous souhaitez utiliser un modèle de SMS, enregistrez votre message ici. Sinon, laisser la boîte en blanc.',
|
||||
'msg_pwd' => 'SMS-API Password',
|
||||
'msg_pwd_required' => 'SMS-API Password est un champ obligatoire',
|
||||
'msg_src' => "ID de l'expéditeur de SMS-API",
|
||||
'msg_src_required' => "L'ID de l'expéditeur de SMS-API est un champ obligatoire",
|
||||
'msg_uid' => "Nom d'utilisateur de l'API SMS",
|
||||
@@ -285,7 +284,7 @@ return [
|
||||
'right' => 'Droite',
|
||||
'sales_invoice_format' => 'Format de la facture de vente',
|
||||
'sales_quote_format' => 'Format de devis de vente',
|
||||
'mailpath_invalid' => 'Chemin sendmail invalide. Seuls les lettres, chiffres, tirets, underscores, barres obliques et points sont autorisés.',
|
||||
'mailpath_invalid' => 'Chemin sendmail invalide. Seuls les lettres, chiffres, tirets, traits de soulignement, barres obliques, antislashs, deux-points, espaces et points sont autorisés.',
|
||||
'saved_successfully' => 'Configuration enregistrer avec succès.',
|
||||
'saved_unsuccessfully' => "L'enregistrement de configuration a échoué.",
|
||||
'security_issue' => 'Avertissement de faille de sécurité',
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"no_permission_module" => "Vous n'avez pas d'autorisation d'accès pour le module",
|
||||
"unknown" => "inconnu",
|
||||
'no_permission_module' => 'Vous n\'avez pas d\'autorisation d\'accès pour le module',
|
||||
'unable_to_create_env_file' => 'Impossible de créer {filePath} pour provisionner throttle.key',
|
||||
'unable_to_lock_file' => 'Impossible de verrouiller {filePath} : {reason}',
|
||||
'unable_to_open_lock_file' => 'Impossible d\'ouvrir {filePath} : {reason}',
|
||||
'unable_to_persist_throttle_key' => 'Impossible d\'enregistrer throttle.key dans {filePath}',
|
||||
'unable_to_persist_encryption_key' => '',
|
||||
'encryption_key_not_provisioned' => '',
|
||||
'throttle_key_not_provisioned' => '',
|
||||
'unable_to_read_env_file' => 'Impossible de lire {filePath} pour provisionner throttle.key',
|
||||
'unknown' => 'inconnu',
|
||||
];
|
||||
@@ -1,34 +1,36 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"attribute_value_invalid_chars" => "ערך השדה אינו יכול להכיל ':' או '|'",
|
||||
"confirm_delete" => "האם אתה בטוח שברצונך למחוק את המאפיינים שנבחרו?",
|
||||
"confirm_restore" => "האם אתה בטוח שברצונך לשחזר את המאפיינים שנבחרו?",
|
||||
"definition_cannot_be_deleted" => "לא ניתן למחוק מאפיינים נבחר(ים)",
|
||||
"definition_invalid_group" => "הקבוצה שנבחרה לא קיימת או אינה תקינה.",
|
||||
"definition_error_adding_updating" => "לא ניתן להוסיף או לעדכן את הערך {0}. בדוק את יומן השגיאות.",
|
||||
"definition_flags" => "מאפיין גלוי",
|
||||
"definition_group" => "קבוצה",
|
||||
"definition_id" => "מספר זיהוי",
|
||||
"definition_name" => "הוסף מאפיין",
|
||||
"definition_name_required" => "מאפיין שם הינו שדה חובה",
|
||||
"definition_one_or_multiple" => "תכונה (תכונות)",
|
||||
"definition_successful_adding" => "הוספת בהצלחה את הפריט",
|
||||
"definition_successful_deleted" => "נמחק בהצלחה",
|
||||
"definition_successful_updating" => "עדכנת בהצלחה את המאפיין",
|
||||
"definition_type" => "סוג מאפיין",
|
||||
"definition_type_required" => "שדה המאפיין הינו שדה חובה",
|
||||
"definition_unit" => "יחידת מדידה",
|
||||
"definition_values" => "ערכי המאפיין",
|
||||
"new" => "מאפיין חדש",
|
||||
"no_attributes_to_display" => "אין פריטים להצגה",
|
||||
"receipt_visibility" => "קבלה",
|
||||
"show_in_items" => "הצג בפריטים",
|
||||
"show_in_items_visibility" => "פריטים",
|
||||
"show_in_receipt" => "הצג בקבלה",
|
||||
"show_in_receivings" => "הצג בקבלת סחורה",
|
||||
"show_in_receivings_visibility" => "קבלת סחורה",
|
||||
"show_in_sales" => "הצג במכירות",
|
||||
"show_in_sales_visibility" => "מכירות",
|
||||
"update" => "עדכן מאפיין",
|
||||
'attribute_value_invalid_chars' => 'ערך השדה אינו יכול להכיל \':\' או \'|\'',
|
||||
'confirm_delete' => 'האם אתה בטוח שברצונך למחוק את המאפיינים שנבחרו?',
|
||||
'confirm_restore' => 'האם אתה בטוח שברצונך לשחזר את המאפיינים שנבחרו?',
|
||||
'definition_cannot_be_deleted' => 'לא ניתן למחוק מאפיינים נבחר(ים)',
|
||||
'definition_invalid_group' => 'הקבוצה שנבחרה לא קיימת או אינה תקינה.',
|
||||
'definition_error_adding_updating' => 'לא ניתן להוסיף או לעדכן את הערך {0}. בדוק את יומן השגיאות.',
|
||||
'definition_flags' => 'מאפיין גלוי',
|
||||
'definition_group' => 'קבוצה',
|
||||
'definition_id' => 'מספר זיהוי',
|
||||
'definition_name' => 'הוסף מאפיין',
|
||||
'definition_name_required' => 'מאפיין שם הינו שדה חובה',
|
||||
'definition_one_or_multiple' => 'תכונה (תכונות)',
|
||||
'definition_successful_adding' => 'הוספת בהצלחה את הפריט',
|
||||
'definition_successful_deleted' => 'נמחק בהצלחה',
|
||||
'definition_successful_updating' => 'עדכנת בהצלחה את המאפיין',
|
||||
'definition_type' => 'סוג מאפיין',
|
||||
'definition_type_required' => 'שדה המאפיין הינו שדה חובה',
|
||||
'definition_unit' => 'יחידת מדידה',
|
||||
'definition_values' => 'ערכי המאפיין',
|
||||
'new' => 'מאפיין חדש',
|
||||
'no_attributes_to_display' => 'אין פריטים להצגה',
|
||||
'receipt_visibility' => 'קבלה',
|
||||
'show_in_items' => 'הצג בפריטים',
|
||||
'show_in_items_visibility' => 'פריטים',
|
||||
'show_in_receipt' => 'הצג בקבלה',
|
||||
'show_in_receivings' => 'הצג בקבלת סחורה',
|
||||
'show_in_receivings_visibility' => 'קבלת סחורה',
|
||||
'show_in_sales' => 'הצג במכירות',
|
||||
'show_in_sales_visibility' => 'מכירות',
|
||||
'show_in_search' => 'הצג בחיפוש',
|
||||
'show_in_search_visibility' => 'חיפוש',
|
||||
'update' => 'עדכן מאפיין',
|
||||
];
|
||||
@@ -210,7 +210,6 @@ return [
|
||||
'msg_msg' => 'הודעת טקסט שמורה',
|
||||
'msg_msg_placeholder' => 'אם ברצונך להשתמש בתבנית הודעה שמור את ההודעה שלך כאן, אחרת השאר את התיבה ריקה.',
|
||||
'msg_pwd' => 'סיסמה של SMS-API',
|
||||
'msg_pwd_required' => 'סיסמה של SMS-API הינו שדה חובה',
|
||||
'msg_src' => 'מזהה שולח SMS-API',
|
||||
'msg_src_required' => 'מזהה שולח SMS-API הינו שדה חובה',
|
||||
'msg_uid' => 'שם משתמש של SMS-API',
|
||||
@@ -285,7 +284,7 @@ return [
|
||||
'right' => 'ימין',
|
||||
'sales_invoice_format' => 'תבנית חשבונית מכירות',
|
||||
'sales_quote_format' => 'תבנית חשבונית הצעת מחיר',
|
||||
'mailpath_invalid' => '',
|
||||
'mailpath_invalid' => 'נתיב sendmail לא תקין. מותרים רק אותיות, ספרות, מקפים, קווים תחתונים, לוכסנים, לוכסנים הפוכים, נקודתיים, רווחים ונקודות.',
|
||||
'saved_successfully' => 'ההגדרות נשמרו בהצלחה.',
|
||||
'saved_unsuccessfully' => 'שמירת ההגדרות נכשלה.',
|
||||
'security_issue' => 'Security Vulnerability Warning',
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"no_permission_module" => "אין לך הרשאה לגשת אל המודול ששמו",
|
||||
"unknown" => "שגיאה לא ידועה",
|
||||
'no_permission_module' => 'אין לך הרשאה לגשת אל המודול ששמו',
|
||||
'unable_to_create_env_file' => 'לא ניתן ליצור את {filePath} כדי להגדיר את throttle.key',
|
||||
'unable_to_lock_file' => 'לא ניתן לנעול את {filePath}: {reason}',
|
||||
'unable_to_open_lock_file' => 'לא ניתן לפתוח את {filePath}: {reason}',
|
||||
'unable_to_persist_throttle_key' => 'לא ניתן לשמור את throttle.key בקובץ {filePath}',
|
||||
'unable_to_persist_encryption_key' => '',
|
||||
'encryption_key_not_provisioned' => '',
|
||||
'throttle_key_not_provisioned' => '',
|
||||
'unable_to_read_env_file' => 'לא ניתן לקרוא את {filePath} כדי להגדיר את throttle.key',
|
||||
'unknown' => 'שגיאה לא ידועה',
|
||||
];
|
||||
@@ -128,6 +128,7 @@ return [
|
||||
"tax_rate" => "שיעור מס",
|
||||
"taxes" => "מסים",
|
||||
"taxes_summary_report" => "דוח סיכום מסים",
|
||||
"toggle_cost_and_profit" => "",
|
||||
"total" => "סהכ",
|
||||
"total_inventory_value" => "סהכ ערך מלאי",
|
||||
"total_low_sell_quantity" => "סהכ מינימום כמות למכירה",
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"attribute_value_invalid_chars" => "",
|
||||
"confirm_delete" => "",
|
||||
"confirm_restore" => "",
|
||||
"definition_cannot_be_deleted" => "",
|
||||
"definition_invalid_group" => "",
|
||||
"definition_error_adding_updating" => "",
|
||||
"definition_flags" => "",
|
||||
"definition_group" => "",
|
||||
"definition_id" => "",
|
||||
"definition_name" => "",
|
||||
"definition_name_required" => "",
|
||||
"definition_one_or_multiple" => "",
|
||||
"definition_successful_adding" => "",
|
||||
"definition_successful_deleted" => "",
|
||||
"definition_successful_updating" => "",
|
||||
"definition_type" => "",
|
||||
"definition_type_required" => "",
|
||||
"definition_unit" => "",
|
||||
"definition_values" => "",
|
||||
"new" => "",
|
||||
"no_attributes_to_display" => "",
|
||||
"receipt_visibility" => "",
|
||||
"show_in_items" => "",
|
||||
"show_in_items_visibility" => "",
|
||||
"show_in_receipt" => "",
|
||||
"show_in_receivings" => "",
|
||||
"show_in_receivings_visibility" => "",
|
||||
"show_in_sales" => "",
|
||||
"show_in_sales_visibility" => "",
|
||||
"update" => "",
|
||||
'attribute_value_invalid_chars' => '',
|
||||
'confirm_delete' => '',
|
||||
'confirm_restore' => '',
|
||||
'definition_cannot_be_deleted' => '',
|
||||
'definition_invalid_group' => '',
|
||||
'definition_error_adding_updating' => '',
|
||||
'definition_flags' => '',
|
||||
'definition_group' => '',
|
||||
'definition_id' => '',
|
||||
'definition_name' => '',
|
||||
'definition_name_required' => '',
|
||||
'definition_one_or_multiple' => '',
|
||||
'definition_successful_adding' => '',
|
||||
'definition_successful_deleted' => '',
|
||||
'definition_successful_updating' => '',
|
||||
'definition_type' => '',
|
||||
'definition_type_required' => '',
|
||||
'definition_unit' => '',
|
||||
'definition_values' => '',
|
||||
'new' => '',
|
||||
'no_attributes_to_display' => '',
|
||||
'receipt_visibility' => '',
|
||||
'show_in_items' => '',
|
||||
'show_in_items_visibility' => '',
|
||||
'show_in_receipt' => '',
|
||||
'show_in_receivings' => '',
|
||||
'show_in_receivings_visibility' => '',
|
||||
'show_in_sales' => '',
|
||||
'show_in_sales_visibility' => '',
|
||||
'show_in_search' => 'Prikaži u pretrazi',
|
||||
'show_in_search_visibility' => 'Pretraga',
|
||||
'update' => '',
|
||||
];
|
||||
@@ -210,7 +210,6 @@ return [
|
||||
'msg_msg' => 'Saved Text Message',
|
||||
'msg_msg_placeholder' => 'If you wish to use a SMS template save your message here. Otherwise leave the box blank.',
|
||||
'msg_pwd' => 'SMS-API Password',
|
||||
'msg_pwd_required' => 'SMS-API Password is a required field',
|
||||
'msg_src' => 'SMS-API Sender ID',
|
||||
'msg_src_required' => 'SMS-API Sender ID is a required field',
|
||||
'msg_uid' => 'SMS-API Username',
|
||||
@@ -285,7 +284,7 @@ return [
|
||||
'right' => 'Right',
|
||||
'sales_invoice_format' => 'Oblik fakture',
|
||||
'sales_quote_format' => '',
|
||||
'mailpath_invalid' => '',
|
||||
'mailpath_invalid' => 'Nevažeća sendmail putanja. Dopuštena su samo slova, brojevi, crtice, podvlake, kose crte, obrnute kose crte, dvotočke, razmaci i točke.',
|
||||
'saved_successfully' => 'Konfiguracija je uspješno snimljena',
|
||||
'saved_unsuccessfully' => 'Konfiguracija nije uspješno snimljena',
|
||||
'security_issue' => 'Security Vulnerability Warning',
|
||||
|
||||
Loaded 100 of 239 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user