Compare commits

..

4 Commits

Author SHA1 Message Date
objecttothis
302a76b84a Merge branch 'master' into feature/integration-tests 2026-03-08 13:15:43 -07:00
Ollama
2725c6e872 Fix integration tests: run playwright on failure, fix test path and syntax error
- Add 'needs: integration' and 'if: always()' so playwright job runs
  even when integration job fails, ensuring screenshots get uploaded
- Fix npm run test command (remove incorrect cd into integration-tests)
- Fix JavaScript error: navigationOption used before declaration
- Add explicit outputDir and outputFolder in playwright config
2026-03-07 18:27:08 +00:00
Ollama
7cd2d3e61f Fix CI: Remove --wait flag from docker compose up
The --wait flag causes CI to fail when init containers (like sqlscript)
exit successfully with code 0. The workflow already has a dedicated
'Wait for Application' step that polls the application endpoint.

Also remove redundant webServer config from playwright.config.ts since
the workflow handles container startup separately.
2026-03-06 21:39:50 +00:00
jekkos
3c5f4c1465 Add integration test harness with Playwright E2E tests
- Add basic Docker integration tests (run-integration-tests.sh)
  - Validates Docker stack startup
  - Checks login page accessibility and HTTP status
  - Verifies login form presence

- Add Playwright E2E test suite
  - Login tests: valid/invalid credentials, protected pages
  - Items tests: create, update, verify in inventory table
  - Customers tests: create with details, search, table verification
  - Sales tests: full sale flow with items, customers, payment, and receipt validation

- Configure Playwright with multi-browser support (Chrome, Firefox)
- Add GitHub Actions workflow for CI/CD
  - Runs on push/PR to master
  - Includes both basic and Playwright tests
  - Uploads screenshots, traces, and logs on failure

- Organize tests in integration-tests/ directory
- Update package.json with test scripts
- Include comprehensive documentation

This provides automated testing for core POS workflows including
item management, customer management, and complete sales transactions
with receipt generation verification.
2026-03-04 21:37:07 +00:00
115 changed files with 1397 additions and 1612 deletions

144
.github/workflows/integration-tests.yml vendored Normal file
View File

@@ -0,0 +1,144 @@
name: Integration Tests
on:
push:
branches: [ master, main ]
paths:
- 'app/**'
- 'public/**'
- 'docker/**'
- 'docker-compose*.yml'
- 'tests/**'
- 'integration-tests/**'
- '.github/workflows/integration-tests.yml'
pull_request:
branches: [ master, main ]
paths:
- 'app/**'
- 'public/**'
- 'docker/**'
- 'docker-compose*.yml'
- 'tests/**'
- 'integration-tests/**'
- '.github/workflows/integration-tests.yml'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read
jobs:
integration:
name: Docker Integration Tests
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run Basic Integration Tests
run: chmod +x integration-tests/run-integration-tests.sh && cd integration-tests && ./run-integration-tests.sh
- name: View Logs on Failure
if: failure()
run: |
echo "=== Application Logs ==="
docker logs opensourcepos-integration-tests-ospos-1
echo ""
echo "=== Database Logs ==="
docker logs mysql || docker logs opensourcepos-mysql || echo "No database logs found"
- name: Stop Docker Stack
if: always()
run: docker compose down -v || true
playwright:
name: Playwright E2E Tests
runs-on: ubuntu-22.04
needs: integration
if: always()
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Cache node modules
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps chromium firefox
- name: Start Docker Stack
run: docker compose up -d
- name: Wait for Application
run: |
echo "Waiting for application to be ready..."
timeout 90 bash -c 'until curl -s -f http://localhost/ > /dev/null; do sleep 2; done'
echo "Application is ready!"
- name: Run Playwright Tests
run: npm run test
env:
BASE_URL: http://localhost
- name: Upload Playwright Report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: integration-tests/playwright-report/
retention-days: 7
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-results
path: integration-tests/test-results/
retention-days: 7
- name: Upload Screenshots on Failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-screenshots
path: integration-tests/test-results/**/*.png
retention-days: 7
- name: Upload Trace Files on Failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-traces
path: integration-tests/test-results/**/*.zip
retention-days: 7
- name: View Logs on Failure
if: failure()
run: |
echo "=== Application Logs ==="
docker logs opensourcepos-integration-tests-ospos-1
echo ""
echo "=== Database Logs ==="
docker logs mysql || docker logs opensourcepos-mysql || echo "No database logs found"
- name: Stop Docker Stack
if: always()
run: docker compose down -v || true

View File

@@ -205,7 +205,6 @@ class Autoload extends AutoloadConfig
'cookie',
'tabular',
'locale',
'security',
'plugin'
'security'
];
}

View File

@@ -8,7 +8,23 @@ use CodeIgniter\HotReloader\HotReloader;
use App\Events\Db_log;
use App\Events\Load_config;
use App\Events\Method;
use App\Libraries\Plugins\PluginManager;
/*
* --------------------------------------------------------------------
* Application Events
* --------------------------------------------------------------------
* Events allow you to tap into the execution of the program without
* modifying or extending core files. This file provides a central
* location to define your events, though they can always be added
* at run-time, also, if needed.
*
* You create code that can execute by subscribing to events with
* the 'on()' method. This accepts any form of callable, including
* Closures, that will be executed when the event is triggered.
*
* Example:
* Events::on('create', [$myInstance, 'myMethod']);
*/
Events::on('pre_system', static function (): void {
if (ENVIRONMENT !== 'testing') {
@@ -23,19 +39,22 @@ Events::on('pre_system', static function (): void {
ob_start(static fn ($buffer) => $buffer);
}
/*
* --------------------------------------------------------------------
* Debug Toolbar Listeners.
* --------------------------------------------------------------------
* If you delete, they will no longer be collected.
*/
if (CI_DEBUG && ! is_cli()) {
Events::on('DBQuery', 'CodeIgniter\Debug\Toolbar\Collectors\Database::collect');
service('toolbar')->respond();
// Hot Reload route - for framework use on the hot reloader.
if (ENVIRONMENT === 'development') {
service('routes')->get('__hot-reload', static function (): void {
(new HotReloader())->run();
});
}
}
$pluginManager = new PluginManager();
$pluginManager->discoverPlugins();
$pluginManager->registerPluginEvents();
});
$config = new Load_config();
@@ -45,4 +64,4 @@ $db_log = new Db_log();
Events::on('DBQuery', [$db_log, 'db_log_queries']);
$method = new Method();
Events::on('pre_controller', [$method, 'validate_method']);
Events::on('pre_controller', [$method, 'validate_method']);

View File

@@ -876,12 +876,12 @@ class Items extends Secure_Controller
$items_to_update = $this->request->getPost('item_ids');
$item_data = [];
foreach (Item::ALLOWED_BULK_EDIT_FIELDS as $field) {
$value = $this->request->getPost($field);
if ($field === 'supplier_id' && $value !== '') {
$item_data[$field] = $value;
} elseif ($value !== null && $value !== '') {
$item_data[$field] = $value;
foreach ($_POST as $key => $value) {
// This field is nullable, so treat it differently
if ($key === 'supplier_id' && $value !== '') {
$item_data[$key] = $value;
} elseif ($value !== '' && !(in_array($key, ['item_ids', 'tax_names', 'tax_percents']))) {
$item_data[$key] = $value;
}
}

View File

@@ -1,99 +0,0 @@
<?php
namespace App\Controllers\Plugins;
use App\Controllers\Secure_Controller;
use App\Libraries\Plugins\PluginManager;
use CodeIgniter\HTTP\ResponseInterface;
class Manage extends Secure_Controller
{
private PluginManager $pluginManager;
public function __construct()
{
parent::__construct('plugins');
$this->pluginManager = new PluginManager();
$this->pluginManager->discoverPlugins();
}
public function getIndex(): string
{
$plugins = $this->pluginManager->getAllPlugins();
$enabledPlugins = $this->pluginManager->getEnabledPlugins();
$pluginData = [];
foreach ($plugins as $pluginId => $plugin) {
$pluginData[$pluginId] = [
'id' => $plugin->getPluginId(),
'name' => $plugin->getPluginName(),
'description' => $plugin->getPluginDescription(),
'version' => $plugin->getVersion(),
'enabled' => isset($enabledPlugins[$pluginId]),
'has_config' => $plugin->getConfigView() !== null,
];
}
echo view('plugins/manage', ['plugins' => $pluginData]);
return '';
}
public function postEnable(string $pluginId): ResponseInterface
{
if ($this->pluginManager->enablePlugin($pluginId)) {
return $this->response->setJSON(['success' => true, 'message' => lang('Plugins.plugin_enabled')]);
}
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.plugin_enable_failed')]);
}
public function postDisable(string $pluginId): ResponseInterface
{
if ($this->pluginManager->disablePlugin($pluginId)) {
return $this->response->setJSON(['success' => true, 'message' => lang('Plugins.plugin_disabled')]);
}
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.plugin_disable_failed')]);
}
public function postUninstall(string $pluginId): ResponseInterface
{
if ($this->pluginManager->uninstallPlugin($pluginId)) {
return $this->response->setJSON(['success' => true, 'message' => lang('Plugins.plugin_uninstalled')]);
}
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.plugin_uninstall_failed')]);
}
public function getConfig(string $pluginId): ResponseInterface
{
$plugin = $this->pluginManager->getPlugin($pluginId);
if (!$plugin) {
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.plugin_not_found')]);
}
$configView = $plugin->getConfigView();
if (!$configView) {
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.plugin_no_config')]);
}
$settings = $plugin->getSettings();
echo view($configView, ['settings' => $settings, 'plugin' => $plugin]);
return $this->response;
}
public function postSaveConfig(string $pluginId): ResponseInterface
{
$plugin = $this->pluginManager->getPlugin($pluginId);
if (!$plugin) {
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.plugin_not_found')]);
}
$settings = $this->request->getPost();
unset($settings['_method'], $settings['csrf_token_name']);
if ($plugin->saveSettings($settings)) {
return $this->response->setJSON(['success' => true, 'message' => lang('Plugins.settings_saved')]);
}
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.settings_save_failed')]);
}
}

View File

@@ -1,20 +0,0 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class PluginConfigTableCreate extends Migration
{
public function up(): void
{
log_message('info', 'Migrating plugin_config table started');
execute_script(APPPATH . 'Database/Migrations/sqlscripts/3.4.1_PluginConfigTableCreate.sql');
}
public function down(): void
{
$this->forge->dropTable('plugin_config', true);
}
}

View File

@@ -1,7 +0,0 @@
CREATE TABLE IF NOT EXISTS `ospos_plugin_config` (
`key` varchar(100) NOT NULL,
`value` text NOT NULL,
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@@ -1,24 +0,0 @@
<?php
use CodeIgniter\Events\Events;
if (!function_exists('plugin_content')) {
function plugin_content(string $section, array $data = []): string
{
$results = Events::trigger("view:{$section}", $data);
if (is_array($results)) {
return implode('', array_filter($results, fn($r) => is_string($r)));
}
return is_string($results) ? $results : '';
}
}
if (!function_exists('plugin_content_exists')) {
function plugin_content_exists(string $section): bool
{
$observers = Events::listRegistered("view:{$section}");
return !empty($observers);
}
}

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "كلمة المرور الحالية غير صحيحة.",
"employee" => "موظف",
"error_adding_updating" => "خطاء فى إضافة/تعديل موظف.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "لايمكن حذف المستخدم admin الخاص بنسخة العرض.",
"error_updating_demo_admin" => "لايمكن تغيير بيانات المستخدم admin الخاص بنسخة العرض.",
"language" => "اللغة",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "سعر التكلفة مطلوب.",
"count" => "تحديث المخزون",
"csv_import_failed" => "فشل الإستيراد من اكسل",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "الملف الذى رفعته إما فارغ أو أنه مختلف البنية.",
"csv_import_partially_failed" => "يوجد خطأ بنسبة {0} في استيراد الاصناف في السطر: {1}. لم يتم استيرادهم.",
"csv_import_success" => "تم استيراد الأصناف بنجاح.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "كلمة المرور الحالية غير صحيحة.",
"employee" => "موظف",
"error_adding_updating" => "خطاء فى إضافة/تعديل موظف.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "لايمكن حذف المستخدم admin الخاص بنسخة العرض.",
"error_updating_demo_admin" => "لايمكن تغيير بيانات المستخدم admin الخاص بنسخة العرض.",
"language" => "اللغة",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "سعر التكلفة مطلوب.",
"count" => "تحديث المخزون",
"csv_import_failed" => "فشل الإستيراد من اكسل",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "الملف الذى رفعته إما فارغ أو أنه مختلف البنية.",
"csv_import_partially_failed" => "يوجد خطأ بنسبة {0} في استيراد الاصناف في السطر: {1}. لم يتم استيرادهم.",
"csv_import_success" => "تم استيراد الأصناف بنجاح.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "Hazirki Şifrə düzgün deyil.",
"employee" => "Əməkdaş",
"error_adding_updating" => "Əməkdaş əlavə etməsk və ya yeniləməsi baş vermədi.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "Demo administrator istifadəçisini silə bilməzsiniz.",
"error_updating_demo_admin" => "Demo administrator istifadəçisini dəyişə bilməzsiniz.",
"language" => "Dil",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "Topdan satiış - doldurulması vacib sahə.",
"count" => "inventorun yenilənməsi",
"csv_import_failed" => "səhv csv import",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "Yüklənmiş faylda məlumat yoxdur və ya düzgün formatlanmır.",
"csv_import_partially_failed" => "Xətlərdə {0} element idxalı uğursuzluq (lar) var: {1}. Heç bir sıra idxal edilmədi.",
"csv_import_success" => "Malların İdxalı Uğurla Həyata Keçdi.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "Текущата парола е невалидна.",
"employee" => "Служител",
"error_adding_updating" => "Добавянето или актуализирането на служителите е неуспешно.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "Не може да изтриете Пробният Администратор.",
"error_updating_demo_admin" => "Не може да промените Пробният Администратор.",
"language" => "Език",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "Wholesale Price is a required field.",
"count" => "Update Inventory",
"csv_import_failed" => "CSV import failed",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "The uploaded file has no data or is formatted incorrectly.",
"csv_import_partially_failed" => "Item import successful with some failures:",
"csv_import_success" => "Item import successful.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "Trenutna lozinka je nevažeća.",
"employee" => "Zaposlenik",
"error_adding_updating" => "Dodavanje ili ažuriranje zaposlenika nije uspjelo.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "Ne možete izbrisati demo korisnika administratora.",
"error_updating_demo_admin" => "Ne možete promijeniti korisnika demo administratora.",
"language" => "Jezik",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "Fakturna cijena je obavezno polje.",
"count" => "Ažuriraj zalihu",
"csv_import_failed" => "Uvoz CSV-a nije uspio",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "Učitana CSV datoteka nema podatke ili je pogrešno formatirana.",
"csv_import_partially_failed" => "Bilo je {0} grešaka pri uvozu stavke na liniji: {1}. Nijedan red nije uvezen.",
"csv_import_success" => "Uvoz CSV stavke je uspješan.",

View File

@@ -14,8 +14,6 @@ return [
'current_password_invalid' => "وشەی نهێنی ئێستا نادروستە.",
'employee' => "فەرمانبەر",
'error_adding_updating' => "زیادکردن یان نوێکردنەوەی کارمەند سەرکەوتوو نەبوو.",
'error_deleting_admin' => "",
'error_updating_admin' => "",
'error_deleting_demo_admin' => "ناتوانیت بەکارهێنەری ئەدمینی تاقیکردنەوەیی بسڕیتەوە.",
'error_updating_demo_admin' => "ناتوانیت بەکارهێنەری ئەدمین تاقیکردنەوەیی بگۆڕیت.",
'language' => "زمان",

View File

@@ -26,7 +26,6 @@ return [
'cost_price_required' => "نرخی جوملە خانەیەکی پێویستە.",
'count' => "جەرد نوێ بکەوە",
'csv_import_failed' => "هاوردەکردنی CSV سەرکەوتوو نەبوو",
'csv_import_invalid_location' => "",
'csv_import_nodata_wrongformat' => "پەڕگەی CSV بارکراو هیچ داتایەکی نییە یان بە هەڵە فۆرمات کراوە.",
'csv_import_partially_failed' => "{0} شکستی هاوردەکردنی بابەتی لەسەر هێڵەکان هەبوو: {1}. هیچ ڕیزێک هاوردە نەکرا.",
'csv_import_success' => "بابەتی هاوردەکردنی CSV سەرکەوتوو بوو.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "",
"employee" => "",
"error_adding_updating" => "",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "",
"error_updating_demo_admin" => "",
"language" => "",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "Musíte zadat nákupní cenu.",
"count" => "Upravit množství",
"csv_import_failed" => "Import z CSVu se nepovedl",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "Nahraný soubor neobsahuje žádná data nebo má špatný formát.",
"csv_import_partially_failed" => "Při importu položek došlo k několika chybám:",
"csv_import_success" => "Import položek proběhl bez chyby.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "Current Password is invalid.",
"employee" => "Employee",
"error_adding_updating" => "Employee add or update failed.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "You can not delete the demo admin user.",
"error_updating_demo_admin" => "You can not change the demo admin user.",
"language" => "Language",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "",
"count" => "",
"csv_import_failed" => "",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "",
"csv_import_partially_failed" => "",
"csv_import_success" => "",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "",
"employee" => "Mitarbeiter",
"error_adding_updating" => "Fehler beim Hinzufügen/Ändern",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "Sie können den Admin nicht löschen",
"error_updating_demo_admin" => "Sie können den Admin nicht ändern",
"language" => "",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "Einstandspreis ist erforderlich",
"count" => "Ändere Bestand",
"csv_import_failed" => "CSV Import fehlerhaft",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "Your uploaded file has no data or wrong format",
"csv_import_partially_failed" => "Most Items imported. But some were not, here is the list",
"csv_import_success" => "Import of Items successful",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "Aktuelles Passwort ist ungültig.",
"employee" => "Mitarbeiter",
"error_adding_updating" => "Fehler beim Hinzufügen/Ändern.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "Sie können den Demo-Administrator nicht löschen.",
"error_updating_demo_admin" => "Sie können den Demo-Administrator nicht verändern.",
"language" => "Sprache",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "Der Großhandelspreis ist ein Pflichtfeld.",
"count" => "Ändere Bestand",
"csv_import_failed" => "CSV Import fehlgeschlagen",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "Die hochgeladene Datei enthält keine Daten oder ist falsch formatiert.",
"csv_import_partially_failed" => "{0} Artikel-Import Fehler in Zeile: {1}. Keine Reihen wurden importiert.",
"csv_import_success" => "Artikelimport erfolgreich.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "",
"employee" => "",
"error_adding_updating" => "",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "",
"error_updating_demo_admin" => "",
"language" => "",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "",
"count" => "",
"csv_import_failed" => "",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "",
"csv_import_partially_failed" => "",
"csv_import_success" => "",

View File

@@ -1,27 +0,0 @@
<?php
return [
// Plugin Management
"plugins" => "Plugins",
"plugin_management" => "Plugin Management",
"plugin_name" => "Plugin Name",
"plugin_description" => "Description",
"plugin_version" => "Version",
"plugin_status" => "Status",
"plugin_enabled" => "Plugin enabled successfully",
"plugin_enable_failed" => "Failed to enable plugin",
"plugin_disabled" => "Plugin disabled successfully",
"plugin_disable_failed" => "Failed to disable plugin",
"plugin_uninstalled" => "Plugin uninstalled successfully",
"plugin_uninstall_failed" => "Failed to uninstall plugin",
"plugin_not_found" => "Plugin not found",
"plugin_no_config" => "This plugin has no configuration options",
"settings_saved" => "Plugin settings saved successfully",
"settings_save_failed" => "Failed to save plugin settings",
"enable" => "Enable",
"disable" => "Disable",
"configure" => "Configure",
"uninstall" => "Uninstall",
"no_plugins_found" => "No plugins found",
"active" => "Active",
"inactive" => "Inactive",
];

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "Contraseña Actual Inválida.",
"employee" => "Empleado",
"error_adding_updating" => "Error al agregar/actualizar empleado.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "No puedes borrar el usuario admin del demo.",
"error_updating_demo_admin" => "No puedes cambiar el usuario admin del demo.",
"language" => "Idioma",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "Precio al Por Mayor es un campo requerido.",
"count" => "Actualizar Inventario",
"csv_import_failed" => "Falló la importación de Hoja de Cálculo",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "El archivo subido no tiene datos o el formato es incorrecto.",
"csv_import_partially_failed" => "Hubo {0} falla(s) en la importación de producto(s) en la(s) línea(s): {1}. Ninguna fila ha sido importada.",
"csv_import_success" => "Se importaron los articulos exitosamente.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "La contraseña actual es inválida.",
"employee" => "Empleado",
"error_adding_updating" => "Agregar ó Actualizar empleado ha fallado.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "No puede borrar el usuario demo de administrador.",
"error_updating_demo_admin" => "No puede cambiar el usuario demo de administrador.",
"language" => "Idioma",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "El precio de mayoreo es requerido.",
"count" => "Actualizar inventario",
"csv_import_failed" => "",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "",
"csv_import_partially_failed" => "",
"csv_import_success" => "",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "گذرواژه فعلی نامعتبر است.",
"employee" => "کارمند",
"error_adding_updating" => "افزودن یا به روزرسانی کارکنان انجام نشد.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "شما نمی توانید کاربر مدیر نسخه ی نمایشی را حذف کنید.",
"error_updating_demo_admin" => "شما نمی توانید کاربر مدیر نسخه ی نمایشی را تغییر دهید.",
"language" => "زبان",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "قیمت عمده فروشی یک زمینه ضروری است.",
"count" => "به روزرسانی موجودی",
"csv_import_failed" => "واردات سی‌اس‌وی انجام نشد",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "پرونده سی‌اس‌وی آپلود شده داده ای ندارد یا به طور نادرست قالب بندی شده است.",
"csv_import_partially_failed" => "در خط (ها){0} شکست واردات کالا وجود دارد:{1}. هیچ سطر وارد نشده است.",
"csv_import_success" => "وارد کردن سی‌اس‌وی مورد موفقیت آمیز است.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "Le mot de passe actuel est invalide.",
"employee" => "Employé",
"error_adding_updating" => "Erreur d'ajout/édition d'employé.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "Vous ne pouvez pas supprimer l'utilisateur de démonstration admin.",
"error_updating_demo_admin" => "Vous ne pouvez pas modifier l'utilisateur de démonstration admin.",
"language" => "Langue",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "Le prix de gros est requis.",
"count" => "Mise à jour de l'inventaire",
"csv_import_failed" => "Échec d'import CSV",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "Le CSV envoyé ne contient aucune donnée, ou elles sont dans un format erroné.",
"csv_import_partially_failed" => "Il y a eu {0} importation(s) d'articles échoué(s) au(x) ligne(s) : {1}. Aucune ligne n'a été importée.",
"csv_import_success" => "Importation des articles réussie.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "הסיסמה הנוכחית אינה חוקית.",
"employee" => "עובד",
"error_adding_updating" => "הוספה או עדכון של עובד נכשלה.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "לא ניתן למחוק את משתמש המנהל ההדגמה.",
"error_updating_demo_admin" => "לא ניתן לשנות את משתמש המנהל ההדגמה.",
"language" => "שפה",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "מחיר סיטונאי הינו שדה חובה.",
"count" => "עדכן מלאי",
"csv_import_failed" => "ייבוא אקסל נכשל",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "בקובץ שהועלה אין נתונים או פורמט שגוי.",
"csv_import_partially_failed" => "ייבוא פריט הצליח עם מספר שגיאות:",
"csv_import_success" => "ייבוא הפריט הצליח.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "",
"employee" => "Radnik",
"error_adding_updating" => "Greška kod dodavanja/ažuriranja radnika",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "Ne možete obrisati demo admin korisnika",
"error_updating_demo_admin" => "Ne možete promijeniti demo admin korisnika",
"language" => "",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "Nabavna cijena je potrebna",
"count" => "Ažuriraj inveturu",
"csv_import_failed" => "Greška kod uvoza iz CSV-a",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "Your uploaded file has no data or wrong format",
"csv_import_partially_failed" => "Most Items imported. But some were not, here is the list",
"csv_import_success" => "Import of Items successful",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "",
"employee" => "Munkavállaló",
"error_adding_updating" => "Hiba a munkavállaló módosításánál/hozzáadásánál",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "Nem tudja törölni a demo admin felhasználót",
"error_updating_demo_admin" => "Nem tudja módosítani a demo admin felhasználót",
"language" => "",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "Bekerülési ár kötelező mező",
"count" => "Raktárkészlet módosítása",
"csv_import_failed" => "CSV import sikertelen",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "A feltöltött fájlban nincs adat, vagy rossz formátum.",
"csv_import_partially_failed" => "Most Items imported. But some were not, here is the list",
"csv_import_success" => "Import of Items successful",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "",
"employee" => "",
"error_adding_updating" => "",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "",
"error_updating_demo_admin" => "",
"language" => "",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "",
"count" => "",
"csv_import_failed" => "",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "",
"csv_import_partially_failed" => "",
"csv_import_success" => "",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "Kata kunci sekarang salah.",
"employee" => "Karyawan",
"error_adding_updating" => "Kesalahan menambah / memperbarui karyawan.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "Anda tidak dapat menghapus Demo admin user.",
"error_updating_demo_admin" => "Anda tidak dapat mengubah Demo admin user.",
"language" => "Bahasa",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "Harga beli harus diisi.",
"count" => "Mutasi Inventori",
"csv_import_failed" => "Gagal impor CSV",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "Berkas CSV terunggah tidak berisi data atau formatnya salah.",
"csv_import_partially_failed" => "Terdapat {0} item gagal impor pada baris: {1}. Tidak ada baris yang diimpor.",
"csv_import_success" => "Impor item CSV berhasil.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "Password corrente non valida.",
"employee" => "Impiegato",
"error_adding_updating" => "Aggiunta o aggiornamento di impiegati fallito.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "Non puoi eliminare l'utente admin demo.",
"error_updating_demo_admin" => "Non puoi cambiare l'utente admin demo.",
"language" => "Lingua",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "Prezzo all'ingrosso è un campo obbligatorio.",
"count" => "Aggiorna Inventario",
"csv_import_failed" => "Importazione CSV fallita",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "L'upload del file non ha dati o non è formattato correttamente.",
"csv_import_partially_failed" => "Si sono verificati {0} errori di importazione degli elementi nelle righe: {1}. Nessuna riga è stata importata.",
"csv_import_success" => "Importazione CSV dell'articolo riuscita.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "ពាក្យសម្ងាត់បច្ចុប្បន្ន មិនត្រឹមត្រូវ។",
"employee" => "បុគ្គលិក",
"error_adding_updating" => "បន្ថែម ឬកែប្រែបុគ្គលិកមិនបានសំរេច។",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "អ្នកមិនអាចលុប គណនីសាកល្បង បានទេ។",
"error_updating_demo_admin" => "អ្នកមិនអាចកែប្រែ គណនីសាកល្បងបានទេ។",
"language" => "ភាសា",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "ត្រូវការតម្លៃលក់ដុំជាចាំបាច់។",
"count" => "កែប្រែ ទំនិញក្នុងស្តុក",
"csv_import_failed" => "CSV បញ្ចូលមិនបានសំរេច",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "ដាក់បញ្ជុល CSV មិនមានទិន្នន័យ ឬទំរង់មិនត្រឹមត្រូវ។",
"csv_import_partially_failed" => "មានទំននិញ {0} បញ្ជូលមិនបានសំរេច នៅជួរ: {1} ។ គ្មានជួរណាមួយត្រូវបានបញ្ជូលនោះទេ។",
"csv_import_success" => "ទំនិញក្នុង CSV បញ្ចូលបានសំរេច។",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "Password ປັດຈຸບັນບໍ່ຖືກຕ້ອງ.",
"employee" => "ພະນັກງານ",
"error_adding_updating" => "ເພີ່ມ ຫຼື ແກ້ໄຂ ພະນັກງານ ບໍ່ສຳເລັດ.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "ທ່ານບໍ່ສາມາດລຶບບັນຊີທົດລອງຜູ້ດູແລລະບົບໄດ້.",
"error_updating_demo_admin" => "ທ່ານບໍ່ສາມາດປ່ຽນແປງບັນຊີທົດລອງຜູ້ດູແລລະບົບໄດ້.",
"language" => "ພາສາ",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "ກະລຸນາກຳນົດລາຄາຕົ້ນທຶນ.",
"count" => "ອັບເດດປະລິມານສິນຄ້າໃນສາງ",
"csv_import_failed" => "CSV import failed",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "The uploaded file has no data or is formatted incorrectly.",
"csv_import_partially_failed" => "Item import successful with some failures:",
"csv_import_success" => "Item import successful.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "",
"employee" => "",
"error_adding_updating" => "",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "",
"error_updating_demo_admin" => "",
"language" => "",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "",
"count" => "",
"csv_import_failed" => "",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "",
"csv_import_partially_failed" => "",
"csv_import_success" => "",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "",
"employee" => "",
"error_adding_updating" => "",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "",
"error_updating_demo_admin" => "",
"language" => "",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "",
"count" => "",
"csv_import_failed" => "",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "",
"csv_import_partially_failed" => "",
"csv_import_success" => "",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "Huidig paswoord is ongeldig.",
"employee" => "Werknemer",
"error_adding_updating" => "Fout bij het toevoegen/aanpassen medewerker.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "Je kan de demo gebruilker niet verwijderen.",
"error_updating_demo_admin" => "Jij kan de demo gebruiker niet veranderen.",
"language" => "Taal",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "Groothandelsprijs is een verplicht veld.",
"count" => "Update Stock",
"csv_import_failed" => "CSV import mislukt",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "Het geüploade CSV-bestand bevat geen gegevens of is onjuist geformatteerd.",
"csv_import_partially_failed" => "Er waren {0} artikel import fout(en) op regel(s): {1}. Er werden geen rijen geïmporteerd.",
"csv_import_success" => "Artikel CSV import geslaagd.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "Huidige wachtwoord is ongeldig.",
"employee" => "Werknemer",
"error_adding_updating" => "Werknemer toevoegen of bijwerken mislukt.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "Kan de demo admin gebruiker niet verwijderen.",
"error_updating_demo_admin" => "Kan de demo admin gebruiker niet wijzigen.",
"language" => "Taal",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "Inkoopprijs is een vereist veld.",
"count" => "Voorraad bijwerken",
"csv_import_failed" => "CSV importeren mislukt",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "Het geüploade CSV-bestand bevat geen gegevens or heeft de verkeerde indeling.",
"csv_import_partially_failed" => "Er zijn {0} artikel import fout(en) in lijn(en): {1}. Geen rijen geïmporteerd.",
"csv_import_success" => "Artikel CSV geïmporteerd.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "",
"employee" => "",
"error_adding_updating" => "",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "",
"error_updating_demo_admin" => "",
"language" => "",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "",
"count" => "",
"csv_import_failed" => "",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "",
"csv_import_partially_failed" => "",
"csv_import_success" => "",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "Senha atual inválida.",
"employee" => "Funcionário",
"error_adding_updating" => "Erro ao adicionar/atualizar funcionário.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "Você não pode excluir o usuário administrador de demonstração.",
"error_updating_demo_admin" => "Você não pode alterar o usuário de demonstração de administração.",
"language" => "Linguagem",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "Preço de custo é um campo obrigatório.",
"count" => "Acrescentar ao Inventário",
"csv_import_failed" => "Importação do CSV falhou",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "Seu arquivo enviado não contém dados ou formato errado.",
"csv_import_partially_failed" => "Houve {0} falha na importação de itens na(s) linha(s): {1}. Nenhuma linha foi importada.",
"csv_import_success" => "Importação de Itens com sucesso.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "",
"employee" => "",
"error_adding_updating" => "",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "",
"error_updating_demo_admin" => "",
"language" => "",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "",
"count" => "",
"csv_import_failed" => "",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "",
"csv_import_partially_failed" => "",
"csv_import_success" => "",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "Текущий пароль введен неверно.",
"employee" => "Сотрудник",
"error_adding_updating" => "Ошибка при добавлении/обновлении сотрудника.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "Вы не можете удалить демо-администратора.",
"error_updating_demo_admin" => "Вы не можете изменить демо-администратора.",
"language" => "Язык",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "Оптовая цена - обязательное поле.",
"count" => "Обновление запасов",
"csv_import_failed" => "Ошибка импорта CSV",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "Загруженный файл CSV не содержит данных или имеет неправильный формат.",
"csv_import_partially_failed" => "В строке (строках) произошло {0} ошибок импорта: {1}. Ничего не было импортировано.",
"csv_import_success" => "Товар успешно импортирован из CSV.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "Nuvarande lösenord är fel.",
"employee" => "Anställd",
"error_adding_updating" => "Anställd lägg till eller uppdatering misslyckades.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "Du kan inte radera demo admin-användaren.",
"error_updating_demo_admin" => "Du kan inte ändra demo admin-användaren.",
"language" => "Språk",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "Grossistpris är ett obligatoriskt fält.",
"count" => "Uppdatera Inventory",
"csv_import_failed" => "CSV-import misslyckades",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "Den uppladdade filen har ingen data eller är formaterad felaktigt.",
"csv_import_partially_failed" => "Det fanns{0} importfel (er) på rad (er):{1}. Inga rader importerades.",
"csv_import_success" => "Artikelimporten lyckades.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "Nenosiri la sasa si sahihi.",
"employee" => "Mfanyakazi",
"error_adding_updating" => "Kuongeza au kusasisha mfanyakazi kumeshindikana.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "Huwezi kufuta mtumiaji wa admin wa majaribio.",
"error_updating_demo_admin" => "Huwezi kubadilisha mtumiaji wa admin wa majaribio.",
"language" => "Lugha",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "Bei ya Jumla ni kiashiria kinachohitajika.",
"count" => "Sasisha Hisa",
"csv_import_failed" => "Uingizaji wa CSV umeshindikana",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "Faili ya CSV iliyopakiwa haina data au imepangwa vibaya.",
"csv_import_partially_failed" => "Kumekuwa na makosa {0} ya uingizaji wa bidhaa kwenye mstari: {1}. Hakuna safu zilizoingizwa.",
"csv_import_success" => "Uingizaji wa Bidhaa kutoka CSV umefanikiwa.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "Nenosiri la sasa si sahihi.",
"employee" => "Mfanyakazi",
"error_adding_updating" => "Kuongeza au kusasisha mfanyakazi kumeshindikana.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "Huwezi kufuta mtumiaji wa admin wa majaribio.",
"error_updating_demo_admin" => "Huwezi kubadilisha mtumiaji wa admin wa majaribio.",
"language" => "Lugha",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "Bei ya Jumla ni kiashiria kinachohitajika.",
"count" => "Sasisha Hisa",
"csv_import_failed" => "Uingizaji wa CSV umeshindikana",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "Faili ya CSV iliyopakiwa haina data au imepangwa vibaya.",
"csv_import_partially_failed" => "Kumekuwa na makosa {0} ya uingizaji wa bidhaa kwenye mstari: {1}. Hakuna safu zilizoingizwa.",
"csv_import_success" => "Uingizaji wa Bidhaa kutoka CSV umefanikiwa.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "Current Password is invalid.",
"employee" => "Employee",
"error_adding_updating" => "Employee add or update failed.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "You can not delete the demo admin user.",
"error_updating_demo_admin" => "You can not change the demo admin user.",
"language" => "Language",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "Wholesale Price is a required field.",
"count" => "Update Inventory",
"csv_import_failed" => "CSV import failed",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "The uploaded CSV file has no data or is formatted incorrectly.",
"csv_import_partially_failed" => "There were {0} item import failure(s) on line(s): {1}. No rows were imported.",
"csv_import_success" => "Item CSV import successful.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "รหัสผ่านปัจจุบันไม่ถูกต้อง",
"employee" => "พนักงาน",
"error_adding_updating" => "การเพิ่มหรือปรับปรุงข้อมูลพนักงานผิดพลาด",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "คุณไม่สามารถลบผู้ใช้งานสำหรับการเดโม้ได้",
"error_updating_demo_admin" => "คุณไม่สามารถทำการเปลี่ยนข้อมูลผู้ใช้งานเดโม้ได้",
"language" => "ภาษา",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "ต้องกรอกราคาขายส่ง",
"count" => "แก้ไขจำนวนสินค้าคงคลัง",
"csv_import_failed" => "นำเข้าข้อมูล CSV ล้มเหลว",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "Your uploaded file has no data or wrong format",
"csv_import_partially_failed" => "มีรายการ {0} รายการที่นำเข้าล้มเหลว : {1} รายการที่ยังไม่ได้นำเข้า",
"csv_import_success" => "Import of Items successful",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "Current Password is invalid.",
"employee" => "Employee",
"error_adding_updating" => "Employee add or update failed.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "You can not change the demo admin user.",
"error_updating_demo_admin" => "You can not delete the demo admin user.",
"language" => "Language",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "Purchase Price is a required field.",
"count" => "Update Inventory",
"csv_import_failed" => "CSV import failed",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "The uploaded file has no data or is formatted incorrectly.",
"csv_import_partially_failed" => "Customer import successful with some failures:",
"csv_import_success" => "Item import successful.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "Var Olan Parola geçersiz.",
"employee" => "Personel",
"error_adding_updating" => "Personel ekleme/güncelleme hatası.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "Admin güncellenemez.",
"error_updating_demo_admin" => "Admin silinemez.",
"language" => "Dil",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "Toptan Fiyatı zorunlu alandır.",
"count" => "Stoğu Güncelle",
"csv_import_failed" => "CSV aktarım hatası",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "Yüklenen dosya herhangi bir veri içermiyor veya hatalı formatta.",
"csv_import_partially_failed" => "Bazı ürünler aktarılamadı. Aktarılamayanlar listesi.",
"csv_import_success" => "Ürün aktarımı başarılı.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "Поточний пароль невірний.",
"employee" => "Працівник",
"error_adding_updating" => "Помилка при додаванні/оновлені працівника.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "Ви не можете видалити аккаунт користувача.",
"error_updating_demo_admin" => "Ви не можете змінити аккаунт користувача.",
"language" => "Мова",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "Оптова ціна - обов'язкове поле.",
"count" => "Оновити інвентар",
"csv_import_failed" => "Помилка імпорту CSV",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "Завандажений файл порожній або відформатований неправильно.",
"csv_import_partially_failed" => "У рядках виявлено {0} помилки імпортування елементів: {1}. Не було імпортовано жодних рядків.",
"csv_import_success" => "Імпорт товару CSV успішний.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "",
"employee" => "",
"error_adding_updating" => "",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "",
"error_updating_demo_admin" => "",
"language" => "",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "",
"count" => "",
"csv_import_failed" => "",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "",
"csv_import_partially_failed" => "",
"csv_import_success" => "",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "Mật khẩu hiện tại không hợp lệ.",
"employee" => "Nhân viên",
"error_adding_updating" => "Gặp lỗi khi cập nhật hay thêm nhân viên.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "Bạn không thể xóa người dùng demo admin.",
"error_updating_demo_admin" => "Bạn không thể thay đổi người dùng demo admin.",
"language" => "Ngôn ngữ",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "Trường Giá bán buôn là bắt buộc.",
"count" => "Cập hàng tồn kho",
"csv_import_failed" => "Gặp lỗi khi nhập từ CSV",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "Tập tin tải lên không có dữ liệu hoặc là nó có định dạng không đúng.",
"csv_import_partially_failed" => "{0} mục tin gặp lỗi khi nhập trên dòng: {1}. Không có hàng nào được nhập vào.",
"csv_import_success" => "Nhập hàng hóa thành công.",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "",
"employee" => "員工",
"error_adding_updating" => "添加/更新員工錯誤",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "您不能刪除admin用戶",
"error_updating_demo_admin" => "您不能更改admin用戶",
"language" => "",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "成本價為必填攔位",
"count" => "更新庫存",
"csv_import_failed" => "CSV匯入失敗",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "Your uploaded file has no data or wrong format",
"csv_import_partially_failed" => "Most Items imported. But some were not, here is the list",
"csv_import_success" => "Import of Items successful",

View File

@@ -14,8 +14,6 @@ return [
"current_password_invalid" => "當前密碼無效。",
"employee" => "員工",
"error_adding_updating" => "添加/更新員工錯誤.",
"error_deleting_admin" => "",
"error_updating_admin" => "",
"error_deleting_demo_admin" => "您不能刪除admin用戶.",
"error_updating_demo_admin" => "您不能更改admin用戶.",
"language" => "語言",

View File

@@ -26,7 +26,6 @@ return [
"cost_price_required" => "成本價為必填攔位.",
"count" => "更新庫存",
"csv_import_failed" => "CSV匯入失敗",
"csv_import_invalid_location" => "",
"csv_import_nodata_wrongformat" => "上傳的 CSV 文件沒有數據或格式不正確。",
"csv_import_partially_failed" => "線上有 {0} 個項目導入失敗:{1}。未導入任何行。",
"csv_import_success" => "項目 CSV 導入成功。",

View File

@@ -1,70 +0,0 @@
<?php
namespace App\Libraries\Plugins;
use App\Models\PluginConfig;
abstract class BasePlugin implements PluginInterface
{
protected PluginConfig $configModel;
public function __construct()
{
$this->configModel = new PluginConfig();
}
public function install(): bool
{
return true;
}
public function uninstall(): bool
{
return true;
}
public function isEnabled(): bool
{
$enabled = $this->configModel->getValue("{$this->getPluginId()}_enabled");
return $enabled === '1' || $enabled === 'true';
}
protected function getSetting(string $key, mixed $default = null): mixed
{
$value = $this->configModel->getValue("{$this->getPluginId()}_{$key}");
return $value ?? $default;
}
protected function setSetting(string $key, mixed $value): bool
{
$stringValue = is_array($value) || is_object($value)
? json_encode($value)
: (string)$value;
return $this->configModel->setValue("{$this->getPluginId()}_{$key}", $stringValue);
}
public function getSettings(): array
{
return $this->configModel->getPluginSettings($this->getPluginId());
}
public function saveSettings(array $settings): bool
{
$prefixedSettings = [];
foreach ($settings as $key => $value) {
if (is_array($value) || is_object($value)) {
$prefixedSettings["{$this->getPluginId()}_{$key}"] = json_encode($value);
} else {
$prefixedSettings["{$this->getPluginId()}_{$key}"] = (string)$value;
}
}
return $this->configModel->batchSave($prefixedSettings);
}
protected function log(string $level, string $message): void
{
log_message($level, "[Plugin:{$this->getPluginName()}] {$message}");
}
}

View File

@@ -1,56 +0,0 @@
<?php
namespace App\Libraries\Plugins;
interface PluginInterface
{
public function getPluginId(): string;
public function getPluginName(): string;
public function getPluginDescription(): string;
public function getVersion(): string;
/**
* Register event listeners for this plugin.
*
* Use Events::on() to register callbacks for OSPOS events.
* This method is called when the plugin is loaded and enabled.
*
* Example:
* Events::on('item_sale', [$this, 'onItemSale']);
* Events::on('item_change', [$this, 'onItemChange']);
*/
public function registerEvents(): void;
/**
* Install the plugin.
*
* Called when the plugin is first enabled. Use this to create database tables,
* set default configuration values, and run any setup required.
*/
public function install(): bool;
/**
* Uninstall the plugin.
*
* Called when the plugin is being removed. Use this to remove database tables,
* clean up configuration, etc.
*/
public function uninstall(): bool;
public function isEnabled(): bool;
/**
* Get the path to the plugin's configuration view file.
* Returns null if the plugin has no configuration UI.
*
* Example: 'Plugins/mailchimp/config'
*/
public function getConfigView(): ?string;
public function getSettings(): array;
public function saveSettings(array $settings): bool;
}

View File

@@ -1,174 +0,0 @@
<?php
namespace App\Libraries\Plugins;
use App\Models\PluginConfig;
use CodeIgniter\Events\Events;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
class PluginManager
{
private array $plugins = [];
private array $enabledPlugins = [];
private PluginConfig $configModel;
private string $pluginsPath;
public function __construct()
{
$this->configModel = new PluginConfig();
$this->pluginsPath = APPPATH . 'Plugins';
}
public function discoverPlugins(): void
{
if (!is_dir($this->pluginsPath)) {
log_message('debug', 'Plugins directory does not exist: ' . $this->pluginsPath);
return;
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($this->pluginsPath, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $file) {
if ($file->isDir() || $file->getExtension() !== 'php') {
continue;
}
$className = $this->getClassNameFromFile($file->getPathname());
if (!$className) {
continue;
}
if (!class_exists($className)) {
continue;
}
if (!is_subclass_of($className, PluginInterface::class)) {
continue;
}
$plugin = new $className();
$this->plugins[$plugin->getPluginId()] = $plugin;
log_message('debug', "Discovered plugin: {$plugin->getPluginName()}");
}
}
private function getClassNameFromFile(string $pathname): ?string
{
$relativePath = str_replace($this->pluginsPath . DIRECTORY_SEPARATOR, '', $pathname);
$relativePath = str_replace(DIRECTORY_SEPARATOR, '\\', $relativePath);
$className = 'App\\Plugins\\' . str_replace('.php', '', $relativePath);
return $className;
}
public function registerPluginEvents(): void
{
foreach ($this->plugins as $pluginId => $plugin) {
if ($this->isPluginEnabled($pluginId)) {
$this->enabledPlugins[$pluginId] = $plugin;
$plugin->registerEvents();
log_message('debug', "Registered events for plugin: {$plugin->getPluginName()}");
}
}
}
public function getAllPlugins(): array
{
return $this->plugins;
}
public function getEnabledPlugins(): array
{
return $this->enabledPlugins;
}
public function getPlugin(string $pluginId): ?PluginInterface
{
return $this->plugins[$pluginId] ?? null;
}
public function isPluginEnabled(string $pluginId): bool
{
$enabled = $this->configModel->getValue($this->getEnabledKey($pluginId));
return $enabled === '1' || $enabled === 'true';
}
public function enablePlugin(string $pluginId): bool
{
$plugin = $this->getPlugin($pluginId);
if (!$plugin) {
log_message('error', "Plugin not found: {$pluginId}");
return false;
}
if (!$this->configModel->exists($this->getInstalledKey($pluginId))) {
if (!$plugin->install()) {
log_message('error', "Failed to install plugin: {$pluginId}");
return false;
}
$this->configModel->setValue($this->getInstalledKey($pluginId), '1');
}
$this->configModel->setValue($this->getEnabledKey($pluginId), '1');
log_message('info', "Plugin enabled: {$pluginId}");
return true;
}
public function disablePlugin(string $pluginId): bool
{
if (!$this->getPlugin($pluginId)) {
log_message('error', "Plugin not found: {$pluginId}");
return false;
}
$this->configModel->setValue($this->getEnabledKey($pluginId), '0');
log_message('info', "Plugin disabled: {$pluginId}");
return true;
}
public function uninstallPlugin(string $pluginId): bool
{
$plugin = $this->getPlugin($pluginId);
if (!$plugin) {
log_message('error', "Plugin not found: {$pluginId}");
return false;
}
if (!$plugin->uninstall()) {
log_message('error', "Failed to uninstall plugin: {$pluginId}");
return false;
}
$this->configModel->deleteAllStartingWith($pluginId . '_');
return true;
}
public function getSetting(string $pluginId, string $key, mixed $default = null): mixed
{
return $this->configModel->getValue("{$pluginId}_{$key}") ?? $default;
}
public function setSetting(string $pluginId, string $key, mixed $value): bool
{
return $this->configModel->setValue("{$pluginId}_{$key}", $value);
}
private function getEnabledKey(string $pluginId): string
{
return "{$pluginId}_enabled";
}
private function getInstalledKey(string $pluginId): string
{
return "{$pluginId}_installed";
}
}

View File

@@ -16,18 +16,6 @@ use stdClass;
*/
class Item extends Model
{
public const ALLOWED_BULK_EDIT_FIELDS = [
'name',
'category',
'supplier_id',
'cost_price',
'unit_price',
'reorder_level',
'description',
'allow_alt_description',
'is_serialized'
];
protected $table = 'items';
protected $primaryKey = 'item_id';
protected $useAutoIncrement = true;
@@ -211,9 +199,9 @@ class Item extends Model
if (!empty($search)) {
if ($attributes_enabled && $filters['search_custom']) {
$builder->havingLike('attribute_values', $search);
$builder->orHavingLike('attribute_dtvalues', $search);
$builder->orHavingLike('attribute_dvalues', $search);
$builder->having("attribute_values LIKE :search:", ['search' => "%$search%"]);
$builder->orHaving("attribute_dtvalues LIKE :search_dt:", ['search_dt' => "%$search%"]);
$builder->orHaving("attribute_dvalues LIKE :search_d:", ['search_d' => "%$search%"]);
} else {
$builder->groupStart();
$builder->like('name', $search);

View File

@@ -1,107 +0,0 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class PluginConfig extends Model
{
protected $table = 'plugin_config';
protected $primaryKey = 'key';
protected $useAutoIncrement = false;
protected $useSoftDeletes = false;
protected $allowedFields = [
'key',
'value'
];
public function exists(string $key): bool
{
$builder = $this->db->table('plugin_config');
$builder->where('key', $key);
return ($builder->get()->getNumRows() === 1);
}
public function getValue(string $key): ?string
{
$builder = $this->db->table('plugin_config');
$query = $builder->getWhere(['key' => $key], 1);
if ($query->getNumRows() === 1) {
return $query->getRow()->value;
}
return null;
}
public function setValue(string $key, string $value): bool
{
$builder = $this->db->table('plugin_config');
if ($this->exists($key)) {
return $builder->update(['value' => $value], ['key' => $key]);
}
return $builder->insert(['key' => $key, 'value' => $value]);
}
public function getPluginSettings(string $pluginId): array
{
$builder = $this->db->table('plugin_config');
$builder->like('key', $pluginId . '_', 'after');
$query = $builder->get();
$settings = [];
$prefix = $pluginId . '_';
foreach ($query->getResult() as $row) {
$key = str_starts_with($row->key, $prefix)
? substr($row->key, strlen($prefix))
: $row->key;
$settings[$key] = $row->value;
}
return $settings;
}
public function deleteKey(string $key): bool
{
$builder = $this->db->table('plugin_config');
return $builder->delete(['key' => $key]);
}
public function deleteAllStartingWith(string $prefix): bool
{
$builder = $this->db->table('plugin_config');
$builder->like('key', $prefix, 'after');
return $builder->delete();
}
public function batchSave(array $data): bool
{
$success = true;
$this->db->transStart();
foreach ($data as $key => $value) {
$success &= $this->setValue($key, $value);
}
$this->db->transComplete();
return $success && $this->db->transStatus();
}
public function getAll(): array
{
$builder = $this->db->table('plugin_config');
$query = $builder->get();
$configs = [];
foreach ($query->getResult() as $row) {
$configs[$row->key] = $row->value;
}
return $configs;
}
}

Some files were not shown because too many files have changed in this diff Show More