mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-05-25 08:44:42 -04:00
Compare commits
2 Commits
WebShells-
...
ubl-invoic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c55e7fcbd | ||
|
|
4d9540633a |
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
<?php
|
||||
|
||||
use CodeIgniter\Router\RouteCollection;
|
||||
|
||||
@@ -12,40 +12,6 @@ $routes->get('login', 'Login::index');
|
||||
$routes->post('login', 'Login::index');
|
||||
$routes->post('migrate', 'Login::migrate');
|
||||
|
||||
$routes->get('sales', 'Sales::getIndex');
|
||||
$routes->get('sales/customerDisplay', 'Sales::getCustomerDisplay');
|
||||
$routes->get('sales/itemSearch', 'Sales::getItemSearch');
|
||||
$routes->post('sales/selectCustomer', 'Sales::postSelectCustomer');
|
||||
$routes->post('sales/changeMode', 'Sales::postChangeMode');
|
||||
$routes->post('sales/setComment', 'Sales::postSetComment');
|
||||
$routes->post('sales/setInvoiceNumber', 'Sales::postSetInvoiceNumber');
|
||||
$routes->post('sales/setPaymentType', 'Sales::postSetPaymentType');
|
||||
$routes->post('sales/setPrintAfterSale', 'Sales::postSetPrintAfterSale');
|
||||
$routes->post('sales/setPriceWorkOrders', 'Sales::postSetPriceWorkOrders');
|
||||
$routes->post('sales/setEmailReceipt', 'Sales::postSetEmailReceipt');
|
||||
$routes->post('sales/addPayment', 'Sales::postAddPayment');
|
||||
$routes->post('sales/add', 'Sales::postAdd');
|
||||
$routes->post('sales/editItem/(:segment)', 'Sales::postEditItem/$1');
|
||||
$routes->post('sales/deleteItem/(:segment)', 'Sales::getDeleteItem/$1');
|
||||
$routes->post('sales/deletePayment/(:segment)', 'Sales::getDeletePayment/$1');
|
||||
$routes->post('sales/removeCustomer', 'Sales::getRemoveCustomer');
|
||||
$routes->post('sales/complete', 'Sales::postComplete');
|
||||
$routes->post('sales/cancel', 'Sales::postCancel');
|
||||
$routes->post('sales/suspend', 'Sales::postSuspend');
|
||||
$routes->post('sales/unsuspend', 'Sales::postUnsuspend');
|
||||
$routes->post('sales/checkInvoiceNumber', 'Sales::postCheckInvoiceNumber');
|
||||
$routes->post('sales/changeItemNumber', 'Sales::postChangeItemNumber');
|
||||
$routes->post('sales/changeItemName', 'Sales::postChangeItemName');
|
||||
$routes->post('sales/changeItemDescription', 'Sales::postChangeItemDescription');
|
||||
$routes->get('sales/suspended', 'Sales::getSuspended');
|
||||
$routes->get('sales/discardSuspendedSale', 'Sales::getDiscardSuspendedSale');
|
||||
$routes->get('sales/sales_keyboard_help', 'Sales::getSalesKeyboardHelp');
|
||||
$routes->get('sales/receipt/(:num)', 'Sales::getReceipt/$1');
|
||||
$routes->get('sales/invoice/(:num)', 'Sales::getInvoice/$1');
|
||||
$routes->get('sales/edit/(:num)', 'Sales::getEdit/$1');
|
||||
$routes->post('sales/delete/(:num)', 'Sales::postDelete/$1');
|
||||
$routes->post('sales/save/(:num)', 'Sales::postSave/$1');
|
||||
|
||||
$routes->add('no_access/index/(:segment)', 'No_access::index/$1');
|
||||
$routes->add('no_access/index/(:segment)/(:segment)', 'No_access::index/$1/$2');
|
||||
|
||||
@@ -73,4 +39,4 @@ $routes->add('reports/specific_(:any)/(:any)/(:any)/(:any)', 'Reports::Specific_
|
||||
$routes->add('reports/specific_customers', 'Reports::specific_customer_input');
|
||||
$routes->add('reports/specific_employees', 'Reports::specific_employee_input');
|
||||
$routes->add('reports/specific_discounts', 'Reports::specific_discount_input');
|
||||
$routes->add('reports/specific_suppliers', 'Reports::specific_supplier_input');
|
||||
$routes->add('reports/specific_suppliers', 'Reports::specific_supplier_input');
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
24
app/Database/Migrations/20260304000000_AddUBLConfig.php
Normal file
24
app/Database/Migrations/20260304000000_AddUBLConfig.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddUBLConfig extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
log_message('info', 'Adding UBL configuration.');
|
||||
|
||||
$config_values = [
|
||||
['key' => 'invoice_format', 'value' => 'pdf_only'],
|
||||
];
|
||||
|
||||
$this->db->table('app_config')->ignore(true)->insertBatch($config_values);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->db->table('app_config')->whereIn('key', ['invoice_format'])->delete();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
229
app/Helpers/country_helper.php
Normal file
229
app/Helpers/country_helper.php
Normal file
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
use Config\OSPOS;
|
||||
|
||||
/**
|
||||
* Country code helper for mapping country names to ISO 3166-1 alpha-2 codes
|
||||
*/
|
||||
if (!function_exists('getCountryCode')) {
|
||||
/**
|
||||
* Convert country name to ISO 3166-1 alpha-2 code
|
||||
*
|
||||
* @param string $countryName Country name (full name in English)
|
||||
* @return string ISO 3166-1 alpha-2 code, or 'BE' as default for Belgium
|
||||
*/
|
||||
function getCountryCode(string $countryName): string
|
||||
{
|
||||
if (empty($countryName)) {
|
||||
return 'BE'; // Default to Belgium
|
||||
}
|
||||
|
||||
$countryMap = [
|
||||
// Major countries
|
||||
'Belgium' => 'BE',
|
||||
'Belgique' => 'BE',
|
||||
'België' => 'BE',
|
||||
'United States' => 'US',
|
||||
'USA' => 'US',
|
||||
'United States of America' => 'US',
|
||||
'United Kingdom' => 'GB',
|
||||
'UK' => 'GB',
|
||||
'Great Britain' => 'GB',
|
||||
'France' => 'FR',
|
||||
'Germany' => 'DE',
|
||||
'Deutschland' => 'DE',
|
||||
'Netherlands' => 'NL',
|
||||
'The Netherlands' => 'NL',
|
||||
'Nederland' => 'NL',
|
||||
'Italy' => 'IT',
|
||||
'Italia' => 'IT',
|
||||
'Spain' => 'ES',
|
||||
'España' => 'ES',
|
||||
'Poland' => 'PL',
|
||||
'Polska' => 'PL',
|
||||
'Portugal' => 'PT',
|
||||
'Sweden' => 'SE',
|
||||
'Sverige' => 'SE',
|
||||
'Norway' => 'NO',
|
||||
'Norge' => 'NO',
|
||||
'Denmark' => 'DK',
|
||||
'Danmark' => 'DK',
|
||||
'Finland' => 'FI',
|
||||
'Suomi' => 'FI',
|
||||
'Switzerland' => 'CH',
|
||||
'Suisse' => 'CH',
|
||||
'Schweiz' => 'CH',
|
||||
'Austria' => 'AT',
|
||||
'Österreich' => 'AT',
|
||||
'Ireland' => 'IE',
|
||||
'Luxembourg' => 'LU',
|
||||
'Greece' => 'GR',
|
||||
'Czech Republic' => 'CZ',
|
||||
'Czechia' => 'CZ',
|
||||
'Hungary' => 'HU',
|
||||
'Romania' => 'RO',
|
||||
'Bulgaria' => 'BG',
|
||||
'Slovakia' => 'SK',
|
||||
'Slovenia' => 'SI',
|
||||
'Estonia' => 'EE',
|
||||
'Latvia' => 'LV',
|
||||
'Lithuania' => 'LT',
|
||||
'Croatia' => 'HR',
|
||||
'Serbia' => 'RS',
|
||||
'Montenegro' => 'ME',
|
||||
'Bosnia and Herzegovina' => 'BA',
|
||||
'North Macedonia' => 'MK',
|
||||
'Albania' => 'AL',
|
||||
'Kosovo' => 'XK',
|
||||
'Turkey' => 'TR',
|
||||
'Türkiye' => 'TR',
|
||||
'Russia' => 'RU',
|
||||
'Russian Federation' => 'RU',
|
||||
'Ukraine' => 'UA',
|
||||
'Belarus' => 'BY',
|
||||
'Moldova' => 'MD',
|
||||
'Georgia' => 'GE',
|
||||
'Armenia' => 'AM',
|
||||
'Azerbaijan' => 'AZ',
|
||||
'Kazakhstan' => 'KZ',
|
||||
'Uzbekistan' => 'UZ',
|
||||
|
||||
// Other major economies
|
||||
'China' => 'CN',
|
||||
'Japan' => 'JP',
|
||||
'South Korea' => 'KR',
|
||||
'Korea' => 'KR',
|
||||
'India' => 'IN',
|
||||
'Australia' => 'AU',
|
||||
'New Zealand' => 'NZ',
|
||||
'Canada' => 'CA',
|
||||
'Mexico' => 'MX',
|
||||
'Brazil' => 'BR',
|
||||
'Argentina' => 'AR',
|
||||
'Chile' => 'CL',
|
||||
'Colombia' => 'CO',
|
||||
'Peru' => 'PE',
|
||||
'South Africa' => 'ZA',
|
||||
'Egypt' => 'EG',
|
||||
'Nigeria' => 'NG',
|
||||
'Kenya' => 'KE',
|
||||
'Morocco' => 'MA',
|
||||
|
||||
// If already ISO code, return as-is
|
||||
'BE' => 'BE',
|
||||
'US' => 'US',
|
||||
'GB' => 'GB',
|
||||
'FR' => 'FR',
|
||||
'DE' => 'DE',
|
||||
'NL' => 'NL',
|
||||
'IT' => 'IT',
|
||||
'ES' => 'ES',
|
||||
'PT' => 'PT',
|
||||
'SE' => 'SE',
|
||||
'NO' => 'NO',
|
||||
'DK' => 'DK',
|
||||
'FI' => 'FI',
|
||||
'CH' => 'CH',
|
||||
'AT' => 'AT',
|
||||
'IE' => 'IE',
|
||||
'LU' => 'LU',
|
||||
'GR' => 'GR',
|
||||
'CZ' => 'CZ',
|
||||
'HU' => 'HU',
|
||||
'RO' => 'RO',
|
||||
'BG' => 'BG',
|
||||
'SK' => 'SK',
|
||||
'SI' => 'SI',
|
||||
'EE' => 'EE',
|
||||
'LV' => 'LV',
|
||||
'LT' => 'LT',
|
||||
'HR' => 'HR',
|
||||
'RS' => 'RS',
|
||||
'ME' => 'ME',
|
||||
'BA' => 'BA',
|
||||
'MK' => 'MK',
|
||||
'AL' => 'AL',
|
||||
'TR' => 'TR',
|
||||
'RU' => 'RU',
|
||||
'UA' => 'UA',
|
||||
];
|
||||
|
||||
// Try exact match first
|
||||
$normalized = trim($countryName);
|
||||
if (isset($countryMap[$normalized])) {
|
||||
return $countryMap[$normalized];
|
||||
}
|
||||
|
||||
// Try case-insensitive match
|
||||
$normalizedLower = strtolower($normalized);
|
||||
foreach ($countryMap as $key => $code) {
|
||||
if (strtolower($key) === $normalizedLower) {
|
||||
return $code;
|
||||
}
|
||||
}
|
||||
|
||||
// Try partial match (e.g., "United States" → "US")
|
||||
foreach ($countryMap as $key => $code) {
|
||||
if (stripos($key, $normalized) !== false || stripos($normalized, $key) !== false) {
|
||||
return $code;
|
||||
}
|
||||
}
|
||||
|
||||
// Try matching ISO code directly
|
||||
if (preg_match('/^[A-Z]{2}$/i', $normalized)) {
|
||||
return strtoupper($normalized);
|
||||
}
|
||||
|
||||
// Check if the country_codes config has a default
|
||||
$config = config(OSPOS::class)->settings;
|
||||
if (isset($config['country_codes']) && !empty($config['country_codes'])) {
|
||||
$countries = explode(',', $config['country_codes']);
|
||||
if (!empty($countries)) {
|
||||
return strtoupper(trim($countries[0]));
|
||||
}
|
||||
}
|
||||
|
||||
// Default to Belgium (for Peppol compliance in Belgium)
|
||||
return 'BE';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('getCurrencyCode')) {
|
||||
/**
|
||||
* Get ISO 4217 currency code for a country
|
||||
*
|
||||
* @param string $countryCode ISO 3166-1 alpha-2 country code
|
||||
* @return string ISO 4217 currency code
|
||||
*/
|
||||
function getCurrencyCode(string $countryCode): string
|
||||
{
|
||||
$currencyMap = [
|
||||
'BE' => 'EUR',
|
||||
'FR' => 'EUR',
|
||||
'DE' => 'EUR',
|
||||
'NL' => 'EUR',
|
||||
'IT' => 'EUR',
|
||||
'ES' => 'EUR',
|
||||
'PT' => 'EUR',
|
||||
'IE' => 'EUR',
|
||||
'AT' => 'EUR',
|
||||
'LU' => 'EUR',
|
||||
'FI' => 'EUR',
|
||||
'GR' => 'EUR',
|
||||
'US' => 'USD',
|
||||
'GB' => 'GBP',
|
||||
'CH' => 'CHF',
|
||||
'JP' => 'JPY',
|
||||
'CN' => 'CNY',
|
||||
'CA' => 'CAD',
|
||||
'AU' => 'AUD',
|
||||
'NZ' => 'NZD',
|
||||
'IN' => 'INR',
|
||||
'BR' => 'BRL',
|
||||
'MX' => 'MXN',
|
||||
'ZA' => 'ZAR',
|
||||
];
|
||||
|
||||
return $currencyMap[$countryCode] ?? 'EUR'; // Default to EUR
|
||||
}
|
||||
}
|
||||
@@ -272,6 +272,9 @@ function get_payment_options(): array
|
||||
$payments[lang('Sales.upi')] = lang('Sales.upi');
|
||||
}
|
||||
|
||||
$payments[lang('Sales.bank_transfer')] = lang('Sales.bank_transfer');
|
||||
$payments[lang('Sales.wallet')] = lang('Sales.wallet');
|
||||
|
||||
return $payments;
|
||||
}
|
||||
|
||||
@@ -365,74 +368,6 @@ function to_currency_no_money(?string $number): string
|
||||
return to_decimals($number, 'currency_decimals');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the secondary currency rendering context from app config values.
|
||||
*
|
||||
* @param array $config
|
||||
* @return array{show:bool,rate:float,symbol:string,code:string,decimals:int}
|
||||
*/
|
||||
function secondary_currency_context(array $config): array
|
||||
{
|
||||
$rate = (float) ($config['secondary_currency_rate'] ?? 0);
|
||||
$symbol = trim((string) ($config['secondary_currency_symbol'] ?? ''));
|
||||
$code = trim((string) ($config['secondary_currency_code'] ?? ''));
|
||||
$decimals = (int) ($config['secondary_currency_decimals'] ?? ($config['currency_decimals'] ?? DEFAULT_PRECISION));
|
||||
|
||||
return [
|
||||
'show' => (($config['secondary_currency_enabled'] ?? false) == 1) && $rate > 0,
|
||||
'rate' => $rate,
|
||||
'symbol' => $symbol,
|
||||
'code' => $code,
|
||||
'decimals' => $decimals,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a value in the secondary currency.
|
||||
*
|
||||
* @param float|int|string|null $number
|
||||
* @param array{show:bool,rate:float,symbol:string,code:string,decimals:int} $secondaryCurrency
|
||||
* @return string
|
||||
*/
|
||||
function to_secondary_currency(float|int|string|null $number, array $secondaryCurrency): string
|
||||
{
|
||||
if (!isset($number) || !$secondaryCurrency['show']) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$config = config(OSPOS::class)->settings;
|
||||
$amount = (float) $number * (float) $secondaryCurrency['rate'];
|
||||
$fmt = new NumberFormatter($config['number_locale'], NumberFormatter::CURRENCY);
|
||||
$fmt->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, $secondaryCurrency['decimals']);
|
||||
$fmt->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, $secondaryCurrency['decimals']);
|
||||
|
||||
if (empty($config['thousands_separator'])) {
|
||||
$fmt->setTextAttribute(NumberFormatter::GROUPING_SEPARATOR_SYMBOL, '');
|
||||
}
|
||||
|
||||
$fmt->setSymbol(NumberFormatter::CURRENCY_SYMBOL, $secondaryCurrency['symbol'] !== '' ? $secondaryCurrency['symbol'] : ($secondaryCurrency['code'] !== '' ? $secondaryCurrency['code'] : ''));
|
||||
|
||||
return $fmt->format($amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the secondary and primary currency amounts together.
|
||||
*
|
||||
* @param float|int|string|null $number
|
||||
* @param array{show:bool,rate:float,symbol:string,code:string,decimals:int} $secondaryCurrency
|
||||
* @return string
|
||||
*/
|
||||
function to_secondary_currency_dual(float|int|string|null $number, array $secondaryCurrency): string
|
||||
{
|
||||
$secondary = to_secondary_currency($number, $secondaryCurrency);
|
||||
|
||||
if ($secondary === '') {
|
||||
return to_currency((string) $number);
|
||||
}
|
||||
|
||||
return $secondary . '<br>' . to_currency((string) $number);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $number
|
||||
* @return string
|
||||
|
||||
@@ -172,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 = [];
|
||||
|
||||
@@ -85,6 +85,7 @@ function get_sales_manage_table_headers(): string
|
||||
if ($config['invoice_enable']) {
|
||||
$headers[] = ['invoice_number' => lang('Sales.invoice_number')];
|
||||
$headers[] = ['invoice' => '', 'sortable' => false, 'escape' => false];
|
||||
$headers[] = ['ubl' => '', 'sortable' => false, 'escape' => false];
|
||||
}
|
||||
|
||||
$headers[] = ['receipt' => '', 'sortable' => false, 'escape' => false];
|
||||
@@ -121,6 +122,13 @@ function get_sale_data_row(object $sale): array
|
||||
'<span class="glyphicon glyphicon-list-alt"></span>',
|
||||
['title' => lang('Sales.show_invoice')]
|
||||
);
|
||||
$row['ubl'] = empty($sale->invoice_number)
|
||||
? '-'
|
||||
: anchor(
|
||||
"$controller/ublInvoice/$sale->sale_id",
|
||||
'<span class="glyphicon glyphicon-download"></span>',
|
||||
['title' => lang('Sales.download_ubl'), 'target' => '_blank']
|
||||
);
|
||||
}
|
||||
|
||||
$row['receipt'] = anchor(
|
||||
|
||||
@@ -1,344 +1,336 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"address" => "Company Address",
|
||||
"address_required" => "Company address is a required field.",
|
||||
"all_set" => "All file permissions are set correctly!",
|
||||
"allow_duplicate_barcodes" => "Allow Duplicate Barcodes",
|
||||
"apostrophe" => "apostrophe",
|
||||
"backup_button" => "Backup",
|
||||
"backup_database" => "Backup Database",
|
||||
"barcode" => "Barcode",
|
||||
"barcode_company" => "Company Name",
|
||||
"barcode_configuration" => "Barcode Configuration",
|
||||
"barcode_content" => "Barcode Content",
|
||||
"barcode_first_row" => "Row 1",
|
||||
"barcode_font" => "Font",
|
||||
"barcode_formats" => "Input Formats",
|
||||
"barcode_generate_if_empty" => "Generate if empty.",
|
||||
"barcode_height" => "Height (px)",
|
||||
"barcode_id" => "Item Id/Name",
|
||||
"barcode_info" => "Barcode Configuration Information",
|
||||
"barcode_layout" => "Barcode Layout",
|
||||
"barcode_name" => "Name",
|
||||
"barcode_number" => "Barcode",
|
||||
"barcode_number_in_row" => "Number in row",
|
||||
"barcode_page_cellspacing" => "Display page cellspacing.",
|
||||
"barcode_page_width" => "Display page width",
|
||||
"barcode_price" => "Price",
|
||||
"barcode_second_row" => "Row 2",
|
||||
"barcode_third_row" => "Row 3",
|
||||
"barcode_tooltip" => "Warning: This feature can cause duplicate items to be imported or created. Do not use if you do not want duplicate barcodes.",
|
||||
"barcode_type" => "Barcode Type",
|
||||
"barcode_width" => "Width (px)",
|
||||
"bottom" => "Bottom",
|
||||
"cash_button" => "",
|
||||
"cash_button_1" => "",
|
||||
"cash_button_2" => "",
|
||||
"cash_button_3" => "",
|
||||
"cash_button_4" => "",
|
||||
"cash_button_5" => "",
|
||||
"cash_button_6" => "",
|
||||
"cash_decimals" => "Cash Decimals",
|
||||
"cash_decimals_tooltip" => "If Cash Decimals and Currency Decimals are the same then no cash triggered rounding will take place, unless Cash Rounding is set to Half Five.",
|
||||
"cash_rounding" => "Cash Rounding",
|
||||
"category_dropdown" => "Show Category as a dropdown",
|
||||
"center" => "Center",
|
||||
"change_apperance_tooltip" => "",
|
||||
"comma" => "comma",
|
||||
"company" => "Company Name",
|
||||
"company_avatar" => "",
|
||||
"company_change_image" => "Change Image",
|
||||
"company_logo" => "Company Logo",
|
||||
"company_remove_image" => "Remove Image",
|
||||
"company_required" => "Company name is a required field",
|
||||
"company_select_image" => "Select Image",
|
||||
"company_website_url" => "Company website is not a valid URL (http://...).",
|
||||
"country_codes" => "Country Codes",
|
||||
"country_codes_tooltip" => "Comma separated list of country codes for nominatim address lookup.",
|
||||
"currency_code" => "Currency Code",
|
||||
"currency_decimals" => "Currency Decimals",
|
||||
"currency_symbol" => "Currency Symbol",
|
||||
"current_employee_only" => "",
|
||||
"customer_reward" => "Reward",
|
||||
"customer_reward_duplicate" => "Reward must be unique.",
|
||||
"customer_reward_enable" => "Enable Customer Rewards",
|
||||
"customer_reward_invalid_chars" => "Reward can not contain '_'",
|
||||
"customer_reward_required" => "Reward is a required field",
|
||||
"customer_sales_tax_support" => "",
|
||||
"date_or_time_format" => "Date and Time Filter",
|
||||
"datetimeformat" => "Date and Time Format",
|
||||
"decimal_point" => "Decimal Point",
|
||||
"default_barcode_font_size_number" => "Default Barcode Font Size must be a number.",
|
||||
"default_barcode_font_size_required" => "Default Barcode Font Size is a required field.",
|
||||
"default_barcode_height_number" => "Default Barcode Height must be a number.",
|
||||
"default_barcode_height_required" => "Default Barcode Height is a required field.",
|
||||
"default_barcode_num_in_row_number" => "Default Barcode Number in Row must be a number.",
|
||||
"default_barcode_num_in_row_required" => "Default Barcode Number in Row is a required field.",
|
||||
"default_barcode_page_cellspacing_number" => "Default Barcode Page Cellspacing must be a number.",
|
||||
"default_barcode_page_cellspacing_required" => "Default Barcode Page Cellspacing is a required field.",
|
||||
"default_barcode_page_width_number" => "Default Barcode Page Width must be a number.",
|
||||
"default_barcode_page_width_required" => "Default Barcode Page Width is a required field.",
|
||||
"default_barcode_width_number" => "Default Barcode Width must be a number.",
|
||||
"default_barcode_width_required" => "Default Barcode Width is a required field.",
|
||||
"default_item_columns" => "Default Visible Item Columns",
|
||||
"default_origin_tax_code" => "Default Origin Tax Code",
|
||||
"default_receivings_discount" => "Default Receivings Discount",
|
||||
"default_receivings_discount_number" => "Default Receivings Discount must be a number.",
|
||||
"default_receivings_discount_required" => "Default Receivings Discount is a required field.",
|
||||
"default_sales_discount" => "Default Sales Discount",
|
||||
"default_sales_discount_number" => "Default Sales Discount must be a number.",
|
||||
"default_sales_discount_required" => "Default Sales Discount is a required field.",
|
||||
"default_tax_category" => "Default Tax Category",
|
||||
"default_tax_code" => "Default Tax Code",
|
||||
"default_tax_jurisdiction" => "Default Tax Jurisdiction",
|
||||
"default_tax_name_number" => "Default Tax Name must be a string.",
|
||||
"default_tax_name_required" => "Default Tax Name is a required field.",
|
||||
"default_tax_rate" => "Default Tax Rate %",
|
||||
"default_tax_rate_1" => "Tax 1 Rate",
|
||||
"default_tax_rate_2" => "Tax 2 Rate",
|
||||
"default_tax_rate_3" => "",
|
||||
"default_tax_rate_number" => "Default Tax Rate must be a number.",
|
||||
"default_tax_rate_required" => "Default Tax Rate is a required field.",
|
||||
"derive_sale_quantity" => "Allow Derived Sale Quantity",
|
||||
"derive_sale_quantity_tooltip" => "If checked then a new item type will be provided for items ordered by extended amount",
|
||||
"dinner_table" => "Table",
|
||||
"dinner_table_duplicate" => "Table must be unique.",
|
||||
"dinner_table_enable" => "Enable Dinner Tables",
|
||||
"dinner_table_invalid_chars" => "Table Name can not contain '_'.",
|
||||
"dinner_table_required" => "Table is a required field.",
|
||||
"dot" => "dot",
|
||||
"email" => "Email",
|
||||
"email_configuration" => "Email Configuration",
|
||||
"email_mailpath" => "Path to Sendmail",
|
||||
"email_protocol" => "Protocol",
|
||||
"email_receipt_check_behaviour" => "Email Receipt checkbox",
|
||||
"email_receipt_check_behaviour_always" => "Always checked",
|
||||
"email_receipt_check_behaviour_last" => "Remember last selection",
|
||||
"email_receipt_check_behaviour_never" => "Always unchecked",
|
||||
"email_smtp_crypto" => "SMTP Encryption",
|
||||
"email_smtp_host" => "SMTP Server",
|
||||
"email_smtp_pass" => "SMTP Password",
|
||||
"email_smtp_port" => "SMTP Port",
|
||||
"email_smtp_timeout" => "SMTP Timeout (s)",
|
||||
"email_smtp_user" => "SMTP Username",
|
||||
"enable_avatar" => "",
|
||||
"enable_avatar_tooltip" => "",
|
||||
"enable_dropdown_tooltip" => "",
|
||||
"enable_new_look" => "",
|
||||
"enable_right_bar" => "",
|
||||
"enable_right_bar_tooltip" => "",
|
||||
"enforce_privacy" => "Enforce privacy",
|
||||
"enforce_privacy_tooltip" => "Protect Customers privacy enforcing data scrambling in case of their data being deleted",
|
||||
"fax" => "Fax",
|
||||
"file_perm" => "There are problems with file permissions. Please fix and reload this page.",
|
||||
"financial_year" => "Fiscal Year Start",
|
||||
"financial_year_apr" => "1st of April",
|
||||
"financial_year_aug" => "1st of August",
|
||||
"financial_year_dec" => "1st of December",
|
||||
"financial_year_feb" => "1st of February",
|
||||
"financial_year_jan" => "1st of January",
|
||||
"financial_year_jul" => "1st of July",
|
||||
"financial_year_jun" => "1st of June",
|
||||
"financial_year_mar" => "1st of March",
|
||||
"financial_year_may" => "1st of May",
|
||||
"financial_year_nov" => "1st of November",
|
||||
"financial_year_oct" => "1st of October",
|
||||
"financial_year_sep" => "1st of September",
|
||||
"floating_labels" => "Floating Labels",
|
||||
"gcaptcha_enable" => "Login Page reCAPTCHA",
|
||||
"gcaptcha_secret_key" => "reCAPTCHA Secret Key",
|
||||
"gcaptcha_secret_key_required" => "reCAPTCHA Secret Key is a required field",
|
||||
"gcaptcha_site_key" => "reCAPTCHA Site Key",
|
||||
"gcaptcha_site_key_required" => "reCAPTCHA Site Key is a required field",
|
||||
"gcaptcha_tooltip" => "Protect the Login page with Google reCAPTCHA, click the icon for an API key pair.",
|
||||
"general" => "General",
|
||||
"general_configuration" => "General Configuration",
|
||||
"giftcard_number" => "Gift Card Number",
|
||||
"giftcard_random" => "Generate Random",
|
||||
"giftcard_series" => "Generate in Series",
|
||||
"image_allowed_file_types" => "Allowed file types",
|
||||
"image_max_height_tooltip" => "Maximum allowed height of image uploads in pixels (px).",
|
||||
"image_max_size_tooltip" => "Maximum allowed file size of image uploads in kilobytes (kb).",
|
||||
"image_max_width_tooltip" => "Maximum allowed width of image uploads in pixels (px).",
|
||||
"image_restrictions" => "Image Upload Restrictions",
|
||||
"include_hsn" => "Include Support for HSN Codes",
|
||||
"info" => "Information",
|
||||
"info_configuration" => "Store Information",
|
||||
"input_groups" => "Input Groups",
|
||||
"integrations" => "Integrations",
|
||||
"integrations_configuration" => "Third Party Integrations",
|
||||
"invoice" => "Invoice",
|
||||
"invoice_configuration" => "Invoice Print Settings",
|
||||
"invoice_default_comments" => "Default Invoice Comments",
|
||||
"invoice_email_message" => "Invoice Email Template",
|
||||
"invoice_enable" => "Enable Invoicing",
|
||||
"invoice_printer" => "Invoice Printer",
|
||||
"invoice_type" => "Invoice Type",
|
||||
"is_readable" => "is readable, but the permissions are incorrectly set. Please set it to 640 or 660 and refresh.",
|
||||
"is_writable" => "is writable, but the permissions are incorrectly set. Please set it to 750 and refresh.",
|
||||
"item_markup" => "",
|
||||
"jsprintsetup_required" => "Warning: This functionality will only work if you have the FireFox jsPrintSetup addon installed. Save anyway?",
|
||||
"language" => "Language",
|
||||
"last_used_invoice_number" => "Last used Invoice Number",
|
||||
"last_used_quote_number" => "Last used Quote Number",
|
||||
"last_used_work_order_number" => "Last used W/O Number",
|
||||
"left" => "Left",
|
||||
"license" => "License",
|
||||
"license_configuration" => "License Statement",
|
||||
"line_sequence" => "Line Sequence",
|
||||
"lines_per_page" => "Lines per Page",
|
||||
"lines_per_page_number" => "Lines per Page must be a number.",
|
||||
"lines_per_page_required" => "Lines per Page is a required field.",
|
||||
"locale" => "Localization",
|
||||
"locale_configuration" => "Localization Configuration",
|
||||
"locale_info" => "Localization Configuration Information",
|
||||
"location" => "Stock",
|
||||
"location_configuration" => "Stock Locations",
|
||||
"location_info" => "Location Configuration Information",
|
||||
"login_form" => "Login Form Style",
|
||||
"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_configuration" => "MailChimp Configuration",
|
||||
"mailchimp_key_successfully" => "API Key is valid.",
|
||||
"mailchimp_key_unsuccessfully" => "API Key is invalid.",
|
||||
"mailchimp_lists" => "MailChimp List(s)",
|
||||
"mailchimp_tooltip" => "Click the icon for an API Key.",
|
||||
"message" => "Message",
|
||||
"message_configuration" => "Message Configuration",
|
||||
"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",
|
||||
"msg_uid_required" => "SMS-API Username is a required field",
|
||||
"multi_pack_enabled" => "Multiple Packages per Item",
|
||||
"no_risk" => "No security/vulnerability risks.",
|
||||
"none" => "none",
|
||||
"notify_alignment" => "Notification Popup Position",
|
||||
"number_format" => "Number Format",
|
||||
"number_locale" => "Localization",
|
||||
"number_locale_invalid" => "The entered locale is invalid. Check the link in the tooltip to find a valid locale.",
|
||||
"number_locale_required" => "Number Locale is a required field.",
|
||||
"number_locale_tooltip" => "Find a suitable locale through this link.",
|
||||
"os_timezone" => "OSPOS Timezone:",
|
||||
"ospos_info" => "OSPOS Installation Info",
|
||||
"payment_options_order" => "Payment Options Order",
|
||||
"perm_risk" => "Incorrect permissions leaves this software at risk.",
|
||||
"phone" => "Company Phone",
|
||||
"phone_required" => "Company Phone is a required field.",
|
||||
"print_bottom_margin" => "Margin Bottom",
|
||||
"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_footer" => "Print Browser Footer",
|
||||
"print_header" => "Print Browser Header",
|
||||
"print_left_margin" => "Margin Left",
|
||||
"print_left_margin_number" => "Margin Left must be a number.",
|
||||
"print_left_margin_required" => "Margin Left is a required field.",
|
||||
"print_receipt_check_behaviour" => "Print Receipt checkbox",
|
||||
"print_receipt_check_behaviour_always" => "Always checked",
|
||||
"print_receipt_check_behaviour_last" => "Remember last selection",
|
||||
"print_receipt_check_behaviour_never" => "Always unchecked",
|
||||
"print_right_margin" => "Margin Right",
|
||||
"print_right_margin_number" => "Margin Right must be a number.",
|
||||
"print_right_margin_required" => "Margin Right is a required field.",
|
||||
"print_silently" => "Show Print Dialog",
|
||||
"print_top_margin" => "Margin Top",
|
||||
"print_top_margin_number" => "Margin Top must be a number.",
|
||||
"print_top_margin_required" => "Margin Top is a required field.",
|
||||
"quantity_decimals" => "Quantity Decimals",
|
||||
"quick_cash_enable" => "",
|
||||
"quote_default_comments" => "Default Quote Comments",
|
||||
"receipt" => "Receipt",
|
||||
"receipt_category" => "",
|
||||
"receipt_configuration" => "Receipt Print Settings",
|
||||
"receipt_default" => "Default",
|
||||
"receipt_font_size" => "Font Size",
|
||||
"receipt_font_size_number" => "Font Size must be a number.",
|
||||
"receipt_font_size_required" => "Font Size is a required field.",
|
||||
"receipt_info" => "Receipt Configuration Information",
|
||||
"receipt_printer" => "Ticket Printer",
|
||||
"receipt_short" => "Short",
|
||||
<?php
|
||||
|
||||
return [
|
||||
"address" => "Company Address",
|
||||
"address_required" => "Company address is a required field.",
|
||||
"all_set" => "All file permissions are set correctly!",
|
||||
"allow_duplicate_barcodes" => "Allow Duplicate Barcodes",
|
||||
"apostrophe" => "apostrophe",
|
||||
"backup_button" => "Backup",
|
||||
"backup_database" => "Backup Database",
|
||||
"barcode" => "Barcode",
|
||||
"barcode_company" => "Company Name",
|
||||
"barcode_configuration" => "Barcode Configuration",
|
||||
"barcode_content" => "Barcode Content",
|
||||
"barcode_first_row" => "Row 1",
|
||||
"barcode_font" => "Font",
|
||||
"barcode_formats" => "Input Formats",
|
||||
"barcode_generate_if_empty" => "Generate if empty.",
|
||||
"barcode_height" => "Height (px)",
|
||||
"barcode_id" => "Item Id/Name",
|
||||
"barcode_info" => "Barcode Configuration Information",
|
||||
"barcode_layout" => "Barcode Layout",
|
||||
"barcode_name" => "Name",
|
||||
"barcode_number" => "Barcode",
|
||||
"barcode_number_in_row" => "Number in row",
|
||||
"barcode_page_cellspacing" => "Display page cellspacing.",
|
||||
"barcode_page_width" => "Display page width",
|
||||
"barcode_price" => "Price",
|
||||
"barcode_second_row" => "Row 2",
|
||||
"barcode_third_row" => "Row 3",
|
||||
"barcode_tooltip" => "Warning: This feature can cause duplicate items to be imported or created. Do not use if you do not want duplicate barcodes.",
|
||||
"barcode_type" => "Barcode Type",
|
||||
"barcode_width" => "Width (px)",
|
||||
"bottom" => "Bottom",
|
||||
"cash_button" => "",
|
||||
"cash_button_1" => "",
|
||||
"cash_button_2" => "",
|
||||
"cash_button_3" => "",
|
||||
"cash_button_4" => "",
|
||||
"cash_button_5" => "",
|
||||
"cash_button_6" => "",
|
||||
"cash_decimals" => "Cash Decimals",
|
||||
"cash_decimals_tooltip" => "If Cash Decimals and Currency Decimals are the same then no cash triggered rounding will take place, unless Cash Rounding is set to Half Five.",
|
||||
"cash_rounding" => "Cash Rounding",
|
||||
"category_dropdown" => "Show Category as a dropdown",
|
||||
"center" => "Center",
|
||||
"change_apperance_tooltip" => "",
|
||||
"comma" => "comma",
|
||||
"company" => "Company Name",
|
||||
"company_avatar" => "",
|
||||
"company_change_image" => "Change Image",
|
||||
"company_logo" => "Company Logo",
|
||||
"company_remove_image" => "Remove Image",
|
||||
"company_required" => "Company name is a required field",
|
||||
"company_select_image" => "Select Image",
|
||||
"company_website_url" => "Company website is not a valid URL (http://...).",
|
||||
"country_codes" => "Country Codes",
|
||||
"country_codes_tooltip" => "Comma separated list of country codes for nominatim address lookup.",
|
||||
"currency_code" => "Currency Code",
|
||||
"currency_decimals" => "Currency Decimals",
|
||||
"currency_symbol" => "Currency Symbol",
|
||||
"current_employee_only" => "",
|
||||
"customer_reward" => "Reward",
|
||||
"customer_reward_duplicate" => "Reward must be unique.",
|
||||
"customer_reward_enable" => "Enable Customer Rewards",
|
||||
"customer_reward_invalid_chars" => "Reward can not contain '_'",
|
||||
"customer_reward_required" => "Reward is a required field",
|
||||
"customer_sales_tax_support" => "",
|
||||
"date_or_time_format" => "Date and Time Filter",
|
||||
"datetimeformat" => "Date and Time Format",
|
||||
"decimal_point" => "Decimal Point",
|
||||
"default_barcode_font_size_number" => "Default Barcode Font Size must be a number.",
|
||||
"default_barcode_font_size_required" => "Default Barcode Font Size is a required field.",
|
||||
"default_barcode_height_number" => "Default Barcode Height must be a number.",
|
||||
"default_barcode_height_required" => "Default Barcode Height is a required field.",
|
||||
"default_barcode_num_in_row_number" => "Default Barcode Number in Row must be a number.",
|
||||
"default_barcode_num_in_row_required" => "Default Barcode Number in Row is a required field.",
|
||||
"default_barcode_page_cellspacing_number" => "Default Barcode Page Cellspacing must be a number.",
|
||||
"default_barcode_page_cellspacing_required" => "Default Barcode Page Cellspacing is a required field.",
|
||||
"default_barcode_page_width_number" => "Default Barcode Page Width must be a number.",
|
||||
"default_barcode_page_width_required" => "Default Barcode Page Width is a required field.",
|
||||
"default_barcode_width_number" => "Default Barcode Width must be a number.",
|
||||
"default_barcode_width_required" => "Default Barcode Width is a required field.",
|
||||
"default_item_columns" => "Default Visible Item Columns",
|
||||
"default_origin_tax_code" => "Default Origin Tax Code",
|
||||
"default_receivings_discount" => "Default Receivings Discount",
|
||||
"default_receivings_discount_number" => "Default Receivings Discount must be a number.",
|
||||
"default_receivings_discount_required" => "Default Receivings Discount is a required field.",
|
||||
"default_sales_discount" => "Default Sales Discount",
|
||||
"default_sales_discount_number" => "Default Sales Discount must be a number.",
|
||||
"default_sales_discount_required" => "Default Sales Discount is a required field.",
|
||||
"default_tax_category" => "Default Tax Category",
|
||||
"default_tax_code" => "Default Tax Code",
|
||||
"default_tax_jurisdiction" => "Default Tax Jurisdiction",
|
||||
"default_tax_name_number" => "Default Tax Name must be a string.",
|
||||
"default_tax_name_required" => "Default Tax Name is a required field.",
|
||||
"default_tax_rate" => "Default Tax Rate %",
|
||||
"default_tax_rate_1" => "Tax 1 Rate",
|
||||
"default_tax_rate_2" => "Tax 2 Rate",
|
||||
"default_tax_rate_3" => "",
|
||||
"default_tax_rate_number" => "Default Tax Rate must be a number.",
|
||||
"default_tax_rate_required" => "Default Tax Rate is a required field.",
|
||||
"derive_sale_quantity" => "Allow Derived Sale Quantity",
|
||||
"derive_sale_quantity_tooltip" => "If checked then a new item type will be provided for items ordered by extended amount",
|
||||
"dinner_table" => "Table",
|
||||
"dinner_table_duplicate" => "Table must be unique.",
|
||||
"dinner_table_enable" => "Enable Dinner Tables",
|
||||
"dinner_table_invalid_chars" => "Table Name can not contain '_'.",
|
||||
"dinner_table_required" => "Table is a required field.",
|
||||
"dot" => "dot",
|
||||
"email" => "Email",
|
||||
"email_configuration" => "Email Configuration",
|
||||
"email_mailpath" => "Path to Sendmail",
|
||||
"email_protocol" => "Protocol",
|
||||
"email_receipt_check_behaviour" => "Email Receipt checkbox",
|
||||
"email_receipt_check_behaviour_always" => "Always checked",
|
||||
"email_receipt_check_behaviour_last" => "Remember last selection",
|
||||
"email_receipt_check_behaviour_never" => "Always unchecked",
|
||||
"email_smtp_crypto" => "SMTP Encryption",
|
||||
"email_smtp_host" => "SMTP Server",
|
||||
"email_smtp_pass" => "SMTP Password",
|
||||
"email_smtp_port" => "SMTP Port",
|
||||
"email_smtp_timeout" => "SMTP Timeout (s)",
|
||||
"email_smtp_user" => "SMTP Username",
|
||||
"enable_avatar" => "",
|
||||
"enable_avatar_tooltip" => "",
|
||||
"enable_dropdown_tooltip" => "",
|
||||
"enable_new_look" => "",
|
||||
"enable_right_bar" => "",
|
||||
"enable_right_bar_tooltip" => "",
|
||||
"enforce_privacy" => "Enforce privacy",
|
||||
"enforce_privacy_tooltip" => "Protect Customers privacy enforcing data scrambling in case of their data being deleted",
|
||||
"fax" => "Fax",
|
||||
"file_perm" => "There are problems with file permissions. Please fix and reload this page.",
|
||||
"financial_year" => "Fiscal Year Start",
|
||||
"financial_year_apr" => "1st of April",
|
||||
"financial_year_aug" => "1st of August",
|
||||
"financial_year_dec" => "1st of December",
|
||||
"financial_year_feb" => "1st of February",
|
||||
"financial_year_jan" => "1st of January",
|
||||
"financial_year_jul" => "1st of July",
|
||||
"financial_year_jun" => "1st of June",
|
||||
"financial_year_mar" => "1st of March",
|
||||
"financial_year_may" => "1st of May",
|
||||
"financial_year_nov" => "1st of November",
|
||||
"financial_year_oct" => "1st of October",
|
||||
"financial_year_sep" => "1st of September",
|
||||
"floating_labels" => "Floating Labels",
|
||||
"gcaptcha_enable" => "Login Page reCAPTCHA",
|
||||
"gcaptcha_secret_key" => "reCAPTCHA Secret Key",
|
||||
"gcaptcha_secret_key_required" => "reCAPTCHA Secret Key is a required field",
|
||||
"gcaptcha_site_key" => "reCAPTCHA Site Key",
|
||||
"gcaptcha_site_key_required" => "reCAPTCHA Site Key is a required field",
|
||||
"gcaptcha_tooltip" => "Protect the Login page with Google reCAPTCHA, click the icon for an API key pair.",
|
||||
"general" => "General",
|
||||
"general_configuration" => "General Configuration",
|
||||
"giftcard_number" => "Gift Card Number",
|
||||
"giftcard_random" => "Generate Random",
|
||||
"giftcard_series" => "Generate in Series",
|
||||
"image_allowed_file_types" => "Allowed file types",
|
||||
"image_max_height_tooltip" => "Maximum allowed height of image uploads in pixels (px).",
|
||||
"image_max_size_tooltip" => "Maximum allowed file size of image uploads in kilobytes (kb).",
|
||||
"image_max_width_tooltip" => "Maximum allowed width of image uploads in pixels (px).",
|
||||
"image_restrictions" => "Image Upload Restrictions",
|
||||
"include_hsn" => "Include Support for HSN Codes",
|
||||
"info" => "Information",
|
||||
"info_configuration" => "Store Information",
|
||||
"input_groups" => "Input Groups",
|
||||
"integrations" => "Integrations",
|
||||
"integrations_configuration" => "Third Party Integrations",
|
||||
"invoice" => "Invoice",
|
||||
"invoice_configuration" => "Invoice Print Settings",
|
||||
"invoice_default_comments" => "Default Invoice Comments",
|
||||
"invoice_email_message" => "Invoice Email Template",
|
||||
"invoice_enable" => "Enable Invoicing",
|
||||
"invoice_printer" => "Invoice Printer",
|
||||
"invoice_type" => "Invoice Type",
|
||||
"is_readable" => "is readable, but the permissions are incorrectly set. Please set it to 640 or 660 and refresh.",
|
||||
"is_writable" => "is writable, but the permissions are incorrectly set. Please set it to 750 and refresh.",
|
||||
"item_markup" => "",
|
||||
"jsprintsetup_required" => "Warning: This functionality will only work if you have the FireFox jsPrintSetup addon installed. Save anyway?",
|
||||
"language" => "Language",
|
||||
"last_used_invoice_number" => "Last used Invoice Number",
|
||||
"last_used_quote_number" => "Last used Quote Number",
|
||||
"last_used_work_order_number" => "Last used W/O Number",
|
||||
"left" => "Left",
|
||||
"license" => "License",
|
||||
"license_configuration" => "License Statement",
|
||||
"line_sequence" => "Line Sequence",
|
||||
"lines_per_page" => "Lines per Page",
|
||||
"lines_per_page_number" => "Lines per Page must be a number.",
|
||||
"lines_per_page_required" => "Lines per Page is a required field.",
|
||||
"locale" => "Localization",
|
||||
"locale_configuration" => "Localization Configuration",
|
||||
"locale_info" => "Localization Configuration Information",
|
||||
"location" => "Stock",
|
||||
"location_configuration" => "Stock Locations",
|
||||
"location_info" => "Location Configuration Information",
|
||||
"login_form" => "Login Form Style",
|
||||
"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_configuration" => "MailChimp Configuration",
|
||||
"mailchimp_key_successfully" => "API Key is valid.",
|
||||
"mailchimp_key_unsuccessfully" => "API Key is invalid.",
|
||||
"mailchimp_lists" => "MailChimp List(s)",
|
||||
"mailchimp_tooltip" => "Click the icon for an API Key.",
|
||||
"message" => "Message",
|
||||
"message_configuration" => "Message Configuration",
|
||||
"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",
|
||||
"msg_uid_required" => "SMS-API Username is a required field",
|
||||
"multi_pack_enabled" => "Multiple Packages per Item",
|
||||
"no_risk" => "No security/vulnerability risks.",
|
||||
"none" => "none",
|
||||
"notify_alignment" => "Notification Popup Position",
|
||||
"number_format" => "Number Format",
|
||||
"number_locale" => "Localization",
|
||||
"number_locale_invalid" => "The entered locale is invalid. Check the link in the tooltip to find a valid locale.",
|
||||
"number_locale_required" => "Number Locale is a required field.",
|
||||
"number_locale_tooltip" => "Find a suitable locale through this link.",
|
||||
"os_timezone" => "OSPOS Timezone:",
|
||||
"ospos_info" => "OSPOS Installation Info",
|
||||
"payment_options_order" => "Payment Options Order",
|
||||
"perm_risk" => "Incorrect permissions leaves this software at risk.",
|
||||
"phone" => "Company Phone",
|
||||
"phone_required" => "Company Phone is a required field.",
|
||||
"print_bottom_margin" => "Margin Bottom",
|
||||
"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_footer" => "Print Browser Footer",
|
||||
"print_header" => "Print Browser Header",
|
||||
"print_left_margin" => "Margin Left",
|
||||
"print_left_margin_number" => "Margin Left must be a number.",
|
||||
"print_left_margin_required" => "Margin Left is a required field.",
|
||||
"print_receipt_check_behaviour" => "Print Receipt checkbox",
|
||||
"print_receipt_check_behaviour_always" => "Always checked",
|
||||
"print_receipt_check_behaviour_last" => "Remember last selection",
|
||||
"print_receipt_check_behaviour_never" => "Always unchecked",
|
||||
"print_right_margin" => "Margin Right",
|
||||
"print_right_margin_number" => "Margin Right must be a number.",
|
||||
"print_right_margin_required" => "Margin Right is a required field.",
|
||||
"print_silently" => "Show Print Dialog",
|
||||
"print_top_margin" => "Margin Top",
|
||||
"print_top_margin_number" => "Margin Top must be a number.",
|
||||
"print_top_margin_required" => "Margin Top is a required field.",
|
||||
"quantity_decimals" => "Quantity Decimals",
|
||||
"quick_cash_enable" => "",
|
||||
"quote_default_comments" => "Default Quote Comments",
|
||||
"receipt" => "Receipt",
|
||||
"receipt_category" => "",
|
||||
"receipt_configuration" => "Receipt Print Settings",
|
||||
"receipt_default" => "Default",
|
||||
"receipt_font_size" => "Font Size",
|
||||
"receipt_font_size_number" => "Font Size must be a number.",
|
||||
"receipt_font_size_required" => "Font Size is a required field.",
|
||||
"receipt_info" => "Receipt Configuration Information",
|
||||
"receipt_printer" => "Ticket Printer",
|
||||
"receipt_short" => "Short",
|
||||
"receipt_show_company_name" => "Show Company Name",
|
||||
"receipt_show_description" => "Show Description",
|
||||
"receipt_show_serialnumber" => "Show Serial Number",
|
||||
"receipt_show_secondary_currency" => "Show Secondary Currency",
|
||||
"receipt_show_tax_ind" => "Show Tax Indicator",
|
||||
"receipt_show_taxes" => "Show Taxes",
|
||||
"receipt_show_total_discount" => "Show Total Discount",
|
||||
"receipt_template" => "Receipt Template",
|
||||
"secondary_currency" => "Secondary Currency",
|
||||
"secondary_currency_decimals" => "Secondary Currency Decimals",
|
||||
"secondary_currency_code" => "Secondary Currency Code",
|
||||
"secondary_currency_enable" => "Enable Secondary Currency",
|
||||
"secondary_currency_enable_tooltip" => "Show secondary currency fields and print/display values across the app.",
|
||||
"secondary_currency_rate" => "Secondary Currency Rate",
|
||||
"secondary_currency_settings" => "Secondary Currency Settings",
|
||||
"secondary_currency_symbol" => "Secondary Currency Symbol",
|
||||
"receiving_calculate_average_price" => "Calc avg. Price (Receiving)",
|
||||
"recv_invoice_format" => "Receivings Invoice Format",
|
||||
"register_mode_default" => "Default Register Mode",
|
||||
"report_an_issue" => "Report an issue",
|
||||
"return_policy_required" => "Return policy is a required field.",
|
||||
"reward" => "Reward",
|
||||
"reward_configuration" => "Reward Configuration",
|
||||
"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.",
|
||||
"saved_successfully" => "Configuration save successful.",
|
||||
"saved_unsuccessfully" => "Configuration save failed.",
|
||||
"security_issue" => "Security Vulnerability Warning",
|
||||
"recv_invoice_format" => "Receivings Invoice Format",
|
||||
"register_mode_default" => "Default Register Mode",
|
||||
"report_an_issue" => "Report an issue",
|
||||
"return_policy_required" => "Return policy is a required field.",
|
||||
"reward" => "Reward",
|
||||
"reward_configuration" => "Reward Configuration",
|
||||
"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.",
|
||||
"saved_successfully" => "Configuration save successful.",
|
||||
"saved_unsuccessfully" => "Configuration save failed.",
|
||||
"security_issue" => "Security Vulnerability Warning",
|
||||
"server_notice" => "Please use the below info for issue reporting.",
|
||||
"service_charge" => "",
|
||||
"customer_display" => "Customer Display",
|
||||
"show_due_enable" => "",
|
||||
"show_office_group" => "Show office icon",
|
||||
"statistics" => "Send Statistics",
|
||||
"statistics_tooltip" => "Send statistics for development and feature improvement purposes.",
|
||||
"stock_location" => "Stock location",
|
||||
"stock_location_duplicate" => "Stock Location must be unique.",
|
||||
"stock_location_invalid_chars" => "Stock Location can not contain '_'.",
|
||||
"stock_location_required" => "Stock location is a required field.",
|
||||
"suggestions_fifth_column" => "",
|
||||
"suggestions_first_column" => "Column 1",
|
||||
"suggestions_fourth_column" => "",
|
||||
"suggestions_layout" => "Search Suggestions Layout",
|
||||
"suggestions_second_column" => "Column 2",
|
||||
"suggestions_third_column" => "Column 3",
|
||||
"system_conf" => "Setup & Conf",
|
||||
"system_info" => "System Info",
|
||||
"table" => "Table",
|
||||
"table_configuration" => "Table Configuration",
|
||||
"takings_printer" => "Receipt Printer",
|
||||
"tax" => "Tax",
|
||||
"tax_category" => "Tax Category",
|
||||
"tax_category_duplicate" => "The entered tax category already exists.",
|
||||
"tax_category_invalid_chars" => "The entered tax category is invalid.",
|
||||
"tax_category_required" => "The tax category is required.",
|
||||
"tax_category_used" => "Tax category cannot be deleted because it is being used.",
|
||||
"tax_configuration" => "Tax Configuration",
|
||||
"tax_decimals" => "Tax Decimals",
|
||||
"tax_id" => "Tax Id",
|
||||
"tax_included" => "Tax Included",
|
||||
"theme" => "Theme",
|
||||
"theme_preview" => "Preview Theme:",
|
||||
"thousands_separator" => "Thousands Separator",
|
||||
"timezone" => "Timezone",
|
||||
"timezone_error" => "OSPOS Timezone is Different from your Local Timezone.",
|
||||
"top" => "Top",
|
||||
"use_destination_based_tax" => "Use Destination Based Tax",
|
||||
"user_timezone" => "Local Timezone:",
|
||||
"website" => "Website",
|
||||
"wholesale_markup" => "",
|
||||
"work_order_enable" => "Work Order Support",
|
||||
"work_order_format" => "Work Order Format",
|
||||
];
|
||||
|
||||
|
||||
"show_office_group" => "Show office icon",
|
||||
"statistics" => "Send Statistics",
|
||||
"statistics_tooltip" => "Send statistics for development and feature improvement purposes.",
|
||||
"stock_location" => "Stock location",
|
||||
"stock_location_duplicate" => "Stock Location must be unique.",
|
||||
"stock_location_invalid_chars" => "Stock Location can not contain '_'.",
|
||||
"stock_location_required" => "Stock location is a required field.",
|
||||
"suggestions_fifth_column" => "",
|
||||
"suggestions_first_column" => "Column 1",
|
||||
"suggestions_fourth_column" => "",
|
||||
"suggestions_layout" => "Search Suggestions Layout",
|
||||
"suggestions_second_column" => "Column 2",
|
||||
"suggestions_third_column" => "Column 3",
|
||||
"shortcuts" => "Shortcuts",
|
||||
"shortcuts_configuration" => "Sales Keyboard Shortcut Configuration",
|
||||
"shortcuts_duplicate_bindings" => "Shortcut bindings must be unique.",
|
||||
"shortcuts_save_error" => "Unable to save shortcut settings.",
|
||||
"system_conf" => "Setup & Conf",
|
||||
"system_info" => "System Info",
|
||||
"table" => "Table",
|
||||
"table_configuration" => "Table Configuration",
|
||||
"takings_printer" => "Receipt Printer",
|
||||
"tax" => "Tax",
|
||||
"tax_category" => "Tax Category",
|
||||
"tax_category_duplicate" => "The entered tax category already exists.",
|
||||
"tax_category_invalid_chars" => "The entered tax category is invalid.",
|
||||
"tax_category_required" => "The tax category is required.",
|
||||
"tax_category_used" => "Tax category cannot be deleted because it is being used.",
|
||||
"tax_configuration" => "Tax Configuration",
|
||||
"tax_decimals" => "Tax Decimals",
|
||||
"tax_id" => "Tax Id",
|
||||
"tax_included" => "Tax Included",
|
||||
"theme" => "Theme",
|
||||
"theme_preview" => "Preview Theme:",
|
||||
"thousands_separator" => "Thousands Separator",
|
||||
"timezone" => "Timezone",
|
||||
"timezone_error" => "OSPOS Timezone is Different from your Local Timezone.",
|
||||
"top" => "Top",
|
||||
"use_destination_based_tax" => "Use Destination Based Tax",
|
||||
"user_timezone" => "Local Timezone:",
|
||||
"website" => "Website",
|
||||
"wholesale_markup" => "",
|
||||
"work_order_enable" => "Work Order Support",
|
||||
"work_order_format" => "Work Order Format",
|
||||
];
|
||||
|
||||
@@ -1,240 +1,237 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
"customers_available_points" => "Points Available",
|
||||
"rewards_package" => "Rewards",
|
||||
"rewards_remaining_balance" => "Reward Points remaining value is ",
|
||||
"account_number" => "Account #",
|
||||
"add_payment" => "Add Payment",
|
||||
"amount_due" => "Amount Due",
|
||||
"amount_due_lbp" => "Amount Due LBP",
|
||||
"amount_tendered" => "Amount Tendered",
|
||||
"authorized_signature" => "Authorized Signature",
|
||||
"cancel_sale" => "Cancel",
|
||||
"cash" => "Cash",
|
||||
"cash_1" => "",
|
||||
"cash_2" => "",
|
||||
"cash_3" => "",
|
||||
"cash_4" => "",
|
||||
"cash_adjustment" => "Cash Adjustment",
|
||||
"cash_deposit" => "Cash Deposit",
|
||||
"cash_filter" => "Cash",
|
||||
"change_due" => "Change Due",
|
||||
"change" => "Change",
|
||||
"currency_rate" => "Currency Rate",
|
||||
"change_price" => "Change Selling Price",
|
||||
"check" => "Check",
|
||||
"check_balance" => "Check remainder",
|
||||
"check_filter" => "Check",
|
||||
"close" => "",
|
||||
"comment" => "Comment",
|
||||
"comments" => "Comments",
|
||||
"company_name" => "",
|
||||
"complete" => "",
|
||||
"complete_sale" => "Complete",
|
||||
"confirm_cancel_sale" => "Are you sure you want to clear this sale? All items will be cleared.",
|
||||
"confirm_delete" => "Are you sure you want to delete the selected Sale(s)?",
|
||||
"confirm_restore" => "Are you sure you want to restore the selected Sale(s)?",
|
||||
"credit" => "Credit Card",
|
||||
"credit_deposit" => "Credit Deposit",
|
||||
"credit_filter" => "Credit Card",
|
||||
"current_table" => "",
|
||||
"customer" => "Customer",
|
||||
"customer_address" => "Address",
|
||||
"customer_discount" => "Discount",
|
||||
"customer_email" => "Email",
|
||||
"customer_name" => "Customer Name",
|
||||
"customer_location" => "Location",
|
||||
"customer_mailchimp_status" => "MailChimp Status",
|
||||
"customer_optional" => "(Required for Due Payments)",
|
||||
"customer_required" => "(Required)",
|
||||
"customer_total" => "Total",
|
||||
"customer_total_spent" => "",
|
||||
"daily_sales" => "",
|
||||
"date" => "Sale Date",
|
||||
"date_range" => "Date Range",
|
||||
"date_required" => "A correct date must be entered.",
|
||||
"date_type" => "Date is a required field.",
|
||||
"debit" => "Debit Card",
|
||||
"debit_filter" => "",
|
||||
"delete" => "Allow Delete",
|
||||
"delete_confirmation" => "Are you sure you want to delete this sale? This action cannot be undone.",
|
||||
"delete_entire_sale" => "Delete Entire Sale",
|
||||
"delete_successful" => "Sale delete successful.",
|
||||
"delete_unsuccessful" => "Sale delete failed.",
|
||||
"description_abbrv" => "Desc.",
|
||||
"discard" => "Discard",
|
||||
"discard_quote" => "",
|
||||
"discount" => "Disc",
|
||||
"discount_included" => "% Discount",
|
||||
"discount_short" => "%",
|
||||
"due" => "Due",
|
||||
"due_filter" => "Due",
|
||||
"edit" => "Edit",
|
||||
"edit_item" => "Edit Item",
|
||||
"edit_sale" => "Edit Sale",
|
||||
"email_receipt" => "Email Receipt",
|
||||
"employee" => "Employee",
|
||||
"entry" => "Entry",
|
||||
"error_editing_item" => "Error editing item",
|
||||
"find_or_scan_item" => "Find or Scan Item",
|
||||
"find_or_scan_item_or_receipt" => "Find or Scan Item or Receipt",
|
||||
"giftcard" => "Gift Card",
|
||||
"giftcard_balance" => "Gift Card Balance",
|
||||
"giftcard_filter" => "",
|
||||
"giftcard_number" => "Gift Card Number",
|
||||
"group_by_category" => "Group by Category",
|
||||
"group_by_type" => "Group by Type",
|
||||
"hsn" => "HSN",
|
||||
"id" => "Sale ID",
|
||||
"include_prices" => "Include Prices?",
|
||||
"invoice" => "Invoice",
|
||||
"invoice_confirm" => "This invoice will be sent to",
|
||||
"invoice_enable" => "Invoice Number",
|
||||
"invoice_filter" => "Invoices",
|
||||
"invoice_no_email" => "This customer does not have a valid email address.",
|
||||
"invoice_number" => "Invoice #",
|
||||
"invoice_number_duplicate" => "Invoice Number {0} must be unique.",
|
||||
"invoice_sent" => "Invoice sent to",
|
||||
"invoice_total" => "Invoice Total",
|
||||
"invoice_type_custom_invoice" => "Custom Invoice (custom_invoice.php)",
|
||||
"invoice_type_custom_tax_invoice" => "Custom Tax Invoice (custom_tax_invoice.php)",
|
||||
"invoice_type_invoice" => "Invoice (invoice.php)",
|
||||
"invoice_type_tax_invoice" => "Tax Invoice (tax_invoice.php)",
|
||||
"invoice_unsent" => "Invoice failed to be sent to",
|
||||
"invoice_update" => "Recount",
|
||||
"item_insufficient_of_stock" => "Item has insufficient stock.",
|
||||
"item_name" => "Item Name",
|
||||
"item_number" => "Item #",
|
||||
"item_out_of_stock" => "Item is out of stock.",
|
||||
"items" => "Items",
|
||||
"key_browser" => "Helpful Shortcuts",
|
||||
"key_cancel" => "Cancels Current Quote/Invoice/Sale",
|
||||
"key_customer_search" => "Customer Search",
|
||||
"key_finish_quote" => "Finish Quote/Invoice without payment",
|
||||
"key_finish_sale" => "Add Payment and Complete Invoice/Sale",
|
||||
"key_full" => "Open in Full Screen Mode",
|
||||
"key_function" => "Function",
|
||||
"key_help" => "Shortcuts",
|
||||
"key_help_modal" => "Open Shortcuts Window",
|
||||
"key_in" => "Zoom in",
|
||||
"key_item_search" => "Item Search",
|
||||
"key_out" => "Zoom Out",
|
||||
"key_payment" => "Add Payment",
|
||||
"key_print" => "Print Current Page",
|
||||
"key_restore" => "Restore Original Display/Zoom",
|
||||
"key_search" => "Search Reports Tables",
|
||||
"key_suspend" => "Suspend Current Sale",
|
||||
"key_suspended" => "Show Suspended Sales",
|
||||
"key_system" => "System Shortcuts",
|
||||
"key_tendered" => "Edit Amount Tendered",
|
||||
"key_title" => "Sales Keyboard Shortcuts",
|
||||
"mc" => "",
|
||||
"mode" => "Register Mode",
|
||||
"must_enter_numeric" => "Amount Tendered must be a number.",
|
||||
"must_enter_numeric_giftcard" => "Gift Card Number must be a number.",
|
||||
"new_customer" => "New Customer",
|
||||
"new_item" => "New Item",
|
||||
"no_description" => "No description",
|
||||
"no_filter" => "All",
|
||||
"no_items_in_cart" => "There are no Items in the cart.",
|
||||
"no_sales_to_display" => "No Sales to display.",
|
||||
"none_selected" => "You have not selected any Sale(s) to delete.",
|
||||
"nontaxed_ind" => " ",
|
||||
"not_authorized" => "This action is not authorized.",
|
||||
"one_or_multiple" => "Sale(s)",
|
||||
"payment" => "Payment Type",
|
||||
"payment_amount" => "Amount",
|
||||
"payment_not_cover_total" => "Payment Amount must be greater than or equal to Total.",
|
||||
"payment_type" => "Type",
|
||||
"payments" => "",
|
||||
"payments_total" => "Payments Total",
|
||||
"loyalty_reward_points" => "Loyalty Reward Points",
|
||||
"price" => "Price",
|
||||
"price_with_currency" => "Price (%s)",
|
||||
"print_after_sale" => "Print after Sale",
|
||||
"quantity" => "Quantity",
|
||||
"quantity_less_than_reorder_level" => "Warning: Desired Quantity is below Reorder Level for that Item.",
|
||||
"quantity_less_than_zero" => "Warning: Desired Quantity is insufficient. You can still process the sale, but audit your inventory.",
|
||||
"quantity_of_items" => "Quantity of {0} Items",
|
||||
"quote" => "Quote",
|
||||
"quote_number" => "Quote Number",
|
||||
"quote_number_duplicate" => "Quote Number must be unique.",
|
||||
"quote_sent" => "Quote sent to",
|
||||
"quote_unsent" => "Quote failed to be sent to",
|
||||
"receipt" => "Sales Receipt",
|
||||
"receipt_no_email" => "This customer does not have a valid email address.",
|
||||
"receipt_number" => "Sale #",
|
||||
"receipt_sent" => "Receipt sent to",
|
||||
"receipt_unsent" => "Receipt failed to be sent to",
|
||||
"rate" => "Rate",
|
||||
"refund" => "Refund Type",
|
||||
"register" => "Sales Register",
|
||||
"remove_customer" => "Remove Customer",
|
||||
"remove_discount" => "",
|
||||
"customer_display" => "Customer Display",
|
||||
"summary" => "Summary",
|
||||
"return" => "Return",
|
||||
"rewards" => "Reward Points",
|
||||
"rewards_balance" => "Reward Points Balance",
|
||||
"sale" => "Sale",
|
||||
"sale_by_invoice" => "Sale by Invoice",
|
||||
"sale_for_customer" => "Customer:",
|
||||
"sale_time" => "Time",
|
||||
"sales_tax" => "Sales Tax",
|
||||
"sales_total" => "",
|
||||
"select_customer" => "Select Customer",
|
||||
"selected_customer" => "Selected Customer",
|
||||
"walk_in_customer" => "Walk-in Customer",
|
||||
"send_invoice" => "Send Invoice",
|
||||
"send_quote" => "Send Quote",
|
||||
"send_receipt" => "Send Receipt",
|
||||
"send_work_order" => "Send Work Order",
|
||||
"serial" => "Serial",
|
||||
"service_charge" => "",
|
||||
"show_due" => "",
|
||||
"show_invoice" => "Show Invoice",
|
||||
"show_receipt" => "Show Receipt",
|
||||
"start_typing_customer_name" => "Start typing customer details...",
|
||||
"start_typing_item_name" => "Start typing Item Name or scan Barcode...",
|
||||
"stock" => "Stock",
|
||||
"stock_location" => "Stock Location",
|
||||
"sub_total" => "Subtotal",
|
||||
"successfully_deleted" => "You have successfully deleted",
|
||||
"successfully_restored" => "You have successfully restored",
|
||||
"successfully_suspended_sale" => "Sale suspend successful.",
|
||||
"successfully_updated" => "Sale update successful.",
|
||||
"suspend_sale" => "Suspend",
|
||||
"suspended_doc_id" => "Document",
|
||||
"suspended_sale_id" => "ID",
|
||||
"suspended_sales" => "Suspended",
|
||||
"table" => "Table",
|
||||
"takings" => "Daily Sales",
|
||||
"tax" => "Tax",
|
||||
"tax_id" => "Tax Id",
|
||||
"tax_invoice" => "Tax Invoice",
|
||||
"tax_percent" => "Tax %",
|
||||
"taxed_ind" => "T",
|
||||
"total" => "Total",
|
||||
"total_lbp" => "Total LBP",
|
||||
"total_tax_exclusive" => "Tax excluded",
|
||||
"transaction_failed" => "Sales Transaction failed.",
|
||||
"unable_to_add_item" => "Item add to Sale failed",
|
||||
"unsuccessfully_deleted" => "Sale(s) delete failed.",
|
||||
"unsuccessfully_restored" => "Sale(s) restore failed.",
|
||||
"unsuccessfully_suspended_sale" => "Sale suspend failed.",
|
||||
"unsuccessfully_updated" => "Sale update failed.",
|
||||
"unsuspend" => "Unsuspend",
|
||||
"unsuspend_and_delete" => "Action",
|
||||
"update" => "Update",
|
||||
"upi" => "UPI",
|
||||
"visa" => "",
|
||||
"wholesale" => "",
|
||||
"work_order" => "Work Order",
|
||||
"work_order_number" => "Work Order Number",
|
||||
"work_order_number_duplicate" => "Work Order Number must be unique.",
|
||||
"work_order_sent" => "Work Order sent to",
|
||||
"work_order_unsent" => "Work Order failed to be sent to",
|
||||
'customers_available_points' => 'Points Available',
|
||||
'rewards_package' => 'Rewards',
|
||||
'rewards_remaining_balance' => 'Reward Points remaining value is ',
|
||||
'account_number' => 'Account #',
|
||||
'add_payment' => 'Add Payment',
|
||||
'amount_due' => 'Amount Due',
|
||||
'amount_tendered' => 'Amount Tendered',
|
||||
'authorized_signature' => 'Authorized Signature',
|
||||
'bank_transfer' => 'Bank Transfer',
|
||||
'cancel_sale' => 'Cancel',
|
||||
'cash' => 'Cash',
|
||||
'cash_1' => '',
|
||||
'cash_2' => '',
|
||||
'cash_3' => '',
|
||||
'cash_4' => '',
|
||||
'cash_adjustment' => 'Cash Adjustment',
|
||||
'cash_deposit' => 'Cash Deposit',
|
||||
'cash_filter' => 'Cash',
|
||||
'change_due' => 'Change Due',
|
||||
'change_price' => 'Change Selling Price',
|
||||
'check' => 'Check',
|
||||
'check_balance' => 'Check remainder',
|
||||
'check_filter' => 'Check',
|
||||
'close' => '',
|
||||
'comment' => 'Comment',
|
||||
'comments' => 'Comments',
|
||||
'company_name' => '',
|
||||
'complete' => '',
|
||||
'complete_sale' => 'Complete',
|
||||
'confirm_cancel_sale' => 'Are you sure you want to clear this sale? All items will be cleared.',
|
||||
'confirm_delete' => 'Are you sure you want to delete the selected Sale(s)?',
|
||||
'confirm_restore' => 'Are you sure you want to restore the selected Sale(s)?',
|
||||
'credit' => 'Credit Card',
|
||||
'credit_deposit' => 'Credit Deposit',
|
||||
'credit_filter' => 'Credit Card',
|
||||
'current_table' => '',
|
||||
'customer' => 'Customer',
|
||||
'customer_address' => 'Address',
|
||||
'customer_discount' => 'Discount',
|
||||
'customer_email' => 'Email',
|
||||
'customer_location' => 'Location',
|
||||
'customer_mailchimp_status' => 'MailChimp Status',
|
||||
'customer_optional' => '(Required for Due Payments)',
|
||||
'customer_required' => '(Required)',
|
||||
'customer_total' => 'Total',
|
||||
'customer_total_spent' => '',
|
||||
'daily_sales' => '',
|
||||
'date' => 'Sale Date',
|
||||
'date_range' => 'Date Range',
|
||||
'date_required' => 'A correct date must be entered.',
|
||||
'date_type' => 'Date is a required field.',
|
||||
'debit' => 'Debit Card',
|
||||
'debit_filter' => '',
|
||||
'delete' => 'Allow Delete',
|
||||
'delete_confirmation' => 'Are you sure you want to delete this sale? This action cannot be undone.',
|
||||
'delete_entire_sale' => 'Delete Entire Sale',
|
||||
'delete_successful' => 'Sale delete successful.',
|
||||
'delete_unsuccessful' => 'Sale delete failed.',
|
||||
'description_abbrv' => 'Desc.',
|
||||
'discard' => 'Discard',
|
||||
'discard_quote' => '',
|
||||
'discount' => 'Disc',
|
||||
'discount_included' => '% Discount',
|
||||
'discount_short' => '%',
|
||||
'due' => 'Due',
|
||||
'due_filter' => 'Due',
|
||||
'edit' => 'Edit',
|
||||
'edit_item' => 'Edit Item',
|
||||
'edit_sale' => 'Edit Sale',
|
||||
'email_receipt' => 'Email Receipt',
|
||||
'employee' => 'Employee',
|
||||
'entry' => 'Entry',
|
||||
'error_editing_item' => 'Error editing item',
|
||||
'negative_price_invalid' => 'Price cannot be negative.',
|
||||
'negative_quantity_invalid' => 'Quantity cannot be negative.',
|
||||
'negative_discount_invalid' => 'Discount cannot be negative.',
|
||||
'discount_percent_exceeds_100' => 'Percentage discount cannot exceed 100%.',
|
||||
'discount_exceeds_item_total' => 'Discount cannot exceed the item total.',
|
||||
'negative_total_invalid' => 'Sale total cannot be negative. Check item discounts and quantities.',
|
||||
'find_or_scan_item' => 'Find or Scan Item',
|
||||
'find_or_scan_item_or_receipt' => 'Find or Scan Item or Receipt',
|
||||
'giftcard' => 'Gift Card',
|
||||
'giftcard_balance' => 'Gift Card Balance',
|
||||
'giftcard_filter' => '',
|
||||
'giftcard_number' => 'Gift Card Number',
|
||||
'group_by_category' => 'Group by Category',
|
||||
'group_by_type' => 'Group by Type',
|
||||
'hsn' => 'HSN',
|
||||
'id' => 'Sale ID',
|
||||
'include_prices' => 'Include Prices?',
|
||||
'invoice' => 'Invoice',
|
||||
'invoice_confirm' => 'This invoice will be sent to',
|
||||
'invoice_enable' => 'Invoice Number',
|
||||
'invoice_filter' => 'Invoices',
|
||||
'invoice_no_email' => 'This customer does not have a valid email address.',
|
||||
'invoice_number' => 'Invoice #',
|
||||
'invoice_number_duplicate' => 'Invoice Number {0} must be unique.',
|
||||
'invoice_sent' => 'Invoice sent to',
|
||||
'invoice_total' => 'Invoice Total',
|
||||
'invoice_type_custom_invoice' => 'Custom Invoice (custom_invoice.php)',
|
||||
'invoice_type_custom_tax_invoice' => 'Custom Tax Invoice (custom_tax_invoice.php)',
|
||||
'invoice_type_invoice' => 'Invoice (invoice.php)',
|
||||
'invoice_type_tax_invoice' => 'Tax Invoice (tax_invoice.php)',
|
||||
'invoice_unsent' => 'Invoice failed to be sent to',
|
||||
'invoice_update' => 'Recount',
|
||||
'item_insufficient_of_stock' => 'Item has insufficient stock.',
|
||||
'item_name' => 'Item Name',
|
||||
'item_number' => 'Item #',
|
||||
'item_out_of_stock' => 'Item is out of stock.',
|
||||
'key_browser' => 'Helpful Shortcuts',
|
||||
'key_cancel' => 'Cancels Current Quote/Invoice/Sale',
|
||||
'key_customer_search' => 'Customer Search',
|
||||
'key_finish_quote' => 'Finish Quote/Invoice without payment',
|
||||
'key_finish_sale' => 'Add Payment and Complete Invoice/Sale',
|
||||
'key_full' => 'Open in Full Screen Mode',
|
||||
'key_function' => 'Function',
|
||||
'key_help' => 'Shortcuts',
|
||||
'key_help_modal' => 'Open Shortcuts Window',
|
||||
'key_in' => 'Zoom in',
|
||||
'key_item_search' => 'Item Search',
|
||||
'key_out' => 'Zoom Out',
|
||||
'key_payment' => 'Add Payment',
|
||||
'key_print' => 'Print Current Page',
|
||||
'key_restore' => 'Restore Original Display/Zoom',
|
||||
'key_search' => 'Search Reports Tables',
|
||||
'key_suspend' => 'Suspend Current Sale',
|
||||
'key_suspended' => 'Show Suspended Sales',
|
||||
'key_system' => 'System Shortcuts',
|
||||
'key_tendered' => 'Edit Amount Tendered',
|
||||
'key_title' => 'Sales Keyboard Shortcuts',
|
||||
'mc' => '',
|
||||
'mode' => 'Register Mode',
|
||||
'must_enter_numeric' => 'Amount Tendered must be a number.',
|
||||
'must_enter_numeric_giftcard' => 'Gift Card Number must be a number.',
|
||||
'new_customer' => 'New Customer',
|
||||
'new_item' => 'New Item',
|
||||
'no_description' => 'No description',
|
||||
'no_filter' => 'All',
|
||||
'no_items_in_cart' => 'There are no Items in the cart.',
|
||||
'no_sales_to_display' => 'No Sales to display.',
|
||||
'none_selected' => 'You have not selected any Sale(s) to delete.',
|
||||
'nontaxed_ind' => ' ',
|
||||
'not_authorized' => 'This action is not authorized.',
|
||||
'one_or_multiple' => 'Sale(s)',
|
||||
'payment' => 'Payment Type',
|
||||
'payment_amount' => 'Amount',
|
||||
'payment_not_cover_total' => 'Payment Amount must be greater than or equal to Total.',
|
||||
'payment_type' => 'Type',
|
||||
'payments' => '',
|
||||
'payments_total' => 'Payments Total',
|
||||
'price' => 'Price',
|
||||
'print_after_sale' => 'Print after Sale',
|
||||
'quantity' => 'Quantity',
|
||||
'quantity_less_than_reorder_level' => 'Warning: Desired Quantity is below Reorder Level for that Item.',
|
||||
'quantity_less_than_zero' => 'Warning: Desired Quantity is insufficient. You can still process the sale, but audit your inventory.',
|
||||
'quantity_of_items' => 'Quantity of {0} Items',
|
||||
'quote' => 'Quote',
|
||||
'quote_number' => 'Quote Number',
|
||||
'quote_number_duplicate' => 'Quote Number must be unique.',
|
||||
'quote_sent' => 'Quote sent to',
|
||||
'quote_unsent' => 'Quote failed to be sent to',
|
||||
'receipt' => 'Sales Receipt',
|
||||
'receipt_no_email' => 'This customer does not have a valid email address.',
|
||||
'receipt_number' => 'Sale #',
|
||||
'receipt_sent' => 'Receipt sent to',
|
||||
'receipt_unsent' => 'Receipt failed to be sent to',
|
||||
'refund' => 'Refund Type',
|
||||
'register' => 'Sales Register',
|
||||
'remove_customer' => 'Remove Customer',
|
||||
'remove_discount' => '',
|
||||
'return' => 'Return',
|
||||
'rewards' => 'Reward Points',
|
||||
'rewards_balance' => 'Reward Points Balance',
|
||||
'sale' => 'Sale',
|
||||
'sale_by_invoice' => 'Sale by Invoice',
|
||||
'sale_for_customer' => 'Customer:',
|
||||
'sale_time' => 'Time',
|
||||
'sales_tax' => 'Sales Tax',
|
||||
'sales_total' => '',
|
||||
'select_customer' => 'Select Customer',
|
||||
'selected_customer' => 'Selected Customer',
|
||||
'send_invoice' => 'Send Invoice',
|
||||
'send_quote' => 'Send Quote',
|
||||
'send_receipt' => 'Send Receipt',
|
||||
'send_work_order' => 'Send Work Order',
|
||||
'serial' => 'Serial',
|
||||
'service_charge' => '',
|
||||
'show_due' => '',
|
||||
'show_invoice' => 'Show Invoice',
|
||||
'show_receipt' => 'Show Receipt',
|
||||
'start_typing_customer_name' => 'Start typing customer details...',
|
||||
'start_typing_item_name' => 'Start typing Item Name or scan Barcode...',
|
||||
'stock' => 'Stock',
|
||||
'stock_location' => 'Stock Location',
|
||||
'sub_total' => 'Subtotal',
|
||||
'successfully_deleted' => 'You have successfully deleted',
|
||||
'successfully_restored' => 'You have successfully restored',
|
||||
'successfully_suspended_sale' => 'Sale suspend successful.',
|
||||
'successfully_updated' => 'Sale update successful.',
|
||||
'suspend_sale' => 'Suspend',
|
||||
'suspended_doc_id' => 'Document',
|
||||
'suspended_sale_id' => 'ID',
|
||||
'suspended_sales' => 'Suspended',
|
||||
'table' => 'Table',
|
||||
'takings' => 'Daily Sales',
|
||||
'tax' => 'Tax',
|
||||
'tax_id' => 'Tax Id',
|
||||
'tax_invoice' => 'Tax Invoice',
|
||||
'tax_percent' => 'Tax %',
|
||||
'taxed_ind' => 'T',
|
||||
'total' => 'Total',
|
||||
'total_tax_exclusive' => 'Tax excluded',
|
||||
'transaction_failed' => 'Sales Transaction failed.',
|
||||
'unable_to_add_item' => 'Item add to Sale failed',
|
||||
'unsuccessfully_deleted' => 'Sale(s) delete failed.',
|
||||
'unsuccessfully_restored' => 'Sale(s) restore failed.',
|
||||
'unsuccessfully_suspended_sale' => 'Sale suspend failed.',
|
||||
'unsuccessfully_updated' => 'Sale update failed.',
|
||||
'unsuspend' => 'Unsuspend',
|
||||
'unsuspend_and_delete' => 'Action',
|
||||
'update' => 'Update',
|
||||
'upi' => 'UPI',
|
||||
'visa' => '',
|
||||
'wallet' => 'Wallet',
|
||||
'wholesale' => '',
|
||||
'work_order' => 'Work Order',
|
||||
'work_order_number' => 'Work Order Number',
|
||||
'work_order_number_duplicate' => 'Work Order Number must be unique.',
|
||||
'work_order_sent' => 'Work Order sent to',
|
||||
'work_order_unsent' => 'Work Order failed to be sent to',
|
||||
'download_ubl' => 'Download UBL Invoice',
|
||||
'ubl_generation_failed' => 'UBL invoice generation failed',
|
||||
'sale_not_found' => 'Sale not found',
|
||||
];
|
||||
|
||||
|
||||
|
||||
@@ -118,4 +118,38 @@ class Email_lib
|
||||
|
||||
return '<img id="image" src="data:' . $mimeType . ';base64,' . $logo_data . '" alt="company_logo">';
|
||||
}
|
||||
|
||||
/**
|
||||
* Send email with multiple attachments
|
||||
*
|
||||
* @param string $to
|
||||
* @param string $subject
|
||||
* @param string $message
|
||||
* @param array $attachments
|
||||
* @return bool
|
||||
*/
|
||||
public function sendMultipleAttachments(string $to, string $subject, string $message, array $attachments): bool
|
||||
{
|
||||
$email = $this->email;
|
||||
|
||||
$email->setFrom($this->config['email'], $this->config['company']);
|
||||
$email->setTo($to);
|
||||
$email->setSubject($subject);
|
||||
$email->setMessage($message);
|
||||
|
||||
foreach ($attachments as $attachment) {
|
||||
if (!empty($attachment) && file_exists($attachment)) {
|
||||
$email->attach($attachment);
|
||||
}
|
||||
}
|
||||
|
||||
$result = $email->send();
|
||||
|
||||
if (!$result) {
|
||||
log_message('error', $email->printDebugger());
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
40
app/Libraries/InvoiceAttachment/InvoiceAttachment.php
Normal file
40
app/Libraries/InvoiceAttachment/InvoiceAttachment.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries\InvoiceAttachment;
|
||||
|
||||
interface InvoiceAttachment
|
||||
{
|
||||
/**
|
||||
* Generate the attachment content and write to a temp file.
|
||||
*
|
||||
* @param array $saleData The sale data from _load_sale_data()
|
||||
* @param string $type The document type (invoice, tax_invoice, quote, work_order, receipt)
|
||||
* @return string|null Absolute path to generated file, or null on failure
|
||||
*/
|
||||
public function generate(array $saleData, string $type): ?string;
|
||||
|
||||
/**
|
||||
* Check if this attachment type is applicable for the document type.
|
||||
* E.g., UBL only works for invoice/tax_invoice
|
||||
*
|
||||
* @param string $type The document type
|
||||
* @param array $saleData The sale data (to check invoice_number existence)
|
||||
* @return bool
|
||||
*/
|
||||
public function isApplicableForType(string $type, array $saleData): bool;
|
||||
|
||||
/**
|
||||
* Get the file extension for this attachment.
|
||||
*
|
||||
* @return string E.g., 'pdf', 'xml'
|
||||
*/
|
||||
public function getFileExtension(): string;
|
||||
|
||||
/**
|
||||
* Get the config values that enable this attachment.
|
||||
* Returns array of config values that should generate this attachment.
|
||||
*
|
||||
* @return array E.g., ['pdf_only', 'both'] for PDF
|
||||
*/
|
||||
public function getEnabledConfigValues(): array;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries\InvoiceAttachment;
|
||||
|
||||
class InvoiceAttachmentGenerator
|
||||
{
|
||||
/** @var InvoiceAttachment[] */
|
||||
private array $attachments = [];
|
||||
|
||||
/**
|
||||
* Register an attachment generator.
|
||||
*/
|
||||
public function register(InvoiceAttachment $attachment): self
|
||||
{
|
||||
$this->attachments[] = $attachment;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create generator with attachments based on config.
|
||||
* Factory method that instantiates the right attachments.
|
||||
*
|
||||
* @param string $invoiceFormat Config value: 'pdf_only', 'ubl_only', or 'both'
|
||||
* @return self
|
||||
*/
|
||||
public static function createFromConfig(string $invoiceFormat): self
|
||||
{
|
||||
$generator = new self();
|
||||
|
||||
if (in_array($invoiceFormat, ['pdf_only', 'both'], true)) {
|
||||
$generator->register(new PdfAttachment());
|
||||
}
|
||||
|
||||
if (in_array($invoiceFormat, ['ubl_only', 'both'], true)) {
|
||||
$generator->register(new UblAttachment());
|
||||
}
|
||||
|
||||
return $generator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate all applicable attachments for a sale.
|
||||
*
|
||||
* @param array $saleData The sale data
|
||||
* @param string $type The document type
|
||||
* @return string[] Array of file paths to generated attachments
|
||||
*/
|
||||
public function generateAttachments(array $saleData, string $type): array
|
||||
{
|
||||
$files = [];
|
||||
|
||||
foreach ($this->attachments as $attachment) {
|
||||
if ($attachment->isApplicableForType($type, $saleData)) {
|
||||
$filepath = $attachment->generate($saleData, $type);
|
||||
if ($filepath !== null) {
|
||||
$files[] = $filepath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up temporary attachment files.
|
||||
*
|
||||
* @param string[] $files
|
||||
*/
|
||||
public static function cleanup(array $files): void
|
||||
{
|
||||
foreach ($files as $file) {
|
||||
@unlink($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
61
app/Libraries/InvoiceAttachment/PdfAttachment.php
Normal file
61
app/Libraries/InvoiceAttachment/PdfAttachment.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries\InvoiceAttachment;
|
||||
|
||||
use CodeIgniter\Config\Services;
|
||||
|
||||
class PdfAttachment implements InvoiceAttachment
|
||||
{
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function generate(array $saleData, string $type): ?string
|
||||
{
|
||||
$view = Services::renderer();
|
||||
$html = $view->setData($saleData)->render("sales/{$type}_email");
|
||||
|
||||
helper(['dompdf', 'file']);
|
||||
|
||||
$tempPath = tempnam(sys_get_temp_dir(), 'ospos_pdf_');
|
||||
if ($tempPath === false) {
|
||||
log_message('error', 'PDF attachment: failed to create temp file');
|
||||
return null;
|
||||
}
|
||||
|
||||
$filename = $tempPath . '.pdf';
|
||||
rename($tempPath, $filename);
|
||||
|
||||
$pdfContent = create_pdf($html);
|
||||
if (file_put_contents($filename, $pdfContent) === false) {
|
||||
log_message('error', 'PDF attachment: failed to write content');
|
||||
@unlink($filename);
|
||||
return null;
|
||||
}
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function isApplicableForType(string $type, array $saleData): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getFileExtension(): string
|
||||
{
|
||||
return 'pdf';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getEnabledConfigValues(): array
|
||||
{
|
||||
return ['pdf_only', 'both'];
|
||||
}
|
||||
}
|
||||
69
app/Libraries/InvoiceAttachment/UblAttachment.php
Normal file
69
app/Libraries/InvoiceAttachment/UblAttachment.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries\InvoiceAttachment;
|
||||
|
||||
use App\Libraries\UBLGenerator;
|
||||
|
||||
class UblAttachment implements InvoiceAttachment
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
require_once ROOTPATH . 'vendor/autoload.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function generate(array $saleData, string $type): ?string
|
||||
{
|
||||
try {
|
||||
$generator = new UBLGenerator();
|
||||
$xml = $generator->generateUblInvoice($saleData);
|
||||
|
||||
$tempPath = tempnam(sys_get_temp_dir(), 'ospos_ubl_');
|
||||
if ($tempPath === false) {
|
||||
log_message('error', 'UBL attachment: failed to create temp file');
|
||||
return null;
|
||||
}
|
||||
|
||||
$filename = $tempPath . '.xml';
|
||||
rename($tempPath, $filename);
|
||||
|
||||
if (file_put_contents($filename, $xml) === false) {
|
||||
log_message('error', 'UBL attachment: failed to write content');
|
||||
@unlink($filename);
|
||||
return null;
|
||||
}
|
||||
|
||||
return $filename;
|
||||
} catch (\Exception $e) {
|
||||
log_message('error', 'UBL attachment generation failed: ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function isApplicableForType(string $type, array $saleData): bool
|
||||
{
|
||||
return in_array($type, ['invoice', 'tax_invoice'], true)
|
||||
&& !empty($saleData['invoice_number']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getFileExtension(): string
|
||||
{
|
||||
return 'xml';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getEnabledConfigValues(): array
|
||||
{
|
||||
return ['ubl_only', 'both'];
|
||||
}
|
||||
}
|
||||
369
app/Libraries/UBLGenerator.php
Normal file
369
app/Libraries/UBLGenerator.php
Normal file
@@ -0,0 +1,369 @@
|
||||
<?php
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
use DateTime;
|
||||
use NumNum\UBL\AccountingParty;
|
||||
use NumNum\UBL\Address;
|
||||
use NumNum\UBL\AllowanceCharge;
|
||||
use NumNum\UBL\Contact;
|
||||
use NumNum\UBL\Country;
|
||||
use NumNum\UBL\Generator;
|
||||
use NumNum\UBL\Invoice;
|
||||
use NumNum\UBL\InvoiceLine;
|
||||
use NumNum\UBL\Item;
|
||||
use NumNum\UBL\LegalMonetaryTotal;
|
||||
use NumNum\UBL\Party;
|
||||
use NumNum\UBL\PartyTaxScheme;
|
||||
use NumNum\UBL\Price;
|
||||
use NumNum\UBL\TaxCategory;
|
||||
use NumNum\UBL\TaxScheme;
|
||||
use NumNum\UBL\TaxSubTotal;
|
||||
use NumNum\UBL\TaxTotal;
|
||||
use NumNum\UBL\UnitCode;
|
||||
|
||||
helper(['country']);
|
||||
|
||||
class UBLGenerator
|
||||
{
|
||||
/**
|
||||
* Generate UBL invoice XML from sale data
|
||||
*
|
||||
* @param array $saleData Sale data from _load_sale_data()
|
||||
*
|
||||
* @return string UBL XML string
|
||||
*/
|
||||
public function generateUblInvoice(array $saleData): string
|
||||
{
|
||||
$taxScheme = (new TaxScheme())->setId('VAT');
|
||||
$isTaxIncluded = ! empty($saleData['tax_included']);
|
||||
|
||||
$supplierParty = $this->buildSupplierParty($saleData, $taxScheme);
|
||||
$customerParty = $this->buildCustomerParty($saleData['customer_object'] ?? null, $taxScheme);
|
||||
$invoiceLines = $this->buildInvoiceLines($saleData, $taxScheme, $isTaxIncluded);
|
||||
$taxTotal = $this->buildTaxTotal($saleData['taxes'] ?? [], $taxScheme);
|
||||
$monetaryTotal = $this->buildMonetaryTotal($saleData);
|
||||
|
||||
$invoice = (new Invoice())
|
||||
->setUBLVersionId('2.1')
|
||||
->setCustomizationId('urn:cen.eu:en16931:2017')
|
||||
->setProfileId('urn:fdc:peppol.eu:2017:poacc:billing:01:1.0')
|
||||
->setId($saleData['invoice_number'] ?? '')
|
||||
->setIssueDate(new DateTime($saleData['transaction_date'] ?? 'now'))
|
||||
->setInvoiceTypeCode(380)
|
||||
->setAccountingSupplierParty($supplierParty)
|
||||
->setAccountingCustomerParty($customerParty)
|
||||
->setInvoiceLines($invoiceLines)
|
||||
->setTaxTotal($taxTotal)
|
||||
->setLegalMonetaryTotal($monetaryTotal);
|
||||
|
||||
// Set currency if available
|
||||
if (! empty($saleData['currency_code'])) {
|
||||
Generator::$currencyID = $saleData['currency_code'];
|
||||
}
|
||||
|
||||
$generator = new Generator();
|
||||
|
||||
return $generator->invoice($invoice);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build supplier (seller) party
|
||||
*/
|
||||
protected function buildSupplierParty(array $saleData, TaxScheme $taxScheme): AccountingParty
|
||||
{
|
||||
$config = $saleData['config'] ?? [];
|
||||
|
||||
$addressParts = $this->parseAddress($config['address'] ?? '');
|
||||
$countryCode = getCountryCode($config['country'] ?? '');
|
||||
|
||||
$country = (new Country())->setIdentificationCode($countryCode);
|
||||
$address = (new Address())
|
||||
->setStreetName($addressParts['street'] ?? '')
|
||||
->setBuildingNumber($addressParts['number'] ?? '')
|
||||
->setCityName($addressParts['city'] ?? '')
|
||||
->setPostalZone($addressParts['zip'] ?? '')
|
||||
->setCountrySubentity($config['state'] ?? '')
|
||||
->setCountry($country);
|
||||
|
||||
$party = (new Party())
|
||||
->setName($config['company'] ?? '')
|
||||
->setPostalAddress($address);
|
||||
|
||||
$partyTaxScheme = null;
|
||||
if (! empty($config['account_number'])) {
|
||||
$partyTaxScheme = (new PartyTaxScheme())
|
||||
->setCompanyId($config['account_number'])
|
||||
->setTaxScheme($taxScheme);
|
||||
$party->setPartyTaxScheme($partyTaxScheme);
|
||||
} elseif (! empty($config['tax_id'])) {
|
||||
// Use tax_id if account_number is not set
|
||||
$partyTaxScheme = (new PartyTaxScheme())
|
||||
->setCompanyId($config['tax_id'])
|
||||
->setTaxScheme($taxScheme);
|
||||
$party->setPartyTaxScheme($partyTaxScheme);
|
||||
}
|
||||
|
||||
return (new AccountingParty())->setParty($party);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build customer (buyer) party
|
||||
*/
|
||||
protected function buildCustomerParty(?object $customerInfo, TaxScheme $taxScheme): AccountingParty
|
||||
{
|
||||
if ($customerInfo === null) {
|
||||
return (new AccountingParty())->setParty(new Party());
|
||||
}
|
||||
|
||||
$countryCode = getCountryCode($customerInfo->country ?? '');
|
||||
|
||||
$country = (new Country())->setIdentificationCode($countryCode);
|
||||
$address = (new Address())
|
||||
->setStreetName($customerInfo->address_1 ?? '')
|
||||
->setAddressLine([$customerInfo->address_2 ?? ''])
|
||||
->setCityName($customerInfo->city ?? '')
|
||||
->setPostalZone($customerInfo->zip ?? '')
|
||||
->setCountrySubentity($customerInfo->state ?? '')
|
||||
->setCountry($country);
|
||||
|
||||
$partyName = ! empty($customerInfo->company_name)
|
||||
? $customerInfo->company_name
|
||||
: trim(($customerInfo->first_name ?? '') . ' ' . ($customerInfo->last_name ?? ''));
|
||||
|
||||
$party = (new Party())
|
||||
->setName($partyName)
|
||||
->setPostalAddress($address);
|
||||
|
||||
if (! empty($customerInfo->email)) {
|
||||
$contact = (new Contact())
|
||||
->setElectronicMail($customerInfo->email)
|
||||
->setTelephone($customerInfo->phone_number ?? '');
|
||||
$party->setContact($contact);
|
||||
}
|
||||
|
||||
$accountingParty = (new AccountingParty())->setParty($party);
|
||||
|
||||
if (! empty($customerInfo->account_number)) {
|
||||
$accountingParty->setSupplierAssignedAccountId($customerInfo->account_number);
|
||||
}
|
||||
|
||||
if (! empty($customerInfo->tax_id)) {
|
||||
$partyTaxScheme = (new PartyTaxScheme())
|
||||
->setCompanyId($customerInfo->tax_id)
|
||||
->setTaxScheme($taxScheme);
|
||||
$party->setPartyTaxScheme($partyTaxScheme);
|
||||
}
|
||||
|
||||
return $accountingParty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build invoice lines
|
||||
*/
|
||||
protected function buildInvoiceLines(array $saleData, TaxScheme $taxScheme, bool $isTaxIncluded): array
|
||||
{
|
||||
$lines = [];
|
||||
$itemTaxes = $saleData['item_taxes'] ?? [];
|
||||
$cart = $saleData['cart'] ?? [];
|
||||
|
||||
foreach ($cart as $item) {
|
||||
$itemId = $item['item_id'] ?? 0;
|
||||
$quantity = (float) ($item['quantity'] ?? 0);
|
||||
$unitPrice = (float) ($item['price'] ?? 0);
|
||||
$discount = (float) ($item['discount'] ?? 0);
|
||||
$discountType = (int) ($item['discount_type'] ?? 0);
|
||||
|
||||
// Calculate discount amount per unit
|
||||
if ($discountType === PERCENT && $discount > 0) {
|
||||
// Percentage discount
|
||||
$discountAmountPerUnit = round($unitPrice * $discount / 100, 4);
|
||||
} else {
|
||||
// Fixed discount (discount is total for the line, divide by quantity)
|
||||
$discountAmountPerUnit = $quantity > 0 ? round($discount / $quantity, 4) : 0;
|
||||
}
|
||||
|
||||
// Net price per unit (after discount)
|
||||
$netPricePerUnit = round($unitPrice - $discountAmountPerUnit, 4);
|
||||
if ($netPricePerUnit < 0) {
|
||||
$netPricePerUnit = 0;
|
||||
}
|
||||
|
||||
// Get tax rate for this item
|
||||
$taxRate = 0.0;
|
||||
$taxCategory = (new TaxCategory())
|
||||
->setId('S')
|
||||
->setPercent(0)
|
||||
->setTaxScheme($taxScheme);
|
||||
|
||||
if (isset($itemTaxes[$itemId]) && ! empty($itemTaxes[$itemId])) {
|
||||
// Use the first (primary) tax for this item
|
||||
$itemTax = $itemTaxes[$itemId][0];
|
||||
$taxRate = (float) ($itemTax['percent'] ?? 0);
|
||||
|
||||
if (abs($taxRate) < 0.001) {
|
||||
$taxCategory->setId('Z'); // Zero rated
|
||||
} elseif ($taxRate < 0) {
|
||||
$taxCategory->setId('E'); // Exempt
|
||||
} else {
|
||||
$taxCategory->setId('S'); // Standard
|
||||
}
|
||||
$taxCategory->setPercent(round($taxRate, 2));
|
||||
}
|
||||
|
||||
// Calculate line extension amount (net line total)
|
||||
$lineExtensionAmount = round($netPricePerUnit * $quantity, 2);
|
||||
|
||||
// Build Price - PriceAmount MUST be the net price excluding VAT per Peppol EN16931 (BR-27)
|
||||
// "The price of an item, exclusive of VAT, after subtracting discount"
|
||||
$price = (new Price())
|
||||
->setBaseQuantity(1.0)
|
||||
->setUnitCode(UnitCode::UNIT);
|
||||
|
||||
if ($isTaxIncluded && $taxRate > 0) {
|
||||
// Tax-inclusive: cart price includes VAT, so extract the net price
|
||||
// net_price = gross_price / (1 + tax_rate/100)
|
||||
$taxExclusivePricePerUnit = round($netPricePerUnit / (1 + $taxRate / 100), 4);
|
||||
$price->setPriceAmount($taxExclusivePricePerUnit);
|
||||
|
||||
// Recalculate line extension amount with tax-exclusive price
|
||||
$lineExtensionAmount = round($taxExclusivePricePerUnit * $quantity, 2);
|
||||
} else {
|
||||
// Tax-exclusive: cart price is already the net price
|
||||
$price->setPriceAmount(round($netPricePerUnit, 4));
|
||||
}
|
||||
|
||||
// Add AllowanceCharge if there's a discount (gross-to-net price reduction)
|
||||
if ($discountAmountPerUnit > 0) {
|
||||
$allowanceCharge = (new AllowanceCharge())
|
||||
->setChargeIndicator(false) // false = allowance/discount
|
||||
->setAllowanceChargeReason('Discount')
|
||||
->setAmount(round($discountAmountPerUnit, 4))
|
||||
->setBaseAmount(round((float) ($item['price'] ?? 0), 4));
|
||||
|
||||
$price->setAllowanceCharge($allowanceCharge);
|
||||
}
|
||||
|
||||
// Build Item
|
||||
$itemObj = (new Item())
|
||||
->setName($item['name'] ?? '')
|
||||
->setDescription($item['description'] ?? '')
|
||||
->setClassifiedTaxCategory($taxCategory);
|
||||
|
||||
// Add SellersItemIdentification if item_number exists (BR-25)
|
||||
if (! empty($item['item_number'])) {
|
||||
$itemObj->setSellersItemIdentification((string) $item['item_number']);
|
||||
}
|
||||
|
||||
// Build InvoiceLine
|
||||
$line = (new InvoiceLine())
|
||||
->setId(isset($item['line']) ? (string) $item['line'] : '1')
|
||||
->setInvoicedQuantity($quantity)
|
||||
->setLineExtensionAmount($lineExtensionAmount)
|
||||
->setItem($itemObj)
|
||||
->setPrice($price);
|
||||
|
||||
$lines[] = $line;
|
||||
}
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build tax total from sales_taxes table data
|
||||
*/
|
||||
protected function buildTaxTotal(array $taxes, TaxScheme $taxScheme): TaxTotal
|
||||
{
|
||||
$totalTax = '0';
|
||||
$taxSubTotals = [];
|
||||
|
||||
foreach ($taxes as $tax) {
|
||||
if (isset($tax['tax_rate'])) {
|
||||
$taxRate = (string) $tax['tax_rate'];
|
||||
$taxAmount = (string) ($tax['sale_tax_amount'] ?? '0');
|
||||
|
||||
// Use sale_tax_basis directly from DB instead of reverse-computing
|
||||
$taxableAmount = (string) ($tax['sale_tax_basis'] ?? '0');
|
||||
|
||||
// Determine category ID based on tax rate
|
||||
$categoryId = 'S'; // Standard
|
||||
$floatRate = (float) $taxRate;
|
||||
if (abs($floatRate) < 0.001) {
|
||||
$categoryId = 'Z'; // Zero rated
|
||||
} elseif ($floatRate < 0) {
|
||||
$categoryId = 'E'; // Exempt
|
||||
}
|
||||
|
||||
$taxCategory = (new TaxCategory())
|
||||
->setId($categoryId)
|
||||
->setPercent(round($floatRate, 2))
|
||||
->setTaxScheme($taxScheme);
|
||||
|
||||
$taxSubTotal = (new TaxSubTotal())
|
||||
->setTaxableAmount(round((float) $taxableAmount, 2))
|
||||
->setTaxAmount(round((float) $taxAmount, 2))
|
||||
->setTaxCategory($taxCategory);
|
||||
|
||||
$taxSubTotals[] = $taxSubTotal;
|
||||
$totalTax = bcadd($totalTax, $taxAmount);
|
||||
}
|
||||
}
|
||||
|
||||
$taxTotal = new TaxTotal();
|
||||
$taxTotal->setTaxAmount(round((float) $totalTax, 2));
|
||||
|
||||
foreach ($taxSubTotals as $subTotal) {
|
||||
$taxTotal->addTaxSubTotal($subTotal);
|
||||
}
|
||||
|
||||
return $taxTotal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build monetary total
|
||||
*/
|
||||
protected function buildMonetaryTotal(array $saleData): LegalMonetaryTotal
|
||||
{
|
||||
// In OSPOS, after get_totals(): subtotal is ALWAYS tax-exclusive (net)
|
||||
// total is ALWAYS tax-inclusive (gross)
|
||||
$subtotal = (float) ($saleData['subtotal'] ?? 0);
|
||||
$total = (float) ($saleData['total'] ?? 0);
|
||||
$amountDue = (float) ($saleData['amount_due'] ?? 0);
|
||||
|
||||
return (new LegalMonetaryTotal())
|
||||
->setLineExtensionAmount(round($subtotal, 2))
|
||||
->setTaxExclusiveAmount(round($subtotal, 2))
|
||||
->setTaxInclusiveAmount(round($total, 2))
|
||||
->setPayableAmount(round($amountDue, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse address string into components
|
||||
*/
|
||||
protected function parseAddress(string $address): array
|
||||
{
|
||||
$parts = array_filter(array_map('trim', explode("\n", $address)));
|
||||
|
||||
$result = [
|
||||
'street' => '',
|
||||
'number' => '',
|
||||
'city' => '',
|
||||
'zip' => '',
|
||||
];
|
||||
|
||||
if (! empty($parts)) {
|
||||
$result['street'] = $parts[0];
|
||||
if (isset($parts[1])) {
|
||||
// Match 4-5 digit postal codes (e.g., 1234, 12345) followed by city name
|
||||
if (preg_match('/(\d{4,5})\s*(.+)/', $parts[1], $matches)) {
|
||||
$result['zip'] = $matches[1];
|
||||
$result['city'] = $matches[2];
|
||||
} else {
|
||||
$result['city'] = $parts[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Libraries\Sale_lib;
|
||||
use CodeIgniter\Database\BaseBuilder;
|
||||
use CodeIgniter\Database\ResultInterface;
|
||||
use CodeIgniter\Model;
|
||||
use App\Libraries\Sale_lib;
|
||||
use Config\OSPOS;
|
||||
use ReflectionException;
|
||||
|
||||
@@ -14,11 +14,11 @@ use ReflectionException;
|
||||
*/
|
||||
class Sale extends Model
|
||||
{
|
||||
protected $table = 'sales';
|
||||
protected $primaryKey = 'sale_id';
|
||||
protected $table = 'sales';
|
||||
protected $primaryKey = 'sale_id';
|
||||
protected $useAutoIncrement = true;
|
||||
protected $useSoftDeletes = false;
|
||||
protected $allowedFields = [
|
||||
protected $useSoftDeletes = false;
|
||||
protected $allowedFields = [
|
||||
'sale_time',
|
||||
'customer_id',
|
||||
'employee_id',
|
||||
@@ -28,7 +28,7 @@ class Sale extends Model
|
||||
'invoice_number',
|
||||
'dinner_table_id',
|
||||
'work_order_number',
|
||||
'sale_type'
|
||||
'sale_type',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
@@ -45,16 +45,16 @@ class Sale extends Model
|
||||
$config = config(OSPOS::class)->settings;
|
||||
$this->create_temp_table(['sale_id' => $sale_id]);
|
||||
|
||||
$decimals = totals_decimals();
|
||||
$sales_tax = 'IFNULL(SUM(sales_items_taxes.sales_tax), 0)';
|
||||
$decimals = totals_decimals();
|
||||
$sales_tax = 'IFNULL(SUM(sales_items_taxes.sales_tax), 0)';
|
||||
$cash_adjustment = 'IFNULL(SUM(payments.sale_cash_adjustment), 0)';
|
||||
$sale_price = 'CASE WHEN sales_items.discount_type = ' . PERCENT
|
||||
. " THEN sales_items.quantity_purchased * sales_items.item_unit_price - ROUND(sales_items.quantity_purchased * sales_items.item_unit_price * sales_items.discount / 100, $decimals) "
|
||||
$sale_price = 'CASE WHEN sales_items.discount_type = ' . PERCENT
|
||||
. " THEN sales_items.quantity_purchased * sales_items.item_unit_price - ROUND(sales_items.quantity_purchased * sales_items.item_unit_price * sales_items.discount / 100, {$decimals}) "
|
||||
. 'ELSE sales_items.quantity_purchased * (sales_items.item_unit_price - sales_items.discount) END';
|
||||
|
||||
$sale_total = $config['tax_included']
|
||||
? "ROUND(SUM($sale_price), $decimals) + $cash_adjustment"
|
||||
: "ROUND(SUM($sale_price), $decimals) + $sales_tax + $cash_adjustment";
|
||||
? "ROUND(SUM({$sale_price}), {$decimals}) + {$cash_adjustment}"
|
||||
: "ROUND(SUM({$sale_price}), {$decimals}) + {$sales_tax} + {$cash_adjustment}";
|
||||
|
||||
$sql = 'sales.sale_id AS sale_id,
|
||||
MAX(DATE(sales.sale_time)) AS sale_date,
|
||||
@@ -73,9 +73,9 @@ class Sale extends Model
|
||||
MAX(IFnull(payments.sale_cash_adjustment, 0)) AS cash_adjustment,
|
||||
MAX(IFnull(payments.sale_cash_refund, 0)) AS cash_refund,
|
||||
' . "
|
||||
$sale_total AS amount_due,
|
||||
{$sale_total} AS amount_due,
|
||||
MAX(IFnull(payments.sale_payment_amount, 0)) AS amount_tendered,
|
||||
(MAX(payments.sale_payment_amount)) - ($sale_total) AS change_due,
|
||||
(MAX(payments.sale_payment_amount)) - ({$sale_total}) AS change_due,
|
||||
" . '
|
||||
MAX(payments.payment_type) AS payment_type';
|
||||
|
||||
@@ -89,7 +89,7 @@ class Sale extends Model
|
||||
$builder->join(
|
||||
'sales_items_taxes_temp AS sales_items_taxes',
|
||||
'sales_items.sale_id = sales_items_taxes.sale_id AND sales_items.item_id = sales_items_taxes.item_id AND sales_items.line = sales_items_taxes.line',
|
||||
'LEFT OUTER'
|
||||
'LEFT OUTER',
|
||||
);
|
||||
|
||||
$builder->where('sales.sale_id', $sale_id);
|
||||
@@ -114,15 +114,25 @@ class Sale extends Model
|
||||
public function search(?string $search, array $filters, ?int $rows = 0, ?int $limit_from = 0, ?string $sort = 'sales.sale_time', ?string $order = 'desc', ?bool $count_only = false)
|
||||
{
|
||||
// Set default values
|
||||
if ($rows == null) $rows = 0;
|
||||
if ($limit_from == null) $limit_from = 0;
|
||||
if ($sort == null) $sort = 'sales.sale_time';
|
||||
if ($order == null) $order = 'desc';
|
||||
if ($count_only == null) $count_only = false;
|
||||
if ($rows === null) {
|
||||
$rows = 0;
|
||||
}
|
||||
if ($limit_from === null) {
|
||||
$limit_from = 0;
|
||||
}
|
||||
if ($sort === null) {
|
||||
$sort = 'sales.sale_time';
|
||||
}
|
||||
if ($order === null) {
|
||||
$order = 'desc';
|
||||
}
|
||||
if ($count_only === null) {
|
||||
$count_only = false;
|
||||
}
|
||||
|
||||
$config = config(OSPOS::class)->settings;
|
||||
$config = config(OSPOS::class)->settings;
|
||||
$db_prefix = $this->db->getPrefix();
|
||||
$decimals = totals_decimals();
|
||||
$decimals = totals_decimals();
|
||||
|
||||
// Only non-suspended records
|
||||
$where = 'sales.sale_status = 0 AND ';
|
||||
@@ -133,18 +143,18 @@ class Sale extends Model
|
||||
$this->create_temp_table_sales_payments_data($where);
|
||||
|
||||
$sale_price = 'CASE WHEN `sales_items`.`discount_type` = ' . PERCENT
|
||||
. " THEN `sales_items`.`quantity_purchased` * `sales_items`.`item_unit_price` - ROUND(`sales_items`.`quantity_purchased` * `sales_items`.`item_unit_price` * `sales_items`.`discount` / 100, $decimals) "
|
||||
. " THEN `sales_items`.`quantity_purchased` * `sales_items`.`item_unit_price` - ROUND(`sales_items`.`quantity_purchased` * `sales_items`.`item_unit_price` * `sales_items`.`discount` / 100, {$decimals}) "
|
||||
. 'ELSE `sales_items`.`quantity_purchased` * (`sales_items`.`item_unit_price` - `sales_items`.`discount`) END';
|
||||
|
||||
$sale_cost = 'SUM(`sales_items`.`item_cost_price` * `sales_items`.`quantity_purchased`)';
|
||||
|
||||
$tax = 'IFNULL(SUM(`sales_items_taxes`.`tax`), 0)';
|
||||
$sales_tax = 'IFNULL(SUM(`sales_items_taxes`.`sales_tax`), 0)';
|
||||
$internal_tax = 'IFNULL(SUM(`sales_items_taxes`.`internal_tax`), 0)';
|
||||
$tax = 'IFNULL(SUM(`sales_items_taxes`.`tax`), 0)';
|
||||
$sales_tax = 'IFNULL(SUM(`sales_items_taxes`.`sales_tax`), 0)';
|
||||
$internal_tax = 'IFNULL(SUM(`sales_items_taxes`.`internal_tax`), 0)';
|
||||
$cash_adjustment = 'IFNULL(SUM(`payments`.`sale_cash_adjustment`), 0)';
|
||||
|
||||
$sale_subtotal = "ROUND(SUM($sale_price), $decimals) - $internal_tax";
|
||||
$sale_total = "ROUND(SUM($sale_price), $decimals) + $sales_tax + $cash_adjustment";
|
||||
$sale_subtotal = "ROUND(SUM({$sale_price}), {$decimals}) - {$internal_tax}";
|
||||
$sale_total = "ROUND(SUM({$sale_price}), {$decimals}) + {$sales_tax} + {$cash_adjustment}";
|
||||
|
||||
$this->create_temp_table_sales_items_taxes_data($where);
|
||||
|
||||
@@ -171,7 +181,7 @@ class Sale extends Model
|
||||
$sale_total . ' AS amount_due',
|
||||
'MAX(`payments`.`sale_payment_amount`) AS amount_tendered',
|
||||
'(MAX(`payments`.`sale_payment_amount`)) - (' . $sale_total . ') AS change_due',
|
||||
'MAX(`payments`.`payment_type`) AS payment_type'
|
||||
'MAX(`payments`.`payment_type`) AS payment_type',
|
||||
], false);
|
||||
}
|
||||
|
||||
@@ -182,7 +192,7 @@ class Sale extends Model
|
||||
$builder->join(
|
||||
'sales_items_taxes_temp AS sales_items_taxes',
|
||||
'sales_items.sale_id = sales_items_taxes.sale_id AND sales_items.item_id = sales_items_taxes.item_id AND sales_items.line = sales_items_taxes.line',
|
||||
'LEFT OUTER'
|
||||
'LEFT OUTER',
|
||||
);
|
||||
|
||||
$builder->where($where);
|
||||
@@ -227,7 +237,7 @@ class Sale extends Model
|
||||
$builder->where('sales.sale_time BETWEEN ' . $this->db->escape(rawurldecode($filters['start_date'])) . ' AND ' . $this->db->escape(rawurldecode($filters['end_date'])));
|
||||
}
|
||||
|
||||
if (!empty($search)) { // TODO: duplicated code. We should think about refactoring out a method.
|
||||
if (! empty($search)) { // TODO: duplicated code. We should think about refactoring out a method.
|
||||
if ($filters['is_valid_receipt']) {
|
||||
$pieces = explode(' ', $search);
|
||||
$builder->where('sales.sale_id', $pieces[1]);
|
||||
@@ -242,13 +252,13 @@ class Sale extends Model
|
||||
}
|
||||
|
||||
// TODO: This needs to be converted to a switch statement
|
||||
if ($filters['sale_type'] == 'sales') { // TODO: we need to think about refactoring this block to a switch statement.
|
||||
if ($filters['sale_type'] === 'sales') { // TODO: we need to think about refactoring this block to a switch statement.
|
||||
$builder->where('sales.sale_status = ' . COMPLETED . ' AND payment_amount > 0');
|
||||
} elseif ($filters['sale_type'] == 'quotes') {
|
||||
} elseif ($filters['sale_type'] === 'quotes') {
|
||||
$builder->where('sales.sale_status = ' . SUSPENDED . ' AND sales.quote_number IS NOT NULL');
|
||||
} elseif ($filters['sale_type'] == 'returns') {
|
||||
} elseif ($filters['sale_type'] === 'returns') {
|
||||
$builder->where('sales.sale_status = ' . COMPLETED . ' AND payment_amount < 0');
|
||||
} elseif ($filters['sale_type'] == 'all') {
|
||||
} elseif ($filters['sale_type'] === 'all') {
|
||||
$builder->where('sales.sale_status = ' . COMPLETED);
|
||||
}
|
||||
|
||||
@@ -290,12 +300,12 @@ class Sale extends Model
|
||||
$payments = $builder->get()->getResultArray();
|
||||
|
||||
// Consider Gift Card as only one type of payment and do not show "Gift Card: 1, Gift Card: 2, etc." in the total
|
||||
$gift_card_count = 0;
|
||||
$gift_card_count = 0;
|
||||
$gift_card_amount = 0;
|
||||
|
||||
foreach ($payments as $key => $payment) {
|
||||
if (strstr($payment['payment_type'], lang('Sales.giftcard'))) {
|
||||
$gift_card_count += $payment['count'];
|
||||
$gift_card_count += $payment['count'];
|
||||
$gift_card_amount += $payment['payment_amount'];
|
||||
|
||||
// Remove the "Gift Card: 1", "Gift Card: 2", etc. payment string
|
||||
@@ -327,7 +337,7 @@ class Sale extends Model
|
||||
{
|
||||
$suggestions = [];
|
||||
|
||||
if (!$this->is_valid_receipt($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');
|
||||
@@ -369,21 +379,11 @@ class Sale extends Model
|
||||
return $builder->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $year
|
||||
* @param int $start_from
|
||||
* @return int
|
||||
*/
|
||||
public function get_invoice_number_for_year(string $year = '', int $start_from = 0): int
|
||||
{
|
||||
return $this->get_number_for_year('invoice_number', $year, $start_from);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $year
|
||||
* @param int $start_from
|
||||
* @return int
|
||||
*/
|
||||
public function get_quote_number_for_year(string $year = '', int $start_from = 0): int
|
||||
{
|
||||
return $this->get_number_for_year('quote_number', $year, $start_from);
|
||||
@@ -394,31 +394,32 @@ class Sale extends Model
|
||||
*/
|
||||
private function get_number_for_year(string $field, string $year = '', int $start_from = 0): int
|
||||
{
|
||||
$year = $year == '' ? date('Y') : $year;
|
||||
$year = $year === '' ? date('Y') : $year;
|
||||
|
||||
$builder = $this->db->table('sales');
|
||||
$builder->select('COUNT( 1 ) AS number_year');
|
||||
$builder->where('DATE_FORMAT(sale_time, "%Y" ) = ', $year);
|
||||
$builder->where("$field IS NOT NULL");
|
||||
$builder->where("{$field} IS NOT NULL");
|
||||
$result = $builder->get()->getRowArray();
|
||||
|
||||
return ($start_from + $result['number_year']);
|
||||
return $start_from + $result['number_year'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if valid receipt
|
||||
*/
|
||||
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.
|
||||
public function is_valid_receipt(?string &$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($receipt_sale_id)) {
|
||||
if (! empty($receipt_sale_id)) {
|
||||
// POS #
|
||||
$pieces = explode(' ', $receipt_sale_id);
|
||||
|
||||
if (count($pieces) == 2 && preg_match('/(POS)/i', $pieces[0])) {
|
||||
if (count($pieces) === 2 && preg_match('/(POS)/i', $pieces[0])) {
|
||||
return $this->exists($pieces[1]);
|
||||
} elseif ($config['invoice_enable']) {
|
||||
}
|
||||
if ($config['invoice_enable']) {
|
||||
$sale_info = $this->get_sale_by_invoice_number($receipt_sale_id);
|
||||
|
||||
if ($sale_info->getNumRows() > 0) {
|
||||
@@ -440,11 +441,14 @@ class Sale extends Model
|
||||
$builder = $this->db->table('sales');
|
||||
$builder->where('sale_id', $sale_id);
|
||||
|
||||
return ($builder->get()->getNumRows() == 1); // TODO: ===
|
||||
return $builder->get()->getNumRows() === 1; // TODO: ===
|
||||
}
|
||||
|
||||
/**
|
||||
* Update sale
|
||||
*
|
||||
* @param mixed|null $sale_id
|
||||
* @param mixed|null $sale_data
|
||||
*/
|
||||
public function update($sale_id = null, $sale_data = null): bool
|
||||
{
|
||||
@@ -455,7 +459,7 @@ class Sale extends Model
|
||||
$success = $builder->update($update_data);
|
||||
|
||||
// Touch payment only if update sale is successful and there is a payments object otherwise the result would be to delete all the payments associated to the sale
|
||||
if ($success && !empty($sale_data['payments'])) {
|
||||
if ($success && ! empty($sale_data['payments'])) {
|
||||
// Run these queries as a transaction, we want to make sure we do all or nothing
|
||||
$this->db->transStart();
|
||||
|
||||
@@ -463,14 +467,14 @@ class Sale extends Model
|
||||
|
||||
// Add new payments
|
||||
foreach ($sale_data['payments'] as $payment) {
|
||||
$payment_id = $payment['payment_id'];
|
||||
$payment_type = $payment['payment_type'];
|
||||
$payment_amount = $payment['payment_amount'];
|
||||
$cash_refund = $payment['cash_refund'];
|
||||
$payment_id = $payment['payment_id'];
|
||||
$payment_type = $payment['payment_type'];
|
||||
$payment_amount = $payment['payment_amount'];
|
||||
$cash_refund = $payment['cash_refund'];
|
||||
$cash_adjustment = $payment['cash_adjustment'];
|
||||
$employee_id = $payment['employee_id'];
|
||||
$employee_id = $payment['employee_id'];
|
||||
|
||||
if ($payment_id == NEW_ENTRY && $payment_amount != 0) {
|
||||
if ($payment_id === NEW_ENTRY && $payment_amount !== 0) {
|
||||
// Add a new payment transaction
|
||||
$sales_payments_data = [
|
||||
'sale_id' => $sale_id,
|
||||
@@ -478,17 +482,17 @@ class Sale extends Model
|
||||
'payment_amount' => $payment_amount,
|
||||
'cash_refund' => $cash_refund,
|
||||
'cash_adjustment' => $cash_adjustment,
|
||||
'employee_id' => $employee_id
|
||||
'employee_id' => $employee_id,
|
||||
];
|
||||
$success = $builder->insert($sales_payments_data);
|
||||
} elseif ($payment_id != NEW_ENTRY) {
|
||||
if ($payment_amount != 0) {
|
||||
} elseif ($payment_id !== NEW_ENTRY) {
|
||||
if ($payment_amount !== 0) {
|
||||
// Update existing payment transactions (payment_type only)
|
||||
$sales_payments_data = [
|
||||
'payment_type' => $payment_type,
|
||||
'payment_amount' => $payment_amount,
|
||||
'cash_refund' => $cash_refund,
|
||||
'cash_adjustment' => $cash_adjustment
|
||||
'cash_adjustment' => $cash_adjustment,
|
||||
];
|
||||
|
||||
$builder->where('payment_id', $payment_id);
|
||||
@@ -510,6 +514,7 @@ class Sale extends Model
|
||||
/**
|
||||
* Save the sale information after the sales is complete but before the final document is printed
|
||||
* The sales_taxes variable needs to be initialized to an empty array before calling
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function save_value(
|
||||
@@ -525,22 +530,22 @@ class Sale extends Model
|
||||
int $sale_type,
|
||||
?array $payments,
|
||||
?int $dinner_table_id,
|
||||
?array &$sales_taxes
|
||||
?array &$sales_taxes,
|
||||
): int { // TODO: this method returns the sale_id but the override is expecting it to return a bool. The signature needs to be reworked. Generally when there are more than 3 maybe 4 parameters, there's a good chance that an object needs to be passed rather than so many params.
|
||||
$config = config(OSPOS::class)->settings;
|
||||
$config = config(OSPOS::class)->settings;
|
||||
$attribute = model(Attribute::class);
|
||||
$customer = model(Customer::class);
|
||||
$giftcard = model(Giftcard::class);
|
||||
$customer = model(Customer::class);
|
||||
$giftcard = model(Giftcard::class);
|
||||
$inventory = model('Inventory');
|
||||
$item = model(Item::class);
|
||||
$item = model(Item::class);
|
||||
|
||||
$item_quantity = model(Item_quantity::class);
|
||||
|
||||
if ($sale_id != NEW_ENTRY) {
|
||||
if ($sale_id !== NEW_ENTRY) {
|
||||
$this->clear_suspended_sale_detail($sale_id);
|
||||
}
|
||||
|
||||
if (count($items) == 0) { // TODO: ===
|
||||
if (count($items) === 0) { // TODO: ===
|
||||
return -1; // TODO: Replace -1 with a constant
|
||||
}
|
||||
|
||||
@@ -554,13 +559,13 @@ class Sale extends Model
|
||||
'quote_number' => $quote_number,
|
||||
'work_order_number' => $work_order_number,
|
||||
'dinner_table_id' => $dinner_table_id,
|
||||
'sale_type' => $sale_type
|
||||
'sale_type' => $sale_type,
|
||||
];
|
||||
|
||||
// Run these queries as a transaction, we want to make sure we do all or nothing
|
||||
$this->db->transStart();
|
||||
|
||||
if ($sale_id == NEW_ENTRY) {
|
||||
if ($sale_id === NEW_ENTRY) {
|
||||
$builder = $this->db->table('sales');
|
||||
$builder->insert($sales_data);
|
||||
$sale_id = $this->db->insertID();
|
||||
@@ -570,19 +575,19 @@ class Sale extends Model
|
||||
$builder->update($sales_data);
|
||||
}
|
||||
|
||||
$total_amount = 0;
|
||||
$total_amount = 0;
|
||||
$total_amount_used = 0;
|
||||
|
||||
foreach ($payments as $payment_id => $payment) {
|
||||
if (!empty(strstr($payment['payment_type'], lang('Sales.giftcard')))) {
|
||||
if (! empty(strstr($payment['payment_type'], lang('Sales.giftcard')))) {
|
||||
// We have a gift card, and we have to deduct the used value from the total value of the card.
|
||||
$splitpayment = explode(':', $payment['payment_type']); // TODO: this variable doesn't follow our naming conventions. Probably should be refactored to split_payment.
|
||||
$splitpayment = explode(':', $payment['payment_type']); // TODO: this variable doesn't follow our naming conventions. Probably should be refactored to split_payment.
|
||||
$cur_giftcard_value = $giftcard->get_giftcard_value($splitpayment[1]); // TODO: this should be refactored to $current_giftcard_value
|
||||
$giftcard->update_giftcard_value($splitpayment[1], $cur_giftcard_value - $payment['payment_amount']);
|
||||
} elseif (!empty(strstr($payment['payment_type'], lang('Sales.rewards')))) {
|
||||
} elseif (! empty(strstr($payment['payment_type'], lang('Sales.rewards')))) {
|
||||
$cur_rewards_value = $customer->get_info($customer_id)->points;
|
||||
$customer->update_reward_points_value($customer_id, $cur_rewards_value - $payment['payment_amount']);
|
||||
$total_amount_used = floatval($total_amount_used) + floatval($payment['payment_amount']);
|
||||
$total_amount_used = (float) $total_amount_used + (float) ($payment['payment_amount']);
|
||||
}
|
||||
|
||||
$sales_payments_data = [
|
||||
@@ -591,13 +596,13 @@ class Sale extends Model
|
||||
'payment_amount' => $payment['payment_amount'],
|
||||
'cash_refund' => $payment['cash_refund'],
|
||||
'cash_adjustment' => $payment['cash_adjustment'],
|
||||
'employee_id' => $employee_id
|
||||
'employee_id' => $employee_id,
|
||||
];
|
||||
|
||||
$builder = $this->db->table('sales_payments');
|
||||
$builder->insert($sales_payments_data);
|
||||
|
||||
$total_amount = floatval($total_amount) + floatval($payment['payment_amount']) - floatval($payment['cash_refund']);
|
||||
$total_amount = (float) $total_amount + (float) ($payment['payment_amount']) - (float) ($payment['cash_refund']);
|
||||
}
|
||||
|
||||
$this->save_customer_rewards($customer_id, $sale_id, $total_amount, $total_amount_used);
|
||||
@@ -607,7 +612,7 @@ class Sale extends Model
|
||||
foreach ($items as $line => $item_data) {
|
||||
$cur_item_info = $item->get_info($item_data['item_id']);
|
||||
|
||||
if ($item_data['price'] == 0.00) {
|
||||
if ($item_data['price'] === 0.00) {
|
||||
$item_data['discount'] = 0.00;
|
||||
}
|
||||
|
||||
@@ -623,13 +628,13 @@ class Sale extends Model
|
||||
'item_cost_price' => $item_data['cost_price'],
|
||||
'item_unit_price' => $item_data['price'],
|
||||
'item_location' => $item_data['item_location'],
|
||||
'print_option' => $item_data['print_option']
|
||||
'print_option' => $item_data['print_option'],
|
||||
];
|
||||
|
||||
$builder = $this->db->table('sales_items');
|
||||
$builder->insert($sales_items_data);
|
||||
|
||||
if ($cur_item_info->stock_type == HAS_STOCK && $sale_status == COMPLETED) { // TODO: === ?
|
||||
if ($cur_item_info->stock_type === HAS_STOCK && $sale_status === COMPLETED) { // TODO: === ?
|
||||
// Update stock quantity if item type is a standard stock item and the sale is a standard sale
|
||||
$item_quantity_data = $item_quantity->get_item_quantity($item_data['item_id'], $item_data['item_location']);
|
||||
|
||||
@@ -637,10 +642,10 @@ class Sale extends Model
|
||||
[
|
||||
'quantity' => $item_quantity_data->quantity - $item_data['quantity'],
|
||||
'item_id' => $item_data['item_id'],
|
||||
'location_id' => $item_data['item_location']
|
||||
'location_id' => $item_data['item_location'],
|
||||
],
|
||||
$item_data['item_id'],
|
||||
$item_data['item_location']
|
||||
$item_data['item_location'],
|
||||
);
|
||||
|
||||
// If an items was deleted but later returned it's restored with this rule
|
||||
@@ -650,13 +655,13 @@ class Sale extends Model
|
||||
|
||||
// Inventory Count Details
|
||||
$sale_remarks = 'POS ' . $sale_id; // TODO: Use string interpolation here.
|
||||
$inv_data = [
|
||||
$inv_data = [
|
||||
'trans_date' => date('Y-m-d H:i:s'),
|
||||
'trans_items' => $item_data['item_id'],
|
||||
'trans_user' => $employee_id,
|
||||
'trans_location' => $item_data['item_location'],
|
||||
'trans_comment' => $sale_remarks,
|
||||
'trans_inventory' => -$item_data['quantity']
|
||||
'trans_inventory' => -$item_data['quantity'],
|
||||
];
|
||||
|
||||
$inventory->insert($inv_data, false);
|
||||
@@ -665,14 +670,14 @@ class Sale extends Model
|
||||
$attribute->copy_attribute_links($item_data['item_id'], 'sale_id', $sale_id);
|
||||
}
|
||||
|
||||
if ($customer_id == NEW_ENTRY || $customer->taxable) {
|
||||
if ($customer_id === NEW_ENTRY || $customer->taxable) {
|
||||
$this->save_sales_tax($sale_id, $sales_taxes[0]);
|
||||
$this->save_sales_items_taxes($sale_id, $sales_taxes[1]);
|
||||
}
|
||||
|
||||
if ($config['dinner_table_enable']) {
|
||||
$dinner_table = model(Dinner_table::class);
|
||||
if ($sale_status == COMPLETED) { // TODO: === ?
|
||||
if ($sale_status === COMPLETED) { // TODO: === ?
|
||||
$dinner_table->release($dinner_table_id);
|
||||
} else {
|
||||
$dinner_table->occupy($dinner_table_id);
|
||||
@@ -721,7 +726,7 @@ class Sale extends Model
|
||||
'item_tax_amount' => $tax_item['item_tax_amount'],
|
||||
'sales_tax_code_id' => $tax_item['sales_tax_code_id'],
|
||||
'tax_category_id' => $tax_item['tax_category_id'],
|
||||
'jurisdiction_id' => $tax_item['jurisdiction_id']
|
||||
'jurisdiction_id' => $tax_item['jurisdiction_id'],
|
||||
];
|
||||
|
||||
$builder->insert($sales_items_taxes);
|
||||
@@ -756,8 +761,36 @@ class Sale extends Model
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all item taxes for a sale (for UBL invoice generation)
|
||||
* Returns array keyed by item_id, each containing array of tax info
|
||||
*/
|
||||
public function get_sale_item_taxes_by_sale(int $sale_id): array
|
||||
{
|
||||
$builder = $this->db->table('sales_items_taxes');
|
||||
$builder->select('item_id, line, name, percent, tax_type, item_tax_amount');
|
||||
$builder->where('sale_id', $sale_id);
|
||||
$builder->orderBy('line', 'asc');
|
||||
|
||||
$results = $builder->get()->getResultArray();
|
||||
|
||||
// Group by item_id
|
||||
$itemTaxes = [];
|
||||
|
||||
foreach ($results as $row) {
|
||||
$itemId = $row['item_id'];
|
||||
if (! isset($itemTaxes[$itemId])) {
|
||||
$itemTaxes[$itemId] = [];
|
||||
}
|
||||
$itemTaxes[$itemId][] = $row;
|
||||
}
|
||||
|
||||
return $itemTaxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes list of sales
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function delete_list(array $sale_ids, int $employee_id, bool $update_inventory = true): bool
|
||||
@@ -787,6 +820,10 @@ class Sale extends Model
|
||||
* Delete sale. Hard deletes are not supported for sales transactions.
|
||||
* When a sale is "deleted" it is simply changed to a status of canceled.
|
||||
* However, if applicable the inventory still needs to be updated
|
||||
*
|
||||
* @param mixed|null $sale_id
|
||||
* @param mixed|null $employee_id
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function delete($sale_id = null, bool $purge = false, bool $update_inventory = true, $employee_id = null): bool
|
||||
@@ -796,11 +833,11 @@ class Sale extends Model
|
||||
|
||||
$sale_status = $this->get_sale_status($sale_id);
|
||||
|
||||
if ($update_inventory && $sale_status == COMPLETED) {
|
||||
if ($update_inventory && $sale_status === COMPLETED) {
|
||||
// Defect, not all item deletions will be undone?
|
||||
// Get array with all the items involved in the sale to update the inventory tracking
|
||||
$inventory = model('Inventory');
|
||||
$item = model(Item::class);
|
||||
$inventory = model('Inventory');
|
||||
$item = model(Item::class);
|
||||
$item_quantity = model(Item_quantity::class);
|
||||
|
||||
$items = $this->get_sale_items($sale_id)->getResultArray();
|
||||
@@ -808,7 +845,7 @@ class Sale extends Model
|
||||
foreach ($items as $item_data) {
|
||||
$cur_item_info = $item->get_info($item_data['item_id']);
|
||||
|
||||
if ($cur_item_info->stock_type == HAS_STOCK) {
|
||||
if ($cur_item_info->stock_type === HAS_STOCK) {
|
||||
// Create query to update inventory tracking
|
||||
$inv_data = [
|
||||
'trans_date' => date('Y-m-d H:i:s'),
|
||||
@@ -816,7 +853,7 @@ class Sale extends Model
|
||||
'trans_user' => $employee_id,
|
||||
'trans_comment' => 'Deleting sale ' . $sale_id,
|
||||
'trans_location' => $item_data['item_location'],
|
||||
'trans_inventory' => $item_data['quantity_purchased']
|
||||
'trans_inventory' => $item_data['quantity_purchased'],
|
||||
];
|
||||
// Update inventory
|
||||
$inventory->insert($inv_data, false);
|
||||
@@ -852,7 +889,7 @@ class Sale extends Model
|
||||
public function get_sale_items_ordered(int $sale_id): ResultInterface
|
||||
{
|
||||
$config = config(OSPOS::class)->settings;
|
||||
$item = model(Item::class);
|
||||
$item = model(Item::class);
|
||||
|
||||
$builder = $this->db->table('sales_items AS sales_items');
|
||||
$builder->select('
|
||||
@@ -876,18 +913,18 @@ class Sale extends Model
|
||||
$builder->where('sales_items.sale_id', $sale_id);
|
||||
|
||||
// Entry sequence (this will render kits in the expected sequence)
|
||||
if ($config['line_sequence'] == '0') { // TODO: Replace these with constants and this should be converted to a switch.
|
||||
if ($config['line_sequence'] === '0') { // TODO: Replace these with constants and this should be converted to a switch.
|
||||
$builder->orderBy('line', 'asc');
|
||||
}
|
||||
// Group by Stock Type (nonstock first - type 1, stock next - type 0)
|
||||
elseif ($config['line_sequence'] == '1') {
|
||||
elseif ($config['line_sequence'] === '1') {
|
||||
$builder->orderBy('stock_type', 'desc');
|
||||
$builder->orderBy('sales_items.description', 'asc');
|
||||
$builder->orderBy('items.name', 'asc');
|
||||
$builder->orderBy('items.qty_per_pack', 'asc');
|
||||
}
|
||||
// Group by Item Category
|
||||
elseif ($config['line_sequence'] == '2') {
|
||||
elseif ($config['line_sequence'] === '2') {
|
||||
$builder->orderBy('category', 'asc');
|
||||
$builder->orderBy('sales_items.description', 'asc');
|
||||
$builder->orderBy('items.name', 'asc');
|
||||
@@ -927,8 +964,8 @@ class Sale extends Model
|
||||
$payments[lang('Sales.rewards')] = lang('Sales.rewards');
|
||||
}
|
||||
$sale_lib = new Sale_lib();
|
||||
if ($sale_lib->get_mode() == 'sale_work_order') {
|
||||
$payments[lang('Sales.cash_deposit')] = lang('Sales.cash_deposit');
|
||||
if ($sale_lib->get_mode() === 'sale_work_order') {
|
||||
$payments[lang('Sales.cash_deposit')] = lang('Sales.cash_deposit');
|
||||
$payments[lang('Sales.credit_deposit')] = lang('Sales.credit_deposit');
|
||||
}
|
||||
|
||||
@@ -969,11 +1006,11 @@ class Sale extends Model
|
||||
$builder = $this->db->table('sales');
|
||||
$builder->where('quote_number', $quote_number);
|
||||
|
||||
if (!empty($sale_id)) {
|
||||
if (! empty($sale_id)) {
|
||||
$builder->where('sale_id !=', $sale_id);
|
||||
}
|
||||
|
||||
return ($builder->get()->getNumRows() == 1); // TODO: ===
|
||||
return $builder->get()->getNumRows() === 1; // TODO: ===
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -984,11 +1021,11 @@ class Sale extends Model
|
||||
$builder = $this->db->table('sales');
|
||||
$builder->where('invoice_number', $invoice_number);
|
||||
|
||||
if (!empty($sale_id)) {
|
||||
if (! empty($sale_id)) {
|
||||
$builder->where('sale_id !=', $sale_id);
|
||||
}
|
||||
|
||||
return ($builder->get()->getNumRows() == 1); // TODO: ===
|
||||
return $builder->get()->getNumRows() === 1; // TODO: ===
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -998,11 +1035,11 @@ class Sale extends Model
|
||||
{
|
||||
$builder = $this->db->table('sales');
|
||||
$builder->where('invoice_number', $work_order_number);
|
||||
if (!empty($sale_id)) {
|
||||
if (! empty($sale_id)) {
|
||||
$builder->where('sale_id !=', $sale_id);
|
||||
}
|
||||
|
||||
return ($builder->get()->getNumRows() == 1); // TODO: ===
|
||||
return $builder->get()->getNumRows() === 1; // TODO: ===
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1012,7 +1049,7 @@ class Sale extends Model
|
||||
{
|
||||
$giftcard = model(Giftcard::class);
|
||||
|
||||
if (!$giftcard->exists($giftcard->get_giftcard_id($giftcardNumber))) { // TODO: camelCase is used here for the variable name but we are using _ everywhere else. CI4 moved to camelCase... we should pick one and do that.
|
||||
if (! $giftcard->exists($giftcard->get_giftcard_id($giftcardNumber))) { // TODO: camelCase is used here for the variable name but we are using _ everywhere else. CI4 moved to camelCase... we should pick one and do that.
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1043,22 +1080,22 @@ class Sale extends Model
|
||||
$decimals = totals_decimals();
|
||||
|
||||
$sale_price = 'CASE WHEN sales_items.discount_type = ' . PERCENT
|
||||
. " THEN sales_items.quantity_purchased * sales_items.item_unit_price - ROUND(sales_items.quantity_purchased * sales_items.item_unit_price * sales_items.discount / 100, $decimals) "
|
||||
. " THEN sales_items.quantity_purchased * sales_items.item_unit_price - ROUND(sales_items.quantity_purchased * sales_items.item_unit_price * sales_items.discount / 100, {$decimals}) "
|
||||
. 'ELSE sales_items.quantity_purchased * (sales_items.item_unit_price - sales_items.discount) END';
|
||||
|
||||
$sale_cost = 'SUM(sales_items.item_cost_price * sales_items.quantity_purchased)';
|
||||
|
||||
$tax = 'IFNULL(SUM(sales_items_taxes.tax), 0)';
|
||||
$sales_tax = 'IFNULL(SUM(sales_items_taxes.sales_tax), 0)';
|
||||
$internal_tax = 'IFNULL(SUM(sales_items_taxes.internal_tax), 0)';
|
||||
$tax = 'IFNULL(SUM(sales_items_taxes.tax), 0)';
|
||||
$sales_tax = 'IFNULL(SUM(sales_items_taxes.sales_tax), 0)';
|
||||
$internal_tax = 'IFNULL(SUM(sales_items_taxes.internal_tax), 0)';
|
||||
$cash_adjustment = 'IFNULL(SUM(payments.sale_cash_adjustment), 0)';
|
||||
|
||||
if ($config['tax_included']) {
|
||||
$sale_total = "ROUND(SUM($sale_price), $decimals) + $cash_adjustment";
|
||||
$sale_subtotal = "$sale_total - $internal_tax";
|
||||
$sale_total = "ROUND(SUM({$sale_price}), {$decimals}) + {$cash_adjustment}";
|
||||
$sale_subtotal = "{$sale_total} - {$internal_tax}";
|
||||
} else {
|
||||
$sale_subtotal = "ROUND(SUM($sale_price), $decimals) - $internal_tax + $cash_adjustment";
|
||||
$sale_total = "ROUND(SUM($sale_price), $decimals) + $sales_tax + $cash_adjustment";
|
||||
$sale_subtotal = "ROUND(SUM({$sale_price}), {$decimals}) - {$internal_tax} + {$cash_adjustment}";
|
||||
$sale_total = "ROUND(SUM({$sale_price}), {$decimals}) + {$sales_tax} + {$cash_adjustment}";
|
||||
}
|
||||
|
||||
// Create a temporary table to contain all the sum of taxes per sale item
|
||||
@@ -1100,7 +1137,7 @@ class Sale extends Model
|
||||
|
||||
$this->db->query($sql);
|
||||
$item = model(Item::class);
|
||||
$sql = 'CREATE TEMPORARY TABLE IF NOT EXISTS ' . $this->db->prefixTable('sales_items_temp') .
|
||||
$sql = 'CREATE TEMPORARY TABLE IF NOT EXISTS ' . $this->db->prefixTable('sales_items_temp') .
|
||||
' (INDEX(sale_date), INDEX(sale_time), INDEX(sale_id))
|
||||
(
|
||||
SELECT
|
||||
@@ -1122,7 +1159,7 @@ class Sale extends Model
|
||||
MAX(sales.employee_id) AS employee_id,
|
||||
MAX(CONCAT(employee.first_name, " ", employee.last_name)) AS employee_name,
|
||||
items.item_id AS item_id,
|
||||
MAX(' . $item->get_item_name() . ') AS name,
|
||||
MAX(' . $item->get_item_name() . ") AS name,
|
||||
MAX(items.item_number) AS item_number,
|
||||
MAX(items.category) AS category,
|
||||
MAX(items.supplier_id) AS supplier_id,
|
||||
@@ -1137,12 +1174,12 @@ class Sale extends Model
|
||||
MAX(sales_items.description) AS description,
|
||||
MAX(payments.payment_type) AS payment_type,
|
||||
MAX(payments.sale_payment_amount) AS sale_payment_amount,
|
||||
' . "
|
||||
$sale_subtotal AS subtotal,
|
||||
$tax AS tax,
|
||||
$sale_total AS total,
|
||||
$sale_cost AS cost,
|
||||
($sale_subtotal - $sale_cost) AS profit
|
||||
|
||||
{$sale_subtotal} AS subtotal,
|
||||
{$tax} AS tax,
|
||||
{$sale_total} AS total,
|
||||
{$sale_cost} AS cost,
|
||||
({$sale_subtotal} - {$sale_cost}) AS profit
|
||||
" . '
|
||||
FROM ' . $this->db->prefixTable('sales_items') . ' AS sales_items
|
||||
INNER JOIN ' . $this->db->prefixTable('sales') . ' AS sales
|
||||
@@ -1173,7 +1210,7 @@ class Sale extends Model
|
||||
*/
|
||||
public function get_all_suspended(?int $customer_id = null): array
|
||||
{
|
||||
if ($customer_id == NEW_ENTRY) {
|
||||
if ($customer_id === NEW_ENTRY) {
|
||||
$query = $this->db->query("SELECT sale_id, case when sale_type = '" . SALE_TYPE_QUOTE . "' THEN quote_number WHEN sale_type = '" . SALE_TYPE_WORK_ORDER . "' THEN work_order_number else sale_id end as doc_id, sale_id as suspended_sale_id, sale_status, sale_time, dinner_table_id, customer_id, employee_id, comment FROM "
|
||||
. $this->db->prefixTable('sales') . ' where sale_status = ' . SUSPENDED);
|
||||
} else {
|
||||
@@ -1189,7 +1226,7 @@ class Sale extends Model
|
||||
*/
|
||||
public function get_dinner_table(int $sale_id) // TODO: this is returning null or the table_id. We can keep it this way but multiple return types can't be declared until PHP 8.x
|
||||
{
|
||||
if ($sale_id == NEW_ENTRY) {
|
||||
if ($sale_id === NEW_ENTRY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1221,11 +1258,6 @@ class Sale extends Model
|
||||
return $builder->get()->getRow()->sale_status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $sale_id
|
||||
* @param int $sale_status
|
||||
* @return void
|
||||
*/
|
||||
public function update_sale_status(int $sale_id, int $sale_status): void
|
||||
{
|
||||
$builder = $this->db->table('sales');
|
||||
@@ -1244,7 +1276,7 @@ class Sale extends Model
|
||||
|
||||
$row = $builder->get()->getRow();
|
||||
|
||||
if ($row != null) {
|
||||
if ($row !== null) {
|
||||
return $row->quote_number;
|
||||
}
|
||||
|
||||
@@ -1261,7 +1293,7 @@ class Sale extends Model
|
||||
|
||||
$row = $builder->get()->getRow();
|
||||
|
||||
if ($row != null) { // TODO: === ?
|
||||
if ($row !== null) { // TODO: === ?
|
||||
return $row->work_order_number;
|
||||
}
|
||||
|
||||
@@ -1278,7 +1310,7 @@ class Sale extends Model
|
||||
|
||||
$row = $builder->get()->getRow();
|
||||
|
||||
if ($row != null) { // TODO: === ?
|
||||
if ($row !== null) { // TODO: === ?
|
||||
return $row->comment;
|
||||
}
|
||||
|
||||
@@ -1308,7 +1340,7 @@ class Sale extends Model
|
||||
$config = config(OSPOS::class)->settings;
|
||||
|
||||
if ($config['dinner_table_enable']) {
|
||||
$dinner_table = model(Dinner_table::class);
|
||||
$dinner_table = model(Dinner_table::class);
|
||||
$dinner_table_id = $this->get_dinner_table($sale_id);
|
||||
$dinner_table->release($dinner_table_id);
|
||||
}
|
||||
@@ -1330,7 +1362,7 @@ class Sale extends Model
|
||||
$config = config(OSPOS::class)->settings;
|
||||
|
||||
if ($config['dinner_table_enable']) {
|
||||
$dinner_table = model(Dinner_table::class);
|
||||
$dinner_table = model(Dinner_table::class);
|
||||
$dinner_table_id = $this->get_dinner_table($sale_id);
|
||||
$dinner_table->release($dinner_table_id);
|
||||
}
|
||||
@@ -1365,30 +1397,24 @@ class Sale extends Model
|
||||
return $builder->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $customer_id
|
||||
* @param int $sale_id
|
||||
* @param float $total_amount
|
||||
* @param float $total_amount_used
|
||||
*/
|
||||
private function save_customer_rewards(int $customer_id, int $sale_id, float $total_amount, float $total_amount_used): void
|
||||
{
|
||||
$config = config(OSPOS::class)->settings;
|
||||
|
||||
if (!empty($customer_id) && $config['customer_reward_enable']) {
|
||||
$customer = model(Customer::class);
|
||||
if (! empty($customer_id) && $config['customer_reward_enable']) {
|
||||
$customer = model(Customer::class);
|
||||
$customer_rewards = model(Customer_rewards::class);
|
||||
$rewards = model(Rewards::class);
|
||||
$rewards = model(Rewards::class);
|
||||
|
||||
$package_id = $customer->get_info($customer_id)->package_id;
|
||||
|
||||
if (!empty($package_id)) {
|
||||
$points_percent = $customer_rewards->get_points_percent($package_id);
|
||||
$points = $customer->get_info($customer_id)->points;
|
||||
$points = ($points == null ? 0 : $points);
|
||||
$points_percent = ($points_percent == null ? 0 : $points_percent);
|
||||
if (! empty($package_id)) {
|
||||
$points_percent = $customer_rewards->get_points_percent($package_id);
|
||||
$points = $customer->get_info($customer_id)->points;
|
||||
$points = ($points === null ? 0 : $points);
|
||||
$points_percent = ($points_percent === null ? 0 : $points_percent);
|
||||
$total_amount_earned = ($total_amount * $points_percent / 100);
|
||||
$points = $points + $total_amount_earned;
|
||||
$points += $total_amount_earned;
|
||||
|
||||
$customer->update_reward_points_value($customer_id, $points);
|
||||
|
||||
@@ -1402,7 +1428,6 @@ class Sale extends Model
|
||||
/**
|
||||
* Creates a temporary table to store the sales_payments data
|
||||
*
|
||||
* @param string $where
|
||||
* @return array
|
||||
*/
|
||||
private function create_temp_table_sales_payments_data(string $where): void
|
||||
@@ -1412,7 +1437,7 @@ class Sale extends Model
|
||||
'payments.sale_id',
|
||||
'SUM(CASE WHEN `payments`.`cash_adjustment` = 0 THEN `payments`.`payment_amount` ELSE 0 END) AS sale_payment_amount',
|
||||
'SUM(CASE WHEN `payments`.`cash_adjustment` = 1 THEN `payments`.`payment_amount` ELSE 0 END) AS sale_cash_adjustment',
|
||||
'GROUP_CONCAT(CONCAT(`payments`.`payment_type`, " ", (`payments`.`payment_amount` - `payments`.`cash_refund`)) SEPARATOR ", ") AS payment_type'
|
||||
'GROUP_CONCAT(CONCAT(`payments`.`payment_type`, " ", (`payments`.`payment_amount` - `payments`.`cash_refund`)) SEPARATOR ", ") AS payment_type',
|
||||
]);
|
||||
$builder->join('sales', 'sales.sale_id = payments.sale_id', 'inner');
|
||||
$builder->where($where);
|
||||
@@ -1429,12 +1454,10 @@ class Sale extends Model
|
||||
/**
|
||||
* Temporary table to store the sales_items_taxes data
|
||||
*
|
||||
* @param string $where
|
||||
* @return \CodeIgniter\Database\BaseBuilder
|
||||
* @return BaseBuilder
|
||||
*/
|
||||
private function create_temp_table_sales_items_taxes_data(string $where): void
|
||||
{
|
||||
|
||||
$builder = $this->db->table('sales_items_taxes AS sales_items_taxes');
|
||||
$builder->select([
|
||||
'sales_items_taxes.sale_id AS sale_id',
|
||||
@@ -1442,7 +1465,7 @@ class Sale extends Model
|
||||
'sales_items_taxes.line AS line',
|
||||
'SUM(sales_items_taxes.item_tax_amount) AS tax',
|
||||
'SUM(CASE WHEN sales_items_taxes.tax_type = 0 THEN sales_items_taxes.item_tax_amount ELSE 0 END) AS internal_tax',
|
||||
'SUM(CASE WHEN sales_items_taxes.tax_type = 1 THEN sales_items_taxes.item_tax_amount ELSE 0 END) AS sales_tax'
|
||||
'SUM(CASE WHEN sales_items_taxes.tax_type = 1 THEN sales_items_taxes.item_tax_amount ELSE 0 END) AS sales_tax',
|
||||
]);
|
||||
$builder->join('sales', 'sales.sale_id = sales_items_taxes.sale_id', 'inner');
|
||||
$builder->join('sales_items', 'sales_items.sale_id = sales_items_taxes.sale_id AND sales_items.line = sales_items_taxes.line', 'inner');
|
||||
@@ -1455,15 +1478,9 @@ class Sale extends Model
|
||||
. ' (INDEX(sale_id), INDEX(item_id)) ENGINE=MEMORY AS (' . $sub_query . ')');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $search
|
||||
* @param array $filters
|
||||
* @param BaseBuilder $builder
|
||||
* @return void
|
||||
*/
|
||||
private function add_filters_to_query(?string $search, array $filters, BaseBuilder $builder): void
|
||||
{
|
||||
if (!empty($search)) { // TODO: this is duplicated code. We should think about refactoring out a method
|
||||
if (! empty($search)) { // TODO: this is duplicated code. We should think about refactoring out a method
|
||||
if ($filters['is_valid_receipt']) {
|
||||
$pieces = explode(' ', $search);
|
||||
$builder->where('sales.sale_id', $pieces[1]);
|
||||
@@ -1481,16 +1498,15 @@ class Sale extends Model
|
||||
}
|
||||
}
|
||||
|
||||
if ($filters['location_id'] != 'all') {
|
||||
if ($filters['location_id'] !== 'all') {
|
||||
$builder->where('sales_items.item_location', $filters['location_id']);
|
||||
}
|
||||
|
||||
if ($filters['selected_customer'] != false) {
|
||||
if ($filters['selected_customer'] !== false) {
|
||||
$sale_lib = new Sale_lib();
|
||||
$builder->where('sales.customer_id', $sale_lib->get_customer());
|
||||
}
|
||||
|
||||
|
||||
if ($filters['only_invoices']) {
|
||||
$builder->where('sales.invoice_number IS NOT NULL');
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,249 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @var array $config
|
||||
* @var string $companyName
|
||||
* @var string $companyDetails
|
||||
*/
|
||||
|
||||
helper('url');
|
||||
?>
|
||||
|
||||
<!doctype html>
|
||||
<html lang="<?= esc(service('request')->getLocale()) ?>">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><?= lang('Sales.customer_display') ?></title>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="<?= base_url('images/favicon.ico') ?>">
|
||||
<link rel="stylesheet" href="<?= base_url('resources/bootswatch/' . (empty($config['theme']) ? 'flatly' : esc($config['theme'])) . '/bootstrap.min.css') ?>">
|
||||
<link rel="stylesheet" href="<?= base_url('resources/opensourcepos-8e34d6a398.min.css') ?>">
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #f8f8f8;
|
||||
color: #333;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.customer-display-header {
|
||||
background: #1f3143;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
padding: 6px 12px;
|
||||
border-bottom: 1px solid #102131;
|
||||
}
|
||||
|
||||
.customer-display-shell {
|
||||
width: 100%;
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 12px 18px 18px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.customer-display-company {
|
||||
text-align: center;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.customer-display-company img {
|
||||
display: block;
|
||||
margin: 0 auto 6px;
|
||||
max-height: 84px;
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.customer-display-company .company-name {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.customer-display-company .company-details {
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.customer-display-company .company-phone {
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.customer-display-main-row {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: flex-start;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.customer-display-cart-column {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.customer-display-summary-column {
|
||||
flex: 0 0 320px;
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel,
|
||||
.customer-display-info-panel,
|
||||
.customer-display-items-panel {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel .panel-heading,
|
||||
.customer-display-info-panel .panel-heading,
|
||||
.customer-display-items-panel .panel-heading {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel .table,
|
||||
.customer-display-info-table {
|
||||
margin-bottom: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel .table > tbody > tr > th,
|
||||
.customer-display-info-table > tbody > tr > th {
|
||||
background: #f8fbfd;
|
||||
width: 56%;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel .table > tbody > tr > td,
|
||||
.customer-display-info-table > tbody > tr > td {
|
||||
width: 44%;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel .rate-row th,
|
||||
.customer-display-summary-panel .rate-row td {
|
||||
color: #c00000;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel .summary-section-row th {
|
||||
background: #eaf2f8;
|
||||
color: #1f3b5b;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel .summary-subtable {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel .summary-subtable > tbody > tr > th {
|
||||
background: #fdfefe;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel .summary-subtable > tbody > tr > td {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.register-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#register {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
table-layout: fixed;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#register th,
|
||||
#register td {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
padding: 6px 5px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#register thead th {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#register tbody td {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
#register tbody td.item-name-cell {
|
||||
font-size: 16px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#register tbody td.price-cell {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
#register tbody td.serial-cell {
|
||||
font-size: 12px;
|
||||
color: #2F4F4F;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel .table > tbody > tr > th,
|
||||
.customer-display-info-table > tbody > tr > th {
|
||||
border-top: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel .table > tbody > tr > td,
|
||||
.customer-display-info-table > tbody > tr > td {
|
||||
border-top: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.customer-display-summary-panel .panel-body,
|
||||
.customer-display-info-panel .panel-body,
|
||||
.customer-display-items-panel .panel-body {
|
||||
padding: 12px 15px;
|
||||
}
|
||||
|
||||
.customer-display-summary-column .panel-body {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.customer-display-summary-column .customer-name-value,
|
||||
.customer-display-summary-column .giftcard-value,
|
||||
.customer-display-summary-column .reward-value {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.customer-display-footer {
|
||||
margin-top: 14px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #777;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="customer-display-header">Open Source Point of Sale</div>
|
||||
<div class="customer-display-shell">
|
||||
<div class="customer-display-company">
|
||||
<?php if (!empty($config['company_logo'])) { ?>
|
||||
<img src="<?= base_url('uploads/' . esc($config['company_logo'], 'url')) ?>" alt="company_logo">
|
||||
<?php } ?>
|
||||
<div class="company-name"><?= esc($companyName) ?></div>
|
||||
<div class="company-phone">Phone: <?= esc((string)($config['phone'] ?? '')) ?></div>
|
||||
<?php if ($companyDetails !== '') { ?>
|
||||
<div class="company-details"><?= nl2br(esc($companyDetails)) ?></div>
|
||||
<?php } ?>
|
||||
</div>
|
||||
|
||||
<div class="customer-display-main-row">
|
||||
|
||||
@@ -1,224 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @var array $cart
|
||||
* @var array $config
|
||||
* @var float $rate
|
||||
* @var float $total
|
||||
* @var float $subtotal
|
||||
* @var float $prediscount_subtotal
|
||||
* @var array $taxes
|
||||
* @var array $payments
|
||||
* @var float $amount_change
|
||||
*/
|
||||
|
||||
$priceWithCurrencyLabel = lang('Sales.price_with_currency');
|
||||
|
||||
?>
|
||||
|
||||
<?= view('partial/customer_display_header') ?>
|
||||
|
||||
<div class="customer-display-cart-column">
|
||||
<div class="register-wrap">
|
||||
<div class="panel panel-default customer-display-items-panel">
|
||||
<div class="panel-heading"><?= lang('Sales.items') ?></div>
|
||||
<div class="panel-body table-responsive">
|
||||
<table class="table table-striped table-condensed" id="register">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: <?= (int) $cartItemWidth ?>%;"><?= lang('Sales.item_name') ?></th>
|
||||
<?php if ($cartHasCustomerDisplay) { ?>
|
||||
<th style="width: <?= (int) $cartPriceWidth ?>%;"><?= sprintf($priceWithCurrencyLabel, esc($customerDisplayCurrencyLabel)) ?></th>
|
||||
<?php } ?>
|
||||
<th style="width: <?= (int) $cartOriginalWidth ?>%;"><?= sprintf($priceWithCurrencyLabel, esc($originalCurrencyLabel)) ?></th>
|
||||
<th style="width: <?= (int) $cartQuantityWidth ?>%;"><?= lang('Sales.quantity') ?></th>
|
||||
<th style="width: <?= (int) $cartDiscountWidth ?>%;"><?= lang('Sales.discount') ?></th>
|
||||
<th style="width: <?= (int) $cartTotalWidth ?>%;"><?= lang('Sales.total') ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="cart_contents">
|
||||
<?php if (count($cart) == 0) { ?>
|
||||
<tr>
|
||||
<td colspan="<?= (int) $cartColspan ?>">
|
||||
<div class="alert alert-dismissible alert-info"><?= lang('Sales.no_items_in_cart') ?></div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php } else { ?>
|
||||
<?php foreach (array_reverse($cart, true) as $line => $item) { ?>
|
||||
<tr>
|
||||
<td class="item-name-cell">
|
||||
<?= esc($item['name']) ?><br>
|
||||
<?= !empty($item['attribute_values']) ? esc($item['attribute_values']) : '' ?>
|
||||
</td>
|
||||
<?php if ($cartHasCustomerDisplay) { ?>
|
||||
<td class="price-cell">
|
||||
<?= to_secondary_currency((float)$item['price'], $secondaryCurrency) ?>
|
||||
</td>
|
||||
<?php } ?>
|
||||
<td class="price-cell">
|
||||
<?= to_currency($item['price']) ?>
|
||||
</td>
|
||||
<td class="price-cell">
|
||||
<?= to_quantity_decimals($item['quantity']) ?>
|
||||
</td>
|
||||
<td class="price-cell">
|
||||
<?= to_decimals($item['discount'], 0) ?>
|
||||
</td>
|
||||
<td class="price-cell">
|
||||
<?= $item['item_type'] == ITEM_AMOUNT_ENTRY ? to_currency_no_money($item['discounted_total']) : to_currency($item['discounted_total']) ?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="<?= $cartHasCustomerDisplay ? 3 : 2 ?>"></td>
|
||||
<td class="serial-cell">
|
||||
<?= $item['is_serialized'] == 1 ? lang('Sales.serial') : '' ?>
|
||||
</td>
|
||||
<td colspan="2" class="serial-cell">
|
||||
<?php if ($item['is_serialized'] == 1) {
|
||||
echo esc($item['serialnumber']);
|
||||
} ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php } ?>
|
||||
<?php } ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="customer-display-summary-column">
|
||||
<div class="panel panel-primary customer-display-summary-panel">
|
||||
<div class="panel-heading"><?= lang('Sales.summary') ?></div>
|
||||
<div class="panel-body">
|
||||
<table class="table table-condensed summary-subtable">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th><?= lang('Sales.total') ?></th>
|
||||
<td><?= to_currency($total) ?></td>
|
||||
</tr>
|
||||
<?php if ($showCustomerDisplay): ?>
|
||||
<tr>
|
||||
<th><?= lang('Sales.total') ?> <?= esc($customerDisplayCurrencyLabel) ?></th>
|
||||
<td><?= to_secondary_currency((float)$total, $secondaryCurrency) ?></td>
|
||||
</tr>
|
||||
<tr class="rate-row">
|
||||
<th><?= lang('Sales.rate') ?></th>
|
||||
<td><?= number_format((float) $rate, 2) ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="table table-condensed summary-subtable" style="margin-top: 10px;">
|
||||
<tbody>
|
||||
<tr class="summary-section-row">
|
||||
<th colspan="2"><?= lang('Sales.customer') ?></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><?= lang('Sales.customer_name') ?></th>
|
||||
<td class="customer-name-value"><?= esc($customerName ?? lang('Sales.walk_in_customer')) ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><?= lang('Sales.giftcard_balance') ?></th>
|
||||
<td class="giftcard-value"><?= to_currency((float) ($giftcardRemainder ?? 0)) ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><?= lang('Sales.loyalty_reward_points') ?></th>
|
||||
<td class="reward-value"><?= esc((string)($customerRewardPoints ?? 0)) ?></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="table table-condensed summary-subtable" style="margin-top: 10px;">
|
||||
<tbody>
|
||||
<tr class="summary-section-row">
|
||||
<th colspan="2"><?= lang('Sales.change') ?></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><?= lang('Sales.payments_total') ?></th>
|
||||
<td><?= to_currency($payments_total) ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><?= lang('Sales.amount_due') ?></th>
|
||||
<td><?= to_currency($amount_due) ?></td>
|
||||
</tr>
|
||||
<?php if ($showCustomerDisplay): ?>
|
||||
<tr>
|
||||
<th><?= lang('Sales.amount_due') ?> <?= esc($customerDisplayCurrencyLabel) ?></th>
|
||||
<td><?= to_secondary_currency((float)$amount_due, $secondaryCurrency) ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
<tr>
|
||||
<th><?= lang('Sales.change_due') ?></th>
|
||||
<td><?= to_currency($paymentChangeDue ?? 0) ?></td>
|
||||
</tr>
|
||||
<?php if ($showCustomerDisplay): ?>
|
||||
<tr>
|
||||
<th><?= lang('Sales.change_due') ?> <?= esc($customerDisplayCurrencyLabel) ?></th>
|
||||
<td><?= to_secondary_currency((float)($paymentChangeDue ?? 0), $secondaryCurrency) ?></td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="customer-display-footer"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const customerDisplayId = new URLSearchParams(window.location.search).get('displayId') || '';
|
||||
const customerDisplayStorageSuffix = customerDisplayId !== '' ? '_' + customerDisplayId : '';
|
||||
const customerDisplayStorageKeys = {
|
||||
open: 'customerDisplayOpen' + customerDisplayStorageSuffix,
|
||||
dirtyAt: 'customerDisplayDirtyAt' + customerDisplayStorageSuffix
|
||||
};
|
||||
|
||||
localStorage.setItem(customerDisplayStorageKeys.open, '1');
|
||||
|
||||
let lastDirtyAt = localStorage.getItem(customerDisplayStorageKeys.dirtyAt) || '';
|
||||
let refreshTimer = null;
|
||||
|
||||
const scheduleRefresh = function(dirtyAt) {
|
||||
if (refreshTimer !== null) {
|
||||
clearTimeout(refreshTimer);
|
||||
}
|
||||
|
||||
refreshTimer = setTimeout(function() {
|
||||
if (localStorage.getItem(customerDisplayStorageKeys.open) !== '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (localStorage.getItem(customerDisplayStorageKeys.dirtyAt) === dirtyAt) {
|
||||
window.location.reload();
|
||||
}
|
||||
}, 700);
|
||||
};
|
||||
|
||||
const checkForRefresh = function() {
|
||||
const dirtyAt = localStorage.getItem(customerDisplayStorageKeys.dirtyAt) || '';
|
||||
if (dirtyAt !== '' && dirtyAt !== lastDirtyAt) {
|
||||
lastDirtyAt = dirtyAt;
|
||||
scheduleRefresh(dirtyAt);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('storage', function(event) {
|
||||
if (event.key === customerDisplayStorageKeys.dirtyAt) {
|
||||
checkForRefresh();
|
||||
}
|
||||
});
|
||||
|
||||
setInterval(checkForRefresh, 500);
|
||||
|
||||
window.addEventListener('beforeunload', function() {
|
||||
localStorage.removeItem(customerDisplayStorageKeys.open);
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ if (isset($error_message)) {
|
||||
<div class="btn btn-info btn-sm" id="show_email_button"><?= '<span class="glyphicon glyphicon-envelope"> </span>' . lang('Sales.send_invoice') ?></div>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?= anchor("sales/ublInvoice/$sale_id_num", '<span class="glyphicon glyphicon-download"> </span>' . lang('Sales.download_ubl'), ['class' => 'btn btn-info btn-sm']) ?>
|
||||
<?= anchor("sales", '<span class="glyphicon glyphicon-shopping-cart"> </span>' . lang('Sales.register'), ['class' => 'btn btn-info btn-sm', 'id' => 'show_sales_button']) ?>
|
||||
<?= anchor("sales/manage", '<span class="glyphicon glyphicon-list-alt"> </span>' . lang('Sales.takings'), ['class' => 'btn btn-info btn-sm', 'id' => 'show_takings_button']) ?>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,8 @@
|
||||
"dompdf/dompdf": "^2.0.3",
|
||||
"ezyang/htmlpurifier": "^4.17",
|
||||
"laminas/laminas-escaper": "2.18.0",
|
||||
"nesbot/carbon": "^2.72",
|
||||
"num-num/ubl-invoice": "^2.4",
|
||||
"paragonie/random_compat": "^2.0.21",
|
||||
"picqer/php-barcode-generator": "^2.4.0",
|
||||
"tamtamchik/namecase": "^3.0.0"
|
||||
|
||||
1755
composer.lock
generated
1755
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Libraries\InvoiceAttachment;
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use App\Libraries\InvoiceAttachment\InvoiceAttachmentGenerator;
|
||||
use App\Libraries\InvoiceAttachment\InvoiceAttachment;
|
||||
use App\Libraries\InvoiceAttachment\PdfAttachment;
|
||||
use App\Libraries\InvoiceAttachment\UblAttachment;
|
||||
|
||||
class InvoiceAttachmentGeneratorTest extends CIUnitTestCase
|
||||
{
|
||||
public function testCreateFromConfigPdfOnly(): void
|
||||
{
|
||||
$generator = InvoiceAttachmentGenerator::createFromConfig('pdf_only');
|
||||
$this->assertInstanceOf(InvoiceAttachmentGenerator::class, $generator);
|
||||
}
|
||||
|
||||
public function testCreateFromConfigUblOnly(): void
|
||||
{
|
||||
$generator = InvoiceAttachmentGenerator::createFromConfig('ubl_only');
|
||||
$this->assertInstanceOf(InvoiceAttachmentGenerator::class, $generator);
|
||||
}
|
||||
|
||||
public function testCreateFromConfigBoth(): void
|
||||
{
|
||||
$generator = InvoiceAttachmentGenerator::createFromConfig('both');
|
||||
$this->assertInstanceOf(InvoiceAttachmentGenerator::class, $generator);
|
||||
}
|
||||
|
||||
public function testCreateFromConfigPdfOnlyRegistersPdfAttachment(): void
|
||||
{
|
||||
$generator = InvoiceAttachmentGenerator::createFromConfig('pdf_only');
|
||||
$attachments = $this->getPrivateProperty($generator, 'attachments');
|
||||
|
||||
$this->assertCount(1, $attachments);
|
||||
$this->assertInstanceOf(PdfAttachment::class, $attachments[0]);
|
||||
}
|
||||
|
||||
public function testCreateFromConfigUblOnlyRegistersUblAttachment(): void
|
||||
{
|
||||
$generator = InvoiceAttachmentGenerator::createFromConfig('ubl_only');
|
||||
$attachments = $this->getPrivateProperty($generator, 'attachments');
|
||||
|
||||
$this->assertCount(1, $attachments);
|
||||
$this->assertInstanceOf(UblAttachment::class, $attachments[0]);
|
||||
}
|
||||
|
||||
public function testCreateFromConfigBothRegistersBothAttachments(): void
|
||||
{
|
||||
$generator = InvoiceAttachmentGenerator::createFromConfig('both');
|
||||
$attachments = $this->getPrivateProperty($generator, 'attachments');
|
||||
|
||||
$this->assertCount(2, $attachments);
|
||||
$this->assertInstanceOf(PdfAttachment::class, $attachments[0]);
|
||||
$this->assertInstanceOf(UblAttachment::class, $attachments[1]);
|
||||
}
|
||||
|
||||
public function testRegisterAddsAttachment(): void
|
||||
{
|
||||
$generator = new InvoiceAttachmentGenerator();
|
||||
$mockAttachment = new class implements InvoiceAttachment {
|
||||
public function generate(array $saleData, string $type): ?string { return null; }
|
||||
public function isApplicableForType(string $type, array $saleData): bool { return true; }
|
||||
public function getFileExtension(): string { return 'test'; }
|
||||
public function getEnabledConfigValues(): array { return ['test']; }
|
||||
};
|
||||
|
||||
$result = $generator->register($mockAttachment);
|
||||
|
||||
$this->assertSame($generator, $result);
|
||||
$attachments = $this->getPrivateProperty($generator, 'attachments');
|
||||
$this->assertCount(1, $attachments);
|
||||
}
|
||||
|
||||
public function testRegisterIsChainable(): void
|
||||
{
|
||||
$generator = new InvoiceAttachmentGenerator();
|
||||
$mockAttachment = new class implements InvoiceAttachment {
|
||||
public function generate(array $saleData, string $type): ?string { return null; }
|
||||
public function isApplicableForType(string $type, array $saleData): bool { return true; }
|
||||
public function getFileExtension(): string { return 'test'; }
|
||||
public function getEnabledConfigValues(): array { return ['test']; }
|
||||
};
|
||||
|
||||
$result = $generator->register($mockAttachment)->register($mockAttachment);
|
||||
|
||||
$attachments = $this->getPrivateProperty($result, 'attachments');
|
||||
$this->assertCount(2, $attachments);
|
||||
}
|
||||
|
||||
public function testGenerateAttachmentsReturnsEmptyArrayWhenNoAttachmentsRegistered(): void
|
||||
{
|
||||
$generator = new InvoiceAttachmentGenerator();
|
||||
$result = $generator->generateAttachments([], 'invoice');
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertEmpty($result);
|
||||
}
|
||||
|
||||
public function testCleanupRemovesFiles(): void
|
||||
{
|
||||
$tempFile1 = tempnam(sys_get_temp_dir(), 'test_');
|
||||
$tempFile2 = tempnam(sys_get_temp_dir(), 'test_');
|
||||
|
||||
$this->assertFileExists($tempFile1);
|
||||
$this->assertFileExists($tempFile2);
|
||||
|
||||
InvoiceAttachmentGenerator::cleanup([$tempFile1, $tempFile2]);
|
||||
|
||||
$this->assertFileDoesNotExist($tempFile1);
|
||||
$this->assertFileDoesNotExist($tempFile2);
|
||||
}
|
||||
|
||||
public function testCleanupHandlesNonExistentFiles(): void
|
||||
{
|
||||
// Should not throw an exception
|
||||
InvoiceAttachmentGenerator::cleanup(['/non/existent/file1', '/non/existent/file2']);
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
70
tests/Libraries/InvoiceAttachment/PdfAttachmentTest.php
Normal file
70
tests/Libraries/InvoiceAttachment/PdfAttachmentTest.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Libraries\InvoiceAttachment;
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use App\Libraries\InvoiceAttachment\PdfAttachment;
|
||||
|
||||
class PdfAttachmentTest extends CIUnitTestCase
|
||||
{
|
||||
private PdfAttachment $attachment;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->attachment = new PdfAttachment();
|
||||
}
|
||||
|
||||
public function testGetFileExtensionReturnsPdf(): void
|
||||
{
|
||||
$this->assertEquals('pdf', $this->attachment->getFileExtension());
|
||||
}
|
||||
|
||||
public function testGetEnabledConfigValuesReturnsCorrectArray(): void
|
||||
{
|
||||
$values = $this->attachment->getEnabledConfigValues();
|
||||
|
||||
$this->assertIsArray($values);
|
||||
$this->assertContains('pdf_only', $values);
|
||||
$this->assertContains('both', $values);
|
||||
$this->assertCount(2, $values);
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsTrueForInvoice(): void
|
||||
{
|
||||
$this->assertTrue($this->attachment->isApplicableForType('invoice', []));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsTrueForTaxInvoice(): void
|
||||
{
|
||||
$this->assertTrue($this->attachment->isApplicableForType('tax_invoice', []));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsTrueForQuote(): void
|
||||
{
|
||||
$this->assertTrue($this->attachment->isApplicableForType('quote', []));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsTrueForWorkOrder(): void
|
||||
{
|
||||
$this->assertTrue($this->attachment->isApplicableForType('work_order', []));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsTrueForReceipt(): void
|
||||
{
|
||||
$this->assertTrue($this->attachment->isApplicableForType('receipt', []));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsTrueForAnyType(): void
|
||||
{
|
||||
// PDF should work for any document type
|
||||
$this->assertTrue($this->attachment->isApplicableForType('random_type', []));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeIgnoresSaleData(): void
|
||||
{
|
||||
// PDF attachment doesn't depend on invoice_number
|
||||
$this->assertTrue($this->attachment->isApplicableForType('invoice', ['invoice_number' => null]));
|
||||
$this->assertTrue($this->attachment->isApplicableForType('invoice', ['invoice_number' => 'INV-001']));
|
||||
}
|
||||
}
|
||||
103
tests/Libraries/InvoiceAttachment/UblAttachmentTest.php
Normal file
103
tests/Libraries/InvoiceAttachment/UblAttachmentTest.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Libraries\InvoiceAttachment;
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use App\Libraries\InvoiceAttachment\UblAttachment;
|
||||
|
||||
class UblAttachmentTest extends CIUnitTestCase
|
||||
{
|
||||
private UblAttachment $attachment;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->attachment = new UblAttachment();
|
||||
}
|
||||
|
||||
public function testGetFileExtensionReturnsXml(): void
|
||||
{
|
||||
$this->assertEquals('xml', $this->attachment->getFileExtension());
|
||||
}
|
||||
|
||||
public function testGetEnabledConfigValuesReturnsCorrectArray(): void
|
||||
{
|
||||
$values = $this->attachment->getEnabledConfigValues();
|
||||
|
||||
$this->assertIsArray($values);
|
||||
$this->assertContains('ubl_only', $values);
|
||||
$this->assertContains('both', $values);
|
||||
$this->assertCount(2, $values);
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsTrueForInvoiceWithInvoiceNumber(): void
|
||||
{
|
||||
$saleData = ['invoice_number' => 'INV-001'];
|
||||
|
||||
$this->assertTrue($this->attachment->isApplicableForType('invoice', $saleData));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsTrueForTaxInvoiceWithInvoiceNumber(): void
|
||||
{
|
||||
$saleData = ['invoice_number' => 'INV-001'];
|
||||
|
||||
$this->assertTrue($this->attachment->isApplicableForType('tax_invoice', $saleData));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsFalseForInvoiceWithoutInvoiceNumber(): void
|
||||
{
|
||||
$saleData = ['invoice_number' => null];
|
||||
|
||||
$this->assertFalse($this->attachment->isApplicableForType('invoice', $saleData));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsFalseForInvoiceWithEmptyInvoiceNumber(): void
|
||||
{
|
||||
$saleData = ['invoice_number' => ''];
|
||||
|
||||
$this->assertFalse($this->attachment->isApplicableForType('invoice', $saleData));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsFalseForInvoiceWithoutInvoiceNumberKey(): void
|
||||
{
|
||||
$saleData = [];
|
||||
|
||||
$this->assertFalse($this->attachment->isApplicableForType('invoice', $saleData));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsFalseForQuoteEvenWithInvoiceNumber(): void
|
||||
{
|
||||
$saleData = ['invoice_number' => 'INV-001'];
|
||||
|
||||
$this->assertFalse($this->attachment->isApplicableForType('quote', $saleData));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsFalseForWorkOrderEvenWithInvoiceNumber(): void
|
||||
{
|
||||
$saleData = ['invoice_number' => 'INV-001'];
|
||||
|
||||
$this->assertFalse($this->attachment->isApplicableForType('work_order', $saleData));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsFalseForReceiptEvenWithInvoiceNumber(): void
|
||||
{
|
||||
$saleData = ['invoice_number' => 'INV-001'];
|
||||
|
||||
$this->assertFalse($this->attachment->isApplicableForType('receipt', $saleData));
|
||||
}
|
||||
|
||||
public function testIsApplicableForTypeReturnsFalseForUnknownType(): void
|
||||
{
|
||||
$saleData = ['invoice_number' => 'INV-001'];
|
||||
|
||||
$this->assertFalse($this->attachment->isApplicableForType('unknown_type', $saleData));
|
||||
}
|
||||
|
||||
public function testGenerateReturnsNullForMissingConfig(): void
|
||||
{
|
||||
// Without proper sale_data, generate should fail gracefully
|
||||
$result = $this->attachment->generate([], 'invoice');
|
||||
|
||||
$this->assertNull($result);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user