mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-05-25 08:44:42 -04:00
Compare commits
11 Commits
master
...
fix/attrib
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3c2175b04 | ||
|
|
2d93a96ac9 | ||
|
|
c681dc51ed | ||
|
|
65568cf224 | ||
|
|
5cb4371344 | ||
|
|
281f402b8e | ||
|
|
c74fa7aeb5 | ||
|
|
be01555a34 | ||
|
|
abc381d35c | ||
|
|
cacd320206 | ||
|
|
a7a52f800c |
219
.github/workflows/deploy-core.yml
vendored
219
.github/workflows/deploy-core.yml
vendored
@@ -1,219 +0,0 @@
|
||||
name: Deploy Core
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
image_tag:
|
||||
description: 'Docker image tag to deploy'
|
||||
type: string
|
||||
required: true
|
||||
sha:
|
||||
description: 'Git commit SHA to deploy'
|
||||
type: string
|
||||
required: true
|
||||
description:
|
||||
description: 'Deployment description'
|
||||
type: string
|
||||
required: true
|
||||
pr_number:
|
||||
description: 'Pull request number (optional)'
|
||||
type: string
|
||||
required: false
|
||||
outputs:
|
||||
deployment_id:
|
||||
description: 'GitHub deployment ID'
|
||||
value: ${{ jobs.deploy.outputs.deployment_id }}
|
||||
status:
|
||||
description: 'Deployment status (success/failure)'
|
||||
value: ${{ jobs.deploy.outputs.status }}
|
||||
|
||||
concurrency:
|
||||
group: deploy-staging
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy to staging
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
environment:
|
||||
name: staging
|
||||
url: ${{ vars.DEPLOY_URL || 'https://dev.opensourcepos.org' }}
|
||||
deployment: false
|
||||
|
||||
outputs:
|
||||
deployment_id: ${{ steps.deployment.outputs.deployment_id }}
|
||||
status: ${{ steps.webhook.outputs.status }}
|
||||
|
||||
steps:
|
||||
- name: Create GitHub Deployment
|
||||
id: deployment
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
IMAGE_TAG: ${{ inputs.image_tag }}
|
||||
REF_SHA: ${{ inputs.sha }}
|
||||
DESCRIPTION: ${{ inputs.description }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
DEPLOYMENT_ID=$(gh api "repos/${GITHUB_REPOSITORY}/deployments" \
|
||||
-X POST \
|
||||
-f ref="${REF_SHA}" \
|
||||
-f environment="staging" \
|
||||
-f description="${DESCRIPTION}" \
|
||||
-F auto_merge=false \
|
||||
-F required_contexts[] \
|
||||
--jq '.id')
|
||||
|
||||
if [ -z "$DEPLOYMENT_ID" ]; then
|
||||
echo "::error::Failed to create deployment"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "deployment_id=$DEPLOYMENT_ID" >> "$GITHUB_OUTPUT"
|
||||
echo "Created deployment: $DEPLOYMENT_ID"
|
||||
|
||||
- name: Set deployment status to in_progress
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
gh api "repos/${GITHUB_REPOSITORY}/deployments/${{ steps.deployment.outputs.deployment_id }}/statuses" \
|
||||
-X POST \
|
||||
-f state="in_progress" \
|
||||
-f description="Deployment in progress..." \
|
||||
-f log_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
|
||||
|
||||
- name: Trigger deployment webhook
|
||||
id: webhook
|
||||
env:
|
||||
DEPLOY_WEBHOOK_URL: ${{ secrets.DEPLOY_WEBHOOK_URL }}
|
||||
DEPLOY_WEBHOOK_SECRET: ${{ secrets.DEPLOY_WEBHOOK_SECRET }}
|
||||
DOCKER_REPO_NAME: ${{ secrets.DOCKER_REPO_NAME }}
|
||||
IMAGE_TAG: ${{ inputs.image_tag }}
|
||||
REF_SHA: ${{ inputs.sha }}
|
||||
DEPLOYMENT_ID: ${{ steps.deployment.outputs.deployment_id }}
|
||||
PR_NUMBER: ${{ inputs.pr_number }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "$DEPLOY_WEBHOOK_URL" ]; then
|
||||
echo "::error::DEPLOY_WEBHOOK_URL secret is not configured"
|
||||
echo "Please add the DEPLOY_WEBHOOK_URL secret in your repository settings"
|
||||
echo "status=failure" >> "$GITHUB_OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REPO_NAME="${DOCKER_REPO_NAME:-opensourcepos/opensourcepos}"
|
||||
REPO_NAMESPACE="${REPO_NAME%%/*}"
|
||||
REPO_SHORT_NAME="${REPO_NAME#*/}"
|
||||
PUSHED_AT=$(date +%s)
|
||||
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg callback_url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
||||
--argjson pushed_at "$PUSHED_AT" \
|
||||
--arg pusher "$GITHUB_ACTOR" \
|
||||
--arg tag "$IMAGE_TAG" \
|
||||
--arg repo_name "$REPO_NAME" \
|
||||
--arg name "$REPO_SHORT_NAME" \
|
||||
--arg namespace "$REPO_NAMESPACE" \
|
||||
--arg repo_url "https://hub.docker.com/r/${REPO_NAME}/" \
|
||||
--arg deployment_id "$DEPLOYMENT_ID" \
|
||||
--arg repository "$GITHUB_REPOSITORY" \
|
||||
--arg sha "$REF_SHA" \
|
||||
--arg run_id "$GITHUB_RUN_ID" \
|
||||
--arg actor "$GITHUB_ACTOR" \
|
||||
--argjson pr_number "$PR_NUMBER" \
|
||||
'{
|
||||
callback_url: $callback_url,
|
||||
push_data: {pushed_at: $pushed_at, pusher: $pusher, tag: $tag},
|
||||
repository: {repo_name: $repo_name, name: $name, namespace: $namespace, repo_url: $repo_url, status: "Active"},
|
||||
github_deployment: {id: $deployment_id, environment: "staging", repository: $repository, sha: $sha, run_id: $run_id, actor: $actor, pull_request: $pr_number}
|
||||
}')
|
||||
else
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg callback_url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
||||
--argjson pushed_at "$PUSHED_AT" \
|
||||
--arg pusher "$GITHUB_ACTOR" \
|
||||
--arg tag "$IMAGE_TAG" \
|
||||
--arg repo_name "$REPO_NAME" \
|
||||
--arg name "$REPO_SHORT_NAME" \
|
||||
--arg namespace "$REPO_NAMESPACE" \
|
||||
--arg repo_url "https://hub.docker.com/r/${REPO_NAME}/" \
|
||||
--arg deployment_id "$DEPLOYMENT_ID" \
|
||||
--arg repository "$GITHUB_REPOSITORY" \
|
||||
--arg sha "$REF_SHA" \
|
||||
--arg run_id "$GITHUB_RUN_ID" \
|
||||
--arg actor "$GITHUB_ACTOR" \
|
||||
'{
|
||||
callback_url: $callback_url,
|
||||
push_data: {pushed_at: $pushed_at, pusher: $pusher, tag: $tag},
|
||||
repository: {repo_name: $repo_name, name: $name, namespace: $namespace, repo_url: $repo_url, status: "Active"},
|
||||
github_deployment: {id: $deployment_id, environment: "staging", repository: $repository, sha: $sha, run_id: $run_id, actor: $actor}
|
||||
}')
|
||||
fi
|
||||
|
||||
echo "Sending webhook..."
|
||||
echo "Image: ${IMAGE_TAG}"
|
||||
echo "Environment: staging"
|
||||
|
||||
HEADERS=(-H "Content-Type: application/json")
|
||||
|
||||
if [ -n "$DEPLOY_WEBHOOK_SECRET" ]; then
|
||||
SIGNATURE=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$DEPLOY_WEBHOOK_SECRET" | sed 's/.*= //')
|
||||
HEADERS+=(-H "X-Hub-Signature-256: sha256=$SIGNATURE")
|
||||
echo "Using HMAC-SHA256 signature verification"
|
||||
else
|
||||
echo "::warning::DEPLOY_WEBHOOK_SECRET not set - webhook calls will not be signed"
|
||||
echo "For security, configure DEPLOY_WEBHOOK_SECRET in your repository settings"
|
||||
fi
|
||||
|
||||
HTTP_CODE=$(curl -sS --connect-timeout 10 --max-time 120 \
|
||||
-o response.txt -w "%{http_code}" \
|
||||
-X POST \
|
||||
"${HEADERS[@]}" \
|
||||
-d "$PAYLOAD" \
|
||||
"$DEPLOY_WEBHOOK_URL") || HTTP_CODE="000"
|
||||
|
||||
echo "Response code: $HTTP_CODE"
|
||||
if [ -s response.txt ]; then
|
||||
cat response.txt
|
||||
fi
|
||||
|
||||
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
||||
echo "status=success" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "status=failure" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Set deployment status
|
||||
if: always()
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
IMAGE_TAG: ${{ inputs.image_tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
STATE="${{ steps.webhook.outputs.status }}"
|
||||
|
||||
if [ "$STATE" = "success" ]; then
|
||||
DESCRIPTION=$(jq -nr --arg tag "$IMAGE_TAG" \
|
||||
'"Deployed image \($tag) to staging"')
|
||||
|
||||
gh api "repos/${GITHUB_REPOSITORY}/deployments/${{ steps.deployment.outputs.deployment_id }}/statuses" \
|
||||
-X POST \
|
||||
-f state="success" \
|
||||
-f description="$DESCRIPTION"
|
||||
else
|
||||
gh api "repos/${GITHUB_REPOSITORY}/deployments/${{ steps.deployment.outputs.deployment_id }}/statuses" \
|
||||
-X POST \
|
||||
-f state="failure" \
|
||||
-f description="Deployment failed"
|
||||
exit 1
|
||||
fi
|
||||
79
.github/workflows/deploy-pr.yml
vendored
79
.github/workflows/deploy-pr.yml
vendored
@@ -1,79 +0,0 @@
|
||||
name: PR Deploy
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
concurrency:
|
||||
group: staging-deploy
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: Prepare deployment
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
github.event.review.state == 'approved' &&
|
||||
github.event.pull_request.head.repo.full_name == github.repository
|
||||
outputs:
|
||||
image_tag: ${{ steps.image.outputs.tag }}
|
||||
sha: ${{ github.event.pull_request.head.sha }}
|
||||
pr_number: ${{ github.event.pull_request.number }}
|
||||
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Get image tag
|
||||
id: image
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
IMAGE_TAG="pr-${PR_NUMBER}-${PR_SHA:0:7}"
|
||||
echo "tag=$IMAGE_TAG" >> "$GITHUB_OUTPUT"
|
||||
|
||||
deploy:
|
||||
name: Deploy to staging
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/deploy-core.yml
|
||||
with:
|
||||
image_tag: ${{ needs.prepare.outputs.image_tag }}
|
||||
sha: ${{ needs.prepare.outputs.sha }}
|
||||
description: Deploy PR #${{ needs.prepare.outputs.pr_number }} to staging
|
||||
pr_number: ${{ needs.prepare.outputs.pr_number }}
|
||||
secrets: inherit
|
||||
|
||||
comment:
|
||||
name: Comment deployment status
|
||||
needs: [prepare, deploy]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
IMAGE_TAG: ${{ needs.prepare.outputs.image_tag }}
|
||||
PR_NUMBER: ${{ needs.prepare.outputs.pr_number }}
|
||||
REF_SHA: ${{ needs.prepare.outputs.sha }}
|
||||
STATUS: ${{ needs.deploy.outputs.status }}
|
||||
|
||||
steps:
|
||||
- name: Comment on PR
|
||||
run: |
|
||||
if [ "$STATUS" = "success" ]; then
|
||||
BODY=$(jq -nr --arg tag "$IMAGE_TAG" --arg sha "$REF_SHA" --arg url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
||||
'"✅ **Staging deployment completed**\n\n🔗 **URL**: https://dev.opensourcepos.org\n📦 **Image Tag**: `\($tag)`\n🔨 **Commit**: \($sha)\n\nView logs: \($url)"')
|
||||
else
|
||||
BODY=$(jq -nr --arg url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
||||
'"❌ **Staging deployment failed**\n\nCheck the [workflow logs](\($url)) for details."')
|
||||
fi
|
||||
|
||||
gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \
|
||||
-X POST \
|
||||
-f body="$BODY"
|
||||
23
.github/workflows/deploy.yml
vendored
23
.github/workflows/deploy.yml
vendored
@@ -1,23 +0,0 @@
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
image_tag:
|
||||
description: 'Docker image tag to deploy (e.g., v3.4.0, latest)'
|
||||
required: true
|
||||
default: 'latest'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy to staging
|
||||
uses: ./.github/workflows/deploy-core.yml
|
||||
with:
|
||||
image_tag: ${{ inputs.image_tag }}
|
||||
sha: ${{ github.sha }}
|
||||
description: Deploy image ${{ inputs.image_tag }}
|
||||
secrets: inherit
|
||||
@@ -5,7 +5,6 @@ namespace Config;
|
||||
use App\Models\Appconfig;
|
||||
use CodeIgniter\Cache\CacheInterface;
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
use Config\Database;
|
||||
|
||||
/**
|
||||
* This class holds the configuration options stored from the database so that on launch those settings can be cached
|
||||
@@ -14,7 +13,7 @@ use Config\Database;
|
||||
*/
|
||||
class OSPOS extends BaseConfig
|
||||
{
|
||||
public array $settings = [];
|
||||
public array $settings;
|
||||
public string $commit_sha1 = 'dev'; // TODO: Travis scripts need to be updated to replace this with the commit hash on build
|
||||
private CacheInterface $cache;
|
||||
|
||||
@@ -34,37 +33,28 @@ class OSPOS extends BaseConfig
|
||||
|
||||
if ($cache) {
|
||||
$this->settings = decode_array($cache);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$db = Database::connect();
|
||||
|
||||
if (!$db->tableExists('app_config')) {
|
||||
$this->settings = $this->getDefaultSettings();
|
||||
return;
|
||||
} else {
|
||||
try {
|
||||
$appconfig = model(Appconfig::class);
|
||||
foreach ($appconfig->get_all()->getResult() as $app_config) {
|
||||
$this->settings[$app_config->key] = $app_config->value;
|
||||
}
|
||||
$this->cache->save('settings', encode_array($this->settings));
|
||||
} catch (\Exception $e) {
|
||||
// Database table doesn't exist yet (migrations haven't run)
|
||||
// or database connection failed. Return empty settings to
|
||||
// allow migration page to display. Catches mysqli_sql_exception
|
||||
// which is not a subclass of DatabaseException.
|
||||
$this->settings = [
|
||||
'language' => 'english',
|
||||
'language_code' => 'en',
|
||||
'company' => 'Home',
|
||||
'barcode_type' => 'Code39'
|
||||
];
|
||||
}
|
||||
|
||||
$appconfig = model(Appconfig::class);
|
||||
foreach ($appconfig->get_all()->getResult() as $app_config) {
|
||||
$this->settings[$app_config->key] = $app_config->value;
|
||||
}
|
||||
$this->cache->save('settings', encode_array($this->settings));
|
||||
} catch (\Exception $e) {
|
||||
$this->settings = $this->getDefaultSettings();
|
||||
}
|
||||
}
|
||||
|
||||
private function getDefaultSettings(): array
|
||||
{
|
||||
return [
|
||||
'language' => 'english',
|
||||
'language_code' => 'en',
|
||||
'company' => 'Home',
|
||||
'barcode_type' => 'Code39'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
@@ -73,4 +63,4 @@ class OSPOS extends BaseConfig
|
||||
$this->cache->delete('settings');
|
||||
$this->set_settings();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,7 +246,7 @@ class Attributes extends Secure_Controller
|
||||
$data['definition_group'][''] = lang('Common.none_selected_text');
|
||||
$data['definition_info'] = $info;
|
||||
|
||||
$show_all = Attribute::SHOW_IN_ITEMS | Attribute::SHOW_IN_RECEIVINGS | Attribute::SHOW_IN_SALES;
|
||||
$show_all = Attribute::SHOW_IN_ITEMS | Attribute::SHOW_IN_RECEIVINGS | Attribute::SHOW_IN_SALES | Attribute::SHOW_IN_SEARCH;
|
||||
$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);
|
||||
|
||||
@@ -105,13 +105,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');
|
||||
|
||||
$definition_names = $this->attribute->get_definitions_by_flags(Attribute::SHOW_IN_ITEMS);
|
||||
|
||||
$sort = $this->sanitizeSortColumn(item_sort_columns(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'items.item_id');
|
||||
$order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
|
||||
|
||||
$this->item_lib->set_item_location($this->request->getGet('stock_location'));
|
||||
|
||||
$definition_names = $this->attribute->get_definitions_by_flags(Attribute::SHOW_IN_ITEMS);
|
||||
|
||||
$filters = [
|
||||
'start_date' => $this->request->getGet('start_date'),
|
||||
'end_date' => $this->request->getGet('end_date'),
|
||||
@@ -129,6 +130,13 @@ class Items extends Secure_Controller
|
||||
// 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);
|
||||
|
||||
// When search_custom is enabled, include attributes that are searchable but may not be visible in table
|
||||
if (!empty($filters['search_custom'])) {
|
||||
$searchable_definitions = $this->attribute->get_definitions_by_flags(Attribute::SHOW_IN_ITEMS | Attribute::SHOW_IN_SEARCH);
|
||||
$filters['definition_ids'] = array_keys($searchable_definitions);
|
||||
}
|
||||
|
||||
$items = $this->item->search($search, $filters, $limit, $offset, $sort, $order);
|
||||
$total_rows = $this->item->get_found_rows($search, $filters);
|
||||
$data_rows = [];
|
||||
@@ -1055,20 +1063,14 @@ class Items extends Secure_Controller
|
||||
});
|
||||
|
||||
if (!$isFailedRow && $this->item->save_value($itemData, $itemId)) {
|
||||
if (!$this->save_tax_data($row, $itemData)) {
|
||||
$isFailedRow = true;
|
||||
}
|
||||
if (!$this->save_inventory_quantities($row, $itemData, $allowedStockLocations, $employeeId)) {
|
||||
$isFailedRow = true;
|
||||
}
|
||||
$this->save_tax_data($row, $itemData);
|
||||
$this->save_inventory_quantities($row, $itemData, $allowedStockLocations, $employeeId);
|
||||
$csvAttributeValues = $this->extractAttributeData($row);
|
||||
if (!$this->attribute->saveCSVRowAttributeData($csvAttributeValues, $itemData, $attributeData)) {
|
||||
$isFailedRow = true;
|
||||
}
|
||||
$isFailedRow = !$this->attribute->saveCSVRowAttributeData($csvAttributeValues, $itemData, $attributeData);
|
||||
if ($isFailedRow) {
|
||||
$failedRow = $key + 2;
|
||||
$failCodes[] = $failedRow;
|
||||
log_message('error', "CSV Item import failed on line $failedRow while saving item.");
|
||||
log_message('error', "CSV Item import failed on line $failedRow while saving attributes.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1258,15 +1260,13 @@ class Items extends Secure_Controller
|
||||
* @param array $item_data
|
||||
* @param array $allowed_locations
|
||||
* @param int $employee_id
|
||||
* @return bool Returns true on success, false on failure
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
private function save_inventory_quantities(array $row, array $item_data, array $allowed_locations, int $employee_id): bool
|
||||
private function save_inventory_quantities(array $row, array $item_data, array $allowed_locations, int $employee_id): void
|
||||
{
|
||||
// Quantities & Inventory Section
|
||||
$comment = lang('Items.inventory_CSV_import_quantity');
|
||||
$is_update = (bool)$row['Id'];
|
||||
$success = true;
|
||||
|
||||
foreach ($allowed_locations as $location_id => $location_name) {
|
||||
$item_quantity_data = ['item_id' => $item_data['item_id'], 'location_id' => $location_id];
|
||||
@@ -1280,22 +1280,20 @@ class Items extends Secure_Controller
|
||||
|
||||
if (!empty($row["location_$location_name"]) || $row["location_$location_name"] === '0') {
|
||||
$item_quantity_data['quantity'] = $row["location_$location_name"];
|
||||
$success &= $this->item_quantity->save_value($item_quantity_data, $item_data['item_id'], $location_id);
|
||||
$this->item_quantity->save_value($item_quantity_data, $item_data['item_id'], $location_id);
|
||||
|
||||
$csv_data['trans_inventory'] = $row["location_$location_name"];
|
||||
$success &= (bool)$this->inventory->insert($csv_data, false);
|
||||
$this->inventory->insert($csv_data, false);
|
||||
} elseif ($is_update) {
|
||||
continue;
|
||||
return;
|
||||
} else {
|
||||
$item_quantity_data['quantity'] = 0;
|
||||
$success &= $this->item_quantity->save_value($item_quantity_data, $item_data['item_id'], $location_id);
|
||||
$this->item_quantity->save_value($item_quantity_data, $item_data['item_id'], $location_id);
|
||||
|
||||
$csv_data['trans_inventory'] = 0;
|
||||
$success &= (bool)$this->inventory->insert($csv_data, false);
|
||||
$this->inventory->insert($csv_data, false);
|
||||
}
|
||||
}
|
||||
|
||||
return (bool)$success;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1303,9 +1301,8 @@ class Items extends Secure_Controller
|
||||
*
|
||||
* @param array $row
|
||||
* @param array $item_data
|
||||
* @return bool Returns true on success, false on failure
|
||||
*/
|
||||
private function save_tax_data(array $row, array $item_data): bool
|
||||
private function save_tax_data(array $row, array $item_data): void
|
||||
{
|
||||
$items_taxes_data = [];
|
||||
|
||||
@@ -1317,11 +1314,9 @@ class Items extends Secure_Controller
|
||||
$items_taxes_data[] = ['name' => $row['Tax 2 Name'], 'percent' => $row['Tax 2 Percent']];
|
||||
}
|
||||
|
||||
if (!empty($items_taxes_data)) {
|
||||
return $this->item_taxes->save_value($items_taxes_data, $item_data['item_id']);
|
||||
if (isset($items_taxes_data)) {
|
||||
$this->item_taxes->save_value($items_taxes_data, $item_data['item_id']);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -49,13 +49,6 @@ class Login extends BaseController
|
||||
return view('login', $data);
|
||||
}
|
||||
|
||||
if (!$data['is_latest'] || $data['is_new_install']) {
|
||||
set_time_limit(3600);
|
||||
|
||||
$migration->setNamespace('App')->latest();
|
||||
return redirect()->to('login');
|
||||
}
|
||||
|
||||
$rules = ['username' => 'required|login_check[data]'];
|
||||
$messages = [
|
||||
'username' => [
|
||||
@@ -69,6 +62,13 @@ class Login extends BaseController
|
||||
|
||||
return view('login', $data);
|
||||
}
|
||||
|
||||
if (!$data['is_latest']) {
|
||||
set_time_limit(3600);
|
||||
|
||||
$migration->setNamespace('App')->latest();
|
||||
return redirect()->to('login');
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->to('home');
|
||||
@@ -79,18 +79,18 @@ class Login extends BaseController
|
||||
try {
|
||||
$migration = new MY_Migration(config('Migrations'));
|
||||
$migration->migrate_to_ci4();
|
||||
|
||||
|
||||
set_time_limit(3600);
|
||||
$migration->setNamespace('App')->latest();
|
||||
|
||||
|
||||
return $this->response->setJSON([
|
||||
'success' => true,
|
||||
'message' => 'Migration completed successfully'
|
||||
]);
|
||||
|
||||
|
||||
} catch (\Exception $e) {
|
||||
log_message('error', 'Migration failed: ' . $e->getMessage());
|
||||
|
||||
|
||||
return $this->response->setJSON([
|
||||
'success' => false,
|
||||
'message' => 'Migration failed: ' . $e->getMessage()
|
||||
|
||||
@@ -161,7 +161,7 @@ class Sales extends Secure_Controller
|
||||
'only_bank_transfer'=> false,
|
||||
'only_wallet' => false,
|
||||
'only_invoices' => $this->config['invoice_enable'] && $this->request->getGet('only_invoices', FILTER_SANITIZE_NUMBER_INT),
|
||||
'is_valid_receipt' => $this->sale->isValidReceipt($search)
|
||||
'is_valid_receipt' => $this->sale->is_valid_receipt($search)
|
||||
];
|
||||
|
||||
// Check if any filter is set in the multiselect dropdown
|
||||
@@ -198,7 +198,7 @@ class Sales extends Secure_Controller
|
||||
? $this->request->getGet('term')
|
||||
: null;
|
||||
|
||||
if ($this->sale_lib->get_mode() == 'return' && $this->sale->isValidReceipt($receipt)) {
|
||||
if ($this->sale_lib->get_mode() == 'return' && $this->sale->is_valid_receipt($receipt)) {
|
||||
// If a valid receipt or invoice was found the search term will be replaced with a receipt number (POS #)
|
||||
$suggestions[] = $receipt;
|
||||
}
|
||||
@@ -525,7 +525,7 @@ class Sales extends Secure_Controller
|
||||
$quantity = ($mode == 'return') ? -$quantity : $quantity;
|
||||
$item_location = $this->sale_lib->get_sale_location();
|
||||
|
||||
if ($mode == 'return' && $this->sale->isValidReceipt($item_id_or_number_or_item_kit_or_receipt)) {
|
||||
if ($mode == 'return' && $this->sale->is_valid_receipt($item_id_or_number_or_item_kit_or_receipt)) {
|
||||
$this->sale_lib->return_entire_sale($item_id_or_number_or_item_kit_or_receipt);
|
||||
} elseif ($this->item_kit->is_valid_item_kit($item_id_or_number_or_item_kit_or_receipt)) {
|
||||
// Add kit item to order if one is assigned
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class Migration_Upgrade_To_3_1_1 extends Migration
|
||||
@@ -18,37 +17,7 @@ class Migration_Upgrade_To_3_1_1 extends Migration
|
||||
public function up(): void
|
||||
{
|
||||
helper('migration');
|
||||
|
||||
// MariaDB blocks CONVERT TO CHARACTER SET on tables with FK constraints.
|
||||
// Drop all FKs across affected tables before running the SQL script, recreate after.
|
||||
$fkColumns = [
|
||||
['modules', 'module_id'],
|
||||
['stock_locations', 'location_id'],
|
||||
['permissions', 'permission_id'],
|
||||
['people', 'person_id'],
|
||||
['suppliers', 'supplier_id'],
|
||||
['items', 'item_id'],
|
||||
['item_kits', 'item_kit_id'],
|
||||
['sales', 'sale_id'],
|
||||
['receivings', 'receiving_id'],
|
||||
['employees', 'employee_id'],
|
||||
['customers', 'person_id'],
|
||||
];
|
||||
|
||||
$constraints = [];
|
||||
foreach ($fkColumns as [$table, $column]) {
|
||||
foreach (dropAllForeignKeyConstraints($table, $column) as $c) {
|
||||
$constraints[$c['constraintName']] = $c;
|
||||
}
|
||||
}
|
||||
|
||||
if (!execute_script(APPPATH . 'Database/Migrations/sqlscripts/3.0.2_to_3.1.1.sql')) {
|
||||
throw new DatabaseException('Migration script 3.0.2_to_3.1.1.sql failed. Check logs for details.');
|
||||
}
|
||||
|
||||
$droppedTables = ['sales_suspended', 'sales_suspended_items', 'sales_suspended_items_taxes', 'sales_suspended_payments'];
|
||||
$toRecreate = array_filter($constraints, fn($c) => !in_array($c['tableName'], $droppedTables, true));
|
||||
recreateForeignKeyConstraints(array_values($toRecreate));
|
||||
execute_script(APPPATH . 'Database/Migrations/sqlscripts/3.0.2_to_3.1.1.sql');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -327,6 +327,19 @@ INSERT INTO `ospos_sales_items` (sale_id, item_id, description, serialnumber, li
|
||||
INSERT INTO `ospos_sales_payments` (sale_id, payment_type, payment_amount) SELECT sale_id, payment_type, payment_amount FROM `ospos_sales_suspended_payments`;
|
||||
INSERT INTO `ospos_sales_items_taxes` (sale_id, item_id, line, name, percent) SELECT sale_id, item_id, line, name, percent FROM `ospos_sales_suspended_items_taxes`;
|
||||
|
||||
ALTER TABLE `ospos_sales_suspended_payments` DROP FOREIGN KEY `ospos_sales_suspended_payments_ibfk_1`;
|
||||
|
||||
ALTER TABLE `ospos_sales_suspended_items_taxes` DROP FOREIGN KEY `ospos_sales_suspended_items_taxes_ibfk_1`;
|
||||
ALTER TABLE `ospos_sales_suspended_items_taxes` DROP FOREIGN KEY `ospos_sales_suspended_items_taxes_ibfk_2`;
|
||||
|
||||
ALTER TABLE `ospos_sales_suspended_items` DROP FOREIGN KEY `ospos_sales_suspended_items_ibfk_1`;
|
||||
ALTER TABLE `ospos_sales_suspended_items` DROP FOREIGN KEY `ospos_sales_suspended_items_ibfk_2`;
|
||||
ALTER TABLE `ospos_sales_suspended_items` DROP FOREIGN KEY `ospos_sales_suspended_items_ibfk_3`;
|
||||
|
||||
ALTER TABLE `ospos_sales_suspended` DROP FOREIGN KEY `ospos_sales_suspended_ibfk_1`;
|
||||
ALTER TABLE `ospos_sales_suspended` DROP FOREIGN KEY `ospos_sales_suspended_ibfk_2`;
|
||||
ALTER TABLE `ospos_sales_suspended` DROP FOREIGN KEY `ospos_sales_suspended_ibfk_3`;
|
||||
|
||||
DROP TABLE `ospos_sales_suspended_payments`, `ospos_sales_suspended_items_taxes`, `ospos_sales_suspended_items`, `ospos_sales_suspended`;
|
||||
|
||||
--
|
||||
|
||||
@@ -140,7 +140,7 @@ CREATE TABLE IF NOT EXISTS `ospos_expense_categories` (
|
||||
`category_name` varchar(255) DEFAULT NULL,
|
||||
`category_description` varchar(255) NOT NULL,
|
||||
`deleted` int(1) NOT NULL DEFAULT '0'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
|
||||
-- Table structure for table `ospos_expenses`
|
||||
@@ -154,7 +154,7 @@ CREATE TABLE IF NOT EXISTS `ospos_expenses` (
|
||||
`description` varchar(255) NOT NULL,
|
||||
`employee_id` int(10) NOT NULL,
|
||||
`deleted` int(1) NOT NULL DEFAULT '0'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
|
||||
-- Indexes for table `ospos_expense_categories`
|
||||
|
||||
@@ -75,7 +75,7 @@ CREATE TABLE `ospos_cash_up` (
|
||||
`open_employee_id` int(10) NOT NULL,
|
||||
`close_employee_id` int(10) NOT NULL,
|
||||
`deleted` int(1) NOT NULL DEFAULT '0'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
-- Indexes for table `ospos_cash_up`
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS `ospos_tax_codes` (
|
||||
`state` varchar(255) NOT NULL DEFAULT '',
|
||||
`deleted` int(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`tax_code_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
ALTER TABLE `ospos_customers`
|
||||
ADD COLUMN `tax_id` varchar(32) NOT NULL DEFAULT '' AFTER `taxable`,
|
||||
@@ -59,7 +59,7 @@ CREATE TABLE `ospos_sales_taxes` (
|
||||
`rounding_code` tinyint(2) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`sales_taxes_id`),
|
||||
KEY `print_sequence` (`sale_id`,`print_sequence`,`tax_group`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `ospos_tax_jurisdictions` (
|
||||
`jurisdiction_id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
@@ -71,7 +71,7 @@ CREATE TABLE IF NOT EXISTS `ospos_tax_jurisdictions` (
|
||||
`cascade_sequence` tinyint(2) NOT NULL DEFAULT 0,
|
||||
`deleted` int(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`jurisdiction_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci AUTO_INCREMENT=1;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;
|
||||
|
||||
ALTER TABLE `ospos_suppliers`
|
||||
ADD COLUMN `tax_id` varchar(32) DEFAULT NULL AFTER `account_number`;
|
||||
@@ -89,7 +89,7 @@ CREATE TABLE IF NOT EXISTS `ospos_tax_rates` (
|
||||
`tax_rate` decimal(15,4) NOT NULL DEFAULT 0.0000,
|
||||
`tax_rounding_code` tinyint(2) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`tax_rate_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
-- Add support for sales tax report
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ CREATE TABLE `ospos_sales_payments` (
|
||||
`reference_code` varchar(40) NOT NULL DEFAULT '',
|
||||
PRIMARY KEY (`payment_id`),
|
||||
KEY `payment_sale` (`sale_id`, `payment_type`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
|
||||
INSERT INTO ospos_sales_payments (sale_id, payment_type, payment_amount, payment_user)
|
||||
SELECT payments.sale_id, payments.payment_type, payments.payment_amount, sales.employee_id
|
||||
|
||||
@@ -172,7 +172,6 @@ function dropAllForeignKeyConstraints(string $table, string $column): array {
|
||||
WHERE kcu.TABLE_SCHEMA = DATABASE()
|
||||
AND ((kcu.REFERENCED_TABLE_NAME = '" . $db->getPrefix() . "$table' AND kcu.REFERENCED_COLUMN_NAME = '$column')
|
||||
OR (kcu.TABLE_NAME = '" . $db->getPrefix() . "$table' AND kcu.COLUMN_NAME = '$column'))
|
||||
AND rc.CONSTRAINT_NAME IS NOT NULL
|
||||
");
|
||||
|
||||
$deletedConstraints = [];
|
||||
|
||||
@@ -402,6 +402,25 @@ function item_headers(): array
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all sortable column keys for items table, including dynamic attribute columns.
|
||||
*
|
||||
* @return array Array of column headers in format expected by sanitizeSortColumn
|
||||
*/
|
||||
function item_sort_columns(): array
|
||||
{
|
||||
$attribute = model(Attribute::class);
|
||||
$definitionIds = array_keys($attribute->get_definitions_by_flags($attribute::SHOW_IN_ITEMS));
|
||||
|
||||
$headers = item_headers();
|
||||
|
||||
foreach ($definitionIds as $definitionId) {
|
||||
$headers[] = [(string) $definitionId => ''];
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the header for the items tabular view
|
||||
*/
|
||||
@@ -422,7 +441,7 @@ 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];
|
||||
$headers[] = [$definition_id => $definitionInfo['name'], 'sortable' => true];
|
||||
}
|
||||
|
||||
$headers[] = ['inventory' => '', 'escape' => false];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "استلام البضائع",
|
||||
"show_in_sales" => "اظهار خلال البيع",
|
||||
"show_in_sales_visibility" => "البيع",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "تحديث الميزات",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "استلام البضائع",
|
||||
"show_in_sales" => "اظهار خلال البيع",
|
||||
"show_in_sales_visibility" => "البيع",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "تحديث الميزات",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "Alınanlar",
|
||||
"show_in_sales" => "Satışda göstərin",
|
||||
"show_in_sales_visibility" => "Satışlar",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "Atributları yenilə",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "",
|
||||
"show_in_sales" => "",
|
||||
"show_in_sales_visibility" => "",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "Ulazi",
|
||||
"show_in_sales" => "Prikaži u prodaji",
|
||||
"show_in_sales_visibility" => "Prodaja",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "Ažuriraj atribut",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "بەدەستگەیشتووکان",
|
||||
"show_in_sales" => "لە فرۆشتندا نیشانی بدە",
|
||||
"show_in_sales_visibility" => "فرۆشتن",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "تایبەتمەندی نوێ بکەرەوە",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "",
|
||||
"show_in_sales" => "",
|
||||
"show_in_sales_visibility" => "",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "Modtagelser",
|
||||
"show_in_sales" => "Vis i salg",
|
||||
"show_in_sales_visibility" => "Salg",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "Opdater egenskab",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "",
|
||||
"show_in_sales" => "",
|
||||
"show_in_sales_visibility" => "",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"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",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "",
|
||||
"show_in_sales" => "",
|
||||
"show_in_sales_visibility" => "",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"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",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"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",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"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",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "Recepciones",
|
||||
"show_in_sales" => "Mostrar en Ventas",
|
||||
"show_in_sales_visibility" => "Ventas",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "Actualizar atributo",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "دریافت",
|
||||
"show_in_sales" => "نمایش در فروش",
|
||||
"show_in_sales_visibility" => "حراجی",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "به روز کردن ویژگی",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"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",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "קבלת סחורה",
|
||||
"show_in_sales" => "הצג במכירות",
|
||||
"show_in_sales_visibility" => "מכירות",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "עדכן מאפיין",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "",
|
||||
"show_in_sales" => "",
|
||||
"show_in_sales_visibility" => "",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "Áruátvételek",
|
||||
"show_in_sales" => "Megjelenítés az értékesítésekben",
|
||||
"show_in_sales_visibility" => "Értékesítések",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "Tulajdonság frissítése",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"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",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "Penerimaan",
|
||||
"show_in_sales" => "Tampilkan dalam penjualan",
|
||||
"show_in_sales_visibility" => "Penjualan",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "Perbarui Atribut",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "Ricezione",
|
||||
"show_in_sales" => "Visualizza in vendite",
|
||||
"show_in_sales_visibility" => "Vendite",
|
||||
"show_in_search" => "Visualizza nella ricerca",
|
||||
"show_in_search_visibility" => "Ricerca",
|
||||
"update" => "Aggiorna attributo",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "",
|
||||
"show_in_sales" => "",
|
||||
"show_in_sales_visibility" => "",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "",
|
||||
"show_in_sales" => "",
|
||||
"show_in_sales_visibility" => "",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"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",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"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",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "Orders",
|
||||
"show_in_sales" => "Toon in verkoop",
|
||||
"show_in_sales_visibility" => "Verkoop",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "Wijzig Attribuut",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "Leveringen",
|
||||
"show_in_sales" => "Weergeven in verkopen",
|
||||
"show_in_sales_visibility" => "Verkopen",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "Kenmerk bijwerken",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "Dostawy",
|
||||
"show_in_sales" => "Pokaż w sprzedażach",
|
||||
"show_in_sales_visibility" => "Sprzedaże",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "Zaktualizuj atrybut",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "Recebimentos",
|
||||
"show_in_sales" => "Mostrar em vendas",
|
||||
"show_in_sales_visibility" => "Vendas",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "Atualizar atributo",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "Receptii",
|
||||
"show_in_sales" => "Arata in vanzari",
|
||||
"show_in_sales_visibility" => "Vanzari",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "Actualizare Atribut",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "Закупки",
|
||||
"show_in_sales" => "Показать в продажах",
|
||||
"show_in_sales_visibility" => "Продажи",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "Обновить атрибут",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "Inleveranser",
|
||||
"show_in_sales" => "Visa i försäljning",
|
||||
"show_in_sales_visibility" => "Försäljning",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "Uppdatera attribut",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "Manunuzi",
|
||||
"show_in_sales" => "Onyesha kwenye Mauzo",
|
||||
"show_in_sales_visibility" => "Mauzo",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "Sasisha Sifa",
|
||||
];
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "Manunuzi",
|
||||
"show_in_sales" => "Onyesha kwenye Mauzo",
|
||||
"show_in_sales_visibility" => "Mauzo",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "Sasisha Sifa",
|
||||
];
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"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",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "สินค้าขาเข้า",
|
||||
"show_in_sales" => "แสดงใน การขาย",
|
||||
"show_in_sales_visibility" => "การขาย",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "ปรับปรุงแอตทริบิวต์",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"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",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "Alacaklar",
|
||||
"show_in_sales" => "Satışlarda göster",
|
||||
"show_in_sales_visibility" => "Satışlar",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "Nitelik Güncelle",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "Надходження",
|
||||
"show_in_sales" => "Показати в продажах",
|
||||
"show_in_sales_visibility" => "Продажі",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "Оновити атрибут",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"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",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "Nhập hàng",
|
||||
"show_in_sales" => "Hiển thị trong bán hàng",
|
||||
"show_in_sales_visibility" => "Bán hàng",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "Cập nhật thuộc tính",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "收据",
|
||||
"show_in_sales" => "在销售中显示",
|
||||
"show_in_sales_visibility" => "销售",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "更新属性",
|
||||
];
|
||||
|
||||
@@ -30,5 +30,7 @@ return [
|
||||
"show_in_receivings_visibility" => "收貨",
|
||||
"show_in_sales" => "在銷售中顯示",
|
||||
"show_in_sales_visibility" => "銷售",
|
||||
"show_in_search" => "Show in search",
|
||||
"show_in_search_visibility" => "Search",
|
||||
"update" => "更新屬性",
|
||||
];
|
||||
|
||||
@@ -25,7 +25,7 @@ class MY_Migration extends MigrationRunner
|
||||
public function get_latest_migration(): int
|
||||
{
|
||||
$migrations = $this->findMigrations();
|
||||
return (int) basename(end($migrations)->version);
|
||||
return basename(end($migrations)->version);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,7 +41,7 @@ class MY_Migration extends MigrationRunner
|
||||
$builder = $db->table('migrations');
|
||||
$builder->select('version')->orderBy('version', 'DESC')->limit(1);
|
||||
$result = $builder->get()->getRow();
|
||||
return $result ? (int) $result->version : 0;
|
||||
return $result ? $result->version : 0;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Database not available yet (e.g. fresh install before schema).
|
||||
|
||||
@@ -38,9 +38,10 @@ class Attribute extends Model
|
||||
'attribute_decimal'
|
||||
];
|
||||
|
||||
public const SHOW_IN_ITEMS = 1; // TODO: These need to be moved to constants.php
|
||||
public const SHOW_IN_ITEMS = 1;
|
||||
public const SHOW_IN_SALES = 2;
|
||||
public const SHOW_IN_RECEIVINGS = 4;
|
||||
public const SHOW_IN_SEARCH = 8;
|
||||
public function deleteDropdownAttributeValue(string $attribute_value, int $definition_id): bool
|
||||
{
|
||||
$attribute_id = $this->getAttributeIdByValue($attribute_value);
|
||||
@@ -601,10 +602,6 @@ class Attribute extends Model
|
||||
*/
|
||||
public function saveAttributeLink(int $itemId, int $definitionId, int $attributeId): bool
|
||||
{
|
||||
if ($attributeId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$normalizedItemId = empty($itemId) ? null : $itemId;
|
||||
$normalizedAttributeId = empty($attributeId) ? null : $attributeId;
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ class Item extends Model
|
||||
'allow_alt_description',
|
||||
'is_serialized'
|
||||
];
|
||||
|
||||
protected $table = 'items';
|
||||
protected $primaryKey = 'item_id';
|
||||
protected $useAutoIncrement = true;
|
||||
@@ -58,7 +59,6 @@ class Item extends Model
|
||||
'hsn_code'
|
||||
];
|
||||
|
||||
|
||||
/**
|
||||
* Determines if a given item_id is an item
|
||||
*/
|
||||
@@ -132,32 +132,186 @@ class Item extends Model
|
||||
return $this->search($search, $filters, 0, 0, 'items.name', 'asc', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse search string for attribute-specific queries
|
||||
* Supports syntax like "color: blue size: large" or "color:blue AND size:large"
|
||||
*
|
||||
* @param string $search The raw search string
|
||||
* @return array{terms: array, attributes: array} Parsed terms and attribute queries
|
||||
*/
|
||||
public function parseAttributeSearch(string $search): array
|
||||
{
|
||||
$result = [
|
||||
'terms' => [],
|
||||
'attributes' => []
|
||||
];
|
||||
|
||||
if ($search === '') {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$pattern = '/([[:alpha:]][[:alnum:] _-]*?)\s*:\s*([^\s,]+)(?:\s+(?:AND|OR)\s+)?/iu';
|
||||
$remaining = preg_replace($pattern, '', $search);
|
||||
|
||||
if (preg_match_all($pattern, $search, $matches, PREG_SET_ORDER)) {
|
||||
foreach ($matches as $match) {
|
||||
$attrName = strtolower(trim($match[1]));
|
||||
$attrValue = trim($match[2]);
|
||||
$result['attributes'][$attrName][] = $attrValue;
|
||||
}
|
||||
}
|
||||
|
||||
$remaining = trim(preg_replace('/\s+/', ' ', $remaining));
|
||||
if ($remaining !== '') {
|
||||
$result['terms'][] = $remaining;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for items by attribute values
|
||||
* Returns an array of item_ids matching the attribute search criteria
|
||||
*
|
||||
* @param string $search Search term
|
||||
* @param array $definitionIds Attribute definition IDs to search within
|
||||
* @param bool $matchDeleted Whether to match items where deleted flag equals this value
|
||||
* @param string $logic 'AND' or 'OR' for multiple attribute matching
|
||||
* @return array Array of matching item_ids
|
||||
*/
|
||||
public function searchByAttributes(string $search, array $definitionIds, bool $matchDeleted = false, string $logic = 'OR'): array
|
||||
{
|
||||
if ($definitionIds === [] || $search === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$parsed = $this->parseAttributeSearch($search);
|
||||
$matchingItemIds = [];
|
||||
|
||||
if (!empty($parsed['attributes'])) {
|
||||
$attribute = model(Attribute::class);
|
||||
$allDefinitions = $attribute->get_definitions_by_flags(Attribute::SHOW_IN_ITEMS | Attribute::SHOW_IN_SEARCH, true);
|
||||
$definitionNameToId = [];
|
||||
|
||||
foreach ($allDefinitions as $id => $defInfo) {
|
||||
$name = is_array($defInfo) ? $defInfo['name'] : $defInfo;
|
||||
$definitionNameToId[strtolower($name)] = (int) $id;
|
||||
}
|
||||
|
||||
foreach ($parsed['attributes'] as $attrName => $values) {
|
||||
if (!isset($definitionNameToId[$attrName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$definitionId = $definitionNameToId[$attrName];
|
||||
|
||||
// Skip if this attribute is not in the caller-provided definitionIds filter
|
||||
if (!in_array($definitionId, $definitionIds, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($values as $value) {
|
||||
$builder = $this->db->table('attribute_links');
|
||||
$builder->select('DISTINCT attribute_links.item_id');
|
||||
$builder->join('attribute_values', 'attribute_values.attribute_id = attribute_links.attribute_id');
|
||||
$builder->join('items', 'items.item_id = attribute_links.item_id');
|
||||
$builder->groupStart();
|
||||
$builder->like('attribute_values.attribute_value', $value);
|
||||
$builder->orWhere('attribute_values.attribute_decimal', $value);
|
||||
$builder->orWhere('attribute_values.attribute_date', $value);
|
||||
$builder->groupEnd();
|
||||
$builder->where('attribute_links.definition_id', $definitionId);
|
||||
$builder->where('attribute_links.sale_id', null);
|
||||
$builder->where('attribute_links.receiving_id', null);
|
||||
$builder->where('items.deleted', $matchDeleted);
|
||||
|
||||
$foundIds = array_column($builder->get()->getResultArray(), 'item_id');
|
||||
|
||||
if ($logic === 'AND') {
|
||||
if (empty($matchingItemIds)) {
|
||||
$matchingItemIds = $foundIds;
|
||||
} else {
|
||||
$matchingItemIds = array_intersect($matchingItemIds, $foundIds);
|
||||
}
|
||||
} else {
|
||||
$matchingItemIds = array_unique(array_merge($matchingItemIds, $foundIds));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($parsed['terms'])) {
|
||||
$term = implode(' ', $parsed['terms']);
|
||||
$termIds = $this->searchByAttributeValue($term, $definitionIds, $matchDeleted);
|
||||
|
||||
if (empty($matchingItemIds)) {
|
||||
return $termIds;
|
||||
}
|
||||
|
||||
return $logic === 'AND'
|
||||
? array_values(array_intersect($matchingItemIds, $termIds))
|
||||
: array_values(array_unique(array_merge($matchingItemIds, $termIds)));
|
||||
}
|
||||
|
||||
return $matchingItemIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for items by a single attribute value
|
||||
*
|
||||
* @param string $search Search term
|
||||
* @param array $definitionIds Attribute definition IDs to search within
|
||||
* @param bool $matchDeleted Whether to match items where deleted flag equals this value
|
||||
* @return array Array of matching item_ids
|
||||
*/
|
||||
private function searchByAttributeValue(string $search, array $definitionIds, bool $matchDeleted = false): array
|
||||
{
|
||||
$builder = $this->db->table('attribute_links');
|
||||
$builder->select('DISTINCT attribute_links.item_id');
|
||||
$builder->join('attribute_values', 'attribute_values.attribute_id = attribute_links.attribute_id');
|
||||
$builder->join('items', 'items.item_id = attribute_links.item_id');
|
||||
$builder->groupStart();
|
||||
$builder->like('attribute_values.attribute_value', $search);
|
||||
$builder->orWhere('attribute_values.attribute_decimal', $search);
|
||||
$builder->orWhere('attribute_values.attribute_date', $search);
|
||||
$builder->groupEnd();
|
||||
$builder->whereIn('attribute_links.definition_id', $definitionIds);
|
||||
$builder->where('attribute_links.sale_id', null);
|
||||
$builder->where('attribute_links.receiving_id', null);
|
||||
$builder->where('items.deleted', $matchDeleted);
|
||||
|
||||
return array_column($builder->get()->getResultArray(), 'item_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attribute definition ID from column name for sorting
|
||||
*
|
||||
* @param string $sortColumn The sort column name
|
||||
* @return int|null The definition ID or null if not an attribute column
|
||||
*/
|
||||
private function getAttributeSortDefinitionId(string $sortColumn): ?int
|
||||
{
|
||||
if (!ctype_digit($sortColumn)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $sortColumn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a search on items
|
||||
*/
|
||||
public function search(string $search, array $filters, ?int $rows = 0, ?int $limit_from = 0, ?string $sort = 'items.name', ?string $order = 'asc', ?bool $count_only = false)
|
||||
{
|
||||
// Set default values
|
||||
if ($rows == null) {
|
||||
$rows = 0;
|
||||
}
|
||||
if ($limit_from == null) {
|
||||
$limit_from = 0;
|
||||
}
|
||||
if ($sort == null) {
|
||||
$sort = 'items.name';
|
||||
}
|
||||
if ($order == null) {
|
||||
$order = 'asc';
|
||||
}
|
||||
if ($count_only == null) {
|
||||
$count_only = false;
|
||||
}
|
||||
$rows = $rows ?? 0;
|
||||
$limit_from = $limit_from ?? 0;
|
||||
$sort = $sort ?? 'items.name';
|
||||
$order = $order ?? 'asc';
|
||||
$count_only = $count_only ?? false;
|
||||
|
||||
$config = config(OSPOS::class)->settings;
|
||||
$builder = $this->db->table('items AS items'); // TODO: I'm not sure if it's needed to write items AS items... I think you can just get away with items
|
||||
$builder = $this->db->table('items AS items');
|
||||
|
||||
// get_found_rows case
|
||||
if ($count_only) {
|
||||
$builder->select('COUNT(DISTINCT items.item_id) AS count');
|
||||
} else {
|
||||
@@ -212,13 +366,33 @@ class Item extends Model
|
||||
: 'trans_date BETWEEN ' . $this->db->escape(rawurldecode($filters['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($filters['end_date']));
|
||||
$builder->where($where);
|
||||
|
||||
$attributes_enabled = count($filters['definition_ids']) > 0;
|
||||
$attributesEnabled = count($filters['definition_ids']) > 0;
|
||||
$matchingItemIds = [];
|
||||
|
||||
if (!empty($search)) {
|
||||
if ($attributes_enabled && $filters['search_custom']) {
|
||||
$builder->havingLike('attribute_values', $search);
|
||||
$builder->orHavingLike('attribute_dtvalues', $search);
|
||||
$builder->orHavingLike('attribute_dvalues', $search);
|
||||
if ($search !== '' && $attributesEnabled && $filters['search_custom']) {
|
||||
$matchingItemIds = $this->searchByAttributes($search, $filters['definition_ids'], $filters['is_deleted']);
|
||||
}
|
||||
|
||||
if ($search !== '') {
|
||||
if ($attributesEnabled && $filters['search_custom']) {
|
||||
if (empty($matchingItemIds)) {
|
||||
$builder->groupStart();
|
||||
$builder->like('name', $search);
|
||||
$builder->orLike('item_number', $search);
|
||||
$builder->orLike('items.item_id', $search);
|
||||
$builder->orLike('company_name', $search);
|
||||
$builder->orLike('items.category', $search);
|
||||
$builder->groupEnd();
|
||||
} else {
|
||||
$builder->groupStart();
|
||||
$builder->whereIn('items.item_id', $matchingItemIds);
|
||||
$builder->orLike('name', $search);
|
||||
$builder->orLike('item_number', $search);
|
||||
$builder->orLike('items.item_id', $search);
|
||||
$builder->orLike('company_name', $search);
|
||||
$builder->orLike('items.category', $search);
|
||||
$builder->groupEnd();
|
||||
}
|
||||
} else {
|
||||
$builder->groupStart();
|
||||
$builder->like('name', $search);
|
||||
@@ -230,16 +404,43 @@ class Item extends Model
|
||||
}
|
||||
}
|
||||
|
||||
if ($attributes_enabled) {
|
||||
if ($attributesEnabled && !$count_only) {
|
||||
$format = $this->db->escape(dateformat_mysql());
|
||||
$this->db->simpleQuery('SET SESSION group_concat_max_len=49152');
|
||||
$builder->select('GROUP_CONCAT(DISTINCT CONCAT_WS(\'_\', definition_id, attribute_value) ORDER BY definition_id SEPARATOR \'|\') AS attribute_values');
|
||||
$builder->select("GROUP_CONCAT(DISTINCT CONCAT_WS('_', definition_id, DATE_FORMAT(attribute_date, $format)) SEPARATOR '|') AS attribute_dtvalues");
|
||||
$builder->select('GROUP_CONCAT(DISTINCT CONCAT_WS(\'_\', definition_id, attribute_decimal) SEPARATOR \'|\') AS attribute_dvalues');
|
||||
$builder->join('attribute_links', 'attribute_links.item_id = items.item_id AND attribute_links.receiving_id IS NULL AND attribute_links.sale_id IS NULL AND definition_id IN (' . implode(',', $filters['definition_ids']) . ')', 'left');
|
||||
$sanitizedIds = array_map('intval', $filters['definition_ids']);
|
||||
$builder->join('attribute_links', 'attribute_links.item_id = items.item_id AND attribute_links.receiving_id IS NULL AND attribute_links.sale_id IS NULL AND definition_id IN (' . implode(',', $sanitizedIds) . ')', 'left');
|
||||
$builder->join('attribute_values', 'attribute_values.attribute_id = attribute_links.attribute_id', 'left');
|
||||
}
|
||||
|
||||
// Handle attribute column sorting
|
||||
$sortDefinitionId = $this->getAttributeSortDefinitionId($sort);
|
||||
if ($sortDefinitionId !== null && $attributesEnabled && !$count_only) {
|
||||
$sortAlias = "sort_attr_{$sortDefinitionId}";
|
||||
$builder->join("attribute_links AS {$sortAlias}", "{$sortAlias}.item_id = items.item_id AND {$sortAlias}.definition_id = {$sortDefinitionId} AND {$sortAlias}.sale_id IS NULL AND {$sortAlias}.receiving_id IS NULL", 'left');
|
||||
$builder->join("attribute_values AS {$sortAlias}_val", "{$sortAlias}_val.attribute_id = {$sortAlias}.attribute_id", 'left');
|
||||
|
||||
// Determine the correct column to sort by based on attribute type
|
||||
$attribute = model(Attribute::class);
|
||||
$definitionInfo = $attribute->get_definitions_by_flags(Attribute::SHOW_IN_ITEMS, true);
|
||||
$sortColumn = "{$sortAlias}_val.attribute_value"; // default to text
|
||||
|
||||
if (isset($definitionInfo[$sortDefinitionId])) {
|
||||
$defType = is_array($definitionInfo[$sortDefinitionId]) ? ($definitionInfo[$sortDefinitionId]['type'] ?? TEXT) : TEXT;
|
||||
if ($defType === DECIMAL) {
|
||||
$sortColumn = "{$sortAlias}_val.attribute_decimal";
|
||||
} elseif ($defType === DATE) {
|
||||
$sortColumn = "{$sortAlias}_val.attribute_date";
|
||||
}
|
||||
}
|
||||
|
||||
$builder->orderBy($sortColumn, $order);
|
||||
} else {
|
||||
$builder->orderBy($sort, $order);
|
||||
}
|
||||
|
||||
$builder->where('items.deleted', $filters['is_deleted']);
|
||||
|
||||
if ($filters['empty_upc']) {
|
||||
@@ -261,17 +462,12 @@ class Item extends Model
|
||||
$builder->whereIn('items.item_type', $non_temp);
|
||||
}
|
||||
|
||||
// get_found_rows case
|
||||
if ($count_only) {
|
||||
return $builder->get()->getRow()->count;
|
||||
}
|
||||
|
||||
// Avoid duplicated entries with same name because of inventory reporting multiple changes on the same item in the same date range
|
||||
$builder->groupBy('items.item_id');
|
||||
|
||||
// Order by name of item by default
|
||||
$builder->orderBy($sort, $order);
|
||||
|
||||
if ($rows > 0) {
|
||||
$builder->limit($rows, $limit_from);
|
||||
}
|
||||
|
||||
@@ -327,7 +327,7 @@ class Sale extends Model
|
||||
{
|
||||
$suggestions = [];
|
||||
|
||||
if (!$this->isValidReceipt($search)) {
|
||||
if (!$this->is_valid_receipt($search)) {
|
||||
$builder = $this->db->table('sales');
|
||||
$builder->distinct()->select('first_name, last_name');
|
||||
$builder->join('people', 'people.person_id = sales.customer_id');
|
||||
@@ -408,21 +408,21 @@ class Sale extends Model
|
||||
/**
|
||||
* Checks if valid receipt
|
||||
*/
|
||||
public function isValidReceipt(string|null &$receiptSaleId): bool // TODO: like the others, maybe this should be an array rather than a delimited string... either that or the parameter name needs to be changed. $receipt_sale_id implies that it's an int.
|
||||
public function is_valid_receipt(string|null &$receipt_sale_id): bool // TODO: like the others, maybe this should be an array rather than a delimited string... either that or the parameter name needs to be changed. $receipt_sale_id implies that it's an int.
|
||||
{
|
||||
$config = config(OSPOS::class)->settings;
|
||||
|
||||
if (!empty($receiptSaleId)) {
|
||||
if (!empty($receipt_sale_id)) {
|
||||
// POS #
|
||||
$pieces = explode(' ', trim($receiptSaleId));
|
||||
$pieces = explode(' ', $receipt_sale_id);
|
||||
|
||||
if (count($pieces) == 2 && strtoupper($pieces[0]) === 'POS' && ctype_digit($pieces[1])) {
|
||||
return $this->exists((int)$pieces[1]);
|
||||
if (count($pieces) == 2 && preg_match('/(POS)/i', $pieces[0])) {
|
||||
return $this->exists($pieces[1]);
|
||||
} elseif ($config['invoice_enable']) {
|
||||
$saleInfo = $this->get_sale_by_invoice_number($receiptSaleId);
|
||||
$sale_info = $this->get_sale_by_invoice_number($receipt_sale_id);
|
||||
|
||||
if ($saleInfo->getNumRows() > 0) {
|
||||
$receiptSaleId = 'POS ' . $saleInfo->getRow()->sale_id;
|
||||
if ($sale_info->getNumRows() > 0) {
|
||||
$receipt_sale_id = 'POS ' . $sale_info->getRow()->sale_id;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user